cwvaServer-py: Architecture Reference

Derived from the Python retooling project session, May–June 2026


Assumptions

These premises were accepted during the session and underpin the architecture. If any is wrong, the decisions built on it may need revisiting.


Technology Stack

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

Server Architecture

Single-Server Design

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:

The function server architecture is preserved in Policy.upd as documentation and for portability back to Jena, but is not deployed.

Process Model

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.

Data Stores (in-memory RDFLib graphs)

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

Load Sequence

  1. GCP bucket sync — TTL files downloaded to local folders
  2. Discrete stores loaded from TTL folders
  3. Stores merged → data; schema loaded separately
  4. owlrl RDFS closure applied → rdfs store
  5. Policy executed: license/copyright appended per work; skos:Collection instances created; ontology concept schemes built by walking rdfs:subClassOf hierarchy from top-level classes (list parsed from Policy.upd)
  6. SHACL validation run against rdfs store (pyshacl, advanced=True)
  7. Load lock file released

Refresh

/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.


Routing Structure

Two-tier routing inherited from the Groovy servlet structure, flattened into FastAPI route handlers in servlet.py and servlet_base.py.

Primary Routes (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

Base Routes (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)

Policy Gate

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.


Key Implementation Decisions

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 rehostingschema: 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 disambiguationthe: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 dateTimestr(?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.


Deployment Environment

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:


Module Map (Groovy → Python)

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)



visualartsdna@gmail.com
Copyright © 2026 visualartsdna.org. All Rights Reserved.
v 5.0.0