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 +4 -0
- schemap/base.py +1 -0
- schemap/builder.py +44 -0
- schemap/types.py +64 -0
- schemap/utils/likeness.py +49 -0
- schemap/utils/mapping.py +57 -0
- schemap/utils/schema.py +78 -0
- schemap-0.1.0.dist-info/METADATA +23 -0
- schemap-0.1.0.dist-info/RECORD +11 -0
- schemap-0.1.0.dist-info/WHEEL +5 -0
- schemap-0.1.0.dist-info/top_level.txt +1 -0
schemap/__init__.py
ADDED
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
|
schemap/utils/mapping.py
ADDED
|
@@ -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
|
+
}
|
schemap/utils/schema.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
schemap
|