cks-core 1.1.1__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.
cks/__init__.py ADDED
@@ -0,0 +1,124 @@
1
+ """
2
+ Canonical Knowledge Structure (CKS).
3
+
4
+ Reference implementation of the Canonical Knowledge Structure (CKS)
5
+ specifications.
6
+
7
+ The package exposes the canonical public API defined by CKS-007 together
8
+ with the immutable canonical data model, structural evolution operators,
9
+ and a plugin system for external constraints.
10
+
11
+ Typical usage:
12
+
13
+ import cks
14
+
15
+ structure = cks.parse(source)
16
+ result = cks.validate(structure)
17
+
18
+ Advanced users may instantiate their own ReferenceEngine or build
19
+ custom validation pipelines using the lower-level modules.
20
+
21
+ Only symbols defined here should normally be imported by user code.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from .core import (
27
+ CanonicalRelation,
28
+ KnowledgeObject,
29
+ KnowledgeStructure,
30
+ ObjectIdentity,
31
+ )
32
+
33
+ from .diagnostics import (
34
+ Diagnostic,
35
+ DiagnosticCollection,
36
+ DiagnosticSeverity,
37
+ )
38
+
39
+ from .engine import ReferenceEngine
40
+
41
+ from .evolution import (
42
+ AddObject,
43
+ AddRelation,
44
+ RemoveObject,
45
+ RemoveRelation,
46
+ compose,
47
+ )
48
+
49
+ from .plugin import load_external_constraints
50
+
51
+ from .result import ValidationResult
52
+
53
+ from .serialization import SerializationError
54
+
55
+ from .interface import (
56
+ compare,
57
+ construct,
58
+ diagnose,
59
+ evolve,
60
+ extract,
61
+ inspect,
62
+ parse,
63
+ project,
64
+ serialize,
65
+ validate,
66
+ )
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Bootstrap external constraint plugins
70
+ # ---------------------------------------------------------------------------
71
+
72
+ _EXTERNAL_COUNT = load_external_constraints()
73
+
74
+ __version__ = "1.1.1"
75
+
76
+ VERSION = tuple(
77
+ int(part)
78
+ for part in __version__.split(".")
79
+ )
80
+
81
+ __all__ = [
82
+ # Public API
83
+ "construct",
84
+ "parse",
85
+ "serialize",
86
+ "validate",
87
+ "diagnose",
88
+ "inspect",
89
+ "compare",
90
+ "extract",
91
+ "project",
92
+ "evolve",
93
+
94
+ # Core model
95
+ "ObjectIdentity",
96
+ "KnowledgeObject",
97
+ "CanonicalRelation",
98
+ "KnowledgeStructure",
99
+
100
+ # Evolution operators
101
+ "AddObject",
102
+ "AddRelation",
103
+ "RemoveObject",
104
+ "RemoveRelation",
105
+ "compose",
106
+
107
+ # Diagnostics
108
+ "DiagnosticSeverity",
109
+ "Diagnostic",
110
+ "DiagnosticCollection",
111
+
112
+ # Validation
113
+ "ValidationResult",
114
+
115
+ # Engine
116
+ "ReferenceEngine",
117
+
118
+ # Exceptions
119
+ "SerializationError",
120
+
121
+ # Package metadata
122
+ "__version__",
123
+ "VERSION",
124
+ ]
@@ -0,0 +1,62 @@
1
+ """
2
+ CKS Adapter — CKS to JSON‑LD Converter.
3
+
4
+ Converts a Canonical Knowledge Structure into a JSON‑LD document.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, List
10
+
11
+ from ..core import KnowledgeStructure, CanonicalRelation
12
+
13
+
14
+ class CksToJsonLdConverter:
15
+ """Transform a KnowledgeStructure into a JSON‑LD document."""
16
+
17
+ def __init__(self, structure: KnowledgeStructure) -> None:
18
+ self._structure = structure
19
+
20
+ def convert(self) -> Dict[str, Any]:
21
+ """Run the conversion and return a JSON‑LD dictionary."""
22
+ graph: List[Dict[str, Any]] = []
23
+
24
+ # 1. Convert every KnowledgeObject to a JSON‑LD entity
25
+ for obj in self._structure.objects:
26
+ if isinstance(obj, CanonicalRelation):
27
+ continue # handled separately
28
+ entity: Dict[str, Any] = {
29
+ "@id": obj.identity.id,
30
+ "@type": obj.identity.type,
31
+ "name": obj.identity.name,
32
+ }
33
+ # Add remaining structure fields
34
+ for key, value in obj.structure.items():
35
+ if key not in ("participants", "relation_type"):
36
+ entity[key] = value
37
+ graph.append(entity)
38
+
39
+ # 2. Convert every CanonicalRelation to JSON‑LD properties
40
+ for rel in self._structure.relations():
41
+ # Find the subject entity and add the relation as a property
42
+ subj_id = rel.participants[0]
43
+ obj_id = rel.participants[1] if len(rel.participants) > 1 else None
44
+ if obj_id is None:
45
+ continue
46
+
47
+ # Locate the subject entity in the graph
48
+ for entity in graph:
49
+ if entity["@id"] == subj_id:
50
+ if rel.relation_type not in entity:
51
+ entity[rel.relation_type] = []
52
+ entity[rel.relation_type].append({"@id": obj_id})
53
+ break
54
+ else:
55
+ # Subject not found — create a minimal entity
56
+ graph.append({
57
+ "@id": subj_id,
58
+ "@type": "Entity",
59
+ rel.relation_type: [{"@id": obj_id}],
60
+ })
61
+
62
+ return {"@graph": graph}
@@ -0,0 +1,69 @@
1
+ """
2
+ CKS Adapter — CKS to RDF Converter.
3
+
4
+ Converts a Canonical Knowledge Structure into an RDF graph
5
+ (Turtle or RDF/XML).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ from rdflib import RDF, RDFS, Graph, Literal, URIRef
12
+
13
+ from ..core import CanonicalRelation, KnowledgeStructure
14
+
15
+ # Base namespace for CKS entities that don't have an absolute URI.
16
+ CKS_NS = "http://cks.org/"
17
+
18
+
19
+ def _to_uri(raw: str) -> URIRef:
20
+ """Return *raw* as an absolute URI, prepending CKS_NS if necessary."""
21
+ if "://" in raw:
22
+ return URIRef(raw)
23
+ return URIRef(CKS_NS + raw)
24
+
25
+
26
+ class CksToRdfConverter:
27
+ """Transform a KnowledgeStructure into an RDF graph."""
28
+
29
+ def __init__(self, structure: KnowledgeStructure) -> None:
30
+ self._structure = structure
31
+ self._graph = Graph()
32
+
33
+ def convert(self) -> Graph:
34
+ """Run the conversion and return an rdflib Graph."""
35
+ # 1. Convert every KnowledgeObject to an RDF resource
36
+ for obj in self._structure.objects:
37
+ if isinstance(obj, CanonicalRelation):
38
+ continue
39
+ subject = _to_uri(obj.identity.id)
40
+ # Type
41
+ self._graph.add((subject, RDF.type, _to_uri(obj.identity.type)))
42
+ # Name / label
43
+ self._graph.add((subject, RDFS.label, Literal(obj.identity.name)))
44
+ # Additional structure fields
45
+ for key, value in obj.structure.items():
46
+ if key in ("participants", "relation_type"):
47
+ continue
48
+ if isinstance(value, (str, int, float)):
49
+ self._graph.add((subject, _to_uri(key), Literal(value)))
50
+
51
+ # 2. Convert every CanonicalRelation to RDF triples
52
+ for rel in self._structure.relations():
53
+ subj_id = rel.participants[0]
54
+ obj_id = rel.participants[1] if len(rel.participants) > 1 else None
55
+ if obj_id is None:
56
+ continue
57
+ self._graph.add(
58
+ (_to_uri(subj_id), _to_uri(rel.relation_type), _to_uri(obj_id))
59
+ )
60
+
61
+ return self._graph
62
+
63
+ def to_turtle(self) -> str:
64
+ """Return the RDF graph serialised as Turtle."""
65
+ return self._graph.serialize(format="turtle")
66
+
67
+ def to_rdfxml(self) -> str:
68
+ """Return the RDF graph serialised as RDF/XML."""
69
+ return self._graph.serialize(format="xml")
@@ -0,0 +1,121 @@
1
+ """
2
+ CKS Adapter — JSON‑LD to CKS Converter.
3
+
4
+ Converts a JSON‑LD document into a Canonical Knowledge Structure.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from ..core import (
12
+ CanonicalRelation,
13
+ KnowledgeObject,
14
+ KnowledgeStructure,
15
+ ObjectIdentity,
16
+ )
17
+
18
+
19
+ class JsonLdToCksConverter:
20
+ """Transform a JSON‑LD document into a KnowledgeStructure."""
21
+
22
+ def __init__(self, jsonld_data: Dict[str, Any]) -> None:
23
+ self._data = jsonld_data
24
+
25
+ # ------------------------------------------------------------------
26
+ # Public API
27
+ # ------------------------------------------------------------------
28
+
29
+ def convert(self) -> KnowledgeStructure:
30
+ """Run the conversion and return a KnowledgeStructure."""
31
+ objects: List[KnowledgeObject] = []
32
+
33
+ # 1. Merge entities with the same @id
34
+ merged_entities = self._merge_entities()
35
+
36
+ # 2. Convert each merged entity to a KnowledgeObject
37
+ for entity in merged_entities.values():
38
+ objects.append(self._entity_to_ko(entity))
39
+
40
+ # 3. Convert relations
41
+ for relation in self._iter_relations():
42
+ objects.append(relation)
43
+
44
+ return KnowledgeStructure(objects)
45
+
46
+ # ------------------------------------------------------------------
47
+ # Internal helpers
48
+ # ------------------------------------------------------------------
49
+
50
+ def _merge_entities(self) -> Dict[str, Dict[str, Any]]:
51
+ """Merge all nodes that share the same @id."""
52
+ merged: Dict[str, Dict[str, Any]] = {}
53
+ for entity in self._iter_entities():
54
+ oid = entity.get("@id", "unknown")
55
+ if oid not in merged:
56
+ merged[oid] = dict(entity)
57
+ else:
58
+ # Merge: later properties override earlier ones
59
+ merged[oid].update(entity)
60
+ return merged
61
+
62
+ def _iter_entities(self) -> List[Dict[str, Any]]:
63
+ """Yield every entity described in the JSON‑LD document."""
64
+ graph = self._data.get("@graph")
65
+ if isinstance(graph, list):
66
+ return graph
67
+ if isinstance(self._data, dict):
68
+ return [self._data]
69
+ return []
70
+
71
+ def _entity_to_ko(self, entity: Dict[str, Any]) -> KnowledgeObject:
72
+ oid = entity.get("@id", "unknown")
73
+ otype = self._pick_type(entity)
74
+ name = entity.get("name", entity.get("rdfs:label", oid))
75
+
76
+ identity = ObjectIdentity(id=oid, type=otype, name=str(name))
77
+ structure = dict(entity)
78
+ return KnowledgeObject(identity=identity, structure=structure)
79
+
80
+ @staticmethod
81
+ def _pick_type(entity: Dict[str, Any]) -> str:
82
+ types = entity.get("@type", [])
83
+ if isinstance(types, list) and types:
84
+ return str(types[0])
85
+ if isinstance(types, str):
86
+ return types
87
+ return "Entity"
88
+
89
+ def _iter_relations(self) -> List[CanonicalRelation]:
90
+ relations: List[CanonicalRelation] = []
91
+ for entity in self._iter_entities():
92
+ subject_id = entity.get("@id")
93
+ if not subject_id:
94
+ continue
95
+ for predicate, objects in entity.items():
96
+ if predicate in ("@id", "@type", "@context", "@graph", "name", "rdfs:label"):
97
+ continue
98
+ if not isinstance(objects, list):
99
+ objects = [objects]
100
+ for obj in objects:
101
+ obj_id = self._object_to_id(obj)
102
+ if obj_id is None:
103
+ continue
104
+ rel_id = f"{subject_id}-{predicate}-{obj_id}"
105
+ relation = CanonicalRelation(
106
+ identity=ObjectIdentity(
107
+ id=rel_id, type="Relation", name=predicate
108
+ ),
109
+ participants=[subject_id, obj_id],
110
+ relation_type=predicate,
111
+ )
112
+ relations.append(relation)
113
+ return relations
114
+
115
+ @staticmethod
116
+ def _object_to_id(obj: Any) -> Optional[str]:
117
+ if isinstance(obj, str):
118
+ return obj
119
+ if isinstance(obj, dict):
120
+ return obj.get("@id")
121
+ return None
@@ -0,0 +1,88 @@
1
+ """
2
+ CKS Adapter — RDF to CKS Converter.
3
+
4
+ Converts an RDF graph (RDF/XML, Turtle, etc.) into a Canonical Knowledge Structure.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ import rdflib
11
+
12
+ from ..core import (
13
+ CanonicalRelation,
14
+ KnowledgeObject,
15
+ KnowledgeStructure,
16
+ ObjectIdentity,
17
+ )
18
+
19
+
20
+ class RdfToCksConverter:
21
+ """Transform an RDF graph into a KnowledgeStructure."""
22
+
23
+ def __init__(self, rdf_data: str, format: str = "turtle") -> None:
24
+ self._graph = rdflib.Graph()
25
+ self._graph.parse(data=rdf_data, format=format)
26
+
27
+ def convert(self) -> KnowledgeStructure:
28
+ """Run the conversion and return a KnowledgeStructure."""
29
+ objects: list[KnowledgeObject] = []
30
+ seen_ids: set[str] = set()
31
+ relations: list[CanonicalRelation] = []
32
+
33
+ # 1. Convert every subject to a KnowledgeObject
34
+ for subject in self._graph.subjects():
35
+ oid = str(subject)
36
+ if oid not in seen_ids:
37
+ seen_ids.add(oid)
38
+ objects.append(self._subject_to_ko(subject))
39
+
40
+ # 2. Convert triples
41
+ for s, p, o in self._graph:
42
+ if isinstance(o, rdflib.Literal):
43
+ # Skip literal objects – they are not KnowledgeObjects
44
+ continue
45
+
46
+ subj_id = str(s)
47
+ pred = str(p)
48
+ obj_id = str(o)
49
+
50
+ # Ensure participants exist
51
+ for pid in (subj_id, obj_id):
52
+ if pid not in seen_ids:
53
+ seen_ids.add(pid)
54
+ objects.append(
55
+ KnowledgeObject(
56
+ identity=ObjectIdentity(id=pid, type="Entity", name=pid),
57
+ structure={},
58
+ )
59
+ )
60
+
61
+ rel_id = f"{subj_id}-{pred}-{obj_id}"
62
+ relation = CanonicalRelation(
63
+ identity=ObjectIdentity(id=rel_id, type="Relation", name=pred),
64
+ participants=[subj_id, obj_id],
65
+ relation_type=pred,
66
+ )
67
+ relations.append(relation)
68
+
69
+ all_objects = objects + list(relations)
70
+ return KnowledgeStructure(all_objects)
71
+
72
+ def _subject_to_ko(self, subject: rdflib.term.Node) -> KnowledgeObject:
73
+ oid = str(subject)
74
+ # Try to get a human-readable label
75
+ label = None
76
+ for _, _, lbl in self._graph.triples((subject, rdflib.RDFS.label, None)):
77
+ label = str(lbl)
78
+ break
79
+ name = label or oid
80
+
81
+ # Try to get a type
82
+ otype = "Entity"
83
+ for _, _, t in self._graph.triples((subject, rdflib.RDF.type, None)):
84
+ otype = str(t).split("#")[-1] if "#" in str(t) else str(t)
85
+ break
86
+
87
+ identity = ObjectIdentity(id=oid, type=otype, name=name)
88
+ return KnowledgeObject(identity=identity, structure={})
cks/cli/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ CKS Command-Line Interface.
3
+
4
+ Canonical entry point for interacting with CKS from the terminal.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import sys
10
+ from typing import Optional, Sequence
11
+
12
+ from .commands import (
13
+ validate,
14
+ parse,
15
+ inspect,
16
+ evolve,
17
+ convert,
18
+ export,
19
+ schema,
20
+ plugin,
21
+ )
22
+
23
+
24
+ def _create_parser() -> argparse.ArgumentParser:
25
+ parser = argparse.ArgumentParser(
26
+ prog="cks",
27
+ description="Canonical Knowledge Structure — CLI",
28
+ )
29
+
30
+ parser.add_argument("--strict", action="store_true", help="Fail on any plugin loading error")
31
+
32
+ sub = parser.add_subparsers(dest="command", required=True)
33
+
34
+ validate.add_parser(sub)
35
+ parse.add_parser(sub)
36
+ inspect.add_parser(sub)
37
+ evolve.add_parser(sub)
38
+ convert.add_parser(sub)
39
+ export.add_parser(sub)
40
+ schema.add_parser(sub)
41
+ plugin.add_parser(sub)
42
+
43
+ return parser
44
+
45
+
46
+ def main(argv: Optional[Sequence[str]] = None) -> None:
47
+ parser = _create_parser()
48
+ args = parser.parse_args(argv)
49
+
50
+ from cks.plugin import load_external_constraints
51
+ load_external_constraints(strict=args.strict)
52
+
53
+ if args.command == "validate":
54
+ validate.handle(args)
55
+ elif args.command == "parse":
56
+ parse.handle(args)
57
+ elif args.command == "inspect":
58
+ inspect.handle(args)
59
+ elif args.command == "evolve":
60
+ evolve.handle(args)
61
+ elif args.command == "convert":
62
+ convert.handle(args)
63
+ elif args.command == "export":
64
+ export.handle(args)
65
+ elif args.command == "schema":
66
+ schema.handle(args)
67
+ elif args.command == "plugin":
68
+ plugin.handle(args)
69
+ else:
70
+ parser.print_help()
71
+ sys.exit(1)
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
File without changes
@@ -0,0 +1,54 @@
1
+ """
2
+ CLI command: convert.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ...serialization import serialize as cks_serialize
11
+
12
+
13
+ def add_parser(subparsers):
14
+ parser = subparsers.add_parser("convert", help="Convert JSON‑LD, Turtle, or RDF/XML to CKS")
15
+ parser.add_argument("input", type=Path, help="Path to input file")
16
+ parser.add_argument("--output", "-o", type=Path, default=None, help="Write CKS JSON to file")
17
+ parser.add_argument(
18
+ "--format", "-f",
19
+ choices=("json-ld", "rdf-xml", "turtle"),
20
+ default="json-ld",
21
+ help="Input format (default: json-ld)",
22
+ )
23
+ return parser
24
+
25
+
26
+ def handle(args):
27
+ try:
28
+ raw = args.input.read_text(encoding="utf-8")
29
+ data = json.loads(raw) if args.format == "json-ld" else raw
30
+ except FileNotFoundError:
31
+ print(f"File not found: {args.input}", file=sys.stderr)
32
+ sys.exit(1)
33
+ except json.JSONDecodeError as exc:
34
+ print(f"Invalid JSON in {args.input}: {exc}", file=sys.stderr)
35
+ sys.exit(1)
36
+
37
+ if args.format == "json-ld":
38
+ from ...adapters.jsonld_to_cks import JsonLdToCksConverter
39
+ converter = JsonLdToCksConverter(data)
40
+ structure = converter.convert()
41
+ elif args.format in ("rdf-xml", "turtle"):
42
+ from ...adapters.rdf_to_cks import RdfToCksConverter
43
+ rdf_format = "xml" if args.format == "rdf-xml" else args.format
44
+ converter = RdfToCksConverter(data, format=rdf_format)
45
+ structure = converter.convert()
46
+ else:
47
+ print(f"Unknown format: {args.format}", file=sys.stderr)
48
+ sys.exit(1)
49
+
50
+ output = cks_serialize(structure)
51
+ if args.output is None:
52
+ print(output)
53
+ else:
54
+ args.output.write_text(output, encoding="utf-8")
@@ -0,0 +1,97 @@
1
+ """
2
+ CLI command: evolve.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from ...serialization import parse as cks_parse, serialize as cks_serialize, SerializationError
11
+ from ...evolution import compose, AddObject, AddRelation, RemoveObject, RemoveRelation
12
+ from ...core import KnowledgeObject, CanonicalRelation, ObjectIdentity
13
+ from ...evolution import StructuralOperator
14
+
15
+
16
+ def add_parser(subparsers):
17
+ parser = subparsers.add_parser("evolve", help="Apply structural evolution")
18
+ parser.add_argument("input", type=Path, help="Path to canonical JSON file")
19
+ parser.add_argument("operations", type=Path, help="JSON file describing operations")
20
+ parser.add_argument("--output", "-o", type=Path, default=None, help="Write result to file")
21
+ return parser
22
+
23
+
24
+ def handle(args):
25
+ try:
26
+ raw = args.input.read_text(encoding="utf-8")
27
+ structure = cks_parse(raw)
28
+ except FileNotFoundError:
29
+ print(f"File not found: {args.input}", file=sys.stderr)
30
+ sys.exit(1)
31
+ except SerializationError as exc:
32
+ print(f"Serialization error: {exc}", file=sys.stderr)
33
+ sys.exit(1)
34
+
35
+ try:
36
+ ops_data = json.loads(args.operations.read_text(encoding="utf-8"))
37
+ operators = _parse_operations(ops_data)
38
+ except json.JSONDecodeError as exc:
39
+ print(f"Invalid JSON in operations file: {exc}", file=sys.stderr)
40
+ sys.exit(1)
41
+ except ValueError as exc:
42
+ print(f"Invalid operations: {exc}", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ new_structure = compose(structure, operators)
46
+ result = cks_serialize(new_structure)
47
+
48
+ if args.output is None:
49
+ print(result)
50
+ else:
51
+ args.output.write_text(result, encoding="utf-8")
52
+
53
+
54
+ def _parse_operations(ops_data: list[dict]) -> list:
55
+ operators: list[StructuralOperator] = []
56
+ for i, op in enumerate(ops_data):
57
+ op_type = op.get("type")
58
+ if op_type is None:
59
+ raise ValueError(f"Operation #{i}: missing 'type' field")
60
+ if op_type == "add_object":
61
+ identity_data = op.get("identity")
62
+ if identity_data is None:
63
+ raise ValueError(f"Operation #{i}: missing 'identity' field")
64
+ identity = ObjectIdentity(**identity_data)
65
+ obj = KnowledgeObject(identity=identity, structure=op.get("structure", {}))
66
+ operators.append(AddObject(obj))
67
+ elif op_type == "add_relation":
68
+ identity_data = op.get("identity")
69
+ if identity_data is None:
70
+ raise ValueError(f"Operation #{i}: missing 'identity' field")
71
+ identity = ObjectIdentity(**identity_data)
72
+ participants = op.get("participants")
73
+ if participants is None:
74
+ raise ValueError(f"Operation #{i}: missing 'participants' field")
75
+ relation_type = op.get("relation_type")
76
+ if relation_type is None:
77
+ raise ValueError(f"Operation #{i}: missing 'relation_type' field")
78
+ relation = CanonicalRelation(
79
+ identity=identity,
80
+ participants=participants,
81
+ relation_type=relation_type,
82
+ structure=op.get("structure", {}),
83
+ )
84
+ operators.append(AddRelation(relation))
85
+ elif op_type == "remove_object":
86
+ object_id = op.get("object_id")
87
+ if object_id is None:
88
+ raise ValueError(f"Operation #{i}: missing 'object_id' field")
89
+ operators.append(RemoveObject(object_id))
90
+ elif op_type == "remove_relation":
91
+ relation_id = op.get("relation_id")
92
+ if relation_id is None:
93
+ raise ValueError(f"Operation #{i}: missing 'relation_id' field")
94
+ operators.append(RemoveRelation(relation_id))
95
+ else:
96
+ raise ValueError(f"Operation #{i}: unknown operation type '{op_type}'")
97
+ return operators