schemap 0.1.0__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.
schemap/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """ Automatic Pydantic schemas for SQLAlchemy models """
2
+
3
+ __version__ = "0.1.0"
4
+
schemap/base.py ADDED
@@ -0,0 +1 @@
1
+ """Base classes for AutoSchema models."""
schemap/builder.py ADDED
@@ -0,0 +1,44 @@
1
+ from typing import Type, Any, Optional
2
+ from sqlalchemy.orm import DeclarativeBase
3
+ from sqlalchemy import inspect
4
+ from pydantic import create_model, ConfigDict, Field
5
+
6
+ from .types import extract_column_metadata
7
+ from .utils.schema import should_include, transform_for_schema
8
+
9
+
10
+ def build_schema(
11
+ model: Type[DeclarativeBase],
12
+ schema_type: str = "default", # "default", "create", "update", "public"
13
+ config: Optional[Any] = None, # SchemaConfig
14
+ ) -> Any:
15
+ """
16
+ Build a Pydantic schema class for a SQLAlchemy model.
17
+
18
+ Args:
19
+ model: The SQLAlchemy model class (e.g., User)
20
+ schema_type: Which schema variant to build
21
+ config: Optional SchemaConfig for customization
22
+
23
+ Returns:
24
+ A Pydantic model class
25
+ """
26
+
27
+ inspector = inspect(model)
28
+
29
+ columns_meta = []
30
+
31
+ for col in inspector.columns:
32
+ meta = extract_column_metadata(col)
33
+ if should_include(schema_type, meta):
34
+ columns_meta.append(meta)
35
+
36
+ fields = {}
37
+
38
+ for col_meta in columns_meta:
39
+ field_type, field_kwargs = transform_for_schema(col_meta, schema_type)
40
+ fields[col_meta["name"]] = (field_type, field_kwargs)
41
+
42
+ schema_name = f"{model.__name__}{schema_type.capitalize()}Schema"
43
+
44
+ return create_model(schema_name, __config__=ConfigDict(from_attributes=True), **fields)
schemap/types.py ADDED
@@ -0,0 +1,64 @@
1
+ from .utils.mapping import TYPE_MAP
2
+ from .utils.likeness import ColumnLike
3
+
4
+ from sqlalchemy import types as sa_types, inspect
5
+ from sqlalchemy.sql.schema import Column
6
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
7
+ from typing import Union, Any
8
+
9
+
10
+ def extract_python_type(column : ColumnLike) -> tuple[type, bool]:
11
+ """
12
+ Extract Python type and optional flag from a SQLAlchemy column.
13
+
14
+ Args:
15
+ column: A SQLAlchemy Column or InstrumentedAttribute
16
+
17
+ Returns:
18
+ tuple[type, bool]: (python_type, is_optional)
19
+
20
+ Example:
21
+ >>> from sqlalchemy import Integer, String
22
+ >>> from sqlalchemy.orm import Mapped, mapped_column
23
+ >>>
24
+ >>> # Simulating Mapped[int]
25
+ >>> extract_python_type(Integer())
26
+ (int, False)
27
+ """
28
+ # If passed a TypeEngine directly (like Integer()), use it as-is
29
+ if isinstance(column, sa_types.TypeEngine):
30
+ sql_type = type(column)
31
+ return TYPE_MAP.get(sql_type, (Any, False)), False
32
+
33
+ sql_type = type(column.type)
34
+ return TYPE_MAP.get(sql_type, (Any, False)), False
35
+
36
+ def extract_column_metadata(column : ColumnLike) -> dict[str, Any]:
37
+ """
38
+ Extract all metadata from column for Pydantic Field construction.
39
+
40
+ Returns dict with keys:
41
+ - "python_type": the Python type from extract_python_type
42
+ - "is_optional": boolean
43
+ - "max_length": int | None (from String, Text)
44
+ - "ge": int | None (minimum value for numeric)
45
+ - "le": int | None (maximum value for numeric)
46
+ - "default": Any | None (from column.default)
47
+ - "primary_key": bool
48
+
49
+ Args:
50
+ column: SQLAlchemy Column object
51
+
52
+ Returns:
53
+ dict with above keys
54
+ """
55
+
56
+ return{
57
+ "name": column.name,
58
+ "python_type": extract_python_type(column)[0],
59
+ "is_optional": column.nullable,
60
+ "primary_key": column.primary_key,
61
+ "max_length": getattr(column.type, 'length', None),
62
+ "default": column.default.arg if column.default else None,
63
+ "server_default" : column.server_default,
64
+ }
@@ -0,0 +1,49 @@
1
+ from typing import Protocol, Optional, Set, Any
2
+ from sqlalchemy.sql.type_api import TypeEngine
3
+ from sqlalchemy.sql.schema import (
4
+ ColumnDefault,
5
+ DefaultClause,
6
+ Identity,
7
+ Computed,
8
+ ForeignKey,
9
+ Constraint,
10
+ DialectKWArgs,
11
+ )
12
+
13
+
14
+ class ColumnLike(Protocol):
15
+ """Protocol for objects that behave like SQLAlchemy Column."""
16
+
17
+ # --- Identity ---
18
+ name: str
19
+ key: str
20
+
21
+ # --- Type ---
22
+ type: TypeEngine[Any]
23
+
24
+ # --- Nullability / defaults ---
25
+ nullable: bool
26
+ default: Optional[ColumnDefault]
27
+ server_default: Optional[DefaultClause]
28
+ autoincrement: Any # SQLAlchemy uses bool | Literal["auto"] | etc
29
+
30
+ # --- Constraints / keys ---
31
+ primary_key: bool
32
+ unique: Optional[bool]
33
+ index: Optional[bool]
34
+ foreign_keys: Set[ForeignKey]
35
+ constraints: Set[Constraint]
36
+
37
+ # --- Generated / identity ---
38
+ identity: Optional[Identity]
39
+ computed: Optional[Computed]
40
+
41
+ # --- Docs / comments ---
42
+ comment: Optional[str]
43
+ doc: Optional[str]
44
+
45
+ # --- Misc ---
46
+ quote: Optional[bool]
47
+ system: bool
48
+ info: dict[str, Any]
49
+ dialect_options: DialectKWArgs
@@ -0,0 +1,57 @@
1
+ from sqlalchemy import types as sa_types
2
+ from datetime import date, time, datetime
3
+ from decimal import Decimal
4
+ import uuid
5
+
6
+ TYPE_MAP = {
7
+ # --- Strings ---
8
+ sa_types.String: str,
9
+ sa_types.Text: str,
10
+ sa_types.Unicode: str,
11
+ sa_types.UnicodeText: str,
12
+ sa_types.CHAR: str,
13
+ sa_types.VARCHAR: str,
14
+ sa_types.NCHAR: str,
15
+ sa_types.NVARCHAR: str,
16
+ sa_types.CLOB: str,
17
+
18
+ # --- Integers ---
19
+ sa_types.Integer: int,
20
+ sa_types.SmallInteger: int,
21
+ sa_types.BigInteger: int,
22
+
23
+ # --- Numeric ---
24
+ sa_types.Numeric: Decimal,
25
+ sa_types.DECIMAL: Decimal,
26
+
27
+ # --- Floating point ---
28
+ sa_types.Float: float,
29
+ sa_types.REAL: float,
30
+
31
+ # --- Boolean ---
32
+ sa_types.Boolean: bool,
33
+
34
+ # --- Date / Time ---
35
+ sa_types.Date: date,
36
+ sa_types.Time: time,
37
+ sa_types.DateTime: datetime,
38
+ sa_types.TIMESTAMP: datetime,
39
+
40
+ # --- Binary ---
41
+ sa_types.LargeBinary: bytes,
42
+ sa_types.BLOB: bytes,
43
+ sa_types.BINARY: bytes,
44
+ sa_types.VARBINARY: bytes,
45
+
46
+ # --- JSON ---
47
+ sa_types.JSON: dict,
48
+
49
+ # --- UUID ---
50
+ sa_types.UUID: uuid.UUID,
51
+
52
+ # --- Arrays ---
53
+ sa_types.ARRAY: list,
54
+
55
+ # --- Generic fallback ---
56
+ sa_types.NullType: object,
57
+ }
@@ -0,0 +1,78 @@
1
+ from typing import Any, Optional
2
+ from pydantic import Field
3
+
4
+ def should_include(schema_type : str, metadata : dict[str, Any]):
5
+ """Determine if a column should be included in the schema."""
6
+
7
+ if schema_type == "default":
8
+ return True
9
+
10
+ elif schema_type == "create":
11
+ # Exclude primary keys and server_default fields
12
+ if metadata["primary_key"]:
13
+ return False
14
+
15
+ if metadata.get("server_default") is not None:
16
+ return False
17
+
18
+ if metadata.get("default") is not None:
19
+ return False
20
+
21
+ return True
22
+
23
+ elif schema_type == "update":
24
+ # Exclude primary keys only
25
+ if metadata["primary_key"]:
26
+ return False
27
+
28
+ return True
29
+
30
+ elif schema_type == "public":
31
+ # Exclude fields starting with __ (private)
32
+ if metadata["name"].startswith("__"):
33
+ return False
34
+
35
+ return True
36
+
37
+ else:
38
+ raise ValueError(f"Unknown schema_type: {schema_type}")
39
+
40
+ def transform_for_schema(metadata: dict, schema_type: str) -> tuple[type, Any]:
41
+ """Transform column metadata into (field_type, Field(...)) for create_model."""
42
+
43
+ python_type = metadata["python_type"]
44
+
45
+ # Build Field kwargs from constraints
46
+ field_kwargs = {}
47
+
48
+ # String length constraint
49
+ if metadata.get("max_length") is not None:
50
+ field_kwargs["max_length"] = metadata["max_length"]
51
+
52
+ if schema_type == "update":
53
+ field_kwargs["default"] = None
54
+ return (Optional[python_type], Field(**field_kwargs))
55
+
56
+ elif schema_type == "create":
57
+ if metadata.get("default") is not None:
58
+ field_kwargs["default"] = metadata["default"]
59
+ return (python_type, Field(**field_kwargs))
60
+
61
+ elif metadata["is_optional"]:
62
+ field_kwargs["default"] = None
63
+ return (Optional[python_type], Field(**field_kwargs))
64
+
65
+ else:
66
+ return (python_type, Field(**field_kwargs) if field_kwargs else ...)
67
+
68
+ else: # default or public
69
+ if metadata.get("default") is not None:
70
+ field_kwargs["default"] = metadata["default"]
71
+ return (python_type, Field(**field_kwargs))
72
+
73
+ elif metadata["is_optional"]:
74
+ field_kwargs["default"] = None
75
+ return (Optional[python_type], Field(**field_kwargs))
76
+
77
+ else:
78
+ return (python_type, Field(**field_kwargs) if field_kwargs else ...)
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: schemap
3
+ Version: 0.1.0
4
+ Summary: Automatic Pydantic Schemas for SQLAlchemy models
5
+ Author-email: Emiliano Gandini Outeda <emiliano.gandini@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/emiliano-gandini-outeda/schemap
8
+ Project-URL: Repository, https://github.com/emiliano-gandini-outeda/schemap
9
+ Project-URL: Issues, https://github.com/emiliano-gandini-outeda/schemap/issues
10
+ Keywords: python,schemas,models,orm
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.12.7
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: pydantic>=2.13.4
17
+ Requires-Dist: sqlalchemy>=2.0.49
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
20
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
21
+ Provides-Extra: example
22
+ Requires-Dist: fastapi>=0.100.0; extra == "example"
23
+ Requires-Dist: uvicorn>=0.23.0; extra == "example"
@@ -0,0 +1,11 @@
1
+ schemap/__init__.py,sha256=rfKizb9zqsS03rwcjI_fmR7Hf_mGUAy45jYYitpQLYw,81
2
+ schemap/base.py,sha256=AoLt4I4X6epfSA3oWvz39w39HAmiHFBfw4ui-UAbCQg,41
3
+ schemap/builder.py,sha256=EHMjanCt2sDQlU0zY9Pd2U7mXCW1STZFkOCwC1Bfl14,1350
4
+ schemap/types.py,sha256=mwvlg4bdBJq7INEMEVoSrvx_JbCUkJ6Z1L0orJHmrEk,2125
5
+ schemap/utils/likeness.py,sha256=5LdSTtJzoZmzaKaAg7X1PN8mclbiumGtWrHkR1Hq17c,1134
6
+ schemap/utils/mapping.py,sha256=YDG7MEOQ5GAW9B_LGQJocAtWkC9Wxs9AVFFr4X7P-No,1219
7
+ schemap/utils/schema.py,sha256=uYmXPNkCYIciUBohuU7ZNVfzfRx49S_lVwexMbBperI,2527
8
+ schemap-0.1.0.dist-info/METADATA,sha256=IAG3YeHHTSf_zSDr3Hi3HDPYcP8oxWfMkJ_GPGMA4oE,981
9
+ schemap-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ schemap-0.1.0.dist-info/top_level.txt,sha256=5zq746A8UTvlSgJspE7jLl2gpcxWNmCaBK2J62m3M-Y,8
11
+ schemap-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ schemap