typebind 0.1.0a1__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.
- typebind-0.1.0a1/.python-version +1 -0
- typebind-0.1.0a1/CONTRIBUTING.md +184 -0
- typebind-0.1.0a1/LICENSE +21 -0
- typebind-0.1.0a1/PKG-INFO +149 -0
- typebind-0.1.0a1/README.md +128 -0
- typebind-0.1.0a1/SECURITY.md +39 -0
- typebind-0.1.0a1/benchmarks/behavior.py +90 -0
- typebind-0.1.0a1/benchmarks/models.py +329 -0
- typebind-0.1.0a1/docs/behaviors.md +114 -0
- typebind-0.1.0a1/docs/discovery.md +220 -0
- typebind-0.1.0a1/docs/models.md +680 -0
- typebind-0.1.0a1/docs/releasing.md +396 -0
- typebind-0.1.0a1/examples/README.md +72 -0
- typebind-0.1.0a1/examples/commands.py +54 -0
- typebind-0.1.0a1/examples/decorators.py +92 -0
- typebind-0.1.0a1/examples/discovery.py +44 -0
- typebind-0.1.0a1/examples/manual.py +40 -0
- typebind-0.1.0a1/examples/models.py +47 -0
- typebind-0.1.0a1/examples/plugins/core/pyproject.toml +20 -0
- typebind-0.1.0a1/examples/plugins/core/src/typebind_example_core/__init__.py +51 -0
- typebind-0.1.0a1/examples/plugins/core/src/typebind_example_core/py.typed +0 -0
- typebind-0.1.0a1/examples/plugins/extra/pyproject.toml +19 -0
- typebind-0.1.0a1/examples/plugins/extra/src/typebind_example_extra/__init__.py +19 -0
- typebind-0.1.0a1/examples/plugins/extra/src/typebind_example_extra/py.typed +0 -0
- typebind-0.1.0a1/pyproject.toml +207 -0
- typebind-0.1.0a1/pyproject.toml.orig +144 -0
- typebind-0.1.0a1/scripts/audit_dependencies.py +41 -0
- typebind-0.1.0a1/scripts/check_dist.py +178 -0
- typebind-0.1.0a1/scripts/check_release.py +84 -0
- typebind-0.1.0a1/src/typebind/__init__.py +37 -0
- typebind-0.1.0a1/src/typebind/_behavior.py +202 -0
- typebind-0.1.0a1/src/typebind/_discovery.py +181 -0
- typebind-0.1.0a1/src/typebind/_models.py +584 -0
- typebind-0.1.0a1/src/typebind/py.typed +0 -0
- typebind-0.1.0a1/tests/test_behavior.py +385 -0
- typebind-0.1.0a1/tests/test_behavior_typing.py +88 -0
- typebind-0.1.0a1/tests/test_check_dist.py +65 -0
- typebind-0.1.0a1/tests/test_decorators.py +437 -0
- typebind-0.1.0a1/tests/test_discovery.py +412 -0
- typebind-0.1.0a1/tests/test_discovery_install.py +142 -0
- typebind-0.1.0a1/tests/test_model_policies.py +193 -0
- typebind-0.1.0a1/tests/test_models.py +997 -0
- typebind-0.1.0a1/tests/test_release_version.py +41 -0
- typebind-0.1.0a1/tests/test_typing.py +259 -0
- typebind-0.1.0a1/uv.lock +1370 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14.8
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Set up and run checks
|
|
4
|
+
|
|
5
|
+
Fork the repository and create a branch from `main`. Install the uv version
|
|
6
|
+
specified by `tool.uv.required-version` in [pyproject.toml](pyproject.toml), using
|
|
7
|
+
[uv's installation instructions](https://docs.astral.sh/uv/getting-started/installation/).
|
|
8
|
+
Do not relax that requirement to use a different local tool version.
|
|
9
|
+
|
|
10
|
+
From the checkout root:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
uv sync --locked
|
|
14
|
+
uv run --no-sync poe check
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
uv installs the Python version selected by `.python-version` when needed. The
|
|
18
|
+
root development group includes the two local plugin examples. No package-index
|
|
19
|
+
credentials, publishing permissions or application secrets are needed.
|
|
20
|
+
|
|
21
|
+
`poe check` runs formatting, lint, strict Pyright, tests, runnable examples,
|
|
22
|
+
builds and isolated wheel verification. Run focused tasks while developing:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
uv run --no-sync poe test
|
|
26
|
+
uv run --no-sync pytest -m integration
|
|
27
|
+
uv run --no-sync poe benchmark
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See [Examples](examples/README.md) for individual example commands and what each
|
|
31
|
+
script demonstrates.
|
|
32
|
+
|
|
33
|
+
The test task reports branch coverage and missing lines as diagnostics; there is
|
|
34
|
+
no percentage gate. The `integration` marker selects the installed-distribution
|
|
35
|
+
test independently. It builds the two plugin wheels and the current library wheel,
|
|
36
|
+
then verifies installation, version, exact local wheel origins and imports.
|
|
37
|
+
It runs the discovery example against installed wheels, verifying real entry points,
|
|
38
|
+
behavior results, nested JSON round trips and cross-package Pyright inference.
|
|
39
|
+
Generic model and decorator wheel checks belong to artifact verification.
|
|
40
|
+
|
|
41
|
+
The build creates the wheel from the sdist. Artifact verification checks package
|
|
42
|
+
metadata with Twine, the packaged license and `py.typed`, and isolated runtime/typing
|
|
43
|
+
behavior. The distribution directory must contain exactly one wheel and one sdist
|
|
44
|
+
for the same release; use a clean output directory after changing versions.
|
|
45
|
+
Installers follow wheel dependency metadata under lockfile-derived
|
|
46
|
+
constraints; they do not preinstall undeclared dependencies. Isolated installs
|
|
47
|
+
may access PyPI, so `poe check` is not an offline command. Runnable examples are
|
|
48
|
+
usage acceptance checks; documentation prose is reviewed, not parsed by tests.
|
|
49
|
+
|
|
50
|
+
Example packages belong to this checkout and must use its current source, not an
|
|
51
|
+
independently selected typebind release. Their dependency metadata declares
|
|
52
|
+
package relationships without version constraints. Root uv source configuration
|
|
53
|
+
selects the local examples during development; the integration test installs the
|
|
54
|
+
library and plugin wheels built from this checkout and verifies their installation
|
|
55
|
+
origins. Changing the main package version does not require bumping the examples'
|
|
56
|
+
own versions. Git retains the target `x.y.z` version; release preparation writes
|
|
57
|
+
the full candidate version only into the CI working copy, as described in
|
|
58
|
+
[Releasing](docs/releasing.md#prepare-and-check-a-candidate). Prerelease candidates
|
|
59
|
+
use the same installation checks with their prepared version; the release workflow verifies the actual candidate rather than
|
|
60
|
+
creating additional synthetic development and alpha versions in each test run.
|
|
61
|
+
|
|
62
|
+
To check the minimum supported Python on POSIX shells, run
|
|
63
|
+
`UV_PYTHON=3.12 uv sync --locked`, then `uv run --no-sync poe check`.
|
|
64
|
+
On PowerShell set `$env:UV_PYTHON = "3.12"` before running `uv sync --locked`
|
|
65
|
+
and `uv run --no-sync poe check`. CI checks Python 3.12–3.15 on Ubuntu 24.04. macOS and Windows are not part of
|
|
66
|
+
the ongoing CI validation. The full matrix runs on PRs, main pushes, manual
|
|
67
|
+
checks and the release workflow. Ordinary branch pushes do not trigger CI.
|
|
68
|
+
Each job syncs dependencies once and runs the Poe sub-tasks as separate steps.
|
|
69
|
+
|
|
70
|
+
A separate Python 3.12 job checks the minimum runtime dependencies declared in
|
|
71
|
+
`pyproject.toml`. It keeps the locked development tools, resolves runtime
|
|
72
|
+
constraints with `uv pip compile --resolution lowest-direct`, and installs that
|
|
73
|
+
combination before running strict typing, non-integration tests and all examples.
|
|
74
|
+
Artifact verification consumes the same constraints through `check-dist --constraint`,
|
|
75
|
+
so its isolated wheel installation also exercises the minimum dependencies.
|
|
76
|
+
The installed-plugin integration tests remain in the locked Python matrix; their
|
|
77
|
+
own isolated environments use lockfile constraints.
|
|
78
|
+
|
|
79
|
+
For workflow changes, run actionlint (the version is declared in `ci.yml`) and:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
uv run --no-sync poe workflow-security
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This runs offline zizmor checks. Validate orchestration with native tools and
|
|
86
|
+
actual Actions runs; do not add library tests that mirror workflow YAML or step
|
|
87
|
+
order. Exercise new project-owned automation behavior at its own boundary.
|
|
88
|
+
|
|
89
|
+
## Measure performance changes
|
|
90
|
+
|
|
91
|
+
Run `poe benchmark` for performance-related changes, or use
|
|
92
|
+
`poe benchmark-models` and `poe benchmark-behavior` separately. Benchmarks are
|
|
93
|
+
manual diagnostics, not a routine CI step or a timing gate.
|
|
94
|
+
|
|
95
|
+
Model cases compare omitted and explicit tags, both public bases, multilevel
|
|
96
|
+
families, Literal aliases and decorators against native Pydantic controls with
|
|
97
|
+
matching frozen fields and default validation. A native identity-marking validator
|
|
98
|
+
separates the cost of that guarantee from plain defaults. Copy cases isolate the
|
|
99
|
+
identity guard cost; JSON cases exercise serialization, validation and nested
|
|
100
|
+
`exclude_unset=True` round trips. Behavior cases compare table-based adaptation
|
|
101
|
+
with direct implementation construction. These microbenchmarks do not establish
|
|
102
|
+
service throughput; report the interpreter and comparison cases with results.
|
|
103
|
+
|
|
104
|
+
## Submit a pull request
|
|
105
|
+
|
|
106
|
+
Describe the concrete behavior changed, why it is needed, and the checks actually
|
|
107
|
+
run. Include a small reproduction for bug fixes. Add tests for observable library
|
|
108
|
+
behavior; keep configuration and documentation changes focused on their owners.
|
|
109
|
+
Keep code, examples, comments and repository documentation in English.
|
|
110
|
+
|
|
111
|
+
Call out public API, serialization, typing and compatibility changes. Preserve
|
|
112
|
+
field definitions as the source of tags and keep discovery, assembly and runtime
|
|
113
|
+
adaptation separate. The contracts and current limitations are in `docs/`.
|
|
114
|
+
Benchmarks report measurements; they are not machine-dependent pass/fail gates.
|
|
115
|
+
|
|
116
|
+
Keep credentials, private data, generated distributions and virtual environments
|
|
117
|
+
out of commits. Report vulnerabilities through [the security policy](SECURITY.md),
|
|
118
|
+
not public bug reports. External PRs run without publishing credentials. CI
|
|
119
|
+
approval does not authorize publishing. Maintainers review and merge PRs; there
|
|
120
|
+
is no automatic merge for dependency updates.
|
|
121
|
+
|
|
122
|
+
## Environment and dependency maintenance
|
|
123
|
+
|
|
124
|
+
The workflow files own runner labels. CI and publishing both use Ubuntu 24.04 (x64). Change OS
|
|
125
|
+
versions explicitly in a reviewed PR and verify all affected jobs. Versioned
|
|
126
|
+
labels avoid automatic OS-major migration; GitHub still updates the images and
|
|
127
|
+
preinstalled tools. This is not a bit-for-bit frozen VM.
|
|
128
|
+
|
|
129
|
+
The Python compatibility matrix intentionally follows current patch releases of
|
|
130
|
+
3.12 through 3.15. Local development and publishing select `.python-version`.
|
|
131
|
+
`pyproject.toml` fixes uv and the release build-backend constraint; `uv.lock`
|
|
132
|
+
selects development and runtime dependencies. Third-party Actions use commit
|
|
133
|
+
SHAs. actionlint has a fixed Go module version. Go, Git, shell utilities and the
|
|
134
|
+
GitHub CLI are runner-provided tooling, deliberately updated with the image;
|
|
135
|
+
they are not claimed to be pinned or covered by the Python lock audit.
|
|
136
|
+
|
|
137
|
+
Actions Dependabot checks weekly. Minor/patch version updates share a PR; major
|
|
138
|
+
updates remain separate. Security updates are not delayed by that version-update
|
|
139
|
+
group. Review the diff and require the full CI checks before merging.
|
|
140
|
+
|
|
141
|
+
Python Dependabot is not enabled yet: the inspected updater source
|
|
142
|
+
uses uv 0.12.19, while this project's required uv is 0.13.0. This source inspection
|
|
143
|
+
is not a successful hosted updater test. Keep the project pin; do not add a second
|
|
144
|
+
lockfile or weaken the toolchain to accommodate a bot. Until a hosted updater can
|
|
145
|
+
resolve the local example sources with the required uv, maintainers update Python
|
|
146
|
+
dependencies in ordinary PRs:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
uv lock --upgrade-package PACKAGE_NAME
|
|
150
|
+
uv sync --locked
|
|
151
|
+
uv run --no-sync poe check
|
|
152
|
+
uv run --no-sync poe audit
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For Pydantic updates, update `pydantic` and let its exact dependency select
|
|
156
|
+
`pydantic-core`; typebind does not impose another core version constraint.
|
|
157
|
+
Follow the [compatibility checks](docs/models.md#current-boundaries) for dependency
|
|
158
|
+
changes. The locked matrix and minimum-dependency job cover their actual resolved
|
|
159
|
+
combinations, not every version allowed by the package metadata.
|
|
160
|
+
|
|
161
|
+
`poe audit` exports all locked third-party runtime/development entries to a
|
|
162
|
+
temporary native `pylock.toml`, then uses pip-audit's lock reader with strict
|
|
163
|
+
collection. The independent `Dependency audit` workflow runs weekly or manually.
|
|
164
|
+
Vulnerabilities, collection failures and service errors fail that workflow and
|
|
165
|
+
remain visible in Actions notifications; they are not suppressed. It is not a
|
|
166
|
+
required PR check or a dependency of the publishing workflow. Maintainers triage
|
|
167
|
+
findings for affected code paths and environments, and review new dependency risks
|
|
168
|
+
when updating dependencies. A failed advisory query is not a vulnerability finding.
|
|
169
|
+
An unresolved risk relevant to the release must be addressed before approving its
|
|
170
|
+
upload; an unrelated development-dependency advisory is not an automatic release veto.
|
|
171
|
+
The audit does not cover local project code, Actions, runner tools or the separate
|
|
172
|
+
build-backend constraint. Review backend advisories when upgrading the toolchain.
|
|
173
|
+
Security fixes still require review and tests; the audit does not update packages.
|
|
174
|
+
Run it from Actions → Dependency audit → Run workflow, selecting `main`, or use
|
|
175
|
+
`gh workflow run audit.yml --ref main`. The workflow must exist on the default
|
|
176
|
+
branch for dispatch and scheduling. Inspect its first scheduled result and
|
|
177
|
+
notification delivery on GitHub; local audit success does not establish either.
|
|
178
|
+
|
|
179
|
+
Before enabling Python Dependabot, verify a real hosted update that changes a
|
|
180
|
+
resolved dependency and passes CI with the required uv. A no-op lock rewrite is
|
|
181
|
+
not that acceptance test. See [uv's Dependabot guide](https://docs.astral.sh/uv/guides/integration/dependabot/)
|
|
182
|
+
and the [updater source](https://github.com/dependabot/dependabot-core/blob/780c17e16ac2cd5b8e04a9e27fc2da4de6043290/uv/Dockerfile).
|
|
183
|
+
|
|
184
|
+
Publishing and one-time owner settings are described in [Releasing](docs/releasing.md).
|
typebind-0.1.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 typebind contributors
|
|
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,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typebind
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Typed Pydantic model families, plugin discovery, and scoped behavior adapters.
|
|
5
|
+
Keywords: pydantic,discriminator,generics,plugins,adapters
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Dist: pydantic>=2.11.2,<3
|
|
15
|
+
Requires-Dist: typing-extensions>=4.15
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Project-URL: Homepage, https://github.com/nicholas-1vbw/typebind
|
|
18
|
+
Project-URL: Repository, https://github.com/nicholas-1vbw/typebind
|
|
19
|
+
Project-URL: Issues, https://github.com/nicholas-1vbw/typebind/issues
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# typebind
|
|
23
|
+
|
|
24
|
+
Build extensible Pydantic model families with automatic discriminator tags and
|
|
25
|
+
statically typed fields. Bind behavior implementations to models without mixing
|
|
26
|
+
them into the data classes. Select variants explicitly, or optionally discover
|
|
27
|
+
models and implementations supplied by installed plugin packages.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
Python 3.12 or later is required. This project is preparing its first release and
|
|
32
|
+
has not been published to PyPI. From a local checkout:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
python -m pip install .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The supported Pydantic range and other runtime requirements are declared in
|
|
39
|
+
[pyproject.toml](https://github.com/nicholas-1vbw/typebind/blob/main/pyproject.toml).
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Declare a family and its variants, then build an adapter for the variants you accept:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from typebind import DiscriminatedModel, build_adapter, model
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@model(tag="kind")
|
|
50
|
+
class Message(DiscriminatedModel):
|
|
51
|
+
identifier: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@model(variant="query")
|
|
55
|
+
class Query(Message):
|
|
56
|
+
expression: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@model(variant="ping")
|
|
60
|
+
class Ping(Message):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
adapter = build_adapter(Message, variants=(Query, Ping))
|
|
65
|
+
query = Query(identifier="q", expression="hello") # kind defaults to "query"
|
|
66
|
+
wire = adapter.dump_json(query)
|
|
67
|
+
assert adapter.validate_json(wire) == query
|
|
68
|
+
assert type(adapter.validate_python({"kind": "ping", "identifier": "p"})) is Ping
|
|
69
|
+
print(wire.decode()) # {"kind":"query","identifier":"q","expression":"hello"}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`build_adapter` returns a native Pydantic `TypeAdapter`. For nested model fields,
|
|
73
|
+
use [`bind_variants`](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#consumer-types).
|
|
74
|
+
Import public APIs from `typebind`; underscore-prefixed modules are internal.
|
|
75
|
+
|
|
76
|
+
The [`@model` decorator](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#decorator-declarations)
|
|
77
|
+
preserves static checking for declared fields such as `identifier` and `expression`.
|
|
78
|
+
Generated tag attributes such as `kind` are runtime-only: Pyright does not see
|
|
79
|
+
them or accept them as constructor keywords. Use explicit generics when your
|
|
80
|
+
code needs statically typed `Literal` tags:
|
|
81
|
+
|
|
82
|
+
<details>
|
|
83
|
+
<summary>Explicit generics with statically typed tags</summary>
|
|
84
|
+
|
|
85
|
+
This standalone example provides the same validation and JSON round trip:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from typing import Any, Literal
|
|
89
|
+
|
|
90
|
+
from typebind import DiscriminatedModel, build_adapter, tag_field
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Message[Kind: str](DiscriminatedModel):
|
|
94
|
+
kind: Kind = tag_field()
|
|
95
|
+
identifier: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Query(Message[Literal["query"]]):
|
|
99
|
+
expression: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Ping(Message[Literal["ping"]]):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
adapter = build_adapter(Message[Any], variants=(Query, Ping))
|
|
107
|
+
query = Query(identifier="q", expression="hello") # kind defaults to "query"
|
|
108
|
+
wire = adapter.dump_json(query)
|
|
109
|
+
assert adapter.validate_json(wire) == query
|
|
110
|
+
assert type(adapter.validate_python({"kind": "ping", "identifier": "p"})) is Ping
|
|
111
|
+
print(wire.decode()) # {"kind":"query","identifier":"q","expression":"hello"}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`Message[Any]` keeps common fields typed while allowing runtime-selected tags;
|
|
115
|
+
concrete variants retain their `Literal` tags. For example, `query.kind` has the
|
|
116
|
+
static type `Literal["query"]`.
|
|
117
|
+
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
## Usage notes
|
|
121
|
+
|
|
122
|
+
- Constructors can omit fixed tags, but incoming data routed through an adapter
|
|
123
|
+
must contain them. Keep tags in persisted JSON and bind the reader's accepted variants.
|
|
124
|
+
- Ordinary dumps retain tags. If you need `exclude_unset=True`, use
|
|
125
|
+
`IdentityDiscriminatedModel` to retain omitted tags. Explicit tag exclusions and
|
|
126
|
+
`exclude_defaults=True` can still remove them; see the
|
|
127
|
+
[serialization contract](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#json-serialization-contract).
|
|
128
|
+
- Strict Pyright is verified. Discriminator aliases, additional payload type
|
|
129
|
+
parameters and multiple model-family parents are unsupported; see
|
|
130
|
+
[typing and compatibility boundaries](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#current-boundaries).
|
|
131
|
+
|
|
132
|
+
## Documentation and examples
|
|
133
|
+
|
|
134
|
+
- [Runnable examples](https://github.com/nicholas-1vbw/typebind/blob/main/examples/README.md): setup and commands for model families, manual behavior assembly, decorators and cross-package discovery.
|
|
135
|
+
- [Models](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md): declarations, nested binding, serialization and copying.
|
|
136
|
+
- [Behaviors](https://github.com/nicholas-1vbw/typebind/blob/main/docs/behaviors.md): typed interfaces, assembly and dispatch contracts.
|
|
137
|
+
- [Discovery](https://github.com/nicholas-1vbw/typebind/blob/main/docs/discovery.md): package layout, entry points and scoped selection. Discovery is optional for all model and behavior APIs.
|
|
138
|
+
|
|
139
|
+
## Contributing
|
|
140
|
+
|
|
141
|
+
See [Contributing](https://github.com/nicholas-1vbw/typebind/blob/main/CONTRIBUTING.md)
|
|
142
|
+
for development setup and checks, and
|
|
143
|
+
[Releasing](https://github.com/nicholas-1vbw/typebind/blob/main/docs/releasing.md)
|
|
144
|
+
for publishing. Report vulnerabilities through the
|
|
145
|
+
[security policy](https://github.com/nicholas-1vbw/typebind/blob/main/SECURITY.md).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
[MIT](https://github.com/nicholas-1vbw/typebind/blob/main/LICENSE).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# typebind
|
|
2
|
+
|
|
3
|
+
Build extensible Pydantic model families with automatic discriminator tags and
|
|
4
|
+
statically typed fields. Bind behavior implementations to models without mixing
|
|
5
|
+
them into the data classes. Select variants explicitly, or optionally discover
|
|
6
|
+
models and implementations supplied by installed plugin packages.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Python 3.12 or later is required. This project is preparing its first release and
|
|
11
|
+
has not been published to PyPI. From a local checkout:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
python -m pip install .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The supported Pydantic range and other runtime requirements are declared in
|
|
18
|
+
[pyproject.toml](https://github.com/nicholas-1vbw/typebind/blob/main/pyproject.toml).
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Declare a family and its variants, then build an adapter for the variants you accept:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from typebind import DiscriminatedModel, build_adapter, model
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@model(tag="kind")
|
|
29
|
+
class Message(DiscriminatedModel):
|
|
30
|
+
identifier: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@model(variant="query")
|
|
34
|
+
class Query(Message):
|
|
35
|
+
expression: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@model(variant="ping")
|
|
39
|
+
class Ping(Message):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
adapter = build_adapter(Message, variants=(Query, Ping))
|
|
44
|
+
query = Query(identifier="q", expression="hello") # kind defaults to "query"
|
|
45
|
+
wire = adapter.dump_json(query)
|
|
46
|
+
assert adapter.validate_json(wire) == query
|
|
47
|
+
assert type(adapter.validate_python({"kind": "ping", "identifier": "p"})) is Ping
|
|
48
|
+
print(wire.decode()) # {"kind":"query","identifier":"q","expression":"hello"}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`build_adapter` returns a native Pydantic `TypeAdapter`. For nested model fields,
|
|
52
|
+
use [`bind_variants`](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#consumer-types).
|
|
53
|
+
Import public APIs from `typebind`; underscore-prefixed modules are internal.
|
|
54
|
+
|
|
55
|
+
The [`@model` decorator](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#decorator-declarations)
|
|
56
|
+
preserves static checking for declared fields such as `identifier` and `expression`.
|
|
57
|
+
Generated tag attributes such as `kind` are runtime-only: Pyright does not see
|
|
58
|
+
them or accept them as constructor keywords. Use explicit generics when your
|
|
59
|
+
code needs statically typed `Literal` tags:
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary>Explicit generics with statically typed tags</summary>
|
|
63
|
+
|
|
64
|
+
This standalone example provides the same validation and JSON round trip:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from typing import Any, Literal
|
|
68
|
+
|
|
69
|
+
from typebind import DiscriminatedModel, build_adapter, tag_field
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Message[Kind: str](DiscriminatedModel):
|
|
73
|
+
kind: Kind = tag_field()
|
|
74
|
+
identifier: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Query(Message[Literal["query"]]):
|
|
78
|
+
expression: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Ping(Message[Literal["ping"]]):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
adapter = build_adapter(Message[Any], variants=(Query, Ping))
|
|
86
|
+
query = Query(identifier="q", expression="hello") # kind defaults to "query"
|
|
87
|
+
wire = adapter.dump_json(query)
|
|
88
|
+
assert adapter.validate_json(wire) == query
|
|
89
|
+
assert type(adapter.validate_python({"kind": "ping", "identifier": "p"})) is Ping
|
|
90
|
+
print(wire.decode()) # {"kind":"query","identifier":"q","expression":"hello"}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`Message[Any]` keeps common fields typed while allowing runtime-selected tags;
|
|
94
|
+
concrete variants retain their `Literal` tags. For example, `query.kind` has the
|
|
95
|
+
static type `Literal["query"]`.
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
## Usage notes
|
|
100
|
+
|
|
101
|
+
- Constructors can omit fixed tags, but incoming data routed through an adapter
|
|
102
|
+
must contain them. Keep tags in persisted JSON and bind the reader's accepted variants.
|
|
103
|
+
- Ordinary dumps retain tags. If you need `exclude_unset=True`, use
|
|
104
|
+
`IdentityDiscriminatedModel` to retain omitted tags. Explicit tag exclusions and
|
|
105
|
+
`exclude_defaults=True` can still remove them; see the
|
|
106
|
+
[serialization contract](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#json-serialization-contract).
|
|
107
|
+
- Strict Pyright is verified. Discriminator aliases, additional payload type
|
|
108
|
+
parameters and multiple model-family parents are unsupported; see
|
|
109
|
+
[typing and compatibility boundaries](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md#current-boundaries).
|
|
110
|
+
|
|
111
|
+
## Documentation and examples
|
|
112
|
+
|
|
113
|
+
- [Runnable examples](https://github.com/nicholas-1vbw/typebind/blob/main/examples/README.md): setup and commands for model families, manual behavior assembly, decorators and cross-package discovery.
|
|
114
|
+
- [Models](https://github.com/nicholas-1vbw/typebind/blob/main/docs/models.md): declarations, nested binding, serialization and copying.
|
|
115
|
+
- [Behaviors](https://github.com/nicholas-1vbw/typebind/blob/main/docs/behaviors.md): typed interfaces, assembly and dispatch contracts.
|
|
116
|
+
- [Discovery](https://github.com/nicholas-1vbw/typebind/blob/main/docs/discovery.md): package layout, entry points and scoped selection. Discovery is optional for all model and behavior APIs.
|
|
117
|
+
|
|
118
|
+
## Contributing
|
|
119
|
+
|
|
120
|
+
See [Contributing](https://github.com/nicholas-1vbw/typebind/blob/main/CONTRIBUTING.md)
|
|
121
|
+
for development setup and checks, and
|
|
122
|
+
[Releasing](https://github.com/nicholas-1vbw/typebind/blob/main/docs/releasing.md)
|
|
123
|
+
for publishing. Report vulnerabilities through the
|
|
124
|
+
[security policy](https://github.com/nicholas-1vbw/typebind/blob/main/SECURITY.md).
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
[MIT](https://github.com/nicholas-1vbw/typebind/blob/main/LICENSE).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Report a vulnerability privately
|
|
4
|
+
|
|
5
|
+
Use [Report a vulnerability](https://github.com/nicholas-1vbw/typebind/security/advisories/new)
|
|
6
|
+
on the repository's Security tab. Include the affected version, a minimal
|
|
7
|
+
reproduction, expected and actual behavior, and the impact. Remove credentials
|
|
8
|
+
and private data from examples. Do not publish exploit details in an issue or PR
|
|
9
|
+
before coordinating disclosure with the maintainer.
|
|
10
|
+
|
|
11
|
+
This channel requires the owner to enable GitHub private vulnerability reporting
|
|
12
|
+
when the repository becomes public. If the button is unavailable, open an issue
|
|
13
|
+
asking for a private reporting channel without including vulnerability details.
|
|
14
|
+
No response-time or remediation SLA is promised.
|
|
15
|
+
|
|
16
|
+
## Supported releases
|
|
17
|
+
|
|
18
|
+
Before the first stable release, fixes target the latest published prerelease.
|
|
19
|
+
After a stable release, fixes target the latest release; upgrading may be
|
|
20
|
+
necessary. Older release branches do not have a separate backport commitment.
|
|
21
|
+
Report suspected problems even if you cannot yet reproduce them on that version.
|
|
22
|
+
|
|
23
|
+
## Trust boundaries
|
|
24
|
+
|
|
25
|
+
Typebind parses model data using Pydantic and assembles explicitly selected model
|
|
26
|
+
and behavior classes. Discriminator tags identify branches; they do not authorize
|
|
27
|
+
a caller. Applications own authentication, authorization, input size limits and
|
|
28
|
+
resource budgets.
|
|
29
|
+
|
|
30
|
+
Entry point loading imports and executes installed Python packages. Behavior
|
|
31
|
+
implementations are also trusted executable code. Family checks and source
|
|
32
|
+
scopes are not a sandbox, and post-import selection cannot undo import side
|
|
33
|
+
effects. Install only plugins you trust. Reports of bypassed selection, incorrect
|
|
34
|
+
branch validation or other unexpected behavior remain welcome; these boundaries
|
|
35
|
+
do not exempt defects in the library from investigation.
|
|
36
|
+
|
|
37
|
+
The persistence contract is Pydantic JSON round trips. Pickle is not a supported
|
|
38
|
+
interchange format. Dependency alerts and audits detect known dependency issues;
|
|
39
|
+
they do not replace reports about typebind itself or a security review.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Warm single-process microbenchmark, not a service throughput/load test."""
|
|
2
|
+
|
|
3
|
+
import gc
|
|
4
|
+
import json
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from statistics import median
|
|
9
|
+
from timeit import Timer
|
|
10
|
+
from typing import Any, Literal, Protocol, get_type_hints
|
|
11
|
+
|
|
12
|
+
from typebind import DiscriminatedModel, Implementation, assemble, tag_field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Message[Kind: str](DiscriminatedModel):
|
|
16
|
+
kind: Kind = tag_field()
|
|
17
|
+
text: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Query(Message[Literal["query"]]):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Describable(Protocol):
|
|
25
|
+
@property
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def model(self) -> Message[Any]: ...
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def describe(self) -> str: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class QueryObject(Implementation[Query], Describable):
|
|
34
|
+
def describe(self) -> str:
|
|
35
|
+
return self.model.text
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
runtime = assemble(
|
|
39
|
+
variants=[Query], implementations=[QueryObject], required_interfaces=[Describable]
|
|
40
|
+
)
|
|
41
|
+
query = Query(text="hello")
|
|
42
|
+
view = runtime.adapt(query, Describable)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def direct(model: Query) -> str:
|
|
46
|
+
return model.text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def reflecting_call() -> str:
|
|
50
|
+
"""Comparison only: simulate repeated family annotation resolution."""
|
|
51
|
+
member = vars(Describable)["model"]
|
|
52
|
+
get_type_hints(member.fget)
|
|
53
|
+
return runtime.adapt(query, Describable).describe()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
cases = {
|
|
57
|
+
"direct_function": "direct(query)",
|
|
58
|
+
"reuse_adapter": "view.describe()",
|
|
59
|
+
"known_constructor_and_call": "QueryObject(query).describe()",
|
|
60
|
+
"adapt_and_call": "runtime.adapt(query, Describable).describe()",
|
|
61
|
+
"reflection_and_adapt_and_call": "reflecting_call()",
|
|
62
|
+
}
|
|
63
|
+
assert direct(query) == view.describe() == reflecting_call() == "hello"
|
|
64
|
+
results: dict[str, object] = {}
|
|
65
|
+
for name, statement in cases.items():
|
|
66
|
+
timer = Timer(statement, setup="gc.enable()", globals=globals())
|
|
67
|
+
timer.timeit(number=1_000)
|
|
68
|
+
samples = [elapsed / 100_000 * 1e6 for elapsed in timer.repeat(repeat=7, number=100_000)]
|
|
69
|
+
results[name] = {
|
|
70
|
+
"minimum_us": min(samples),
|
|
71
|
+
"median_us": median(samples),
|
|
72
|
+
"samples_us": samples,
|
|
73
|
+
}
|
|
74
|
+
print(
|
|
75
|
+
json.dumps(
|
|
76
|
+
{
|
|
77
|
+
"python": sys.version,
|
|
78
|
+
"platform": platform.platform(),
|
|
79
|
+
"gc_enabled": gc.isenabled(),
|
|
80
|
+
"iterations": 100_000,
|
|
81
|
+
"repeats": 7,
|
|
82
|
+
"scope": (
|
|
83
|
+
"Sequential warm calls on an existing model; excludes discovery, "
|
|
84
|
+
"assembly, parsing, I/O and concurrency."
|
|
85
|
+
),
|
|
86
|
+
"results": results,
|
|
87
|
+
},
|
|
88
|
+
indent=2,
|
|
89
|
+
)
|
|
90
|
+
)
|