schemez 1.0.0__py3-none-any.whl → 1.1.1__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/helpers.py CHANGED
@@ -2,15 +2,21 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import importlib
6
7
  import os
7
- from typing import TYPE_CHECKING, Any
8
+ from pathlib import Path
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ from typing import TYPE_CHECKING, Any, Literal
8
13
 
9
14
  from pydantic import BaseModel
15
+ from pydantic_core import to_json
10
16
 
11
17
 
12
18
  StrPath = str | os.PathLike[str]
13
-
19
+ PythonVersion = Literal["3.13", "3.14", "3.15"]
14
20
 
15
21
  if TYPE_CHECKING:
16
22
  from collections.abc import Callable
@@ -169,3 +175,111 @@ def resolve_type_string(type_string: str, safe: bool = True) -> type:
169
175
  except Exception as e:
170
176
  msg = f"Failed to resolve type {type_string} in unsafe mode"
171
177
  raise ValueError(msg) from e
178
+
179
+
180
+ async def _detect_command(command: str, *, test_flag: str = "--version") -> list[str]:
181
+ """Detect the correct command prefix for running a command.
182
+
183
+ Tries 'uv run' first, then falls back to direct execution.
184
+
185
+ Args:
186
+ command: The command to detect
187
+ test_flag: Flag to test command availability with
188
+
189
+ Returns:
190
+ Command prefix list (empty for direct execution)
191
+
192
+ Raises:
193
+ RuntimeError: If command is not available
194
+ """
195
+ cmd_prefixes = [["uv", "run"], []]
196
+
197
+ for prefix in cmd_prefixes:
198
+ try:
199
+ proc = await asyncio.create_subprocess_exec(
200
+ *prefix,
201
+ command,
202
+ test_flag,
203
+ stdout=asyncio.subprocess.PIPE,
204
+ stderr=asyncio.subprocess.PIPE,
205
+ )
206
+ await proc.communicate()
207
+ if proc.returncode == 0:
208
+ return prefix
209
+ except FileNotFoundError:
210
+ continue
211
+
212
+ msg = f"{command} not available (tried both 'uv run' and direct execution)"
213
+ raise RuntimeError(msg)
214
+
215
+
216
+ async def model_to_python_code(
217
+ model: type[BaseModel],
218
+ *,
219
+ class_name: str | None = None,
220
+ target_python_version: PythonVersion | None = None,
221
+ model_type: str = "pydantic.BaseModel",
222
+ ) -> str:
223
+ """Convert a BaseModel to Python code asynchronously.
224
+
225
+ Args:
226
+ model: The BaseModel class to convert
227
+ class_name: Optional custom class name for the generated code
228
+ target_python_version: Target Python version for code generation.
229
+ Defaults to current system Python version.
230
+ model_type: Type of the generated model. Defaults to "pydantic.BaseModel".
231
+
232
+ Returns:
233
+ Generated Python code as string
234
+
235
+ Raises:
236
+ RuntimeError: If datamodel-codegen is not available
237
+ subprocess.CalledProcessError: If code generation fails
238
+ """
239
+ working_prefix = await _detect_command("datamodel-codegen")
240
+
241
+ schema = model.model_json_schema()
242
+ name = class_name or model.__name__
243
+ py = target_python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
244
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
245
+ # Use pydantic_core.to_json for proper schema serialization
246
+ schema_json = to_json(schema, indent=2).decode()
247
+ f.write(schema_json)
248
+ schema_file = Path(f.name)
249
+
250
+ args = [
251
+ "--input",
252
+ str(schema_file),
253
+ "--input-file-type",
254
+ "jsonschema",
255
+ "--output-model-type",
256
+ model_type,
257
+ "--class-name",
258
+ name,
259
+ "--disable-timestamp",
260
+ "--use-union-operator",
261
+ "--use-schema-description",
262
+ "--enum-field-as-literal",
263
+ "all",
264
+ "--target-python-version",
265
+ py,
266
+ ]
267
+
268
+ try: # Generate model using datamodel-codegen
269
+ proc = await asyncio.create_subprocess_exec(
270
+ *working_prefix,
271
+ "datamodel-codegen",
272
+ *args,
273
+ stdout=asyncio.subprocess.PIPE,
274
+ stderr=asyncio.subprocess.PIPE,
275
+ )
276
+ stdout, _stderr = await proc.communicate()
277
+
278
+ if proc.returncode != 0:
279
+ code = proc.returncode or -1
280
+ raise subprocess.CalledProcessError(code, "datamodel-codegen")
281
+
282
+ return stdout.decode().strip()
283
+
284
+ finally: # Cleanup temp file
285
+ schema_file.unlink(missing_ok=True)
schemez/schema.py CHANGED
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
19
19
 
20
20
  StrPath = str | os.PathLike[str]
21
21
  SourceType = Literal["pdf", "image"]
22
+ PythonVersion = Literal["3.13", "3.14", "3.15"]
22
23
 
23
24
  DEFAULT_SYSTEM_PROMPT = "You are a schema extractor for {name} BaseModels."
24
25
  DEFAULT_USER_PROMPT = "Extract information from this document:"
@@ -254,3 +255,27 @@ class Schema(BaseModel):
254
255
  except Exception as exc:
255
256
  msg = f"Failed to save configuration to {path}"
256
257
  raise ValueError(msg) from exc
258
+
259
+ @classmethod
260
+ async def to_python_code(
261
+ cls,
262
+ *,
263
+ class_name: str | None = None,
264
+ target_python_version: PythonVersion | None = None,
265
+ ) -> str:
266
+ """Convert this model to Python code asynchronously.
267
+
268
+ Args:
269
+ class_name: Optional custom class name for the generated code
270
+ target_python_version: Target Python version for code generation
271
+
272
+ Returns:
273
+ Generated Python code as string
274
+ """
275
+ from schemez.helpers import model_to_python_code
276
+
277
+ return await model_to_python_code(
278
+ cls,
279
+ class_name=class_name,
280
+ target_python_version=target_python_version,
281
+ )
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Callable
5
6
  from enum import Enum
6
7
  from typing import Annotated, Any, Literal
7
8
 
@@ -213,29 +214,22 @@ class InlineSchemaDef(BaseSchemaDef):
213
214
  # Handle enum type
214
215
  if field.type == "enum":
215
216
  if not field.values:
216
- msg = f"Field '{name}' has type 'enum' but no values defined"
217
+ msg = f"Field {name!r} has type 'enum' but no values defined"
217
218
  raise ValueError(msg)
218
219
 
219
- # Create dynamic Enum class
220
- enum_name = f"{name.capitalize()}Enum"
221
-
222
- # Create enum members dictionary
223
- enum_members = {}
220
+ enum_name = f"{name.capitalize()}Enum" # Create dynamic Enum class
221
+ enum_members = {} # Create enum members dictionary
224
222
  for i, value in enumerate(field.values):
225
223
  if isinstance(value, str) and value.isidentifier():
226
- # If value is a valid Python identifier, use it as is
227
- key = value
224
+ key = value # If value is a valid Python identifier, use as is
228
225
  else:
229
- # Otherwise, create a synthetic name
230
- key = f"VALUE_{i}"
226
+ key = f"VALUE_{i}" # Otherwise, create a synthetic name
231
227
  enum_members[key] = value
232
228
 
233
- # Create the enum class
234
- enum_class = Enum(enum_name, enum_members)
229
+ enum_class = Enum(enum_name, enum_members) # Create the enum class
235
230
  python_type: Any = enum_class
236
231
 
237
- # Handle enum default value specially
238
- if field.default is not None:
232
+ if field.default is not None: # Handle enum default value specially
239
233
  # Store default value as the enum value string
240
234
  # Pydantic v2 will convert it to the enum instance
241
235
  if field.default in list(field.values):
@@ -252,18 +246,13 @@ class InlineSchemaDef(BaseSchemaDef):
252
246
  msg = f"Unsupported field type: {field.type}"
253
247
  raise ValueError(msg)
254
248
 
255
- # Handle literal constraint if provided
256
- if field.literal_value is not None:
257
- from typing import Literal as LiteralType
258
-
259
- python_type = LiteralType[field.literal_value]
249
+ if field.literal_value is not None: # Handle literal constraint if provided
250
+ python_type = Literal[field.literal_value]
260
251
 
261
- # Handle optional fields (allowing None)
262
- if field.optional:
252
+ if field.optional: # Handle optional fields (allowing None)
263
253
  python_type = python_type | None # type: ignore
264
254
 
265
- # Add standard Pydantic constraints
266
- # Collect all constraint values
255
+ # Add standard Pydantic constraints. Collect all constraint values
267
256
  for constraint in [
268
257
  "default",
269
258
  "title",
@@ -282,23 +271,19 @@ class InlineSchemaDef(BaseSchemaDef):
282
271
  if value is not None:
283
272
  field_constraints[constraint] = value
284
273
 
285
- # Handle examples separately (Pydantic v2 way)
286
274
  if field.examples:
287
275
  if field.json_schema_extra is None:
288
276
  field.json_schema_extra = {}
289
277
  field.json_schema_extra["examples"] = field.examples
290
278
 
291
- # Add json_schema_extra if provided
292
279
  if field.json_schema_extra:
293
280
  field_constraints["json_schema_extra"] = field.json_schema_extra
294
281
 
295
- # Handle field dependencies
296
282
  if field.dependent_required or field.dependent_schema:
297
283
  if field.json_schema_extra is None:
298
284
  field_constraints["json_schema_extra"] = {}
299
285
 
300
286
  json_extra = field_constraints.get("json_schema_extra", {})
301
-
302
287
  if field.dependent_required:
303
288
  if "dependentRequired" not in json_extra:
304
289
  json_extra["dependentRequired"] = {}
@@ -311,9 +296,7 @@ class InlineSchemaDef(BaseSchemaDef):
311
296
 
312
297
  field_constraints["json_schema_extra"] = json_extra
313
298
 
314
- # Add any additional constraints
315
- field_constraints.update(field.constraints)
316
-
299
+ field_constraints.update(field.constraints) # Add any additional constraints
317
300
  field_info = Field(description=field.description, **field_constraints)
318
301
  fields[name] = (python_type, field_info)
319
302
 
@@ -321,25 +304,18 @@ class InlineSchemaDef(BaseSchemaDef):
321
304
  if field.dependent_required or field.dependent_schema:
322
305
  if not model_dependencies:
323
306
  model_dependencies = {"json_schema_extra": {}}
324
-
307
+ extra = model_dependencies["json_schema_extra"]
325
308
  if field.dependent_required:
326
- if "dependentRequired" not in model_dependencies["json_schema_extra"]:
327
- model_dependencies["json_schema_extra"]["dependentRequired"] = {}
328
- model_dependencies["json_schema_extra"]["dependentRequired"].update(
329
- field.dependent_required
330
- )
331
-
309
+ if "dependentRequired" not in extra:
310
+ extra["dependentRequired"] = {}
311
+ extra["dependentRequired"].update(field.dependent_required)
332
312
  if field.dependent_schema:
333
- if "dependentSchemas" not in model_dependencies["json_schema_extra"]:
334
- model_dependencies["json_schema_extra"]["dependentSchemas"] = {}
335
- model_dependencies["json_schema_extra"]["dependentSchemas"].update(
336
- field.dependent_schema
337
- )
338
-
339
- # Create the model class with field definitions
340
- cls_name = self.description or "ResponseType"
341
- model = create_model(
342
- cls_name,
313
+ if "dependentSchemas" not in extra:
314
+ extra["dependentSchemas"] = {}
315
+ extra["dependentSchemas"].update(field.dependent_schema)
316
+
317
+ model = create_model( # Create the model class
318
+ self.description or "ResponseType",
343
319
  **fields,
344
320
  __base__=BaseModel,
345
321
  __doc__=self.description,
@@ -347,23 +323,22 @@ class InlineSchemaDef(BaseSchemaDef):
347
323
 
348
324
  # Add model-level JSON Schema extras for dependencies
349
325
  if model_dependencies:
350
- if not hasattr(model, "model_config") or not model.model_config:
351
- model.model_config = {}
352
-
353
- if "json_schema_extra" not in model.model_config:
354
- model.model_config["json_schema_extra"] = {}
355
-
356
- schema_extra = model.model_config["json_schema_extra"]
357
-
358
- if "dependentRequired" in model_dependencies["json_schema_extra"]:
359
- schema_extra["dependentRequired"] = model_dependencies[
360
- "json_schema_extra"
361
- ]["dependentRequired"]
362
-
363
- if "dependentSchemas" in model_dependencies["json_schema_extra"]:
364
- schema_extra["dependentSchemas"] = model_dependencies[
365
- "json_schema_extra"
366
- ]["dependentSchemas"]
326
+ existing_extra = model.model_config.get("json_schema_extra")
327
+ deps_extra = model_dependencies["json_schema_extra"]
328
+
329
+ match existing_extra:
330
+ case None:
331
+ model.model_config["json_schema_extra"] = deps_extra
332
+ case dict() as schema_extra:
333
+ schema_extra.update(deps_extra)
334
+ case Callable() as callable_func:
335
+
336
+ def wrapped_extra(*args: Any) -> None:
337
+ callable_func(*args)
338
+ schema = args[0]
339
+ schema.update(deps_extra)
340
+
341
+ model.model_config["json_schema_extra"] = wrapped_extra
367
342
 
368
343
  # Return the created model
369
344
  return model
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemez
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: Pydantic shim for config stuff
5
5
  Keywords:
6
6
  Author: Philipp Temminghoff
@@ -2,12 +2,12 @@ schemez/__init__.py,sha256=iDo1ZpV07BUOmRmISV6QDA8s3viJR5V1NnrBsdw6eVM,985
2
2
  schemez/code.py,sha256=usZLov9i5KpK1W2VJxngUzeetgrINtodiooG_AxN-y4,2072
3
3
  schemez/convert.py,sha256=b6Sz11lq0HvpXfMREOqnnw8rcVg2XzTKhjjPNc4YIoE,4403
4
4
  schemez/docstrings.py,sha256=kmd660wcomXzKac0SSNYxPRNbVCUovrpmE9jwnVRS6c,4115
5
- schemez/helpers.py,sha256=Ee3wvFbt65ljhWDFdb6ACVUJK4KLjJFVzl4Le75pOBQ,5159
5
+ schemez/helpers.py,sha256=IVuoFbzPJs3eqJBrr0BA6SIHncoU6BFJGP41zTijzXM,8716
6
6
  schemez/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  schemez/pydantic_types.py,sha256=8vgSl8i2z9n0fB-8AJj-D3TBByEWE5IxItBxQ0XwXFI,1640
8
- schemez/schema.py,sha256=VeNSFec6aCR9GgqXLBE3t4TZeUoS9BDmIoofe9nbqVI,8804
8
+ schemez/schema.py,sha256=u6SDhYDtfCjgy2Aa-_MDLLNcUfEXbeye4T-W6s3AED8,9558
9
9
  schemez/schemadef/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- schemez/schemadef/schemadef.py,sha256=m4m6f24xEgJgkp7jhp-6TpfLcwkVLL3WmKF9zDRhwAM,15091
11
- schemez-1.0.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
12
- schemez-1.0.0.dist-info/METADATA,sha256=8aIPUY5UbLeBZ_3vjIf8a692jMjeZF49wiUv8_Ywvxc,5359
13
- schemez-1.0.0.dist-info/RECORD,,
10
+ schemez/schemadef/schemadef.py,sha256=FtD7TOnYxiuYOIfadRHKkkbZn98mWFb0_lKfPsPR-hI,14393
11
+ schemez-1.1.1.dist-info/WHEEL,sha256=I8-bO5cg2sb8TH6ZM6EgCP87Y1cV_f9UGgWnfAhVOZI,78
12
+ schemez-1.1.1.dist-info/METADATA,sha256=JwgerPl41Wss2TGr4FLUr3tic62ma4lUAN8rTqgJA9I,5359
13
+ schemez-1.1.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.24
2
+ Generator: uv 0.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any