Skip to content
Merged
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
96 changes: 96 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Python Client CI

on:
push:
branches: [main]
paths:
- 'clients/python/**'
- 'spec/**'
- '.github/workflows/python-ci.yml'
pull_request:
branches: [main]
paths:
- 'clients/python/**'
- 'spec/**'
- '.github/workflows/python-ci.yml'

jobs:
build-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: clients/python

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install pipx
run: pip install pipx

- name: Generate client from spec
working-directory: .
run: ./scripts/generate.sh

- name: Set up generated symlink
run: ln -sf ../../generated src/ros2_medkit_client/_generated

- name: Install package with dev deps
run: pip install -e '.[dev]'

- name: Test
run: python -m pytest -v

- name: Lint
run: ruff check src/ tests/

- name: Format check
run: ruff format --check src/ tests/

publish:
needs: build-and-test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
defaults:
run:
working-directory: clients/python

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install build tools
run: pip install pipx build twine

- name: Generate client from spec
working-directory: .
run: ./scripts/generate.sh

- name: Set up generated symlink
run: ln -sf ../../generated src/ros2_medkit_client/_generated

- name: Build wheel
run: python -m build

- name: Publish to GitHub Packages
run: twine upload --skip-existing --repository-url https://pypi.pkg.github.com/selfpatch/ dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,40 @@ const authedClient = createMedkitClient({

See `clients/typescript/` for the full source and API.

## Python Client

### Setup

```bash
pip install ros2-medkit-client --index-url https://pypi.pkg.github.com/selfpatch/simple/
```

### Usage

```python
from ros2_medkit_client import MedkitClient, MedkitError
from ros2_medkit_client.api.discovery import list_apps

async with MedkitClient(base_url="localhost:8080") as client:
# Option 1: call() bridges errors into MedkitError exceptions
apps = await client.call(list_apps.asyncio)

# Option 2: raw generated API (returns SuccessType | GenericError | None)
result = await list_apps.asyncio(client=client.http)

# SSE streaming
async for event in client.streams.faults():
print(event.data)

# Error handling with call()
try:
apps = await client.call(list_apps.asyncio)
except MedkitError as e:
print(e.code, e.message)
```

See `clients/python/` for the full source and API.

## License

Apache 2.0 - see [LICENSE](LICENSE).
46 changes: 46 additions & 0 deletions clients/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "ros2-medkit-client"
version = "0.1.0"
description = "Async Python client for the ros2_medkit gateway"
license = "Apache-2.0"
requires-python = ">=3.11"
authors = [{ name = "bburda" }]
keywords = ["ros2", "sovd", "medkit", "diagnostics", "openapi", "client"]
dependencies = [
"httpx>=0.27",
"attrs>=23.0", # Required by generated client code (openapi-python-client)
"python-dateutil>=2.9", # Required by generated models (dateutil.parser.isoparse)
]

[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"respx>=0.22",
"ruff>=0.8",
]

[project.urls]
Homepage = "https://github.com/selfpatch/ros2_medkit_clients"
Issues = "https://github.com/selfpatch/ros2_medkit_clients/issues"

[tool.hatch.build.targets.wheel]
# Note: _generated/ is a symlink to ../../generated/ (created by generate.sh).
# Run ./scripts/generate.sh && ln -sf ../../generated src/ros2_medkit_client/_generated
# before building the wheel.
packages = ["src/ros2_medkit_client"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

[tool.ruff]
target-version = "py311"
line-length = 120

[tool.ruff.lint]
select = ["E", "F", "I", "W"]
35 changes: 35 additions & 0 deletions clients/python/src/ros2_medkit_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2026 bburda
# SPDX-License-Identifier: Apache-2.0

"""Async Python client for the ros2_medkit gateway.

Usage::

from ros2_medkit_client import MedkitClient, MedkitError
from ros2_medkit_client.api.discovery import list_apps

async with MedkitClient(base_url="localhost:8080") as client:
# Option 1: call() bridges errors into MedkitError exceptions
apps = await client.call(list_apps.asyncio)

# Option 2: raw API - returns SuccessType | GenericError | None
result = await list_apps.asyncio(client=client.http)

# Stream faults via SSE
async for event in client.streams.faults():
print(event.data)
"""

from ros2_medkit_client.client import MedkitClient, normalize_base_url
from ros2_medkit_client.errors import MedkitConnectionError, MedkitError, MedkitTimeoutError
from ros2_medkit_client.sse import SseEvent, SseStream

__all__ = [
"MedkitClient",
"MedkitError",
"MedkitConnectionError",
"MedkitTimeoutError",
"SseStream",
"SseEvent",
"normalize_base_url",
]
9 changes: 9 additions & 0 deletions clients/python/src/ros2_medkit_client/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2026 bburda
# SPDX-License-Identifier: Apache-2.0
"""Re-exported API modules from generated code.

Import individual modules to call generated endpoint functions::

from ros2_medkit_client.api.discovery import list_apps
result = await list_apps.asyncio(client=client.http)
"""
15 changes: 15 additions & 0 deletions clients/python/src/ros2_medkit_client/api/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 bburda
# SPDX-License-Identifier: Apache-2.0
"""Authentication API - authorization, token management."""

from ros2_medkit_client._generated.api.authentication import (
authorize,
get_token,
revoke_token,
)

__all__ = [
"authorize",
"get_token",
"revoke_token",
]
53 changes: 53 additions & 0 deletions clients/python/src/ros2_medkit_client/api/bulk_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2026 bburda
# SPDX-License-Identifier: Apache-2.0
"""Bulk Data API - bulk data upload, download, and management."""

from ros2_medkit_client._generated.api.bulk_data import (
delete_app_bulk_data,
delete_component_bulk_data,
download_app_bulk_data,
download_area_bulk_data,
download_component_bulk_data,
download_function_bulk_data,
download_subarea_bulk_data,
download_subcomponent_bulk_data,
list_app_bulk_data_categories,
list_app_bulk_data_descriptors,
list_area_bulk_data_categories,
list_area_bulk_data_descriptors,
list_component_bulk_data_categories,
list_component_bulk_data_descriptors,
list_function_bulk_data_categories,
list_function_bulk_data_descriptors,
list_subarea_bulk_data_categories,
list_subarea_bulk_data_descriptors,
list_subcomponent_bulk_data_categories,
list_subcomponent_bulk_data_descriptors,
upload_app_bulk_data,
upload_component_bulk_data,
)

__all__ = [
"delete_app_bulk_data",
"delete_component_bulk_data",
"download_app_bulk_data",
"download_area_bulk_data",
"download_component_bulk_data",
"download_function_bulk_data",
"download_subarea_bulk_data",
"download_subcomponent_bulk_data",
"list_app_bulk_data_categories",
"list_app_bulk_data_descriptors",
"list_area_bulk_data_categories",
"list_area_bulk_data_descriptors",
"list_component_bulk_data_categories",
"list_component_bulk_data_descriptors",
"list_function_bulk_data_categories",
"list_function_bulk_data_descriptors",
"list_subarea_bulk_data_categories",
"list_subarea_bulk_data_descriptors",
"list_subcomponent_bulk_data_categories",
"list_subcomponent_bulk_data_descriptors",
"upload_app_bulk_data",
"upload_component_bulk_data",
]
49 changes: 49 additions & 0 deletions clients/python/src/ros2_medkit_client/api/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2026 bburda
# SPDX-License-Identifier: Apache-2.0
"""Configuration API - read and write entity configurations."""

from ros2_medkit_client._generated.api.configuration import (
delete_all_app_configurations,
delete_all_area_configurations,
delete_all_component_configurations,
delete_all_function_configurations,
delete_app_configuration,
delete_area_configuration,
delete_component_configuration,
delete_function_configuration,
get_app_configuration,
get_area_configuration,
get_component_configuration,
get_function_configuration,
list_app_configurations,
list_area_configurations,
list_component_configurations,
list_function_configurations,
set_app_configuration,
set_area_configuration,
set_component_configuration,
set_function_configuration,
)

__all__ = [
"delete_all_app_configurations",
"delete_all_area_configurations",
"delete_all_component_configurations",
"delete_all_function_configurations",
"delete_app_configuration",
"delete_area_configuration",
"delete_component_configuration",
"delete_function_configuration",
"get_app_configuration",
"get_area_configuration",
"get_component_configuration",
"get_function_configuration",
"list_app_configurations",
"list_area_configurations",
"list_component_configurations",
"list_function_configurations",
"set_app_configuration",
"set_area_configuration",
"set_component_configuration",
"set_function_configuration",
]
Loading
Loading