schemez 0.1.1__tar.gz → 0.2.1__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 (31) hide show
  1. {schemez-0.1.1 → schemez-0.2.1}/PKG-INFO +6 -1
  2. {schemez-0.1.1 → schemez-0.2.1}/pyproject.toml +6 -5
  3. schemez-0.2.1/src/schemez/__init__.py +23 -0
  4. schemez-0.2.1/src/schemez/helpers.py +171 -0
  5. schemez-0.2.1/src/schemez/schema.py +246 -0
  6. schemez-0.2.1/src/schemez/schemadef/__init__.py +0 -0
  7. schemez-0.2.1/src/schemez/schemadef/schemadef.py +120 -0
  8. schemez-0.1.1/src/schemez/__init__.py +0 -7
  9. schemez-0.1.1/src/schemez/helpers.py +0 -35
  10. schemez-0.1.1/src/schemez/schema.py +0 -102
  11. {schemez-0.1.1 → schemez-0.2.1}/.copier-answers.yml +0 -0
  12. {schemez-0.1.1 → schemez-0.2.1}/.github/FUNDING.yml +0 -0
  13. {schemez-0.1.1 → schemez-0.2.1}/.github/copilot-instructions.md +0 -0
  14. {schemez-0.1.1 → schemez-0.2.1}/.github/dependabot.yml +0 -0
  15. {schemez-0.1.1 → schemez-0.2.1}/.github/workflows/build.yml +0 -0
  16. {schemez-0.1.1 → schemez-0.2.1}/.github/workflows/documentation.yml +0 -0
  17. {schemez-0.1.1 → schemez-0.2.1}/.gitignore +0 -0
  18. {schemez-0.1.1 → schemez-0.2.1}/.pre-commit-config.yaml +0 -0
  19. {schemez-0.1.1 → schemez-0.2.1}/LICENSE +0 -0
  20. {schemez-0.1.1 → schemez-0.2.1}/README.md +0 -0
  21. {schemez-0.1.1 → schemez-0.2.1}/docs/.empty +0 -0
  22. {schemez-0.1.1 → schemez-0.2.1}/duties.py +0 -0
  23. {schemez-0.1.1 → schemez-0.2.1}/mkdocs.yml +0 -0
  24. {schemez-0.1.1 → schemez-0.2.1}/overrides/_dummy.txt +0 -0
  25. {schemez-0.1.1 → schemez-0.2.1}/src/schemez/code.py +0 -0
  26. {schemez-0.1.1 → schemez-0.2.1}/src/schemez/convert.py +0 -0
  27. {schemez-0.1.1 → schemez-0.2.1}/src/schemez/docstrings.py +0 -0
  28. {schemez-0.1.1 → schemez-0.2.1}/src/schemez/py.typed +0 -0
  29. {schemez-0.1.1 → schemez-0.2.1}/tests/__init__.py +0 -0
  30. {schemez-0.1.1 → schemez-0.2.1}/tests/conftest.py +0 -0
  31. {schemez-0.1.1 → schemez-0.2.1}/tests/test_schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemez
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: Pydantic shim for config stuff
5
5
  Project-URL: Documentation, https://phil65.github.io/schemez/
6
6
  Project-URL: Source, https://github.com/phil65/schemez
@@ -49,6 +49,11 @@ Requires-Python: >=3.12
49
49
  Requires-Dist: griffe>=1.7.3
50
50
  Requires-Dist: pydantic
51
51
  Requires-Dist: universal-pathlib>=0.2.6
52
+ Provides-Extra: ai
53
+ Requires-Dist: anyenv>=0.4.14; extra == 'ai'
54
+ Requires-Dist: llmling-agent; extra == 'ai'
55
+ Provides-Extra: yaml
56
+ Requires-Dist: yamling; extra == 'yaml'
52
57
  Description-Content-Type: text/markdown
53
58
 
54
59
  # Schemez
@@ -27,9 +27,9 @@ classifiers = [
27
27
  "Topic :: Documentation",
28
28
  "Topic :: Software Development",
29
29
  "Topic :: Utilities",
30
- "Typing :: Typed","Framework :: Pydantic",
30
+ "Typing :: Typed",
31
+ "Framework :: Pydantic",
31
32
  "Framework :: Pydantic :: 2",
32
-
33
33
  ]
34
34
  keywords = []
35
35
  requires-python = ">=3.12"
@@ -41,8 +41,9 @@ dependencies = [
41
41
  "universal-pathlib>=0.2.6",
42
42
  ]
43
43
 
44
- [project-optional-dependencies]
44
+ [project.optional-dependencies]
45
45
  yaml = ["yamling"]
46
+ ai = ["llmling-agent", "anyenv>=0.4.14"]
46
47
 
47
48
  [tool.uv]
48
49
  default-groups = ["dev", "lint", "docs"]
@@ -158,7 +159,7 @@ select = [
158
159
  # "TD", # flake8-todos
159
160
  "T10", # flake8-debugger
160
161
  # "T20", # flake8-print
161
- "TC", # flake8-type-checking
162
+ "TC", # flake8-type-checking
162
163
  "TID", # flake8-tidy-imports
163
164
  "TRY", # tryceratops
164
165
  "UP", # PyUpgrade
@@ -198,7 +199,7 @@ ignore = [
198
199
  # "PLR2004", # Magic values instead of named consts
199
200
  "SLF001", # Private member accessed
200
201
  "TRY003", # Avoid specifying long messages outside the exception class
201
- "TC006", # runtime-cast-value
202
+ "TC006", # runtime-cast-value
202
203
  ]
203
204
 
204
205
  [tool.ruff.lint.flake8-quotes]
@@ -0,0 +1,23 @@
1
+ __version__ = "0.2.1"
2
+
3
+
4
+ from schemez.schema import Schema
5
+ from schemez.code import PythonCode, JSONCode, TOMLCode, YAMLCode
6
+ from schemez.schemadef.schemadef import (
7
+ SchemaDef,
8
+ SchemaField,
9
+ ImportedSchemaDef,
10
+ InlineSchemaDef,
11
+ )
12
+
13
+ __all__ = [
14
+ "ImportedSchemaDef",
15
+ "InlineSchemaDef",
16
+ "JSONCode",
17
+ "PythonCode",
18
+ "Schema",
19
+ "SchemaDef",
20
+ "SchemaField",
21
+ "TOMLCode",
22
+ "YAMLCode",
23
+ ]
@@ -0,0 +1,171 @@
1
+ """Helpers for BaseModels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import os
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from pydantic import BaseModel
10
+
11
+
12
+ StrPath = str | os.PathLike[str]
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Callable
17
+
18
+
19
+ def import_callable(path: str) -> Callable[..., Any]:
20
+ """Import a callable from a dotted path.
21
+
22
+ Supports both dot and colon notation:
23
+ - Dot notation: module.submodule.Class.method
24
+ - Colon notation: module.submodule:Class.method
25
+
26
+ Args:
27
+ path: Import path using dots and/or colon
28
+
29
+ Raises:
30
+ ValueError: If path cannot be imported or result isn't callable
31
+ """
32
+ if not path:
33
+ msg = "Import path cannot be empty"
34
+ raise ValueError(msg)
35
+
36
+ # Normalize path - replace colon with dot if present
37
+ normalized_path = path.replace(":", ".")
38
+ parts = normalized_path.split(".")
39
+
40
+ # Try importing progressively smaller module paths
41
+ for i in range(len(parts), 0, -1):
42
+ try:
43
+ # Try current module path
44
+ module_path = ".".join(parts[:i])
45
+ module = importlib.import_module(module_path)
46
+
47
+ # Walk remaining parts as attributes
48
+ obj = module
49
+ for part in parts[i:]:
50
+ obj = getattr(obj, part)
51
+
52
+ # Check if we got a callable
53
+ if callable(obj):
54
+ return obj
55
+
56
+ msg = f"Found object at {path} but it isn't callable"
57
+ raise ValueError(msg)
58
+
59
+ except ImportError:
60
+ # Try next shorter path
61
+ continue
62
+ except AttributeError:
63
+ # Attribute not found - try next shorter path
64
+ continue
65
+
66
+ # If we get here, no import combination worked
67
+ msg = f"Could not import callable from path: {path}"
68
+ raise ValueError(msg)
69
+
70
+
71
+ def import_class(path: str) -> type:
72
+ """Import a class from a dotted path.
73
+
74
+ Args:
75
+ path: Dot-separated path to the class
76
+
77
+ Returns:
78
+ The imported class
79
+
80
+ Raises:
81
+ ValueError: If path is invalid or doesn't point to a class
82
+ """
83
+ try:
84
+ obj = import_callable(path)
85
+ if not isinstance(obj, type):
86
+ msg = f"{path} is not a class"
87
+ raise TypeError(msg) # noqa: TRY301
88
+ except Exception as exc:
89
+ msg = f"Failed to import class from {path}"
90
+ raise ValueError(msg) from exc
91
+ else:
92
+ return obj
93
+
94
+
95
+ def merge_models[T: BaseModel](base: T, overlay: T) -> T:
96
+ """Deep merge two Pydantic models."""
97
+ if not isinstance(overlay, type(base)):
98
+ msg = f"Cannot merge different types: {type(base)} and {type(overlay)}"
99
+ raise TypeError(msg)
100
+
101
+ merged_data = base.model_dump()
102
+ overlay_data = overlay.model_dump(exclude_none=True)
103
+ for field_name, field_value in overlay_data.items():
104
+ base_value = merged_data.get(field_name)
105
+
106
+ match (base_value, field_value):
107
+ case (list(), list()):
108
+ merged_data[field_name] = [
109
+ *base_value,
110
+ *(item for item in field_value if item not in base_value),
111
+ ]
112
+ case (dict(), dict()):
113
+ merged_data[field_name] = base_value | field_value
114
+ case _:
115
+ merged_data[field_name] = field_value
116
+
117
+ return base.__class__.model_validate(merged_data)
118
+
119
+
120
+ def resolve_type_string(type_string: str, safe: bool = True) -> type:
121
+ """Convert a string representation to an actual Python type.
122
+
123
+ Args:
124
+ type_string: String representation of a type (e.g. "list[str]", "int")
125
+ safe: If True, uses a limited set of allowed types. If False, allows any valid
126
+ Python type expression but has potential security implications
127
+ if input is untrusted
128
+
129
+ Returns:
130
+ The corresponding Python type
131
+
132
+ Raises:
133
+ ValueError: If the type string cannot be resolved
134
+ """
135
+ if safe:
136
+ # Create a safe context with just the allowed types
137
+ type_context = {
138
+ "str": str,
139
+ "int": int,
140
+ "float": float,
141
+ "bool": bool,
142
+ "list": list,
143
+ "dict": dict,
144
+ "set": set,
145
+ "tuple": tuple,
146
+ "Any": Any,
147
+ # Add other safe types as needed
148
+ }
149
+
150
+ try:
151
+ return eval(type_string, {"__builtins__": {}}, type_context)
152
+ except Exception as e:
153
+ msg = f"Failed to resolve type {type_string} in safe mode"
154
+ raise ValueError(msg) from e
155
+ else: # unsafe mode
156
+ # Import common typing modules to make them available
157
+ import collections.abc
158
+ import typing
159
+
160
+ # Create a context with full typing module available
161
+ type_context = {
162
+ **vars(typing),
163
+ **vars(collections.abc),
164
+ **{t.__name__: t for t in __builtins__.values() if isinstance(t, type)}, # type: ignore
165
+ }
166
+
167
+ try:
168
+ return eval(type_string, {"__builtins__": {}}, type_context)
169
+ except Exception as e:
170
+ msg = f"Failed to resolve type {type_string} in unsafe mode"
171
+ raise ValueError(msg) from e
@@ -0,0 +1,246 @@
1
+ """Configuration models for Schemez."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING, Any, Literal, Self
7
+
8
+ import anyenv
9
+ from pydantic import BaseModel, ConfigDict
10
+ import upath
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable
15
+
16
+ from llmling_agent.agent.agent import AgentType
17
+ from llmling_agent.models.content import BaseContent
18
+
19
+
20
+ StrPath = str | os.PathLike[str]
21
+ SourceType = Literal["pdf", "image"]
22
+
23
+ DEFAULT_SYSTEM_PROMPT = "You are a schema extractor for {name} BaseModels."
24
+ DEFAULT_USER_PROMPT = "Extract information from this document:"
25
+
26
+
27
+ class Schema(BaseModel):
28
+ """Base class configuration models.
29
+
30
+ Provides:
31
+ - Common Pydantic settings
32
+ - YAML serialization
33
+ - Basic merge functionality
34
+ """
35
+
36
+ model_config = ConfigDict(extra="forbid", use_attribute_docstrings=True)
37
+
38
+ def merge(self, other: Self) -> Self:
39
+ """Merge with another instance by overlaying its non-None values."""
40
+ from schemez.helpers import merge_models
41
+
42
+ return merge_models(self, other)
43
+
44
+ @classmethod
45
+ def from_yaml(cls, content: str, inherit_path: StrPath | None = None) -> Self:
46
+ """Create from YAML string."""
47
+ import yamling
48
+
49
+ data = yamling.load_yaml(content, resolve_inherit=inherit_path or False)
50
+ return cls.model_validate(data)
51
+
52
+ @classmethod
53
+ def for_function(
54
+ cls, func: Callable[..., Any], *, name: str | None = None
55
+ ) -> type[Schema]:
56
+ """Create a schema model from a function's signature.
57
+
58
+ Args:
59
+ func: The function to create a schema from
60
+ name: Optional name for the model
61
+
62
+ Returns:
63
+ A new schema model class based on the function parameters
64
+ """
65
+ from schemez.convert import get_function_model
66
+
67
+ return get_function_model(func, name=name)
68
+
69
+ @classmethod
70
+ def from_vision_llm_sync(
71
+ cls,
72
+ file_content: bytes,
73
+ source_type: SourceType = "pdf",
74
+ model: str = "google-gla:gemini-2.0-flash",
75
+ system_prompt: str = DEFAULT_SYSTEM_PROMPT,
76
+ user_prompt: str = DEFAULT_USER_PROMPT,
77
+ provider: AgentType = "pydantic_ai",
78
+ ) -> Self:
79
+ """Create a schema model from a document using AI.
80
+
81
+ Args:
82
+ file_content: The document content to create a schema from
83
+ source_type: The type of the document
84
+ model: The AI model to use for schema extraction
85
+ system_prompt: The system prompt to use for schema extraction
86
+ user_prompt: The user prompt to use for schema extraction
87
+ provider: The provider to use for schema extraction
88
+
89
+ Returns:
90
+ A new schema model class based on the document
91
+ """
92
+ from llmling_agent import Agent, ImageBase64Content, PDFBase64Content
93
+
94
+ if source_type == "pdf":
95
+ content: BaseContent = PDFBase64Content.from_bytes(file_content)
96
+ else:
97
+ content = ImageBase64Content.from_bytes(file_content)
98
+ agent = Agent[None]( # type:ignore[var-annotated]
99
+ model=model,
100
+ system_prompt=system_prompt.format(name=cls.__name__),
101
+ provider=provider,
102
+ ).to_structured(cls)
103
+ chat_message = anyenv.run_sync(agent.run(user_prompt, content))
104
+ return chat_message.content
105
+
106
+ @classmethod
107
+ async def from_vision_llm(
108
+ cls,
109
+ file_content: bytes,
110
+ source_type: SourceType = "pdf",
111
+ model: str = "google-gla:gemini-2.0-flash",
112
+ system_prompt: str = DEFAULT_SYSTEM_PROMPT,
113
+ user_prompt: str = DEFAULT_USER_PROMPT,
114
+ provider: AgentType = "pydantic_ai",
115
+ ) -> Self:
116
+ """Create a schema model from a document using AI.
117
+
118
+ Args:
119
+ file_content: The document content to create a schema from
120
+ source_type: The type of the document
121
+ model: The AI model to use for schema extraction
122
+ system_prompt: The system prompt to use for schema extraction
123
+ user_prompt: The user prompt to use for schema extraction
124
+ provider: The provider to use for schema extraction
125
+
126
+ Returns:
127
+ A new schema model class based on the document
128
+ """
129
+ from llmling_agent import Agent, ImageBase64Content, PDFBase64Content
130
+
131
+ if source_type == "pdf":
132
+ content: BaseContent = PDFBase64Content.from_bytes(file_content)
133
+ else:
134
+ content = ImageBase64Content.from_bytes(file_content)
135
+ agent = Agent[None]( # type:ignore[var-annotated]
136
+ model=model,
137
+ system_prompt=system_prompt.format(name=cls.__name__),
138
+ provider=provider,
139
+ ).to_structured(cls)
140
+ chat_message = await agent.run(user_prompt, content)
141
+ return chat_message.content
142
+
143
+ @classmethod
144
+ def from_llm_sync(
145
+ cls,
146
+ text: str,
147
+ model: str = "google-gla:gemini-2.0-flash",
148
+ system_prompt: str = DEFAULT_SYSTEM_PROMPT,
149
+ user_prompt: str = DEFAULT_USER_PROMPT,
150
+ provider: AgentType = "pydantic_ai",
151
+ ) -> Self:
152
+ """Create a schema model from a text snippet using AI.
153
+
154
+ Args:
155
+ text: The text to create a schema from
156
+ model: The AI model to use for schema extraction
157
+ system_prompt: The system prompt to use for schema extraction
158
+ user_prompt: The user prompt to use for schema extraction
159
+ provider: The provider to use for schema extraction
160
+
161
+ Returns:
162
+ A new schema model class based on the document
163
+ """
164
+ from llmling_agent import Agent
165
+
166
+ agent = Agent[None]( # type:ignore[var-annotated]
167
+ model=model,
168
+ system_prompt=system_prompt.format(name=cls.__name__),
169
+ provider=provider,
170
+ ).to_structured(cls)
171
+ chat_message = anyenv.run_sync(agent.run(user_prompt, text))
172
+ return chat_message.content
173
+
174
+ @classmethod
175
+ async def from_llm(
176
+ cls,
177
+ text: str,
178
+ model: str = "google-gla:gemini-2.0-flash",
179
+ system_prompt: str = DEFAULT_SYSTEM_PROMPT,
180
+ user_prompt: str = DEFAULT_USER_PROMPT,
181
+ provider: AgentType = "pydantic_ai",
182
+ ) -> Self:
183
+ """Create a schema model from a text snippet using AI.
184
+
185
+ Args:
186
+ text: The text to create a schema from
187
+ model: The AI model to use for schema extraction
188
+ system_prompt: The system prompt to use for schema extraction
189
+ user_prompt: The user prompt to use for schema extraction
190
+ provider: The provider to use for schema extraction
191
+
192
+ Returns:
193
+ A new schema model class based on the document
194
+ """
195
+ from llmling_agent import Agent
196
+
197
+ agent = Agent[None]( # type:ignore[var-annotated]
198
+ model=model,
199
+ system_prompt=system_prompt.format(name=cls.__name__),
200
+ provider=provider,
201
+ ).to_structured(cls)
202
+ chat_message = await agent.run(user_prompt, text)
203
+ return chat_message.content
204
+
205
+ @classmethod
206
+ def for_class_ctor(cls, target_cls: type) -> type[Schema]:
207
+ """Create a schema model from a class constructor.
208
+
209
+ Args:
210
+ target_cls: The class whose constructor to convert
211
+
212
+ Returns:
213
+ A new schema model class based on the constructor parameters
214
+ """
215
+ from schemez.convert import get_ctor_basemodel
216
+
217
+ return get_ctor_basemodel(target_cls)
218
+
219
+ def model_dump_yaml(self) -> str:
220
+ """Dump configuration to YAML string."""
221
+ import yamling
222
+
223
+ return yamling.dump_yaml(self.model_dump(exclude_none=True))
224
+
225
+ def save(self, path: StrPath, overwrite: bool = False) -> None:
226
+ """Save configuration to a YAML file.
227
+
228
+ Args:
229
+ path: Path to save the configuration to
230
+ overwrite: Whether to overwrite an existing file
231
+
232
+ Raises:
233
+ OSError: If file cannot be written
234
+ ValueError: If path is invalid
235
+ """
236
+ yaml_str = self.model_dump_yaml()
237
+ try:
238
+ file_path = upath.UPath(path)
239
+ if file_path.exists() and not overwrite:
240
+ msg = f"File already exists: {path}"
241
+ raise FileExistsError(msg) # noqa: TRY301
242
+ file_path.parent.mkdir(parents=True, exist_ok=True)
243
+ file_path.write_text(yaml_str)
244
+ except Exception as exc:
245
+ msg = f"Failed to save configuration to {path}"
246
+ raise ValueError(msg) from exc
File without changes
@@ -0,0 +1,120 @@
1
+ """Models for schema fields and definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Any, Literal
6
+
7
+ from pydantic import BaseModel, Field, create_model
8
+
9
+ from schemez import Schema, helpers
10
+
11
+
12
+ class SchemaField(Schema):
13
+ """Field definition for inline response types.
14
+
15
+ Defines a single field in an inline response definition, including:
16
+ - Data type specification
17
+ - Optional description
18
+ - Validation constraints
19
+
20
+ Used by InlineSchemaDef to structure response fields.
21
+ """
22
+
23
+ type: str
24
+ """Data type of the response field"""
25
+
26
+ description: str | None = None
27
+ """Optional description of what this field represents"""
28
+
29
+ constraints: dict[str, Any] = Field(default_factory=dict)
30
+ """Optional validation constraints for the field"""
31
+
32
+
33
+ class BaseSchemaDef(Schema):
34
+ """Base class for response definitions."""
35
+
36
+ type: str = Field(init=False)
37
+
38
+ description: str | None = None
39
+ """A description for this response definition."""
40
+
41
+
42
+ class InlineSchemaDef(BaseSchemaDef):
43
+ """Inline definition of schema.
44
+
45
+ Allows defining response types directly in the configuration using:
46
+ - Field definitions with types and descriptions
47
+ - Optional validation constraints
48
+ - Custom field descriptions
49
+
50
+ Example:
51
+ schemas:
52
+ BasicResult:
53
+ type: inline
54
+ fields:
55
+ success: {type: bool, description: "Operation success"}
56
+ message: {type: str, description: "Result details"}
57
+ """
58
+
59
+ type: Literal["inline"] = Field("inline", init=False)
60
+ """Inline response definition."""
61
+
62
+ fields: dict[str, SchemaField]
63
+ """A dictionary containing all fields."""
64
+
65
+ def get_schema(self) -> type[Schema]: # type: ignore
66
+ """Create Pydantic model from inline definition."""
67
+ fields = {}
68
+ for name, field in self.fields.items():
69
+ python_type = helpers.resolve_type_string(field.type)
70
+ if not python_type:
71
+ msg = f"Unsupported field type: {field.type}"
72
+ raise ValueError(msg)
73
+
74
+ field_info = Field(description=field.description, **(field.constraints))
75
+ fields[name] = (python_type, field_info)
76
+
77
+ cls_name = self.description or "ResponseType"
78
+ return create_model(cls_name, **fields, __base__=Schema, __doc__=self.description) # type: ignore[call-overload]
79
+
80
+
81
+ class ImportedSchemaDef(BaseSchemaDef):
82
+ """Response definition that imports an existing Pydantic model.
83
+
84
+ Allows using externally defined Pydantic models as response types.
85
+ Benefits:
86
+ - Reuse existing model definitions
87
+ - Full Python type support
88
+ - Complex validation logic
89
+ - IDE support for imported types
90
+
91
+ Example:
92
+ responses:
93
+ AnalysisResult:
94
+ type: import
95
+ import_path: myapp.models.AnalysisResult
96
+ """
97
+
98
+ type: Literal["import"] = Field("import", init=False)
99
+ """Import-path based response definition."""
100
+
101
+ import_path: str
102
+ """The path to the pydantic model to use as the response type."""
103
+
104
+ # mypy is confused about "type"
105
+ # TODO: convert BaseModel to Schema?
106
+ def get_schema(self) -> type[BaseModel]: # type: ignore
107
+ """Import and return the model class."""
108
+ try:
109
+ model_class = helpers.import_class(self.import_path)
110
+ if not issubclass(model_class, BaseModel):
111
+ msg = f"{self.import_path} must be a Pydantic model"
112
+ raise TypeError(msg) # noqa: TRY301
113
+ except Exception as e:
114
+ msg = f"Failed to import response type {self.import_path}"
115
+ raise ValueError(msg) from e
116
+ else:
117
+ return model_class
118
+
119
+
120
+ SchemaDef = Annotated[InlineSchemaDef | ImportedSchemaDef, Field(discriminator="type")]
@@ -1,7 +0,0 @@
1
- __version__ = "0.1.1"
2
-
3
-
4
- from schemez.schema import Schema
5
- from schemez.code import PythonCode, JSONCode, TOMLCode, YAMLCode
6
-
7
- __all__ = ["JSONCode", "PythonCode", "Schema", "TOMLCode", "YAMLCode"]
@@ -1,35 +0,0 @@
1
- """Helpers for BaseModels."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
-
7
- from pydantic import BaseModel
8
-
9
-
10
- StrPath = str | os.PathLike[str]
11
-
12
-
13
- def merge_models[T: BaseModel](base: T, overlay: T) -> T:
14
- """Deep merge two Pydantic models."""
15
- if not isinstance(overlay, type(base)):
16
- msg = f"Cannot merge different types: {type(base)} and {type(overlay)}"
17
- raise TypeError(msg)
18
-
19
- merged_data = base.model_dump()
20
- overlay_data = overlay.model_dump(exclude_none=True)
21
- for field_name, field_value in overlay_data.items():
22
- base_value = merged_data.get(field_name)
23
-
24
- match (base_value, field_value):
25
- case (list(), list()):
26
- merged_data[field_name] = [
27
- *base_value,
28
- *(item for item in field_value if item not in base_value),
29
- ]
30
- case (dict(), dict()):
31
- merged_data[field_name] = base_value | field_value
32
- case _:
33
- merged_data[field_name] = field_value
34
-
35
- return base.__class__.model_validate(merged_data)
@@ -1,102 +0,0 @@
1
- """Configuration models for Schemez."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- from typing import TYPE_CHECKING, Any, Self
7
-
8
- from pydantic import BaseModel, ConfigDict
9
- import upath
10
-
11
-
12
- if TYPE_CHECKING:
13
- from collections.abc import Callable
14
-
15
-
16
- StrPath = str | os.PathLike[str]
17
-
18
-
19
- class Schema(BaseModel):
20
- """Base class configuration models.
21
-
22
- Provides:
23
- - Common Pydantic settings
24
- - YAML serialization
25
- - Basic merge functionality
26
- """
27
-
28
- model_config = ConfigDict(extra="forbid", use_attribute_docstrings=True)
29
-
30
- def merge(self, other: Self) -> Self:
31
- """Merge with another instance by overlaying its non-None values."""
32
- from schemez.helpers import merge_models
33
-
34
- return merge_models(self, other)
35
-
36
- @classmethod
37
- def from_yaml(cls, content: str, inherit_path: StrPath | None = None) -> Self:
38
- """Create from YAML string."""
39
- import yamling
40
-
41
- data = yamling.load_yaml(content, resolve_inherit=inherit_path or False)
42
- return cls.model_validate(data)
43
-
44
- @classmethod
45
- def for_function(
46
- cls, func: Callable[..., Any], *, name: str | None = None
47
- ) -> type[Schema]:
48
- """Create a schema model from a function's signature.
49
-
50
- Args:
51
- func: The function to create a schema from
52
- name: Optional name for the model
53
-
54
- Returns:
55
- A new schema model class based on the function parameters
56
- """
57
- from schemez.convert import get_function_model
58
-
59
- return get_function_model(func, name=name)
60
-
61
- @classmethod
62
- def for_class_ctor(cls, target_cls: type) -> type[Schema]:
63
- """Create a schema model from a class constructor.
64
-
65
- Args:
66
- target_cls: The class whose constructor to convert
67
-
68
- Returns:
69
- A new schema model class based on the constructor parameters
70
- """
71
- from schemez.convert import get_ctor_basemodel
72
-
73
- return get_ctor_basemodel(target_cls)
74
-
75
- def model_dump_yaml(self) -> str:
76
- """Dump configuration to YAML string."""
77
- import yamling
78
-
79
- return yamling.dump_yaml(self.model_dump(exclude_none=True))
80
-
81
- def save(self, path: StrPath, overwrite: bool = False) -> None:
82
- """Save configuration to a YAML file.
83
-
84
- Args:
85
- path: Path to save the configuration to
86
- overwrite: Whether to overwrite an existing file
87
-
88
- Raises:
89
- OSError: If file cannot be written
90
- ValueError: If path is invalid
91
- """
92
- yaml_str = self.model_dump_yaml()
93
- try:
94
- file_path = upath.UPath(path)
95
- if file_path.exists() and not overwrite:
96
- msg = f"File already exists: {path}"
97
- raise FileExistsError(msg) # noqa: TRY301
98
- file_path.parent.mkdir(parents=True, exist_ok=True)
99
- file_path.write_text(yaml_str)
100
- except Exception as exc:
101
- msg = f"Failed to save configuration to {path}"
102
- raise ValueError(msg) from exc
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes