All documentation Download (Markdown) Technical

Termbank · termbank.pdhc

Technical manual


termbank.pdhc — Technical description

Bulk canonical terminology store for the PDHC platform. Read-only over
HTTP; mutations happen only through the import CLIs. This document
describes the service as it is deployed today (read API + seven
importers, FHIR conformance hardened under rollups #347 / #374).

Role in PDHC

termbank.pdhc imports upstream vocabularies as-is and serves them
read-only. It owns the canonical URI namespace
https://termbank.pdhc.se/CodeSystem/<system>/<code> (Rule 27). No
other service mints canonical URIs: plan.pdhc mirrors the subset it
curates into its working set, rosetta.pdhc stores mappings that point
at these URIs, and downstream data services (cdr.pdhc, forms.pdhc,
1177.pdhc, request.pdhc) reference them.

Imported vocabularies (seven importers, one system each):

system Importer script Source
loinc scripts/import_loinc.py LOINC release table
snomed scripts/import_snomed.py SNOMED CT Managed Service SE (RF2)
icd10 scripts/import_icd10.py ICD-10-SE
atc scripts/import_atc.py ATC (Läkemedelsverket / SeNSL)
kva scripts/import_kva.py KVA medical (KMA) + surgical (KKA) procedures
socialstyrelsen scripts/import_sosstyr.py Socialstyrelsen termbank (full XML)
icf scripts/import_icf.py ICF

There is no ICD-11 importer.

FHIR conformance scope

Deliberately narrow (Rule 15): termbank implements CodeSystem
$lookup
plus a /fhir/metadata CapabilityStatement, and nothing
else FHIR-shaped. ValueSet + $expand / $validate-code, ConceptMap +
$translate, and TerminologyCapabilities are out of scope by design
— that curated terminology layer lives in plan.pdhc. See
docs/decisions/ADR-001-scope-rule15.md.

Data model

Five tables (app/models/__init__.py):

ConceptDesignation and ConceptProperty cascade-delete with their
parent concept.

HTTP API

Routes live in app/api/terminology.py; the app factory and read-only
guard are in app/__init__.py.

GET /fhir/metadata

FHIR R5 CapabilityStatement. Advertises CodeSystem $lookup as the
headline operation, plus the shortcut/auxiliary routes and the
termbank-search OperationDefinition. CapabilityStatement.date is
derived from the module file's mtime (not datetime.now()) so it is
identical across gunicorn workers and only advances on an image rebuild
(finding §1.4 / #352).

GET|POST /fhir/CodeSystem/$lookup (and /fhir/CodeSystem/%24lookup)

The primary operation. Parameters:
- system (required), code (required)
- displayLanguage (optional)

On GET these are query params; on POST they come from a FHIR
Parameters request body (_extract_from_parameters_body, accepting
valueUri / valueUrl / valueCode / valueString). The %24
(URL-escaped $) path variant is registered for clients that emit it.
Missing system/code400 OperationOutcome; no match → 404.
Success → a FHIR Parameters resource.

GET /CodeSystem/<system>/<code>

Convenience path equivalent to $lookup. <code> is captured with a
<path:...> converter so codes containing / work. Supports
displayLanguage. Same Parameters response / 404 behaviour.

GET /Concept?uri=<canonical_uri>

Lookup by canonical URI. The URI is a query parameter (?uri=...),
not a path segment. Missing uri400; no match → 404. Supports
displayLanguage. Same Parameters response.

GET /search?q=&system=&limit=&displayLanguage=

Fuzzy, ranked search across Concept.display and designation values in
a single query. q required (empty → empty result set). system
repeatable or comma-separated. limit default 20, clamped to 1..200.
Rank tiers: exact code/display/designation match (1) → display prefix
(2) → preferred-designation prefix (3) → display/designation contains
(4) → designation contains (5); ties broken by active status, then
display length, then canonical URI. Response is plain JSON
({query, systems, system, count, results[]}), not a FHIR Bundle.

GET /health

DB reachability plus a per-source version/staleness summary. Returns
200 / status: "ok" when SELECT 1 succeeds, 503 /
status: "degraded" otherwise (CLAUDE.md §10 shape). Each source row
carries version, concept_count, the pinned <SOURCE>_VERSION env
value, and a stale flag when the pin and the loaded version differ.
version reports TERMBANK_GIT_SHA (default dev).

Response shapes

Read-only enforcement (Rule 6)

A before_request hook returns 405 (OperationOutcome, code
not-supported) for any POST/PUT/PATCH/DELETE, except the
two $lookup POST endpoints (/fhir/CodeSystem/$lookup and
/fhir/CodeSystem/%24lookup), which are reads with no side effects.
GET/HEAD/OPTIONS always pass.

Import CLIs

Each importer parses upstream, builds concepts + designations +
properties, and writes them through the shared importer_base.py
upsert. Every importer requires --source-url and --version;
--operator and --batch-size (default 2000) are optional everywhere.
File-input flags differ by source:

Importer Required file flag(s)
import_loinc.py --local-file
import_icd10.py --local-file
import_atc.py --local-file
import_icf.py --local-file
import_snomed.py --release-dir (RF2 release folder)
import_kva.py --kma-file and/or --kka-file (at least one)
import_sosstyr.py --local-file optional (downloads from --source-url otherwise)

There are no --release-csv / --release-tsv flags. Idempotency
(Rule 27): re-running with an already-imported --version is a no-op; a
new --version updates concepts in place while preserving each
canonical_uri.

Example invocations are in docs/user_manual.md.

Configuration & deployment

Tests

app/tests/ covers the read API, health, models, per-importer parsing,
and a test_capability_truth.py conformance check that keeps the
CapabilityStatement honest against the routes actually registered. The
Postgres-only search ranking (func.least) is deselected on SQLite via
make test (-k "not search"); make test-all runs everything against
Postgres. make corpus / make conformance emit and validate $lookup
+ CapabilityStatement samples against the HL7 R5 validator.

Port Allocation

All ports bind to 127.0.0.1 (loopback only); external traffic arrives
via the reverse proxy.

Port Service
9012 Flask application (Gunicorn)
9011 PostgreSQL database