diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b9e32c..145f92d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies with pip run: | cd kuksa-client diff --git a/kuksa-client/Dockerfile b/kuksa-client/Dockerfile index 5bc1a0b..c36ab57 100644 --- a/kuksa-client/Dockerfile +++ b/kuksa-client/Dockerfile @@ -9,7 +9,7 @@ # Note: This dockerfile needs to be executed one level above in the root folder -FROM python:3.10-slim-bookworm AS build +FROM python:3.14-slim-trixie AS build # binutils is required by pyinstaller to strip any .so libs that are collected # git is used to determine & embed version information during build time RUN apt update && apt -yy install binutils git @@ -30,11 +30,11 @@ RUN rm -rf dist RUN pyinstaller --collect-data kuksa_client --add-data=/kuksa-python-sdk/kuksa-client/kuksa:kuksa --add-data=/kuksa-python-sdk/kuksa-client/sdv:sdv --clean -s /usr/local/bin/kuksa-client -# Debian 12 is bookworm, so the glibc version matches. Distroless is a lot smaller than +# Debian 13 is trixie, so the glibc version matches. Distroless is a lot smaller than # Debian slim versions # For development add :debug like this -# FROM gcr.io/distroless/base-debian12:debug to get a busybox shell as well -FROM gcr.io/distroless/base-debian12 +# FROM gcr.io/distroless/base-debian13:debug to get a busybox shell as well +FROM gcr.io/distroless/base-debian13 COPY --from=build /dist/kuksa-client /kuksa-client diff --git a/kuksa-client/kuksa_client/__main__.py b/kuksa-client/kuksa_client/__main__.py index 69a83e0..6c2c009 100755 --- a/kuksa-client/kuksa_client/__main__.py +++ b/kuksa-client/kuksa_client/__main__.py @@ -17,7 +17,6 @@ # SPDX-License-Identifier: Apache-2.0 ######################################################################## -import argparse import functools import json import logging.config @@ -32,11 +31,12 @@ from pygments import lexers from pygments import formatters from cmd2 import Cmd +from cmd2 import Cmd2ArgumentParser from cmd2 import CompletionItem +from cmd2 import Completions from cmd2 import with_argparser from cmd2 import with_category from cmd2 import constants -from cmd2.utils import basic_complete from urllib.parse import urlparse from kuksa_client import KuksaClientThread @@ -108,42 +108,34 @@ def refresh_metadata(self): def path_completer(self, text, line, begidx, endidx): if not self.connection_established(): - return None + return Completions() if len(self.pathCompletionItems) == 0: self.refresh_metadata() - # Normalize the delimiter used delimiter = "." if "/" in text: delimiter = "/" text = text.replace(delimiter, ".") - # Generate the list of all possible completions self.pathCompletionItems = [] for path in self.metadata.keys(): - # Compare case insensitive if path.lower().startswith(text.lower()): if delimiter != ".": path = path.replace(".", delimiter) - self.pathCompletionItems.append(path) + self.pathCompletionItems.append(CompletionItem(path)) - # Generate the list of completions to display - self.display_matches = display_completions(self.pathCompletionItems, delimiter) - - # Return all completions - return self.pathCompletionItems + return self.basic_complete(text, line, begidx, endidx, self.pathCompletionItems) def subscribeCallback(self, logPath, resp): if logPath is None: - with self.terminal_lock: - self.async_alert( - highlight( - json.dumps(json.loads(resp), indent=2), - lexers.JsonLexer(), - formatters.TerminalFormatter(), - ) + self.add_alert( + msg=highlight( + json.dumps(json.loads(resp), indent=2), + lexers.JsonLexer(), + formatters.TerminalFormatter(), ) + ) else: with logPath.open("a", encoding="utf-8") as logFile: logFile.write(resp + "\n") @@ -152,22 +144,22 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): self.pathCompletionItems = [] for sub_id in self.subscribeIds: self.pathCompletionItems.append(CompletionItem(sub_id)) - return basic_complete(text, line, begidx, endidx, self.pathCompletionItems) + return self.basic_complete(text, line, begidx, endidx, self.pathCompletionItems) COMM_SETUP_COMMANDS = "Communication Set-up Commands" VSS_COMMANDS = "Kuksa Interaction Commands (Supported by both KUKSA Databroker and KUKSA Server)" VSS_COMMANDS_SERVER = "Kuksa Interaction Commands (Only supported by KUKSA Server)" INFO_COMMANDS = "Info Commands" - ap_connect = argparse.ArgumentParser() + ap_connect = Cmd2ArgumentParser() ap_connect.add_argument( "server", help=f"VSS server to connect to. Format: protocol://host[:port]. \ Supported protocols: [grpc, grpcs, ws, wss]. Example: {DEFAULT_KUKSA_ADDRESS}", ) - ap_disconnect = argparse.ArgumentParser() - ap_authorize = argparse.ArgumentParser() + ap_disconnect = Cmd2ArgumentParser() + ap_authorize = Cmd2ArgumentParser() tokenfile_completer_method = functools.partial( Cmd.path_complete, path_filter=lambda path: (os.path.isdir(path) or path.endswith(".token")), @@ -175,19 +167,19 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): ap_authorize.add_argument( "token_or_tokenfile", help="JWT(or the file storing the token) for authorizing the client.", - completer_method=tokenfile_completer_method, + completer=tokenfile_completer_method, ) - ap_setValue = argparse.ArgumentParser() + ap_setValue = Cmd2ArgumentParser() ap_setValue.add_argument( - "Path", help="Path to be set", completer_method=path_completer + "Path", help="Path to be set", completer=path_completer ) ap_setValue.add_argument("Value", nargs="+", help="Value to be set") ap_setValue.add_argument( "-a", "--attribute", help="Attribute to be set", default="value" ) - ap_setValues = argparse.ArgumentParser() + ap_setValues = Cmd2ArgumentParser() ap_setValues.add_argument( "Path=Value", help="Path and new value this path is to be set with", @@ -198,34 +190,34 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): "-a", "--attribute", help="Attribute to be set", default="value" ) - ap_getValue = argparse.ArgumentParser() + ap_getValue = Cmd2ArgumentParser() ap_getValue.add_argument( - "Path", help="Path to be read", completer_method=path_completer + "Path", help="Path to be read", completer=path_completer ) ap_getValue.add_argument( "-a", "--attribute", help="Attribute to be get", default="value" ) - ap_getValues = argparse.ArgumentParser() + ap_getValues = Cmd2ArgumentParser() ap_getValues.add_argument( "Path", help="Path whose value is to be read", nargs="+", - completer_method=path_completer, + completer=path_completer, ) ap_getValues.add_argument( "-a", "--attribute", help="Attribute to be get", default="value" ) - ap_setTargetValue = argparse.ArgumentParser() + ap_setTargetValue = Cmd2ArgumentParser() ap_setTargetValue.add_argument( "Path", help="Path whose target value to be set", - completer_method=path_completer, + completer=path_completer, ) ap_setTargetValue.add_argument("Value", help="Value to be set") - ap_setTargetValues = argparse.ArgumentParser() + ap_setTargetValues = Cmd2ArgumentParser() ap_setTargetValues.add_argument( "Path=Value", help="Path and new target value this path is to be set with", @@ -233,24 +225,24 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): type=assignment_statement, ) - ap_getTargetValue = argparse.ArgumentParser() + ap_getTargetValue = Cmd2ArgumentParser() ap_getTargetValue.add_argument( "Path", help="Path whose target value is to be read", - completer_method=path_completer, + completer=path_completer, ) - ap_getTargetValues = argparse.ArgumentParser() + ap_getTargetValues = Cmd2ArgumentParser() ap_getTargetValues.add_argument( "Path", help="Path whose target value is to be read", nargs="+", - completer_method=path_completer, + completer=path_completer, ) - ap_subscribe = argparse.ArgumentParser() + ap_subscribe = Cmd2ArgumentParser() ap_subscribe.add_argument( - "Path", help="Path to subscribe to", completer_method=path_completer + "Path", help="Path to subscribe to", completer=path_completer ) ap_subscribe.add_argument( "-a", "--attribute", help="Attribute to subscribe to", default="value" @@ -263,9 +255,9 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): action="store_true", ) - ap_subscribeMultiple = argparse.ArgumentParser() + ap_subscribeMultiple = Cmd2ArgumentParser() ap_subscribeMultiple.add_argument( - "Path", help="Path to subscribe to", nargs="+", completer_method=path_completer + "Path", help="Path to subscribe to", nargs="+", completer=path_completer ) ap_subscribeMultiple.add_argument( "-a", "--attribute", help="Attribute to subscribe to", default="value" @@ -277,22 +269,22 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): action="store_true", ) - ap_unsubscribe = argparse.ArgumentParser() + ap_unsubscribe = Cmd2ArgumentParser() ap_unsubscribe.add_argument( "SubscribeId", help="Corresponding subscription Id", - completer_method=subscriptionIdCompleter, + completer=subscriptionIdCompleter, ) - ap_getMetaData = argparse.ArgumentParser() + ap_getMetaData = Cmd2ArgumentParser() ap_getMetaData.add_argument( "Path", help="Path whose metadata is to be read", - completer_method=path_completer, + completer=path_completer, ) - ap_updateMetaData = argparse.ArgumentParser() + ap_updateMetaData = Cmd2ArgumentParser() ap_updateMetaData.add_argument( - "Path", help="Path whose MetaData is to update", completer_method=path_completer + "Path", help="Path whose MetaData is to update", completer=path_completer ) ap_updateMetaData.add_argument( "Json", @@ -300,7 +292,7 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): " `updateVSSTree` instead.", ) - ap_updateVSSTree = argparse.ArgumentParser() + ap_updateVSSTree = Cmd2ArgumentParser() jsonfile_completer_method = functools.partial( Cmd.path_complete, path_filter=lambda path: (os.path.isdir(path) or path.endswith(".json")), @@ -308,7 +300,7 @@ def subscriptionIdCompleter(self, text, line, begidx, endidx): ap_updateVSSTree.add_argument( "Json", help="Json tree to update VSS", - completer_method=jsonfile_completer_method, + completer=jsonfile_completer_method, ) # Constructor, request names after protocol to avoid errors @@ -660,7 +652,7 @@ def main(): kuksa_logger = KuksaLogger() kuksa_logger.init_logging() - parser = argparse.ArgumentParser() + parser = Cmd2ArgumentParser() parser.add_argument( "server", nargs="?", diff --git a/kuksa-client/kuksa_client/cli_backend/grpc.py b/kuksa-client/kuksa_client/cli_backend/grpc.py index 85081b4..f0d1586 100644 --- a/kuksa-client/kuksa_client/cli_backend/grpc.py +++ b/kuksa-client/kuksa_client/cli_backend/grpc.py @@ -30,6 +30,8 @@ import os import logging +import grpc + from kuksa_client import cli_backend import kuksa_client.grpc import kuksa_client.grpc.aio @@ -45,7 +47,7 @@ def wrapper(updates: Iterable[EntryUpdate]) -> None: try: callback(json.dumps([update.to_dict() for update in updates], cls=DatabrokerEncoder)) except Exception as e: - logger.error("Callback could not be executed", e) + logger.error("Callback could not be executed: %s", e) return wrapper @@ -196,8 +198,8 @@ def subscribeMultiple(self, paths: Iterable[str], callback, attribute="value", t for path in paths ] requestArgs = { + "paths": list(paths), "entries": entries, - "try_v2": True, "callback": callback_wrapper(callback), } return self._sendReceiveMsg(("subscribe", requestArgs), timeout) @@ -261,9 +263,16 @@ async def _grpcHandler(self, vss_client: kuksa_client.grpc.aio.VSSClient): resp = await vss_client.authorize(str(requestArgs["token"])) elif call == "subscribe": callback = requestArgs.pop('callback') - subscriber_response_stream = vss_client.subscribe( - **requestArgs) - resp = await subscriber_manager.add_subscriber(subscriber_response_stream, callback) + paths = requestArgs.pop('paths') + entries = requestArgs.pop('entries') + try: + subscriber_response_stream = vss_client.v2_subscribe(paths=paths) + resp = await subscriber_manager.add_subscriber(subscriber_response_stream, callback) + except kuksa_client.grpc.VSSClientError as exc: + if exc.error["code"] != grpc.StatusCode.UNIMPLEMENTED.value[0]: + raise + subscriber_response_stream = vss_client.subscribe(entries=entries) + resp = await subscriber_manager.add_subscriber(subscriber_response_stream, callback) resp = {"subscriptionId": str(resp)} elif call == "unsubscribe": resp = await subscriber_manager.remove_subscriber(**requestArgs) diff --git a/kuksa-client/pyproject.toml b/kuksa-client/pyproject.toml index b7a8cdb..7b8d3f0 100644 --- a/kuksa-client/pyproject.toml +++ b/kuksa-client/pyproject.toml @@ -1,7 +1,8 @@ [build-system] requires = [ # Make sure to use the same exact version criteria in setup.cfg and update requirements.txt after changing - "grpcio-tools==1.68.0", + "grpcio-tools==1.82.1", + "protobuf>=7.35.1,<8.0.0", "setuptools>=77", "setuptools-git-versioning", "wheel", diff --git a/kuksa-client/requirements.txt b/kuksa-client/requirements.txt index 331ac50..d591e1e 100644 --- a/kuksa-client/requirements.txt +++ b/kuksa-client/requirements.txt @@ -1,33 +1,45 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile setup.cfg # -attrs==26.1.0 - # via cmd2 -cmd2==1.5.0 +cmd2==4.1.2 + # via kuksa_client (setup.cfg) +gnureadline==8.3.3 ; sys_platform == "darwin" # via kuksa_client (setup.cfg) -colorama==0.4.6 - # via cmd2 grpcio==1.82.1 # via grpcio-tools -grpcio-tools==1.68.0 +grpcio-tools==1.82.1 # via kuksa_client (setup.cfg) jsonpath-ng==1.8.0 # via kuksa_client (setup.cfg) -protobuf==5.29.6 +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +prompt-toolkit==3.0.52 + # via cmd2 +protobuf==7.35.1 # via # grpcio-tools # kuksa_client (setup.cfg) pygments==2.20.0 - # via kuksa_client (setup.cfg) + # via + # kuksa_client (setup.cfg) + # rich pyperclip==1.11.0 # via cmd2 +rich==15.0.0 + # via + # cmd2 + # rich-argparse +rich-argparse==1.8.0 + # via cmd2 typing-extensions==4.16.0 # via grpcio wcwidth==0.8.2 - # via cmd2 + # via prompt-toolkit websockets==16.1 # via kuksa_client (setup.cfg) diff --git a/kuksa-client/setup.cfg b/kuksa-client/setup.cfg index 9374d62..9f3ef19 100644 --- a/kuksa-client/setup.cfg +++ b/kuksa-client/setup.cfg @@ -22,15 +22,17 @@ license_files = LICENSE [options] -python_requires = >=3.8 +python_requires = >=3.11 install_requires = websockets >= 10.1 - cmd2 >= 1.4, <2.0 + cmd2 >= 4.0, <5.0 pygments >= 2.15 # Make sure to use the same version criteria in pyproject.toml and update requirements.txt after changing - grpcio-tools == 1.68.0 - protobuf>=5.29.6, <6.0dev + grpcio-tools == 1.82.1 + protobuf>=7.35.1, <8.0.0 jsonpath-ng >= 1.5.3 + gnureadline >= 8.2; sys_platform == "darwin" + pyreadline; sys_platform == "win32" packages = find: [options.extras_require] diff --git a/kuksa-client/test-requirements.txt b/kuksa-client/test-requirements.txt index 758e32b..d15ae0f 100644 --- a/kuksa-client/test-requirements.txt +++ b/kuksa-client/test-requirements.txt @@ -1,28 +1,22 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --extra=test --output-file=test-requirements.txt setup.cfg # astroid==4.0.4 # via pylint -attrs==26.1.0 - # via cmd2 -backports-asyncio-runner==1.2.0; python_version < "3.11" - # via pytest-asyncio -cmd2==1.5.0 +cmd2==4.1.2 # via kuksa_client (setup.cfg) -colorama==0.4.6 - # via cmd2 coverage[toml]==7.15.2 # via pytest-cov dill==0.4.1 # via pylint -exceptiongroup==1.3.1 - # via pytest +gnureadline==8.3.3 ; sys_platform == "darwin" + # via kuksa_client (setup.cfg) grpcio==1.82.1 # via grpcio-tools -grpcio-tools==1.68.0 +grpcio-tools==1.82.1 # via kuksa_client (setup.cfg) iniconfig==2.3.0 # via pytest @@ -30,8 +24,12 @@ isort==8.0.1 # via pylint jsonpath-ng==1.8.0 # via kuksa_client (setup.cfg) +markdown-it-py==4.2.0 + # via rich mccabe==0.7.0 # via pylint +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via pytest platformdirs==4.10.0 @@ -40,7 +38,9 @@ pluggy==1.6.0 # via # pytest # pytest-cov -protobuf==5.29.6 +prompt-toolkit==3.0.52 + # via cmd2 +protobuf==7.35.1 # via # grpcio-tools # kuksa_client (setup.cfg) @@ -48,6 +48,7 @@ pygments==2.20.0 # via # kuksa_client (setup.cfg) # pytest + # rich pylint==4.0.6 # via kuksa_client (setup.cfg) pyperclip==1.11.0 @@ -67,21 +68,18 @@ pytest-mock==3.15.1 # via kuksa_client (setup.cfg) pytest-timeout==2.4.0 # via kuksa_client (setup.cfg) -tomli==2.4.1 +rich==15.0.0 # via - # coverage - # pylint - # pytest + # cmd2 + # rich-argparse +rich-argparse==1.8.0 + # via cmd2 tomlkit==0.15.1 # via pylint typing-extensions==4.16.0 - # via - # astroid - # exceptiongroup - # grpcio - # pytest-asyncio + # via grpcio wcwidth==0.8.2 - # via cmd2 + # via prompt-toolkit websockets==16.1 # via kuksa_client (setup.cfg)