schemez 0.1.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

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.
schemez/__init__.py CHANGED
@@ -1,7 +1,27 @@
1
- __version__ = "0.1.1"
1
+ __version__ = "0.2.2"
2
2
 
3
3
 
4
4
  from schemez.schema import Schema
5
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
+ from schemez.pydantic_types import ModelIdentifier, ModelTemperature, MimeType
6
13
 
7
- __all__ = ["JSONCode", "PythonCode", "Schema", "TOMLCode", "YAMLCode"]
14
+ __all__ = [
15
+ "ImportedSchemaDef",
16
+ "InlineSchemaDef",
17
+ "JSONCode",
18
+ "MimeType",
19
+ "ModelIdentifier",
20
+ "ModelTemperature",
21
+ "PythonCode",
22
+ "Schema",
23
+ "SchemaDef",
24
+ "SchemaField",
25
+ "TOMLCode",
26
+ "YAMLCode",
27
+ ]
schemez/helpers.py CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import importlib
5
6
  import os
7
+ from typing import TYPE_CHECKING, Any
6
8
 
7
9
  from pydantic import BaseModel
8
10
 
@@ -10,6 +12,86 @@ from pydantic import BaseModel
10
12
  StrPath = str | os.PathLike[str]
11
13
 
12
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
+
13
95
  def merge_models[T: BaseModel](base: T, overlay: T) -> T:
14
96
  """Deep merge two Pydantic models."""
15
97
  if not isinstance(overlay, type(base)):
@@ -33,3 +115,57 @@ def merge_models[T: BaseModel](base: T, overlay: T) -> T:
33
115
  merged_data[field_name] = field_value
34
116
 
35
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,42 @@
1
+ """Custom field types with 'field_type' metadata for UI rendering hints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from pydantic import Field
8
+
9
+
10
+ ModelIdentifier = Annotated[
11
+ str,
12
+ Field(
13
+ json_schema_extra={"field_type": "model_identifier"},
14
+ pattern=r"^[a-zA-Z0-9\-]+(/[a-zA-Z0-9\-]+)*(:[\w\-\.]+)?$",
15
+ examples=["openai:gpt-o1-mini", "anthropic/claude-3-opus"],
16
+ description="Identifier for an AI model, optionally including provider.",
17
+ ),
18
+ ]
19
+
20
+ ModelTemperature = Annotated[
21
+ float,
22
+ Field(
23
+ json_schema_extra={"field_type": "temperature", "step": 0.1},
24
+ ge=0.0,
25
+ le=2.0,
26
+ description=(
27
+ "Controls randomness in model responses.\n"
28
+ "Lower values are more deterministic, higher values more creative"
29
+ ),
30
+ examples=[0.0, 0.7, 1.0],
31
+ ),
32
+ ]
33
+
34
+ MimeType = Annotated[
35
+ str,
36
+ Field(
37
+ json_schema_extra={"field_type": "mime_type"},
38
+ pattern=r"^[a-z]+/[a-z0-9\-+.]+$",
39
+ examples=["text/plain", "application/pdf", "image/jpeg", "application/json"],
40
+ description="Standard MIME type identifying file formats and content types",
41
+ ),
42
+ ]
schemez/schema.py CHANGED
@@ -3,8 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from typing import TYPE_CHECKING, Any, Self
6
+ from typing import TYPE_CHECKING, Any, Literal, Self
7
7
 
8
+ import anyenv
8
9
  from pydantic import BaseModel, ConfigDict
9
10
  import upath
10
11
 
@@ -12,8 +13,15 @@ import upath
12
13
  if TYPE_CHECKING:
13
14
  from collections.abc import Callable
14
15
 
16
+ from llmling_agent.agent.agent import AgentType
17
+ from llmling_agent.models.content import BaseContent
18
+
15
19
 
16
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:"
17
25
 
18
26
 
19
27
  class Schema(BaseModel):
@@ -58,6 +66,142 @@ class Schema(BaseModel):
58
66
 
59
67
  return get_function_model(func, name=name)
60
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
+
61
205
  @classmethod
62
206
  def for_class_ctor(cls, target_cls: type) -> type[Schema]:
63
207
  """Create a schema model from a class constructor.
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemez
3
- Version: 0.1.1
3
+ Version: 0.2.2
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
@@ -0,0 +1,14 @@
1
+ schemez/__init__.py,sha256=R1tqWI3CF06HlDwunmw4TB-egVZ8sr1QmL9cr1eJ75k,565
2
+ schemez/code.py,sha256=usZLov9i5KpK1W2VJxngUzeetgrINtodiooG_AxN-y4,2072
3
+ schemez/convert.py,sha256=b6Sz11lq0HvpXfMREOqnnw8rcVg2XzTKhjjPNc4YIoE,4403
4
+ schemez/docstrings.py,sha256=kmd660wcomXzKac0SSNYxPRNbVCUovrpmE9jwnVRS6c,4115
5
+ schemez/helpers.py,sha256=Ee3wvFbt65ljhWDFdb6ACVUJK4KLjJFVzl4Le75pOBQ,5159
6
+ schemez/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ schemez/pydantic_types.py,sha256=kQRR9jVZ4vyLPPqxZSGcbP2gtcHUPUWsOR3tcPNM4X0,1197
8
+ schemez/schema.py,sha256=8nwLYW6J-RIMbKjt0BZK3zlktZZzBYXabGFlaQsecCQ,8538
9
+ schemez/schemadef/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ schemez/schemadef/schemadef.py,sha256=TLfcmCxPLZEvScxuIMlss0QDRWtQNhxLQ8z2i0Linoc,3794
11
+ schemez-0.2.2.dist-info/METADATA,sha256=mTyUilTrGhabvoQXB3roUwSWJwnNTV7CrSXAlTKmy90,5891
12
+ schemez-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ schemez-0.2.2.dist-info/licenses/LICENSE,sha256=AteGCH9r177TxxrOFEiOARrastASsf7yW6MQxlAHdwA,1078
14
+ schemez-0.2.2.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- schemez/__init__.py,sha256=5rZvW7U4pMKyiONvY8rok5fVr6Bw8hjN8Kz8xnfmzoI,196
2
- schemez/code.py,sha256=usZLov9i5KpK1W2VJxngUzeetgrINtodiooG_AxN-y4,2072
3
- schemez/convert.py,sha256=b6Sz11lq0HvpXfMREOqnnw8rcVg2XzTKhjjPNc4YIoE,4403
4
- schemez/docstrings.py,sha256=kmd660wcomXzKac0SSNYxPRNbVCUovrpmE9jwnVRS6c,4115
5
- schemez/helpers.py,sha256=_leGedEf5AoeQOV0eyrJpDnvDOPB5XV3pd5YNANASeI,1081
6
- schemez/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- schemez/schema.py,sha256=qlkNigpDQJIopjSjfS4yp8vXReCr2o2eWBEDjIN7YjM,3021
8
- schemez-0.1.1.dist-info/METADATA,sha256=I4hRIS0_WvWnmdr9Fs1-1jhvnScc27WKua0fXwJGU8w,5722
9
- schemez-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- schemez-0.1.1.dist-info/licenses/LICENSE,sha256=AteGCH9r177TxxrOFEiOARrastASsf7yW6MQxlAHdwA,1078
11
- schemez-0.1.1.dist-info/RECORD,,