Derived from the Python retooling project session, May–June 2026
These premises were accepted during the session and underpin the architecture. If any is wrong, the decisions built on it may need revisiting.
visualartsdna.org; the server is the authoritative host for all RDF content queries and HTML renderingBIND(IRI(...) AS ?subject) patterns — this is a known gap vs. Apache JenaPolicy.upd is the single authoritative source for the ontology scheme top-class list; Python parses that list rather than hardcoding itskos:ConceptScheme instances; ontology and thesaurus schemes are differentiated by the:tag the:forOntology vs the:tag the:forThesaurus, not by RDF typepolicy.py walking the rdfs:subClassOf hierarchy; thesaurus concept schemes exist in static TTL files| Concern | Choice | Rationale |
|---|---|---|
| HTTP framework | FastAPI + Uvicorn | Async support, clean routing, Claude Code compatibility |
| RDF storage | RDFLib (in-process) | Standard Python RDF library; no external triple store required |
| RDFS inferencing | owlrl | RDFS + SKOS closure over merged store |
| SKOS rules | Python policy.py (parsed from Policy.upd) |
RDFLib SPARQL UPDATE cannot handle BIND(IRI(...)) subjects; rules parsed from .upd file, executed in Python |
| Ontology/thesaurus graphs | Cytoscape.js + dagre plugin | Replaces Graphviz subprocess; browser-side rendering, no external binary dependency |
| Image processing | Pillow | On-demand GCP image fetch and thumbnail generation |
| Static assets | FastAPI StaticFiles mount |
No CDN; served directly from the main server |
| Data format | TTL (Turtle) | Source files; loaded into RDFLib graphs at startup |
| Config format | RSON (JSON + comments) | Inherited from Groovy server; parsed by stripping comment lines then json.loads() |
| Git | GitHub, MIT license | Public open-source repository |
The original Groovy system used two servers (main on port 80, function on port 8080). The Python port consolidates to a single server. The function server's role in production was solely to handle SPARQL browser queries in an isolated process with a limited memory footprint. That risk is mitigated in Python by three controls applied to the built-in SPARQL browser endpoint:
concurrent.futures thread wrapperLIMIT 10000 appended to any incoming query lacking an explicit LIMITThe function server architecture is preserved in Policy.upd as documentation and for portability back to Jena, but is not deployed.
Single Uvicorn process, async throughout. Blocking operations (RDFLib queries, GCP fetches, DBMgr reload) are dispatched via asyncio.to_thread() to avoid blocking the event loop.
| Store | Contents |
|---|---|
instances |
Work instance TTL files (data/) |
tags |
Concept tagging TTL files (tags/) |
vocab |
Vocabulary TTL files (vocab/) |
data |
Merged: instances + tags + vocab |
schema |
Ontology model TTL files (model/) |
rdfs |
Merged all stores + owlrl RDFS + SKOS inference applied; primary query target |
data; schema loaded separatelyrdfs storeskos:Collection instances created; ontology concept schemes built by walking rdfs:subClassOf hierarchy from top-level classes (list parsed from Policy.upd)rdfs store (pyshacl, advanced=True)/refresh endpoint triggers a full DBMgr rebuild (re-sync from GCP + full RDFLib reload). The rebuild runs in a thread to keep the server responsive. Protected by a load lock file to prevent concurrent reloads. Also available via /cmd?cmd=refresh with token authentication.
Two-tier routing inherited from the Groovy servlet structure, flattened into FastAPI route handlers in servlet.py and servlet_base.py.
servlet.py)| Path | Handler |
|---|---|
/ |
Gallery (paginated, filterable) |
/work/{guid} |
Browser/detail page |
/model/{path} |
Ontology resource; format negotiation (ttl, rdf/xml, n3, jsonld, n-triples) |
/thesaurus/{path} |
Thesaurus resource; format negotiation |
/browseSort, /browseFilter |
Gallery sort/filter controls |
/vocabTree |
SKOS vocabulary tree (JSON) |
/agentClient |
Ask page (AI agent proxy) |
/sparql |
Built-in SPARQL browser |
servlet_base.py)| Path | Handler |
|---|---|
/sparqlEndpoint |
Internal SPARQL API (POST + GET), returns JSON |
/explore/* |
Ontology/thesaurus graph data, Concept Collections, Data Visualizations |
/explore/graph-data |
Cytoscape node/edge JSON from SPARQL against rdfs store |
/cmd |
Token-authenticated admin command endpoint |
/refresh |
Full data reload |
/cestfini |
Flush metrics to stdout, graceful shutdown |
/status/os |
OS-level status (token-authenticated) |
/metrics |
Server metrics (JSON) |
/thumbnails/* |
On-demand thumbnail serving (excluded from metrics) |
/images/*, /documents/*, /dist/*, /html/* |
Static assets (FastAPI StaticFiles) |
servletPolicy.rson is loaded at startup. For the main server the policy is effectively open (".*" pattern matches all paths). The gate is implemented faithfully but does not block any requests in normal operation.
Browser page rendering — JSON-LD intermediate serialization from the Groovy JsonLd2Html is replaced with direct RDFLib graph traversal in rdf2html.py. The property/value dict is built directly from the graph; no intermediate serialization. Avoids the JSON-LD 1.0 vs 1.1 migration issue entirely.
SPARQL query engine — all operational queries (gallery, browser page, explore, vocab tree) run against the in-process rdfs RDFLib graph. No connection to an external triple store for operational queries.
URI rehosting — schema: and vad: URIs containing http://visualartsdna.org are rewritten to cfg["host"] at query/render time, enabling local dev and production to use the same TTL files.
On-demand GCP image fetch — images and documents are not bulk-downloaded at startup. Individual files are fetched from the GCP bucket on first request and cached locally. Thumbnails are generated at fetch time via Pillow. Stale local files are evicted on /refresh.
Ontology graph scheme disambiguation — the:tag the:forOntology / the:tag the:forThesaurus on skos:ConceptScheme instances is the correct and only mechanism for separating the two select lists on the Explore page. Both scheme types are skos:ConceptScheme; RDF type is not a reliable discriminator.
RDFLib ORDER BY on dateTime — str(?dt) cast is required for correct chronological sorting. Without the cast, timezone-aware and timezone-naive xsd:dateTime literals sort incorrectly.
WSL2 networking — all generated HTML uses root-relative URLs (/images/..., /dist/...). Absolute URLs using localhost or the WSL2 internal IP break when the server is accessed by IP from another device on the LAN.
const redeclaration — gallery JavaScript must use let for variables declared in loops or conditional blocks. const redeclaration across script inclusion points causes a SyntaxError in strict-mode browsers.
Logging — normal operational output → stdout. Error and exception output → stderr. Both streams are captured to separate log files at process startup.
Development: WSL2 (Ubuntu) on Windows. Server runs on a WSL2-internal IP (172.29.x.x); LAN access from other devices requires a Windows port proxy (netsh portproxy). The WSL2 IP changes on each restart, so the proxy must be refreshed each session. The Groovy function server (retained for comparison) occupies port 8080; the Python server runs on a different port (e.g. 8081) in dev.
Production target: GCP Debian instance. Single server process, single port. The agent server (cwva_agent, separate process) runs on the same instance on port 8090; agentUrl in the rson config points to localhost:8090. No port proxy or firewall complexity — co-located processes communicate on loopback.
Content repositories:
cwvaServer-py — server code (GitHub, MIT)cwvaMetacontent — ontology model and vocabulary TTL files (separate repo)cwvaContent — user content TTL files; images and documents excluded via .gitignore| Groovy | Python | Notes |
|---|---|---|
Main.groovy |
main.py |
Startup, config load, Uvicorn launch |
Server.groovy |
server.py |
Singleton; rehost(), log_out(), DBMgr reference |
Servlet.groovy |
servlet.py |
Primary route handlers |
ServletBase.groovy |
servlet_base.py |
Base routes, static serving, admin endpoints |
JsonLd2Html.groovy |
rdf2html.py |
Direct RDFLib graph → HTML; no JSON-LD intermediate |
DBMgr.groovy |
db_mgr.py |
Store loading, GCP sync, owlrl inference |
QuerySupport.groovy |
query_support.py |
SPARQL query wrappers against rdfs store |
Policy.groovy |
policy.py |
Post-load policy; parses ontology scheme list from Policy.upd |
Gcp.groovy |
util/gcp.py |
GCP bucket sync, on-demand fetch, thumbnail eviction |
| (new) | sparql_browser.py |
Built-in SPARQL browser with timeout + row cap |
| (new) | metrics.py |
Request metrics middleware |
| (new) | metricsCompiler.py |
Standalone metrics aggregation (replaces RDFLib-based approach) |