From c6712f9ca31de92cc0b1f33cf9a9a6867553945e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 23 Jan 2026 16:40:26 -0500 Subject: [PATCH 1/5] feat(python): Add examples from python-client branch Adds example application demonstrating taskbroker client usage including: - Example app setup with Kafka producer configuration - CLI commands for spawning tasks, running scheduler, and worker - Sample tasks demonstrating various features (retries, at-most-once, timed tasks) - Stub at-most-once store implementation Co-Authored-By: Claude Sonnet 4.5 --- clients/python/src/examples/__init__.py | 0 clients/python/src/examples/app.py | 22 ++++++ clients/python/src/examples/cli.py | 96 +++++++++++++++++++++++++ clients/python/src/examples/py.typed | 0 clients/python/src/examples/store.py | 12 ++++ clients/python/src/examples/tasks.py | 74 +++++++++++++++++++ 6 files changed, 204 insertions(+) create mode 100644 clients/python/src/examples/__init__.py create mode 100644 clients/python/src/examples/app.py create mode 100644 clients/python/src/examples/cli.py create mode 100644 clients/python/src/examples/py.typed create mode 100644 clients/python/src/examples/store.py create mode 100644 clients/python/src/examples/tasks.py diff --git a/clients/python/src/examples/__init__.py b/clients/python/src/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clients/python/src/examples/app.py b/clients/python/src/examples/app.py new file mode 100644 index 00000000..df059332 --- /dev/null +++ b/clients/python/src/examples/app.py @@ -0,0 +1,22 @@ +from arroyo.backends.kafka import KafkaProducer + +from examples.store import StubAtMostOnce +from taskbroker_client.app import TaskbrokerApp + + +def producer_factory(topic: str) -> KafkaProducer: + # TODO use env vars for kafka host/port + config = { + "bootstrap.servers": "127.0.0.1:9092", + "compression.type": "lz4", + "message.max.bytes": 50000000, # 50MB + } + return KafkaProducer(config) + + +app = TaskbrokerApp( + name="example-app", + producer_factory=producer_factory, + at_most_once_store=StubAtMostOnce(), +) +app.set_modules(["examples.tasks"]) diff --git a/clients/python/src/examples/cli.py b/clients/python/src/examples/cli.py new file mode 100644 index 00000000..26cd8e01 --- /dev/null +++ b/clients/python/src/examples/cli.py @@ -0,0 +1,96 @@ +import logging +import os +import time + +import click + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(message)s", + handlers=[logging.StreamHandler()], +) + + +@click.group() +def main() -> None: + click.echo("Example application") + + +@main.command() +@click.option( + "--count", + help="The number of tasks to generate", + default=1, +) +def spawn(count: int = 1) -> None: + from examples.tasks import timed_task + + click.echo(f"Spawning {count} tasks") + for _ in range(0, count): + timed_task.delay(sleep_seconds=0.1) + click.echo("Complete") + + +@main.command() +def scheduler() -> None: + from redis import StrictRedis + + from examples.app import app + from taskbroker_client.metrics import NoOpMetricsBackend + from taskbroker_client.scheduler import RunStorage, ScheduleRunner, crontab + + redis_host = os.getenv("REDIS_HOST") or "localhost" + redis_port = int(os.getenv("REDIS_PORT") or 6379) + + # Ensure all task modules are loaded. + app.load_modules() + + redis = StrictRedis(host=redis_host, port=redis_port, decode_responses=True) + metrics = NoOpMetricsBackend() + run_storage = RunStorage(metrics=metrics, redis=redis) + scheduler = ScheduleRunner(app, run_storage) + + # Define a scheduled task + scheduler.add( + "simple-task", {"task": "examples:examples.simple_task", "schedule": crontab(minute="*/1")} + ) + + click.echo("Starting scheduler") + scheduler.log_startup() + while True: + sleep_time = scheduler.tick() + time.sleep(sleep_time) + + +@main.command() +@click.option( + "--rpc-host", + help="The address of the taskbroker this worker connects to.", + default="127.0.0.1:50051", +) +@click.option( + "--concurrency", + help="The number of child processes to start.", + default=2, +) +def worker(rpc_host: str, concurrency: int) -> None: + from taskbroker_client.worker import TaskWorker + + click.echo("Starting worker") + worker = TaskWorker( + app_module="examples.app:app", + broker_hosts=[rpc_host], + max_child_task_count=100, + concurrency=concurrency, + child_tasks_queue_maxsize=concurrency * 2, + result_queue_maxsize=concurrency * 2, + rebalance_after=32, + processing_pool_name="examples", + process_type="forkserver", + ) + exitcode = worker.start() + raise SystemExit(exitcode) + + +if __name__ == "__main__": + main() diff --git a/clients/python/src/examples/py.typed b/clients/python/src/examples/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/clients/python/src/examples/store.py b/clients/python/src/examples/store.py new file mode 100644 index 00000000..3e7fc996 --- /dev/null +++ b/clients/python/src/examples/store.py @@ -0,0 +1,12 @@ +from taskbroker_client.types import AtMostOnceStore + + +class StubAtMostOnce(AtMostOnceStore): + def __init__(self) -> None: + self._keys: dict[str, str] = {} + + def add(self, key: str, value: str, timeout: int) -> bool: + if key in self._keys: + return False + self._keys[key] = value + return True diff --git a/clients/python/src/examples/tasks.py b/clients/python/src/examples/tasks.py new file mode 100644 index 00000000..8e87c9e3 --- /dev/null +++ b/clients/python/src/examples/tasks.py @@ -0,0 +1,74 @@ +""" +Example taskbroker application with tasks + +Used in tests for the worker. +""" + +import logging +from time import sleep +from typing import Any + +from redis import StrictRedis + +from examples.app import app +from taskbroker_client.retry import LastAction, NoRetriesRemainingError, Retry, RetryTaskError +from taskbroker_client.retry import retry_task as retry_task_helper + +logger = logging.getLogger(__name__) + + +# Create a namespace and register tasks +exampletasks = app.taskregistry.create_namespace("examples") + + +@exampletasks.register(name="examples.simple_task") +def simple_task(*args: list[Any], **kwargs: dict[str, Any]) -> None: + sleep(0.1) + logger.debug("simple_task complete") + + +@exampletasks.register(name="examples.retry_task", retry=Retry(times=2)) +def retry_task() -> None: + raise RetryTaskError + + +@exampletasks.register(name="examples.fail_task") +def fail_task() -> None: + raise ValueError("nope") + + +@exampletasks.register(name="examples.at_most_once", at_most_once=True) +def at_most_once_task() -> None: + pass + + +@exampletasks.register( + name="examples.retry_state", retry=Retry(times=2, times_exceeded=LastAction.Deadletter) +) +def retry_state() -> None: + try: + retry_task_helper() + except NoRetriesRemainingError: + # TODO read host from env vars + redis = StrictRedis(host="localhost", port=6379, decode_responses=True) + redis.set("no-retries-remaining", 1) + + +@exampletasks.register( + name="examples.will_retry", + retry=Retry(times=3, on=(RuntimeError,), times_exceeded=LastAction.Discard), +) +def will_retry(failure: str) -> None: + if failure == "retry": + logger.debug("going to retry with explicit retry error") + raise RetryTaskError + if failure == "raise": + logger.debug("raising runtimeerror") + raise RuntimeError("oh no") + logger.debug("got %s", failure) + + +@exampletasks.register(name="examples.timed") +def timed_task(sleep_seconds: float | str, *args: list[Any], **kwargs: dict[str, Any]) -> None: + sleep(float(sleep_seconds)) + logger.debug("timed_task complete") From 130781d39c231d68ee23429eb89803426d1e954a Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 23 Jan 2026 17:27:34 -0500 Subject: [PATCH 2/5] feat(ci): Add CI jobs for Python client testing and linting Add dedicated CI jobs that run conditionally when Python client files change: - python-client-lint: runs pre-commit checks (black, isort, flake8, mypy) - python-client-test: runs pytest with coverage and uploads to codecov Add Makefile targets for local development: - python-venv: builds Python virtual environment with uv - python-test: runs Python client tests with coverage Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/ci.yml | 89 +++++++++++++++++++++++++++++++++++ Makefile | 8 ++++ clients/python/pyproject.toml | 1 + 3 files changed, 98 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 718027f3..cf6c8089 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,95 @@ jobs: jq '.[]' --raw-output <<< '${{steps.changes.outputs.all_files}}' | xargs pre-commit run --files + python-client-lint: + name: Python Client Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Get changed Python client files + id: changes + uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0.0 + with: + list-files: json + filters: | + python: + - added|modified: 'clients/python/**' + + - name: Skip if no Python client changes + if: steps.changes.outputs.python != 'true' + run: echo "No Python client files changed, skipping pre-commit checks" + + - uses: astral-sh/setup-uv@884ad927a57e558e7a70b92f2bccf9198a4be546 # v6 + if: steps.changes.outputs.python == 'true' + with: + version: '0.8.2' + enable-cache: false + + - uses: getsentry/action-setup-venv@5a80476d175edf56cb205b08bc58986fa99d1725 # v3.2.0 + if: steps.changes.outputs.python == 'true' + with: + cache-dependency-path: uv.lock + install-cmd: uv sync --all-packages --all-groups --frozen --active + + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + if: steps.changes.outputs.python == 'true' + with: + path: ~/.cache/pre-commit + key: cache-epoch-1|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml', 'uv.lock') }} + + - name: Install pre-commit + if: steps.changes.outputs.python == 'true' + run: pre-commit install-hooks + + - name: Run pre-commit on Python client files + if: steps.changes.outputs.python == 'true' + run: | + jq '.[]' --raw-output <<< '${{steps.changes.outputs.python_files}}' | + xargs pre-commit run --files + + python-client-test: + name: Python Client Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Get changed Python client files + id: changes + uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0.0 + with: + filters: | + python: + - 'clients/python/**' + + - name: Skip if no Python client changes + if: steps.changes.outputs.python != 'true' + run: echo "No Python client files changed, skipping tests" + + - uses: astral-sh/setup-uv@884ad927a57e558e7a70b92f2bccf9198a4be546 # v6 + if: steps.changes.outputs.python == 'true' + with: + version: '0.8.2' + enable-cache: false + + - uses: getsentry/action-setup-venv@5a80476d175edf56cb205b08bc58986fa99d1725 # v3.2.0 + if: steps.changes.outputs.python == 'true' + with: + cache-dependency-path: uv.lock + install-cmd: uv sync --all-packages --all-groups --frozen --active + + - name: Run pytest with coverage + if: steps.changes.outputs.python == 'true' + run: make python-test + + - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5 + if: steps.changes.outputs.python == 'true' + with: + files: clients/python/coverage.xml + slug: getsentry/taskbroker + token: ${{ secrets.CODECOV_TOKEN }} + flags: python-client + test: name: Tests (ubuntu) runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index a073f777..bec43a42 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,10 @@ setup: @test -n "$$CI" || devenv sync .PHONY: setup +python-venv: ## Build Python virtual environment + uv sync --all-packages --all-groups +.PHONY: python-venv + # Builds build: ## Build all features without debug symbols @@ -38,6 +42,10 @@ unit-test: ## Run unit tests cargo test .PHONY: unit-test +python-test: ## Run Python client tests + cd clients/python && uv run pytest --cov=src/taskbroker_client --cov-report=xml --cov-report=term +.PHONY: python-test + reset-kafka: setup ## Reset kafka devservices down -docker container rm kafka-kafka-1 diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index b9063cca..0a88a474 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -25,6 +25,7 @@ dev = [ "black==24.10.0", "pre-commit>=4.2.0", "pytest>=8.3.3", + "pytest-cov>=6.0.0", "flake8>=7.3.0", "isort>=5.13.2", "mypy>=1.17.1", From 5ee97308ea87921f791b8cd697b6f07c2321867b Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 23 Jan 2026 17:33:57 -0500 Subject: [PATCH 3/5] Fix up mistakes --- Makefile | 1 + clients/python/pyproject.toml | 2 +- uv.lock | 57 +++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bec43a42..ee63db0f 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ setup: .PHONY: setup python-venv: ## Build Python virtual environment + python -m venv .venv uv sync --all-packages --all-groups .PHONY: python-venv diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index 0a88a474..becfbca2 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -25,7 +25,7 @@ dev = [ "black==24.10.0", "pre-commit>=4.2.0", "pytest>=8.3.3", - "pytest-cov>=6.0.0", + "pytest-cov>=4.0.0", "flake8>=7.3.0", "isort>=5.13.2", "mypy>=1.17.1", diff --git a/uv.lock b/uv.lock index 2381e956..b3e62668 100644 --- a/uv.lock +++ b/uv.lock @@ -101,6 +101,30 @@ wheels = [ { url = "https://pypi.devinfra.sentry.io/wheels/confluent_kafka-2.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e75230b51456de5cfaefe94c35f3de5101864d8c21518f114d5cd9dd1d7d43b1" }, ] +[[package]] +name = "coverage" +version = "7.6.4" +source = { registry = "https://pypi.devinfra.sentry.io/simple" } +wheels = [ + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c" }, + { url = "https://pypi.devinfra.sentry.io/wheels/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "(python_full_version <= '3.11' and sys_platform == 'darwin') or (python_full_version <= '3.11' and sys_platform == 'linux')" }, +] + [[package]] name = "cronsim" version = "2.6" @@ -412,6 +436,18 @@ wheels = [ { url = "https://pypi.devinfra.sentry.io/wheels/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" }, ] +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://pypi.devinfra.sentry.io/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://pypi.devinfra.sentry.io/wheels/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -621,6 +657,7 @@ dev = [ { name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pre-commit", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-cov", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentry-devenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "time-machine", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] @@ -652,6 +689,7 @@ dev = [ { name = "mypy", specifier = ">=1.17.1" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=4.0.0" }, { name = "sentry-devenv", specifier = ">=1.22.2" }, { name = "time-machine", specifier = ">=2.16.0" }, ] @@ -725,6 +763,25 @@ wheels = [ { url = "https://pypi.devinfra.sentry.io/wheels/time_machine-2.16.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:667b150fedb54acdca2a4bea5bf6da837b43e6dd12857301b48191f8803ba93f" }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.devinfra.sentry.io/simple" } +wheels = [ + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13" }, + { url = "https://pypi.devinfra.sentry.io/wheels/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281" }, +] + [[package]] name = "types-protobuf" version = "6.30.2.20250703" From b9e4082f182df33b649a4f2fd51f847dd41f2b4d Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 26 Jan 2026 09:50:10 -0500 Subject: [PATCH 4/5] Add devservices --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf6c8089..f14ef170 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,11 @@ jobs: cache-dependency-path: uv.lock install-cmd: uv sync --all-packages --all-groups --frozen --active + - name: Start devservices + if: steps.changes.outputs.python == 'true' + run: | + devservices up --mode=client + - name: Run pytest with coverage if: steps.changes.outputs.python == 'true' run: make python-test From 1acab549c036b568c5391a2bd463b1bbef8e0fb1 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 26 Jan 2026 10:01:15 -0500 Subject: [PATCH 5/5] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 691c7192..c17a20c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Editors .DS_Store +.claude # Sqlite artifacts *.sqlite @@ -13,6 +14,8 @@ **/.pytest_cache/ **/integration_tests/.tests_output/ **/.venv +clients/python/.coverage* +clients/python/coverage.xml .VERSION VERSION