From aa5190155e2c872d2bd954c087aa7d3809ef28f8 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 16 Feb 2026 11:22:21 +0200 Subject: [PATCH 1/4] usermanager: Improve help on invalid arguments Signed-off-by: Denys Fedoryshchenko --- scripts/usermanager.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/scripts/usermanager.py b/scripts/usermanager.py index 653c3b7f..094a6cab 100755 --- a/scripts/usermanager.py +++ b/scripts/usermanager.py @@ -298,14 +298,9 @@ def main(): default_paths = "\n".join(f" - {path}" for path in DEFAULT_CONFIG_PATHS) parser = argparse.ArgumentParser( description="KernelCI API user management helper", - usage=( - "usermanager.py [-h] [--config CONFIG] [--api-url API_URL] " - "[--token TOKEN] [--instance INSTANCE] [--token-label TOKEN_LABEL]\n" - " []\n\n" - "Commands:\n" - f"{command_list}" - ), epilog=( + "Commands:\n" + f"{command_list}\n\n" "Examples:\n" " ./scripts/usermanager.py invite --username alice --email " "alice@example.org --return-token\n" From b0562331b5e037c4a2fae10be02956121b38f3a7 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 16 Feb 2026 19:02:59 +0200 Subject: [PATCH 2/4] telemetry: copilot suggestion fixes Signed-off-by: Denys Fedoryshchenko --- api/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/main.py b/api/main.py index 6d23f6b7..dec4f4f3 100644 --- a/api/main.py +++ b/api/main.py @@ -895,7 +895,13 @@ def _build_events_query(query_params: dict) -> tuple: if from_ts: if isinstance(from_ts, str): - from_ts = datetime.fromisoformat(from_ts) + try: + from_ts = datetime.fromisoformat(from_ts) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid 'from' parameter, must be an ISO 8601 datetime" + ) from exc query_params['timestamp'] = {'$gt': from_ts} if path: query_params['data.path'] = {'$regex': path} From b89ee733b43ee24e37afef2fabe9810766d8f434 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Fri, 13 Mar 2026 13:12:00 +0200 Subject: [PATCH 3/4] linter: Multiple fixes across the project to satisfy linters Signed-off-by: Denys Fedoryshchenko --- .pylintrc | 6 +++--- api/db.py | 8 ++++---- api/main.py | 6 +++--- api/pubsub.py | 3 ++- api/pubsub_mongo.py | 3 ++- scripts/usermanager.py | 6 ++++-- tests/e2e_tests/conftest.py | 3 ++- tests/e2e_tests/test_pipeline.py | 16 ++++++++++------ tests/e2e_tests/test_pubsub_handler.py | 5 +++-- tests/unit_tests/conftest.py | 3 ++- tests/unit_tests/test_events_handler.py | 2 +- tests/unit_tests/test_node_handler.py | 7 ++++--- tests/unit_tests/test_pubsub.py | 22 +++++++++++++++------- tests/unit_tests/test_user_handler.py | 2 +- 14 files changed, 56 insertions(+), 36 deletions(-) diff --git a/.pylintrc b/.pylintrc index e3fb239d..d0ec30d2 100644 --- a/.pylintrc +++ b/.pylintrc @@ -56,7 +56,7 @@ py-version=3.10 # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. -suggestion-mode=yes +# suggestion-mode is removed in pylint 4.x # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. @@ -564,5 +564,5 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/api/db.py b/api/db.py index 6bbd68f8..e38f5be4 100644 --- a/api/db.py +++ b/api/db.py @@ -70,7 +70,7 @@ async def get_kv(self, namespace, key): Get value from redis key-value store Create a keyname by concatenating namespace and key """ - keyname = f"{namespace}:{key}" + keyname = ":".join([namespace, key]) return await self._redis.get(keyname) async def set_kv(self, namespace, key, value): @@ -78,7 +78,7 @@ async def set_kv(self, namespace, key, value): Set value in redis key-value store Create a keyname by concatenating namespace and key """ - keyname = f"{namespace}:{key}" + keyname = ":".join([namespace, key]) return await self._redis.set(keyname, value) async def del_kv(self, namespace, key): @@ -86,7 +86,7 @@ async def del_kv(self, namespace, key): Delete key from redis key-value store Create a keyname by concatenating namespace and key """ - keyname = f"{namespace}:{key}" + keyname = ":".join([namespace, key]) return await self._redis.delete(keyname) async def exists_kv(self, namespace, key): @@ -94,7 +94,7 @@ async def exists_kv(self, namespace, key): Check if key exists in redis key-value store Create a keyname by concatenating namespace and key """ - keyname = f"{namespace}:{key}" + keyname = ":".join([namespace, key]) return await self._redis.exists(keyname) async def create_indexes(self): diff --git a/api/main.py b/api/main.py index dec4f4f3..4832e972 100644 --- a/api/main.py +++ b/api/main.py @@ -305,7 +305,7 @@ def _is_proxy_addr() -> bool: if forwarded_host: scheme = forwarded_proto or request.url.scheme - return f"{scheme}://{forwarded_host}".rstrip("/") + return (scheme + "://" + forwarded_host).rstrip("/") return str(request.base_url).rstrip("/") @@ -745,8 +745,8 @@ def _user_can_edit_node(user: User, node: Node) -> bool: return True runtime = _get_node_runtime(node) if runtime: - runtime_editor = f'runtime:{runtime}:node-editor' - runtime_admin = f'runtime:{runtime}:node-admin' + runtime_editor = ":".join(['runtime', runtime, 'node-editor']) + runtime_admin = ":".join(['runtime', runtime, 'node-admin']) if (runtime_editor in user_group_names or runtime_admin in user_group_names): return True diff --git a/api/pubsub.py b/api/pubsub.py index 9f5bf89d..83cdfeb7 100644 --- a/api/pubsub.py +++ b/api/pubsub.py @@ -43,7 +43,8 @@ def __init__(self, host=None, db_number=None): if db_number is None: db_number = self._settings.redis_db_number self._redis = aioredis.from_url( - f'redis://{host}/{db_number}', health_check_interval=30 + 'redis://' + host + '/' + str(db_number), + health_check_interval=30 ) # self._subscriptions is a dict that matches a subscription id # (key) with a Subscription object ('sub') and a redis diff --git a/api/pubsub_mongo.py b/api/pubsub_mongo.py index 513385f0..0188f72b 100644 --- a/api/pubsub_mongo.py +++ b/api/pubsub_mongo.py @@ -77,7 +77,8 @@ def __init__(self, mongo_client=None, host=None, db_number=None, db_number = self._settings.redis_db_number self._redis = aioredis.from_url( - f'redis://{host}/{db_number}', health_check_interval=30 + 'redis://' + host + '/' + str(db_number), + health_check_interval=30 ) # MongoDB setup diff --git a/scripts/usermanager.py b/scripts/usermanager.py index 094a6cab..784f861d 100755 --- a/scripts/usermanager.py +++ b/scripts/usermanager.py @@ -294,8 +294,10 @@ def main(): ("update-user", "Patch user by id/email/username"), ("whoami", "Show current user"), ] - command_list = "\n".join(f" {name:<18} {desc}" for name, desc in command_help) - default_paths = "\n".join(f" - {path}" for path in DEFAULT_CONFIG_PATHS) + command_list = "\n".join( + " {:<18} {}".format(name, desc) for name, desc in command_help) + default_paths = "\n".join( + " - {}".format(path) for path in DEFAULT_CONFIG_PATHS) parser = argparse.ArgumentParser( description="KernelCI API user management helper", epilog=( diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 217d075a..d290af51 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -10,9 +10,10 @@ from httpx import AsyncClient from motor.motor_asyncio import AsyncIOMotorClient -from api.main import versioned_app from kernelci.api.models import Node, Regression +from api.main import versioned_app + BASE_URL = 'http://api:8000/latest/' DB_URL = 'mongodb://db:27017' DB_NAME = 'kernelci' diff --git a/tests/e2e_tests/test_pipeline.py b/tests/e2e_tests/test_pipeline.py index e78c0bde..243490ce 100644 --- a/tests/e2e_tests/test_pipeline.py +++ b/tests/e2e_tests/test_pipeline.py @@ -37,8 +37,9 @@ async def test_node_pipeline(test_async_client): """ # Create Task to listen pubsub event on 'node' channel - task_listen = create_listen_task(test_async_client, - pytest.node_channel_subscription_id) # pylint: disable=no-member + task_listen = create_listen_task( + test_async_client, + pytest.node_channel_subscription_id) # pylint: disable=no-member # Create a node node = { @@ -48,8 +49,10 @@ async def test_node_pipeline(test_async_client): "data": { "kernel_revision": { "tree": "mainline", - "url": ("https://git.kernel.org/pub/scm/linux/kernel/git/" - "torvalds/linux.git"), + "url": ( + "https://git.kernel.org/pub/scm/" + "linux/kernel/git/torvalds/linux.git" + ), "branch": "master", "commit": "2a987e65025e2b79c6d453b78cb5985ac6e5eb28", "describe": "v5.16-rc4-31-g2a987e65025e" @@ -73,8 +76,9 @@ async def test_node_pipeline(test_async_client): node = response.json() # Create Task to listen 'updated' event on 'node' channel - task_listen = create_listen_task(test_async_client, - pytest.node_channel_subscription_id) # pylint: disable=no-member + task_listen = create_listen_task( + test_async_client, + pytest.node_channel_subscription_id) # pylint: disable=no-member # Update node.state node.update({"state": "done"}) diff --git a/tests/e2e_tests/test_pubsub_handler.py b/tests/e2e_tests/test_pubsub_handler.py index 9e9ccd8b..42c4e327 100644 --- a/tests/e2e_tests/test_pubsub_handler.py +++ b/tests/e2e_tests/test_pubsub_handler.py @@ -23,8 +23,9 @@ async def test_pubsub_handler(test_async_client): Use pubsub listener task to verify published event message. """ # Create Task to listen pubsub event on 'test_channel' channel - task_listen = create_listen_task(test_async_client, - pytest.test_channel_subscription_id) # pylint: disable=no-member + task_listen = create_listen_task( + test_async_client, + pytest.test_channel_subscription_id) # pylint: disable=no-member # Created and publish CloudEvent attributes = { diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 47fd4448..e602e0e6 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -38,7 +38,7 @@ t3bAE-pHSzZaSHp7FMlImqgYvL6f_0xDUD-nQwxEm3k' API_VERSION = 'latest' -BASE_URL = f'http://testserver/{API_VERSION}/' +BASE_URL = 'http://testserver/' + API_VERSION + '/' def mock_get_current_user(request: Request): @@ -90,6 +90,7 @@ def mock_get_current_admin_user(request: Request): is_verified=True ) + # Mock dependency callables for getting current user app.dependency_overrides[get_current_user] = mock_get_current_user app.dependency_overrides[get_current_superuser] = mock_get_current_admin_user diff --git a/tests/unit_tests/test_events_handler.py b/tests/unit_tests/test_events_handler.py index 07aef4c4..63cd8d3e 100644 --- a/tests/unit_tests/test_events_handler.py +++ b/tests/unit_tests/test_events_handler.py @@ -34,7 +34,7 @@ def test_get_events_filter_by_ids(mock_db_find_by_attributes, test_client): oid1, oid2 = ObjectId(), ObjectId() mock_db_find_by_attributes.return_value = [] - resp = test_client.get(f"events?ids={oid1},{oid2}") + resp = test_client.get("events?ids=" + oid1 + "," + oid2) assert resp.status_code == 200 called_model, called_query = mock_db_find_by_attributes.call_args.args diff --git a/tests/unit_tests/test_node_handler.py b/tests/unit_tests/test_node_handler.py index 80ffd219..00034deb 100644 --- a/tests/unit_tests/test_node_handler.py +++ b/tests/unit_tests/test_node_handler.py @@ -12,9 +12,10 @@ import json -from tests.unit_tests.conftest import BEARER_TOKEN from kernelci.api.models import Node, Revision + from api.models import PageModel +from tests.unit_tests.conftest import BEARER_TOKEN def test_create_node_endpoint(mock_db_create, mock_publish_cloudevent, @@ -40,7 +41,7 @@ def test_create_node_endpoint(mock_db_create, mock_publish_cloudevent, name="checkout", path=["checkout"], group="debug", - data= {'kernel_revision': revision_obj}, + data={'kernel_revision': revision_obj}, parent=None, state="closing", result=None, @@ -216,7 +217,7 @@ def test_get_node_by_id_endpoint(mock_db_find_by_id, name="checkout", path=["checkout"], group="blah", - data = {'kernel_revision': revision_obj}, + data={'kernel_revision': revision_obj}, parent=None, state="closing", result=None, diff --git a/tests/unit_tests/test_pubsub.py b/tests/unit_tests/test_pubsub.py index 481a95b5..2c3f0ab2 100644 --- a/tests/unit_tests/test_pubsub.py +++ b/tests/unit_tests/test_pubsub.py @@ -99,16 +99,24 @@ async def test_pubsub_publish_couldevent(mock_pubsub_publish): """ data = 'validate json' - attributes = { "specversion": "1.0", "id": "6878b661-96dc-4e93-8c92-26eb9ff8db64", - "source": "https://api.kernelci.org/", "type": "api.kernelci.org", - "time": "2022-01-31T21:29:29.675593+00:00"} + attributes = { + "specversion": "1.0", + "id": "6878b661-96dc-4e93-8c92-26eb9ff8db64", + "source": "https://api.kernelci.org/", + "type": "api.kernelci.org", + "time": "2022-01-31T21:29:29.675593+00:00", + } await mock_pubsub_publish.publish_cloudevent('CHANNEL1', data, attributes) - expected_json = str.encode('{"specversion": "1.0", '\ - '"id": "6878b661-96dc-4e93-8c92-26eb9ff8db64", "source": "https://api.kernelci.org/", '\ - '"type": "api.kernelci.org", "time": "2022-01-31T21:29:29.675593+00:00", '\ - '"data": "validate json"}') + expected_json = str.encode( + '{"specversion": "1.0", ' + '"id": "6878b661-96dc-4e93-8c92-26eb9ff8db64", ' + '"source": "https://api.kernelci.org/", ' + '"type": "api.kernelci.org", ' + '"time": "2022-01-31T21:29:29.675593+00:00", ' + '"data": "validate json"}' + ) json_arg = mock_pubsub_publish._redis.execute_command.call_args.args[2] diff --git a/tests/unit_tests/test_user_handler.py b/tests/unit_tests/test_user_handler.py index 9f512fcd..43e8ce9c 100644 --- a/tests/unit_tests/test_user_handler.py +++ b/tests/unit_tests/test_user_handler.py @@ -129,7 +129,7 @@ async def test_create_user_endpoint_negative(test_async_client): @pytest.mark.asyncio async def test_create_user_with_group(test_async_client, mock_db_find_one, - mock_db_update,mock_db_find_by_id): + mock_db_update, mock_db_find_by_id): """ Test Case : Test KernelCI API /user/register endpoint to create a user with a user group From 1619b808966a1944dd79d88078cb3aaba68c9fe2 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Fri, 13 Mar 2026 13:15:11 +0200 Subject: [PATCH 4/4] linters: Update max line length to 100 Signed-off-by: Denys Fedoryshchenko --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 22f7ffe9..c005545e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,7 +62,7 @@ jobs: - name: Run pycodestyle run: | - pycodestyle api/*.py + pycodestyle --max-line-length=100 api/*.py - name: Run API containers run: |