nodus-schema 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.
- nodus_schema-0.1.0/LICENSE +21 -0
- nodus_schema-0.1.0/PKG-INFO +156 -0
- nodus_schema-0.1.0/README.md +141 -0
- nodus_schema-0.1.0/nodus_schema/__init__.py +37 -0
- nodus_schema-0.1.0/nodus_schema/registry.py +84 -0
- nodus_schema-0.1.0/nodus_schema/schema.py +85 -0
- nodus_schema-0.1.0/nodus_schema/versioning.py +58 -0
- nodus_schema-0.1.0/nodus_schema.egg-info/PKG-INFO +156 -0
- nodus_schema-0.1.0/nodus_schema.egg-info/SOURCES.txt +15 -0
- nodus_schema-0.1.0/nodus_schema.egg-info/dependency_links.txt +1 -0
- nodus_schema-0.1.0/nodus_schema.egg-info/requires.txt +3 -0
- nodus_schema-0.1.0/nodus_schema.egg-info/top_level.txt +1 -0
- nodus_schema-0.1.0/pyproject.toml +27 -0
- nodus_schema-0.1.0/setup.cfg +4 -0
- nodus_schema-0.1.0/tests/test_registry.py +56 -0
- nodus_schema-0.1.0/tests/test_schema.py +68 -0
- nodus_schema-0.1.0/tests/test_versioning.py +58 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shawn Knight
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nodus-schema
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Versioned ABI schema validation, parsing, and registry
|
|
5
|
+
Author: Shawn Knight
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Masterplanner25/nodus-schema
|
|
8
|
+
Project-URL: Repository, https://github.com/Masterplanner25/nodus-schema
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# nodus-schema
|
|
17
|
+
|
|
18
|
+
**Versioned ABI schema validation, parsing, and registry for Nodus AI systems.**
|
|
19
|
+
|
|
20
|
+
Validates payloads against simple-form or JSON Schema object definitions,
|
|
21
|
+
parses versioned surface names (`sys.v1.domain.action`), and maintains a
|
|
22
|
+
thread-safe `SchemaRegistry` of versioned schemas with stability tracking.
|
|
23
|
+
No required external dependencies — pure stdlib.
|
|
24
|
+
|
|
25
|
+
> **Note on naming:** This is the standalone `nodus-schema` package
|
|
26
|
+
> (`C:\dev\nodus-schema`). The nodus-lang runtime also ships an in-tree
|
|
27
|
+
> `nodus_schema` package (`src/nodus_schema/`) with `SyscallSpec`,
|
|
28
|
+
> `HandlerContract`, and `VALID_EFFECTS`. The two are distinct; check
|
|
29
|
+
> `python -c "import nodus_schema; print(nodus_schema.__file__)"` to confirm
|
|
30
|
+
> which is active.
|
|
31
|
+
|
|
32
|
+
> **Status:** v0.1.0 — prepared, not yet published.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nodus-schema
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## What it provides
|
|
45
|
+
|
|
46
|
+
| Component | Purpose |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `validate_payload` | Validate a dict against a schema; returns list of error strings |
|
|
49
|
+
| `validate_input` / `validate_output` | Aliases for `validate_payload` |
|
|
50
|
+
| `parse_versioned_name` | Parse `"sys.v1.domain.action"` → `(version, action)` |
|
|
51
|
+
| `resolve_version` | Find best compatible version from available set |
|
|
52
|
+
| `is_stable_version` | True for `"v1"`, `"v2"`, etc. (not alpha/beta) |
|
|
53
|
+
| `SchemaEntry` | One versioned schema with stability and deprecation state |
|
|
54
|
+
| `SchemaRegistry` | Thread-safe registry of `SchemaEntry` objects |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Schema validation
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from nodus_schema import validate_payload
|
|
62
|
+
|
|
63
|
+
schema = {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"name": {"type": "string"},
|
|
67
|
+
"age": {"type": "integer"},
|
|
68
|
+
},
|
|
69
|
+
"required": ["name"],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
errors = validate_payload(schema, {"name": "Alice", "age": 30})
|
|
73
|
+
# [] — valid
|
|
74
|
+
|
|
75
|
+
errors = validate_payload(schema, {"age": 30})
|
|
76
|
+
# ["Missing required field: 'name'"]
|
|
77
|
+
|
|
78
|
+
errors = validate_payload(schema, {"name": "Alice", "age": "thirty"})
|
|
79
|
+
# ["Field 'age': expected type 'integer', got 'str'"]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Simple-form schema
|
|
83
|
+
|
|
84
|
+
A flat `{field: type_string}` dict is also accepted:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
errors = validate_payload({"key": "str", "value": "any"}, {"key": "hello"})
|
|
88
|
+
# [] — "any" allows any type
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Supported type strings: `string`/`str`, `integer`/`int`, `number`/`float`,
|
|
92
|
+
`boolean`/`bool`, `object`/`map`/`dict`, `array`/`list`, `null`/`nil`, `any`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Versioned name parsing
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from nodus_schema import parse_versioned_name, resolve_version, is_stable_version
|
|
100
|
+
|
|
101
|
+
version, action = parse_versioned_name("sys.v1.memory.write")
|
|
102
|
+
# ("v1", "memory.write")
|
|
103
|
+
|
|
104
|
+
best = resolve_version("v1", available={"v1", "v2"}, fallback=True)
|
|
105
|
+
# "v1"
|
|
106
|
+
|
|
107
|
+
is_stable_version("v1") # True
|
|
108
|
+
is_stable_version("v1alpha1") # False
|
|
109
|
+
is_stable_version("v1beta2") # False
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## SchemaRegistry
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from nodus_schema import SchemaRegistry, SchemaEntry
|
|
118
|
+
|
|
119
|
+
registry = SchemaRegistry()
|
|
120
|
+
registry.register(SchemaEntry(
|
|
121
|
+
name="memory.write",
|
|
122
|
+
version="v1",
|
|
123
|
+
input_schema={"key": "str", "value": "any"},
|
|
124
|
+
output_schema={"stored": "bool"},
|
|
125
|
+
stable=True,
|
|
126
|
+
deprecated=False,
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
entry = registry.get("memory.write", version="v1") # SchemaEntry | None
|
|
130
|
+
versions = registry.versions("memory.write") # ["v1"]
|
|
131
|
+
all_entries = registry.list() # list[SchemaEntry]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Design
|
|
137
|
+
|
|
138
|
+
- **No required dependencies.** Pure stdlib (`json`, `threading`, `dataclasses`).
|
|
139
|
+
- **Simple-form schemas.** Flat `{field: type}` dicts are normalized to JSON
|
|
140
|
+
Schema objects automatically.
|
|
141
|
+
- **Thread-safe.** `SchemaRegistry` uses `threading.Lock`.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install -e ".[dev]"
|
|
149
|
+
pytest tests/ -q
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# nodus-schema
|
|
2
|
+
|
|
3
|
+
**Versioned ABI schema validation, parsing, and registry for Nodus AI systems.**
|
|
4
|
+
|
|
5
|
+
Validates payloads against simple-form or JSON Schema object definitions,
|
|
6
|
+
parses versioned surface names (`sys.v1.domain.action`), and maintains a
|
|
7
|
+
thread-safe `SchemaRegistry` of versioned schemas with stability tracking.
|
|
8
|
+
No required external dependencies — pure stdlib.
|
|
9
|
+
|
|
10
|
+
> **Note on naming:** This is the standalone `nodus-schema` package
|
|
11
|
+
> (`C:\dev\nodus-schema`). The nodus-lang runtime also ships an in-tree
|
|
12
|
+
> `nodus_schema` package (`src/nodus_schema/`) with `SyscallSpec`,
|
|
13
|
+
> `HandlerContract`, and `VALID_EFFECTS`. The two are distinct; check
|
|
14
|
+
> `python -c "import nodus_schema; print(nodus_schema.__file__)"` to confirm
|
|
15
|
+
> which is active.
|
|
16
|
+
|
|
17
|
+
> **Status:** v0.1.0 — prepared, not yet published.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install nodus-schema
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What it provides
|
|
30
|
+
|
|
31
|
+
| Component | Purpose |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `validate_payload` | Validate a dict against a schema; returns list of error strings |
|
|
34
|
+
| `validate_input` / `validate_output` | Aliases for `validate_payload` |
|
|
35
|
+
| `parse_versioned_name` | Parse `"sys.v1.domain.action"` → `(version, action)` |
|
|
36
|
+
| `resolve_version` | Find best compatible version from available set |
|
|
37
|
+
| `is_stable_version` | True for `"v1"`, `"v2"`, etc. (not alpha/beta) |
|
|
38
|
+
| `SchemaEntry` | One versioned schema with stability and deprecation state |
|
|
39
|
+
| `SchemaRegistry` | Thread-safe registry of `SchemaEntry` objects |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Schema validation
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from nodus_schema import validate_payload
|
|
47
|
+
|
|
48
|
+
schema = {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"name": {"type": "string"},
|
|
52
|
+
"age": {"type": "integer"},
|
|
53
|
+
},
|
|
54
|
+
"required": ["name"],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
errors = validate_payload(schema, {"name": "Alice", "age": 30})
|
|
58
|
+
# [] — valid
|
|
59
|
+
|
|
60
|
+
errors = validate_payload(schema, {"age": 30})
|
|
61
|
+
# ["Missing required field: 'name'"]
|
|
62
|
+
|
|
63
|
+
errors = validate_payload(schema, {"name": "Alice", "age": "thirty"})
|
|
64
|
+
# ["Field 'age': expected type 'integer', got 'str'"]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Simple-form schema
|
|
68
|
+
|
|
69
|
+
A flat `{field: type_string}` dict is also accepted:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
errors = validate_payload({"key": "str", "value": "any"}, {"key": "hello"})
|
|
73
|
+
# [] — "any" allows any type
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Supported type strings: `string`/`str`, `integer`/`int`, `number`/`float`,
|
|
77
|
+
`boolean`/`bool`, `object`/`map`/`dict`, `array`/`list`, `null`/`nil`, `any`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Versioned name parsing
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from nodus_schema import parse_versioned_name, resolve_version, is_stable_version
|
|
85
|
+
|
|
86
|
+
version, action = parse_versioned_name("sys.v1.memory.write")
|
|
87
|
+
# ("v1", "memory.write")
|
|
88
|
+
|
|
89
|
+
best = resolve_version("v1", available={"v1", "v2"}, fallback=True)
|
|
90
|
+
# "v1"
|
|
91
|
+
|
|
92
|
+
is_stable_version("v1") # True
|
|
93
|
+
is_stable_version("v1alpha1") # False
|
|
94
|
+
is_stable_version("v1beta2") # False
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## SchemaRegistry
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from nodus_schema import SchemaRegistry, SchemaEntry
|
|
103
|
+
|
|
104
|
+
registry = SchemaRegistry()
|
|
105
|
+
registry.register(SchemaEntry(
|
|
106
|
+
name="memory.write",
|
|
107
|
+
version="v1",
|
|
108
|
+
input_schema={"key": "str", "value": "any"},
|
|
109
|
+
output_schema={"stored": "bool"},
|
|
110
|
+
stable=True,
|
|
111
|
+
deprecated=False,
|
|
112
|
+
))
|
|
113
|
+
|
|
114
|
+
entry = registry.get("memory.write", version="v1") # SchemaEntry | None
|
|
115
|
+
versions = registry.versions("memory.write") # ["v1"]
|
|
116
|
+
all_entries = registry.list() # list[SchemaEntry]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Design
|
|
122
|
+
|
|
123
|
+
- **No required dependencies.** Pure stdlib (`json`, `threading`, `dataclasses`).
|
|
124
|
+
- **Simple-form schemas.** Flat `{field: type}` dicts are normalized to JSON
|
|
125
|
+
Schema objects automatically.
|
|
126
|
+
- **Thread-safe.** `SchemaRegistry` uses `threading.Lock`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pip install -e ".[dev]"
|
|
134
|
+
pytest tests/ -q
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""nodus-schema — versioned ABI schema validation and registry.
|
|
2
|
+
|
|
3
|
+
Validation:
|
|
4
|
+
validate_payload(schema, payload) → list[str] — error strings, empty = valid
|
|
5
|
+
validate_input(schema, payload) → list[str] — alias
|
|
6
|
+
validate_output(schema, data) → list[str] — alias
|
|
7
|
+
|
|
8
|
+
Versioning:
|
|
9
|
+
parse_versioned_name(name) → (version, action)
|
|
10
|
+
resolve_version(requested, available, fallback) → str | None
|
|
11
|
+
is_stable_version(version) → bool
|
|
12
|
+
SYSCALL_VERSION_FALLBACK — default fallback ("v1")
|
|
13
|
+
|
|
14
|
+
Registry:
|
|
15
|
+
SchemaEntry — one versioned schema (name, version, schemas, stable/deprecated)
|
|
16
|
+
SchemaRegistry — thread-safe; register, get, versions(), list()
|
|
17
|
+
"""
|
|
18
|
+
from .registry import SchemaEntry, SchemaRegistry
|
|
19
|
+
from .schema import validate_input, validate_output, validate_payload
|
|
20
|
+
from .versioning import (
|
|
21
|
+
SYSCALL_VERSION_FALLBACK,
|
|
22
|
+
is_stable_version,
|
|
23
|
+
parse_versioned_name,
|
|
24
|
+
resolve_version,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"validate_payload",
|
|
29
|
+
"validate_input",
|
|
30
|
+
"validate_output",
|
|
31
|
+
"parse_versioned_name",
|
|
32
|
+
"resolve_version",
|
|
33
|
+
"is_stable_version",
|
|
34
|
+
"SYSCALL_VERSION_FALLBACK",
|
|
35
|
+
"SchemaEntry",
|
|
36
|
+
"SchemaRegistry",
|
|
37
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""SchemaRegistry — store and retrieve versioned ABI schema entries."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SchemaEntry:
|
|
11
|
+
"""One versioned ABI schema registration.
|
|
12
|
+
|
|
13
|
+
Attributes
|
|
14
|
+
----------
|
|
15
|
+
name: Action name (e.g. ``"memory.read"``).
|
|
16
|
+
version: ABI version string (e.g. ``"v1"``).
|
|
17
|
+
input_schema: Lightweight schema for input validation.
|
|
18
|
+
output_schema: Lightweight schema for output validation.
|
|
19
|
+
stable: False = experimental; deprecation possible.
|
|
20
|
+
deprecated: When True, callers should migrate to *replacement*.
|
|
21
|
+
deprecated_since: Version string when deprecation was introduced.
|
|
22
|
+
replacement: Full versioned name to migrate to.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
version: str
|
|
27
|
+
input_schema: Optional[dict] = None
|
|
28
|
+
output_schema: Optional[dict] = None
|
|
29
|
+
stable: bool = True
|
|
30
|
+
deprecated: bool = False
|
|
31
|
+
deprecated_since: Optional[str] = None
|
|
32
|
+
replacement: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def full_name(self) -> str:
|
|
36
|
+
"""``"v1.memory.read"``"""
|
|
37
|
+
return f"{self.version}.{self.name}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SchemaRegistry:
|
|
41
|
+
"""Thread-safe registry of ``SchemaEntry`` objects.
|
|
42
|
+
|
|
43
|
+
Usage::
|
|
44
|
+
|
|
45
|
+
reg = SchemaRegistry()
|
|
46
|
+
reg.register(SchemaEntry("memory.read", "v1",
|
|
47
|
+
input_schema={...}, output_schema={...}))
|
|
48
|
+
entry = reg.get("v1.memory.read")
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self._entries: dict[str, SchemaEntry] = {}
|
|
53
|
+
self._lock = threading.Lock()
|
|
54
|
+
|
|
55
|
+
def register(self, entry: SchemaEntry) -> None:
|
|
56
|
+
"""Register *entry*, keyed by ``entry.full_name``."""
|
|
57
|
+
with self._lock:
|
|
58
|
+
self._entries[entry.full_name] = entry
|
|
59
|
+
|
|
60
|
+
def get(self, full_name: str) -> Optional[SchemaEntry]:
|
|
61
|
+
"""Return the entry for *full_name* (e.g. ``"v1.memory.read"``)."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
return self._entries.get(full_name)
|
|
64
|
+
|
|
65
|
+
def versions(self) -> frozenset[str]:
|
|
66
|
+
"""Return the set of all registered version strings."""
|
|
67
|
+
with self._lock:
|
|
68
|
+
return frozenset(e.version for e in self._entries.values())
|
|
69
|
+
|
|
70
|
+
def list(self, *, include_deprecated: bool = False) -> list[SchemaEntry]:
|
|
71
|
+
"""Return all registered entries."""
|
|
72
|
+
with self._lock:
|
|
73
|
+
entries = list(self._entries.values())
|
|
74
|
+
if not include_deprecated:
|
|
75
|
+
entries = [e for e in entries if not e.deprecated]
|
|
76
|
+
return entries
|
|
77
|
+
|
|
78
|
+
def __len__(self) -> int:
|
|
79
|
+
with self._lock:
|
|
80
|
+
return len(self._entries)
|
|
81
|
+
|
|
82
|
+
def __contains__(self, full_name: str) -> bool:
|
|
83
|
+
with self._lock:
|
|
84
|
+
return full_name in self._entries
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Lightweight schema validation — extracted from AINDY/kernel/syscall_versioning.py."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
# Normalise type aliases to canonical names
|
|
7
|
+
_TYPE_MAP: dict[str, str] = {
|
|
8
|
+
"str": "string", "string": "string",
|
|
9
|
+
"int": "integer", "integer": "integer",
|
|
10
|
+
"float": "number", "number": "number",
|
|
11
|
+
"bool": "boolean", "boolean": "boolean",
|
|
12
|
+
"list": "list", "array": "list",
|
|
13
|
+
"dict": "dict", "object": "dict",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_payload(schema: dict[str, Any], payload: dict[str, Any]) -> list[str]:
|
|
18
|
+
"""Validate *payload* against a lightweight schema.
|
|
19
|
+
|
|
20
|
+
Schema format::
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
"required": ["field1"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"field1": {"type": "string"},
|
|
26
|
+
"field2": {"type": "int"},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of error strings. Empty list = valid.
|
|
32
|
+
"""
|
|
33
|
+
if not schema:
|
|
34
|
+
return []
|
|
35
|
+
if not isinstance(payload, dict):
|
|
36
|
+
return [f"Payload must be a dict, got {type(payload).__name__}"]
|
|
37
|
+
|
|
38
|
+
errors: list[str] = []
|
|
39
|
+
required = schema.get("required") or []
|
|
40
|
+
properties = schema.get("properties") or {}
|
|
41
|
+
|
|
42
|
+
for field in required:
|
|
43
|
+
if field not in payload:
|
|
44
|
+
errors.append(f"Missing required field: {field!r}")
|
|
45
|
+
|
|
46
|
+
for name, spec in properties.items():
|
|
47
|
+
if name not in payload:
|
|
48
|
+
continue
|
|
49
|
+
expected_type = _TYPE_MAP.get(str(spec.get("type", "")).lower(), "")
|
|
50
|
+
if not expected_type:
|
|
51
|
+
continue
|
|
52
|
+
value = payload[name]
|
|
53
|
+
if not _check_type(value, expected_type):
|
|
54
|
+
actual = type(value).__name__
|
|
55
|
+
errors.append(
|
|
56
|
+
f"Field {name!r}: expected {expected_type}, got {actual}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return errors
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_input(schema: dict[str, Any], payload: dict[str, Any]) -> list[str]:
|
|
63
|
+
"""Alias for :func:`validate_payload` (input validation)."""
|
|
64
|
+
return validate_payload(schema, payload)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_output(schema: dict[str, Any], data: dict[str, Any]) -> list[str]:
|
|
68
|
+
"""Alias for :func:`validate_payload` (output validation)."""
|
|
69
|
+
return validate_payload(schema, data)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_type(value: Any, expected: str) -> bool:
|
|
73
|
+
if expected == "string":
|
|
74
|
+
return isinstance(value, str)
|
|
75
|
+
if expected == "integer":
|
|
76
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
77
|
+
if expected == "number":
|
|
78
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
79
|
+
if expected == "boolean":
|
|
80
|
+
return isinstance(value, bool)
|
|
81
|
+
if expected == "list":
|
|
82
|
+
return isinstance(value, list)
|
|
83
|
+
if expected == "dict":
|
|
84
|
+
return isinstance(value, dict)
|
|
85
|
+
return True # unknown type — pass through
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Versioned name parsing and resolution — from AINDY/kernel/syscall_versioning.py."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
_VERSIONED_NAME_RE = re.compile(r"^(v\d+)\.(.+)$")
|
|
8
|
+
|
|
9
|
+
# When the requested version is not available, fall back to this version.
|
|
10
|
+
SYSCALL_VERSION_FALLBACK: Optional[str] = "v1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_versioned_name(name: str) -> tuple[str, str]:
|
|
14
|
+
"""Parse a versioned name into ``(version, action)``.
|
|
15
|
+
|
|
16
|
+
Examples::
|
|
17
|
+
|
|
18
|
+
parse_versioned_name("v1.memory.read") → ("v1", "memory.read")
|
|
19
|
+
parse_versioned_name("v2.flow.run") → ("v2", "flow.run")
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If *name* does not match ``vN.action`` format.
|
|
23
|
+
"""
|
|
24
|
+
match = _VERSIONED_NAME_RE.match(name)
|
|
25
|
+
if not match:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Versioned name must match 'vN.action', got {name!r}"
|
|
28
|
+
)
|
|
29
|
+
return match.group(1), match.group(2)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_version(
|
|
33
|
+
requested: str,
|
|
34
|
+
available: frozenset[str],
|
|
35
|
+
fallback: Optional[str] = SYSCALL_VERSION_FALLBACK,
|
|
36
|
+
) -> Optional[str]:
|
|
37
|
+
"""Return the version to use, applying the fallback if needed.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
requested: The version string requested (e.g. ``"v1"``).
|
|
41
|
+
available: Set of registered versions (e.g. ``frozenset({"v1", "v2"})``).
|
|
42
|
+
fallback: Version to try when *requested* is not in *available*.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The resolved version string, or None if neither *requested* nor
|
|
46
|
+
*fallback* is available.
|
|
47
|
+
"""
|
|
48
|
+
if requested in available:
|
|
49
|
+
return requested
|
|
50
|
+
if fallback is not None and fallback in available:
|
|
51
|
+
return fallback
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_stable_version(version: str) -> bool:
|
|
56
|
+
"""Return True for ``"v1"`` and above with no pre-release suffix."""
|
|
57
|
+
match = re.match(r"^v(\d+)$", version)
|
|
58
|
+
return bool(match) and int(match.group(1)) >= 1
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nodus-schema
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Versioned ABI schema validation, parsing, and registry
|
|
5
|
+
Author: Shawn Knight
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Masterplanner25/nodus-schema
|
|
8
|
+
Project-URL: Repository, https://github.com/Masterplanner25/nodus-schema
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# nodus-schema
|
|
17
|
+
|
|
18
|
+
**Versioned ABI schema validation, parsing, and registry for Nodus AI systems.**
|
|
19
|
+
|
|
20
|
+
Validates payloads against simple-form or JSON Schema object definitions,
|
|
21
|
+
parses versioned surface names (`sys.v1.domain.action`), and maintains a
|
|
22
|
+
thread-safe `SchemaRegistry` of versioned schemas with stability tracking.
|
|
23
|
+
No required external dependencies — pure stdlib.
|
|
24
|
+
|
|
25
|
+
> **Note on naming:** This is the standalone `nodus-schema` package
|
|
26
|
+
> (`C:\dev\nodus-schema`). The nodus-lang runtime also ships an in-tree
|
|
27
|
+
> `nodus_schema` package (`src/nodus_schema/`) with `SyscallSpec`,
|
|
28
|
+
> `HandlerContract`, and `VALID_EFFECTS`. The two are distinct; check
|
|
29
|
+
> `python -c "import nodus_schema; print(nodus_schema.__file__)"` to confirm
|
|
30
|
+
> which is active.
|
|
31
|
+
|
|
32
|
+
> **Status:** v0.1.0 — prepared, not yet published.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nodus-schema
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## What it provides
|
|
45
|
+
|
|
46
|
+
| Component | Purpose |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `validate_payload` | Validate a dict against a schema; returns list of error strings |
|
|
49
|
+
| `validate_input` / `validate_output` | Aliases for `validate_payload` |
|
|
50
|
+
| `parse_versioned_name` | Parse `"sys.v1.domain.action"` → `(version, action)` |
|
|
51
|
+
| `resolve_version` | Find best compatible version from available set |
|
|
52
|
+
| `is_stable_version` | True for `"v1"`, `"v2"`, etc. (not alpha/beta) |
|
|
53
|
+
| `SchemaEntry` | One versioned schema with stability and deprecation state |
|
|
54
|
+
| `SchemaRegistry` | Thread-safe registry of `SchemaEntry` objects |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Schema validation
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from nodus_schema import validate_payload
|
|
62
|
+
|
|
63
|
+
schema = {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"name": {"type": "string"},
|
|
67
|
+
"age": {"type": "integer"},
|
|
68
|
+
},
|
|
69
|
+
"required": ["name"],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
errors = validate_payload(schema, {"name": "Alice", "age": 30})
|
|
73
|
+
# [] — valid
|
|
74
|
+
|
|
75
|
+
errors = validate_payload(schema, {"age": 30})
|
|
76
|
+
# ["Missing required field: 'name'"]
|
|
77
|
+
|
|
78
|
+
errors = validate_payload(schema, {"name": "Alice", "age": "thirty"})
|
|
79
|
+
# ["Field 'age': expected type 'integer', got 'str'"]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Simple-form schema
|
|
83
|
+
|
|
84
|
+
A flat `{field: type_string}` dict is also accepted:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
errors = validate_payload({"key": "str", "value": "any"}, {"key": "hello"})
|
|
88
|
+
# [] — "any" allows any type
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Supported type strings: `string`/`str`, `integer`/`int`, `number`/`float`,
|
|
92
|
+
`boolean`/`bool`, `object`/`map`/`dict`, `array`/`list`, `null`/`nil`, `any`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Versioned name parsing
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from nodus_schema import parse_versioned_name, resolve_version, is_stable_version
|
|
100
|
+
|
|
101
|
+
version, action = parse_versioned_name("sys.v1.memory.write")
|
|
102
|
+
# ("v1", "memory.write")
|
|
103
|
+
|
|
104
|
+
best = resolve_version("v1", available={"v1", "v2"}, fallback=True)
|
|
105
|
+
# "v1"
|
|
106
|
+
|
|
107
|
+
is_stable_version("v1") # True
|
|
108
|
+
is_stable_version("v1alpha1") # False
|
|
109
|
+
is_stable_version("v1beta2") # False
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## SchemaRegistry
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from nodus_schema import SchemaRegistry, SchemaEntry
|
|
118
|
+
|
|
119
|
+
registry = SchemaRegistry()
|
|
120
|
+
registry.register(SchemaEntry(
|
|
121
|
+
name="memory.write",
|
|
122
|
+
version="v1",
|
|
123
|
+
input_schema={"key": "str", "value": "any"},
|
|
124
|
+
output_schema={"stored": "bool"},
|
|
125
|
+
stable=True,
|
|
126
|
+
deprecated=False,
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
entry = registry.get("memory.write", version="v1") # SchemaEntry | None
|
|
130
|
+
versions = registry.versions("memory.write") # ["v1"]
|
|
131
|
+
all_entries = registry.list() # list[SchemaEntry]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Design
|
|
137
|
+
|
|
138
|
+
- **No required dependencies.** Pure stdlib (`json`, `threading`, `dataclasses`).
|
|
139
|
+
- **Simple-form schemas.** Flat `{field: type}` dicts are normalized to JSON
|
|
140
|
+
Schema objects automatically.
|
|
141
|
+
- **Thread-safe.** `SchemaRegistry` uses `threading.Lock`.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install -e ".[dev]"
|
|
149
|
+
pytest tests/ -q
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
nodus_schema/__init__.py
|
|
5
|
+
nodus_schema/registry.py
|
|
6
|
+
nodus_schema/schema.py
|
|
7
|
+
nodus_schema/versioning.py
|
|
8
|
+
nodus_schema.egg-info/PKG-INFO
|
|
9
|
+
nodus_schema.egg-info/SOURCES.txt
|
|
10
|
+
nodus_schema.egg-info/dependency_links.txt
|
|
11
|
+
nodus_schema.egg-info/requires.txt
|
|
12
|
+
nodus_schema.egg-info/top_level.txt
|
|
13
|
+
tests/test_registry.py
|
|
14
|
+
tests/test_schema.py
|
|
15
|
+
tests/test_versioning.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodus_schema
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nodus-schema"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Versioned ABI schema validation, parsing, and registry"
|
|
9
|
+
authors = [{ name = "Shawn Knight" }]
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = ["pytest>=8.0"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/Masterplanner25/nodus-schema"
|
|
20
|
+
Repository = "https://github.com/Masterplanner25/nodus-schema"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["."]
|
|
24
|
+
include = ["nodus_schema*"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from nodus_schema import SchemaEntry, SchemaRegistry
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _entry(name="memory.read", version="v1", **kwargs):
|
|
5
|
+
return SchemaEntry(name=name, version=version, **kwargs)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_register_and_get():
|
|
9
|
+
reg = SchemaRegistry()
|
|
10
|
+
e = _entry()
|
|
11
|
+
reg.register(e)
|
|
12
|
+
result = reg.get("v1.memory.read")
|
|
13
|
+
assert result is e
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_get_unknown_returns_none():
|
|
17
|
+
reg = SchemaRegistry()
|
|
18
|
+
assert reg.get("v1.unknown") is None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_versions():
|
|
22
|
+
reg = SchemaRegistry()
|
|
23
|
+
reg.register(_entry("memory.read", "v1"))
|
|
24
|
+
reg.register(_entry("memory.read", "v2"))
|
|
25
|
+
assert reg.versions() == frozenset({"v1", "v2"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_list_excludes_deprecated():
|
|
29
|
+
reg = SchemaRegistry()
|
|
30
|
+
reg.register(_entry("memory.read", "v1"))
|
|
31
|
+
reg.register(_entry("memory.read", "v2", deprecated=True))
|
|
32
|
+
result = reg.list()
|
|
33
|
+
assert len(result) == 1
|
|
34
|
+
assert result[0].version == "v1"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_list_include_deprecated():
|
|
38
|
+
reg = SchemaRegistry()
|
|
39
|
+
reg.register(_entry("a", "v1"))
|
|
40
|
+
reg.register(_entry("b", "v1", deprecated=True))
|
|
41
|
+
assert len(reg.list(include_deprecated=True)) == 2
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_contains():
|
|
45
|
+
reg = SchemaRegistry()
|
|
46
|
+
reg.register(_entry())
|
|
47
|
+
assert "v1.memory.read" in reg
|
|
48
|
+
assert "v2.memory.read" not in reg
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_len():
|
|
52
|
+
reg = SchemaRegistry()
|
|
53
|
+
assert len(reg) == 0
|
|
54
|
+
reg.register(_entry("a", "v1"))
|
|
55
|
+
reg.register(_entry("b", "v1"))
|
|
56
|
+
assert len(reg) == 2
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from nodus_schema import validate_payload
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_empty_schema_passes_anything():
|
|
5
|
+
assert validate_payload({}, {"x": 1}) == []
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_required_field_missing():
|
|
9
|
+
errs = validate_payload({"required": ["q"]}, {})
|
|
10
|
+
assert any("q" in e for e in errs)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_required_field_present():
|
|
14
|
+
assert validate_payload({"required": ["q"], "properties": {"q": {"type": "string"}}},
|
|
15
|
+
{"q": "hello"}) == []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_type_string_pass():
|
|
19
|
+
s = {"properties": {"name": {"type": "string"}}}
|
|
20
|
+
assert validate_payload(s, {"name": "Alice"}) == []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_type_string_fail():
|
|
24
|
+
s = {"properties": {"name": {"type": "string"}}}
|
|
25
|
+
errs = validate_payload(s, {"name": 123})
|
|
26
|
+
assert errs
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_type_int_pass():
|
|
30
|
+
s = {"properties": {"limit": {"type": "int"}}}
|
|
31
|
+
assert validate_payload(s, {"limit": 5}) == []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_type_int_fail_on_float():
|
|
35
|
+
s = {"properties": {"n": {"type": "int"}}}
|
|
36
|
+
errs = validate_payload(s, {"n": 1.5})
|
|
37
|
+
assert errs
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_type_bool_pass():
|
|
41
|
+
s = {"properties": {"flag": {"type": "bool"}}}
|
|
42
|
+
assert validate_payload(s, {"flag": True}) == []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_type_bool_fail_on_int():
|
|
46
|
+
s = {"properties": {"flag": {"type": "bool"}}}
|
|
47
|
+
errs = validate_payload(s, {"flag": 1})
|
|
48
|
+
assert errs
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_type_list_pass():
|
|
52
|
+
s = {"properties": {"tags": {"type": "list"}}}
|
|
53
|
+
assert validate_payload(s, {"tags": ["a", "b"]}) == []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_type_dict_pass():
|
|
57
|
+
s = {"properties": {"meta": {"type": "dict"}}}
|
|
58
|
+
assert validate_payload(s, {"meta": {"k": "v"}}) == []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_extra_fields_allowed():
|
|
62
|
+
s = {"properties": {"q": {"type": "string"}}}
|
|
63
|
+
assert validate_payload(s, {"q": "hello", "extra": 123}) == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_non_dict_payload():
|
|
67
|
+
errs = validate_payload({"required": ["q"]}, "not a dict") # type: ignore[arg-type]
|
|
68
|
+
assert errs
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from nodus_schema import (
|
|
3
|
+
SYSCALL_VERSION_FALLBACK, is_stable_version,
|
|
4
|
+
parse_versioned_name, resolve_version,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_parse_basic():
|
|
9
|
+
v, a = parse_versioned_name("v1.memory.read")
|
|
10
|
+
assert v == "v1" and a == "memory.read"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_parse_v2():
|
|
14
|
+
v, a = parse_versioned_name("v2.flow.run")
|
|
15
|
+
assert v == "v2" and a == "flow.run"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_parse_invalid_raises():
|
|
19
|
+
with pytest.raises(ValueError):
|
|
20
|
+
parse_versioned_name("memory.read")
|
|
21
|
+
with pytest.raises(ValueError):
|
|
22
|
+
parse_versioned_name("1.memory.read")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_resolve_present():
|
|
26
|
+
assert resolve_version("v1", frozenset({"v1", "v2"})) == "v1"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_resolve_fallback():
|
|
30
|
+
result = resolve_version("v3", frozenset({"v1", "v2"}), fallback="v1")
|
|
31
|
+
assert result == "v1"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_resolve_none_when_unavailable():
|
|
35
|
+
result = resolve_version("v99", frozenset({"v1"}), fallback="v99")
|
|
36
|
+
assert result is None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_resolve_no_fallback():
|
|
40
|
+
result = resolve_version("v3", frozenset({"v1"}), fallback=None)
|
|
41
|
+
assert result is None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_default_fallback_is_v1():
|
|
45
|
+
assert SYSCALL_VERSION_FALLBACK == "v1"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_is_stable_version():
|
|
49
|
+
assert is_stable_version("v1") is True
|
|
50
|
+
assert is_stable_version("v2") is True
|
|
51
|
+
assert is_stable_version("v1alpha1") is False
|
|
52
|
+
assert is_stable_version("v0") is False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_registry_full_name():
|
|
56
|
+
from nodus_schema import SchemaEntry
|
|
57
|
+
e = SchemaEntry("memory.read", "v1")
|
|
58
|
+
assert e.full_name == "v1.memory.read"
|