From 4873138917383ccfee9b30ea811411199ed85447 Mon Sep 17 00:00:00 2001 From: pierre Date: Fri, 16 Dec 2022 12:20:02 +0100 Subject: [PATCH 1/6] Allow for recent dependencies as well. --- requirements.txt | 122 +++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/requirements.txt b/requirements.txt index bfc231b..743b803 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,61 +1,61 @@ -appdirs==1.4.4 -attrs==20.3.0 -black==20.8b1 -bleach==3.3.0 -build==0.3.1.post1 -certifi==2020.12.5 -cffi==1.14.5 -chardet==4.0.0 -check-manifest==0.46 -click==7.1.2 -colorama==0.4.4 -coverage==5.5 -cryptography==3.4.7 -distlib==0.3.1 -docutils==0.17.1 -filelock==3.0.12 -flake8==3.9.1 -greenlet==1.0.0 -idna==2.10 -importlib-metadata==4.0.1 -iniconfig==1.1.1 -isort==5.8.0 -jeepney==0.6.0 -keyring==23.0.1 -Mako==1.1.4 -Markdown==3.3.4 -MarkupSafe==1.1.1 -mccabe==0.6.1 -mypy==0.812 -mypy-extensions==0.4.3 -packaging==20.9 -pathspec==0.8.1 -pdoc3==0.9.2 -pep517==0.10.0 -pkginfo==1.7.0 -pluggy==0.13.1 -py==1.10.0 -pycodestyle==2.7.0 -pycparser==2.20 -pydantic==1.8.1 -pyflakes==2.3.1 -Pygments==2.8.1 -pyparsing==2.4.7 -pytest==6.2.3 -readme-renderer==29.0 -regex==2021.4.4 -requests==2.25.1 -requests-toolbelt==0.9.1 -rfc3986==1.4.0 -SecretStorage==3.3.1 -six==1.15.0 -SQLAlchemy==1.4.11 -toml==0.10.2 -tqdm==4.60.0 -twine==3.4.1 -typed-ast==1.4.3 -typing-extensions==3.7.4.3 -urllib3==1.26.4 -virtualenv==20.4.4 -webencodings==0.5.1 -zipp==3.4.1 +appdirs>=1.4.4 +attrs>=20.3.0 +black>=20.8b1 +bleach>=3.3.0 +build>=0.3.1.post1 +certifi>=2020.12.5 +cffi>=1.14.5 +chardet>=4.0.0 +check-manifest>=0.46 +click>=7.1.2 +colorama>=0.4.4 +coverage>=5.5 +cryptography>=3.4.7 +distlib>=0.3.1 +docutils>=0.17.1 +filelock>=3.0.12 +flake8>=3.9.1 +greenlet>=1.0.0 +idna>=2.10 +importlib-metadata>=4.0.1 +iniconfig>=1.1.1 +isort>=5.8.0 +jeepney>=0.6.0 +keyring>=23.0.1 +Mako>=1.1.4 +Markdown>=3.3.4 +MarkupSafe>=1.1.1 +mccabe>=0.6.1 +mypy>=0.812 +mypy-extensions>=0.4.3 +packaging>=20.9 +pathspec>=0.8.1 +pdoc3>=0.9.2 +pep517>=0.10.0 +pkginfo>=1.7.0 +pluggy>=0.13.1 +py>=1.10.0 +pycodestyle>=2.7.0 +pycparser>=2.20 +pydantic>=1.8.1 +pyflakes>=2.3.1 +Pygments>=2.8.1 +pyparsing>=2.4.7 +pytest>=6.2.3 +readme-renderer>=29.0 +regex>=2021.4.4 +requests>=2.25.1 +requests-toolbelt>=0.9.1 +rfc3986>=1.4.0 +SecretStorage>=3.3.1 +six>=1.15.0 +SQLAlchemy>=1.4.11 +toml>=0.10.2 +tqdm>=4.60.0 +twine>=3.4.1 +typed-ast>=1.4.3 +typing-extensions>=3.7.4.3 +urllib3>=1.26.4 +virtualenv>=20.4.4 +webencodings>=0.5.1 +zipp>=3.4.1 From 749c3b868588244d008da0f8f6a44933d586ba9a Mon Sep 17 00:00:00 2001 From: pierre Date: Fri, 16 Dec 2022 16:36:49 +0100 Subject: [PATCH 2/6] Adding possibility to create element with existing nested element. --- sqlalchemy_pydantic_orm/main.py | 42 ++++++++++++++++++------------- tests/main.py | 44 +++++++++++++++++++++++++++++++-- tests/test_to_orm_method.py | 18 +++++++++++++- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/sqlalchemy_pydantic_orm/main.py b/sqlalchemy_pydantic_orm/main.py index cd17da6..1a9d053 100644 --- a/sqlalchemy_pydantic_orm/main.py +++ b/sqlalchemy_pydantic_orm/main.py @@ -6,7 +6,7 @@ The ORMBaseSchema is an extension of the Pydantic's BaseModel. It can use the -fields defined in it's own schema to create a SQLAlchemy model, it can do that +fields defined in its own schema to create a SQLAlchemy model, it can do that by using a mandatory predefined link to a corresponding SQLAlchemy model. References: @@ -35,13 +35,13 @@ def __init__(self, **data: Any): """The init is used for validation and throwing errors where needed. Pydantic catches all ValueError's in initialization, and then outputs - the error message in a easy to read format with the specific class + the error message in an easy-to-read format with the specific class name displayed. Every error is given the "sqlalchemy-pydantic-orm" identifier to distinguish between Pydantic's or SQLAlchemy's own errors and those of this package. - For performance its better to execute the `super().__init__()` as late + For performance, it's better to execute the `super().__init__()` as late as possible, only the _orm_model check requires it to work properly. Args: @@ -88,7 +88,7 @@ def _orm_model(self) -> Type[DeclarativeMeta]: This variable/property has a leading underscore and can only be assigned as PrivateAttr (Pydantic). This is because a Pydantic schema - iterates over it's own fields and would otherwise cause problems when + iterates over its own fields and would otherwise cause problems when encountering this variable/property. Returns: @@ -96,13 +96,15 @@ def _orm_model(self) -> Type[DeclarativeMeta]: """ pass - def orm_create(self, **extra_fields: Any) -> DeclarativeMeta: + def orm_create(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: """Method to convert a (nested) pydantic schema to a SQLAlchemy model. Using the validated fields in this class, together with the defined _orm_model, this recursive methods creates a (nested) SQLAlchemy model. Args: + db (Session): + Database session used for `.add()` and `.delete()`. extra_fields (Any): Extra fields (keyword arguments) not defined in the pydantic schema used by the top level ORM model. The fields in the @@ -126,7 +128,8 @@ def orm_create(self, **extra_fields: Any) -> DeclarativeMeta: field_name = self.__fields__[field].alias value = getattr(self, field) if isinstance(value, ORMBaseSchema): # One-to-one - current_level_fields[field_name] = value.orm_create() + current_level_fields[field_name] = value.to_orm(db=db) + # current_level_fields[field_name] = value.orm_create() elif isinstance(value, SUPPORTED_ITERABLES): # One-to-many models = [] @@ -137,7 +140,8 @@ def orm_create(self, **extra_fields: Any) -> DeclarativeMeta: f"inherited from '{ORMBaseSchema.__name__}' " "(sqlalchemy-pydantic-orm)" ) - models.append(schema.orm_create()) + # models.append(schema.orm_create()) + models.append(schema.to_orm(db=db, **extra_fields)) current_level_fields[field_name] = models else: # value without relation @@ -148,12 +152,12 @@ def orm_create(self, **extra_fields: Any) -> DeclarativeMeta: def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: """Method to update a (nested) orm structure. - This method recursively updates an orm model with it's relationships. + This method recursively updates an orm model with its relationships. In one-to-many relationships, each provided item without an id gets added as new item with the `orm_create()` method. When a valid id is provided it updates the item with the `orm_update()` method. It also - keep track of the parsed database items, and afterwards deletes any + keeps track of the parsed database items, and afterwards deletes any unparsed item. Args: @@ -186,7 +190,7 @@ def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: if db_value: update_value.orm_update(db, db_value) else: - setattr(db_model, field_name, update_value.orm_create()) + setattr(db_model, field_name, update_value.orm_create(db=db)) elif isinstance(update_value, SUPPORTED_ITERABLES): # One-to-many parsed_items = set() @@ -213,7 +217,7 @@ def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: schema.orm_update(db, db_item) parsed_items.add(db_item) else: - new_item = schema.orm_create() + new_item = schema.orm_create(db=db) parsed_items.add(new_item) db_value.append(new_item) @@ -231,11 +235,11 @@ def to_orm(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: provided, it retrieves and updates that model. In contrary to the orm_create function on its own, this function does - add the newly created model to the database. So after the this method + add the newly created model to the database. So after this method has been executed you only need to call `db.commit()` after. Args: - db (Session): + db (Session): Database session used for `.add()` and `.delete()`. **extra_fields (Any): Returns: @@ -246,9 +250,7 @@ def to_orm(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: ValueError: When the provided id is not found in the database """ - id_ = getattr(self, "id", None) - if not id_ and "id" in extra_fields: # Pydantic field has priority - id_ = extra_fields["id"] + id_ = self.__detect_id(**extra_fields) if id_: db_model = db.query(self._orm_model).get(id_) if not db_model: @@ -260,7 +262,13 @@ def to_orm(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: ) self.orm_update(db, db_model) else: - db_model = self.orm_create(**extra_fields) + db_model = self.orm_create(db=db, **extra_fields) db.add(db_model) return db_model + + def __detect_id(self, **extra_fields: Any): + id_ = getattr(self, "id", None) + if not id_ and "id" in extra_fields: # Pydantic field has priority + id_ = extra_fields["id"] + return id_ diff --git a/tests/main.py b/tests/main.py index 78f1d74..b070ab7 100644 --- a/tests/main.py +++ b/tests/main.py @@ -16,6 +16,7 @@ class Parent(Base): # type: ignore name = Column(String, nullable=False) children = relationship("Child", cascade="all, delete") + phones = relationship("Phone", cascade="all, delete") car = relationship( "Car", cascade="all, delete", uselist=False, back_populates="owner" ) @@ -49,6 +50,16 @@ class Car(Base): # type: ignore owner = relationship("Parent", back_populates="car") +class Phone(Base): # type: ignore + __tablename__ = "phones" + + id = Column(Integer, primary_key=True, index=True, nullable=False) + color = Column(String, nullable=False) + owner_id = Column(Integer, ForeignKey("parents.id"), nullable=True) + + owner = relationship("Parent", back_populates="phones") + + class PydanticCar(ORMBaseSchema): id: Optional[int] colour: str = Field(alias="color") # mostly used for reserved names @@ -71,11 +82,19 @@ class PydanticChild(ORMBaseSchema): _orm_model = PrivateAttr(Child) +class PydanticPhone(ORMBaseSchema): + id: Optional[int] + color: str = Field(alias="color") # mostly used for reserved names + + _orm_model = PrivateAttr(Phone) + + class PydanticParent(ORMBaseSchema): id: Optional[int] name: str - children: List[PydanticChild] - car: PydanticCar + children: Optional[List[PydanticChild]] + phones: Optional[List[PydanticPhone]] + car: Optional[PydanticCar] _orm_model = PrivateAttr(Parent) @@ -116,6 +135,7 @@ class PydanticParent(ORMBaseSchema): }, ], "car": {"color": "Blue", "id": 1}, + 'phones': [], } orm_update_input_data = { @@ -156,4 +176,24 @@ class PydanticParent(ORMBaseSchema): }, ], "car": {"color": "Red", "id": 1}, + 'phones': [], +} + +orm_create_and_update_input_data = { + "name": "Lancelot", + "children": [], + "phones": [] +} +orm_update_input_data_phone = { + "color": "red", +} +orm_create_and_update_output_data = { + "name": "Lancelot", + "id": 2, + "children": [], + "phones": [{ + "id": 1, + "color": "red" + }], + "car": None, } diff --git a/tests/test_to_orm_method.py b/tests/test_to_orm_method.py index abd1c68..8ef1ade 100644 --- a/tests/test_to_orm_method.py +++ b/tests/test_to_orm_method.py @@ -5,10 +5,12 @@ from .main import ( Base, PydanticParent, + PydanticPhone, orm_create_input_data, orm_create_output_data, orm_update_input_data, - orm_update_output_data, + orm_update_output_data, orm_update_input_data_phone, orm_create_and_update_input_data, + orm_create_and_update_output_data, ) engine = create_engine("sqlite://", echo=False) @@ -33,3 +35,17 @@ def test_to_orm_update() -> None: db.refresh(db_model) schema_out = PydanticParent.from_orm(db_model) assert schema_out.dict(by_alias=True) == orm_update_output_data + + +def test_to_orm_create_with_update() -> None: + schema_phone_in = PydanticPhone.parse_obj(orm_update_input_data_phone) + phone_db_model = schema_phone_in.to_orm(db) + db.commit() + db.refresh(phone_db_model) + father = PydanticParent.parse_obj(orm_create_and_update_input_data) + father.phones.append(PydanticPhone.from_orm(phone_db_model)) + db_model = father.to_orm(db) + db.commit() + db.refresh(db_model) + schema_out = PydanticParent.from_orm(db_model) + assert schema_out.dict(by_alias=True) == orm_create_and_update_output_data From 52239f52b0326006614b4886346be1176b195d4b Mon Sep 17 00:00:00 2001 From: pierre Date: Fri, 16 Dec 2022 16:53:57 +0100 Subject: [PATCH 3/6] Revert "Adding possibility to create element with existing nested element." This reverts commit 749c3b868588244d008da0f8f6a44933d586ba9a. --- sqlalchemy_pydantic_orm/main.py | 42 +++++++++++++------------------ tests/main.py | 44 ++------------------------------- tests/test_to_orm_method.py | 18 +------------- 3 files changed, 20 insertions(+), 84 deletions(-) diff --git a/sqlalchemy_pydantic_orm/main.py b/sqlalchemy_pydantic_orm/main.py index 1a9d053..cd17da6 100644 --- a/sqlalchemy_pydantic_orm/main.py +++ b/sqlalchemy_pydantic_orm/main.py @@ -6,7 +6,7 @@ The ORMBaseSchema is an extension of the Pydantic's BaseModel. It can use the -fields defined in its own schema to create a SQLAlchemy model, it can do that +fields defined in it's own schema to create a SQLAlchemy model, it can do that by using a mandatory predefined link to a corresponding SQLAlchemy model. References: @@ -35,13 +35,13 @@ def __init__(self, **data: Any): """The init is used for validation and throwing errors where needed. Pydantic catches all ValueError's in initialization, and then outputs - the error message in an easy-to-read format with the specific class + the error message in a easy to read format with the specific class name displayed. Every error is given the "sqlalchemy-pydantic-orm" identifier to distinguish between Pydantic's or SQLAlchemy's own errors and those of this package. - For performance, it's better to execute the `super().__init__()` as late + For performance its better to execute the `super().__init__()` as late as possible, only the _orm_model check requires it to work properly. Args: @@ -88,7 +88,7 @@ def _orm_model(self) -> Type[DeclarativeMeta]: This variable/property has a leading underscore and can only be assigned as PrivateAttr (Pydantic). This is because a Pydantic schema - iterates over its own fields and would otherwise cause problems when + iterates over it's own fields and would otherwise cause problems when encountering this variable/property. Returns: @@ -96,15 +96,13 @@ def _orm_model(self) -> Type[DeclarativeMeta]: """ pass - def orm_create(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: + def orm_create(self, **extra_fields: Any) -> DeclarativeMeta: """Method to convert a (nested) pydantic schema to a SQLAlchemy model. Using the validated fields in this class, together with the defined _orm_model, this recursive methods creates a (nested) SQLAlchemy model. Args: - db (Session): - Database session used for `.add()` and `.delete()`. extra_fields (Any): Extra fields (keyword arguments) not defined in the pydantic schema used by the top level ORM model. The fields in the @@ -128,8 +126,7 @@ def orm_create(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: field_name = self.__fields__[field].alias value = getattr(self, field) if isinstance(value, ORMBaseSchema): # One-to-one - current_level_fields[field_name] = value.to_orm(db=db) - # current_level_fields[field_name] = value.orm_create() + current_level_fields[field_name] = value.orm_create() elif isinstance(value, SUPPORTED_ITERABLES): # One-to-many models = [] @@ -140,8 +137,7 @@ def orm_create(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: f"inherited from '{ORMBaseSchema.__name__}' " "(sqlalchemy-pydantic-orm)" ) - # models.append(schema.orm_create()) - models.append(schema.to_orm(db=db, **extra_fields)) + models.append(schema.orm_create()) current_level_fields[field_name] = models else: # value without relation @@ -152,12 +148,12 @@ def orm_create(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: """Method to update a (nested) orm structure. - This method recursively updates an orm model with its relationships. + This method recursively updates an orm model with it's relationships. In one-to-many relationships, each provided item without an id gets added as new item with the `orm_create()` method. When a valid id is provided it updates the item with the `orm_update()` method. It also - keeps track of the parsed database items, and afterwards deletes any + keep track of the parsed database items, and afterwards deletes any unparsed item. Args: @@ -190,7 +186,7 @@ def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: if db_value: update_value.orm_update(db, db_value) else: - setattr(db_model, field_name, update_value.orm_create(db=db)) + setattr(db_model, field_name, update_value.orm_create()) elif isinstance(update_value, SUPPORTED_ITERABLES): # One-to-many parsed_items = set() @@ -217,7 +213,7 @@ def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: schema.orm_update(db, db_item) parsed_items.add(db_item) else: - new_item = schema.orm_create(db=db) + new_item = schema.orm_create() parsed_items.add(new_item) db_value.append(new_item) @@ -235,11 +231,11 @@ def to_orm(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: provided, it retrieves and updates that model. In contrary to the orm_create function on its own, this function does - add the newly created model to the database. So after this method + add the newly created model to the database. So after the this method has been executed you only need to call `db.commit()` after. Args: - db (Session): Database session used for `.add()` and `.delete()`. + db (Session): **extra_fields (Any): Returns: @@ -250,7 +246,9 @@ def to_orm(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: ValueError: When the provided id is not found in the database """ - id_ = self.__detect_id(**extra_fields) + id_ = getattr(self, "id", None) + if not id_ and "id" in extra_fields: # Pydantic field has priority + id_ = extra_fields["id"] if id_: db_model = db.query(self._orm_model).get(id_) if not db_model: @@ -262,13 +260,7 @@ def to_orm(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: ) self.orm_update(db, db_model) else: - db_model = self.orm_create(db=db, **extra_fields) + db_model = self.orm_create(**extra_fields) db.add(db_model) return db_model - - def __detect_id(self, **extra_fields: Any): - id_ = getattr(self, "id", None) - if not id_ and "id" in extra_fields: # Pydantic field has priority - id_ = extra_fields["id"] - return id_ diff --git a/tests/main.py b/tests/main.py index b070ab7..78f1d74 100644 --- a/tests/main.py +++ b/tests/main.py @@ -16,7 +16,6 @@ class Parent(Base): # type: ignore name = Column(String, nullable=False) children = relationship("Child", cascade="all, delete") - phones = relationship("Phone", cascade="all, delete") car = relationship( "Car", cascade="all, delete", uselist=False, back_populates="owner" ) @@ -50,16 +49,6 @@ class Car(Base): # type: ignore owner = relationship("Parent", back_populates="car") -class Phone(Base): # type: ignore - __tablename__ = "phones" - - id = Column(Integer, primary_key=True, index=True, nullable=False) - color = Column(String, nullable=False) - owner_id = Column(Integer, ForeignKey("parents.id"), nullable=True) - - owner = relationship("Parent", back_populates="phones") - - class PydanticCar(ORMBaseSchema): id: Optional[int] colour: str = Field(alias="color") # mostly used for reserved names @@ -82,19 +71,11 @@ class PydanticChild(ORMBaseSchema): _orm_model = PrivateAttr(Child) -class PydanticPhone(ORMBaseSchema): - id: Optional[int] - color: str = Field(alias="color") # mostly used for reserved names - - _orm_model = PrivateAttr(Phone) - - class PydanticParent(ORMBaseSchema): id: Optional[int] name: str - children: Optional[List[PydanticChild]] - phones: Optional[List[PydanticPhone]] - car: Optional[PydanticCar] + children: List[PydanticChild] + car: PydanticCar _orm_model = PrivateAttr(Parent) @@ -135,7 +116,6 @@ class PydanticParent(ORMBaseSchema): }, ], "car": {"color": "Blue", "id": 1}, - 'phones': [], } orm_update_input_data = { @@ -176,24 +156,4 @@ class PydanticParent(ORMBaseSchema): }, ], "car": {"color": "Red", "id": 1}, - 'phones': [], -} - -orm_create_and_update_input_data = { - "name": "Lancelot", - "children": [], - "phones": [] -} -orm_update_input_data_phone = { - "color": "red", -} -orm_create_and_update_output_data = { - "name": "Lancelot", - "id": 2, - "children": [], - "phones": [{ - "id": 1, - "color": "red" - }], - "car": None, } diff --git a/tests/test_to_orm_method.py b/tests/test_to_orm_method.py index 8ef1ade..abd1c68 100644 --- a/tests/test_to_orm_method.py +++ b/tests/test_to_orm_method.py @@ -5,12 +5,10 @@ from .main import ( Base, PydanticParent, - PydanticPhone, orm_create_input_data, orm_create_output_data, orm_update_input_data, - orm_update_output_data, orm_update_input_data_phone, orm_create_and_update_input_data, - orm_create_and_update_output_data, + orm_update_output_data, ) engine = create_engine("sqlite://", echo=False) @@ -35,17 +33,3 @@ def test_to_orm_update() -> None: db.refresh(db_model) schema_out = PydanticParent.from_orm(db_model) assert schema_out.dict(by_alias=True) == orm_update_output_data - - -def test_to_orm_create_with_update() -> None: - schema_phone_in = PydanticPhone.parse_obj(orm_update_input_data_phone) - phone_db_model = schema_phone_in.to_orm(db) - db.commit() - db.refresh(phone_db_model) - father = PydanticParent.parse_obj(orm_create_and_update_input_data) - father.phones.append(PydanticPhone.from_orm(phone_db_model)) - db_model = father.to_orm(db) - db.commit() - db.refresh(db_model) - schema_out = PydanticParent.from_orm(db_model) - assert schema_out.dict(by_alias=True) == orm_create_and_update_output_data From a5ce484d35839a44055124fe3957744d33cfe728 Mon Sep 17 00:00:00 2001 From: pierre Date: Fri, 16 Dec 2022 16:59:50 +0100 Subject: [PATCH 4/6] revert change in that branch --- requirements.txt | 122 +++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/requirements.txt b/requirements.txt index 743b803..bfc231b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,61 +1,61 @@ -appdirs>=1.4.4 -attrs>=20.3.0 -black>=20.8b1 -bleach>=3.3.0 -build>=0.3.1.post1 -certifi>=2020.12.5 -cffi>=1.14.5 -chardet>=4.0.0 -check-manifest>=0.46 -click>=7.1.2 -colorama>=0.4.4 -coverage>=5.5 -cryptography>=3.4.7 -distlib>=0.3.1 -docutils>=0.17.1 -filelock>=3.0.12 -flake8>=3.9.1 -greenlet>=1.0.0 -idna>=2.10 -importlib-metadata>=4.0.1 -iniconfig>=1.1.1 -isort>=5.8.0 -jeepney>=0.6.0 -keyring>=23.0.1 -Mako>=1.1.4 -Markdown>=3.3.4 -MarkupSafe>=1.1.1 -mccabe>=0.6.1 -mypy>=0.812 -mypy-extensions>=0.4.3 -packaging>=20.9 -pathspec>=0.8.1 -pdoc3>=0.9.2 -pep517>=0.10.0 -pkginfo>=1.7.0 -pluggy>=0.13.1 -py>=1.10.0 -pycodestyle>=2.7.0 -pycparser>=2.20 -pydantic>=1.8.1 -pyflakes>=2.3.1 -Pygments>=2.8.1 -pyparsing>=2.4.7 -pytest>=6.2.3 -readme-renderer>=29.0 -regex>=2021.4.4 -requests>=2.25.1 -requests-toolbelt>=0.9.1 -rfc3986>=1.4.0 -SecretStorage>=3.3.1 -six>=1.15.0 -SQLAlchemy>=1.4.11 -toml>=0.10.2 -tqdm>=4.60.0 -twine>=3.4.1 -typed-ast>=1.4.3 -typing-extensions>=3.7.4.3 -urllib3>=1.26.4 -virtualenv>=20.4.4 -webencodings>=0.5.1 -zipp>=3.4.1 +appdirs==1.4.4 +attrs==20.3.0 +black==20.8b1 +bleach==3.3.0 +build==0.3.1.post1 +certifi==2020.12.5 +cffi==1.14.5 +chardet==4.0.0 +check-manifest==0.46 +click==7.1.2 +colorama==0.4.4 +coverage==5.5 +cryptography==3.4.7 +distlib==0.3.1 +docutils==0.17.1 +filelock==3.0.12 +flake8==3.9.1 +greenlet==1.0.0 +idna==2.10 +importlib-metadata==4.0.1 +iniconfig==1.1.1 +isort==5.8.0 +jeepney==0.6.0 +keyring==23.0.1 +Mako==1.1.4 +Markdown==3.3.4 +MarkupSafe==1.1.1 +mccabe==0.6.1 +mypy==0.812 +mypy-extensions==0.4.3 +packaging==20.9 +pathspec==0.8.1 +pdoc3==0.9.2 +pep517==0.10.0 +pkginfo==1.7.0 +pluggy==0.13.1 +py==1.10.0 +pycodestyle==2.7.0 +pycparser==2.20 +pydantic==1.8.1 +pyflakes==2.3.1 +Pygments==2.8.1 +pyparsing==2.4.7 +pytest==6.2.3 +readme-renderer==29.0 +regex==2021.4.4 +requests==2.25.1 +requests-toolbelt==0.9.1 +rfc3986==1.4.0 +SecretStorage==3.3.1 +six==1.15.0 +SQLAlchemy==1.4.11 +toml==0.10.2 +tqdm==4.60.0 +twine==3.4.1 +typed-ast==1.4.3 +typing-extensions==3.7.4.3 +urllib3==1.26.4 +virtualenv==20.4.4 +webencodings==0.5.1 +zipp==3.4.1 From cfaed71b4b57b8ac270e5f0e40274ed55f56dcb4 Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 3 Jan 2023 14:24:12 +0100 Subject: [PATCH 5/6] in some cases __fields_set__ contains less info than __fields__ Also, when a value is optional, it might not be provided and no value should be added. --- sqlalchemy_pydantic_orm/main.py | 132 ++++++++++++++++---------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/sqlalchemy_pydantic_orm/main.py b/sqlalchemy_pydantic_orm/main.py index 1a9d053..6883200 100644 --- a/sqlalchemy_pydantic_orm/main.py +++ b/sqlalchemy_pydantic_orm/main.py @@ -124,28 +124,29 @@ def orm_create(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: When a list is not fully consisted of other ORM schemas. """ current_level_fields = {} - for field in self.__fields_set__: - field_name = self.__fields__[field].alias - value = getattr(self, field) - if isinstance(value, ORMBaseSchema): # One-to-one - current_level_fields[field_name] = value.to_orm(db=db) - # current_level_fields[field_name] = value.orm_create() - - elif isinstance(value, SUPPORTED_ITERABLES): # One-to-many - models = [] - for schema in value: - if not isinstance(schema, ORMBaseSchema): - raise TypeError( - "Lists should only contain other schemas " - f"inherited from '{ORMBaseSchema.__name__}' " - "(sqlalchemy-pydantic-orm)" - ) - # models.append(schema.orm_create()) - models.append(schema.to_orm(db=db, **extra_fields)) - current_level_fields[field_name] = models - - else: # value without relation - current_level_fields[field_name] = value + for key, field in self.__fields__.items(): + field_name = field.alias + value = getattr(self, key) + if value is not None: + # nullable value not provided must be passed. + if isinstance(value, ORMBaseSchema): # One-to-one + current_level_fields[field_name] = value.to_orm(db=db) + # current_level_fields[field_name] = value.orm_create() + elif isinstance(value, SUPPORTED_ITERABLES): # One-to-many + models = [] + for schema in value: + if not isinstance(schema, ORMBaseSchema): + raise TypeError( + "Lists should only contain other schemas " + f"inherited from '{ORMBaseSchema.__name__}' " + "(sqlalchemy-pydantic-orm)" + ) + # models.append(schema.orm_create()) + models.append(schema.to_orm(db=db, **extra_fields)) + current_level_fields[field_name] = models + + else: # value without relation + current_level_fields[field_name] = value return self._orm_model(**extra_fields, **current_level_fields) @@ -182,50 +183,51 @@ def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: f"defined _orm_model '{self._orm_model.__name__}' " "(sqlalchemy-pydantic-orm)" ) - for field in self.__fields_set__: - field_name = self.__fields__[field].alias - db_value = getattr(db_model, field_name) - update_value = getattr(self, field) - if isinstance(update_value, ORMBaseSchema): # One-to-one - if db_value: - update_value.orm_update(db, db_value) - else: - setattr(db_model, field_name, update_value.orm_create(db=db)) - - elif isinstance(update_value, SUPPORTED_ITERABLES): # One-to-many - parsed_items = set() - for schema in update_value: - if not isinstance(schema, ORMBaseSchema): - raise TypeError( - "Lists should only contain other schemas " - f"inherited from '{ORMBaseSchema.__name__}' " - "(sqlalchemy-pydantic-orm)" - ) - if item_id := getattr(schema, "id", None): - try: - db_item = next( - item for item in db_value if item.id == item_id - ) - except StopIteration: - raise ValueError( - f"Provided id '{item_id}' " - f"for field '{field_name}' " - "can't be found in the database " - "(sqlalchemy-pydantic-orm)" - ) from None # removes unnecessary traceback - - schema.orm_update(db, db_item) - parsed_items.add(db_item) + for key, field in self.__fields__.items(): + field_name = field.alias + db_value = getattr(db_model, key) + update_value = getattr(self, key) + if update_value is not None: + if isinstance(update_value, ORMBaseSchema): # One-to-one + if db_value: + update_value.orm_update(db, db_value) else: - new_item = schema.orm_create(db=db) - parsed_items.add(new_item) - db_value.append(new_item) - - for db_item in db_value: - if db_item not in parsed_items: - db.delete(db_item) - else: - setattr(db_model, field_name, update_value) + setattr(db_model, field_name, update_value.orm_create(db=db)) + + elif isinstance(update_value, SUPPORTED_ITERABLES): # One-to-many + parsed_items = set() + for schema in update_value: + if not isinstance(schema, ORMBaseSchema): + raise TypeError( + "Lists should only contain other schemas " + f"inherited from '{ORMBaseSchema.__name__}' " + "(sqlalchemy-pydantic-orm)" + ) + if item_id := getattr(schema, "id", None): + try: + db_item = next( + item for item in db_value if item.id == item_id + ) + except StopIteration: + raise ValueError( + f"Provided id '{item_id}' " + f"for field '{field_name}' " + "can't be found in the database " + "(sqlalchemy-pydantic-orm)" + ) from None # removes unnecessary traceback + + schema.orm_update(db, db_item) + parsed_items.add(db_item) + else: + new_item = schema.orm_create(db=db) + parsed_items.add(new_item) + db_value.append(new_item) + + for db_item in db_value: + if db_item not in parsed_items: + db.delete(db_item) + else: + setattr(db_model, field_name, update_value) def to_orm(self, db: Session, **extra_fields: Any) -> DeclarativeMeta: """Method that combines the functionality of orm_create & orm_update. From dc881b37fbe895467b1262e32798a71978a172d9 Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 3 Jan 2023 17:47:49 +0100 Subject: [PATCH 6/6] Correction of 2 small bugs in tests --- sqlalchemy_pydantic_orm/main.py | 2 +- tests/test_orm_specific_methods.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_pydantic_orm/main.py b/sqlalchemy_pydantic_orm/main.py index 6883200..76c71ba 100644 --- a/sqlalchemy_pydantic_orm/main.py +++ b/sqlalchemy_pydantic_orm/main.py @@ -185,7 +185,7 @@ def orm_update(self, db: Session, db_model: DeclarativeMeta) -> None: ) for key, field in self.__fields__.items(): field_name = field.alias - db_value = getattr(db_model, key) + db_value = getattr(db_model, field_name) update_value = getattr(self, key) if update_value is not None: if isinstance(update_value, ORMBaseSchema): # One-to-one diff --git a/tests/test_orm_specific_methods.py b/tests/test_orm_specific_methods.py index 6343234..434006e 100644 --- a/tests/test_orm_specific_methods.py +++ b/tests/test_orm_specific_methods.py @@ -20,7 +20,7 @@ def test_orm_create() -> None: schema_in = PydanticParent.parse_obj(orm_create_input_data) - db_model = schema_in.orm_create() + db_model = schema_in.orm_create(db) db.add(db_model) db.commit() db.refresh(db_model)