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.
- pdssp_prov_toolkit-1.0.1/PKG-INFO +176 -0
- pdssp_prov_toolkit-1.0.1/README.md +165 -0
- pdssp_prov_toolkit-1.0.1/pyproject.toml +81 -0
- pdssp_prov_toolkit-1.0.1/pyproject.toml.orig +77 -0
- pdssp_prov_toolkit-1.0.1/src/pdssp_prov_toolkit/__init__.py +48 -0
- pdssp_prov_toolkit-1.0.1/src/pdssp_prov_toolkit/_version.py +19 -0
- pdssp_prov_toolkit-1.0.1/src/pdssp_prov_toolkit/document.py +97 -0
- pdssp_prov_toolkit-1.0.1/src/pdssp_prov_toolkit/dot.py +253 -0
- pdssp_prov_toolkit-1.0.1/src/pdssp_prov_toolkit/html.py +170 -0
- pdssp_prov_toolkit-1.0.1/src/pdssp_prov_toolkit/vocab.py +94 -0
|
@@ -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
|
+
[]()
|
|
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
|
+
[]()
|
|
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"
|