prezmanifest 0.2.2__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.
- prezmanifest-0.2.2/LICENSE +29 -0
- prezmanifest-0.2.2/PKG-INFO +60 -0
- prezmanifest-0.2.2/README.adoc +43 -0
- prezmanifest-0.2.2/prezmanifest/__init__.py +9 -0
- prezmanifest-0.2.2/prezmanifest/definednamespaces.py +37 -0
- prezmanifest-0.2.2/prezmanifest/documentor.py +209 -0
- prezmanifest-0.2.2/prezmanifest/get_iris_from_files.py +14 -0
- prezmanifest-0.2.2/prezmanifest/labeller.py +188 -0
- prezmanifest-0.2.2/prezmanifest/loader.py +244 -0
- prezmanifest-0.2.2/prezmanifest/mrr.ttl +87 -0
- prezmanifest-0.2.2/prezmanifest/utils.py +39 -0
- prezmanifest-0.2.2/prezmanifest/validator.py +104 -0
- prezmanifest-0.2.2/prezmanifest/validator.ttl +126 -0
- prezmanifest-0.2.2/pyproject.toml +24 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025, KurrawongAI
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: prezmanifest
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: A Python package that provides a series of functions to work with Prez Manifests.
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Author: Nicholas Car
|
|
7
|
+
Author-email: nick@kurrawong.ai
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: kurra (>=0.6.1,<0.7.0)
|
|
14
|
+
Requires-Dist: labelify (>=0.3.8,<0.4.0)
|
|
15
|
+
Requires-Dist: pyshacl (>=0.29.0,<0.30.0)
|
|
16
|
+
Description-Content-Type: text/plain
|
|
17
|
+
|
|
18
|
+
= Prez Manifest
|
|
19
|
+
|
|
20
|
+
A Prez Manifest is an RDF file that describes and links to a set of resources that can be loaded into an RDF database for the http://prez.dev[Prez graph database publication system] to provide access to.
|
|
21
|
+
|
|
22
|
+
The Prez Manifest specification is online at:
|
|
23
|
+
|
|
24
|
+
* https://prez.dev/manifest/
|
|
25
|
+
|
|
26
|
+
This repository contains the `prez-manifest` Python package that provides a series of functions to work with Prez Manifests. The functions provided are:
|
|
27
|
+
|
|
28
|
+
* `create_table`: creates an ASCIIDOC or Markdown table of Manifest content from a Manifest file
|
|
29
|
+
* `validate`: validates that a Manifest file conforms to the specification and that all linked-to assets are available
|
|
30
|
+
* `load`: loads a Manifest file, and all the content it specifies, into either an n-quads file or a Fuseki database
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
== Installation & Use
|
|
34
|
+
|
|
35
|
+
This Python package is intended to be used on the command line on Linux/UNIX-like systems and/or as a Python library, called directly from other Python code.
|
|
36
|
+
|
|
37
|
+
It can be installed using https://python-poetry.org[Poetry]. It may one day be published on https://pypi.org[PyPI].
|
|
38
|
+
|
|
39
|
+
Please see the `documentor.py`, `loader.py`, & `validator.py` files in the `prezmanifest` folder and the test files in `test` for documentation text and examples of use.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
== Testing
|
|
43
|
+
|
|
44
|
+
Run `python -m pytest` in the top-level folder to test. You must have Docker Desktop running to allow all loader tests to be executed.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
== License
|
|
48
|
+
|
|
49
|
+
This code is available for reuse according to the https://opensource.org/license/bsd-3-clause[BSD 3-Clause License].
|
|
50
|
+
|
|
51
|
+
© 2024-2025 KurrawongAI
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
== Contact
|
|
55
|
+
|
|
56
|
+
For all matters, please contact:
|
|
57
|
+
|
|
58
|
+
*KurrawongAI* +
|
|
59
|
+
info@kurrawong.ai +
|
|
60
|
+
https://kurrawong.ai +
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
= Prez Manifest
|
|
2
|
+
|
|
3
|
+
A Prez Manifest is an RDF file that describes and links to a set of resources that can be loaded into an RDF database for the http://prez.dev[Prez graph database publication system] to provide access to.
|
|
4
|
+
|
|
5
|
+
The Prez Manifest specification is online at:
|
|
6
|
+
|
|
7
|
+
* https://prez.dev/manifest/
|
|
8
|
+
|
|
9
|
+
This repository contains the `prez-manifest` Python package that provides a series of functions to work with Prez Manifests. The functions provided are:
|
|
10
|
+
|
|
11
|
+
* `create_table`: creates an ASCIIDOC or Markdown table of Manifest content from a Manifest file
|
|
12
|
+
* `validate`: validates that a Manifest file conforms to the specification and that all linked-to assets are available
|
|
13
|
+
* `load`: loads a Manifest file, and all the content it specifies, into either an n-quads file or a Fuseki database
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
== Installation & Use
|
|
17
|
+
|
|
18
|
+
This Python package is intended to be used on the command line on Linux/UNIX-like systems and/or as a Python library, called directly from other Python code.
|
|
19
|
+
|
|
20
|
+
It can be installed using https://python-poetry.org[Poetry]. It may one day be published on https://pypi.org[PyPI].
|
|
21
|
+
|
|
22
|
+
Please see the `documentor.py`, `loader.py`, & `validator.py` files in the `prezmanifest` folder and the test files in `test` for documentation text and examples of use.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
== Testing
|
|
26
|
+
|
|
27
|
+
Run `python -m pytest` in the top-level folder to test. You must have Docker Desktop running to allow all loader tests to be executed.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
== License
|
|
31
|
+
|
|
32
|
+
This code is available for reuse according to the https://opensource.org/license/bsd-3-clause[BSD 3-Clause License].
|
|
33
|
+
|
|
34
|
+
© 2024-2025 KurrawongAI
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
== Contact
|
|
38
|
+
|
|
39
|
+
For all matters, please contact:
|
|
40
|
+
|
|
41
|
+
*KurrawongAI* +
|
|
42
|
+
info@kurrawong.ai +
|
|
43
|
+
https://kurrawong.ai +
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
__version__ = importlib.metadata.version(__package__)
|
|
4
|
+
|
|
5
|
+
from .definednamespaces import MRR, OLIS, PREZ
|
|
6
|
+
from .validator import validate
|
|
7
|
+
from .documentor import create_table
|
|
8
|
+
from .loader import load
|
|
9
|
+
from .labeller import label
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from rdflib.namespace import DefinedNamespace, Namespace
|
|
2
|
+
from rdflib.term import URIRef
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MRR(DefinedNamespace):
|
|
6
|
+
|
|
7
|
+
_NS = Namespace("https://prez.dev/ManifestResourceRoles/")
|
|
8
|
+
_fail = True
|
|
9
|
+
|
|
10
|
+
CatalogueData: URIRef
|
|
11
|
+
CatalogueModel: URIRef
|
|
12
|
+
ResourceData: URIRef
|
|
13
|
+
ResourceModel: URIRef
|
|
14
|
+
CatalogueAndResourceModel: URIRef
|
|
15
|
+
CompleteCatalogueAndResourceLabels: URIRef
|
|
16
|
+
IncompleteCatalogueAndResourceLabels: URIRef
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OLIS(DefinedNamespace):
|
|
20
|
+
|
|
21
|
+
_NS = Namespace("https://olis.dev/")
|
|
22
|
+
_fail = True
|
|
23
|
+
|
|
24
|
+
NamedGraph: URIRef
|
|
25
|
+
RealGraph: URIRef
|
|
26
|
+
SystemGraph: URIRef
|
|
27
|
+
VirtualGraph: URIRef
|
|
28
|
+
|
|
29
|
+
isAliasFor: URIRef
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PREZ(DefinedNamespace):
|
|
33
|
+
|
|
34
|
+
_NS = Namespace("https://prez.dev/")
|
|
35
|
+
_fail = True
|
|
36
|
+
|
|
37
|
+
Manifest: URIRef
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This script creates a "Prez Resources" table in either Markdown or ASCIIDOC from a Manifest file which it validates first
|
|
3
|
+
|
|
4
|
+
Run this script with the -h flag for more help, i.e. ~$ python documentor.py -h
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
|
|
8
|
+
Input:
|
|
9
|
+
|
|
10
|
+
PREFIX mrr: <https://prez.dev/ManifestResourceRoles/>
|
|
11
|
+
PREFIX prez: <https://prez.dev/>
|
|
12
|
+
PREFIX prof: <http://www.w3.org/ns/dx/prof/>
|
|
13
|
+
PREFIX schema: <https://schema.org/>
|
|
14
|
+
|
|
15
|
+
[]
|
|
16
|
+
a prez:Manifest ;
|
|
17
|
+
prof:hasResource
|
|
18
|
+
[
|
|
19
|
+
prof:hasArtifact "catalogue.ttl" ;
|
|
20
|
+
prof:hasRole mrr:ContainerData ;
|
|
21
|
+
schema:description "The definition of, and medata for, the container which here is a dcat:Catalog object" ;
|
|
22
|
+
schema:name "Catalogue Definition"
|
|
23
|
+
] ,
|
|
24
|
+
[
|
|
25
|
+
prof:hasArtifact "vocabs/*.ttl" ;
|
|
26
|
+
prof:hasRole mrr:ContentData ;
|
|
27
|
+
schema:description "skos:ConceptScheme objects in RDF (Turtle) files in the vocabs/ folder" ;
|
|
28
|
+
schema:name "Content"
|
|
29
|
+
] ,
|
|
30
|
+
[
|
|
31
|
+
prof:hasArtifact "https://github.com/RDFLib/prez/blob/main/prez/reference_data/profiles/ogc_records_profile.ttl" ;
|
|
32
|
+
prof:hasRole mrr:ContainerAndContentModel ;
|
|
33
|
+
schema:description "The default Prez profile for Records API" ;
|
|
34
|
+
schema:name "Profile Definition"
|
|
35
|
+
] ,
|
|
36
|
+
[
|
|
37
|
+
prof:hasArtifact "_background/labels.ttl" ;
|
|
38
|
+
prof:hasRole mrr:CompleteContainerAndContentLabels ;
|
|
39
|
+
schema:description "An RDF file containing all the labels for the container content" ;
|
|
40
|
+
] ;
|
|
41
|
+
.
|
|
42
|
+
|
|
43
|
+
Output:
|
|
44
|
+
|
|
45
|
+
Resource | Role | Description
|
|
46
|
+
--- | --- | ---
|
|
47
|
+
Catalogue Definition, [`catalogue.ttl`](catalogue.ttl) | [Container Data](https://prez.dev/ManifestResourceRoles/ContainerData) | The definition of, and medata for, the container which here is a dcat:Catalog object
|
|
48
|
+
Content, [`vocabs/*.ttl`](vocabs/*.ttl) | [Content Data](https://prez.dev/ManifestResourceRoles/ContentData) | skos:ConceptScheme objects in RDF (Turtle) files in the vocabs/ folder
|
|
49
|
+
Profile Definition, [`ogc_records_profile.ttl`](https://github.com/RDFLib/prez/blob/main/prez/reference_data/profiles/ogc_records_profile.ttl) | [Container & Content Model](https://prez.dev/ManifestResourceRoles/ContainerAndContentModel) | The default Prez profile for Records API
|
|
50
|
+
Labels file, [`_background/labels.ttl`](_background/labels.ttl) | [Complete Content and Container Labels](https://prez.dev/ManifestResourceRoles/CompleteContainerAndContentLabels) | An RDF file containing all the labels for the container content
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import argparse
|
|
54
|
+
import sys
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from urllib.parse import ParseResult, urlparse
|
|
57
|
+
|
|
58
|
+
from rdflib import DCAT, DCTERMS, PROF, SDO, SKOS, Graph
|
|
59
|
+
from kurra.utils import load_graph
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
from prezmanifest import MRR, validate, __version__
|
|
63
|
+
from prezmanifest.utils import *
|
|
64
|
+
except ImportError:
|
|
65
|
+
import sys
|
|
66
|
+
|
|
67
|
+
sys.path.append(str(Path(__file__).parent.parent.resolve()))
|
|
68
|
+
from prezmanifest import MRR, validate, __version__
|
|
69
|
+
from prezmanifest.utils import *
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_table(manifest: Path, t="markdown") -> str:
|
|
73
|
+
# load and validate manifest
|
|
74
|
+
validate(manifest)
|
|
75
|
+
manifest_graph = load_graph(manifest)
|
|
76
|
+
|
|
77
|
+
# add in MRR vocab
|
|
78
|
+
manifest_graph += load_graph(Path(__file__).parent / "mrr.ttl")
|
|
79
|
+
|
|
80
|
+
if t == "asciidoc":
|
|
81
|
+
header = "|===\n| Resource | Role | Description\n\n"
|
|
82
|
+
else:
|
|
83
|
+
header = "Resource | Role | Description\n--- | --- | ---\n"
|
|
84
|
+
|
|
85
|
+
body = ""
|
|
86
|
+
for s, o in manifest_graph.subject_objects(PROF.hasResource):
|
|
87
|
+
a = str(manifest_graph.value(o, PROF.hasArtifact))
|
|
88
|
+
if t == "asciidoc":
|
|
89
|
+
artifact = f'link:{a}[`{a.split("/")[-1] if a.startswith("http") else a}`]'
|
|
90
|
+
else:
|
|
91
|
+
artifact = f'[`{a.split("/")[-1] if a.startswith("http") else a}`]({a})'
|
|
92
|
+
role_iri = manifest_graph.value(o, PROF.hasRole)
|
|
93
|
+
role_label = manifest_graph.value(role_iri, SKOS.prefLabel)
|
|
94
|
+
if t == "asciidoc":
|
|
95
|
+
role = f"{role_iri}[{role_label}]"
|
|
96
|
+
else:
|
|
97
|
+
role = f"[{role_label}]({role_iri})"
|
|
98
|
+
name = manifest_graph.value(o, SDO.name)
|
|
99
|
+
description = manifest_graph.value(o, SDO.description)
|
|
100
|
+
n = f"{name}, {artifact}" if name is not None else f"{artifact}"
|
|
101
|
+
d = description if description is not None else ""
|
|
102
|
+
if t == "asciidoc":
|
|
103
|
+
body += f"| {n} | {role} | {d}\n"
|
|
104
|
+
else:
|
|
105
|
+
body += f"{n} | {role} | {d}\n"
|
|
106
|
+
|
|
107
|
+
if t == "asciidoc":
|
|
108
|
+
footer = "|===\n"
|
|
109
|
+
else:
|
|
110
|
+
footer = ""
|
|
111
|
+
|
|
112
|
+
return (header + body + footer).strip()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def create_catalogue(manifest: Path):
|
|
116
|
+
MANIFEST_ROOT_DIR = manifest.parent
|
|
117
|
+
# load and validate manifest
|
|
118
|
+
validate(manifest)
|
|
119
|
+
manifest_graph = load_graph(manifest)
|
|
120
|
+
|
|
121
|
+
for s, o in manifest_graph.subject_objects(PROF.hasResource):
|
|
122
|
+
for role in manifest_graph.objects(o, PROF.hasRole):
|
|
123
|
+
if role == MRR.CatalogueData:
|
|
124
|
+
for artifact in manifest_graph.objects(o, PROF.hasArtifact):
|
|
125
|
+
# the artifact can only be a triples file (not a quads file)
|
|
126
|
+
catalogue = load_graph(MANIFEST_ROOT_DIR / artifact)
|
|
127
|
+
|
|
128
|
+
# get the IRI of the catalogue
|
|
129
|
+
catalogue_iri = catalogue.value(predicate=RDF.type, object=DCAT.Catalog)
|
|
130
|
+
|
|
131
|
+
# non-catalogue resources
|
|
132
|
+
for s, o in manifest_graph.subject_objects(PROF.hasResource):
|
|
133
|
+
for role in manifest_graph.objects(o, PROF.hasRole):
|
|
134
|
+
if role in [
|
|
135
|
+
MRR.CompleteCatalogueAndResourceLabels,
|
|
136
|
+
MRR.IncompleteCatalogueAndResourceLabels,
|
|
137
|
+
MRR.ResourceData,
|
|
138
|
+
]:
|
|
139
|
+
for artifact in manifest_graph.objects(o, PROF.hasArtifact):
|
|
140
|
+
for f in get_files_from_artifact(manifest, artifact):
|
|
141
|
+
for iri in sorted(get_identifier_from_file(f)):
|
|
142
|
+
if iri != URIRef("urn:x-rdflib:default"):
|
|
143
|
+
catalogue.add((catalogue_iri, DCTERMS.hasPart, iri))
|
|
144
|
+
|
|
145
|
+
return catalogue
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def setup_cli_parser(args=None):
|
|
149
|
+
def url_file_or_folder(input: str) -> ParseResult | Path:
|
|
150
|
+
parsed = urlparse(input)
|
|
151
|
+
if all([parsed.scheme, parsed.netloc]):
|
|
152
|
+
return parsed
|
|
153
|
+
path = Path(input)
|
|
154
|
+
if path.is_file():
|
|
155
|
+
return path
|
|
156
|
+
if path.is_dir():
|
|
157
|
+
return path
|
|
158
|
+
raise argparse.ArgumentTypeError(
|
|
159
|
+
f"{input} is not a valid input. Must be a file, folder or sparql endpoint"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
parser = argparse.ArgumentParser()
|
|
163
|
+
|
|
164
|
+
parser.add_argument(
|
|
165
|
+
"-v",
|
|
166
|
+
"--version",
|
|
167
|
+
action="version",
|
|
168
|
+
version="{version}".format(version=__version__),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"function",
|
|
173
|
+
help="The documentation function you wish to perform",
|
|
174
|
+
choices=["table", "catalogue"],
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
"-t",
|
|
179
|
+
"--type",
|
|
180
|
+
help="The type of markup you want to export: Markdown or ASCCIDOC. Only relevant for the 'table' function",
|
|
181
|
+
choices=["markdown", "asciidoc"],
|
|
182
|
+
default="markdown",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"input",
|
|
187
|
+
help="A Prez Manifest file",
|
|
188
|
+
type=url_file_or_folder,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return parser.parse_args(args)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def cli(args=None):
|
|
195
|
+
if args is None:
|
|
196
|
+
args = sys.argv[1:]
|
|
197
|
+
|
|
198
|
+
args = setup_cli_parser(args)
|
|
199
|
+
|
|
200
|
+
if args.function == "table":
|
|
201
|
+
print(create_table(args.input, t=args.type))
|
|
202
|
+
else:
|
|
203
|
+
print(create_catalogue(args.input).serialize(format="longturtle"))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
retval = cli(sys.argv[1:])
|
|
208
|
+
if retval is not None:
|
|
209
|
+
sys.exit(retval)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from rdflib import Graph
|
|
3
|
+
from rdflib.namespace import RDF, SKOS
|
|
4
|
+
|
|
5
|
+
FILES_DIR = Path("something")
|
|
6
|
+
|
|
7
|
+
for f in FILES_DIR.glob("*.ttl"):
|
|
8
|
+
g = Graph()
|
|
9
|
+
g.parse(f)
|
|
10
|
+
iri = g.value(predicate=RDF.type, object=SKOS.ConceptScheme)
|
|
11
|
+
|
|
12
|
+
# print(f"{f.name}, {iri}")
|
|
13
|
+
|
|
14
|
+
print(f"<{iri}> ,")
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Assesses a given Manifest, finds any IRIs in any of the given resources missing labels and tries to patch them from
|
|
3
|
+
a given source of labels, such as KurrawongAI's Semantic Background (https://github.com/kurrawong/semantic-background)
|
|
4
|
+
repository.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal as TLiteral
|
|
10
|
+
from urllib.parse import ParseResult, urlparse
|
|
11
|
+
|
|
12
|
+
from kurra.utils import load_graph
|
|
13
|
+
from labelify import find_missing_labels, extract_labels
|
|
14
|
+
from rdflib import Graph, BNode, Literal
|
|
15
|
+
from rdflib.namespace import PROF, RDF
|
|
16
|
+
|
|
17
|
+
from prezmanifest.utils import get_files_from_artifact
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from prezmanifest import MRR, OLIS, validate, load, __version__
|
|
21
|
+
except ImportError:
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
sys.path.append(str(Path(__file__).parent.parent.resolve()))
|
|
25
|
+
from prezmanifest import MRR, OLIS, validate, __version__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def label(
|
|
29
|
+
manifest: Path,
|
|
30
|
+
output: TLiteral["iris", "rdf", "manifest"] = "manifest",
|
|
31
|
+
additional_context: Path | str | Graph = None,
|
|
32
|
+
) -> set | Graph | None:
|
|
33
|
+
""""Main function for labeller module"""
|
|
34
|
+
# create the target from the Manifest
|
|
35
|
+
manifest_content_graph = load(manifest, return_data_type="Graph")
|
|
36
|
+
|
|
37
|
+
output_types = ["iris", "rdf", "manifest"]
|
|
38
|
+
if output not in output_types:
|
|
39
|
+
raise ValueError(f"Parameter output is {output} but must be one of {', '.join(output_types)}")
|
|
40
|
+
|
|
41
|
+
# determine if any labelling context is given in Manifest
|
|
42
|
+
context_graph = Graph()
|
|
43
|
+
for s, o in manifest_content_graph.subject_objects(PROF.hasResource):
|
|
44
|
+
for role in manifest_content_graph.objects(o, PROF.hasRole):
|
|
45
|
+
if role in [
|
|
46
|
+
MRR.IncompleteCatalogueAndResourceLabels,
|
|
47
|
+
MRR.CompleteCatalogueAndResourceLabels,
|
|
48
|
+
]:
|
|
49
|
+
for artifact in manifest_content_graph.objects(o, PROF.hasArtifact):
|
|
50
|
+
artifact: Literal
|
|
51
|
+
for f in get_files_from_artifact(manifest, artifact):
|
|
52
|
+
context_graph += load_graph(f)
|
|
53
|
+
|
|
54
|
+
if output == "iris":
|
|
55
|
+
return find_missing_labels(manifest_content_graph + context_graph, additional_context)
|
|
56
|
+
|
|
57
|
+
elif output == "rdf":
|
|
58
|
+
iris = find_missing_labels(manifest_content_graph, context_graph)
|
|
59
|
+
|
|
60
|
+
if additional_context is not None:
|
|
61
|
+
return extract_labels(iris, additional_context)
|
|
62
|
+
else:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
else: # output == manifest
|
|
66
|
+
# If this is selected, generate the "rdf" output and create a resource for it in the Manifest
|
|
67
|
+
# If there are no more missing labels then we have an mrr:CompleteCatalogueAndResourceLabels
|
|
68
|
+
# else add mrr:IncompleteCatalogueAndResourceLabels
|
|
69
|
+
|
|
70
|
+
# Generate labels for any IRIs missing them, using context given in the Manifest and any
|
|
71
|
+
# Additional Context supplied
|
|
72
|
+
manifest_only_graph = load_graph(manifest)
|
|
73
|
+
rdf_addition = label(manifest, "rdf", additional_context)
|
|
74
|
+
|
|
75
|
+
if len(rdf_addition) > 0:
|
|
76
|
+
new_artifact = manifest.parent / "labels-additional.ttl"
|
|
77
|
+
rdf_addition.serialize(destination=new_artifact, format="longturtle")
|
|
78
|
+
new_resource = BNode()
|
|
79
|
+
|
|
80
|
+
# Find the role of any context in the Manifest
|
|
81
|
+
manifest_iri = None
|
|
82
|
+
context_roles = []
|
|
83
|
+
for s, o in manifest_only_graph.subject_objects(PROF.hasResource):
|
|
84
|
+
manifest_iri = s
|
|
85
|
+
for role in manifest_only_graph.objects(o, PROF.hasRole):
|
|
86
|
+
if role in [
|
|
87
|
+
MRR.IncompleteCatalogueAndResourceLabels,
|
|
88
|
+
MRR.CompleteCatalogueAndResourceLabels,
|
|
89
|
+
]:
|
|
90
|
+
context_roles.append(role)
|
|
91
|
+
|
|
92
|
+
if MRR.CompleteCatalogueAndResourceLabels in context_roles and len(context_roles) == 1:
|
|
93
|
+
# If a CompleteCatalogueAndResourceLabels is present in Manifest and yet more labels were discovered,
|
|
94
|
+
# change CompleteCatalogueAndResourceLabels to IncompleteCatalogueAndResourceLabels and add another
|
|
95
|
+
for s, o in manifest_content_graph.subject_objects(PROF.hasRole):
|
|
96
|
+
if o == MRR.CompleteCatalogueAndResourceLabels:
|
|
97
|
+
manifest_only_graph.remove((s, PROF.hasRole, o))
|
|
98
|
+
manifest_only_graph.add((manifest_iri, PROF.hasResource, new_resource))
|
|
99
|
+
manifest_only_graph.add((new_resource, PROF.hasRole, MRR.IncompleteCatalogueAndResourceLabels))
|
|
100
|
+
manifest_only_graph.add((new_resource, PROF.hasArtifact, Literal(new_artifact.name)))
|
|
101
|
+
else:
|
|
102
|
+
# If an IncompleteCatalogueAndResourceLabels was present, add another IncompleteCatalogueAndResourceLabels
|
|
103
|
+
# which together make a CompleteCatalogueAndResourceLabels
|
|
104
|
+
|
|
105
|
+
# If none was present, add an IncompleteCatalogueAndResourceLabels or a CompleteCatalogueAndResourceLabels
|
|
106
|
+
# TODO: test for completeness of labelling and add in CompleteCatalogueAndResourceLabels if complete
|
|
107
|
+
manifest_only_graph.add((manifest_iri, PROF.hasResource, new_resource))
|
|
108
|
+
manifest_only_graph.add((new_resource, PROF.hasRole, MRR.IncompleteCatalogueAndResourceLabels))
|
|
109
|
+
manifest_only_graph.add((new_resource, PROF.hasArtifact, Literal(new_artifact.name)))
|
|
110
|
+
|
|
111
|
+
manifest_only_graph.serialize(destination=manifest, format="longturtle")
|
|
112
|
+
|
|
113
|
+
else:
|
|
114
|
+
raise Warning("No new labels have been generated for content in this Manifest. "
|
|
115
|
+
"This could be because none were missing or because no new labels can be found in any "
|
|
116
|
+
"supplied additional context.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def setup_cli_parser(args=None):
|
|
120
|
+
def url_file_or_folder(input: str) -> ParseResult | Path:
|
|
121
|
+
parsed = urlparse(input)
|
|
122
|
+
if all([parsed.scheme, parsed.netloc]):
|
|
123
|
+
return parsed
|
|
124
|
+
path = Path(input)
|
|
125
|
+
if path.is_file():
|
|
126
|
+
return path
|
|
127
|
+
if path.is_dir():
|
|
128
|
+
return path
|
|
129
|
+
raise argparse.ArgumentTypeError(
|
|
130
|
+
f"{input} is not a valid input. Must be a file, folder or sparql endpoint"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
parser = argparse.ArgumentParser()
|
|
134
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
135
|
+
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"-v",
|
|
138
|
+
"--version",
|
|
139
|
+
action="version",
|
|
140
|
+
version="{version}".format(version=__version__),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
group.add_argument(
|
|
144
|
+
"-o",
|
|
145
|
+
"--output",
|
|
146
|
+
help="The form of output you want",
|
|
147
|
+
choices=["iris", "rdf", "manifest"],
|
|
148
|
+
default="manifest",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"-a",
|
|
153
|
+
"--additional-context",
|
|
154
|
+
help="File, Folder or Sparql Endpoint to read additional context RDF from",
|
|
155
|
+
type=url_file_or_folder,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"manifest",
|
|
160
|
+
help="A Manifest file to process",
|
|
161
|
+
type=Path,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return parser.parse_args(args)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cli(args=None):
|
|
168
|
+
if args is None:
|
|
169
|
+
args = sys.argv[1:]
|
|
170
|
+
|
|
171
|
+
args = setup_cli_parser(args)
|
|
172
|
+
|
|
173
|
+
x = label(args.manifest, args.output, args.additional_context)
|
|
174
|
+
|
|
175
|
+
if args.output == "iris":
|
|
176
|
+
print("\n".join([str(iri) for iri in x]))
|
|
177
|
+
elif args.output == "rdf":
|
|
178
|
+
if x is not None:
|
|
179
|
+
print(x.serialize(format="longturtle"))
|
|
180
|
+
|
|
181
|
+
else: # manifest
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
retval = cli(sys.argv[1:])
|
|
187
|
+
if retval is not None:
|
|
188
|
+
sys.exit(retval)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Either creates an n-quads files containing the content of a Manifest file or uploads the content to Fuseki.
|
|
3
|
+
|
|
4
|
+
It creates:
|
|
5
|
+
|
|
6
|
+
1. A Named Graph for each resource using the item's IRI as the graph IRI
|
|
7
|
+
2. A Named Graph for the catalogue, either using the catalogue's IRI as the graph IRI + "-catalogue" if given, or by making one up - a Blank Node
|
|
8
|
+
3. All the triples in resources with roles mrr:CompleteCatalogueAndResourceLabels & mrr:IncompleteCatalogueAndResourceLabels within a Named Graph with IRI <https://background>
|
|
9
|
+
4. An Olis Virtual Graph, <https://olis.dev/VirtualGraph> object using the catalogue IRI, if give, which is as an alias for all the Named Graphs from 1., 2. & 3.
|
|
10
|
+
5. Multiple entries in the System Graph - Named Graph with IRI <https://olis.dev/SystemGraph> - for each Named and the Virtual Graph from 1., 2. & 3.
|
|
11
|
+
|
|
12
|
+
Run this script with the -h flag for more help, i.e. ~$ python loader.py -h
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from kurra.format import make_dataset, export_quads
|
|
20
|
+
from kurra.fuseki import upload
|
|
21
|
+
from kurra.utils import load_graph
|
|
22
|
+
from rdflib import DCAT, DCTERMS, OWL, PROF, RDF, SDO, SKOS
|
|
23
|
+
from rdflib import Graph, URIRef, Dataset
|
|
24
|
+
from typing import Literal as TLiteral
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from prezmanifest import MRR, OLIS, validate, __version__
|
|
29
|
+
from prezmanifest.utils import get_files_from_artifact
|
|
30
|
+
except ImportError:
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
sys.path.append(str(Path(__file__).parent.parent.resolve()))
|
|
34
|
+
from prezmanifest import MRR, OLIS, validate, __version__
|
|
35
|
+
from prezmanifest.utils import get_files_from_artifact
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load(
|
|
39
|
+
manifest: Path,
|
|
40
|
+
sparql_endpoint: str = None,
|
|
41
|
+
destination_file: Path = None,
|
|
42
|
+
return_data_type: TLiteral["Graph", "Dataset", None] = None
|
|
43
|
+
) -> None | Graph | Dataset:
|
|
44
|
+
"""Loads a catalogue of data from a prezmanifest file, whose content are valid according to the Prez Manifest Model
|
|
45
|
+
(https://kurrawong.github.io/prez.dev/manifest/) either into a specified quads file in the Trig format, or into a
|
|
46
|
+
given SPARQL Endpoint."""
|
|
47
|
+
|
|
48
|
+
if return_data_type == "Dataset":
|
|
49
|
+
dataset_holder = Dataset()
|
|
50
|
+
|
|
51
|
+
if return_data_type == "Graph":
|
|
52
|
+
graph_holder = Graph()
|
|
53
|
+
|
|
54
|
+
return_data_value_error_message = "return_data_type was set to an invalid value. Must be one of Dataset or Graph or None"
|
|
55
|
+
|
|
56
|
+
def _export(data: Graph | Dataset, iri, sparql_endpoint, destination_file, return_data_type, append=False):
|
|
57
|
+
if type(data) is Dataset:
|
|
58
|
+
if iri is not None:
|
|
59
|
+
raise ValueError("If the data is a Dataset, the parameter iri must be None")
|
|
60
|
+
|
|
61
|
+
if destination_file is not None:
|
|
62
|
+
export_quads(data, destination_file)
|
|
63
|
+
elif sparql_endpoint is not None:
|
|
64
|
+
for g in data.graphs():
|
|
65
|
+
if g.identifier != URIRef("urn:x-rdflib:default"):
|
|
66
|
+
_export(g, g.identifier, sparql_endpoint, None, None)
|
|
67
|
+
else:
|
|
68
|
+
if return_data_type == "Dataset":
|
|
69
|
+
return data
|
|
70
|
+
elif return_data_type == "Graph":
|
|
71
|
+
gx = Graph()
|
|
72
|
+
for g in data.graphs():
|
|
73
|
+
if g.identifier != URIRef("urn:x-rdflib:default"):
|
|
74
|
+
for s, p, o in g.triples((None, None, None)):
|
|
75
|
+
gx.add((s, p, o))
|
|
76
|
+
return gx
|
|
77
|
+
|
|
78
|
+
elif type(data) is Graph:
|
|
79
|
+
if iri is None:
|
|
80
|
+
raise ValueError("If the data is a GRaph, the parameter iri must not be None")
|
|
81
|
+
|
|
82
|
+
msg = f"exporting {iri} "
|
|
83
|
+
if destination_file is not None:
|
|
84
|
+
msg += f"to file {destination_file} "
|
|
85
|
+
export_quads(make_dataset(data, iri), destination_file)
|
|
86
|
+
elif sparql_endpoint is not None:
|
|
87
|
+
msg += f"to SPARQL Endpoint {sparql_endpoint}"
|
|
88
|
+
upload(sparql_endpoint, data, iri, append)
|
|
89
|
+
else: # returning data
|
|
90
|
+
if return_data_type == "Dataset":
|
|
91
|
+
msg += "to Dataset"
|
|
92
|
+
for s, p, o in data:
|
|
93
|
+
dataset_holder.add((s, p, o, iri))
|
|
94
|
+
elif return_data_type == "Graph":
|
|
95
|
+
msg += "to Graph"
|
|
96
|
+
for s, p, o in data:
|
|
97
|
+
graph_holder.add((s, p, o))
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(return_data_value_error_message)
|
|
100
|
+
|
|
101
|
+
logging.info(msg)
|
|
102
|
+
|
|
103
|
+
if sum(x is not None for x in [sparql_endpoint, destination_file, return_data_type]) != 1:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"You must specify exactly 1 of sparql_endpoint, destination_file or return_data_type",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
MANIFEST_ROOT_DIR = manifest.parent
|
|
109
|
+
# load and validate manifest
|
|
110
|
+
validate(manifest)
|
|
111
|
+
manifest_graph = load_graph(manifest)
|
|
112
|
+
|
|
113
|
+
vg = Graph()
|
|
114
|
+
vg_iri = None
|
|
115
|
+
|
|
116
|
+
for s, o in manifest_graph.subject_objects(PROF.hasResource):
|
|
117
|
+
for role in manifest_graph.objects(o, PROF.hasRole):
|
|
118
|
+
# The catalogue - must be processed first
|
|
119
|
+
if role == MRR.CatalogueData:
|
|
120
|
+
for artifact in manifest_graph.objects(o, PROF.hasArtifact):
|
|
121
|
+
# load the Catalogue, determine the Virtual Graph & Catalogue IRIs
|
|
122
|
+
# and fail if we can't see a Catalogue object
|
|
123
|
+
c = load_graph(MANIFEST_ROOT_DIR / str(artifact))
|
|
124
|
+
vg_iri = c.value(predicate=RDF.type, object=DCAT.Catalog)
|
|
125
|
+
if vg_iri is None:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"ERROR: Could not create a Virtual Graph as no Catalog found in the Catalogue data"
|
|
128
|
+
)
|
|
129
|
+
catalogue_iri = URIRef(str(vg_iri) + "-catalogue")
|
|
130
|
+
|
|
131
|
+
# add to the System Graph
|
|
132
|
+
vg.add((vg_iri, RDF.type, OLIS.VirtualGraph))
|
|
133
|
+
vg.add((vg_iri, OLIS.isAliasFor, catalogue_iri))
|
|
134
|
+
vg_name = c.value(
|
|
135
|
+
subject=vg_iri,
|
|
136
|
+
predicate=SDO.name | DCTERMS.title | SKOS.prefLabel,
|
|
137
|
+
) or str(vg_iri)
|
|
138
|
+
vg.add((vg_iri, SDO.name, vg_name))
|
|
139
|
+
|
|
140
|
+
# export the Catalogue data
|
|
141
|
+
_export(c, catalogue_iri, sparql_endpoint, destination_file, return_data_type)
|
|
142
|
+
|
|
143
|
+
# non-catalogue resources
|
|
144
|
+
for s, o in manifest_graph.subject_objects(PROF.hasResource):
|
|
145
|
+
for role in manifest_graph.objects(o, PROF.hasRole):
|
|
146
|
+
# The data files & background - must be processed after Catalogue
|
|
147
|
+
if role in [
|
|
148
|
+
MRR.CompleteCatalogueAndResourceLabels,
|
|
149
|
+
MRR.IncompleteCatalogueAndResourceLabels,
|
|
150
|
+
MRR.ResourceData,
|
|
151
|
+
]:
|
|
152
|
+
for artifact in manifest_graph.objects(o, PROF.hasArtifact):
|
|
153
|
+
for f in get_files_from_artifact(manifest, artifact):
|
|
154
|
+
if str(f.name).endswith(".ttl"):
|
|
155
|
+
fg = Graph().parse(f)
|
|
156
|
+
# fg.bind("rdf", RDF)
|
|
157
|
+
|
|
158
|
+
if role == MRR.ResourceData:
|
|
159
|
+
resource_iri = fg.value(predicate=RDF.type, object=SKOS.ConceptScheme) or fg.value(predicate=RDF.type, object=OWL.Ontology)
|
|
160
|
+
|
|
161
|
+
if role in [
|
|
162
|
+
MRR.CompleteCatalogueAndResourceLabels,
|
|
163
|
+
MRR.IncompleteCatalogueAndResourceLabels,
|
|
164
|
+
]:
|
|
165
|
+
resource_iri = URIRef("http://background")
|
|
166
|
+
|
|
167
|
+
if resource_iri is None:
|
|
168
|
+
raise ValueError(f"Could not determine Resource IRI for file {f}")
|
|
169
|
+
|
|
170
|
+
vg.add((vg_iri, OLIS.isAliasFor, resource_iri))
|
|
171
|
+
|
|
172
|
+
# export one Resource
|
|
173
|
+
_export(fg, resource_iri, sparql_endpoint, destination_file, return_data_type)
|
|
174
|
+
elif str(f.name).endswith(".trig"):
|
|
175
|
+
d = Dataset()
|
|
176
|
+
d.parse(f)
|
|
177
|
+
for g in d.graphs():
|
|
178
|
+
if g.identifier != URIRef("urn:x-rdflib:default"):
|
|
179
|
+
vg.add((vg_iri, OLIS.isAliasFor, g.identifier))
|
|
180
|
+
_export(d, None, sparql_endpoint, destination_file, return_data_type)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# export the System Graph
|
|
184
|
+
_export(vg, OLIS.SystemGraph, sparql_endpoint, destination_file, return_data_type, append=True)
|
|
185
|
+
|
|
186
|
+
if return_data_type == "Dataset":
|
|
187
|
+
return dataset_holder
|
|
188
|
+
elif return_data_type == "Graph":
|
|
189
|
+
return graph_holder
|
|
190
|
+
elif return_data_type is None:
|
|
191
|
+
pass # return nothing
|
|
192
|
+
else:
|
|
193
|
+
raise ValueError(return_data_value_error_message)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def setup_cli_parser(args=None):
|
|
197
|
+
parser = argparse.ArgumentParser()
|
|
198
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
199
|
+
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"-v",
|
|
202
|
+
"--version",
|
|
203
|
+
action="version",
|
|
204
|
+
version="{version}".format(version=__version__),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
group.add_argument(
|
|
208
|
+
"-e",
|
|
209
|
+
"--endpoint",
|
|
210
|
+
help="The SPARQL endpoint you want to load the data into. Cannot be specified when destination is.",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
group.add_argument(
|
|
214
|
+
"-d",
|
|
215
|
+
"--destination",
|
|
216
|
+
help="The n-quads file you want to export the data into. Cannot be specified when endpoint is.",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
parser.add_argument(
|
|
220
|
+
"manifest",
|
|
221
|
+
help="A Manifest file to process",
|
|
222
|
+
type=Path,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return parser.parse_args(args)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def cli(args=None):
|
|
229
|
+
if args is None:
|
|
230
|
+
args = sys.argv[1:]
|
|
231
|
+
|
|
232
|
+
args = setup_cli_parser(args)
|
|
233
|
+
|
|
234
|
+
load(
|
|
235
|
+
Path(args.manifest),
|
|
236
|
+
args.endpoint if args.endpoint is not None else None,
|
|
237
|
+
Path(args.destination) if args.destination is not None else None,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
retval = cli(sys.argv[1:])
|
|
243
|
+
if retval is not None:
|
|
244
|
+
sys.exit(retval)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
PREFIX cs: <https://prez.dev/ManifestResourceRoles>
|
|
2
|
+
PREFIX : <https://prez.dev/ManifestResourceRoles/>
|
|
3
|
+
PREFIX owl: <http://www.w3.org/2002/07/owl#>
|
|
4
|
+
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
5
|
+
PREFIX schema: <https://schema.org/>
|
|
6
|
+
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
|
7
|
+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
:CompleteCatalogueAndResourceLabels
|
|
11
|
+
a skos:Concept ;
|
|
12
|
+
skos:definition "All the labels - possibly including names, descriptions & seeAlso links - for the Catalogue and Resource objects" ;
|
|
13
|
+
skos:inScheme cs: ;
|
|
14
|
+
skos:prefLabel "Complete Catalogue and Resource Labels" ;
|
|
15
|
+
.
|
|
16
|
+
|
|
17
|
+
:CatalogueAndResourceModel
|
|
18
|
+
a skos:Concept ;
|
|
19
|
+
skos:definition "The default model for the container and the content. Must be a set of SHACL Shapes" ;
|
|
20
|
+
skos:inScheme cs: ;
|
|
21
|
+
skos:prefLabel "Catalogue & Resource Model" ;
|
|
22
|
+
.
|
|
23
|
+
|
|
24
|
+
:CatalogueData
|
|
25
|
+
a skos:Concept ;
|
|
26
|
+
skos:definition "Data for the container, usually a Catalogue, including the identity of it and each item fo content" ;
|
|
27
|
+
skos:inScheme cs: ;
|
|
28
|
+
skos:prefLabel "Catalogue Data" ;
|
|
29
|
+
.
|
|
30
|
+
|
|
31
|
+
:CatalogueModel
|
|
32
|
+
a skos:Concept ;
|
|
33
|
+
skos:definition "The default model for the container. Must be a set of SHACL Shapes" ;
|
|
34
|
+
skos:inScheme cs: ;
|
|
35
|
+
skos:prefLabel "Catalogue Model" ;
|
|
36
|
+
.
|
|
37
|
+
|
|
38
|
+
:ResourceData
|
|
39
|
+
a skos:Concept ;
|
|
40
|
+
skos:definition "Data for the content of the container" ;
|
|
41
|
+
skos:inScheme cs: ;
|
|
42
|
+
skos:prefLabel "Resource Data" ;
|
|
43
|
+
.
|
|
44
|
+
|
|
45
|
+
:ResourceModel
|
|
46
|
+
a skos:Concept ;
|
|
47
|
+
skos:definition "The default model for the content. Must be a set of SHACL Shapes" ;
|
|
48
|
+
skos:inScheme cs: ;
|
|
49
|
+
skos:prefLabel "Resource Model" ;
|
|
50
|
+
.
|
|
51
|
+
|
|
52
|
+
:IncompleteCatalogueAndResourceLabels
|
|
53
|
+
a skos:Concept ;
|
|
54
|
+
skos:definition "Some of the labels - possibly including names, descriptions & seeAlso links - for the Catalogue and Resource objects" ;
|
|
55
|
+
skos:inScheme cs: ;
|
|
56
|
+
skos:prefLabel "Incomplete Catalogue and Resource Labels" ;
|
|
57
|
+
.
|
|
58
|
+
|
|
59
|
+
<https://kurrawong.ai>
|
|
60
|
+
a schema:Organization ;
|
|
61
|
+
schema:name "KurrawongAI" ;
|
|
62
|
+
schema:url "https://kurrawong.ai"^^xsd:anyURI ;
|
|
63
|
+
.
|
|
64
|
+
|
|
65
|
+
:1.0.0
|
|
66
|
+
rdfs:label "1.0.0" ;
|
|
67
|
+
.
|
|
68
|
+
|
|
69
|
+
cs:
|
|
70
|
+
a skos:ConceptScheme ;
|
|
71
|
+
owl:versionIRI :1.0.0 ;
|
|
72
|
+
owl:versionInfo "1.0.0: First public version" ;
|
|
73
|
+
skos:definition "This vocabulary is a test vocabulary showing the use of images within Concept definitions"@en ;
|
|
74
|
+
skos:hasTopConcept
|
|
75
|
+
:CompleteCatalogueAndResourceLabels ,
|
|
76
|
+
:CatalogueAndResourceModel ,
|
|
77
|
+
:CatalogueData ,
|
|
78
|
+
:CatalogueModel ,
|
|
79
|
+
:ResourceData ,
|
|
80
|
+
:IncompleteCatalogueAndResourceLabels ;
|
|
81
|
+
skos:historyNote "Create in 2024 for Prez data loading" ;
|
|
82
|
+
skos:prefLabel "Manifest Resource Roles Vocabulary" ;
|
|
83
|
+
schema:creator <https://kurrawong.ai> ;
|
|
84
|
+
schema:dateCreated "2024-11-29"^^xsd:date ;
|
|
85
|
+
schema:dateModified "2024-12-17"^^xsd:date ;
|
|
86
|
+
schema:publisher <https://kurrawong.ai> ;
|
|
87
|
+
.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from rdflib import Literal, URIRef, Graph, Dataset
|
|
5
|
+
from rdflib.namespace import OWL, RDF, RDFS, SKOS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_files_from_artifact(manifest: Path, artifact: Literal) -> List[Path] | Generator[Path]:
|
|
9
|
+
"""Returns an iterable (list or generator) of Path objects for files within an artifact literal.
|
|
10
|
+
|
|
11
|
+
This function will correctly interpret artifacts such as 'file.ttl', '*.ttl', '**/*.trig' etc."""
|
|
12
|
+
if not "*" in str(artifact):
|
|
13
|
+
return [manifest.parent / Path(str(artifact))]
|
|
14
|
+
else:
|
|
15
|
+
artifact_str = str(artifact)
|
|
16
|
+
glob_marker_location = artifact_str.find("*")
|
|
17
|
+
glob_parts = [artifact_str[:glob_marker_location], artifact_str[glob_marker_location:]]
|
|
18
|
+
|
|
19
|
+
return Path(manifest.parent / Path(glob_parts[0])).glob(glob_parts[1])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_identifier_from_file(file: Path) -> List[URIRef]:
|
|
23
|
+
"""Returns a list if RDFLib graph identifier (URIRefs) from a triples or quads file
|
|
24
|
+
for all owl:Ontology and skos:ConceptScheme objects"""
|
|
25
|
+
if file.name.endswith(".ttl"):
|
|
26
|
+
g = Graph().parse(file)
|
|
27
|
+
return [
|
|
28
|
+
g.value(predicate=RDF.type, object=SKOS.ConceptScheme) or
|
|
29
|
+
g.value(predicate=RDF.type, object=OWL.Ontology)
|
|
30
|
+
]
|
|
31
|
+
elif file.name.endswith(".trig"):
|
|
32
|
+
gs = []
|
|
33
|
+
d = Dataset()
|
|
34
|
+
d.parse(file, format="trig")
|
|
35
|
+
for g in d.graphs():
|
|
36
|
+
gs.append(g.identifier)
|
|
37
|
+
return gs
|
|
38
|
+
else:
|
|
39
|
+
return []
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Validate a Prez Manifest file.
|
|
2
|
+
|
|
3
|
+
This script performs both SHACL validation to ensure the Manifest is valid according to the Prez Manifest
|
|
4
|
+
specification (see https://prez.dev/manifest/) and checks that all the resources indicated by the Manifest
|
|
5
|
+
- whether local files/folders or remote resources on the Internet - are reachable.
|
|
6
|
+
|
|
7
|
+
~$ python validate.py {MANIFEST_FILE_PATH}"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from kurra.utils import load_graph
|
|
15
|
+
from pyshacl import validate as shacl_validate
|
|
16
|
+
from rdflib import Graph
|
|
17
|
+
from rdflib.namespace import PROF
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from prezmanifest import __version__
|
|
21
|
+
except ImportError:
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
sys.path.append(str(Path(__file__).parent.parent.resolve()))
|
|
25
|
+
from prezmanifest import __version__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate(manifest: Path) -> bool:
|
|
29
|
+
ME = Path(__file__)
|
|
30
|
+
MANIFEST_ROOT_DIR = manifest.parent
|
|
31
|
+
|
|
32
|
+
# SHACL validation
|
|
33
|
+
manifest_graph = load_graph(manifest)
|
|
34
|
+
mrr_vocab_graph = load_graph(ME.parent / "mrr.ttl")
|
|
35
|
+
data_graph = manifest_graph + mrr_vocab_graph
|
|
36
|
+
shacl_graph = load_graph(ME.parent / "validator.ttl")
|
|
37
|
+
valid, v_graph, v_text = shacl_validate(data_graph, shacl_graph=shacl_graph)
|
|
38
|
+
|
|
39
|
+
if not valid:
|
|
40
|
+
raise ValueError(f"SHACL invalid:\n\n{v_text}")
|
|
41
|
+
|
|
42
|
+
# Content link validation
|
|
43
|
+
for s, o in manifest_graph.subject_objects(PROF.hasResource):
|
|
44
|
+
for artifact in manifest_graph.objects(o, PROF.hasArtifact):
|
|
45
|
+
artifact_str = str(artifact)
|
|
46
|
+
if "http" in artifact_str:
|
|
47
|
+
r = httpx.get(artifact_str)
|
|
48
|
+
if 200 <= r.status_code < 400:
|
|
49
|
+
pass
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Remote content link non-resolving: {artifact_str}"
|
|
53
|
+
)
|
|
54
|
+
elif "*" in artifact_str:
|
|
55
|
+
glob_parts = artifact_str.split("*")
|
|
56
|
+
dir = Path(manifest.parent / Path(glob_parts[0]))
|
|
57
|
+
if not Path(dir).is_dir():
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"The content link {artifact_str} is not a directory"
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
# It must be a local
|
|
63
|
+
if not (MANIFEST_ROOT_DIR / artifact_str).is_file():
|
|
64
|
+
print(
|
|
65
|
+
f"Content link {MANIFEST_ROOT_DIR / artifact_str} is invalid - not a file"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return manifest_graph
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def setup_cli_parser(args=None):
|
|
72
|
+
|
|
73
|
+
parser = argparse.ArgumentParser()
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"-v",
|
|
77
|
+
"--version",
|
|
78
|
+
action="version",
|
|
79
|
+
version="{version}".format(version=__version__),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"manifest",
|
|
84
|
+
help="A Manifest file to process",
|
|
85
|
+
type=Path,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return parser.parse_args(args)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cli(args=None):
|
|
92
|
+
if args is None:
|
|
93
|
+
args = sys.argv[1:]
|
|
94
|
+
|
|
95
|
+
args = setup_cli_parser(args)
|
|
96
|
+
|
|
97
|
+
validate(args.manifest)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
retval = cli(sys.argv[1:])
|
|
102
|
+
if retval is not None:
|
|
103
|
+
sys.exit(retval)
|
|
104
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
PREFIX owl: <http://www.w3.org/2002/07/owl#>
|
|
2
|
+
PREFIX prez: <https://prez.dev/>
|
|
3
|
+
PREFIX prof: <http://www.w3.org/ns/dx/prof/>
|
|
4
|
+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
5
|
+
PREFIX schema: <https://schema.org/>
|
|
6
|
+
PREFIX sh: <http://www.w3.org/ns/shacl#>
|
|
7
|
+
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
|
8
|
+
PREFIX val: <https://prez.dev/manifest-validator/>
|
|
9
|
+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
|
10
|
+
|
|
11
|
+
<https://prez.dev/manifest-validator>
|
|
12
|
+
a owl:Ontology ;
|
|
13
|
+
owl:versionIRI val:1.0.0 ;
|
|
14
|
+
owl:versionInfo "1.0.0: First public version" ;
|
|
15
|
+
schema:creator <https://orcid.org/0000-0002-8742-7730> ;
|
|
16
|
+
schema:dateCreated "2024-11-29"^^xsd:date ;
|
|
17
|
+
schema:dateModified "2024-11-29"^^xsd:date ;
|
|
18
|
+
schema:definition "The SHACL Shapes Validator for the Prez Ontology Manifest Model"@en ;
|
|
19
|
+
schema:name "Prez Manifest Validator" ;
|
|
20
|
+
schema:publisher <https://kurrawong.ai> ;
|
|
21
|
+
.
|
|
22
|
+
|
|
23
|
+
val:ShapeN01
|
|
24
|
+
a sh:NodeShape ;
|
|
25
|
+
sh:property val:ShapeP01 ;
|
|
26
|
+
sh:targetClass prez:Manifest ;
|
|
27
|
+
.
|
|
28
|
+
|
|
29
|
+
val:ShapeN02
|
|
30
|
+
a sh:NodeShape ;
|
|
31
|
+
sh:property
|
|
32
|
+
val:ShapeP02 ,
|
|
33
|
+
val:ShapeP03 ,
|
|
34
|
+
val:ShapeP04 ,
|
|
35
|
+
val:ShapeP05 ;
|
|
36
|
+
sh:targetObjectsOf prof:hasResource ;
|
|
37
|
+
.
|
|
38
|
+
|
|
39
|
+
val:ShapeP01
|
|
40
|
+
a sh:PropertyShape ;
|
|
41
|
+
sh:nodeKind sh:BlankNodeOrIRI ;
|
|
42
|
+
sh:message "ShapeP01: You must supply at least one value for prof:hasResource for a prez:Manifest and it must be either a Blank Node or an IRI" ;
|
|
43
|
+
sh:minCount 1 ;
|
|
44
|
+
sh:path prof:hasResource ;
|
|
45
|
+
.
|
|
46
|
+
|
|
47
|
+
val:ShapeP02
|
|
48
|
+
a sh:PropertyShape ;
|
|
49
|
+
sh:maxCount 1 ;
|
|
50
|
+
sh:message "ShapeP02: You must supply exactly one value for prof:hasArtifact for a prez:Manifest's Resource Descriptors and it must be a string that can be interpreted as a file path, either relative to the manifest file, or an absolute system path or a URL" ;
|
|
51
|
+
sh:minCount 1 ;
|
|
52
|
+
sh:nodeKind sh:Literal ;
|
|
53
|
+
sh:path prof:hasArtifact ;
|
|
54
|
+
.
|
|
55
|
+
|
|
56
|
+
val:ShapeP03
|
|
57
|
+
a sh:PropertyShape ;
|
|
58
|
+
sh:maxCount 1 ;
|
|
59
|
+
sh:message "ShapeP03: You must supply exactly one value for prof:hasRole for a prez:Manifest's Resource Descriptors with the value selected from the Prez Manifest Resource Roles Vocabulary" ;
|
|
60
|
+
sh:minCount 1 ;
|
|
61
|
+
sh:nodeKind sh:IRI ;
|
|
62
|
+
sh:path prof:hasRole ;
|
|
63
|
+
.
|
|
64
|
+
|
|
65
|
+
val:ShapeP04
|
|
66
|
+
a sh:PropertyShape ;
|
|
67
|
+
sh:maxCount 1 ;
|
|
68
|
+
sh:message "ShapeP04: If you supply a schema:description for a prez:Manifest's Resource Descriptors, it must be either a plain string or a langString literal" ;
|
|
69
|
+
sh:or (
|
|
70
|
+
|
|
71
|
+
[
|
|
72
|
+
sh:datatype xsd:string ;
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[
|
|
76
|
+
sh:datatype rdf:langString ;
|
|
77
|
+
]
|
|
78
|
+
) ;
|
|
79
|
+
sh:path schema:description ;
|
|
80
|
+
.
|
|
81
|
+
|
|
82
|
+
val:ShapeP05
|
|
83
|
+
a sh:PropertyShape ;
|
|
84
|
+
sh:maxCount 1 ;
|
|
85
|
+
sh:message "ShapeP05: If you supply a schema:name for a prez:Manifest's Resource Descriptors, it must be either a plain string or a langString literal" ;
|
|
86
|
+
sh:or (
|
|
87
|
+
|
|
88
|
+
[
|
|
89
|
+
sh:datatype xsd:string ;
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[
|
|
93
|
+
sh:datatype rdf:langString ;
|
|
94
|
+
]
|
|
95
|
+
) ;
|
|
96
|
+
sh:path schema:name ;
|
|
97
|
+
.
|
|
98
|
+
|
|
99
|
+
val:ShapeN03
|
|
100
|
+
a sh:NodeShape ;
|
|
101
|
+
sh:targetObjectsOf prof:hasRole ;
|
|
102
|
+
sh:property [
|
|
103
|
+
a sh:PropertyShape ;
|
|
104
|
+
sh:minCount 1 ;
|
|
105
|
+
sh:maxCount 1 ;
|
|
106
|
+
sh:path skos:inScheme ;
|
|
107
|
+
] ;
|
|
108
|
+
.
|
|
109
|
+
|
|
110
|
+
val:1.0.0
|
|
111
|
+
schema:name "1.0.0" ;
|
|
112
|
+
.
|
|
113
|
+
|
|
114
|
+
<https://orcid.org/0000-0002-8742-7730>
|
|
115
|
+
a schema:Person ;
|
|
116
|
+
schema:email "nick@kurrawong.ai"^^xsd:anyURI ;
|
|
117
|
+
schema:identifier "https://orcid.org/0000-0002-8742-7730"^^xsd:anyURI ;
|
|
118
|
+
schema:memberOf <https://kurrawong.ai> ;
|
|
119
|
+
schema:name "Nicholas J. Car" ;
|
|
120
|
+
.
|
|
121
|
+
|
|
122
|
+
<https://kurrawong.ai>
|
|
123
|
+
a schema:Organization ;
|
|
124
|
+
schema:name "KurrawongAI" ;
|
|
125
|
+
schema:url "https://kurrawong.ai"^^xsd:anyURI ;
|
|
126
|
+
.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "prezmanifest"
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "A Python package that provides a series of functions to work with Prez Manifests."
|
|
5
|
+
authors = ["Nicholas Car <nick@kurrawong.ai>"]
|
|
6
|
+
license = "BSD-3-Clause"
|
|
7
|
+
readme = "README.adoc"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = ">=3.12,<4.0"
|
|
11
|
+
pyshacl = "^0.29.0"
|
|
12
|
+
kurra = "^0.6.1"
|
|
13
|
+
labelify = "^0.3.8"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dev-dependencies]
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
pytest = "^8.3.4"
|
|
19
|
+
black = "^24.10.0"
|
|
20
|
+
testcontainers = "^4.9.0"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["poetry-core>=1.0.0"]
|
|
24
|
+
build-backend = "poetry.core.masonry.api"
|