Skip to content

feat(connector): DCAT-AP 3.0 transformer implementation - dataset/series building, license & language resolution, dual-format caching - #208

Open
Vishmayraj wants to merge 72 commits into
istSOS:mainfrom
Vishmayraj:week8-dcat-prep
Open

Vishmayraj wants to merge 72 commits into
istSOS:mainfrom
Vishmayraj:week8-dcat-prep

Conversation

@Vishmayraj

Copy link
Copy Markdown
Contributor

** Depends on #195 and #204.** This branch builds directly on the connector package introduced in #195 and the network-as-subcatalog architecture from #204; thus, pull/merge those first, or this won't apply cleanly on its own.

Summary

This PR implements dcat_transformer.py, the DCAT-AP 3.0 counterpart to the STAC transformer from #195, scoped and stubbed in #204. It builds SHACL-conformant dcat:Catalog / dcat:DatasetSeries / dcat:Dataset / dcat:Distribution graphs from the same harvested Postgres data STAC consumes, wires the DCAT side of the master-switch system, adds license/language URI resolution, and serves the result through Redis-cached JSON-LD and Turtle endpoints.

This closes out the DCAT-AP mapping work scoped as "next PR" in #204 (weeks 8–10, wrapped in the first half of week 10); automated test coverage for the connector package is next.


What's in this PR

DCAT-AP 3.0 transformer (dcat_transformer.py)

  • Same pivot rule as STAC: Datastream -> dcat:Dataset, Thing -> dcat:DatasetSeries, STA root -> dual-typed dcat:Catalog + dcat:DataService, Network -> dcat:Catalog sub-catalog.
  • build_dcat_catalog() for the single-scope (NETWORK=0) case and build_dcat_catalog_with_networks() for the scoped case, returning one independent rdflib.Graph per scope (root, orphan, per-Network) rather than a merged quad store, so each scope serializes and caches on its own.
  • Root graph under NETWORK=1 is deliberately structural-only (Catalog + DataService + Agents + dct:hasPart), with orphan and each Network carrying their own Dataset/DatasetSeries content.
  • dcat:Dataset nodes carry spatial (dct:spatial -> dct:Location -> locn:Geometry -> geosparql:asGeoJSON), temporal (dct:temporal -> dct:PeriodOfTime), theme/subject, license, access rights, contact point (vcard), and three dcat:Distributions per Dataset mirroring STAC's Item Assets (JSON observations, CSV export, Datastream entity metadata).
  • dcat:DatasetSeries extent (bbox/temporal) is computed per-scope from its own successfully-built Datasets, with a fallback to the Thing's own Location geometry, same fallback chain as STAC's Collection extent, kept independent per scope.
  • DatasetSeries is deliberately not linked via dcat:dataset (DCAT-AP has no clean "Catalog contains this Series" predicate because of an open gap, see DatasetSeries subClassOf Resource instead of Dataset SEMICeu/DCAT-AP#289); still, reachability is preserved one hop either direction via dcat:inSeries/dct:isPartOf.

License & language resolution (config.py)

  • resolve_license_uri(): normalizes a license value (URI passthrough, SPDX-id lookup via SPDX_LICENSE_URIS, or best-effort spdx.org guess with a warning) into something safe to wrap in URIRef, shared by every dct:license triple across catalog, dataset, and distribution nodes.
  • resolve_language_uri(): maps a BCP-47 tag to its EU Publications Office NAL URI (EU_LANGUAGE_AUTHORITY_URIS) for dct:language, since DCAT-AP 3.0 requires a skos:Concept/dct:LinguisticSystem URI here, not a bare literal; thus, distinct from the lang= tag used on dct:title/dct:description/dcat:keyword literals, which is untouched.
  • Both are module-level functions (not Settings methods) so dcat_transformer.py can import them directly without needing an instance.

Master-switch system

  • STAC_TRANSFORMER / DCAT_TRANSFORMER env-var flags, parsed once at import time in config.py via a dedicated _env_flag() helper (explicit "1" opt-in, not truthy-string coercion) so both the scheduler and api.py read the same parsed value instead of two independently-parsed copies drifting apart.
  • api.py gates every DCAT route behind @_require_enabled(DCAT_TRANSFORMER, "DCAT_TRANSFORMER"), returning a 404 with an explicit "set this env var to enable it" message, distinct from the 503 used for "enabled but not harvested yet."

Dual-format caching (cache.py)

  • Each DCAT scope (root / orphan / per-Network) is now cached twice: once as Turtle, once as JSON-LD, under sibling dcat:graph:{scope} / dcat:graph:{scope}:jsonld keys using one Graph.serialize() call per format at write time, so reads are a flat Redis GET, never a per-request re-serialization.
  • write_dcat_catalog() / write_dcat_catalog_with_networks() purge stale dcat:* keys before writing (same SCAN-then-delete pattern as the STAC cache writer), so a reader hitting mid-write sees a temporary miss rather than a mixed old/new graph.
  • DCAT availability/last-fetch/network-id metadata tracked independently of STAC's (dcat:meta:* vs stac:meta:*), so one transformer failing doesn't get reported as if both did.

API layer (api.py)

  • New /connector/dcat/root, /connector/dcat/orphan, /connector/dcat/{network_id} routes, each with a .ttl sibling for Turtle whilst JSON-LD is the unsuffixed default as the more broadly consumed format.
  • Static /dcat/root and /dcat/orphan routes are registered ahead of the dynamic /dcat/{network_id} route so the path parameter can't swallow them.
  • /connector root now reports dcat_enabled, dcat_mandatory_fields_set, and DCAT availability/last-fetch alongside the existing STAC fields.

Compliance fixes (last few commits)

  • dct:language corrected to point at a URI (EU NAL concept), not a literal string.
  • Added missing skos:Concept typing and several other RDF-shape fixes (rdfs:Resource on accessURL/endpointURL targets, dct:Standard typing on conformsTo targets, dct:LicenseDocument/dct:RightsStatement/dct:LinguisticSystem typing) surfaced by running the output against the official DCAT-AP SHACL shapes.
  • Added modular serialization: JSON-LD as the primary/default format, Turtle available as a secondary, on every cached scope.

Notes

  • Ran the generated graphs through pyshacl against the official DCAT-AP 3.0 SHACL shapes end-to-end - all scopes (root, orphan, per-Network) validate clean with zero violations.
  • Automated test coverage for the connector package (both STAC and DCAT sides) is the immediate next step, picked up right after this PR.

Testing

Validated against the dummy_data module's generated dataset, both under NETWORK=0 and NETWORK=1. RDFLib parse-back and pyshacl SHACL validation confirm zero dangling links and full DCAT-AP 3.0 conformance across all generated scopes.

…but using a seperate connection for lock and harvest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant