Coverage for oc_ocdm / metadata / metadata_entity.py: 81%
107 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-06 20:05 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-06 20:05 +0000
1#!/usr/bin/python
3# SPDX-FileCopyrightText: 2020-2022 Simone Persiani <iosonopersia@gmail.com>
4#
5# SPDX-License-Identifier: ISC
7# -*- coding: utf-8 -*-
8from __future__ import annotations
10from typing import TYPE_CHECKING, List
12from triplelite import RDFTerm, SubgraphView, Triple, TripleLite, rdflib_to_rdfterm
14from oc_ocdm.abstract_entity import AbstractEntity
15from oc_ocdm.constants import Namespace
17if TYPE_CHECKING:
18 from typing import ClassVar, Dict
20 from oc_ocdm.metadata.metadata_set import MetadataSet
23class MetadataEntity(AbstractEntity):
24 DCTERMS = Namespace("http://purl.org/dc/terms/")
25 DCAT = Namespace("http://www.w3.org/ns/dcat#")
26 VOID = Namespace("http://rdfs.org/ns/void#")
28 iri_dataset = DCAT.Dataset
29 iri_datafile = DCAT.Distribution
31 iri_title = DCTERMS.title
32 iri_description = DCTERMS.description
33 iri_issued = DCTERMS.issued
34 iri_modified = DCTERMS.modified
35 iri_keyword = DCAT.keyword
36 iri_subject = DCAT.theme
37 iri_landing_page = DCAT.landingPage
38 iri_subset = VOID.subset
39 iri_sparql_endpoint = VOID.sparqlEndpoint
40 iri_distribution = DCAT.distribution
41 iri_license = DCTERMS.license
42 iri_download_url = DCAT.downloadURL
43 iri_media_type = DCAT.mediaType
44 iri_byte_size = DCAT.byteSize
46 short_name_to_type_iri: ClassVar[Dict[str, str]] = {"_dataset_": iri_dataset, "di": iri_datafile}
48 def __init__(
49 self,
50 g: TripleLite,
51 base_iri: str,
52 dataset_name: str,
53 m_set: MetadataSet,
54 res_type: str,
55 res: str | None = None,
56 resp_agent: str | None = None,
57 source: str | None = None,
58 count: str | None = None,
59 label: str | None = None,
60 short_name: str = "",
61 preexisting_graph: SubgraphView | None = None,
62 ) -> None:
63 super(MetadataEntity, self).__init__()
64 self.g: TripleLite = g
65 self.base_iri: str = base_iri
66 self.dataset_name: str = dataset_name
67 self.resp_agent: str | None = resp_agent
68 self.source: str | None = source
69 self.short_name: str = short_name
70 self.m_set: MetadataSet = m_set
71 self._preexisting_triples: frozenset[Triple] = frozenset()
72 self._merge_list: tuple[MetadataEntity, ...] = ()
73 # FLAGS
74 self._to_be_deleted: bool = False
75 self._was_merged: bool = False
77 # If res was not specified, create from scratch the URI reference for this entity,
78 # otherwise use the provided one
79 if res is None:
80 base_res: str = self.base_iri + self.dataset_name
81 if base_res[-1] != "/":
82 base_res += "/"
83 self.res = self._generate_new_res(count, base_res, short_name)
84 else:
85 self.res = res
87 # If not already done, register this MetadataEntity instance inside the MetadataSet
88 if self.res not in m_set.res_to_entity:
89 m_set.res_to_entity[self.res] = self
91 if preexisting_graph is not None:
92 self.remove_every_triple()
93 self._preexisting_triples = frozenset(
94 (self.res, p, rdflib_to_rdfterm(o)) for p, o in preexisting_graph.predicate_objects(self.res)
95 )
96 self.g.add_many(self._preexisting_triples)
97 else:
98 # Add mandatory information to the entity graph
99 self._create_type(res_type)
100 if label is not None:
101 self.create_label(label)
103 @staticmethod
104 def _generate_new_res(count: str | None, base_res: str, short_name: str) -> str:
105 if short_name == "_dataset_":
106 return base_res
107 else:
108 assert count is not None
109 return base_res + short_name + "/" + count
111 @property
112 def to_be_deleted(self) -> bool:
113 return self._to_be_deleted
115 @property
116 def was_merged(self) -> bool:
117 return self._was_merged
119 @property
120 def merge_list(self) -> tuple[MetadataEntity, ...]:
121 return self._merge_list
123 @property
124 def preexisting_triples(self) -> frozenset[Triple]:
125 return self._preexisting_triples
127 def mark_as_to_be_deleted(self) -> None:
128 # Here we must REMOVE triples pointing
129 # to 'self' [THIS CANNOT BE UNDONE]:
130 for res, entity in self.m_set.res_to_entity.items():
131 triples_list: List[Triple] = list(entity.g.triples((res, None, RDFTerm("uri", str(self.res)))))
132 for triple in triples_list:
133 entity.g.remove(triple)
135 self._to_be_deleted = True
137 def merge(self, other: object) -> None:
138 """
139 **WARNING:** ``MetadataEntity`` **is an abstract class that cannot be instantiated at runtime.
140 As such, it's only possible to execute this method on entities generated from**
141 ``MetadataEntity``'s **subclasses. Please, refer to their documentation of the** `merge` **method.**
143 :param other: The entity which will be marked as to be deleted and whose properties will
144 be merged into the current entity.
145 :type other: MetadataEntity
146 :raises TypeError: if the parameter is of the wrong type
147 :return: None
148 """
149 if not isinstance(other, MetadataEntity) or other.short_name != self.short_name:
150 raise TypeError(
151 f"[{self.__class__.__name__}.merge] Expected entity type: {self.short_name}. "
152 f"Provided: {type(other).__name__}."
153 )
155 # Here we must REDIRECT triples pointing
156 # to 'other' to make them point to 'self':
157 for res, entity in self.m_set.res_to_entity.items():
158 triples_list: List[Triple] = list(entity.g.triples((res, None, RDFTerm("uri", str(other.res)))))
159 for triple in triples_list:
160 entity.g.remove(triple)
161 new_triple = (triple[0], triple[1], RDFTerm("uri", str(self.res)))
162 entity.g.add(new_triple)
164 types: List[str] = other.get_types()
165 for cur_type in types:
166 self._create_type(cur_type)
168 label: str | None = other.get_label()
169 if label is not None:
170 self.create_label(label)
172 self._was_merged = True
173 self._merge_list = (*self._merge_list, other)
175 # 'other' must be deleted AFTER the redirection of
176 # triples pointing to it, since mark_as_to_be_deleted
177 # also removes every triple pointing to 'other'
178 other.mark_as_to_be_deleted()
180 self._merge_properties(other)
182 def _merge_properties(self, other: MetadataEntity) -> None:
183 pass
185 def commit_changes(self) -> None:
186 if self._to_be_deleted:
187 self._preexisting_triples = frozenset()
188 self.remove_every_triple()
189 else:
190 self._preexisting_triples = frozenset(self.g.triples((self.res, None, None)))
191 self._to_be_deleted = False
192 self._was_merged = False
193 self._merge_list = ()