|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
|
|
|
import json
|
|
|
|
|
|
-from app.models import AtlasEntity, AtlasProvenance
|
|
|
+from app.models import AtlasClaim, AtlasEntity
|
|
|
|
|
|
PREFIXES = """@prefix atlas: <http://world.eu.org/atlas_ontology#> .
|
|
|
@prefix atlas_data: <http://world.eu.org/atlas_data#> .
|
|
|
@@ -34,78 +34,56 @@ def _alias_node(alias_label: str) -> str:
|
|
|
return f"atlas_data:alias_{_safe_fragment(alias_label)}"
|
|
|
|
|
|
|
|
|
-def _identifier_node(identifier_value: str) -> str:
|
|
|
- return f"atlas_data:ident_{_safe_fragment(identifier_value)}"
|
|
|
+def _claim_node(claim: AtlasClaim) -> str:
|
|
|
+ return f"atlas_data:claim_{_safe_fragment(claim.claim_id)}"
|
|
|
|
|
|
|
|
|
-def _provenance_node(source: str, retrieved_at: str | None, retrieval_method: str) -> str:
|
|
|
- parts = [source, retrieval_method, retrieved_at or ""]
|
|
|
+def _provenance_node(claim: AtlasClaim) -> str:
|
|
|
+ prov = claim.provenance
|
|
|
+ if prov is None:
|
|
|
+ return ""
|
|
|
+ parts = [claim.claim_id, prov.source, prov.retrieval_method, prov.retrieved_at or ""]
|
|
|
return f"atlas_data:prov_{_safe_fragment('_'.join(parts))}"
|
|
|
|
|
|
|
|
|
-def _type_assertion_node(entity: AtlasEntity, source: str) -> str:
|
|
|
- return f"atlas_data:typeassert_{_safe_fragment(entity.atlas_id)}_{_safe_fragment(source)}"
|
|
|
-
|
|
|
-
|
|
|
def _literal(text: str) -> str:
|
|
|
return text.replace("\\", "\\\\").replace('"', '\\"')
|
|
|
|
|
|
|
|
|
-def _identifier_type_resource(identifier_type: str) -> str:
|
|
|
- kind = _safe_fragment(identifier_type)
|
|
|
- if kind == "mid":
|
|
|
- return "atlas:Mid"
|
|
|
- if kind in {"qid", "wikidata_qid", "wikidataqid"}:
|
|
|
- return "atlas:WikidataQID"
|
|
|
- return f"atlas:{kind.capitalize()}"
|
|
|
-
|
|
|
-
|
|
|
-def _pick_provenance(entity: AtlasEntity, source_hint: str | None = None, method_hint: str | None = None) -> AtlasProvenance | None:
|
|
|
- if not entity.provenance:
|
|
|
- return None
|
|
|
- if method_hint:
|
|
|
- for p in entity.provenance:
|
|
|
- if p.retrieval_method == method_hint:
|
|
|
- return p
|
|
|
- if source_hint:
|
|
|
- for p in entity.provenance:
|
|
|
- if p.source == source_hint:
|
|
|
- return p
|
|
|
- return entity.provenance[0]
|
|
|
+def _claim_object_iri(claim: AtlasClaim) -> str | None:
|
|
|
+ if claim.object.kind == "type":
|
|
|
+ return claim.object.value
|
|
|
+ if claim.object.kind == "identifier" and claim.object.id_type:
|
|
|
+ return f"atlas_data:ident_{_safe_fragment(claim.object.id_type + '_' + claim.object.value)}"
|
|
|
+ return None
|
|
|
|
|
|
|
|
|
def entity_to_turtle(entity: AtlasEntity) -> str:
|
|
|
lines: list[str] = [PREFIXES]
|
|
|
subject = _entity_node(entity)
|
|
|
|
|
|
- claim_nodes = [f"atlas_data:claim_ident_{_safe_fragment(i.value)}" for i in entity.identifiers]
|
|
|
- if entity.entity_type and entity.entity_type != "unknown":
|
|
|
- claim_nodes.append(f"atlas_data:claim_type_{_safe_fragment(entity.atlas_id)}")
|
|
|
-
|
|
|
lines.append(f"{subject} a atlas:Entity ;")
|
|
|
lines.append(f' atlas:canonicalLabel "{_literal(entity.canonical_label)}" ;')
|
|
|
if entity.canonical_description:
|
|
|
lines.append(f' atlas:canonicalDescription "{_literal(entity.canonical_description)}" ;')
|
|
|
+ if entity.entity_type and entity.entity_type != "unknown":
|
|
|
+ lines.append(f" atlas:hasCanonicalType atlas:{_safe_fragment(entity.entity_type).capitalize()} ;")
|
|
|
|
|
|
- # Lean raw payload persistence (as JSON strings)
|
|
|
wd = entity.raw_payload.get("wikidata") if isinstance(entity.raw_payload, dict) else None
|
|
|
if isinstance(wd, dict) and wd.get("status") == "ok":
|
|
|
lines.append(f' atlas:rawWikidataJson "{_literal(json.dumps(wd, ensure_ascii=False))}"^^xsd:string ;')
|
|
|
|
|
|
- trends_payload = entity.raw_payload.get("g_trends_payload") or {}
|
|
|
- # In our current model, trends live under raw_payload keys directly (non-wikidata)
|
|
|
if isinstance(entity.raw_payload, dict):
|
|
|
trends_payload = {k: v for k, v in entity.raw_payload.items() if k != "wikidata"}
|
|
|
- if isinstance(trends_payload, dict) and trends_payload:
|
|
|
- lines.append(f' atlas:rawTrendsJson "{_literal(json.dumps(trends_payload, ensure_ascii=False))}"^^xsd:string ;')
|
|
|
- if entity.entity_type and entity.entity_type != "unknown":
|
|
|
- lines.append(f" atlas:hasCanonicalType atlas:{_safe_fragment(entity.entity_type).capitalize()} ;")
|
|
|
+ if trends_payload:
|
|
|
+ lines.append(f' atlas:rawTrendsJson "{_literal(json.dumps(trends_payload, ensure_ascii=False))}"^^xsd:string ;')
|
|
|
+
|
|
|
for alias in entity.aliases:
|
|
|
lines.append(f" atlas:hasAlias {_alias_node(alias.label)} ;")
|
|
|
- for ident in entity.identifiers:
|
|
|
- lines.append(f" atlas:hasIdentifier {_identifier_node(ident.value)} ;")
|
|
|
- for claim_node in claim_nodes:
|
|
|
- lines.append(f" atlas:hasClaim {claim_node} ;")
|
|
|
+
|
|
|
+ for claim in entity.claims:
|
|
|
+ lines.append(f" atlas:hasClaim {_claim_node(claim)} ;")
|
|
|
+
|
|
|
lines.append(f" atlas:needsCuration {'true' if entity.needs_curation else 'false'} .")
|
|
|
lines.append("")
|
|
|
|
|
|
@@ -116,82 +94,49 @@ def entity_to_turtle(entity: AtlasEntity) -> str:
|
|
|
lines.append(f" atlas:resolvedTo {subject} .")
|
|
|
lines.append("")
|
|
|
|
|
|
- for ident in entity.identifiers:
|
|
|
- ident_node = _identifier_node(ident.value)
|
|
|
+ # Materialize identifier resources from identifier claims.
|
|
|
+ for claim in entity.claims:
|
|
|
+ if claim.predicate != "atlas:hasIdentifier" or claim.object.kind != "identifier":
|
|
|
+ continue
|
|
|
+ ident_node = _claim_object_iri(claim)
|
|
|
+ if not ident_node:
|
|
|
+ continue
|
|
|
+ id_type = claim.object.id_type or "unknown"
|
|
|
+ id_type_iri = "atlas:Mid" if id_type == "mid" else ("atlas:WikidataQID" if id_type == "qid" else f"atlas:{_safe_fragment(id_type).capitalize()}")
|
|
|
lines.append(f"{ident_node} a atlas:Identifier ;")
|
|
|
- lines.append(f' atlas:identifierValue "{_literal(ident.value)}" ;')
|
|
|
- lines.append(f' atlas:identifierSource "{_literal(ident.source)}" ;')
|
|
|
- lines.append(f" atlas:identifierType {_identifier_type_resource(ident.identifier_type)} ;")
|
|
|
- prov = _pick_provenance(entity, source_hint=ident.source)
|
|
|
- if prov:
|
|
|
- lines.append(f" atlas:hasIdentifierProvenance {_provenance_node(prov.source, prov.retrieved_at, prov.retrieval_method)} .")
|
|
|
- else:
|
|
|
- lines[-1] = lines[-1].rstrip(" ;") + " ."
|
|
|
+ lines.append(f' atlas:identifierValue "{_literal(claim.object.value)}" ;')
|
|
|
+ lines.append(f' atlas:identifierType {id_type_iri} .')
|
|
|
lines.append("")
|
|
|
|
|
|
- for prov in entity.provenance:
|
|
|
- prov_node = _provenance_node(prov.source, prov.retrieved_at, prov.retrieval_method)
|
|
|
- lines.append(f"{prov_node} a atlas:Provenance ;")
|
|
|
- lines.append(f' atlas:provenanceSource "{_literal(prov.source)}" ;')
|
|
|
- lines.append(f' atlas:retrievalMethod "{_literal(prov.retrieval_method)}" ;')
|
|
|
- lines.append(f' atlas:confidence "{prov.confidence}"^^xsd:decimal ;')
|
|
|
- if prov.retrieved_at:
|
|
|
- lines.append(f' atlas:retrievedAt "{_literal(prov.retrieved_at)}"^^xsd:dateTime .')
|
|
|
- else:
|
|
|
- lines[-1] = lines[-1].rstrip(" ;") + " ."
|
|
|
- lines.append("")
|
|
|
-
|
|
|
- wd = entity.raw_payload.get("wikidata") or {}
|
|
|
- if wd.get("status") == "ok":
|
|
|
- typeassert_node = _type_assertion_node(entity, "wikidata")
|
|
|
- lines.append(f"{typeassert_node} a atlas:TypeAssertion ;")
|
|
|
- lines.append(" atlas:assertedType atlas:WikidataType_Q5 ;")
|
|
|
- prov = _pick_provenance(entity, source_hint="wikidata")
|
|
|
- if prov:
|
|
|
- lines.append(f" atlas:hasAssertionProvenance {_provenance_node(prov.source, prov.retrieved_at, prov.retrieval_method)} ;")
|
|
|
- lines.append(' atlas:assertionReason "wikidata instance-of" .')
|
|
|
- lines.append("")
|
|
|
-
|
|
|
- if entity.entity_type and entity.entity_type != "unknown":
|
|
|
- typeassert_node = _type_assertion_node(entity, "canonical")
|
|
|
- lines.append(f"{typeassert_node} a atlas:TypeAssertion ;")
|
|
|
- lines.append(f" atlas:assertedType atlas:{_safe_fragment(entity.entity_type).capitalize()} ;")
|
|
|
- prov = _pick_provenance(entity, method_hint="type-classification")
|
|
|
- if prov:
|
|
|
- lines.append(f" atlas:hasAssertionProvenance {_provenance_node(prov.source, prov.retrieved_at, prov.retrieval_method)} ;")
|
|
|
- lines.append(' atlas:assertionReason "canonical type adjudication" .')
|
|
|
- lines.append("")
|
|
|
-
|
|
|
- # Claim nodes with explicit claim-object semantics
|
|
|
- for ident in entity.identifiers:
|
|
|
- claim_node = f"atlas_data:claim_ident_{_safe_fragment(ident.value)}"
|
|
|
- ident_node = _identifier_node(ident.value)
|
|
|
- prov = _pick_provenance(entity, source_hint=ident.source)
|
|
|
+ for claim in entity.claims:
|
|
|
+ claim_node = _claim_node(claim)
|
|
|
lines.append(f"{claim_node} a atlas:Claim ;")
|
|
|
lines.append(f" atlas:claimSubjectIri {subject} ;")
|
|
|
- lines.append(' atlas:claimPredicate "atlas:hasIdentifier" ;')
|
|
|
- lines.append(f" atlas:claimObjectIri {ident_node} ;")
|
|
|
- lines.append(' atlas:claimLayer "raw" ;')
|
|
|
- lines.append(' atlas:claimStatus "active" ;')
|
|
|
- if prov:
|
|
|
- lines.append(f" atlas:hasProvenance {_provenance_node(prov.source, prov.retrieved_at, prov.retrieval_method)} .")
|
|
|
+ lines.append(f' atlas:claimPredicate "{_literal(claim.predicate)}" ;')
|
|
|
+ obj_iri = _claim_object_iri(claim)
|
|
|
+ if obj_iri:
|
|
|
+ lines.append(f" atlas:claimObjectIri {obj_iri} ;")
|
|
|
else:
|
|
|
- lines[-1] = lines[-1].rstrip(" ;") + " ."
|
|
|
- lines.append("")
|
|
|
-
|
|
|
- if entity.entity_type and entity.entity_type != "unknown":
|
|
|
- claim_node = f"atlas_data:claim_type_{_safe_fragment(entity.atlas_id)}"
|
|
|
- prov = _pick_provenance(entity, method_hint="type-classification")
|
|
|
- lines.append(f"{claim_node} a atlas:Claim ;")
|
|
|
- lines.append(f" atlas:claimSubjectIri {subject} ;")
|
|
|
- lines.append(' atlas:claimPredicate "atlas:hasCanonicalType" ;')
|
|
|
- lines.append(f" atlas:claimObjectIri atlas:{_safe_fragment(entity.entity_type).capitalize()} ;")
|
|
|
- lines.append(' atlas:claimLayer "derived" ;')
|
|
|
- lines.append(' atlas:claimStatus "active" ;')
|
|
|
- if prov:
|
|
|
- lines.append(f" atlas:hasProvenance {_provenance_node(prov.source, prov.retrieved_at, prov.retrieval_method)} .")
|
|
|
+ lines.append(f' atlas:claimObjectLiteral "{_literal(claim.object.value)}" ;')
|
|
|
+ lines.append(f' atlas:claimLayer "{_literal(claim.layer)}" ;')
|
|
|
+ lines.append(f' atlas:claimStatus "{_literal(claim.status)}" ;')
|
|
|
+ prov_node = _provenance_node(claim)
|
|
|
+ if prov_node:
|
|
|
+ lines.append(f" atlas:hasProvenance {prov_node} .")
|
|
|
else:
|
|
|
lines[-1] = lines[-1].rstrip(" ;") + " ."
|
|
|
lines.append("")
|
|
|
|
|
|
+ if claim.provenance:
|
|
|
+ prov = claim.provenance
|
|
|
+ lines.append(f"{prov_node} a atlas:Provenance ;")
|
|
|
+ lines.append(f' atlas:provenanceSource "{_literal(prov.source)}" ;')
|
|
|
+ lines.append(f' atlas:retrievalMethod "{_literal(prov.retrieval_method)}" ;')
|
|
|
+ lines.append(f' atlas:confidence "{prov.confidence}"^^xsd:decimal ;')
|
|
|
+ if prov.retrieved_at:
|
|
|
+ lines.append(f' atlas:retrievedAt "{_literal(prov.retrieved_at)}"^^xsd:dateTime .')
|
|
|
+ else:
|
|
|
+ lines[-1] = lines[-1].rstrip(" ;") + " ."
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
return "\n".join(lines).strip() + "\n"
|