Releases: dhis2-chap/servicekit
Release list
v2.0.2
Patch release completing the RFC 9457 error surface. No breaking changes.
Fixed
- FastAPI request validation failures (a malformed request body, a missing required field, or an out-of-range query parameter such as
?page=0on a list route) were still returned as FastAPI's default{"detail": [...]}body withapplication/json, because onlypydantic.ValidationErrorwas routed through the Problem Details handler.RequestValidationErrornow produces the same422 application/problem+jsonresponse as other validation errors: genericdetail, atrace_id, and the structured error list under theerrorsextension withloc,type, andmsgpreserved.
Upgrading
Drop-in for 2.0.x users. Clients that parsed the top-level detail list of a 422 should read the errors member of the Problem Details body instead; the entries are unchanged.
What's Changed
- fix(api): return Problem Details for FastAPI request validation errors by @mortenoh in #40
- chore: release 2.0.2 by @mortenoh in #41
Full Changelog: v2.0.1...v2.0.2
v2.0.1
Patch release fixing issues found while verifying 2.0.0 end to end with chapkit. No new breaking changes.
Fixed
BaseManager.create()reported every database integrity error as a duplicate id, producing409 "Entity with id None already exists"for a foreign-key violation such as an unknownparent_id. It now re-checks for a real duplicate and otherwise returns409with the detailEntity violates a database constraintand, when it can be derived safely from the driver error, aconstraintextension (foreign_key,unique,not_null,check). The SQL statement never reaches the response. The database error handler in the API layer classifiesIntegrityErrorthe same way through the newservicekit.classify_integrity_errorhelper.- The job router (
/api/v1/jobs) returned plain{"detail": ...}bodies; it now raises servicekit exceptions, so its errors are RFC 9457 Problem Details like the rest of the API. A malformed job id is now400(it was404), matching the CRUD routes.
Added
409is documented in OpenAPI for the CRUD create route and404for the single-entity routes, both with theProblemDetailschema.JobOptionsis a public export ofservicekit.api;ServiceBuilder._create_scheduler(job_options: JobOptions)is the supported hook for subclasses that supply their own scheduler. The private_JobOptionsname remains as an alias.
Upgrading
Drop-in for 2.0.0 users. Clients that matched the exact detail string of a create-time conflict should match on the 409 status instead; the detail now differs between a duplicate id and another constraint violation.
What's Changed
- fix: map create-time integrity errors to accurate conflict details by @mortenoh in #37
- chore: build GitHub release notes from the annotated tag message by @mortenoh in #38
- chore: release 2.0.1 by @mortenoh in #39
Full Changelog: v2.0.0...v2.0.1
v2.0.0
This release addresses the findings of the September 2026 project review. Several fixes change public behavior, so the major version is bumped. Downstream projects, chapkit in particular, need coordinated updates before moving their pin.
Breaking changes
- Dependencies are scoped to the application.
get_database,get_scheduler, andget_app_managernow take the request and readrequest.app.state. The module-levelset_database,set_scheduler, andset_app_managerfunctions are removed. Code running inside the lifespan should capture the objects it needs instead of calling a global getter (#34). - POST is create-only. CRUD routers call the new
Manager.create(), which raisesConflictError(HTTP 409) when the supplied id already exists.save()keeps its upsert semantics for library callers.createis a new abstract method on theManagerprotocol;flushandrollbackare new abstract methods onRepository(#31). - Explicit nulls clear nullable fields.
save,save_all, andcreatedump input withexclude_unset=Trueinstead ofexclude_none=True. Omitted fields are left alone; an explicitnullis assigned. PUT preserves the set of fields the client sent (#31). - Pagination bounds are enforced. List endpoints validate
page >= 1and1 <= size <= 100at the request boundary and return 422 otherwise. Listings are ordered by id (#31). - Health returns 503 when unhealthy.
GET /healthsets HTTP 503 for anunhealthyaggregate state;degradedandhealthyreturn 200. Probes and the registration readiness check now fail for a broken service (#33). - Database errors no longer leak SQL. SQLAlchemy errors return an RFC 9457 Problem Details body with a generic detail and a
trace_id; the full error is logged with the same id.IntegrityErrormaps to 409, other errors to 500. The previous{"detail": ..., "error": ...}body is gone. Request validation errors use the same shape with a structurederrorsextension (#33). - An empty auth allowlist is honored.
with_auth(unauthenticated_paths=[])protects every path. OnlyNoneselects the defaults (/,/docs,/redoc,/openapi.json,/health) (#33). - Scheduler API changes.
JobStatus.cancelingis new: cancelling a synchronous job reportscancelinguntil its thread finishes, and the capacity slot is held until then.Scheduler.shutdown(timeout=...)is a new abstract method.set_max_concurrencyvalidates its argument (Noneor at least 1) and its parameter is renamed tomax_concurrency.max_concurrencymust be at least 1 when set (#34, #35). - Keepalive uses a handle.
start_keepaliverequiresservice_idand returns aKeepaliveHandle;stop_keepalive(handle)takes it. The module globals are removed (#34). fail_on_error=Trueshuts the process down. A failed deferred registration or a readiness timeout marks the service unhealthy, logs at critical level, and raises SIGTERM so the server shuts down gracefully. Startup itself is never aborted (#34).make lintis check-only. Usemake formatto apply ruff formatting and fixes (#32).
Added
- Migrations ship inside the package at
servicekit/alembic;get_alembic_dir()returns the path. A rootalembic.iniplusmake migrateandmake upgradetargets support the checkout. A warning is logged when the bundled migration runs for an application that defines its own tables (#30). with_jobs(shutdown_timeout=...)andServiceBuilder._create_scheduler()for subclasses that supply their own scheduler (#34).InMemoryScheduler._make_record()and_on_job_result()extension hooks, so subclasses no longer copyadd_job(#35).EntryStaticFilesserves the manifestentryfile at an app's mount root (#33).- Custom
ServicekitExceptionextensions are included in Problem Details responses; reserved member names are dropped with a warning (#33). - A built-in
registrationhealth check when registration is configured (#34). - Wheel smoke test and Docker example builds in CI (#30, #32).
Fixed
- Installed file-based databases could not find the default migrations (#30).
- Keepalive recovery after a 404 never re-registered because the service info was serialized to an empty dict (#34).
- Cancelling a queued job left it
pendingforever, and cancellation errors reached the event loop exception handler (#35). - Changing
max_concurrencyat runtime created an independent semaphore (#35). - Bulk-save hooks could not see earlier new entities in the same batch, and a repeated explicit id in one batch raised instead of upserting (#31).
- Application shutdown left scheduled jobs running; startup and shutdown hook errors skipped cleanup (#34).
setup_monitoringreturned a detached Prometheus reader on repeated calls (#34).- SSE
poll_intervalquery parameters are bounded to(0, 60](#35). - The registration example Dockerfile could not build from a clean checkout (#32).
- Stale Vega tests, scheduler guide endpoints, and README class names (#32).
What's Changed
- chore(deps): bump actions/checkout from 6 to 7 in the github-actions group by @dependabot[bot] in #23
- chore(deps): bump the python-dependencies group across 1 directory with 2 updates by @dependabot[bot] in #24
- chore(deps): bump actions/setup-python from 6 to 7 in the github-actions group by @dependabot[bot] in #28
- chore(deps): bump the python-dependencies group across 1 directory with 12 updates by @dependabot[bot] in #29
- fix: ship Alembic migrations inside the servicekit package by @mortenoh in #30
- fix: create-only POST, explicit-null updates, batch flush, bounded pagination by @mortenoh in #31
- chore: fix registration example build, make lint check-only, refresh stale docs by @mortenoh in #32
- fix: harden auth allowlist, error responses, health status codes and app entry by @mortenoh in #33
- refactor: scope database, scheduler, and lifecycle state to each application by @mortenoh in #34
- fix: correct job cancellation, sync capacity accounting, and concurrency resizing by @mortenoh in #35
- chore: release 2.0.0 by @mortenoh in #36
Full Changelog: v1.0.1...v2.0.0
v1.0.1
Fixes
- Self-registration port now follows the bind port.
run_appexports its resolved port asSERVICEKIT_PORTbefore starting uvicorn, so service registration advertises and probes the same port the app actually binds. Previously a non-defaultrun_appport (e.g. 9090) left registration pointing at 8000 — timing out, or worse, registering an unrelated service squatting on 8000. An explicitly setSERVICEKIT_PORT(e.g. an externally advertised port behind a proxy) remains authoritative. (#25)
Full changelog: v1.0.0...v1.0.1
v1.0.0
v0.12.1
What's Changed
- chore(deps): bump the github-actions group with 4 updates by @dependabot[bot] in #14
- chore(deps): bump the python-dependencies group across 1 directory with 4 updates by @dependabot[bot] in #16
- chore(deps): bump locked dependencies by @mortenoh in #17
- chore(deps): add httpx2 for starlette TestClient by @mortenoh in #18
- chore(deps): bump codecov/codecov-action from 6 to 7 in the github-actions group by @dependabot[bot] in #19
- chore: update repository references to dhis2-chap org by @mortenoh in #20
- docs: left sidebar nav, author email, release 0.12.1 by @mortenoh in #21
New Contributors
- @dependabot[bot] made their first contribution in #14
Full Changelog: v0.12.0...v0.12.1
v0.12.0
v0.11.0
What's Changed
Dependency Updates (#13)
- fastapi[standard] 0.121.2 → 0.136.3
- starlette 0.49.3 → 1.1.0 (new explicit floor
starlette>=1.0.1, transitive via fastapi) - Transitive bumps via
uv lock --upgrade: pyright, mypy, ruff, pydantic, sqlalchemy, opentelemetry, uvicorn, typer, websockets, and others
Internal
- Switched
Database.sessionandBaseServiceBuilderlifespan return types fromAsyncIteratortoAsyncGeneratorto satisfy pyright 1.1.409's new deprecation check on@asynccontextmanager-decorated functions
Full Changelog: v0.10.0...v0.11.0
v0.10.0
What's Changed
Bug Fixes
- Re-register service when keepalive ping returns 404 (#11) -- When the orchestrator loses track of a service (e.g. after restart), the keepalive loop now automatically re-registers instead of silently failing.
- Defer service registration until app is serving requests (#12) -- Registration now runs in a background task that waits for the app to be fully ready before announcing to the orchestrator. Fixes the race condition where the orchestrator calls back to fetch configs before uvicorn is accepting connections.
Details on deferred registration (#12)
- Registration always defers to a background task (both
fail_on_error=TrueandFalse) - Readiness check uses the actual health endpoint path from the builder; falls back to TCP connect check when no health endpoint is configured
- Registration aborts if the app never becomes ready
- Registration call is shielded from task cancellation for proper cleanup on shutdown
- Registration state stored on
app.stateinstead of module globals
Full Changelog: v0.8.2...v0.10.0
v0.8.2
Bug Fixes
- fix: Respect
SERVICEKIT_HOSTenv var in service registration -- the env var was previously ignored becausesocket.gethostname()auto-detection always succeeded first. Resolution order changed fromparameter -> auto-detect -> env vartoparameter -> env var -> auto-detect, matching howSERVICEKIT_PORTalready works.
Chores
- Widen
uv_buildversion range to include 0.11 - Remove deprecated license classifier in favor of PEP 639
project.license - Update lockfile