conformly 0.7.4__tar.gz → 0.8.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.
- conformly-0.8.0/PKG-INFO +117 -0
- conformly-0.8.0/README.md +84 -0
- {conformly-0.7.4 → conformly-0.8.0}/pyproject.toml +1 -1
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/__init__.py +3 -1
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/numeric.py +6 -4
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/fields/__init__.py +3 -1
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/registry.py +12 -0
- conformly-0.8.0/src/conformly/_internal/generator/types/bytes.py +25 -0
- conformly-0.8.0/src/conformly/_internal/generator/types/temporal.py +59 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/extractors/constraints.py +6 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/field.py +16 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/resolve.py +25 -2
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/__init__.py +6 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_domains.py +6 -0
- conformly-0.8.0/src/conformly/_internal/resolver/semantics/bytes.py +48 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/factory.py +6 -0
- conformly-0.8.0/src/conformly/_internal/resolver/semantics/temporal.py +139 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/primitives.py +5 -0
- conformly-0.8.0/src/conformly.egg-info/PKG-INFO +117 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/SOURCES.txt +4 -0
- conformly-0.7.4/PKG-INFO +0 -792
- conformly-0.7.4/README.md +0 -759
- conformly-0.7.4/src/conformly.egg-info/PKG-INFO +0 -792
- {conformly-0.7.4 → conformly-0.8.0}/LICENSE +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/setup.cfg +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/_errors.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/_path_proxy.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/_utils.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/case.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/cases.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/path.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/base.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/collections.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/enum.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/keys.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/mapping.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/string.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/fields/registry.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/fields/special_types.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/_errors.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/context.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/orchestration.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/protocol.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/_utils.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/boolean.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/dictionaries.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/email.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/enum.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/float.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/integer.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/ipv4.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/ipv6.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/ipvany.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/list.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/string.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/tuple.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/url.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/uuid.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/registry.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/api.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/core/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/core/element.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/core/field.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/extractors/types.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/models.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/_errors.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/model.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/session.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/models.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_numeric.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_regex.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/base.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/list.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/object.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/string.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/tracer.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/constants.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/strategies.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/values.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/exceptions.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/py.typed +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly/tracer/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/dependency_links.txt +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/requires.txt +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/top_level.txt +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/tests/test_collection.py +0 -0
- {conformly-0.7.4 → conformly-0.8.0}/tests/test_path.py +0 -0
conformly-0.8.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: conformly
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Generate valid & invalid test data from your typed schemas
|
|
5
|
+
Author-email: Nikita Shabanov <nik.shabanov2024@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nashabanov/conformly
|
|
8
|
+
Project-URL: Repository, https://github.com/nashabanov/conformly.git
|
|
9
|
+
Project-URL: Issues, https://github.com/nashabanov/conformly/issues
|
|
10
|
+
Keywords: testing,fixtures,test-data,dataclasses,validation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: rstr>=3.2.2
|
|
23
|
+
Provides-Extra: pydantic
|
|
24
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "pydantic"
|
|
25
|
+
Requires-Dist: email-validator>=2.0.0; extra == "pydantic"
|
|
26
|
+
Provides-Extra: attrs
|
|
27
|
+
Requires-Dist: attrs>=23.0.0; extra == "attrs"
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "all"
|
|
30
|
+
Requires-Dist: email-validator>=2.0.0; extra == "all"
|
|
31
|
+
Requires-Dist: attrs>=23.0.0; extra == "all"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# conformly
|
|
35
|
+
|
|
36
|
+
[](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml)
|
|
37
|
+
[](https://codecov.io/github/nashabanov/conformly)
|
|
38
|
+
[](https://pypi.org/project/conformly/)
|
|
39
|
+
[](https://pypi.org/project/conformly/)
|
|
40
|
+
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
[](https://github.com/nashabanov/conformly/releases)
|
|
43
|
+
[](http://mypy-lang.org/)
|
|
44
|
+
[](https://github.com/astral-sh/ruff)
|
|
45
|
+
|
|
46
|
+
**Declarative test data generator for Python.**
|
|
47
|
+
|
|
48
|
+
Define constraints once in type annotations — generate both valid and minimal invalid test data automatically.
|
|
49
|
+
No factories, no hardcoded fixtures, no drift when schema changes.
|
|
50
|
+
|
|
51
|
+
Conformly turns typed schemas into dictionaries for fixtures and validation tests.
|
|
52
|
+
Use the same schema for happy-path payloads and targeted negative cases.
|
|
53
|
+
|
|
54
|
+
## Key capabilities
|
|
55
|
+
|
|
56
|
+
- **Constraint-driven generation** from type annotations and model metadata.
|
|
57
|
+
- **Minimal invalid cases** targeting one field and a selected violation.
|
|
58
|
+
- **Schema as the single source of truth** for test data.
|
|
59
|
+
- Dataclasses and TypedDicts, with optional Pydantic and attrs integrations.
|
|
60
|
+
- Nested models, collections, reproducible seeds, and field overrides.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
Requires Python 3.12 or newer.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install conformly
|
|
68
|
+
# Optional integrations:
|
|
69
|
+
pip install 'conformly[pydantic]'
|
|
70
|
+
pip install 'conformly[attrs]'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quickstart
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from dataclasses import dataclass
|
|
77
|
+
from typing import Annotated
|
|
78
|
+
|
|
79
|
+
from conformly import GreaterOrEqual, MaxLength, MinLength, V, case, path
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class CreateUser:
|
|
84
|
+
username: Annotated[str, MinLength(3), MaxLength(32)]
|
|
85
|
+
age: Annotated[int, GreaterOrEqual(18)]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
payload = case(CreateUser)
|
|
89
|
+
assert 3 <= len(payload["username"]) <= 32
|
|
90
|
+
assert payload["age"] >= 18
|
|
91
|
+
|
|
92
|
+
invalid = case(
|
|
93
|
+
CreateUser, valid=False, strategy=path("age").violate(V.BELOW_MIN)
|
|
94
|
+
)
|
|
95
|
+
assert invalid["age"] < 18
|
|
96
|
+
assert 3 <= len(invalid["username"]) <= 32
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Results are dictionaries, not model instances. Invalid generation targets one
|
|
100
|
+
field; only the selected violation is guaranteed on that target. Defaults and
|
|
101
|
+
overrides are trusted rather than validated. See the
|
|
102
|
+
[invalid generation contract](https://github.com/nashabanov/conformly/blob/main/docs/strategies.md#invalid-generation-contract).
|
|
103
|
+
|
|
104
|
+
## Documentation and examples
|
|
105
|
+
|
|
106
|
+
Start with [Getting started](https://github.com/nashabanov/conformly/blob/main/docs/getting-started.md), browse the
|
|
107
|
+
[documentation index](https://github.com/nashabanov/conformly/blob/main/docs/README.md), or run the [examples](https://github.com/nashabanov/conformly/blob/main/examples/README.md).
|
|
108
|
+
The [API reference](https://github.com/nashabanov/conformly/blob/main/docs/api-reference.md) lists parameters and defaults.
|
|
109
|
+
|
|
110
|
+
## Development and contributing
|
|
111
|
+
|
|
112
|
+
See [CONTRIBUTING.md](https://github.com/nashabanov/conformly/blob/main/CONTRIBUTING.md) for setup and checks, and
|
|
113
|
+
[CHANGELOG.md](https://github.com/nashabanov/conformly/blob/main/CHANGELOG.md) for release notes and migration guidance.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT — see [LICENSE](https://github.com/nashabanov/conformly/blob/main/LICENSE).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# conformly
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml)
|
|
4
|
+
[](https://codecov.io/github/nashabanov/conformly)
|
|
5
|
+
[](https://pypi.org/project/conformly/)
|
|
6
|
+
[](https://pypi.org/project/conformly/)
|
|
7
|
+
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://github.com/nashabanov/conformly/releases)
|
|
10
|
+
[](http://mypy-lang.org/)
|
|
11
|
+
[](https://github.com/astral-sh/ruff)
|
|
12
|
+
|
|
13
|
+
**Declarative test data generator for Python.**
|
|
14
|
+
|
|
15
|
+
Define constraints once in type annotations — generate both valid and minimal invalid test data automatically.
|
|
16
|
+
No factories, no hardcoded fixtures, no drift when schema changes.
|
|
17
|
+
|
|
18
|
+
Conformly turns typed schemas into dictionaries for fixtures and validation tests.
|
|
19
|
+
Use the same schema for happy-path payloads and targeted negative cases.
|
|
20
|
+
|
|
21
|
+
## Key capabilities
|
|
22
|
+
|
|
23
|
+
- **Constraint-driven generation** from type annotations and model metadata.
|
|
24
|
+
- **Minimal invalid cases** targeting one field and a selected violation.
|
|
25
|
+
- **Schema as the single source of truth** for test data.
|
|
26
|
+
- Dataclasses and TypedDicts, with optional Pydantic and attrs integrations.
|
|
27
|
+
- Nested models, collections, reproducible seeds, and field overrides.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Requires Python 3.12 or newer.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install conformly
|
|
35
|
+
# Optional integrations:
|
|
36
|
+
pip install 'conformly[pydantic]'
|
|
37
|
+
pip install 'conformly[attrs]'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from typing import Annotated
|
|
45
|
+
|
|
46
|
+
from conformly import GreaterOrEqual, MaxLength, MinLength, V, case, path
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class CreateUser:
|
|
51
|
+
username: Annotated[str, MinLength(3), MaxLength(32)]
|
|
52
|
+
age: Annotated[int, GreaterOrEqual(18)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
payload = case(CreateUser)
|
|
56
|
+
assert 3 <= len(payload["username"]) <= 32
|
|
57
|
+
assert payload["age"] >= 18
|
|
58
|
+
|
|
59
|
+
invalid = case(
|
|
60
|
+
CreateUser, valid=False, strategy=path("age").violate(V.BELOW_MIN)
|
|
61
|
+
)
|
|
62
|
+
assert invalid["age"] < 18
|
|
63
|
+
assert 3 <= len(invalid["username"]) <= 32
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Results are dictionaries, not model instances. Invalid generation targets one
|
|
67
|
+
field; only the selected violation is guaranteed on that target. Defaults and
|
|
68
|
+
overrides are trusted rather than validated. See the
|
|
69
|
+
[invalid generation contract](https://github.com/nashabanov/conformly/blob/main/docs/strategies.md#invalid-generation-contract).
|
|
70
|
+
|
|
71
|
+
## Documentation and examples
|
|
72
|
+
|
|
73
|
+
Start with [Getting started](https://github.com/nashabanov/conformly/blob/main/docs/getting-started.md), browse the
|
|
74
|
+
[documentation index](https://github.com/nashabanov/conformly/blob/main/docs/README.md), or run the [examples](https://github.com/nashabanov/conformly/blob/main/examples/README.md).
|
|
75
|
+
The [API reference](https://github.com/nashabanov/conformly/blob/main/docs/api-reference.md) lists parameters and defaults.
|
|
76
|
+
|
|
77
|
+
## Development and contributing
|
|
78
|
+
|
|
79
|
+
See [CONTRIBUTING.md](https://github.com/nashabanov/conformly/blob/main/CONTRIBUTING.md) for setup and checks, and
|
|
80
|
+
[CHANGELOG.md](https://github.com/nashabanov/conformly/blob/main/CHANGELOG.md) for release notes and migration guidance.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT — see [LICENSE](https://github.com/nashabanov/conformly/blob/main/LICENSE).
|
|
@@ -18,7 +18,7 @@ from ._internal.constraints import (
|
|
|
18
18
|
Pattern,
|
|
19
19
|
UniqueItems,
|
|
20
20
|
)
|
|
21
|
-
from ._internal.fields import Email, IPv4, IPv6, IPvAny
|
|
21
|
+
from ._internal.fields import Email, HttpUrl, IPv4, IPv6, IPvAny, Url
|
|
22
22
|
from ._internal.types import ViolationType as V
|
|
23
23
|
from .exceptions import ConformlyError
|
|
24
24
|
|
|
@@ -27,6 +27,7 @@ __all__ = [
|
|
|
27
27
|
"Email",
|
|
28
28
|
"GreaterOrEqual",
|
|
29
29
|
"GreaterThan",
|
|
30
|
+
"HttpUrl",
|
|
30
31
|
"IPv4",
|
|
31
32
|
"IPv6",
|
|
32
33
|
"IPvAny",
|
|
@@ -40,6 +41,7 @@ __all__ = [
|
|
|
40
41
|
"OneOf",
|
|
41
42
|
"Pattern",
|
|
42
43
|
"UniqueItems",
|
|
44
|
+
"Url",
|
|
43
45
|
"V",
|
|
44
46
|
"case",
|
|
45
47
|
"cases",
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from datetime import date, datetime, time, timedelta
|
|
2
3
|
|
|
3
4
|
from .base import Constraint
|
|
4
5
|
|
|
5
6
|
TNum = int | float
|
|
7
|
+
TBound = TNum | datetime | date | time | timedelta
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
@dataclass(frozen=True, slots=True)
|
|
9
11
|
class GreaterThan(Constraint):
|
|
10
|
-
value:
|
|
12
|
+
value: TBound
|
|
11
13
|
|
|
12
14
|
def __repr__(self) -> str:
|
|
13
15
|
return f"GreaterThan(value={self.value})"
|
|
@@ -15,7 +17,7 @@ class GreaterThan(Constraint):
|
|
|
15
17
|
|
|
16
18
|
@dataclass(frozen=True, slots=True)
|
|
17
19
|
class GreaterOrEqual(Constraint):
|
|
18
|
-
value:
|
|
20
|
+
value: TBound
|
|
19
21
|
|
|
20
22
|
def __repr__(self) -> str:
|
|
21
23
|
return f"GreaterOrEqual(value={self.value})"
|
|
@@ -23,7 +25,7 @@ class GreaterOrEqual(Constraint):
|
|
|
23
25
|
|
|
24
26
|
@dataclass(frozen=True, slots=True)
|
|
25
27
|
class LessThan(Constraint):
|
|
26
|
-
value:
|
|
28
|
+
value: TBound
|
|
27
29
|
|
|
28
30
|
def __repr__(self) -> str:
|
|
29
31
|
return f"LessThan(value={self.value})"
|
|
@@ -31,7 +33,7 @@ class LessThan(Constraint):
|
|
|
31
33
|
|
|
32
34
|
@dataclass(frozen=True, slots=True)
|
|
33
35
|
class LessOrEqual(Constraint):
|
|
34
|
-
value:
|
|
36
|
+
value: TBound
|
|
35
37
|
|
|
36
38
|
def __repr__(self) -> str:
|
|
37
39
|
return f"LessOrEqual(value={self.value})"
|
|
@@ -4,7 +4,7 @@ from .registry import (
|
|
|
4
4
|
SPECIAL_STRINGS,
|
|
5
5
|
SPECIAL_TYPE_TO_KIND,
|
|
6
6
|
)
|
|
7
|
-
from .special_types import Email, IPv4, IPv6, IPvAny, SpecialString
|
|
7
|
+
from .special_types import Email, HttpUrl, IPv4, IPv6, IPvAny, SpecialString, Url
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"SPECIAL_KINDS",
|
|
@@ -12,8 +12,10 @@ __all__ = [
|
|
|
12
12
|
"SPECIAL_STRINGS",
|
|
13
13
|
"SPECIAL_TYPE_TO_KIND",
|
|
14
14
|
"Email",
|
|
15
|
+
"HttpUrl",
|
|
15
16
|
"IPv4",
|
|
16
17
|
"IPv6",
|
|
17
18
|
"IPvAny",
|
|
18
19
|
"SpecialString",
|
|
20
|
+
"Url",
|
|
19
21
|
]
|
|
@@ -2,6 +2,7 @@ from ._errors import generation_error
|
|
|
2
2
|
from .protocol import TypeGeneratorProtocol
|
|
3
3
|
from .types import (
|
|
4
4
|
boolean,
|
|
5
|
+
bytes,
|
|
5
6
|
dictionaries,
|
|
6
7
|
email,
|
|
7
8
|
enum,
|
|
@@ -12,6 +13,7 @@ from .types import (
|
|
|
12
13
|
ipvany,
|
|
13
14
|
list,
|
|
14
15
|
string,
|
|
16
|
+
temporal,
|
|
15
17
|
tuple,
|
|
16
18
|
url,
|
|
17
19
|
uuid,
|
|
@@ -21,6 +23,7 @@ from conformly._internal.types import FieldKind
|
|
|
21
23
|
|
|
22
24
|
_GENERATORS: dict[FieldKind, TypeGeneratorProtocol] = {
|
|
23
25
|
FieldKind.STRING: string,
|
|
26
|
+
FieldKind.BYTES: bytes,
|
|
24
27
|
FieldKind.BOOLEAN: boolean,
|
|
25
28
|
FieldKind.FLOAT: float,
|
|
26
29
|
FieldKind.INTEGER: integer,
|
|
@@ -33,12 +36,17 @@ _GENERATORS: dict[FieldKind, TypeGeneratorProtocol] = {
|
|
|
33
36
|
FieldKind.DICT: dictionaries,
|
|
34
37
|
FieldKind.TUPLE: tuple,
|
|
35
38
|
FieldKind.UUID: uuid,
|
|
39
|
+
FieldKind.DATETIME: temporal,
|
|
40
|
+
FieldKind.DATE: temporal,
|
|
41
|
+
FieldKind.TIME: temporal,
|
|
42
|
+
FieldKind.TIMEDELTA: temporal,
|
|
36
43
|
FieldKind.URL: url,
|
|
37
44
|
FieldKind.HTTPURL: url,
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
_MISMATCH_MAPPING: dict[FieldKind, FieldKind] = {
|
|
41
48
|
FieldKind.STRING: FieldKind.INTEGER,
|
|
49
|
+
FieldKind.BYTES: FieldKind.INTEGER,
|
|
42
50
|
FieldKind.BOOLEAN: FieldKind.STRING,
|
|
43
51
|
FieldKind.INTEGER: FieldKind.STRING,
|
|
44
52
|
FieldKind.FLOAT: FieldKind.STRING,
|
|
@@ -51,6 +59,10 @@ _MISMATCH_MAPPING: dict[FieldKind, FieldKind] = {
|
|
|
51
59
|
FieldKind.LIST: FieldKind.INTEGER,
|
|
52
60
|
FieldKind.DICT: FieldKind.STRING,
|
|
53
61
|
FieldKind.UUID: FieldKind.FLOAT,
|
|
62
|
+
FieldKind.DATETIME: FieldKind.STRING,
|
|
63
|
+
FieldKind.DATE: FieldKind.STRING,
|
|
64
|
+
FieldKind.TIME: FieldKind.STRING,
|
|
65
|
+
FieldKind.TIMEDELTA: FieldKind.STRING,
|
|
54
66
|
FieldKind.URL: FieldKind.FLOAT,
|
|
55
67
|
FieldKind.HTTPURL: FieldKind.INTEGER,
|
|
56
68
|
FieldKind.TUPLE: FieldKind.INTEGER,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .._errors import generation_error
|
|
2
|
+
from ..context import GenerationContext
|
|
3
|
+
|
|
4
|
+
from conformly._internal.resolver.semantics import BytesSemantic
|
|
5
|
+
from conformly._internal.types import ViolationType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_value(
|
|
9
|
+
ctx: GenerationContext, semantic: BytesSemantic, violation: ViolationType | None
|
|
10
|
+
) -> bytes:
|
|
11
|
+
lower = semantic.length_range.min_length
|
|
12
|
+
upper = semantic.length_range.max_length
|
|
13
|
+
if violation is None:
|
|
14
|
+
length = ctx.rng.randint(lower, upper if upper is not None else lower + 50)
|
|
15
|
+
elif violation == ViolationType.TOO_SHORT and lower > 0:
|
|
16
|
+
length = lower - 1
|
|
17
|
+
elif violation == ViolationType.TOO_LONG and upper is not None:
|
|
18
|
+
length = upper + 1
|
|
19
|
+
else:
|
|
20
|
+
raise generation_error(
|
|
21
|
+
"Unsupported bytes violation",
|
|
22
|
+
code="unsupported_violation",
|
|
23
|
+
violation=violation.value,
|
|
24
|
+
)
|
|
25
|
+
return ctx.rng.randbytes(length)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from datetime import UTC, date, datetime, time, timedelta
|
|
2
|
+
|
|
3
|
+
from .._errors import generation_error
|
|
4
|
+
from ..context import GenerationContext
|
|
5
|
+
|
|
6
|
+
from conformly._internal.resolver.semantics import TemporalSemantic
|
|
7
|
+
from conformly._internal.resolver.semantics.temporal import MICROSECONDS_PER_DAY
|
|
8
|
+
from conformly._internal.types import FieldKind, ViolationType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_value(
|
|
12
|
+
ctx: GenerationContext, semantic: TemporalSemantic, violation: ViolationType | None
|
|
13
|
+
) -> date | time | timedelta:
|
|
14
|
+
if violation is None:
|
|
15
|
+
tick = ctx.rng.randint(semantic.min_value, semantic.max_value)
|
|
16
|
+
elif (
|
|
17
|
+
violation == ViolationType.BELOW_MIN
|
|
18
|
+
and semantic.min_value > semantic.domain_min
|
|
19
|
+
):
|
|
20
|
+
tick = semantic.min_value - 1
|
|
21
|
+
elif (
|
|
22
|
+
violation == ViolationType.ABOVE_MAX
|
|
23
|
+
and semantic.max_value < semantic.domain_max
|
|
24
|
+
):
|
|
25
|
+
tick = semantic.max_value + 1
|
|
26
|
+
else:
|
|
27
|
+
raise generation_error(
|
|
28
|
+
"No representable temporal violation available",
|
|
29
|
+
code="unsupported_temporal_violation",
|
|
30
|
+
violation=violation.value,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
match semantic.kind:
|
|
34
|
+
case FieldKind.DATE:
|
|
35
|
+
return date.fromordinal(tick)
|
|
36
|
+
case FieldKind.TIME:
|
|
37
|
+
return _time_from_tick(tick)
|
|
38
|
+
case FieldKind.DATETIME:
|
|
39
|
+
day, remainder = divmod(tick, MICROSECONDS_PER_DAY)
|
|
40
|
+
return datetime.combine(
|
|
41
|
+
date.fromordinal(day + 1),
|
|
42
|
+
_time_from_tick(remainder),
|
|
43
|
+
tzinfo=UTC if semantic.aware else None,
|
|
44
|
+
)
|
|
45
|
+
case FieldKind.TIMEDELTA:
|
|
46
|
+
return timedelta(microseconds=tick)
|
|
47
|
+
case _:
|
|
48
|
+
raise generation_error(
|
|
49
|
+
"Unsupported temporal kind",
|
|
50
|
+
code="unsupported_temporal_kind",
|
|
51
|
+
kind=semantic.kind.value,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _time_from_tick(tick: int) -> time:
|
|
56
|
+
seconds, microsecond = divmod(tick, 1_000_000)
|
|
57
|
+
hour, seconds = divmod(seconds, 3600)
|
|
58
|
+
minute, second = divmod(seconds, 60)
|
|
59
|
+
return time(hour, minute, second, microsecond)
|
{conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/extractors/constraints.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
|
+
from datetime import date, datetime, time, timedelta
|
|
2
3
|
from typing import Annotated, Any, cast, get_args, get_origin
|
|
3
4
|
|
|
4
5
|
from conformly._internal.constraints import (
|
|
@@ -121,6 +122,11 @@ def _coerce_constraint_value(k: ConstraintType, v: Any) -> Any:
|
|
|
121
122
|
},
|
|
122
123
|
)
|
|
123
124
|
|
|
125
|
+
if k in ("gt", "ge", "lt", "le") and isinstance(
|
|
126
|
+
v, (date, datetime, time, timedelta)
|
|
127
|
+
):
|
|
128
|
+
return v
|
|
129
|
+
|
|
124
130
|
if k in NUMERIC_CONSTRAINTS:
|
|
125
131
|
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
126
132
|
return v
|
|
@@ -7,12 +7,14 @@ from conformly._internal.fields import SPECIAL_KINDS
|
|
|
7
7
|
from conformly._internal.resolver import ResolvedModel
|
|
8
8
|
from conformly._internal.resolver.semantics import (
|
|
9
9
|
BooleanSemantic,
|
|
10
|
+
BytesSemantic,
|
|
10
11
|
EnumSemantic,
|
|
11
12
|
FieldSemantics,
|
|
12
13
|
ListSemantic,
|
|
13
14
|
NumericSemantic,
|
|
14
15
|
ObjectSemantic,
|
|
15
16
|
StringSemantic,
|
|
17
|
+
TemporalSemantic,
|
|
16
18
|
TupleSemantic,
|
|
17
19
|
UUIDSemantic,
|
|
18
20
|
)
|
|
@@ -131,6 +133,13 @@ def _define_allowed_violation_types(
|
|
|
131
133
|
case StringSemantic(kind=kind) if kind in SPECIAL_KINDS | {FieldKind.STRING}:
|
|
132
134
|
violations = _define_string_violations(semantic)
|
|
133
135
|
|
|
136
|
+
case BytesSemantic():
|
|
137
|
+
violations = []
|
|
138
|
+
if semantic.length_range.min_length > 0:
|
|
139
|
+
violations.append(ViolationType.TOO_SHORT)
|
|
140
|
+
if semantic.length_range.max_length is not None:
|
|
141
|
+
violations.append(ViolationType.TOO_LONG)
|
|
142
|
+
|
|
134
143
|
case NumericSemantic(kind=(FieldKind.INTEGER | FieldKind.FLOAT)):
|
|
135
144
|
violations = _define_numeric_violations(semantic)
|
|
136
145
|
|
|
@@ -152,6 +161,13 @@ def _define_allowed_violation_types(
|
|
|
152
161
|
ViolationType.WRONG_UUID_CHARACTER,
|
|
153
162
|
]
|
|
154
163
|
|
|
164
|
+
case TemporalSemantic():
|
|
165
|
+
violations = []
|
|
166
|
+
if semantic.min_value > semantic.domain_min:
|
|
167
|
+
violations.append(ViolationType.BELOW_MIN)
|
|
168
|
+
if semantic.max_value < semantic.domain_max:
|
|
169
|
+
violations.append(ViolationType.ABOVE_MAX)
|
|
170
|
+
|
|
155
171
|
case (
|
|
156
172
|
ObjectSemantic(kind=FieldKind.OBJECT)
|
|
157
173
|
| BooleanSemantic(kind=FieldKind.BOOLEAN)
|
|
@@ -17,6 +17,8 @@ from .semantics import (
|
|
|
17
17
|
TupleSemantic,
|
|
18
18
|
UUIDSemantic,
|
|
19
19
|
)
|
|
20
|
+
from .semantics.bytes import create_bytes_semantic
|
|
21
|
+
from .semantics.temporal import TEMPORAL_TYPES, create_temporal_semantic
|
|
20
22
|
|
|
21
23
|
from conformly._internal.constraints import (
|
|
22
24
|
Constraint,
|
|
@@ -221,6 +223,10 @@ def create_scalar_semantic(
|
|
|
221
223
|
|
|
222
224
|
return create_string_semantic(c, kind)
|
|
223
225
|
|
|
226
|
+
for kind, temporal_type in TEMPORAL_TYPES.items():
|
|
227
|
+
if t is temporal_type:
|
|
228
|
+
return create_temporal_semantic(kind, c)
|
|
229
|
+
|
|
224
230
|
if t is uuid.UUID:
|
|
225
231
|
return UUIDSemantic(has_constraints=True)
|
|
226
232
|
|
|
@@ -236,6 +242,9 @@ def create_scalar_semantic(
|
|
|
236
242
|
multiple_of=_extract_numeric_multiple_of(c),
|
|
237
243
|
)
|
|
238
244
|
|
|
245
|
+
elif t is bytes:
|
|
246
|
+
return create_bytes_semantic(c)
|
|
247
|
+
|
|
239
248
|
elif t is str:
|
|
240
249
|
return create_string_semantic(c)
|
|
241
250
|
|
|
@@ -351,6 +360,11 @@ def _calculate_int_bounds(constraints: tuple[Constraint, ...]) -> Range:
|
|
|
351
360
|
)
|
|
352
361
|
|
|
353
362
|
v = c.value
|
|
363
|
+
if not isinstance(v, (int, float)):
|
|
364
|
+
raise SchemaError(
|
|
365
|
+
"Numeric bounds require numeric values",
|
|
366
|
+
context={"code": "invalid_numeric_bound_type"},
|
|
367
|
+
)
|
|
354
368
|
|
|
355
369
|
match c:
|
|
356
370
|
case GreaterThan():
|
|
@@ -382,6 +396,11 @@ def _calculate_float_bounds(constraints: tuple[Constraint, ...]) -> Range:
|
|
|
382
396
|
if not isinstance(c, (GreaterThan, GreaterOrEqual, LessThan, LessOrEqual)):
|
|
383
397
|
continue
|
|
384
398
|
|
|
399
|
+
if not isinstance(c.value, (int, float)):
|
|
400
|
+
raise SchemaError(
|
|
401
|
+
"Numeric bounds require numeric values",
|
|
402
|
+
context={"code": "invalid_numeric_bound_type"},
|
|
403
|
+
)
|
|
385
404
|
v = float(c.value)
|
|
386
405
|
if math.isnan(v):
|
|
387
406
|
raise SchemaError(
|
|
@@ -552,7 +571,7 @@ def create_list_semantics(
|
|
|
552
571
|
element_nested_model=element_nested_model,
|
|
553
572
|
length_range=length_range,
|
|
554
573
|
is_unique_items=is_unique_items,
|
|
555
|
-
has_constraints=has_constraints,
|
|
574
|
+
has_constraints=has_constraints or element_semantic.has_constraints,
|
|
556
575
|
)
|
|
557
576
|
|
|
558
577
|
|
|
@@ -601,7 +620,11 @@ def create_dict_semantic(
|
|
|
601
620
|
value_nested_model=value_nested_model,
|
|
602
621
|
length_range=length_range,
|
|
603
622
|
is_unique_items=False,
|
|
604
|
-
has_constraints=
|
|
623
|
+
has_constraints=(
|
|
624
|
+
has_constraints
|
|
625
|
+
or key_semantic.has_constraints
|
|
626
|
+
or value_semantic.has_constraints
|
|
627
|
+
),
|
|
605
628
|
)
|
|
606
629
|
|
|
607
630
|
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
from .boolean import BooleanSemantic
|
|
2
|
+
from .bytes import BytesSemantic
|
|
2
3
|
from .dict import DictSemantic
|
|
3
4
|
from .enum import EnumSemantic
|
|
4
5
|
from .list import ListSemantic
|
|
5
6
|
from .numeric import NumericSemantic
|
|
6
7
|
from .object import ObjectSemantic
|
|
7
8
|
from .string import StringSemantic
|
|
9
|
+
from .temporal import TemporalSemantic
|
|
8
10
|
from .tuple import TupleSemantic
|
|
9
11
|
from .uuid import UUIDSemantic
|
|
10
12
|
|
|
11
13
|
FieldSemantics = (
|
|
12
14
|
NumericSemantic
|
|
13
15
|
| StringSemantic
|
|
16
|
+
| BytesSemantic
|
|
14
17
|
| ObjectSemantic
|
|
15
18
|
| BooleanSemantic
|
|
16
19
|
| EnumSemantic
|
|
17
20
|
| ListSemantic
|
|
18
21
|
| UUIDSemantic
|
|
19
22
|
| DictSemantic
|
|
23
|
+
| TemporalSemantic
|
|
20
24
|
| TupleSemantic
|
|
21
25
|
)
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
__all__ = [
|
|
25
29
|
"BooleanSemantic",
|
|
30
|
+
"BytesSemantic",
|
|
26
31
|
"DictSemantic",
|
|
27
32
|
"EnumSemantic",
|
|
28
33
|
"FieldSemantics",
|
|
@@ -30,6 +35,7 @@ __all__ = [
|
|
|
30
35
|
"NumericSemantic",
|
|
31
36
|
"ObjectSemantic",
|
|
32
37
|
"StringSemantic",
|
|
38
|
+
"TemporalSemantic",
|
|
33
39
|
"TupleSemantic",
|
|
34
40
|
"UUIDSemantic",
|
|
35
41
|
]
|
|
@@ -2,11 +2,17 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
from . import BooleanSemantic, EnumSemantic, FieldSemantics, NumericSemantic
|
|
4
4
|
from ._numeric import integer_multiple_step
|
|
5
|
+
from .bytes import BytesSemantic
|
|
6
|
+
from .temporal import TemporalSemantic
|
|
5
7
|
|
|
6
8
|
from conformly._internal.types import FieldKind
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
def finite_cardinality(semantic: FieldSemantics) -> int | None:
|
|
12
|
+
if isinstance(semantic, BytesSemantic) and semantic.length_range.max_length == 0:
|
|
13
|
+
return 1
|
|
14
|
+
if isinstance(semantic, TemporalSemantic):
|
|
15
|
+
return semantic.max_value - semantic.min_value + 1
|
|
10
16
|
if isinstance(semantic, BooleanSemantic):
|
|
11
17
|
return 2
|
|
12
18
|
if isinstance(semantic, EnumSemantic):
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .base import BaseSemantic
|
|
4
|
+
|
|
5
|
+
from conformly._internal.constraints import Constraint, MaxLength, MinLength
|
|
6
|
+
from conformly._internal.types import FieldKind, LengthRange
|
|
7
|
+
from conformly.exceptions import SchemaError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
11
|
+
class BytesSemantic(BaseSemantic):
|
|
12
|
+
length_range: LengthRange
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def kind(self) -> FieldKind:
|
|
16
|
+
return FieldKind.BYTES
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_bytes_semantic(constraints: tuple[Constraint, ...]) -> BytesSemantic:
|
|
20
|
+
lower = 0
|
|
21
|
+
upper: int | None = None
|
|
22
|
+
for constraint in constraints:
|
|
23
|
+
if not isinstance(constraint, (MinLength, MaxLength)):
|
|
24
|
+
raise SchemaError(
|
|
25
|
+
"Bytes fields only support MinLength and MaxLength",
|
|
26
|
+
context={
|
|
27
|
+
"code": "unsupported_bytes_constraint",
|
|
28
|
+
"constraint": type(constraint).__name__,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
value = constraint.value
|
|
32
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
33
|
+
raise SchemaError(
|
|
34
|
+
"Bytes length must be a non-negative integer",
|
|
35
|
+
context={"code": "invalid_bytes_length", "value": value},
|
|
36
|
+
)
|
|
37
|
+
if isinstance(constraint, MinLength):
|
|
38
|
+
lower = max(lower, value)
|
|
39
|
+
else:
|
|
40
|
+
upper = value if upper is None else min(upper, value)
|
|
41
|
+
if upper is not None and lower > upper:
|
|
42
|
+
raise SchemaError(
|
|
43
|
+
"Invalid bytes length range",
|
|
44
|
+
context={"code": "invalid_length_range", "min": lower, "max": upper},
|
|
45
|
+
)
|
|
46
|
+
return BytesSemantic(
|
|
47
|
+
length_range=LengthRange(lower, upper), has_constraints=bool(constraints)
|
|
48
|
+
)
|