generated from daniil-berg/boilerplate-py
Compare commits
No commits in common. "a64708dedddb1053ec7fb2c57980e7eb7b6f424d" and "dbf304fc2cc3041cbaea68e641069bb6fc026452" have entirely different histories.
a64708dedd
...
dbf304fc2c
@ -1,2 +0,0 @@
|
|||||||
Pydantic
|
|
||||||
SQLAlchemy
|
|
16
setup.cfg
16
setup.cfg
@ -1,8 +1,8 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = orm2pydantic
|
name = orm2pydantic
|
||||||
version = 0.1.0
|
version = 0.0.1
|
||||||
author = Daniil Fajnberg
|
author = Daniil
|
||||||
author_email = mail@daniil.fajnberg.de
|
author_email = mail@placeholder123.to
|
||||||
description = Convert SQLAlchemy models to Pydantic models
|
description = Convert SQLAlchemy models to Pydantic models
|
||||||
long_description = file: README.md
|
long_description = file: README.md
|
||||||
long_description_content_type = text/markdown
|
long_description_content_type = text/markdown
|
||||||
@ -10,20 +10,16 @@ url = https://git.fajnberg.de/daniil/orm2pydantic
|
|||||||
project_urls =
|
project_urls =
|
||||||
Bug Tracker = https://git.fajnberg.de/daniil/orm2pydantic/issues
|
Bug Tracker = https://git.fajnberg.de/daniil/orm2pydantic/issues
|
||||||
classifiers =
|
classifiers =
|
||||||
Development Status :: 3 - Alpha
|
Programming Language :: Python :: 3
|
||||||
Operating System :: OS Independent
|
Operating System :: OS Independent
|
||||||
Programming Language :: Python :: 3 :: Only
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
Programming Language :: Python :: 3.10
|
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
package_dir =
|
package_dir =
|
||||||
= src
|
= src
|
||||||
packages = find:
|
packages = find:
|
||||||
python_requires = >=3.9, <4.0
|
python_requires = >=3
|
||||||
install_requires =
|
install_requires =
|
||||||
Pydantic
|
...
|
||||||
SQLAlchemy
|
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
dev =
|
dev =
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
from typing import Container, Type
|
|
||||||
|
|
||||||
from pydantic import create_model, BaseConfig, Field
|
|
||||||
from pydantic.fields import FieldInfo
|
|
||||||
|
|
||||||
from sqlalchemy.inspection import inspect
|
|
||||||
from sqlalchemy.orm import ColumnProperty, RelationshipProperty, Mapper
|
|
||||||
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
|
||||||
from sqlalchemy.sql.schema import Column, ColumnDefault
|
|
||||||
|
|
||||||
from .utils import resolve_dotted_path
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'field_from_column',
|
|
||||||
'from_sqla'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
FieldDef = tuple[type, FieldInfo]
|
|
||||||
|
|
||||||
|
|
||||||
_local_namespace = {}
|
|
||||||
|
|
||||||
|
|
||||||
class OrmConfig(BaseConfig):
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
def field_from_column(column: Column) -> FieldDef:
|
|
||||||
try:
|
|
||||||
field_type = column.type.impl.python_type
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
field_type = column.type.python_type
|
|
||||||
except AttributeError:
|
|
||||||
raise AssertionError(f"Could not infer Python type for {column.key}")
|
|
||||||
default = ... if column.default is None and not column.nullable else column.default
|
|
||||||
if isinstance(default, ColumnDefault):
|
|
||||||
if default.is_scalar:
|
|
||||||
field_info = Field(default=default.arg)
|
|
||||||
else:
|
|
||||||
assert callable(default.arg)
|
|
||||||
dotted_path = default.arg.__module__ + '.' + default.arg.__name__
|
|
||||||
factory = resolve_dotted_path(dotted_path)
|
|
||||||
assert callable(factory)
|
|
||||||
field_info = Field(default_factory=factory)
|
|
||||||
else:
|
|
||||||
field_info = Field(default=default)
|
|
||||||
return field_type, field_info
|
|
||||||
|
|
||||||
|
|
||||||
def from_sqla(db_model: Type[DeclarativeMeta], incl_many_to_one: bool = True, incl_one_to_many: bool = False,
|
|
||||||
config: Type[BaseConfig] = OrmConfig, exclude: Container[str] = (),
|
|
||||||
add_fields: dict[str, FieldDef] = None):
|
|
||||||
assert isinstance(db_model, DeclarativeMeta)
|
|
||||||
assert not (incl_one_to_many and incl_many_to_one)
|
|
||||||
fields = {}
|
|
||||||
for attr in inspect(db_model).attrs:
|
|
||||||
if attr.key in exclude:
|
|
||||||
continue
|
|
||||||
if isinstance(attr, ColumnProperty):
|
|
||||||
assert len(attr.columns) == 1
|
|
||||||
column = attr.columns[0]
|
|
||||||
fields[attr.key] = field_from_column(column)
|
|
||||||
elif isinstance(attr, RelationshipProperty):
|
|
||||||
related = attr.mapper
|
|
||||||
assert isinstance(related, Mapper)
|
|
||||||
if incl_many_to_one and attr.direction.name == 'MANYTOONE':
|
|
||||||
fields[attr.key] = (related.class_.__name__, Field(default=None))
|
|
||||||
if incl_one_to_many and attr.direction.name == 'ONETOMANY':
|
|
||||||
fields[attr.key] = (list[related.class_.__name__], Field(default=None))
|
|
||||||
else:
|
|
||||||
raise AssertionError("Unknown attr type", attr)
|
|
||||||
if add_fields is not None:
|
|
||||||
fields |= add_fields
|
|
||||||
name = db_model.__name__
|
|
||||||
pydantic_model = create_model(name, __config__=config, **fields)
|
|
||||||
pydantic_model.__name__ = name
|
|
||||||
pydantic_model.update_forward_refs(**_local_namespace)
|
|
||||||
_local_namespace[name] = pydantic_model
|
|
||||||
return pydantic_model
|
|
@ -1,20 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_dotted_path(dotted_path: str) -> object:
|
|
||||||
"""
|
|
||||||
Resolves a dotted path to a global object and returns that object.
|
|
||||||
|
|
||||||
Algorithm shamelessly stolen from the `logging.config` module from the standard library.
|
|
||||||
"""
|
|
||||||
names = dotted_path.split('.')
|
|
||||||
module_name = names.pop(0)
|
|
||||||
found = import_module(module_name)
|
|
||||||
for name in names:
|
|
||||||
try:
|
|
||||||
found = getattr(found, name)
|
|
||||||
except AttributeError:
|
|
||||||
module_name += f'.{name}'
|
|
||||||
import_module(module_name)
|
|
||||||
found = getattr(found, name)
|
|
||||||
return found
|
|
@ -1,93 +0,0 @@
|
|||||||
from sqlalchemy.engine.create import create_engine
|
|
||||||
from sqlalchemy.orm import declarative_base, relationship
|
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
from sqlalchemy.sql.functions import now as db_now
|
|
||||||
from sqlalchemy.sql.schema import Column, ForeignKey as FKey
|
|
||||||
from sqlalchemy.sql.sqltypes import Integer, String, TIMESTAMP, Unicode
|
|
||||||
|
|
||||||
from orm2pydantic.sqla import from_sqla
|
|
||||||
|
|
||||||
|
|
||||||
ORMBase = declarative_base()
|
|
||||||
engine = create_engine("sqlite://")
|
|
||||||
|
|
||||||
|
|
||||||
def default_factory() -> str: return '1'
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractBase(ORMBase):
|
|
||||||
__abstract__ = True
|
|
||||||
|
|
||||||
NON_REPR_FIELDS = ['id', 'date_created', 'date_updated']
|
|
||||||
|
|
||||||
date_created = Column(TIMESTAMP(timezone=False), server_default=db_now())
|
|
||||||
date_updated = Column(TIMESTAMP(timezone=False), server_default=db_now(), onupdate=db_now())
|
|
||||||
|
|
||||||
|
|
||||||
class StateProvince(AbstractBase):
|
|
||||||
__tablename__ = 'state_province'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
country = Column(String(2), nullable=False, index=True)
|
|
||||||
name = Column(Unicode(255), nullable=False, index=True)
|
|
||||||
|
|
||||||
cities = relationship('City', backref='state_province', lazy='selectin')
|
|
||||||
|
|
||||||
|
|
||||||
class City(AbstractBase):
|
|
||||||
__tablename__ = 'city'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
state_province_id = Column(Integer, FKey('state_province.id', ondelete='RESTRICT'), index=True)
|
|
||||||
zip_code = Column(String(5), nullable=False, index=True)
|
|
||||||
name = Column(Unicode(255), nullable=False, index=True)
|
|
||||||
|
|
||||||
streets = relationship('Street', backref='city', lazy='selectin')
|
|
||||||
|
|
||||||
|
|
||||||
class Street(AbstractBase):
|
|
||||||
__tablename__ = 'street'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
city_id = Column(Integer, FKey('city.id', ondelete='RESTRICT'), index=True)
|
|
||||||
name = Column(Unicode(255), nullable=False, index=True)
|
|
||||||
|
|
||||||
addresses = relationship('Address', backref='street', lazy='selectin')
|
|
||||||
|
|
||||||
|
|
||||||
class Address(AbstractBase):
|
|
||||||
__tablename__ = 'address'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
street_id = Column(Integer, FKey('street.id', ondelete='RESTRICT'), index=True)
|
|
||||||
house_number = Column(String(8), nullable=False, default=default_factory)
|
|
||||||
supplement = Column(String(255))
|
|
||||||
|
|
||||||
|
|
||||||
def main_test() -> None:
|
|
||||||
AbstractBase.metadata.create_all(engine)
|
|
||||||
|
|
||||||
from_sqla(StateProvince)
|
|
||||||
from_sqla(City)
|
|
||||||
from_sqla(Street)
|
|
||||||
_PydanticAddress = from_sqla(Address)
|
|
||||||
|
|
||||||
with Session(engine) as session:
|
|
||||||
bavaria = StateProvince(country="de", name="Bavaria")
|
|
||||||
munich = City(zip_code='80333', name="Munich")
|
|
||||||
bavaria.cities.append(munich)
|
|
||||||
maximilian_street = Street(name="Maximilianstrasse")
|
|
||||||
munich.streets.append(maximilian_street)
|
|
||||||
some_address = Address()
|
|
||||||
maximilian_street.addresses.append(some_address)
|
|
||||||
session.add_all([bavaria, munich, maximilian_street, some_address])
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
address = _PydanticAddress.from_orm(some_address)
|
|
||||||
|
|
||||||
assert address.house_number == '1'
|
|
||||||
assert address.street.city.state_province.name == "Bavaria"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main_test()
|
|
Loading…
x
Reference in New Issue
Block a user