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.
Files changed (45) hide show
  1. typebind-0.1.0a1/.python-version +1 -0
  2. typebind-0.1.0a1/CONTRIBUTING.md +184 -0
  3. typebind-0.1.0a1/LICENSE +21 -0
  4. typebind-0.1.0a1/PKG-INFO +149 -0
  5. typebind-0.1.0a1/README.md +128 -0
  6. typebind-0.1.0a1/SECURITY.md +39 -0
  7. typebind-0.1.0a1/benchmarks/behavior.py +90 -0
  8. typebind-0.1.0a1/benchmarks/models.py +329 -0
  9. typebind-0.1.0a1/docs/behaviors.md +114 -0
  10. typebind-0.1.0a1/docs/discovery.md +220 -0
  11. typebind-0.1.0a1/docs/models.md +680 -0
  12. typebind-0.1.0a1/docs/releasing.md +396 -0
  13. typebind-0.1.0a1/examples/README.md +72 -0
  14. typebind-0.1.0a1/examples/commands.py +54 -0
  15. typebind-0.1.0a1/examples/decorators.py +92 -0
  16. typebind-0.1.0a1/examples/discovery.py +44 -0
  17. typebind-0.1.0a1/examples/manual.py +40 -0
  18. typebind-0.1.0a1/examples/models.py +47 -0
  19. typebind-0.1.0a1/examples/plugins/core/pyproject.toml +20 -0
  20. typebind-0.1.0a1/examples/plugins/core/src/typebind_example_core/__init__.py +51 -0
  21. typebind-0.1.0a1/examples/plugins/core/src/typebind_example_core/py.typed +0 -0
  22. typebind-0.1.0a1/examples/plugins/extra/pyproject.toml +19 -0
  23. typebind-0.1.0a1/examples/plugins/extra/src/typebind_example_extra/__init__.py +19 -0
  24. typebind-0.1.0a1/examples/plugins/extra/src/typebind_example_extra/py.typed +0 -0
  25. typebind-0.1.0a1/pyproject.toml +207 -0
  26. typebind-0.1.0a1/pyproject.toml.orig +144 -0
  27. typebind-0.1.0a1/scripts/audit_dependencies.py +41 -0
  28. typebind-0.1.0a1/scripts/check_dist.py +178 -0
  29. typebind-0.1.0a1/scripts/check_release.py +84 -0
  30. typebind-0.1.0a1/src/typebind/__init__.py +37 -0
  31. typebind-0.1.0a1/src/typebind/_behavior.py +202 -0
  32. typebind-0.1.0a1/src/typebind/_discovery.py +181 -0
  33. typebind-0.1.0a1/src/typebind/_models.py +584 -0
  34. typebind-0.1.0a1/src/typebind/py.typed +0 -0
  35. typebind-0.1.0a1/tests/test_behavior.py +385 -0
  36. typebind-0.1.0a1/tests/test_behavior_typing.py +88 -0
  37. typebind-0.1.0a1/tests/test_check_dist.py +65 -0
  38. typebind-0.1.0a1/tests/test_decorators.py +437 -0
  39. typebind-0.1.0a1/tests/test_discovery.py +412 -0
  40. typebind-0.1.0a1/tests/test_discovery_install.py +142 -0
  41. typebind-0.1.0a1/tests/test_model_policies.py +193 -0
  42. typebind-0.1.0a1/tests/test_models.py +997 -0
  43. typebind-0.1.0a1/tests/test_release_version.py +41 -0
  44. typebind-0.1.0a1/tests/test_typing.py +259 -0
  45. 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).
@@ -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
+ )