pdssp-prov-toolkit 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdssp_prov_toolkit
3
+ Version: 1.0.1
4
+ Summary: Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
5
+ Author: Jean-Christophe Malapert
6
+ Author-email: Jean-Christophe Malapert <jean-christophe.malapert@cnes.fr>
7
+ License-Expression: Apache-2.0
8
+ Requires-Dist: prov>=3.1.0
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
12
+ # PDSSP Prov Toolkit
13
+
14
+ [![image](https://img.shields.io/badge/Maintained%3F-yes-green.svg)]()
15
+
16
+ ![image]()
17
+
18
+ Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
19
+
20
+ ## Why this exists
21
+
22
+ Two PDSSP services (`ode_stac_proxy`, a live STAC proxy, and
23
+ `geocoding-api`, an offline GeoPackage/OpenSearch pipeline) each expose a
24
+ `GET /prov` endpoint describing how their own FAIR-transformed data is
25
+ produced, as a W3C PROV-DM document. Both were built independently, before
26
+ this package existed — and by the time it was extracted, each had grown
27
+ its own, already-drifted copy of the same vocabulary and rendering code:
28
+
29
+ - `geocoding-api` had added Subresource Integrity hashes pinning its
30
+ Viz.js CDN scripts (a real security hardening) and `wasInformedBy`/
31
+ `license`/`crs` support — none of which had made it back into
32
+ `ode_stac_proxy`.
33
+ - `ode_stac_proxy` had added `hadMember`/`hadPlan` support — which had
34
+ never made it into `geocoding-api`.
35
+
36
+ Neither service noticed the other's fixes, because there was nothing to
37
+ notice — it was two copies, not one dependency. This package is that one
38
+ dependency: the union of both, so a fix or an addition made once benefits
39
+ every PDSSP service producing FAIR-transformation provenance, present or
40
+ future.
41
+
42
+ **What deliberately did *not* move here**: each service's own
43
+ document-assembly logic — *which* activities and entities its own
44
+ pipeline has, and how they connect — stays in that service. A STAC proxy
45
+ harvesting one live upstream and a batch pipeline harvesting several
46
+ static sources into per-body GeoPackages don't share a pipeline shape, so
47
+ forcing one here would fit neither well. What both actually needed
48
+ identically is the vocabulary they build records with and the code that
49
+ turns the finished document into a graph — that's the whole scope of this
50
+ package.
51
+
52
+ ## Relationship to `prov`'s own `prov.dot`
53
+
54
+ This package depends on [`prov`](https://prov.readthedocs.io/), and `prov`
55
+ already ships its own optional Graphviz export
56
+ (`prov.dot.prov_to_dot`, via the `prov[dot]` extra) — doing, on paper, the
57
+ same job as `pdssp_prov_toolkit.dot`. They even use the *same* node
58
+ colours (`#FFFC87`/`#9FB1FC`/`#FED37F`), because both independently follow
59
+ the same W3C PROV Primer / ProvToolbox convention. So why not just use it?
60
+
61
+ | | `prov[dot]` | `pdssp_prov_toolkit.dot` |
62
+ |---|---|---|
63
+ | Extra dependency | `pydot` + `pyparsing` | none (plain string templating) |
64
+ | Long label/URL wrapping | no — a long value renders as one unbroken line | yes — this is *why* this module exists: an unwrapped long location once made a real graph too wide to read |
65
+ | Relation vocabulary | every PROV-DM relation, n-ary relations, nested bundles, attribute-annotation nodes | exactly the relations PDSSP's own document builders emit (Generation, Usage, Derivation, Association + `hadPlan`, Attribution, Delegation, Membership, Communication) — nothing else, because nothing else is ever produced |
66
+ | Output | a `pydot.Dot` object | a plain DOT string, ready for client-side rendering |
67
+ | HTML/Viz.js page | not provided | `pdssp_prov_toolkit.html.render_prov_html` — a full standalone page, SRI-pinned Viz.js, no server-side `graphviz` binary needed |
68
+
69
+ In short: `prov[dot]` is the right choice for a general-purpose PROV-DM
70
+ visualiser. This package is narrower on purpose — it only ever has to
71
+ render what PDSSP's own toolkit-built documents contain — and adds the
72
+ one thing that mattered enough in practice to justify not just calling
73
+ `prov[dot]` directly: readable graphs when a label or URL is long.
74
+
75
+ ## Usage
76
+
77
+ ```python
78
+ from pdssp_prov_toolkit import (
79
+ ProvAttr,
80
+ ProvType,
81
+ new_document,
82
+ prov_to_dot,
83
+ render_prov_html,
84
+ resolve_agent,
85
+ slug,
86
+ )
87
+
88
+ doc = new_document("https://example.org/api")
89
+ source = doc.entity(
90
+ "source-data",
91
+ {ProvAttr.TYPE: ProvType.COLLECTION, ProvAttr.LABEL: "Upstream source"},
92
+ )
93
+ mapping = doc.activity(
94
+ "mapping", other_attributes={ProvAttr.TYPE: ProvType.ACTIVITY, ProvAttr.LABEL: "Mapping"}
95
+ )
96
+ doc.used(mapping, source)
97
+
98
+ # A Graphviz DOT string, ready to render client-side (see render_prov_html)
99
+ # or with any Graphviz-compatible tool.
100
+ dot = prov_to_dot(doc)
101
+
102
+ # A full standalone HTML page rendering that graph via Viz.js.
103
+ page = render_prov_html(dot=dot, base="https://example.org/api", subtitle_html="Whole catalog.")
104
+ ```
105
+
106
+ See each module's own docstring (`pdssp_prov_toolkit.vocab`,
107
+ `.document`, `.dot`, `.html`) for the full API.
108
+
109
+ ## Installing UV
110
+
111
+ To manage the dependencies of PDSSP Prov Toolkit, we use
112
+ [UV](<https://docs.astral.sh/uv/>). If you don\'t have UV
113
+ installed, follow these steps:
114
+
115
+ 1. **Install UV**:
116
+
117
+ > ``` shell
118
+ > $ curl -LsSf https://astral.sh/uv/install.sh | sh
119
+ > ```
120
+
121
+ 2. **Verify the installation**:
122
+
123
+ > ``` console
124
+ > $ uv --version
125
+ > ```
126
+
127
+ Please note that this project has been tested with UV version 0.9.15.
128
+
129
+ ## From sources
130
+
131
+ ``` console
132
+ $ git clone https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit.git
133
+ $ cd pdssp_prov_toolkit
134
+ $ uv sync
135
+ ```
136
+
137
+ ## Development
138
+
139
+ ``` console
140
+ $ git clone https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit.git
141
+ $ cd pdssp_prov_toolkit
142
+ $ make prepare-dev
143
+ $ source .venv/bin/activate
144
+ $ make install-dev
145
+ ```
146
+
147
+ To get more information about the preconfigured tasks:
148
+
149
+ ``` console
150
+ $ make help
151
+ ```
152
+
153
+ ## Run tests
154
+
155
+ ``` console
156
+ $ make tests
157
+ ```
158
+
159
+ ## Documentation
160
+
161
+ The documentation is automatically deployed on
162
+ <https://pdssp.io.cnes.fr/>pdssp_prov_toolkit based on main branch
163
+
164
+ ## Author
165
+
166
+ 👤 **Jean-Christophe Malapert**
167
+
168
+ ## 🤝 Contributing
169
+
170
+ Contributions, issues and feature requests are welcome!
171
+ Feel free to check [issues page](https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit/issues).
172
+ You can also take a look at the [contributing guide](https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit/blob/main/CONTRIBUTING.rst)
173
+
174
+ ## 📝 License
175
+
176
+ This project is [Apache V2.0](https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit/blob/main/LICENSE) licensed.
@@ -0,0 +1,165 @@
1
+ # PDSSP Prov Toolkit
2
+
3
+ [![image](https://img.shields.io/badge/Maintained%3F-yes-green.svg)]()
4
+
5
+ ![image]()
6
+
7
+ Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
8
+
9
+ ## Why this exists
10
+
11
+ Two PDSSP services (`ode_stac_proxy`, a live STAC proxy, and
12
+ `geocoding-api`, an offline GeoPackage/OpenSearch pipeline) each expose a
13
+ `GET /prov` endpoint describing how their own FAIR-transformed data is
14
+ produced, as a W3C PROV-DM document. Both were built independently, before
15
+ this package existed — and by the time it was extracted, each had grown
16
+ its own, already-drifted copy of the same vocabulary and rendering code:
17
+
18
+ - `geocoding-api` had added Subresource Integrity hashes pinning its
19
+ Viz.js CDN scripts (a real security hardening) and `wasInformedBy`/
20
+ `license`/`crs` support — none of which had made it back into
21
+ `ode_stac_proxy`.
22
+ - `ode_stac_proxy` had added `hadMember`/`hadPlan` support — which had
23
+ never made it into `geocoding-api`.
24
+
25
+ Neither service noticed the other's fixes, because there was nothing to
26
+ notice — it was two copies, not one dependency. This package is that one
27
+ dependency: the union of both, so a fix or an addition made once benefits
28
+ every PDSSP service producing FAIR-transformation provenance, present or
29
+ future.
30
+
31
+ **What deliberately did *not* move here**: each service's own
32
+ document-assembly logic — *which* activities and entities its own
33
+ pipeline has, and how they connect — stays in that service. A STAC proxy
34
+ harvesting one live upstream and a batch pipeline harvesting several
35
+ static sources into per-body GeoPackages don't share a pipeline shape, so
36
+ forcing one here would fit neither well. What both actually needed
37
+ identically is the vocabulary they build records with and the code that
38
+ turns the finished document into a graph — that's the whole scope of this
39
+ package.
40
+
41
+ ## Relationship to `prov`'s own `prov.dot`
42
+
43
+ This package depends on [`prov`](https://prov.readthedocs.io/), and `prov`
44
+ already ships its own optional Graphviz export
45
+ (`prov.dot.prov_to_dot`, via the `prov[dot]` extra) — doing, on paper, the
46
+ same job as `pdssp_prov_toolkit.dot`. They even use the *same* node
47
+ colours (`#FFFC87`/`#9FB1FC`/`#FED37F`), because both independently follow
48
+ the same W3C PROV Primer / ProvToolbox convention. So why not just use it?
49
+
50
+ | | `prov[dot]` | `pdssp_prov_toolkit.dot` |
51
+ |---|---|---|
52
+ | Extra dependency | `pydot` + `pyparsing` | none (plain string templating) |
53
+ | Long label/URL wrapping | no — a long value renders as one unbroken line | yes — this is *why* this module exists: an unwrapped long location once made a real graph too wide to read |
54
+ | Relation vocabulary | every PROV-DM relation, n-ary relations, nested bundles, attribute-annotation nodes | exactly the relations PDSSP's own document builders emit (Generation, Usage, Derivation, Association + `hadPlan`, Attribution, Delegation, Membership, Communication) — nothing else, because nothing else is ever produced |
55
+ | Output | a `pydot.Dot` object | a plain DOT string, ready for client-side rendering |
56
+ | HTML/Viz.js page | not provided | `pdssp_prov_toolkit.html.render_prov_html` — a full standalone page, SRI-pinned Viz.js, no server-side `graphviz` binary needed |
57
+
58
+ In short: `prov[dot]` is the right choice for a general-purpose PROV-DM
59
+ visualiser. This package is narrower on purpose — it only ever has to
60
+ render what PDSSP's own toolkit-built documents contain — and adds the
61
+ one thing that mattered enough in practice to justify not just calling
62
+ `prov[dot]` directly: readable graphs when a label or URL is long.
63
+
64
+ ## Usage
65
+
66
+ ```python
67
+ from pdssp_prov_toolkit import (
68
+ ProvAttr,
69
+ ProvType,
70
+ new_document,
71
+ prov_to_dot,
72
+ render_prov_html,
73
+ resolve_agent,
74
+ slug,
75
+ )
76
+
77
+ doc = new_document("https://example.org/api")
78
+ source = doc.entity(
79
+ "source-data",
80
+ {ProvAttr.TYPE: ProvType.COLLECTION, ProvAttr.LABEL: "Upstream source"},
81
+ )
82
+ mapping = doc.activity(
83
+ "mapping", other_attributes={ProvAttr.TYPE: ProvType.ACTIVITY, ProvAttr.LABEL: "Mapping"}
84
+ )
85
+ doc.used(mapping, source)
86
+
87
+ # A Graphviz DOT string, ready to render client-side (see render_prov_html)
88
+ # or with any Graphviz-compatible tool.
89
+ dot = prov_to_dot(doc)
90
+
91
+ # A full standalone HTML page rendering that graph via Viz.js.
92
+ page = render_prov_html(dot=dot, base="https://example.org/api", subtitle_html="Whole catalog.")
93
+ ```
94
+
95
+ See each module's own docstring (`pdssp_prov_toolkit.vocab`,
96
+ `.document`, `.dot`, `.html`) for the full API.
97
+
98
+ ## Installing UV
99
+
100
+ To manage the dependencies of PDSSP Prov Toolkit, we use
101
+ [UV](<https://docs.astral.sh/uv/>). If you don\'t have UV
102
+ installed, follow these steps:
103
+
104
+ 1. **Install UV**:
105
+
106
+ > ``` shell
107
+ > $ curl -LsSf https://astral.sh/uv/install.sh | sh
108
+ > ```
109
+
110
+ 2. **Verify the installation**:
111
+
112
+ > ``` console
113
+ > $ uv --version
114
+ > ```
115
+
116
+ Please note that this project has been tested with UV version 0.9.15.
117
+
118
+ ## From sources
119
+
120
+ ``` console
121
+ $ git clone https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit.git
122
+ $ cd pdssp_prov_toolkit
123
+ $ uv sync
124
+ ```
125
+
126
+ ## Development
127
+
128
+ ``` console
129
+ $ git clone https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit.git
130
+ $ cd pdssp_prov_toolkit
131
+ $ make prepare-dev
132
+ $ source .venv/bin/activate
133
+ $ make install-dev
134
+ ```
135
+
136
+ To get more information about the preconfigured tasks:
137
+
138
+ ``` console
139
+ $ make help
140
+ ```
141
+
142
+ ## Run tests
143
+
144
+ ``` console
145
+ $ make tests
146
+ ```
147
+
148
+ ## Documentation
149
+
150
+ The documentation is automatically deployed on
151
+ <https://pdssp.io.cnes.fr/>pdssp_prov_toolkit based on main branch
152
+
153
+ ## Author
154
+
155
+ 👤 **Jean-Christophe Malapert**
156
+
157
+ ## 🤝 Contributing
158
+
159
+ Contributions, issues and feature requests are welcome!
160
+ Feel free to check [issues page](https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit/issues).
161
+ You can also take a look at the [contributing guide](https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit/blob/main/CONTRIBUTING.rst)
162
+
163
+ ## 📝 License
164
+
165
+ This project is [Apache V2.0](https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit/blob/main/LICENSE) licensed.
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "pdssp_prov_toolkit"
3
+ readme = "README.md"
4
+ description = "Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services."
5
+ license = "Apache-2.0"
6
+ homepage = "https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit"
7
+ version = "1.0.1"
8
+ requires-python = ">=3.12"
9
+ dependencies = ["prov>=3.1.0"]
10
+
11
+ [[project.authors]]
12
+ name = "Jean-Christophe Malapert"
13
+ email = "jean-christophe.malapert@cnes.fr"
14
+
15
+ [[project.packages]]
16
+ include = "pdssp_prov_toolkit"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.9.14,<0.10.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/pdssp_prov_toolkit"]
24
+
25
+ [tool.black]
26
+ line-length = 88
27
+ include = '\.pyi?$'
28
+ exclude = '''
29
+ /(
30
+ \.git
31
+ | \.hg
32
+ | \.mypy_cache
33
+ | \.tox
34
+ | \.venv
35
+ | docs
36
+ | scripts
37
+ | docker
38
+ | _build
39
+ | buck-out
40
+ | build
41
+ | dist
42
+ )/
43
+ '''
44
+
45
+ [tool.bandit]
46
+ exclude_dirs = [
47
+ ".venv",
48
+ "tests",
49
+ "scripts",
50
+ ]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "bump-my-version>=1.2.4",
58
+ "coverage>=7.12.0",
59
+ "mkdocs>=1.6.1",
60
+ "mkdocs-gen-files>=0.6.0",
61
+ "mkdocs-literate-nav>=0.6.2",
62
+ "mkdocs-macros-plugin>=1.5.0",
63
+ "mkdocs-material>=9.7.0",
64
+ "mkdocs-with-pdf>=0.9.3",
65
+ "mkdocstrings[python]>=1.0.0",
66
+ "pip-licenses>=5.5.0",
67
+ "pre-commit>=4.5.0",
68
+ "pre-commit-hooks>=6.0.0",
69
+ "pytest>=9.0.1",
70
+ "pytest-html>=4.1.1",
71
+ "pytest-json-report>=1.5.0",
72
+ "pytest-profiling>=1.8.1",
73
+ "pytest-mock>=3.15.1",
74
+ "black>=25.11.0",
75
+ "tox>=4.32.0",
76
+ "flake8>=7.3.0",
77
+ "mccabe>=0.7.0",
78
+ "mypy>=1.19.0",
79
+ "pylint>=4.0.4",
80
+ "toml>=0.10.2",
81
+ ]
@@ -0,0 +1,77 @@
1
+ [project]
2
+ name = "pdssp_prov_toolkit"
3
+ readme = "README.md"
4
+ description = "Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services."
5
+ authors = [{name="Jean-Christophe Malapert", email="jean-christophe.malapert@cnes.fr"}, ]
6
+ license = "Apache-2.0"
7
+ packages = [{include = "pdssp_prov_toolkit"}]
8
+ homepage = "https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit"
9
+ version = "1.0.1"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "prov>=3.1.0",
13
+ ]
14
+
15
+ # ── Build ────────────────────────────────────────────────────────────────────
16
+ [build-system]
17
+ requires = ["uv_build>=0.9.14,<0.10.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/pdssp_prov_toolkit"]
22
+
23
+ # ── Dev dependencies ─────────────────────────────────────────────────────────
24
+ [dependency-groups]
25
+ dev = [
26
+ "bump-my-version>=1.2.4",
27
+ "coverage>=7.12.0",
28
+ "mkdocs>=1.6.1",
29
+ "mkdocs-gen-files>=0.6.0",
30
+ "mkdocs-literate-nav>=0.6.2",
31
+ "mkdocs-macros-plugin>=1.5.0",
32
+ "mkdocs-material>=9.7.0",
33
+ "mkdocs-with-pdf>=0.9.3",
34
+ "mkdocstrings[python]>=1.0.0",
35
+ "pip-licenses>=5.5.0",
36
+ "pre-commit>=4.5.0",
37
+ "pre-commit-hooks>=6.0.0",
38
+ "pytest>=9.0.1",
39
+ "pytest-html>=4.1.1",
40
+ "pytest-json-report>=1.5.0",
41
+ "pytest-profiling>=1.8.1",
42
+ "pytest-mock>=3.15.1",
43
+ "black>=25.11.0",
44
+ "tox>=4.32.0",
45
+ "flake8>=7.3.0",
46
+ "mccabe>=0.7.0",
47
+ "mypy>=1.19.0",
48
+ "pylint>=4.0.4",
49
+ "toml>=0.10.2",
50
+ ]
51
+
52
+ [tool.black]
53
+ line-length = 88
54
+ include = '\.pyi?$'
55
+ exclude = '''
56
+ /(
57
+ \.git
58
+ | \.hg
59
+ | \.mypy_cache
60
+ | \.tox
61
+ | \.venv
62
+ | docs
63
+ | scripts
64
+ | docker
65
+ | _build
66
+ | buck-out
67
+ | build
68
+ | dist
69
+ )/
70
+ '''
71
+
72
+ [tool.bandit]
73
+ exclude_dirs = [".venv", "tests", "scripts"]
74
+
75
+ # ── pytest ───────────────────────────────────────────────────────────────────
76
+ [tool.pytest.ini_options]
77
+ testpaths = ["tests"]
@@ -0,0 +1,48 @@
1
+ # PDSSP Prov Toolkit - Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
2
+ # Copyright (C) 2026 - CNES (Jean-Christophe Malapert for PDSSP)
3
+ # This file is part of PDSSP Prov Toolkit <https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit>
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML
7
+ rendering for FAIR-transformation provenance across PDSSP services.
8
+
9
+ A library, not an application -- it configures no logging of its own
10
+ (that is each consuming service's own decision to make) and has no CLI.
11
+ """
12
+
13
+ from ._version import (
14
+ __author__,
15
+ __author_email__,
16
+ __copyright__,
17
+ __description__,
18
+ __license__,
19
+ __name_soft__,
20
+ __title__,
21
+ __url__,
22
+ __version__,
23
+ )
24
+ from .document import FOAF_NS, new_document, resolve_agent, slug
25
+ from .dot import prov_to_dot
26
+ from .html import render_prov_html
27
+ from .vocab import ProvAttr, ProvQualifier, ProvType
28
+
29
+ __all__ = [
30
+ "FOAF_NS",
31
+ "ProvAttr",
32
+ "ProvQualifier",
33
+ "ProvType",
34
+ "__author__",
35
+ "__author_email__",
36
+ "__copyright__",
37
+ "__description__",
38
+ "__license__",
39
+ "__name_soft__",
40
+ "__title__",
41
+ "__url__",
42
+ "__version__",
43
+ "new_document",
44
+ "prov_to_dot",
45
+ "render_prov_html",
46
+ "resolve_agent",
47
+ "slug",
48
+ ]
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8 -*-
2
+ # skeleton-python-binary - Command-line tool that generates project templates based on predefined Python project template
3
+ # Copyright (C) 2024-2025 - Centre National d'Etudes Spatiales
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ # Auto-generated by skeleton-python-binary
6
+ """Project metadata."""
7
+ from importlib.metadata import metadata
8
+
9
+ pkg_metadata = metadata("pdssp_prov_toolkit")
10
+
11
+ __name_soft__ = pkg_metadata.get("name", "unknown")
12
+ __version__ = pkg_metadata.get("version", "0.0.0")
13
+ __title__ = pkg_metadata.get("name", "unknown")
14
+ __description__ = pkg_metadata.get("summary", "")
15
+ __url__ = pkg_metadata.get("homepage", "")
16
+ __author__ = pkg_metadata.get("authors", "unknown")
17
+ __author_email__ = pkg_metadata.get("author-email", "unknown")
18
+ __license__ = pkg_metadata.get("license", "")
19
+ __copyright__ = "2026, CNES (Jean-Christophe Malapert for PDSSP)"
@@ -0,0 +1,97 @@
1
+ # PDSSP Prov Toolkit - Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
2
+ # Copyright (C) 2026 - CNES (Jean-Christophe Malapert for PDSSP)
3
+ # This file is part of PDSSP Prov Toolkit <https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit>
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """
7
+ Generic PROV-DM document helpers.
8
+ ====================================
9
+ Small, domain-agnostic building blocks every service assembling its own
10
+ :class:`~prov.model.ProvDocument` needs regardless of its own pipeline
11
+ shape (a live-mapping STAC proxy, an offline GeoPackage-compilation
12
+ pipeline, ...): a namespaced empty document, a safe local identifier from
13
+ a free-text name, and de-duplicated agent lookup.
14
+
15
+ Deliberately excludes anything that assumes a particular pipeline shape
16
+ (e.g. "one software agent acting on behalf of another", "a source entity
17
+ attributed to a producer/licensor") -- those patterns are common across
18
+ today's two PDSSP consumers of this package, but each has grown its own,
19
+ differently-shaped set of activities/entities around them, so forcing a
20
+ single higher-level "build me a document" helper here would either fit
21
+ neither well or silently constrain a future consumer's own pipeline shape.
22
+ Each service keeps that assembly logic in its own codebase, built out of
23
+ these primitives plus :mod:`.vocab`.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from typing import Any
30
+
31
+ from prov.model import ProvDocument
32
+
33
+ from .vocab import ProvAttr
34
+
35
+ #: Default FOAF namespace URI, bound to the ``foaf`` prefix by
36
+ #: :func:`new_document`.
37
+ FOAF_NS = "https://xmlns.com/foaf/0.1/"
38
+
39
+
40
+ def new_document(base: str, foaf_ns: str = FOAF_NS) -> ProvDocument:
41
+ """Return an empty :class:`ProvDocument` namespaced for *base*.
42
+
43
+ Parameters
44
+ ----------
45
+ base:
46
+ Public base URL of the service (no trailing slash), used as the
47
+ default namespace so record identifiers resolve to real URIs.
48
+ foaf_ns:
49
+ Namespace URI bound to the ``foaf`` prefix.
50
+ """
51
+ doc = ProvDocument()
52
+ doc.set_default_namespace(f"{base}/prov#")
53
+ doc.add_namespace("foaf", foaf_ns)
54
+ return doc
55
+
56
+
57
+ def slug(name: str) -> str:
58
+ """Turn a free-text name into a safe PROV local identifier, e.g.
59
+ ``"NASA PDS Geosciences Node"`` -> ``"nasa-pds-geosciences-node"``.
60
+
61
+ A PROV-N/PROV-XML identifier's local part is a QName-like token that
62
+ cannot contain arbitrary characters (spaces, ``/``, ...) -- this is the
63
+ one place in the toolkit that turns a human-facing name into something
64
+ safe to use as one.
65
+ """
66
+ return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "agent"
67
+
68
+
69
+ def resolve_agent(doc: ProvDocument, agents: dict[str, Any], name: str, prov_type: str) -> Any:
70
+ """Return the existing agent named *name* in *agents*, or add (and
71
+ cache into *agents*) a new one of *prov_type* -- so the same real-world
72
+ party never gets a duplicate record across different callers or calls.
73
+
74
+ Parameters
75
+ ----------
76
+ doc:
77
+ The document to add a new agent to, if needed.
78
+ agents:
79
+ Mutable ``name -> ProvAgent`` cache, shared across every caller
80
+ resolving agents for *doc* (typically built up alongside whatever
81
+ per-pipeline agents a service adds itself).
82
+ name:
83
+ The agent's human-facing name (also used as its ``prov:label`` and
84
+ ``foaf:name`` when a new record is created).
85
+ prov_type:
86
+ One of :class:`~.vocab.ProvType`'s agent-shaped values (``PERSON``,
87
+ ``ORGANIZATION``, ``SOFTWARE_AGENT``) for a newly created record;
88
+ ignored when *name* already resolves to an existing agent.
89
+ """
90
+ agent = agents.get(name)
91
+ if agent is None:
92
+ agent = doc.agent(
93
+ slug(name),
94
+ {ProvAttr.TYPE: prov_type, ProvAttr.LABEL: name, ProvAttr.FOAF_NAME: name},
95
+ )
96
+ agents[name] = agent
97
+ return agent
@@ -0,0 +1,253 @@
1
+ # PDSSP Prov Toolkit - Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
2
+ # Copyright (C) 2026 - CNES (Jean-Christophe Malapert for PDSSP)
3
+ # This file is part of PDSSP Prov Toolkit <https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit>
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """
7
+ DOT (Graphviz) rendering for PROV-DM documents.
8
+ =================================================
9
+ Converts a :class:`prov.model.ProvDocument` into Graphviz DOT source, using
10
+ the standard PROV-DM node shapes/colours (yellow ellipses for entities,
11
+ blue rounded boxes for activities, orange houses for agents), matching the
12
+ W3C PROV Primer and ProvStore/ProvToolbox conventions.
13
+
14
+ Produces plain DOT *text* only -- no native ``graphviz`` binary or extra
15
+ Python dependency beyond ``prov`` itself (deliberately not built on
16
+ ``prov``'s own optional ``prov[dot]``/``pydot`` extra, which needs a real
17
+ dependency and does not word-wrap long labels/URLs -- see this module's
18
+ own history for why that matters: an unwrapped long label/location made an
19
+ early graph too wide to read). Rendering the DOT source itself happens
20
+ client-side, in the browser, via Viz.js (see :mod:`.html`).
21
+
22
+ Covers exactly the record/relation shapes the two services this toolkit
23
+ was extracted from actually produce -- every :class:`~prov.model.ProvRecord`
24
+ subtype ``prov`` itself defines that isn't listed in :data:`_RELATION_SPECS`
25
+ is simply not drawn as an edge (nodes for entities/activities/agents are
26
+ always drawn regardless). Add to :data:`_RELATION_SPECS` (and, for a new
27
+ attribute, to :func:`_label_lines`) rather than working around a gap here.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import textwrap
33
+
34
+ from prov.model import (
35
+ ProvActivity,
36
+ ProvAgent,
37
+ ProvAssociation,
38
+ ProvAttribution,
39
+ ProvCommunication,
40
+ ProvDelegation,
41
+ ProvDerivation,
42
+ ProvDocument,
43
+ ProvEntity,
44
+ ProvGeneration,
45
+ ProvMembership,
46
+ ProvRecord,
47
+ ProvUsage,
48
+ )
49
+
50
+ from .vocab import ProvAttr, ProvQualifier
51
+
52
+
53
+ class ProvDotConfig:
54
+ ENTITY_STYLE = 'shape=ellipse, style=filled, fillcolor="#FFFC87", color="#808080"'
55
+ ACTIVITY_STYLE = 'shape=box, style="filled,rounded", fillcolor="#9FB1FC", color="#0000FF"'
56
+ AGENT_STYLE = 'shape=house, style=filled, fillcolor="#FED37F", color="#D2691E"'
57
+
58
+ #: Relation record type -> (source attribute, target attribute, edge label,
59
+ #: edge line style), per the PROV-DM diagram conventions. Attribute names
60
+ #: are :class:`~.vocab.ProvQualifier` keys.
61
+ RELATION_SPECS: dict[type, tuple[str, str, str, str]] = {
62
+ ProvGeneration: (ProvQualifier.ENTITY, ProvQualifier.ACTIVITY, "wasGeneratedBy", "solid"),
63
+ ProvUsage: (ProvQualifier.ACTIVITY, ProvQualifier.ENTITY, "used", "dashed"),
64
+ ProvDerivation: (
65
+ ProvQualifier.GENERATED_ENTITY,
66
+ ProvQualifier.USED_ENTITY,
67
+ "wasDerivedFrom",
68
+ "solid",
69
+ ),
70
+ ProvAssociation: (
71
+ ProvQualifier.ACTIVITY,
72
+ ProvQualifier.AGENT,
73
+ "wasAssociatedWith",
74
+ "dotted",
75
+ ),
76
+ ProvAttribution: (ProvQualifier.ENTITY, ProvQualifier.AGENT, "wasAttributedTo", "dotted"),
77
+ ProvDelegation: (
78
+ ProvQualifier.DELEGATE,
79
+ ProvQualifier.RESPONSIBLE,
80
+ "actedOnBehalfOf",
81
+ "dotted",
82
+ ),
83
+ ProvMembership: (ProvQualifier.COLLECTION, ProvQualifier.ENTITY, "hadMember", "solid"),
84
+ ProvCommunication: (
85
+ ProvQualifier.INFORMED,
86
+ ProvQualifier.INFORMANT,
87
+ "wasInformedBy",
88
+ "dashed",
89
+ ),
90
+ }
91
+
92
+ #: Target line length for node-label wrapping (short values stay on one
93
+ #: line; long URLs/descriptions get broken up).
94
+ WRAP_WIDTH = 50
95
+
96
+
97
+ def prov_to_dot(doc: ProvDocument) -> str:
98
+ """Render *doc* as Graphviz DOT source.
99
+
100
+ Parameters
101
+ ----------
102
+ doc:
103
+ The PROV-DM document to render.
104
+
105
+ Returns
106
+ -------
107
+ str
108
+ A ``digraph`` DOT source string, ready to be rendered by any
109
+ Graphviz-compatible tool (the ``dot`` CLI, or client-side via
110
+ Viz.js).
111
+ """
112
+ lines = [
113
+ "digraph provenance {",
114
+ " rankdir=LR;",
115
+ " nodesep=0.4;",
116
+ " ranksep=0.6;",
117
+ ' node [fontname="Helvetica,Arial,sans-serif", fontsize=11];',
118
+ ' edge [fontname="Helvetica,Arial,sans-serif", fontsize=9];',
119
+ ]
120
+
121
+ for record in doc.get_records():
122
+ if isinstance(record, (ProvEntity, ProvActivity, ProvAgent)):
123
+ lines.append(_node_line(record))
124
+
125
+ for record in doc.get_records():
126
+ spec = ProvDotConfig.RELATION_SPECS.get(type(record))
127
+ if spec is not None:
128
+ lines.append(_edge_line(record, *spec))
129
+ if isinstance(record, ProvAssociation):
130
+ plan_edge = _plan_edge_line(record)
131
+ if plan_edge is not None:
132
+ lines.append(plan_edge)
133
+
134
+ lines.append("}")
135
+ return "\n".join(lines)
136
+
137
+
138
+ def _node_line(
139
+ record: ProvRecord,
140
+ entity_style: str = ProvDotConfig.ENTITY_STYLE,
141
+ activity_style: str = ProvDotConfig.ACTIVITY_STYLE,
142
+ agent_style: str = ProvDotConfig.AGENT_STYLE,
143
+ ) -> str:
144
+ """Return the DOT statement declaring one entity/activity/agent node."""
145
+ if isinstance(record, ProvEntity):
146
+ style = entity_style
147
+ elif isinstance(record, ProvActivity):
148
+ style = activity_style
149
+ else:
150
+ style = agent_style
151
+ # Joined with a literal "\n" (backslash-n), which Graphviz renders as a
152
+ # line break inside a label -- not an actual newline character, which
153
+ # `_escape` would otherwise be free to mangle along with the rest.
154
+ label = "\\n".join(_escape(line) for line in _label_lines(record))
155
+ return f' "{record.identifier.localpart}" [label="{label}", {style}];'
156
+
157
+
158
+ def _edge_line(record: ProvRecord, from_attr: str, to_attr: str, label: str, style: str) -> str:
159
+ """Return the DOT statement for one relation record, per *spec*."""
160
+ attrs = {str(key): value for key, value in record.formal_attributes}
161
+ src = _local(attrs.get(from_attr))
162
+ dst = _local(attrs.get(to_attr))
163
+ return f' "{src}" -> "{dst}" [label="{label}", style={style}];'
164
+
165
+
166
+ def _plan_edge_line(record: ProvRecord) -> str | None:
167
+ """Return the extra ``hadPlan`` edge for an association naming a plan.
168
+
169
+ Not covered by :data:`ProvDotConfig.RELATION_SPECS` (which only draws
170
+ the activity/agent edge) -- drawn only when a plan is actually present
171
+ (a ``wasAssociatedWith`` with no ``plan=`` produces no edge here).
172
+ """
173
+ attrs = {str(key): value for key, value in record.formal_attributes}
174
+ activity = attrs.get(ProvQualifier.ACTIVITY)
175
+ plan = attrs.get(ProvQualifier.PLAN)
176
+ if activity is None or plan is None:
177
+ return None
178
+ return f' "{_local(activity)}" -> "{_local(plan)}" [label="hadPlan", style=dotted];'
179
+
180
+
181
+ def _local(qname) -> str:
182
+ """Return the local part of a :class:`~prov.identifier.QualifiedName`, or ``"?"``."""
183
+ return qname.localpart if qname is not None else "?"
184
+
185
+
186
+ def _label_lines(record: ProvRecord) -> list[str]:
187
+ """Return the lines to display inside *record*'s node: the word-wrapped
188
+ primary label (``prov:label``, falling back to the identifier), then
189
+ ``version``, ``identifier``, ``license`` and ``crs`` when present, and
190
+ a hard-wrapped ``prov:location`` when present -- each shown only for a
191
+ record that actually carries it, so a consumer that never sets e.g.
192
+ ``crs`` never sees an empty line.
193
+ """
194
+ lines = _wrap_text(_primary_label(record))
195
+ version = _first_attribute(record, ProvAttr.VERSION)
196
+ if version is not None:
197
+ lines.append(f"v{version}")
198
+ identifier = _first_attribute(record, ProvAttr.IDENTIFIER)
199
+ if identifier is not None:
200
+ lines.append(f"id: {identifier}")
201
+ license_ = _first_attribute(record, ProvAttr.LICENSE)
202
+ if license_ is not None:
203
+ lines.append(f"license: {license_}")
204
+ crs = _first_attribute(record, ProvAttr.CRS)
205
+ if crs is not None:
206
+ lines.append(f"crs: {crs}")
207
+ location = _first_attribute(record, ProvAttr.LOCATION)
208
+ if location is not None:
209
+ lines.extend(_wrap_url(str(location)))
210
+ return lines
211
+
212
+
213
+ def _wrap_text(text: str, wrap_width: int = ProvDotConfig.WRAP_WIDTH) -> list[str]:
214
+ """Word-wrap free-form prose (e.g. a ``prov:label``) at *wrap_width*."""
215
+ return textwrap.wrap(text, width=wrap_width, break_on_hyphens=False) or [text]
216
+
217
+
218
+ def _wrap_url(text: str, wrap_width: int = ProvDotConfig.WRAP_WIDTH) -> list[str]:
219
+ """Hard-wrap a URL into chunks of at most *wrap_width* chars, preferring
220
+ to break at ``&``/``/`` near the target width over a hard mid-word cut.
221
+ """
222
+ lines = []
223
+ remaining = text
224
+ while len(remaining) > wrap_width:
225
+ cut = wrap_width
226
+ for sep in ("&", "/"):
227
+ idx = remaining.rfind(sep, 0, wrap_width + 15)
228
+ if idx > wrap_width // 2:
229
+ cut = idx + 1
230
+ break
231
+ lines.append(remaining[:cut])
232
+ remaining = remaining[cut:]
233
+ lines.append(remaining)
234
+ return lines
235
+
236
+
237
+ def _primary_label(record: ProvRecord) -> str:
238
+ """Return ``record``'s ``prov:label``, or its identifier if it has none."""
239
+ values = record.get_attribute(ProvAttr.LABEL)
240
+ if values:
241
+ return str(next(iter(values)))
242
+ return record.identifier.localpart
243
+
244
+
245
+ def _first_attribute(record: ProvRecord, name: str) -> str | None:
246
+ """Return one value of the *name* attribute on *record*, or ``None``."""
247
+ values = record.get_attribute(name)
248
+ return str(next(iter(values))) if values else None
249
+
250
+
251
+ def _escape(text: str) -> str:
252
+ """Escape backslashes and double quotes so *text* is safe inside a DOT string literal."""
253
+ return text.replace("\\", "\\\\").replace('"', '\\"')
@@ -0,0 +1,170 @@
1
+ # PDSSP Prov Toolkit - Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
2
+ # Copyright (C) 2026 - CNES (Jean-Christophe Malapert for PDSSP)
3
+ # This file is part of PDSSP Prov Toolkit <https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit>
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """
7
+ HTML graph visualisation for a ``GET /prov``-style endpoint.
8
+ ===============================================================
9
+ When a browser navigates to a provenance endpoint, a graph is the most
10
+ legible representation of a PROV-DM document -- that is the whole point of
11
+ the diagrams in the W3C PROV Primer -- so :func:`render_prov_html` renders
12
+ one instead of raw PROV-JSON.
13
+
14
+ The Graphviz DOT source produced by :func:`~.dot.prov_to_dot` is rendered
15
+ into an SVG **client-side**, in the browser, via Viz.js (Graphviz compiled
16
+ to WebAssembly/asm.js, loaded from a CDN, pinned by version and by
17
+ Subresource Integrity hash so the CDN cannot silently swap in different
18
+ script content). No native ``graphviz`` binary runs on the server.
19
+
20
+ The SVG is shown at its native size inside a scrollable container, rather
21
+ than shrunk with CSS to fit the viewport width: a PROV graph with several
22
+ same-rank nodes is often wider or taller than one screen, and forcing it
23
+ to fit would shrink its text past legibility. Scrolling (or the browser's
24
+ own zoom) is the tradeoff that keeps the labels readable.
25
+
26
+ The page also links to the raw PROV-JSON / PROV-N / PROV-XML
27
+ serialisations, which are plain, standard PROV-DM documents that can be
28
+ pasted or uploaded into ProvStore (https://openprovenance.org/store/) or
29
+ any other PROV-DM-compliant tool -- this page does not call out to
30
+ ProvStore itself, so nothing about a deployment using it is sent anywhere
31
+ without the operator choosing to do so.
32
+
33
+ Deliberately generic about *what* is being shown: :func:`render_prov_html`
34
+ takes an already-built *subtitle_html* fragment and *query_suffix* rather
35
+ than any scope-specific parameter name (a STAC ``collection_id``/``item_id``
36
+ pair, a gazetteer ``planet``, or anything a future consumer's own resource
37
+ model might add) -- building that scope-specific text/query-string is each
38
+ caller's own job (see the parameters' own docstrings for the escaping
39
+ contract).
40
+
41
+ **Security note**: the DOT source is embedded via a hidden, HTML-escaped
42
+ ``<pre>`` element read back through ``.textContent`` in a *static* inline
43
+ script (see :func:`render_prov_html`), rather than interpolated directly
44
+ into a ``<script>`` block -- this sidesteps both HTML/JS string-escaping
45
+ pitfalls and the ``</script>`` early-termination issue, regardless of what
46
+ free-text labels end up in the DOT source.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ from html import escape
52
+
53
+ #: Pinned Viz.js release -- keep in lock-step with :data:`_VIZ_JS_SRI`/
54
+ #: :data:`_VIZ_FULL_RENDER_JS_SRI` below: bumping the version without
55
+ #: recomputing these hashes makes the browser refuse to run either script
56
+ #: at all (a safe, loud failure -- the graph just won't render -- rather
57
+ #: than a silent security regression).
58
+ _VIZ_JS_BASE = "https://cdn.jsdelivr.net/npm/viz.js@2.1.2"
59
+ _VIZ_JS_SRI = "sha384-f4dIboC5mwQKuVsNplQrKp19L8ttwIgw0LJNV0PsuIJYHTjIljdJ9cZdTqIVtk8y"
60
+ _VIZ_FULL_RENDER_JS_SRI = "sha384-oyiaz0P9mLALNbgQC9EQ48wxHRWGopFpDEMBoCc9Rir7gWL7GrgKzr4+X9mVM77b"
61
+
62
+
63
+ def render_prov_html(
64
+ *,
65
+ dot: str,
66
+ base: str,
67
+ subtitle_html: str,
68
+ query_suffix: str = "",
69
+ footer_extra_html: str = "",
70
+ ) -> str:
71
+ """Return the full HTML page visualising a PROV-DM document as a graph.
72
+
73
+ Parameters
74
+ ----------
75
+ dot:
76
+ Graphviz DOT source produced by :func:`~.dot.prov_to_dot`.
77
+ base:
78
+ Public base URL of the service, shown (HTML-escaped by this
79
+ function) in the page title.
80
+ subtitle_html:
81
+ The page's subtitle, e.g. ``"How this service's STAC catalog is
82
+ produced, scoped to collection <code>foo</code>."`` -- an
83
+ already-safe HTML fragment built by the caller (any dynamic part,
84
+ such as an id, must already be ``html.escape()``-d by the caller
85
+ before it's wrapped in ``<code>...</code>``; this function does
86
+ **not** escape it again, exactly like the caller's existing
87
+ practice before this was extracted).
88
+ query_suffix:
89
+ An already-escaped, already-``&``-prefixed query-string suffix
90
+ (e.g. ``"&collection_id=foo&item_id=bar"`` or ``"&planet=mars"``)
91
+ appended to every format-switch link so re-fetching PROV-JSON/-N/-XML
92
+ preserves the same scope shown here. Empty string (default) for an
93
+ unscoped, whole-catalog document.
94
+ footer_extra_html:
95
+ Extra HTML appended to the footer paragraph (e.g. a link to a
96
+ human-readable licenses/citation page) -- already-safe HTML,
97
+ same escaping contract as *subtitle_html*. Empty by default.
98
+
99
+ Returns
100
+ -------
101
+ str
102
+ A complete, standalone HTML document.
103
+ """
104
+ return f"""<!doctype html>
105
+ <html lang="en">
106
+ <head>
107
+ <meta charset="utf-8">
108
+ <meta name="viewport" content="width=device-width, initial-scale=1">
109
+ <title>Provenance (PROV-DM) — {escape(base)}</title>
110
+ <script src="{_VIZ_JS_BASE}/viz.js" integrity="{_VIZ_JS_SRI}" crossorigin="anonymous"></script>
111
+ <script src="{_VIZ_JS_BASE}/full.render.js" integrity="{_VIZ_FULL_RENDER_JS_SRI}" crossorigin="anonymous"></script>
112
+ <style>
113
+ :root {{ color-scheme: light dark; }}
114
+ body {{
115
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
116
+ margin: 0; padding: 1.5rem 2rem; color: #1a1a1a; background: #fff;
117
+ }}
118
+ h1 {{ font-size: 1.15rem; margin: 0 0 0.25rem; }}
119
+ p.sub {{ color: #666; margin: 0 0 1.25rem; font-size: 0.9rem; }}
120
+ #graph {{ overflow: auto; max-height: 80vh; border: 1px solid #ddd; border-radius: 6px; padding: 0.5rem; background: #fafafa; }}
121
+ #graph svg {{ display: block; }}
122
+ .legend {{ display: flex; gap: 1.5rem; margin: 0 0 1rem; font-size: 0.85rem; flex-wrap: wrap; }}
123
+ .swatch {{ display: inline-block; width: 0.9rem; height: 0.9rem; border-radius: 3px; margin-right: 0.35rem; vertical-align: -1px; }}
124
+ .formats {{ margin-top: 1.25rem; font-size: 0.9rem; }}
125
+ .formats a {{ margin-right: 1rem; }}
126
+ footer {{ margin-top: 1.5rem; font-size: 0.8rem; color: #888; }}
127
+ footer a {{ color: inherit; }}
128
+ #error {{ color: #b00020; white-space: pre-wrap; font-family: ui-monospace, monospace; margin: 0; }}
129
+ </style>
130
+ </head>
131
+ <body>
132
+ <h1>W3C PROV-DM provenance</h1>
133
+ <p class="sub">{subtitle_html}</p>
134
+
135
+ <div class="legend">
136
+ <span><span class="swatch" style="background:#FFFC87;border:1px solid #808080"></span>Entity</span>
137
+ <span><span class="swatch" style="background:#9FB1FC;border:1px solid #0000FF"></span>Activity</span>
138
+ <span><span class="swatch" style="background:#FED37F;border:1px solid #D2691E"></span>Agent</span>
139
+ <span style="color:#888">Rendered at full size — scroll to see the whole graph.</span>
140
+ </div>
141
+
142
+ <div id="graph"><p id="error" hidden></p></div>
143
+
144
+ <pre id="dot-source" hidden>{escape(dot)}</pre>
145
+
146
+ <div class="formats">
147
+ Machine-readable: <a href="?format=json{query_suffix}">PROV-JSON</a><a href="?format=provn{query_suffix}">PROV-N</a><a href="?format=xml{query_suffix}">PROV-XML</a>
148
+ </div>
149
+
150
+ <footer>
151
+ Rendered with the colour convention used throughout the PROV community (W3C PROV Primer,
152
+ ProvStore, ProvToolbox). The PROV-JSON / PROV-N / PROV-XML above are plain PROV-DM documents
153
+ that can be pasted or uploaded into
154
+ <a href="https://openprovenance.org/store/" target="_blank" rel="noopener noreferrer">ProvStore</a>
155
+ or any other PROV-DM-compliant tool.{footer_extra_html}
156
+ </footer>
157
+
158
+ <script>
159
+ var dotSource = document.getElementById('dot-source').textContent;
160
+ new Viz().renderSVGElement(dotSource)
161
+ .then(function (el) {{ document.getElementById('graph').appendChild(el); }})
162
+ .catch(function (err) {{
163
+ var e = document.getElementById('error');
164
+ e.hidden = false;
165
+ e.textContent = 'Graph rendering failed: ' + err;
166
+ }});
167
+ </script>
168
+ </body>
169
+ </html>
170
+ """
@@ -0,0 +1,94 @@
1
+ # PDSSP Prov Toolkit - Shared W3C PROV-DM vocabulary, document helpers, and Graphviz/HTML rendering for FAIR-transformation provenance across PDSSP services.
2
+ # Copyright (C) 2026 - CNES (Jean-Christophe Malapert for PDSSP)
3
+ # This file is part of PDSSP Prov Toolkit <https://gitlab.cnes.fr/pdssp/pdssp_prov_toolkit>
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """
7
+ PROV-DM / FOAF vocabulary.
8
+ ============================
9
+ Plain string constants for the attribute keys, record-type values and
10
+ qualified-relation attribute names used throughout this toolkit's own
11
+ ``document``/``dot`` modules, and by every service building a
12
+ :class:`~prov.model.ProvDocument` with them.
13
+
14
+ This is the union of the vocabulary independently grown by two PDSSP
15
+ services (``ode_stac_proxy`` and ``geocoding-api``) before this package
16
+ existed -- extracted here specifically because both had already drifted
17
+ from each other (one had gained ``PLAN``/``COLLECTION``/``IDENTIFIER``, the
18
+ other ``LICENSE``/``CRS``/``INFORMED``/``INFORMANT``) without either
19
+ noticing the other's additions. Nothing here is specific to either
20
+ service's own domain: a consumer is free to only ever use the subset it
21
+ needs.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+
27
+ class ProvAttr:
28
+ """PROV-DM (and FOAF) attribute keys used on records built by this
29
+ toolkit (or by any caller building its own).
30
+ """
31
+
32
+ TYPE = "prov:type"
33
+ LABEL = "prov:label"
34
+ LOCATION = "prov:location"
35
+ #: Custom attributes below (not part of PROV-DM's core vocabulary) all
36
+ #: deliberately skip the ``prov:`` prefix -- that namespace is reserved
37
+ #: for the fixed set of terms PROV-XML's schema actually knows, and a
38
+ #: validator (ProvToolbox/ProvStore) rejects an unrecognized ``prov:*``
39
+ #: element/attribute outright rather than treating it as an extension
40
+ #: point. A bare name instead resolves against the document's own
41
+ #: default namespace, which is how any extension attribute must be
42
+ #: declared.
43
+ VERSION = "version"
44
+ #: The identifier (e.g. a STAC item/collection id) an entity represents.
45
+ IDENTIFIER = "identifier"
46
+ #: Machine-readable schema URL alongside a human-readable LOCATION.
47
+ SCHEMA = "schema"
48
+ #: The license terms an entity is distributed/available under.
49
+ LICENSE = "license"
50
+ #: The coordinate reference system an entity's geometry uses.
51
+ CRS = "crs"
52
+ FOAF_NAME = "foaf:name"
53
+ FOAF_HOMEPAGE = "foaf:homepage"
54
+
55
+
56
+ class ProvType:
57
+ """PROV-DM record-type values used as a record's :data:`ProvAttr.TYPE`."""
58
+
59
+ COLLECTION = "prov:Collection"
60
+ ENTITY = "prov:Entity"
61
+ ACTIVITY = "prov:Activity"
62
+ PLAN = "prov:Plan"
63
+ PERSON = "prov:Person"
64
+ ORGANIZATION = "prov:Organization"
65
+ SOFTWARE_AGENT = "prov:SoftwareAgent"
66
+
67
+
68
+ class ProvQualifier:
69
+ """Lowercase ``formal_attributes`` keys naming the endpoints of
70
+ ``prov``'s qualified n-ary relations (``ProvGeneration``, ``ProvUsage``,
71
+ ``ProvDerivation``, ``ProvAssociation``, ``ProvAttribution``,
72
+ ``ProvDelegation``, ``ProvMembership``, ``ProvCommunication``) --
73
+ distinct from :class:`ProvAttr` (descriptive attributes on a record)
74
+ and :class:`ProvType`'s capitalized record-type values.
75
+
76
+ Consumed by :func:`~.dot.prov_to_dot` to draw each relation as a graph
77
+ edge; a caller building relations via ``prov``'s own
78
+ ``doc.wasGeneratedBy()``/``doc.used()``/... helpers never needs these
79
+ directly (``prov`` sets the qualifiers internally) -- they matter only
80
+ to code that reads a relation's ``formal_attributes`` back out, as
81
+ :mod:`.dot` does.
82
+ """
83
+
84
+ ENTITY = "prov:entity"
85
+ ACTIVITY = "prov:activity"
86
+ AGENT = "prov:agent"
87
+ PLAN = "prov:plan"
88
+ GENERATED_ENTITY = "prov:generatedEntity"
89
+ USED_ENTITY = "prov:usedEntity"
90
+ DELEGATE = "prov:delegate"
91
+ RESPONSIBLE = "prov:responsible"
92
+ COLLECTION = "prov:collection"
93
+ INFORMED = "prov:informed"
94
+ INFORMANT = "prov:informant"