kanonak-codec 0.1.0__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.
- kanonak_codec-0.1.0/PKG-INFO +20 -0
- kanonak_codec-0.1.0/README.md +7 -0
- kanonak_codec-0.1.0/kanonak_codec/__init__.py +156 -0
- kanonak_codec-0.1.0/kanonak_codec.egg-info/PKG-INFO +20 -0
- kanonak_codec-0.1.0/kanonak_codec.egg-info/SOURCES.txt +8 -0
- kanonak_codec-0.1.0/kanonak_codec.egg-info/dependency_links.txt +1 -0
- kanonak_codec-0.1.0/kanonak_codec.egg-info/requires.txt +1 -0
- kanonak_codec-0.1.0/kanonak_codec.egg-info/top_level.txt +1 -0
- kanonak_codec-0.1.0/pyproject.toml +21 -0
- kanonak_codec-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kanonak-codec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Kanonak codec runtime (Python port): build the canonical input model from typed nodes + an embedded schema, content-address via kanonak-canonical, and (de)serialize the normalized-JSON wire form. Referenced by generated typed SDKs.
|
|
5
|
+
Author: Kanonak Maintainers
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://kanonak.org
|
|
8
|
+
Project-URL: Repository, https://github.com/kanonak-protocol/runtime
|
|
9
|
+
Project-URL: Issues, https://github.com/kanonak-protocol/runtime/issues
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: kanonak-canonical>=0.1.0
|
|
13
|
+
|
|
14
|
+
# kanonak-codec
|
|
15
|
+
|
|
16
|
+
The generic Kanonak codec runtime (Python): builds the canonical input model from typed nodes +
|
|
17
|
+
an embedded schema, content-addresses it via `kanonak-canonical`, and (de)serializes the
|
|
18
|
+
normalized-JSON wire form. Referenced by generated typed SDKs.
|
|
19
|
+
|
|
20
|
+
Source & issues: https://github.com/kanonak-protocol/runtime
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# kanonak-codec
|
|
2
|
+
|
|
3
|
+
The generic Kanonak codec runtime (Python): builds the canonical input model from typed nodes +
|
|
4
|
+
an embedded schema, content-addresses it via `kanonak-canonical`, and (de)serializes the
|
|
5
|
+
normalized-JSON wire form. Referenced by generated typed SDKs.
|
|
6
|
+
|
|
7
|
+
Source & issues: https://github.com/kanonak-protocol/runtime
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""kanonak-codec — the generic, ontology-independent codec runtime (Python port).
|
|
2
|
+
|
|
3
|
+
Given a ``CodecSchema`` (the per-package metadata a generated SDK embeds) and a
|
|
4
|
+
set of typed nodes, it builds the canonical input model and content-addresses it
|
|
5
|
+
via ``kanonak-canonical`` (the same content-form the TypeScript reference and the
|
|
6
|
+
``kanonak hash`` CLI produce). It also (de)serializes the normalized-JSON wire
|
|
7
|
+
form. Self-contained: carriers come from the schema's datatype URIs, and the
|
|
8
|
+
resolved foundation URIs are embedded by the generator, so hashing needs no
|
|
9
|
+
runtime ontology resolution.
|
|
10
|
+
|
|
11
|
+
A node is a plain ``dict`` (the ``$``-envelope plus alias-collapsed local-name
|
|
12
|
+
fields); a generated Pydantic model serializes to one via ``model_dump``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Dict, List
|
|
18
|
+
|
|
19
|
+
from kanonak_canonical import (
|
|
20
|
+
KList,
|
|
21
|
+
Package,
|
|
22
|
+
RawScalar,
|
|
23
|
+
Reference,
|
|
24
|
+
Statement,
|
|
25
|
+
Subject,
|
|
26
|
+
TypedScalar,
|
|
27
|
+
canonical_form as _canonical_form,
|
|
28
|
+
canonical_hash as _canonical_hash,
|
|
29
|
+
carrier_of,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
ENVELOPE_KEYS = {"$type", "$id", "$contentHash", "$version", "$extra"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _lexical(value: Any) -> str:
|
|
36
|
+
"""The raw lexical token of a scalar — the input the canonical form normalizes."""
|
|
37
|
+
if isinstance(value, bool):
|
|
38
|
+
return "true" if value else "false"
|
|
39
|
+
if isinstance(value, str):
|
|
40
|
+
return value
|
|
41
|
+
return str(value)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _value(prop: Dict[str, Any], raw: Any):
|
|
45
|
+
if prop["kind"] == "object":
|
|
46
|
+
if isinstance(raw, dict) and "$ref" in raw:
|
|
47
|
+
return Reference(raw["$ref"])
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Embedded object values are not yet supported by the codec runtime; "
|
|
50
|
+
"pass a reference ({'$ref': ...})."
|
|
51
|
+
)
|
|
52
|
+
carrier = carrier_of(prop["datatype"])
|
|
53
|
+
if carrier is None:
|
|
54
|
+
return RawScalar(_lexical(raw))
|
|
55
|
+
return TypedScalar(carrier, _lexical(raw))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _statements(node: Dict[str, Any], schema: Dict[str, Any]) -> List[Statement]:
|
|
59
|
+
type_uri = node.get("$type")
|
|
60
|
+
if not type_uri:
|
|
61
|
+
raise ValueError("node is missing $type")
|
|
62
|
+
cls = schema["classes"].get(type_uri)
|
|
63
|
+
if cls is None:
|
|
64
|
+
raise ValueError(f"no schema for type {type_uri}")
|
|
65
|
+
|
|
66
|
+
statements: List[Statement] = [Statement(schema["typePredicate"], Reference(type_uri))]
|
|
67
|
+
for key, raw in node.items():
|
|
68
|
+
if key in ENVELOPE_KEYS or raw is None:
|
|
69
|
+
continue
|
|
70
|
+
prop = cls["props"].get(key)
|
|
71
|
+
if prop is None:
|
|
72
|
+
statements.append(Statement(key, RawScalar(_lexical(raw))))
|
|
73
|
+
continue
|
|
74
|
+
if isinstance(raw, list):
|
|
75
|
+
statements.append(Statement(prop["predicate"], KList([_value(prop, x) for x in raw])))
|
|
76
|
+
else:
|
|
77
|
+
statements.append(Statement(prop["predicate"], _value(prop, raw)))
|
|
78
|
+
|
|
79
|
+
extra = node.get("$extra")
|
|
80
|
+
if extra:
|
|
81
|
+
for predicate, raw in extra.items():
|
|
82
|
+
if raw is None:
|
|
83
|
+
continue
|
|
84
|
+
statements.append(Statement(predicate, RawScalar(_lexical(raw))))
|
|
85
|
+
return statements
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_package(nodes: List[Dict[str, Any]], schema: Dict[str, Any], pkg: Dict[str, Any]) -> Package:
|
|
89
|
+
"""Build the canonical input model: a subject per node + the synthesized
|
|
90
|
+
package-wrapper subject (raw label + ``Package`` type), exactly the subject
|
|
91
|
+
set ``kanonak hash`` produces for the equivalent authored package."""
|
|
92
|
+
subjects: List[Subject] = []
|
|
93
|
+
for node in nodes:
|
|
94
|
+
if not node.get("$id"):
|
|
95
|
+
raise ValueError("node is missing $id")
|
|
96
|
+
subjects.append(Subject(node["$id"], _statements(node, schema)))
|
|
97
|
+
|
|
98
|
+
pkg_uri = f"{pkg['publisher']}/{pkg['packageName']}@{pkg['version']}/{pkg['packageName']}"
|
|
99
|
+
pkg_statements: List[Statement] = []
|
|
100
|
+
if pkg.get("label") is not None:
|
|
101
|
+
pkg_statements.append(Statement(schema["labelPredicate"], RawScalar(pkg["label"])))
|
|
102
|
+
pkg_statements.append(Statement(schema["typePredicate"], Reference(schema["packageTypeUri"])))
|
|
103
|
+
subjects.append(Subject(pkg_uri, pkg_statements))
|
|
104
|
+
return Package(subjects)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def canonical_form(nodes: List[Dict[str, Any]], schema: Dict[str, Any], pkg: Dict[str, Any]) -> str:
|
|
108
|
+
"""The canonical form (the ``{subjects:[...]}`` JSON) of a package from nodes."""
|
|
109
|
+
return _canonical_form(build_package(nodes, schema, pkg))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def content_hash(nodes: List[Dict[str, Any]], schema: Dict[str, Any], pkg: Dict[str, Any]) -> str:
|
|
113
|
+
"""The ``sha256:`` content hash of a package from nodes — matches ``kanonak hash``."""
|
|
114
|
+
return _canonical_hash(build_package(nodes, schema, pkg))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def serialize(node: Dict[str, Any]) -> Dict[str, Any]:
|
|
118
|
+
"""Serialize a typed node to its normalized-JSON wire form. ``$extra`` entries
|
|
119
|
+
ride as sibling fields after the modeled ones; a modeled field wins a name
|
|
120
|
+
collision (``[JsonExtensionData]`` semantics)."""
|
|
121
|
+
out: Dict[str, Any] = {}
|
|
122
|
+
for key, value in node.items():
|
|
123
|
+
if key == "$extra" or value is None:
|
|
124
|
+
continue
|
|
125
|
+
out[key] = value
|
|
126
|
+
extra = node.get("$extra")
|
|
127
|
+
if extra:
|
|
128
|
+
for key, value in extra.items():
|
|
129
|
+
if value is not None and key not in out:
|
|
130
|
+
out[key] = value
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def deserialize(json_obj: Dict[str, Any], schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
135
|
+
"""Parse normalized JSON into a typed node. ``$``-envelope keys and fields
|
|
136
|
+
modeled on the node's ``$type`` stay top-level; every other key is collected
|
|
137
|
+
into ``$extra`` so a strongly-typed consumer round-trips it losslessly."""
|
|
138
|
+
type_uri = json_obj.get("$type")
|
|
139
|
+
if not isinstance(type_uri, str):
|
|
140
|
+
raise ValueError("Cannot deserialize: missing string $type")
|
|
141
|
+
cls = schema["classes"].get(type_uri)
|
|
142
|
+
if cls is None:
|
|
143
|
+
raise ValueError(f"Cannot deserialize: no schema for type {type_uri}")
|
|
144
|
+
|
|
145
|
+
node: Dict[str, Any] = {"$type": type_uri}
|
|
146
|
+
extra: Dict[str, Any] = {}
|
|
147
|
+
for key, value in json_obj.items():
|
|
148
|
+
if key == "$type":
|
|
149
|
+
continue
|
|
150
|
+
if key.startswith("$") or key in cls["props"]:
|
|
151
|
+
node[key] = value
|
|
152
|
+
else:
|
|
153
|
+
extra[key] = value
|
|
154
|
+
if extra:
|
|
155
|
+
node["$extra"] = extra
|
|
156
|
+
return node
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kanonak-codec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Kanonak codec runtime (Python port): build the canonical input model from typed nodes + an embedded schema, content-address via kanonak-canonical, and (de)serialize the normalized-JSON wire form. Referenced by generated typed SDKs.
|
|
5
|
+
Author: Kanonak Maintainers
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://kanonak.org
|
|
8
|
+
Project-URL: Repository, https://github.com/kanonak-protocol/runtime
|
|
9
|
+
Project-URL: Issues, https://github.com/kanonak-protocol/runtime/issues
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: kanonak-canonical>=0.1.0
|
|
13
|
+
|
|
14
|
+
# kanonak-codec
|
|
15
|
+
|
|
16
|
+
The generic Kanonak codec runtime (Python): builds the canonical input model from typed nodes +
|
|
17
|
+
an embedded schema, content-addresses it via `kanonak-canonical`, and (de)serializes the
|
|
18
|
+
normalized-JSON wire form. Referenced by generated typed SDKs.
|
|
19
|
+
|
|
20
|
+
Source & issues: https://github.com/kanonak-protocol/runtime
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kanonak-canonical>=0.1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kanonak_codec
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kanonak-codec"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
description = "Kanonak codec runtime (Python port): build the canonical input model from typed nodes + an embedded schema, content-address via kanonak-canonical, and (de)serialize the normalized-JSON wire form. Referenced by generated typed SDKs."
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "Kanonak Maintainers" }]
|
|
13
|
+
dependencies = ["kanonak-canonical>=0.1.0"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://kanonak.org"
|
|
17
|
+
Repository = "https://github.com/kanonak-protocol/runtime"
|
|
18
|
+
Issues = "https://github.com/kanonak-protocol/runtime/issues"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
packages = ["kanonak_codec"]
|