Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions kuksa-client/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
94 changes: 43 additions & 51 deletions kuksa-client/kuksa_client/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
# SPDX-License-Identifier: Apache-2.0
########################################################################

import argparse
import functools
import json
import logging.config
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -152,42 +144,42 @@ 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")),
)
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",
Expand All @@ -198,59 +190,59 @@ 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",
nargs="+",
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"
Expand All @@ -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"
Expand All @@ -277,38 +269,38 @@ 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",
help="MetaData to update. Note, only attributes can be update, if update children or the whole vss tree, use"
" `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")),
)
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
Expand Down Expand Up @@ -660,7 +652,7 @@ def main():
kuksa_logger = KuksaLogger()
kuksa_logger.init_logging()

parser = argparse.ArgumentParser()
parser = Cmd2ArgumentParser()
parser.add_argument(
"server",
nargs="?",
Expand Down
19 changes: 14 additions & 5 deletions kuksa-client/kuksa_client/cli_backend/grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion kuksa-client/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading
Loading