Architecture¶
The CPRMV API is a single-process FastAPI application. All logic lives in serve_api/src/serve.py; constants and parameter definitions are in serve_api/src/utils/constants.py.
Request lifecycle¶
sequenceDiagram
participant Client
participant FastAPI as FastAPI (serve.py)
participant KG as Methods KG (rdflib Graph)
participant Repo as Publication Repository
participant XSLT as lxml XSLT
Client->>FastAPI: GET /rules/{rule_id_path}
FastAPI->>FastAPI: parse rule set ID from path
FastAPI->>KG: detect_cprmv_serve_method(rulesetid)
KG-->>FastAPI: method config (id formats, URLs, XSLT path)
alt index == 'latest' or no date
FastAPI->>Repo: SRU search (zoekservice.overheid.nl)
Repo-->>FastAPI: publication URL + date
end
FastAPI->>Repo: download publication XML
Repo-->>FastAPI: XML bytes (temp file)
FastAPI->>XSLT: transform XML → CPRMV Turtle
XSLT-->>FastAPI: Turtle string
FastAPI->>FastAPI: rdflib.parse(turtle)
FastAPI->>FastAPI: get_first_rule_by_id_path(graph, path)
FastAPI->>FastAPI: get_rule(graph, rule_node) or cbd.serialize()
FastAPI-->>Client: response (cprmv-json or RDF)
Startup¶
On startup, serve.py calls set_CPRMVMETHODSKG twice:
set_CPRMVMETHODSKG("./data/cprmvmethods.ttl", False)— loads the methods registry fresh into a new graph.set_CPRMVMETHODSKG("./data/cprmv.ttl", True)— merges the CPRMV vocabulary into the same graph.
The resulting merged graph is stored in app.cprmvsettings["CPRMVMETHODS_KG"] and used for all method detection at request time.
Method detection¶
detect_cprmv_serve_method(cprmvid) first tries to match the rule set identifier against cprmvmethods:analysismethods (the RDF list of BWB, CVDR, CELLAR entries). If no match, it falls back to cprmvmethods:formalisationmethods (the DMN 1.3 entry).
detect_cprmv_serve_acknowledged_method(cprmvid, collection) walks the RDF list and for each method:
- Reads
cprmv-serve:normalized-id-formatfrom the methods KG. - Calls
normalize_ruleset_id(cprmvid)to canonicalise shortened IDs (appends_now_latestor_na_latestfor forms without date/index). - Uses
parse(normalized_idformat, cprmvid_normalized)to attempt structural matching. - On match, returns a dict of the method's triples plus a
cprmv-serve:id_partsentry containing the named parse result fields.
For _latest index forms, a subsequent SRU call (find_publication_version_valid_on_date) resolves the actual date and index.
Rule graph traversal¶
get_first_rule_by_id_path splits the path by the delimiter and pops the first segment to find root rule nodes via g.subjects(CPRMV.id, Literal(firstid, lang="nl")). It then calls the recursive get_first_rule_by_Id_path_split_from_rule_as_root, which:
- Iterates the
cprmv:hasPartRDF list of the current rule. - Strips non-alphanumeric characters from both the path segment and
g.value(rule, CPRMV.id). - On match, recurses with the remaining path.
- If no direct match at this level, still recurses into sub-rules (depth-first skip — intermediate IDs can be omitted).
get_rule(g, rule) recursively collects all predicate-object pairs for a rule node. For cprmv:hasPart, it walks the RDF list and recurses into each item, building a nested dict keyed by cprmv:id.
Static files¶
The /respec/ path is served as a static file directory mounted from serve_api/respec/. The ReSpec HTML and assets are included in the Docker image at build time.
Locale¶
The Dockerfile installs nl_NL.UTF-8 locale, which serve.py sets at startup. This is intended to influence the parse module's handling of Dutch-formatted strings (e.g. decimal notation), though the practical effect is noted as a TODO in the source.