oldaplib 0.3.27__py3-none-any.whl → 0.3.29__py3-none-any.whl

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.
@@ -4,6 +4,7 @@
4
4
  @prefix sh: <http://www.w3.org/ns/shacl#> .
5
5
  @prefix owl: <http://www.w3.org/2002/07/owl#> .
6
6
  @prefix dcterms: <http://purl.org/dc/terms/> .
7
+ @prefix dcmitype: <http://purl.org/dc/dcmitype/> .
7
8
  @prefix schema: <http://schema.org/> .
8
9
  @prefix oldap: <http://oldap.org/base#> .
9
10
 
@@ -109,6 +110,90 @@ shared:shacl {
109
110
  # Media object
110
111
  ###############################################################################
111
112
 
113
+ # dcmitype:CollectionShape a sh:NodeShape ;
114
+ # sh:targetClass shared:Collection ;
115
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
116
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
117
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
118
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
119
+ # oldap:externalOntology "true"^^xsd:boolean ;
120
+ # rdfs:label "Collection"@en, "Sammlung"@de, "Collection"@fr, "Collezione"@it ;
121
+ # sh:property [
122
+ # sh:path rdf:type ;
123
+ # ] .
124
+ #
125
+ # dcmitype:DatasetShape a sh:NodeShape ;
126
+ # sh:targetClass shared:Dataset ;
127
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
128
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
129
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
130
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
131
+ # oldap:externalOntology "true"^^xsd:boolean ;
132
+ # rdfs:label "Dataset"@en, "Dataset"@de, "Dataset"@fr, "Dataset"@it ;
133
+ # sh:property [
134
+ # sh:path rdf:type ;
135
+ # ] .
136
+ #
137
+ # dcmitype:StillImageShape a sh:NodeShape ;
138
+ # sh:targetClass shared:StillImage ;
139
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
140
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
141
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
142
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
143
+ # oldap:externalOntology "true"^^xsd:boolean ;
144
+ # rdfs:label "StillImage"@en, "Standbild"@de, "Image fixe"@fr, "Immagine fissa"@it ;
145
+ # sh:property [
146
+ # sh:path rdf:type ;
147
+ # ] .
148
+ #
149
+ # dcmitype:ImageShape a sh:NodeShape ;
150
+ # sh:targetClass shared:Image ;
151
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
152
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
153
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
154
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
155
+ # oldap:externalOntology "true"^^xsd:boolean ;
156
+ # rdfs:label "Image"@en, "Bild"@de, "Image"@fr, "Immagine"@it ;
157
+ # sh:property [
158
+ # sh:path rdf:type ;
159
+ # ] .
160
+ #
161
+ # dcmitype:MovingImageShape a sh:NodeShape ;
162
+ # sh:targetClass shared:MovingImage ;
163
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
164
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
165
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
166
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
167
+ # oldap:externalOntology "true"^^xsd:boolean ;
168
+ # rdfs:label "MovingImage"@en, "Bewegtbild"@de, "Image animée"@fr, "Immagine in movimento"@it ;
169
+ # sh:property [
170
+ # sh:path rdf:type ;
171
+ # ] .
172
+ #
173
+ # dcmitype:SoundShape a sh:NodeShape ;
174
+ # sh:targetClass shared:Sound ;
175
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
176
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
177
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
178
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
179
+ # oldap:externalOntology "true"^^xsd:boolean ;
180
+ # rdfs:label "Sound"@en, "Ton"@de, "Son"@fr, "Sound"@it ;
181
+ # sh:property [
182
+ # sh:path rdf:type ;
183
+ # ] .
184
+ #
185
+ # dcmitype:TextShape a sh:NodeShape ;
186
+ # sh:targetClass shared:Text ;
187
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
188
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
189
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
190
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
191
+ # oldap:externalOntology "true"^^xsd:boolean ;
192
+ # rdfs:label "Text"@en, "Text"@de, "Texte"@fr, "Testo"@it ;
193
+ # sh:property [
194
+ # sh:path rdf:type ;
195
+ # ] .
196
+
112
197
  shared:MediaObjectShape a sh:NodeShape ;
113
198
  sh:targetClass shared:MediaObject ;
114
199
  dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
@@ -117,12 +202,33 @@ shared:shacl {
117
202
  dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
118
203
  oldap:externalOntology "false"^^xsd:boolean ;
119
204
  rdfs:label "MediaObject"@en, "Medienobjekt"@de, "MediaObject"@fr, "MediaObject"@it ;
120
- rdfs:comment "Page of a book"@en, "Seite eines Buches"@de ;
121
205
  sh:closed "true"^^xsd:boolean ;
122
206
  sh:node oldap:ThingShape ;
123
207
  sh:property [
124
208
  sh:path rdf:type ;
125
209
  ] ;
210
+ sh:property [
211
+ sh:path dcterms:type ;
212
+ dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
213
+ dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
214
+ dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
215
+ dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
216
+ oldap:statementProperty "false"^^xsd:boolean ;
217
+ oldap:externalOntology "false"^^xsd:boolean ;
218
+ sh:nodeKind sh:IRI ;
219
+ sh:in (
220
+ dcmitype:Collection
221
+ dcmitype:Dataset
222
+ dcmitype:StillImage
223
+ dcmitype:Image
224
+ dcmitype:MovingImage
225
+ dcmitype:Sound
226
+ dcmitype:Text
227
+ ) ;
228
+ sh:minCount 1 ;
229
+ sh:maxCount 1 ;
230
+ sh:order "0.5"^^xsd:decimal ;
231
+ ] ;
126
232
  sh:property [
127
233
  sh:path shared:originalName ;
128
234
  schema:version "0.1.0"^^xsd:string ;
@@ -344,6 +450,15 @@ shared:onto {
344
450
  # Media object
345
451
  ###########################################################################
346
452
 
453
+ # shared:mediaType rdf:type owl:ObjectProperty ;
454
+ # dcterms:creator <https://orcid.org/0000-0003-1681-4036> ;
455
+ # dcterms:created "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime ;
456
+ # dcterms:contributor <https://orcid.org/0000-0003-1681-4036> ;
457
+ # dcterms:modified "2025-11-21T00:15:09.037880+01:00"^^xsd:dateTime .
458
+ # #owl:subPropertyOf dcterms:type ;
459
+ # rdfs:domain shared:MediaObject ;
460
+ # rdfs:range rdfs:Class ;
461
+
347
462
  shared:originalName rdf:type owl:DatatypeProperty ;
348
463
  rdfs:domain shared:MediaObject ;
349
464
  rdfs:range xsd:string ;
@@ -401,6 +516,12 @@ shared:onto {
401
516
  rdfs:label "MediaObject"@en, "Medienobjekt"@de, "MediaObject"@fr, "MediaObject"@it ;
402
517
  rdfs:comment "Page of a book"@en, "Seite eines Buches"@de ;
403
518
  rdfs:subClassOf oldap:Thing ,
519
+ [
520
+ rdf:type owl:Restriction ;
521
+ owl:onProperty dcterms:type ;
522
+ owl:qualifiedCardinality "1"^^xsd:nonNegativeInteger ;
523
+ owl:onClass rdfs:Class ;
524
+ ] ,
404
525
  [
405
526
  rdf:type owl:Restriction ;
406
527
  owl:onProperty shared:originalName ;
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import os
3
- from time import sleep
3
+ import time
4
4
 
5
5
  import bcrypt
6
6
  import jwt
@@ -338,80 +338,52 @@ class Connection(IConnection):
338
338
  if not req.ok:
339
339
  raise OldapError(req.text)
340
340
 
341
+
342
+
341
343
  def upload_turtle(self, filename: str, graphname: Optional[str] = None) -> None:
342
344
  """
343
- Uploads a turtle or trig file to the specified repository. This function sends the file to the triplestore for
344
- import and does not wait for the completion of the import process. The process itself may continue for some time
345
- after the command is issued.
346
-
347
- :param filename: Name of the file to be uploaded.
348
- :type filename: str
349
- :param graphname: Optional; the name of the RDF-graph into which the data is to be imported.
350
- :type graphname: str or None
351
- :return: None
352
- :rtype: None
353
- :raises OldapError: Raised when there are issues with the repository or during the HTTP request.
354
- """
355
- # if not self._userdata:
356
- # raise OldapErrorNoPermission("No permission")
357
- # actor = self._userdata
358
- # sysperms = actor.inProject.get(QName('oldap:SystemProject'))
359
- # is_root: bool = False
360
- # if sysperms and AdminPermission.ADMIN_OLDAP in sysperms:
361
- # is_root = True
362
- # if not is_root:
363
- # raise OldapErrorNoPermission("No permission")
345
+ Upload a TTL/TRiG file to GraphDB using the RDF4J /statements endpoint.
364
346
 
347
+ This call is synchronous: when it returns without error, the data is loaded.
348
+ """
365
349
  logger = get_logger()
366
- with open(filename, encoding="utf-8") as f:
367
- content = f.read()
368
- ext = Path(filename).suffix
369
- mime = ""
370
- if ext == ".ttl":
371
- mime = "text/turtle"
372
- elif ext == ".trig":
373
- mime = "application/x-trig"
374
-
375
- ct = datetime.now().astimezone()
376
- ts = ct.timestamp()
377
- data = {
378
- "name": f'Data from "{filename}"',
379
- "status": None,
380
- "message": "",
381
- "context": graphname,
382
- "replaceGraphs": [],
383
- "baseURI": None,
384
- "forceSerial": False,
385
- "type": "text",
386
- "format": mime,
387
- "data": content,
388
- "timestamp": ts,
389
- "parserSettings": {
390
- "preserveBNodeIds": False,
391
- "failOnUnknownDataTypes": False,
392
- "verifyDataTypeValues": False,
393
- "normalizeDataTypeValues": False,
394
- "failOnUnknownLanguageTags": False,
395
- "verifyLanguageTags": True,
396
- "normalizeLanguageTags": True,
397
- "stopOnError": True
398
- }
399
- }
400
- jsondata = json.dumps(data)
401
- headers = {
402
- "Accept": "application/json, text/plain, */*",
403
- "Content-Type": "application/json; charset=utf-8"
404
- }
405
- url = f"{self._server}/rest/repositories/{self._repo}/import/upload/text"
406
- auth = HTTPBasicAuth(self._dbuser, self._dbpassword) if self._dbuser and self._dbpassword else None
407
- req = requests.post(url,
408
- headers=headers,
409
- data=jsondata,
410
- auth=auth)
411
- if not req.ok:
412
- logger.error(f'Upload of file "{filename}" failed: {req.text}')
413
- raise OldapError(req.text)
414
- logger.info(f'File "{filename}" uploaded.')
350
+
351
+ ext = Path(filename).suffix.lower()
352
+ if ext == ".ttl":
353
+ mime = "text/turtle"
354
+ elif ext == ".trig":
355
+ # use the standard MIME type for TriG
356
+ mime = "application/trig"
357
+ else:
358
+ raise OldapError(f"Unsupported RDF extension: {ext}")
359
+
360
+ with open(filename, "rb") as f:
361
+ data = f.read()
362
+
363
+ # RDF4J / GraphDB statements endpoint
364
+ url = f"{self._server.rstrip('/')}/repositories/{self._repo}/statements"
365
+
366
+ # Optional context: only use this if you really want to force
367
+ # all triples into a single named graph. For TriG you usually
368
+ # leave this empty so the quads' graph IRIs are respected.
369
+ params = {}
370
+ if graphname:
371
+ # GraphDB expects the context IRI wrapped in < > and URL-encoded
372
+ params["context"] = f"<{graphname}>"
373
+
374
+ auth = HTTPBasicAuth(self._dbuser, self._dbpassword) if self._dbuser and self._dbpassword else None
375
+ headers = {
376
+ "Content-Type": mime,
377
+ "Accept": "text/plain" # or */*, result body is usually empty
378
+ }
379
+
380
+ resp = requests.post(url, params=params, headers=headers, data=data, auth=auth)
381
+
382
+ if not resp.ok:
383
+ logger.error(f'Upload of file "{filename}" failed: {resp.status_code} {resp.text}')
384
+ raise OldapError(resp.text)
385
+
386
+ logger.info(f'File "{filename}" uploaded synchronously via /statements.')
415
387
 
416
388
  def query(self, query: str, format: SparqlResultFormat = SparqlResultFormat.JSON) -> Any:
417
389
  """
oldaplib/src/datamodel.py CHANGED
@@ -1,7 +1,8 @@
1
+ import io
1
2
  from copy import deepcopy
2
3
  from dataclasses import dataclass
3
4
  from datetime import datetime
4
- from typing import Dict, List, Optional, Union, Any, Self
5
+ from typing import Dict, List, Optional, Union, Any, Self, TextIO
5
6
 
6
7
  from oldaplib.src.cachesingleton import CacheSingleton, CacheSingletonRedis
7
8
  from oldaplib.src.dtypes.namespaceiri import NamespaceIRI
@@ -723,57 +724,58 @@ class DataModel(Model):
723
724
  cache = CacheSingletonRedis()
724
725
  cache.delete(Xsd_QName(self._project.projectShortName, 'shacl'))
725
726
 
726
-
727
- def write_as_trig(self, filename: str, indent: int = 0, indent_inc: int = 4) -> None:
727
+ def __to_trig_format(self, f: TextIO, indent: int = 0, indent_inc: int = 4) -> None:
728
728
  """
729
- Write the complete datamodel in the trig format to a file.
730
-
731
- This method serializes the complete datamodel, including its SHACL
732
- shapes and OWL ontology, and writes the resulting data in the trig
733
- format to the specified file.
734
-
735
- :param filename: The path of the file where the trig data will be
736
- written
737
- :type filename: str
738
- :param indent: Start level of indentation for serialized data. Defaults
739
- to 0
740
- :type indent: int
741
- :param indent_inc: Number of characters to increment for each indent
742
- level. Defaults to 4
743
- :type indent_inc: int
729
+ Generates and writes TriG-formatted RDF data to the given TextIO object. The method constructs
730
+ SHACL and OWL ontology representations based on the internal state and configuration of the
731
+ object. SHACL validation shapes, ontology metadata, and RDF structure are included. This
732
+ method is primarily used for exporting RDF data in a standard, compliant format.
733
+
734
+ :param f: The output stream (e.g., a file or any TextIO object) where the generated TriG
735
+ data will be written.
736
+ :param indent: The base indentation level applied when formatting the output.
737
+ :param indent_inc: The number of spaces added for each level of indentation to format
738
+ nested structures cleanly.
744
739
  :return: None
745
740
  """
741
+ timestamp = Xsd_dateTime.now()
742
+ blank = ''
743
+ context = Context(name=self._con.context_name)
744
+ f.write('\n')
745
+ f.write(context.turtle_context)
746
+ f.write(f'\n{blank:{indent * indent_inc}}{self.__graph}:shacl {{\n')
747
+ f.write(f'{blank:{(indent + 1) * indent_inc}}{self.__graph}:shapes schema:version {self.__version.toRdf} .\n')
748
+ f.write('\n')
749
+ for qname, onto in self.__extontos.items():
750
+ f.write(onto.create_shacl(timestamp=timestamp, indent=1))
751
+ f.write('\n\n')
752
+ for iri, prop in self.__propclasses.items():
753
+ if not prop.internal:
754
+ f.write(prop.create_shacl(timestamp=timestamp, indent=1))
755
+ f.write('\n\n')
756
+ for iri, resclass in self.__resclasses.items():
757
+ f.write(resclass.create_shacl(timestamp=timestamp, indent=1))
758
+ f.write('\n\n')
759
+ f.write(f'\n{blank:{indent * indent_inc}}}}\n')
760
+
761
+ f.write(f'{blank:{indent * indent_inc}}{self.__graph}:onto {{\n')
762
+ f.write(f'{blank:{(indent + 2) * indent_inc}}{self.__graph}:ontology owl:type owl:Ontology ;\n')
763
+ f.write(f'{blank:{(indent + 2) * indent_inc}}owl:versionInfo {self.__version.toRdf} .\n')
764
+ f.write('\n')
765
+ for iri, prop in self.__propclasses.items():
766
+ f.write(prop.create_owl_part1(timestamp=timestamp, indent=2))
767
+ for iri, resclass in self.__resclasses.items():
768
+ f.write(resclass.create_owl(timestamp=timestamp))
769
+ f.write(f'{blank:{indent * indent_inc}}}}\n')
770
+
771
+ def write_as_trig(self, filename: str, indent: int = 0, indent_inc: int = 4) -> None:
746
772
  with open(filename, 'w') as f:
747
- timestamp = Xsd_dateTime.now()
748
- blank = ''
749
- context = Context(name=self._con.context_name)
750
- f.write('\n')
751
- f.write(context.turtle_context)
752
- f.write(f'\n{blank:{indent * indent_inc}}{self.__graph}:shacl {{\n')
753
- f.write(f'{blank:{(indent + 1) * indent_inc}}{self.__graph}:shapes schema:version {self.__version.toRdf} .\n')
754
- f.write('\n')
755
- for qname, onto in self.__extontos.items():
756
- f.write(onto.create_shacl(timestamp=timestamp, indent=1))
757
- f.write('\n\n')
758
- for iri, prop in self.__propclasses.items():
759
- if not prop.internal:
760
- f.write(prop.create_shacl(timestamp=timestamp, indent=1))
761
- f.write('\n\n')
762
- for iri, resclass in self.__resclasses.items():
763
- f.write(resclass.create_shacl(timestamp=timestamp, indent=1))
764
- f.write('\n\n')
765
- f.write(f'\n{blank:{indent * indent_inc}}}}\n')
766
-
767
- f.write(f'{blank:{indent * indent_inc}}{self.__graph}:onto {{\n')
768
- f.write(f'{blank:{(indent + 2) * indent_inc}}{self.__graph}:ontology owl:type owl:Ontology ;\n')
769
- f.write(f'{blank:{(indent + 2) * indent_inc}}owl:versionInfo {self.__version.toRdf} .\n')
770
- f.write('\n')
771
- for iri, prop in self.__propclasses.items():
772
- f.write(prop.create_owl_part1(timestamp=timestamp, indent=2))
773
- for iri, resclass in self.__resclasses.items():
774
- f.write(resclass.create_owl(timestamp=timestamp))
775
- f.write(f'{blank:{indent * indent_inc}}}}\n')
773
+ self.__to_trig_format(f, indent=indent, indent_inc=indent_inc)
776
774
 
775
+ def write_as_str(self, indent: int = 0, indent_inc: int = 4) -> str:
776
+ f = io.StringIO()
777
+ self.__to_trig_format(f, indent=indent, indent_inc=indent_inc)
778
+ return f.getvalue()
777
779
 
778
780
 
779
781
 
@@ -48,6 +48,7 @@ class ContextSingleton(type):
48
48
  Xsd_NCName('schema'): NamespaceIRI('http://schema.org/'),
49
49
  #Xsd_NCName('dc'): NamespaceIRI('http://purl.org/dc/elements/1.1/'),
50
50
  Xsd_NCName('dcterms'): NamespaceIRI('http://purl.org/dc/terms/'),
51
+ Xsd_NCName('dcmitype'): NamespaceIRI('http://purl.org/dc/dcmitype/'),
51
52
  #Xsd_NCName('foaf'): NamespaceIRI('http://xmlns.com/foaf/0.1/'),
52
53
  Xsd_NCName('oldap'): NamespaceIRI('http://oldap.org/base#'),
53
54
  Xsd_NCName('shared'): NamespaceIRI('http://oldap.org/shared#')
@@ -63,6 +64,7 @@ class ContextSingleton(type):
63
64
  NamespaceIRI('http://schema.org/'): Xsd_NCName('schema'),
64
65
  #NamespaceIRI('http://purl.org/dc/elements/1.1/'): Xsd_NCName('dc'),
65
66
  NamespaceIRI('http://purl.org/dc/terms/'): Xsd_NCName('dcterms'),
67
+ NamespaceIRI('http://purl.org/dc/dcmitype/'): Xsd_NCName('dcmitype'),
66
68
  #NamespaceIRI('http://xmlns.com/foaf/0.1/'): Xsd_NCName('foaf'),
67
69
  NamespaceIRI('http://oldap.org/base#'): Xsd_NCName('oldap'),
68
70
  NamespaceIRI('http://oldap.org/shared#'): Xsd_NCName('shared'),
@@ -237,7 +239,7 @@ class Context(metaclass=ContextSingleton):
237
239
  return Xsd_QName(prefix, fragment, validate=validate)
238
240
  return None
239
241
 
240
- def qname2iri(self, qname: Xsd_QName | str, validate: bool = True) -> NamespaceIRI:
242
+ def qname2iri(self, qname: Xsd_QName | str, validate: bool = True) -> Xsd_anyURI:
241
243
  """
242
244
  Convert a QName into a IRI string.
243
245
 
@@ -1,4 +1,7 @@
1
1
  import re
2
+ import jwt
3
+
4
+ from datetime import datetime, timedelta
2
5
  from enum import Flag, auto
3
6
  from functools import partial
4
7
  from typing import Type, Any, Self, cast
@@ -231,8 +234,25 @@ class ResourceInstance:
231
234
  return # TODO: LangString does not yet allow multiple entries of the same language...
232
235
  if property.get(PropClassAttr.IN):
233
236
  #for val in values:
234
- if not values in property[PropClassAttr.IN]:
235
- raise OldapErrorValue(f'Property {property} with IN={property[PropClassAttr.IN]} has invalid value "{val}"')
237
+ if property.datatype is None: # no defined datatype, e.h.sh:IRI
238
+ tmpinset = {str(x) for x in property[PropClassAttr.IN]}
239
+ if isinstance(values, (list, tuple, set, ObservableSet)):
240
+ for val in values:
241
+ if not str(val) in tmpinset:
242
+ raise OldapErrorValue(
243
+ f'Property {property} with IN={property[PropClassAttr.IN]} has invalid value "{val}"')
244
+ else:
245
+ if not str(values) in tmpinset:
246
+ raise OldapErrorValue(f'Property {property.property_class_iri} with IN={property[PropClassAttr.IN]} has invalid value "{values}"')
247
+ else:
248
+ if isinstance(values, (list, tuple, set, ObservableSet)):
249
+ for val in values:
250
+ if not val in property[PropClassAttr.IN]:
251
+ raise OldapErrorValue(
252
+ f'Property {property} with IN={property[PropClassAttr.IN]} has invalid value "{val}"')
253
+ else:
254
+ if not values in property[PropClassAttr.IN]:
255
+ raise OldapErrorValue(f'Property {property.property_class_iri} with IN={property[PropClassAttr.IN]} has invalid value "{values}"')
236
256
  if property.get(PropClassAttr.MIN_LENGTH):
237
257
  for val in values:
238
258
  l = 0
@@ -1029,6 +1049,7 @@ class ResourceInstance:
1029
1049
  limit: int = 100,
1030
1050
  offset: int = 0,
1031
1051
  indent: int = 0, indent_inc: int = 4) -> dict[Iri, dict[str, Xsd]]:
1052
+ # TODO: PROBLEM: does not work for properties which use MAX_COUNT > 1 !!!!!!!
1032
1053
  """
1033
1054
  Retrieves all resources matching the specified parameters from a data store using a SPARQL query.
1034
1055
  Depending on the `count_only` flag, it can return either a count of matching resources or detailed
@@ -1144,7 +1165,22 @@ class ResourceInstance:
1144
1165
  return result
1145
1166
 
1146
1167
  @staticmethod
1147
- def get_media_object_by_id(con: IConnection, mediaObjectId: Xsd_string | str) -> dict[str, Xsd] | None:
1168
+ def get_media_object_by_id(con: IConnection, mediaObjectId: Xsd_string | str) -> dict[str, Xsd]:
1169
+ """
1170
+ Retrieves a media object by its ID from the system. This method queries a SPARQL endpoint
1171
+ to fetch details about the media object and constructs a resulting dictionary with the
1172
+ media object attributes and its associated metadata. Additionally, a signed JWT token
1173
+ is included in the result for security purposes.
1174
+
1175
+ :param con: A connection interface that provides access to the SPARQL endpoint and user context.
1176
+ :type con: IConnection
1177
+ :param mediaObjectId: The ID of the media object to be fetched, represented as an XSD string or a Python string.
1178
+ :type mediaObjectId: Xsd_string | str
1179
+ :return: A dictionary containing key-value pairs of media object attributes and metadata,
1180
+ with an additional JWT token for security. Returns None if the media object is not found.
1181
+ :rtype: dict[str, Xsd] | None
1182
+ :raises OldapErrorNotFound: Raised when the media object with the specified ID is not found.
1183
+ """
1148
1184
  if not isinstance(mediaObjectId, Xsd_string):
1149
1185
  mediaObjectId = Xsd_string(mediaObjectId, validate=True)
1150
1186
  blank = ''
@@ -1152,19 +1188,14 @@ class ResourceInstance:
1152
1188
  sparql = context.sparql_context
1153
1189
 
1154
1190
  sparql += f"""
1155
- SELECT ?subject ?graph ?path ?permval ?originalName ?serverUrl ?protocol ?originalMimeType
1191
+ SELECT ?subject ?graph ?path ?prop ?val ?permval
1156
1192
  WHERE {{
1157
1193
  VALUES ?inputImageId {{ {mediaObjectId.toRdf} }}
1158
-
1159
1194
  ?subject rdf:type shared:MediaObject .
1160
1195
  GRAPH ?graph {{
1161
- ?subject shared:imageId ?inputImageId .
1162
- ?subject shared:originalName ?originalName .
1163
- ?subject shared:originalMimeType ?originalMimeType .
1164
- ?subject shared:serverUrl ?serverUrl .
1165
- ?subject shared:path ?path .
1166
- ?subject shared:protocol ?protocol .
1167
1196
  ?subject oldap:grantsPermission ?permset .
1197
+ ?subject shared:imageId ?inputImageId .
1198
+ ?subject ?prop ?val .
1168
1199
  }}
1169
1200
  GRAPH oldap:admin {{
1170
1201
  {con.userIri.toRdf} oldap:hasPermissions ?permset .
@@ -1179,16 +1210,110 @@ class ResourceInstance:
1179
1210
  print(sparql)
1180
1211
  raise
1181
1212
  res = QueryProcessor(context, jsonres)
1182
- if len(res) == 0 or len(res) > 1:
1213
+ if len(res) == 0:
1183
1214
  raise OldapErrorNotFound(f'Media object with id {mediaObjectId} not found.')
1184
- return {'iri': res[0]['subject'],
1185
- 'shared:originalName': res[0]['originalName'],
1186
- 'shared:originalMimeType': res[0]['originalMimeType'],
1187
- 'shared:serverUrl': res[0]['serverUrl'],
1188
- 'shared:protocol': res[0]['protocol'],
1189
- 'graph': res[0]['graph'],
1190
- 'shared:path': res[0]['path'],
1191
- 'oldap:permissionValue': res[0]['permval']}
1215
+ result: dict[str, Xsd] = {
1216
+ 'iri': res[0].get('subject'),
1217
+ 'graph': res[0].get('graph'),
1218
+ 'permval': res[0].get('permval')
1219
+ }
1220
+ for r in res:
1221
+ if str(r['prop']) == 'rdf:type':
1222
+ continue
1223
+ if str(r['prop']) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate',
1224
+ 'shared:imageId', 'shared:originalName', 'shared:originalMimeType', 'shared:serverUrl', 'shared:path', 'shared:protocol'}:
1225
+ result[str(r['prop'])] = r['val']
1226
+ else:
1227
+ if result.get(str(r['prop'])) is None:
1228
+ result[str(r['prop'])] = []
1229
+ result[str(r['prop'])].append(r['val'])
1230
+
1231
+ expiration = datetime.now().astimezone() + timedelta(minutes=2)
1232
+ payload = {
1233
+ 'userIri': str(con.userIri),
1234
+ 'userid': str(con.userid),
1235
+ 'id': str(mediaObjectId),
1236
+ 'path': str(result.get('shared:path')),
1237
+ 'permval': str(result.get('permval')),
1238
+ "exp": expiration.timestamp(),
1239
+ "iat": int(datetime.now().astimezone().timestamp()),
1240
+ "iss": "http://oldap.org"
1241
+ }
1242
+ token = jwt.encode(
1243
+ payload=payload,
1244
+ key=con.jwtkey,
1245
+ algorithm="HS256")
1246
+ result['token'] = token
1247
+
1248
+ return result
1249
+
1250
+ @staticmethod
1251
+ def get_media_object_by_iri(con: IConnection, mediaObjectIri: Iri | str) -> dict[str, Xsd] | None:
1252
+ if not isinstance(mediaObjectIri, Iri):
1253
+ mediaObjectIri = Iri(mediaObjectIri, validate=True)
1254
+ blank = ''
1255
+ context = Context(name=con.context_name)
1256
+ sparql = context.sparql_context
1257
+
1258
+ sparql += f"""
1259
+ SELECT ?graph ?prop ?val ?permval
1260
+ WHERE {{
1261
+ {mediaObjectIri.toRdf} rdf:type shared:MediaObject .
1262
+ GRAPH ?graph {{
1263
+ {mediaObjectIri.toRdf} oldap:grantsPermission ?permset .
1264
+ {mediaObjectIri.toRdf} shared:imageId ?inputImageId .
1265
+ {mediaObjectIri.toRdf} ?prop ?val .
1266
+ }}
1267
+ GRAPH oldap:admin {{
1268
+ {con.userIri.toRdf} oldap:hasPermissions ?permset .
1269
+ ?permset oldap:givesPermission ?DataPermission .
1270
+ ?DataPermission oldap:permissionValue ?permval .
1271
+ }}
1272
+ }}
1273
+ """
1274
+ print(sparql)
1275
+ try:
1276
+ jsonres = con.query(sparql)
1277
+ except OldapError:
1278
+ print(sparql)
1279
+ raise
1280
+ res = QueryProcessor(context, jsonres)
1281
+ if len(res) == 0:
1282
+ raise OldapErrorNotFound(f'Media object with iri {mediaObjectIri} not found.')
1283
+ result: dict[str, Xsd] = {
1284
+ 'iri': mediaObjectIri,
1285
+ 'graph': res[0].get('graph'),
1286
+ 'permval': res[0].get('permval')
1287
+ }
1288
+ for r in res:
1289
+ if str(r['prop']) == 'rdf:type':
1290
+ continue
1291
+ if str(r['prop']) in {'oldap:createdBy', 'oldap:creationDate', 'oldap:lastModifiedBy', 'oldap:lastModificationDate',
1292
+ 'shared:imageId', 'shared:originalName', 'shared:originalMimeType', 'shared:serverUrl', 'shared:path', 'shared:protocol'}:
1293
+ result[str(r['prop'])] = r['val']
1294
+ else:
1295
+ if result.get(str(r['prop'])) is None:
1296
+ result[str(r['prop'])] = []
1297
+ result[str(r['prop'])].append(r['val'])
1298
+
1299
+ expiration = datetime.now().astimezone() + timedelta(minutes=2)
1300
+ payload = {
1301
+ 'userIri': str(con.userIri),
1302
+ 'userid': str(con.userid),
1303
+ 'id': str(result.get('shared:imageId')),
1304
+ 'path': str(result['shared:path']),
1305
+ 'permval': str(result['permval']),
1306
+ "exp": expiration.timestamp(),
1307
+ "iat": int(datetime.now().astimezone().timestamp()),
1308
+ "iss": "http://oldap.org"
1309
+ }
1310
+ token = jwt.encode(
1311
+ payload=payload,
1312
+ key=con.jwtkey,
1313
+ algorithm="HS256")
1314
+ result['token'] = token
1315
+
1316
+ return result
1192
1317
 
1193
1318
 
1194
1319
  def toJsonObject(self) -> dict[str, list[str] | str]:
oldaplib/src/oldaplist.py CHANGED
@@ -725,6 +725,7 @@ class OldapList(Model):
725
725
  self.clear_changeset()
726
726
 
727
727
  cache = CacheSingletonRedis()
728
+ cache.delete(Xsd_QName(self.project.projectShortName, 'shacl'))
728
729
  cache.set(self.__iri, self)
729
730
 
730
731
  def update(self, indent: int = 0, indent_inc: int = 4) -> None:
@@ -798,6 +799,7 @@ class OldapList(Model):
798
799
  #
799
800
  cache = CacheSingletonRedis()
800
801
  cache.delete(self.__iri)
802
+ cache.delete(Xsd_QName(self.project.projectShortName, 'shacl'))
801
803
 
802
804
 
803
805
  def in_use_queries(self) -> (str, str):