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.
Files changed (111) hide show
  1. conformly-0.8.0/PKG-INFO +117 -0
  2. conformly-0.8.0/README.md +84 -0
  3. {conformly-0.7.4 → conformly-0.8.0}/pyproject.toml +1 -1
  4. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/__init__.py +3 -1
  5. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/numeric.py +6 -4
  6. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/fields/__init__.py +3 -1
  7. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/registry.py +12 -0
  8. conformly-0.8.0/src/conformly/_internal/generator/types/bytes.py +25 -0
  9. conformly-0.8.0/src/conformly/_internal/generator/types/temporal.py +59 -0
  10. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/extractors/constraints.py +6 -0
  11. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/field.py +16 -0
  12. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/resolve.py +25 -2
  13. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/__init__.py +6 -0
  14. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_domains.py +6 -0
  15. conformly-0.8.0/src/conformly/_internal/resolver/semantics/bytes.py +48 -0
  16. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/factory.py +6 -0
  17. conformly-0.8.0/src/conformly/_internal/resolver/semantics/temporal.py +139 -0
  18. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/primitives.py +5 -0
  19. conformly-0.8.0/src/conformly.egg-info/PKG-INFO +117 -0
  20. {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/SOURCES.txt +4 -0
  21. conformly-0.7.4/PKG-INFO +0 -792
  22. conformly-0.7.4/README.md +0 -759
  23. conformly-0.7.4/src/conformly.egg-info/PKG-INFO +0 -792
  24. {conformly-0.7.4 → conformly-0.8.0}/LICENSE +0 -0
  25. {conformly-0.7.4 → conformly-0.8.0}/setup.cfg +0 -0
  26. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/__init__.py +0 -0
  27. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/__init__.py +0 -0
  28. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/_errors.py +0 -0
  29. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/_path_proxy.py +0 -0
  30. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/_utils.py +0 -0
  31. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/case.py +0 -0
  32. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/cases.py +0 -0
  33. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/api/path.py +0 -0
  34. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/__init__.py +0 -0
  35. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/base.py +0 -0
  36. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/collections.py +0 -0
  37. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/enum.py +0 -0
  38. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/keys.py +0 -0
  39. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/mapping.py +0 -0
  40. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/constraints/string.py +0 -0
  41. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/fields/registry.py +0 -0
  42. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/fields/special_types.py +0 -0
  43. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/__init__.py +0 -0
  44. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/_errors.py +0 -0
  45. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/context.py +0 -0
  46. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/orchestration.py +0 -0
  47. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/protocol.py +0 -0
  48. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/__init__.py +0 -0
  49. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/_utils.py +0 -0
  50. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/boolean.py +0 -0
  51. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/dictionaries.py +0 -0
  52. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/email.py +0 -0
  53. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/enum.py +0 -0
  54. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/float.py +0 -0
  55. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/integer.py +0 -0
  56. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/ipv4.py +0 -0
  57. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/ipv6.py +0 -0
  58. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/ipvany.py +0 -0
  59. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/list.py +0 -0
  60. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/string.py +0 -0
  61. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/tuple.py +0 -0
  62. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/url.py +0 -0
  63. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/generator/types/uuid.py +0 -0
  64. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/__init__.py +0 -0
  65. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
  66. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
  67. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
  68. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
  69. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
  70. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
  71. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/registry.py +0 -0
  72. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
  73. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/api.py +0 -0
  74. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/core/__init__.py +0 -0
  75. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/core/element.py +0 -0
  76. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/core/field.py +0 -0
  77. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
  78. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/extractors/types.py +0 -0
  79. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/parser/models.py +0 -0
  80. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/__init__.py +0 -0
  81. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/_errors.py +0 -0
  82. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/model.py +0 -0
  83. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/planner/session.py +0 -0
  84. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/__init__.py +0 -0
  85. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/models.py +0 -0
  86. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_numeric.py +0 -0
  87. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_regex.py +0 -0
  88. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
  89. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/base.py +0 -0
  90. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
  91. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
  92. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
  93. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/list.py +0 -0
  94. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
  95. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/object.py +0 -0
  96. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/string.py +0 -0
  97. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
  98. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
  99. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/tracer.py +0 -0
  100. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/__init__.py +0 -0
  101. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/constants.py +0 -0
  102. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/strategies.py +0 -0
  103. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/_internal/types/values.py +0 -0
  104. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/exceptions.py +0 -0
  105. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/py.typed +0 -0
  106. {conformly-0.7.4 → conformly-0.8.0}/src/conformly/tracer/__init__.py +0 -0
  107. {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/dependency_links.txt +0 -0
  108. {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/requires.txt +0 -0
  109. {conformly-0.7.4 → conformly-0.8.0}/src/conformly.egg-info/top_level.txt +0 -0
  110. {conformly-0.7.4 → conformly-0.8.0}/tests/test_collection.py +0 -0
  111. {conformly-0.7.4 → conformly-0.8.0}/tests/test_path.py +0 -0
@@ -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
+ [![CI](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml/badge.svg)](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml)
37
+ [![codecov](https://codecov.io/github/nashabanov/conformly/graph/badge.svg?branch=main)](https://codecov.io/github/nashabanov/conformly)
38
+ [![PyPI](https://img.shields.io/pypi/v/conformly.svg)](https://pypi.org/project/conformly/)
39
+ [![Python Versions](https://img.shields.io/pypi/pyversions/conformly.svg)](https://pypi.org/project/conformly/)
40
+
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
42
+ [![Status: Beta](https://img.shields.io/badge/status-beta-orange?style=flat-square)](https://github.com/nashabanov/conformly/releases)
43
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
44
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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
+ [![CI](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml/badge.svg)](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml)
4
+ [![codecov](https://codecov.io/github/nashabanov/conformly/graph/badge.svg?branch=main)](https://codecov.io/github/nashabanov/conformly)
5
+ [![PyPI](https://img.shields.io/pypi/v/conformly.svg)](https://pypi.org/project/conformly/)
6
+ [![Python Versions](https://img.shields.io/pypi/pyversions/conformly.svg)](https://pypi.org/project/conformly/)
7
+
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ [![Status: Beta](https://img.shields.io/badge/status-beta-orange?style=flat-square)](https://github.com/nashabanov/conformly/releases)
10
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
11
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "conformly"
3
- version = "0.7.4"
3
+ version = "0.8.0"
4
4
  description = "Generate valid & invalid test data from your typed schemas"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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: TNum
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: TNum
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: TNum
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: TNum
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)
@@ -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=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
+ )