conformly 0.7.4__tar.gz → 0.9.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.9.0/PKG-INFO +113 -0
- conformly-0.9.0/README.md +80 -0
- {conformly-0.7.4 → conformly-0.9.0}/pyproject.toml +1 -1
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/__init__.py +3 -1
- conformly-0.9.0/src/conformly/_internal/api/_utils.py +162 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/case.py +21 -26
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/cases.py +37 -57
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/path.py +5 -26
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/numeric.py +6 -4
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/fields/__init__.py +3 -1
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/orchestration.py +25 -8
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/registry.py +12 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/_utils.py +6 -2
- conformly-0.9.0/src/conformly/_internal/generator/types/bytes.py +25 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/dictionaries.py +21 -9
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/list.py +42 -13
- conformly-0.9.0/src/conformly/_internal/generator/types/temporal.py +59 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/tuple.py +17 -6
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/extractors/constraints.py +6 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/field.py +16 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/session.py +25 -28
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/resolve.py +25 -2
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/__init__.py +6 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_domains.py +6 -0
- conformly-0.9.0/src/conformly/_internal/resolver/semantics/bytes.py +48 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/factory.py +6 -0
- conformly-0.9.0/src/conformly/_internal/resolver/semantics/temporal.py +139 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/primitives.py +5 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/strategies.py +2 -2
- conformly-0.9.0/src/conformly.egg-info/PKG-INFO +113 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/SOURCES.txt +4 -0
- {conformly-0.7.4 → conformly-0.9.0}/tests/test_path.py +28 -31
- conformly-0.7.4/PKG-INFO +0 -792
- conformly-0.7.4/README.md +0 -759
- conformly-0.7.4/src/conformly/_internal/api/_utils.py +0 -87
- conformly-0.7.4/src/conformly.egg-info/PKG-INFO +0 -792
- {conformly-0.7.4 → conformly-0.9.0}/LICENSE +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/setup.cfg +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/_errors.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/_path_proxy.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/base.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/collections.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/enum.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/keys.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/mapping.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/string.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/fields/registry.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/fields/special_types.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/_errors.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/context.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/protocol.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/boolean.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/email.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/enum.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/float.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/integer.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/ipv4.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/ipv6.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/ipvany.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/string.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/url.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/uuid.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/registry.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/api.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/core/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/core/element.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/core/field.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/extractors/types.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/models.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/_errors.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/model.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/models.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_numeric.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_regex.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/base.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/list.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/object.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/string.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/tracer.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/constants.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/values.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/exceptions.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/py.typed +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly/tracer/__init__.py +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/dependency_links.txt +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/requires.txt +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/top_level.txt +0 -0
- {conformly-0.7.4 → conformly-0.9.0}/tests/test_collection.py +0 -0
conformly-0.9.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: conformly
|
|
3
|
+
Version: 0.9.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
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
42
|
+
**Declarative test data generator for Python.**
|
|
43
|
+
|
|
44
|
+
Define constraints once in type annotations — generate both valid and minimal invalid test data automatically.
|
|
45
|
+
No factories, no hardcoded fixtures, no drift when schema changes.
|
|
46
|
+
|
|
47
|
+
Conformly turns typed schemas into dictionaries for fixtures and validation tests.
|
|
48
|
+
Use the same schema for happy-path payloads and targeted negative cases.
|
|
49
|
+
|
|
50
|
+
## Key capabilities
|
|
51
|
+
|
|
52
|
+
- **Constraint-driven generation** from type annotations and model metadata.
|
|
53
|
+
- **Minimal invalid cases** targeting one field and a selected violation.
|
|
54
|
+
- **Schema as the single source of truth** for test data.
|
|
55
|
+
- Dataclasses and TypedDicts, with optional Pydantic and attrs integrations.
|
|
56
|
+
- Nested models, collections, reproducible seeds, and field overrides.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
Requires Python 3.12 or newer.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install conformly
|
|
64
|
+
# Optional integrations:
|
|
65
|
+
pip install 'conformly[pydantic]'
|
|
66
|
+
pip install 'conformly[attrs]'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quickstart
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from dataclasses import dataclass
|
|
73
|
+
from typing import Annotated
|
|
74
|
+
|
|
75
|
+
from conformly import GreaterOrEqual, MaxLength, MinLength, V, case, path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class CreateUser:
|
|
80
|
+
username: Annotated[str, MinLength(3), MaxLength(32)]
|
|
81
|
+
age: Annotated[int, GreaterOrEqual(18)]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
payload = case(CreateUser)
|
|
85
|
+
assert 3 <= len(payload["username"]) <= 32
|
|
86
|
+
assert payload["age"] >= 18
|
|
87
|
+
|
|
88
|
+
invalid = case(
|
|
89
|
+
CreateUser, valid=False, target=path("age").violate(V.BELOW_MIN)
|
|
90
|
+
)
|
|
91
|
+
assert invalid["age"] < 18
|
|
92
|
+
assert 3 <= len(invalid["username"]) <= 32
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Results are dictionaries, not model instances. Invalid generation targets one
|
|
96
|
+
field; only the selected violation is guaranteed on that target. Defaults and
|
|
97
|
+
overrides are trusted rather than validated. See the
|
|
98
|
+
[invalid generation contract](https://github.com/nashabanov/conformly/blob/main/docs/strategies.md#invalid-generation-contract).
|
|
99
|
+
|
|
100
|
+
## Documentation and examples
|
|
101
|
+
|
|
102
|
+
Start with [Getting started](https://github.com/nashabanov/conformly/blob/main/docs/getting-started.md), browse the
|
|
103
|
+
[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).
|
|
104
|
+
The [API reference](https://github.com/nashabanov/conformly/blob/main/docs/api-reference.md) lists parameters and defaults.
|
|
105
|
+
|
|
106
|
+
## Development and contributing
|
|
107
|
+
|
|
108
|
+
See [CONTRIBUTING.md](https://github.com/nashabanov/conformly/blob/main/CONTRIBUTING.md) for setup and checks, and
|
|
109
|
+
[CHANGELOG.md](https://github.com/nashabanov/conformly/blob/main/CHANGELOG.md) for release notes and migration guidance.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT — see [LICENSE](https://github.com/nashabanov/conformly/blob/main/LICENSE).
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
**Declarative test data generator for Python.**
|
|
10
|
+
|
|
11
|
+
Define constraints once in type annotations — generate both valid and minimal invalid test data automatically.
|
|
12
|
+
No factories, no hardcoded fixtures, no drift when schema changes.
|
|
13
|
+
|
|
14
|
+
Conformly turns typed schemas into dictionaries for fixtures and validation tests.
|
|
15
|
+
Use the same schema for happy-path payloads and targeted negative cases.
|
|
16
|
+
|
|
17
|
+
## Key capabilities
|
|
18
|
+
|
|
19
|
+
- **Constraint-driven generation** from type annotations and model metadata.
|
|
20
|
+
- **Minimal invalid cases** targeting one field and a selected violation.
|
|
21
|
+
- **Schema as the single source of truth** for test data.
|
|
22
|
+
- Dataclasses and TypedDicts, with optional Pydantic and attrs integrations.
|
|
23
|
+
- Nested models, collections, reproducible seeds, and field overrides.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Requires Python 3.12 or newer.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install conformly
|
|
31
|
+
# Optional integrations:
|
|
32
|
+
pip install 'conformly[pydantic]'
|
|
33
|
+
pip install 'conformly[attrs]'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
from typing import Annotated
|
|
41
|
+
|
|
42
|
+
from conformly import GreaterOrEqual, MaxLength, MinLength, V, case, path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class CreateUser:
|
|
47
|
+
username: Annotated[str, MinLength(3), MaxLength(32)]
|
|
48
|
+
age: Annotated[int, GreaterOrEqual(18)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
payload = case(CreateUser)
|
|
52
|
+
assert 3 <= len(payload["username"]) <= 32
|
|
53
|
+
assert payload["age"] >= 18
|
|
54
|
+
|
|
55
|
+
invalid = case(
|
|
56
|
+
CreateUser, valid=False, target=path("age").violate(V.BELOW_MIN)
|
|
57
|
+
)
|
|
58
|
+
assert invalid["age"] < 18
|
|
59
|
+
assert 3 <= len(invalid["username"]) <= 32
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Results are dictionaries, not model instances. Invalid generation targets one
|
|
63
|
+
field; only the selected violation is guaranteed on that target. Defaults and
|
|
64
|
+
overrides are trusted rather than validated. See the
|
|
65
|
+
[invalid generation contract](https://github.com/nashabanov/conformly/blob/main/docs/strategies.md#invalid-generation-contract).
|
|
66
|
+
|
|
67
|
+
## Documentation and examples
|
|
68
|
+
|
|
69
|
+
Start with [Getting started](https://github.com/nashabanov/conformly/blob/main/docs/getting-started.md), browse the
|
|
70
|
+
[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).
|
|
71
|
+
The [API reference](https://github.com/nashabanov/conformly/blob/main/docs/api-reference.md) lists parameters and defaults.
|
|
72
|
+
|
|
73
|
+
## Development and contributing
|
|
74
|
+
|
|
75
|
+
See [CONTRIBUTING.md](https://github.com/nashabanov/conformly/blob/main/CONTRIBUTING.md) for setup and checks, and
|
|
76
|
+
[CHANGELOG.md](https://github.com/nashabanov/conformly/blob/main/CHANGELOG.md) for release notes and migration guidance.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
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",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ._errors import api_error
|
|
4
|
+
from .path import PathSelector
|
|
5
|
+
|
|
6
|
+
from conformly._internal.generator import GenerationContext
|
|
7
|
+
from conformly._internal.parser import ModelSpec, parse_model
|
|
8
|
+
from conformly._internal.planner import PlannedTask, plan_violation_task, select_paths
|
|
9
|
+
from conformly._internal.resolver import ResolvedModel, resolve_model
|
|
10
|
+
from conformly._internal.types import CasesStrategy, FieldPath
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ensure_model_or_spec(
|
|
14
|
+
model_or_spec: ModelSpec | ResolvedModel | type,
|
|
15
|
+
) -> ResolvedModel:
|
|
16
|
+
if isinstance(model_or_spec, ResolvedModel):
|
|
17
|
+
return model_or_spec
|
|
18
|
+
|
|
19
|
+
if isinstance(model_or_spec, ModelSpec):
|
|
20
|
+
return resolve_model(model_or_spec)
|
|
21
|
+
|
|
22
|
+
return resolve_model(parse_model(model_or_spec))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_overrides(
|
|
26
|
+
model: ResolvedModel, overrides: list[PathSelector] | None
|
|
27
|
+
) -> dict[FieldPath, Any] | None:
|
|
28
|
+
if overrides is None:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
normalized_overrides: dict[FieldPath, Any] = {}
|
|
32
|
+
|
|
33
|
+
for v in overrides:
|
|
34
|
+
if (
|
|
35
|
+
not isinstance(v, PathSelector)
|
|
36
|
+
or not v.has_override
|
|
37
|
+
or v.forced_violation is not None
|
|
38
|
+
):
|
|
39
|
+
raise api_error(
|
|
40
|
+
"Overrides must be selectors configured with .set(value)",
|
|
41
|
+
code="invalid_override_selector",
|
|
42
|
+
path=v.raw_path if isinstance(v, PathSelector) else None,
|
|
43
|
+
)
|
|
44
|
+
if v.raw_path not in model.name_to_path:
|
|
45
|
+
raise api_error(
|
|
46
|
+
"Override path not found",
|
|
47
|
+
code="invalid_override_path",
|
|
48
|
+
path=v.raw_path,
|
|
49
|
+
available=list(model.name_to_path),
|
|
50
|
+
)
|
|
51
|
+
path = model.name_to_path[v.raw_path]
|
|
52
|
+
normalized_overrides[path] = v.override
|
|
53
|
+
|
|
54
|
+
return normalized_overrides
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_selection(
|
|
58
|
+
*,
|
|
59
|
+
valid: bool,
|
|
60
|
+
strategy: CasesStrategy,
|
|
61
|
+
target: PathSelector | None,
|
|
62
|
+
allow_all: bool,
|
|
63
|
+
count: int = 1,
|
|
64
|
+
) -> None:
|
|
65
|
+
if valid and strategy != "first":
|
|
66
|
+
raise api_error(
|
|
67
|
+
"Strategy selection is only applicable when valid=False",
|
|
68
|
+
code="invalid_strategy_for_valid",
|
|
69
|
+
strategy=strategy,
|
|
70
|
+
)
|
|
71
|
+
supported = (
|
|
72
|
+
("first", "random", "all", "all_violations")
|
|
73
|
+
if allow_all
|
|
74
|
+
else ("first", "random")
|
|
75
|
+
)
|
|
76
|
+
if not isinstance(strategy, str) or strategy not in supported:
|
|
77
|
+
raise api_error(
|
|
78
|
+
"Unsupported strategy",
|
|
79
|
+
code="unsupported_strategy",
|
|
80
|
+
function="cases" if allow_all else "case",
|
|
81
|
+
strategy=strategy,
|
|
82
|
+
available=list(supported),
|
|
83
|
+
)
|
|
84
|
+
if target is not None:
|
|
85
|
+
if not isinstance(target, PathSelector):
|
|
86
|
+
raise api_error(
|
|
87
|
+
"Target must be a PathSelector; use path('field')",
|
|
88
|
+
code="invalid_target",
|
|
89
|
+
)
|
|
90
|
+
if valid:
|
|
91
|
+
raise api_error(
|
|
92
|
+
"Target selection is only applicable when valid=False",
|
|
93
|
+
code="invalid_target_for_valid",
|
|
94
|
+
target=target.raw_path,
|
|
95
|
+
)
|
|
96
|
+
if strategy != "first":
|
|
97
|
+
raise api_error(
|
|
98
|
+
"Explicit targets require strategy='first'",
|
|
99
|
+
code="invalid_strategy_for_target",
|
|
100
|
+
strategy=strategy,
|
|
101
|
+
target=target.raw_path,
|
|
102
|
+
)
|
|
103
|
+
if count != 1:
|
|
104
|
+
raise api_error(
|
|
105
|
+
"Explicit targets require count=1",
|
|
106
|
+
code="invalid_count_for_target",
|
|
107
|
+
count=count,
|
|
108
|
+
target=target.raw_path,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def plan_tasks(
|
|
113
|
+
model: ResolvedModel,
|
|
114
|
+
*,
|
|
115
|
+
ctx: GenerationContext,
|
|
116
|
+
strategy: CasesStrategy,
|
|
117
|
+
target: PathSelector | None = None,
|
|
118
|
+
allow_all: bool,
|
|
119
|
+
count: int = 1,
|
|
120
|
+
allow_type_mismatch: bool = False,
|
|
121
|
+
allow_structural_violations: bool = False,
|
|
122
|
+
split_by_violations: bool = False,
|
|
123
|
+
) -> list[PlannedTask]:
|
|
124
|
+
forced_violation = target.forced_violation if target is not None else None
|
|
125
|
+
|
|
126
|
+
paths = select_paths(
|
|
127
|
+
model,
|
|
128
|
+
ctx=ctx,
|
|
129
|
+
strategy=strategy,
|
|
130
|
+
target=target.raw_path if target is not None else None,
|
|
131
|
+
forced_violation=forced_violation,
|
|
132
|
+
allow_all=allow_all,
|
|
133
|
+
count=count,
|
|
134
|
+
allow_type_mismatch=allow_type_mismatch,
|
|
135
|
+
allow_structural_violations=allow_structural_violations,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if split_by_violations:
|
|
139
|
+
return [
|
|
140
|
+
plan_violation_task(
|
|
141
|
+
model,
|
|
142
|
+
path,
|
|
143
|
+
allow_type_mismatch,
|
|
144
|
+
allow_structural_violations,
|
|
145
|
+
forced_violation=violation,
|
|
146
|
+
)
|
|
147
|
+
for path in paths
|
|
148
|
+
for violation in plan_violation_task(
|
|
149
|
+
model, path, allow_type_mismatch, allow_structural_violations
|
|
150
|
+
).allowed_violations
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
return [
|
|
154
|
+
plan_violation_task(
|
|
155
|
+
model=model,
|
|
156
|
+
path=path,
|
|
157
|
+
allow_type_mismatch=allow_type_mismatch,
|
|
158
|
+
allow_structural_violations=allow_structural_violations,
|
|
159
|
+
forced_violation=forced_violation,
|
|
160
|
+
)
|
|
161
|
+
for path in paths
|
|
162
|
+
]
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
from ._errors import api_error
|
|
4
|
-
from ._utils import
|
|
4
|
+
from ._utils import (
|
|
5
|
+
ensure_model_or_spec,
|
|
6
|
+
normalize_overrides,
|
|
7
|
+
plan_tasks,
|
|
8
|
+
validate_selection,
|
|
9
|
+
)
|
|
5
10
|
from .path import PathSelector
|
|
6
11
|
|
|
7
12
|
from conformly._internal.generator.context import create_context
|
|
@@ -16,7 +21,8 @@ def case(
|
|
|
16
21
|
*,
|
|
17
22
|
valid: bool = True,
|
|
18
23
|
seed: int | None = None,
|
|
19
|
-
strategy: CaseStrategy
|
|
24
|
+
strategy: CaseStrategy = "first",
|
|
25
|
+
target: PathSelector | None = None,
|
|
20
26
|
overrides: list[PathSelector] | None = None,
|
|
21
27
|
allow_type_mismatch: bool = False,
|
|
22
28
|
tracer: Tracer | None = None,
|
|
@@ -52,16 +58,10 @@ def case(
|
|
|
52
58
|
|
|
53
59
|
- "random": violate a random constrained field
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- "field_name::violation":
|
|
60
|
-
violate a specific field with specific type
|
|
61
|
-
(e.g. strategy="user.profile::below_min")
|
|
62
|
-
|
|
63
|
-
- PathSelector DSL:
|
|
64
|
-
path("user.email").violate(V.TOO_SHORT)
|
|
61
|
+
target:
|
|
62
|
+
Optional selector from path("field") or path(Model, lambda m: m.field).
|
|
63
|
+
Only available with valid=False and strategy="first".
|
|
64
|
+
Use .violate(V.X) to request an exact violation.
|
|
65
65
|
|
|
66
66
|
overrides:
|
|
67
67
|
Optional list of `PathSelector` to set field values.
|
|
@@ -99,27 +99,22 @@ def case(
|
|
|
99
99
|
flag="allow_type_mismatch",
|
|
100
100
|
)
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
validate_selection(
|
|
103
|
+
valid=valid,
|
|
104
|
+
strategy=strategy,
|
|
105
|
+
target=target,
|
|
106
|
+
allow_all=False,
|
|
107
|
+
count=1,
|
|
108
|
+
)
|
|
108
109
|
|
|
110
|
+
if valid:
|
|
109
111
|
return generate_valid(ctx, model, _overrides)
|
|
110
112
|
|
|
111
|
-
if strategy == "all":
|
|
112
|
-
raise api_error(
|
|
113
|
-
"The 'all' strategy is not supported in 'case()' — use 'cases()' instead",
|
|
114
|
-
code="unsupported_strategy",
|
|
115
|
-
function="case",
|
|
116
|
-
strategy=strategy,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
113
|
task = plan_tasks(
|
|
120
114
|
model,
|
|
121
115
|
ctx=ctx,
|
|
122
116
|
strategy=strategy,
|
|
117
|
+
target=target,
|
|
123
118
|
allow_all=False,
|
|
124
119
|
count=1,
|
|
125
120
|
allow_type_mismatch=allow_type_mismatch,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
from ._errors import api_error
|
|
4
|
-
from ._utils import
|
|
5
|
-
|
|
4
|
+
from ._utils import (
|
|
5
|
+
ensure_model_or_spec,
|
|
6
|
+
normalize_overrides,
|
|
7
|
+
plan_tasks,
|
|
8
|
+
validate_selection,
|
|
9
|
+
)
|
|
10
|
+
from .path import PathSelector
|
|
6
11
|
|
|
7
12
|
from conformly._internal.generator.context import create_context
|
|
8
13
|
from conformly._internal.generator.orchestration import generate_invalid, generate_valid
|
|
@@ -15,7 +20,8 @@ def cases(
|
|
|
15
20
|
*,
|
|
16
21
|
valid: bool = True,
|
|
17
22
|
seed: int | None = None,
|
|
18
|
-
strategy: CasesStrategy
|
|
23
|
+
strategy: CasesStrategy = "first",
|
|
24
|
+
target: PathSelector | None = None,
|
|
19
25
|
count: int = 1,
|
|
20
26
|
allow_type_mismatch: bool = False,
|
|
21
27
|
allow_structural_violations: bool = False,
|
|
@@ -60,19 +66,13 @@ def cases(
|
|
|
60
66
|
generate one invalid case per every available violations
|
|
61
67
|
including constraints, structural and type violations (ignores count)
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- "field_name::violation":
|
|
68
|
-
violate a specific field with specific type
|
|
69
|
-
(e.g. strategy="user.profile::below_min")
|
|
70
|
-
|
|
71
|
-
- PathSelector DSL:
|
|
72
|
-
path("user.email").violate(V.TOO_SHORT)
|
|
69
|
+
target:
|
|
70
|
+
Optional selector from path("field") or path(Model, lambda m: m.field).
|
|
71
|
+
Only available with valid=False and strategy="first".
|
|
72
|
+
Use .violate(V.X) to request an exact violation.
|
|
73
73
|
|
|
74
74
|
count:
|
|
75
|
-
Number of cases
|
|
75
|
+
Number of cases; target requires 1. Enumeration ignores count.
|
|
76
76
|
|
|
77
77
|
overrides:
|
|
78
78
|
Optional list of `PathSelector` to set field values.
|
|
@@ -89,7 +89,7 @@ def cases(
|
|
|
89
89
|
violations and cases with extra field in each model.
|
|
90
90
|
Structural violations are available only when:
|
|
91
91
|
- valid=False
|
|
92
|
-
- strategy="all"
|
|
92
|
+
- strategy="all" or "all_violations", or an explicit violation
|
|
93
93
|
|
|
94
94
|
Returns:
|
|
95
95
|
A list of dictionaries.
|
|
@@ -122,59 +122,39 @@ def cases(
|
|
|
122
122
|
flag="allow_structural_violations",
|
|
123
123
|
)
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
validate_selection(
|
|
126
|
+
valid=valid,
|
|
127
|
+
strategy=strategy,
|
|
128
|
+
target=target,
|
|
129
|
+
allow_all=True,
|
|
130
|
+
count=count,
|
|
131
|
+
)
|
|
131
132
|
|
|
133
|
+
if valid:
|
|
132
134
|
return [generate_valid(ctx, model, _overrides) for _ in range(count)]
|
|
133
135
|
|
|
134
|
-
field_strategy, forced_violation = parse_strategy_input(strategy)
|
|
135
|
-
|
|
136
136
|
if (
|
|
137
137
|
allow_structural_violations
|
|
138
|
-
and
|
|
139
|
-
and forced_violation is None
|
|
138
|
+
and strategy not in ("all", "all_violations")
|
|
139
|
+
and (target is None or target.forced_violation is None)
|
|
140
140
|
):
|
|
141
141
|
raise api_error(
|
|
142
142
|
"Structural violations require strategy='all', 'all_violations', "
|
|
143
|
-
"or explicit
|
|
143
|
+
"or an explicit violation on target",
|
|
144
144
|
code="invalid_strategy_for_structural",
|
|
145
145
|
strategy=strategy,
|
|
146
146
|
)
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
elif strategy == "all_violations":
|
|
160
|
-
tasks = plan_tasks(
|
|
161
|
-
model,
|
|
162
|
-
ctx=ctx,
|
|
163
|
-
strategy="all",
|
|
164
|
-
allow_all=True,
|
|
165
|
-
allow_type_mismatch=allow_type_mismatch,
|
|
166
|
-
allow_structural_violations=allow_structural_violations,
|
|
167
|
-
split_by_violations=True,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
else:
|
|
171
|
-
tasks = plan_tasks(
|
|
172
|
-
model,
|
|
173
|
-
ctx=ctx,
|
|
174
|
-
strategy=strategy,
|
|
175
|
-
allow_all=False,
|
|
176
|
-
count=count,
|
|
177
|
-
allow_type_mismatch=allow_type_mismatch,
|
|
178
|
-
)
|
|
148
|
+
tasks = plan_tasks(
|
|
149
|
+
model,
|
|
150
|
+
ctx=ctx,
|
|
151
|
+
strategy="all" if strategy == "all_violations" else strategy,
|
|
152
|
+
target=target,
|
|
153
|
+
allow_all=True,
|
|
154
|
+
count=count,
|
|
155
|
+
allow_type_mismatch=allow_type_mismatch,
|
|
156
|
+
allow_structural_violations=allow_structural_violations,
|
|
157
|
+
split_by_violations=strategy == "all_violations",
|
|
158
|
+
)
|
|
179
159
|
|
|
180
160
|
return [generate_invalid(ctx, model, task, _overrides) for task in tasks]
|
|
@@ -13,10 +13,11 @@ class PathSelector:
|
|
|
13
13
|
raw_path: str
|
|
14
14
|
forced_violation: ViolationType | None = None
|
|
15
15
|
override: Any | None = None
|
|
16
|
+
has_override: bool = False
|
|
16
17
|
|
|
17
18
|
def set(self, override: Any) -> "PathSelector":
|
|
18
19
|
"""Override field value"""
|
|
19
|
-
return PathSelector(self.raw_path, override=override)
|
|
20
|
+
return PathSelector(self.raw_path, override=override, has_override=True)
|
|
20
21
|
|
|
21
22
|
def violate(self, violation: ViolationType) -> "PathSelector":
|
|
22
23
|
"""Force violation type"""
|
|
@@ -39,7 +40,7 @@ def path[T](
|
|
|
39
40
|
|
|
40
41
|
Supports two usage modes:
|
|
41
42
|
|
|
42
|
-
1. String mode (simple
|
|
43
|
+
1. String mode (simple):
|
|
43
44
|
path("user.email").violate(V.TOO_SHORT)
|
|
44
45
|
path("bio").violate(V.TOO_LONG)
|
|
45
46
|
|
|
@@ -62,8 +63,8 @@ def path[T](
|
|
|
62
63
|
Notes:
|
|
63
64
|
- String paths are not validated immediately. Validation occurs during
|
|
64
65
|
the planning stage against the resolved model structure.
|
|
65
|
-
- Lambda paths
|
|
66
|
-
|
|
66
|
+
- Lambda paths execute against an attribute proxy and work in REPLs
|
|
67
|
+
and notebooks.
|
|
67
68
|
"""
|
|
68
69
|
if isinstance(target, str):
|
|
69
70
|
if expr is not None:
|
|
@@ -83,25 +84,3 @@ def path[T](
|
|
|
83
84
|
|
|
84
85
|
raw_path = extract_path_from_proxy(expr)
|
|
85
86
|
return PathSelector(raw_path)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def parse_strategy_input(
|
|
89
|
-
strategy: str | PathSelector,
|
|
90
|
-
) -> tuple[str, ViolationType | None]:
|
|
91
|
-
if isinstance(strategy, PathSelector):
|
|
92
|
-
return strategy.raw_path, strategy.forced_violation
|
|
93
|
-
|
|
94
|
-
if "::" in strategy:
|
|
95
|
-
field_part, violation_part = strategy.split("::", 1)
|
|
96
|
-
try:
|
|
97
|
-
v_type = ViolationType(violation_part)
|
|
98
|
-
return field_part, v_type
|
|
99
|
-
except ValueError:
|
|
100
|
-
available = [v.value for v in ViolationType]
|
|
101
|
-
raise api_error(
|
|
102
|
-
f"Unknown violation type '{violation_part}'",
|
|
103
|
-
code="invalid_violation_type",
|
|
104
|
-
requested=violation_part,
|
|
105
|
-
available=available,
|
|
106
|
-
)
|
|
107
|
-
return strategy, None
|