From 75e6bf3b765c729d5f92384dbf4c6ea78f3c0760 Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 28 Jan 2024 15:26:25 +0200 Subject: [PATCH 01/10] fixed merge --- .tox/py36/.tox-info.json | 6 ++ .tox/py37/.tox-info.json | 6 ++ .tox/py38/.tox-info.json | 6 ++ .tox/py39/.tox-info.json | 6 ++ sqlalchemy_mixins/__init__.py | 19 +---- sqlalchemy_mixins/activerecordasync.py | 105 ++++++++++++++++++++++++ sqlalchemy_mixins/activerecordasync.pyi | 39 +++++++++ sqlalchemy_mixins/session.py | 9 +- sqlalchemy_mixins/session.pyi | 5 +- sqlalchemy_mixins/smartquery.py | 6 ++ 10 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 .tox/py36/.tox-info.json create mode 100644 .tox/py37/.tox-info.json create mode 100644 .tox/py38/.tox-info.json create mode 100644 .tox/py39/.tox-info.json create mode 100644 sqlalchemy_mixins/activerecordasync.py create mode 100644 sqlalchemy_mixins/activerecordasync.pyi diff --git a/.tox/py36/.tox-info.json b/.tox/py36/.tox-info.json new file mode 100644 index 0000000..e13528d --- /dev/null +++ b/.tox/py36/.tox-info.json @@ -0,0 +1,6 @@ +{ + "ToxEnv": { + "name": "py36", + "type": "VirtualEnvRunner" + } +} \ No newline at end of file diff --git a/.tox/py37/.tox-info.json b/.tox/py37/.tox-info.json new file mode 100644 index 0000000..8b0e311 --- /dev/null +++ b/.tox/py37/.tox-info.json @@ -0,0 +1,6 @@ +{ + "ToxEnv": { + "name": "py37", + "type": "VirtualEnvRunner" + } +} \ No newline at end of file diff --git a/.tox/py38/.tox-info.json b/.tox/py38/.tox-info.json new file mode 100644 index 0000000..f0f05cb --- /dev/null +++ b/.tox/py38/.tox-info.json @@ -0,0 +1,6 @@ +{ + "ToxEnv": { + "name": "py38", + "type": "VirtualEnvRunner" + } +} \ No newline at end of file diff --git a/.tox/py39/.tox-info.json b/.tox/py39/.tox-info.json new file mode 100644 index 0000000..0cadca2 --- /dev/null +++ b/.tox/py39/.tox-info.json @@ -0,0 +1,6 @@ +{ + "ToxEnv": { + "name": "py39", + "type": "VirtualEnvRunner" + } +} \ No newline at end of file diff --git a/sqlalchemy_mixins/__init__.py b/sqlalchemy_mixins/__init__.py index 42814dc..58e8be1 100644 --- a/sqlalchemy_mixins/__init__.py +++ b/sqlalchemy_mixins/__init__.py @@ -4,6 +4,7 @@ # high-level mixins from .activerecord import ActiveRecordMixin, ModelNotFoundError +from .activerecordasync import ActiveRecordMixinAsync from .smartquery import SmartQueryMixin, smart_query from .eagerload import EagerLoadMixin, JOINED, SUBQUERY from .repr import ReprMixin @@ -17,18 +18,6 @@ class AllFeaturesMixin(ActiveRecordMixin, SmartQueryMixin, ReprMixin, SerializeM __repr__ = ReprMixin.__repr__ -__all__ = [ - "ActiveRecordMixin", - "AllFeaturesMixin", - "EagerLoadMixin", - "InspectionMixin", - "JOINED", - "ModelNotFoundError", - "ReprMixin", - "SerializeMixin", - "SessionMixin", - "smart_query", - "SmartQueryMixin", - "SUBQUERY", - "TimestampsMixin", -] +class AllFeaturesMixinAsync(ActiveRecordMixinAsync, SmartQueryMixin, ReprMixin, SerializeMixin): + __abstract__ = True + __repr__ = ReprMixin.__repr__ diff --git a/sqlalchemy_mixins/activerecordasync.py b/sqlalchemy_mixins/activerecordasync.py new file mode 100644 index 0000000..348b4a0 --- /dev/null +++ b/sqlalchemy_mixins/activerecordasync.py @@ -0,0 +1,105 @@ +from .utils import classproperty +from .session import SessionMixin +from .inspection import InspectionMixin +from .activerecord import ModelNotFoundError + + +class ActiveRecordMixinAsync(InspectionMixin, SessionMixin): + __abstract__ = True + + @classproperty + def settable_attributes(cls): + return cls.columns + cls.hybrid_properties + cls.settable_relations + + async def fill(self, **kwargs): + for name in kwargs.keys(): + if name in self.settable_attributes: + setattr(self, name, kwargs[name]) + else: + raise KeyError("Attribute '{}' doesn't exist".format(name)) + + return self + + async def save(self): + """Saves the updated model to the current entity db. + """ + try: + async with self.session() as session: + session.add(self) + await session.commit() + return self + except: + async with self.session() as session: + await session.rollback() + raise + + @classmethod + async def create(cls, **kwargs): + """Create and persist a new record for the model + :param kwargs: attributes for the record + :return: the new model instance + """ + return await cls().fill(**kwargs).save() + + async def update(self, **kwargs): + """Same as :meth:`fill` method but persists changes to database. + """ + return await self.fill(**kwargs).save() + + async def delete(self): + """Removes the model from the current entity session and mark for deletion. + """ + try: + async with self.session() as session: + session.delete(self) + await session.commit() + except: + async with self.session() as session: + await session.rollback() + raise + + @classmethod + async def destroy(cls, *ids): + """Delete the records with the given ids + :type ids: list + :param ids: primary key ids of records + """ + for pk in ids: + obj = await cls.find(pk) + if obj: + await obj.delete() + async with cls.session() as session: + await session.flush() + + @classmethod + async def all(cls): + async with cls.session() as session: + result = await session.execute(cls.query) + return result.scalars().all() + + @classmethod + async def first(cls): + async with cls.session() as session: + result = await session.execute(cls.query) + return result.scalars().first() + + @classmethod + async def find(cls, id_): + """Find record by the id + :param id_: the primary key + """ + async with cls.session() as session: + return await session.get(cls, id_) + + + @classmethod + async def find_or_fail(cls, id_): + # assume that query has custom get_or_fail method + result = await cls.find(id_) + if result: + return result + else: + raise ModelNotFoundError("{} with id '{}' was not found" + .format(cls.__name__, id_)) + + diff --git a/sqlalchemy_mixins/activerecordasync.pyi b/sqlalchemy_mixins/activerecordasync.pyi new file mode 100644 index 0000000..76c8946 --- /dev/null +++ b/sqlalchemy_mixins/activerecordasync.pyi @@ -0,0 +1,39 @@ +from typing import List, Any, Optional + +from sqlalchemy_mixins.inspection import InspectionMixin +from sqlalchemy_mixins.session import SessionMixin +from sqlalchemy_mixins.utils import classproperty + + +class ModelNotFoundError(ValueError): ... + +class ActiveRecordMixinAsync(InspectionMixin, SessionMixin): + + @classproperty + def settable_attributes(cls) -> List[str]: ... + + async def fill(self, **kwargs: Any) -> "ActiveRecordMixinAsync": ... + + async def save(self) -> "ActiveRecordMixinAsync": ... + + @classmethod + async def create(cls, **kwargs: Any) -> "ActiveRecordMixinAsync": ... + + async def update(self, **kwargs: dict) -> "ActiveRecordMixinAsync": ... + + async def delete(self) -> None: ... + + @classmethod + async def destroy(cls, *ids: list) -> None: ... + + @classmethod + async def all(cls) -> List["ActiveRecordMixinAsync"]: ... + + @classmethod + async def first(cls) -> Optional["ActiveRecordMixinAsync"]: ... + + @classmethod + async def find(cls, id_: Any) -> Optional["ActiveRecordMixinAsync"]: ... + + @classmethod + async def find_or_fail(cls, id_: Any) -> "ActiveRecordMixinAsync": ... \ No newline at end of file diff --git a/sqlalchemy_mixins/session.py b/sqlalchemy_mixins/session.py index 3c0dbeb..284f06d 100644 --- a/sqlalchemy_mixins/session.py +++ b/sqlalchemy_mixins/session.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import Session, scoped_session, Query +from sqlalchemy import select from .utils import classproperty @@ -8,13 +9,15 @@ class NoSessionError(RuntimeError): class SessionMixin: _session = None + _isAsync = False @classmethod - def set_session(cls, session): + def set_session(cls, session, isAsync=False): """ - :type session: scoped_session | Session + :type session: scoped_session | async_scoped_session | Session """ cls._session = session + cls._isAsync = isAsync @classproperty def session(cls): @@ -32,4 +35,6 @@ def query(cls): """ :rtype: Query """ + if cls._isAsync or not hasattr(cls.session, "query"): + return select(cls) return cls.session.query(cls) diff --git a/sqlalchemy_mixins/session.pyi b/sqlalchemy_mixins/session.pyi index 44b6cfb..4da0111 100644 --- a/sqlalchemy_mixins/session.pyi +++ b/sqlalchemy_mixins/session.pyi @@ -1,16 +1,17 @@ from typing import Optional from sqlalchemy.orm import Session, Query +from sqlalchemy.ext.asyncio.session import AsyncSession from sqlalchemy_mixins.utils import classproperty class SessionMixin: - _session: Optional[Session] + _session: Optional[Session | AsyncSession] @classmethod - def set_session(cls, session: Session) -> None: ... + def set_session(cls, session: Session | AsyncSession, isAsync: bool) -> None: ... @classproperty def session(cls) -> Session: ... diff --git a/sqlalchemy_mixins/smartquery.py b/sqlalchemy_mixins/smartquery.py index 82672d4..ea91215 100644 --- a/sqlalchemy_mixins/smartquery.py +++ b/sqlalchemy_mixins/smartquery.py @@ -116,6 +116,12 @@ def _get_root_cls(query): else: if hasattr(query, '_entity_from_pre_ent_zero'): return query._entity_from_pre_ent_zero().class_ + + # sqlalchemy 2.x + else: + if query.__dict__["_propagate_attrs"]["plugin_subject"].class_: + return query.__dict__["_propagate_attrs"]["plugin_subject"].class_ + raise ValueError('Cannot get a root class from`{}`' .format(query)) From 1fe9bcff442c1d50f1ee1187ca7135df6170331b Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 30 Apr 2023 16:12:12 +0200 Subject: [PATCH 02/10] added AllFeaturesMixinAsync to __init__.pyi --- sqlalchemy_mixins/__init__.pyi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sqlalchemy_mixins/__init__.pyi b/sqlalchemy_mixins/__init__.pyi index 6f827ab..2fc77c9 100644 --- a/sqlalchemy_mixins/__init__.pyi +++ b/sqlalchemy_mixins/__init__.pyi @@ -2,8 +2,13 @@ from .serialize import SerializeMixin from .repr import ReprMixin from .smartquery import SmartQueryMixin from .activerecord import ActiveRecordMixin +from .activerecordasync import ActiveRecordMixinAsync class AllFeaturesMixin(ActiveRecordMixin, SmartQueryMixin, ReprMixin, SerializeMixin): + __abstract__ = True + __repr__ = ReprMixin.__repr__ # type: ignore + +class AllFeaturesMixinAsync(ActiveRecordMixinAsync, SmartQueryMixin, ReprMixin, SerializeMixin): __abstract__ = True __repr__ = ReprMixin.__repr__ # type: ignore \ No newline at end of file From 4b4af95296003468ba12aa372e7b8deb1488b880 Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 28 Jan 2024 16:05:35 +0200 Subject: [PATCH 03/10] refactored as suggested by @raajkumars --- sqlalchemy_mixins/__init__.py | 20 ++- sqlalchemy_mixins/activerecordasync.py | 217 +++++++++++++++++------- sqlalchemy_mixins/activerecordasync.pyi | 38 +++-- sqlalchemy_mixins/inspection.py | 2 +- sqlalchemy_mixins/session.py | 8 +- sqlalchemy_mixins/smartquery.py | 5 - 6 files changed, 201 insertions(+), 89 deletions(-) diff --git a/sqlalchemy_mixins/__init__.py b/sqlalchemy_mixins/__init__.py index 58e8be1..08ee362 100644 --- a/sqlalchemy_mixins/__init__.py +++ b/sqlalchemy_mixins/__init__.py @@ -17,7 +17,19 @@ class AllFeaturesMixin(ActiveRecordMixin, SmartQueryMixin, ReprMixin, SerializeM __abstract__ = True __repr__ = ReprMixin.__repr__ - -class AllFeaturesMixinAsync(ActiveRecordMixinAsync, SmartQueryMixin, ReprMixin, SerializeMixin): - __abstract__ = True - __repr__ = ReprMixin.__repr__ +__all__ = [ + 'SessionMixin', + 'InspectionMixin', + 'ActiveRecordMixin', + 'ModelNotFoundError', + 'ActiveRecordMixinAsync', + 'SmartQueryMixin', + 'smart_query', + 'EagerLoadMixin', + 'JOINED', + 'SUBQUERY', + 'ReprMixin', + 'SerializeMixin', + 'TimestampsMixin', + 'AllFeaturesMixin', +] \ No newline at end of file diff --git a/sqlalchemy_mixins/activerecordasync.py b/sqlalchemy_mixins/activerecordasync.py index 348b4a0..03e21bd 100644 --- a/sqlalchemy_mixins/activerecordasync.py +++ b/sqlalchemy_mixins/activerecordasync.py @@ -1,105 +1,200 @@ +from sqlalchemy import select from .utils import classproperty from .session import SessionMixin from .inspection import InspectionMixin from .activerecord import ModelNotFoundError +from . import smartquery as SmaryQuery + + +def async_root_cls(query): + """Monkey patch SmaryQuery to handle async queries.""" + try: + return SmaryQuery._get_root_cls(query) + except ValueError: + # Handle async queries + if query.__dict__["_propagate_attrs"]["plugin_subject"].class_: + return query.__dict__["_propagate_attrs"]["plugin_subject"].class_ + raise + +SmaryQuery._get_root_cls = lambda query: async_root_cls(query) class ActiveRecordMixinAsync(InspectionMixin, SessionMixin): __abstract__ = True - + @classproperty - def settable_attributes(cls): - return cls.columns + cls.hybrid_properties + cls.settable_relations - - async def fill(self, **kwargs): - for name in kwargs.keys(): - if name in self.settable_attributes: - setattr(self, name, kwargs[name]) - else: - raise KeyError("Attribute '{}' doesn't exist".format(name)) + def query(cls): + """ + Override the default query property to handle async session. + """ + if not hasattr(cls.session, "query"): + return select(cls) - return self + return cls.session.query(cls) + + async def save_async(self): + """ + Async version of :meth:`save` method. - async def save(self): - """Saves the updated model to the current entity db. + :see: :meth:`save` method for more information. """ - try: - async with self.session() as session: + async with self.session() as session: + try: session.add(self) await session.commit() return self - except: - async with self.session() as session: + except: await session.rollback() raise @classmethod - async def create(cls, **kwargs): - """Create and persist a new record for the model - :param kwargs: attributes for the record - :return: the new model instance + async def create_async(cls, **kwargs): """ - return await cls().fill(**kwargs).save() + Async version of :meth:`create` method. - async def update(self, **kwargs): - """Same as :meth:`fill` method but persists changes to database. + :see: :meth:`create` """ - return await self.fill(**kwargs).save() + return await cls().fill(**kwargs).save_async() + + async def update_async(self, **kwargs): + """ + Async version of :meth:`update` method. + + :see: :meth:`update` + """ + return await self.fill(**kwargs).save_async() - async def delete(self): - """Removes the model from the current entity session and mark for deletion. + async def delete_async(self): """ - try: - async with self.session() as session: - session.delete(self) + Async version of :meth:`delete` method. + + :see: :meth:`delete` + """ + async with self.session() as session: + try: + session.sync_session.delete(self) await session.commit() - except: - async with self.session() as session: + return self + except: await session.rollback() raise + finally: + await session.flush() @classmethod - async def destroy(cls, *ids): - """Delete the records with the given ids - :type ids: list - :param ids: primary key ids of records - """ - for pk in ids: - obj = await cls.find(pk) - if obj: - await obj.delete() - async with cls.session() as session: - await session.flush() + async def destroy_async(cls, *ids): + """ + Async version of :meth:`destroy` method. + + :see: :meth:`destroy` + """ + primary_key = cls._get_primary_key_name() + if primary_key: + async with cls.session() as session: + try: + for row in await cls.where_async(**{f"{primary_key}__in": ids}): + session.sync_session.delete(row) + await session.commit() + except: + await session.rollback() + raise + await session.flush() @classmethod - async def all(cls): + async def select_async(cls, stmt=None, filters=None, sort_attrs=None, schema=None): async with cls.session() as session: - result = await session.execute(cls.query) - return result.scalars().all() + if stmt is None: + stmt = cls.smart_query( + filters=filters, sort_attrs=sort_attrs, schema=schema) + return (await session.execute(stmt)).scalars() @classmethod - async def first(cls): - async with cls.session() as session: - result = await session.execute(cls.query) - return result.scalars().first() + async def where_async(cls, **filters): + """ + Aync version of where method. + + :see: :meth:`where` method for more details. + """ + return await cls.select_async(filters=filters) @classmethod - async def find(cls, id_): - """Find record by the id - :param id_: the primary key + async def sort_async(cls, *columns): """ - async with cls.session() as session: - return await session.get(cls, id_) - + Async version of sort method. + + :see: :meth:`sort` method for more details. + """ + return await cls.select_async(sort_attrs=columns) @classmethod - async def find_or_fail(cls, id_): - # assume that query has custom get_or_fail method - result = await cls.find(id_) - if result: - return result + async def all_async(cls): + """ + Async version of all method. + This is same as calling ``(await select_async()).all()``. + + :see: :meth:`all` method for more details. + """ + return (await cls.select_async()).all() + + @classmethod + async def first_async(cls): + """ + Async version of first method. + This is same as calling ``(await select_async()).first()``. + + :see: :meth:`first` method for more details. + """ + return (await cls.select_async()).first() + + @classmethod + async def find_async(cls, id_): + """ + Async version of find method. + + :see: :meth:`find` method for more details. + """ + primary_key = cls._get_primary_key_name() + if primary_key: + return (await cls.where_async(**{primary_key: id_})).first() + return None + + @classmethod + async def find_or_fail_async(cls, id_): + """ + Async version of find_or_fail method. + + :see: :meth:`find_or_fail` method for more details. + """ + cursor = await cls.find_async(id_) + if cursor: + return cursor else: raise ModelNotFoundError("{} with id '{}' was not found" .format(cls.__name__, id_)) + @classmethod + async def with_async(cls, schema): + """ + Async version of with method. + + :see: :meth:`with` method for more details. + """ + return await cls.select_async(cls.with_(schema)) + + @classmethod + async def with_joined_async(cls, *paths): + """ + Async version of with_joined method. + :see: :meth:`with_joined` method for more details. + """ + return await cls.select_async(cls.with_joined(*paths)) + + @classmethod + async def with_subquery_async(cls, *paths): + """ + Async version of with_subquery method. + + :see: :meth:`with_subquery` method for more details. + """ + return await cls.select_async(cls.with_subquery(*paths)) diff --git a/sqlalchemy_mixins/activerecordasync.pyi b/sqlalchemy_mixins/activerecordasync.pyi index 76c8946..179455d 100644 --- a/sqlalchemy_mixins/activerecordasync.pyi +++ b/sqlalchemy_mixins/activerecordasync.pyi @@ -3,7 +3,7 @@ from typing import List, Any, Optional from sqlalchemy_mixins.inspection import InspectionMixin from sqlalchemy_mixins.session import SessionMixin from sqlalchemy_mixins.utils import classproperty - +from sqlalchemy.orm import Query class ModelNotFoundError(ValueError): ... @@ -12,28 +12,44 @@ class ActiveRecordMixinAsync(InspectionMixin, SessionMixin): @classproperty def settable_attributes(cls) -> List[str]: ... - async def fill(self, **kwargs: Any) -> "ActiveRecordMixinAsync": ... + async def save_async(self) -> "ActiveRecordMixinAsync": ... + + @classmethod + async def create_async(cls, **kwargs: Any) -> "ActiveRecordMixinAsync": ... + + async def update_async(self, **kwargs: dict) -> "ActiveRecordMixinAsync": ... - async def save(self) -> "ActiveRecordMixinAsync": ... + async def delete_async(self) -> None: ... @classmethod - async def create(cls, **kwargs: Any) -> "ActiveRecordMixinAsync": ... + async def destroy_async(cls, *ids: list) -> None: ... - async def update(self, **kwargs: dict) -> "ActiveRecordMixinAsync": ... + @classmethod + async def all_async(cls) -> List["ActiveRecordMixinAsync"]: ... - async def delete(self) -> None: ... + @classmethod + async def first_async(cls) -> Optional["ActiveRecordMixinAsync"]: ... + + @classmethod + async def find_async(cls, id_: Any) -> Optional["ActiveRecordMixinAsync"]: ... + + @classmethod + async def find_or_fail_async(cls, id_: Any) -> "ActiveRecordMixinAsync": ... + + @classmethod + async def select_async(cls, stmt=None, filters=None, sort_attrs=None, schema=None) -> "ActiveRecordMixinAsync": ... @classmethod - async def destroy(cls, *ids: list) -> None: ... + async def where_async(cls, **filters) -> Query: ... @classmethod - async def all(cls) -> List["ActiveRecordMixinAsync"]: ... + async def sort_async(cls, *columns) -> Query: ... @classmethod - async def first(cls) -> Optional["ActiveRecordMixinAsync"]: ... + async def with_async(cls, schema) -> Query: ... @classmethod - async def find(cls, id_: Any) -> Optional["ActiveRecordMixinAsync"]: ... + async def with_joined_async(cls, *paths) -> Query: ... @classmethod - async def find_or_fail(cls, id_: Any) -> "ActiveRecordMixinAsync": ... \ No newline at end of file + async def with_subquery_async(cls, *paths) -> Query: ... diff --git a/sqlalchemy_mixins/inspection.py b/sqlalchemy_mixins/inspection.py index bd2584e..e674984 100644 --- a/sqlalchemy_mixins/inspection.py +++ b/sqlalchemy_mixins/inspection.py @@ -1,6 +1,6 @@ from sqlalchemy import inspect from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method -from sqlalchemy.orm import RelationshipProperty, DeclarativeBase +from sqlalchemy.orm import RelationshipProperty from .utils import classproperty diff --git a/sqlalchemy_mixins/session.py b/sqlalchemy_mixins/session.py index 284f06d..e412ede 100644 --- a/sqlalchemy_mixins/session.py +++ b/sqlalchemy_mixins/session.py @@ -1,5 +1,3 @@ -from sqlalchemy.orm import Session, scoped_session, Query -from sqlalchemy import select from .utils import classproperty @@ -9,15 +7,13 @@ class NoSessionError(RuntimeError): class SessionMixin: _session = None - _isAsync = False @classmethod - def set_session(cls, session, isAsync=False): + def set_session(cls, session): """ :type session: scoped_session | async_scoped_session | Session """ cls._session = session - cls._isAsync = isAsync @classproperty def session(cls): @@ -35,6 +31,4 @@ def query(cls): """ :rtype: Query """ - if cls._isAsync or not hasattr(cls.session, "query"): - return select(cls) return cls.session.query(cls) diff --git a/sqlalchemy_mixins/smartquery.py b/sqlalchemy_mixins/smartquery.py index ea91215..6bb7afc 100644 --- a/sqlalchemy_mixins/smartquery.py +++ b/sqlalchemy_mixins/smartquery.py @@ -116,11 +116,6 @@ def _get_root_cls(query): else: if hasattr(query, '_entity_from_pre_ent_zero'): return query._entity_from_pre_ent_zero().class_ - - # sqlalchemy 2.x - else: - if query.__dict__["_propagate_attrs"]["plugin_subject"].class_: - return query.__dict__["_propagate_attrs"]["plugin_subject"].class_ raise ValueError('Cannot get a root class from`{}`' .format(query)) From 36f9c81aab34f576368f0d0f7c72ffc2d25367a7 Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 28 Jan 2024 16:21:14 +0200 Subject: [PATCH 04/10] remove async session references from session.py --- sqlalchemy_mixins/__init__.pyi | 5 ----- sqlalchemy_mixins/session.py | 2 +- sqlalchemy_mixins/session.pyi | 5 ++--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/sqlalchemy_mixins/__init__.pyi b/sqlalchemy_mixins/__init__.pyi index 2fc77c9..c720856 100644 --- a/sqlalchemy_mixins/__init__.pyi +++ b/sqlalchemy_mixins/__init__.pyi @@ -2,13 +2,8 @@ from .serialize import SerializeMixin from .repr import ReprMixin from .smartquery import SmartQueryMixin from .activerecord import ActiveRecordMixin -from .activerecordasync import ActiveRecordMixinAsync class AllFeaturesMixin(ActiveRecordMixin, SmartQueryMixin, ReprMixin, SerializeMixin): __abstract__ = True __repr__ = ReprMixin.__repr__ # type: ignore - -class AllFeaturesMixinAsync(ActiveRecordMixinAsync, SmartQueryMixin, ReprMixin, SerializeMixin): - __abstract__ = True - __repr__ = ReprMixin.__repr__ # type: ignore \ No newline at end of file diff --git a/sqlalchemy_mixins/session.py b/sqlalchemy_mixins/session.py index e412ede..6d64cd4 100644 --- a/sqlalchemy_mixins/session.py +++ b/sqlalchemy_mixins/session.py @@ -11,7 +11,7 @@ class SessionMixin: @classmethod def set_session(cls, session): """ - :type session: scoped_session | async_scoped_session | Session + :type session: scoped_session | Session """ cls._session = session diff --git a/sqlalchemy_mixins/session.pyi b/sqlalchemy_mixins/session.pyi index 4da0111..44b6cfb 100644 --- a/sqlalchemy_mixins/session.pyi +++ b/sqlalchemy_mixins/session.pyi @@ -1,17 +1,16 @@ from typing import Optional from sqlalchemy.orm import Session, Query -from sqlalchemy.ext.asyncio.session import AsyncSession from sqlalchemy_mixins.utils import classproperty class SessionMixin: - _session: Optional[Session | AsyncSession] + _session: Optional[Session] @classmethod - def set_session(cls, session: Session | AsyncSession, isAsync: bool) -> None: ... + def set_session(cls, session: Session) -> None: ... @classproperty def session(cls) -> Session: ... From 97c36917449e9b9e68c883b09c0c36ff9b7b9fd7 Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 28 Jan 2024 16:27:18 +0200 Subject: [PATCH 05/10] restore main package changes --- sqlalchemy_mixins/__init__.pyi | 2 +- sqlalchemy_mixins/inspection.py | 4 ++-- sqlalchemy_mixins/session.py | 3 ++- sqlalchemy_mixins/smartquery.py | 3 +-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sqlalchemy_mixins/__init__.pyi b/sqlalchemy_mixins/__init__.pyi index c720856..6f827ab 100644 --- a/sqlalchemy_mixins/__init__.pyi +++ b/sqlalchemy_mixins/__init__.pyi @@ -6,4 +6,4 @@ from .activerecord import ActiveRecordMixin class AllFeaturesMixin(ActiveRecordMixin, SmartQueryMixin, ReprMixin, SerializeMixin): __abstract__ = True - __repr__ = ReprMixin.__repr__ # type: ignore + __repr__ = ReprMixin.__repr__ # type: ignore \ No newline at end of file diff --git a/sqlalchemy_mixins/inspection.py b/sqlalchemy_mixins/inspection.py index e674984..def1049 100644 --- a/sqlalchemy_mixins/inspection.py +++ b/sqlalchemy_mixins/inspection.py @@ -1,6 +1,6 @@ from sqlalchemy import inspect from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method -from sqlalchemy.orm import RelationshipProperty +from sqlalchemy.orm import RelationshipProperty, DeclarativeBase from .utils import classproperty @@ -54,4 +54,4 @@ def hybrid_methods_full(cls): @classproperty def hybrid_methods(cls): - return list(cls.hybrid_methods_full.keys()) + return list(cls.hybrid_methods_full.keys()) \ No newline at end of file diff --git a/sqlalchemy_mixins/session.py b/sqlalchemy_mixins/session.py index 6d64cd4..d034b61 100644 --- a/sqlalchemy_mixins/session.py +++ b/sqlalchemy_mixins/session.py @@ -1,3 +1,4 @@ +from sqlalchemy.orm import Session, scoped_session, Query from .utils import classproperty @@ -31,4 +32,4 @@ def query(cls): """ :rtype: Query """ - return cls.session.query(cls) + return cls.session.query(cls) \ No newline at end of file diff --git a/sqlalchemy_mixins/smartquery.py b/sqlalchemy_mixins/smartquery.py index 6bb7afc..ffd03bd 100644 --- a/sqlalchemy_mixins/smartquery.py +++ b/sqlalchemy_mixins/smartquery.py @@ -116,7 +116,6 @@ def _get_root_cls(query): else: if hasattr(query, '_entity_from_pre_ent_zero'): return query._entity_from_pre_ent_zero().class_ - raise ValueError('Cannot get a root class from`{}`' .format(query)) @@ -430,4 +429,4 @@ def sort(cls, *columns): Exanple 3 (with joins): Post.sort('comments___rating', 'user___name').all() """ - return cls.smart_query({}, columns) + return cls.smart_query({}, columns) \ No newline at end of file From 285a53037a7b961bbc36304bab6a975961f00a8d Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 28 Jan 2024 16:42:42 +0200 Subject: [PATCH 06/10] upated __init__.pyi for import references to work well --- sqlalchemy_mixins/__init__.pyi | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/sqlalchemy_mixins/__init__.pyi b/sqlalchemy_mixins/__init__.pyi index 6f827ab..62b4093 100644 --- a/sqlalchemy_mixins/__init__.pyi +++ b/sqlalchemy_mixins/__init__.pyi @@ -1,9 +1,31 @@ -from .serialize import SerializeMixin +from .session import SessionMixin +from .inspection import InspectionMixin +from .activerecord import ActiveRecordMixin, ModelNotFoundError +from .activerecordasync import ActiveRecordMixinAsync +from .smartquery import SmartQueryMixin, smart_query +from .eagerload import EagerLoadMixin, JOINED, SUBQUERY from .repr import ReprMixin -from .smartquery import SmartQueryMixin -from .activerecord import ActiveRecordMixin +from .serialize import SerializeMixin +from .timestamp import TimestampsMixin class AllFeaturesMixin(ActiveRecordMixin, SmartQueryMixin, ReprMixin, SerializeMixin): __abstract__ = True - __repr__ = ReprMixin.__repr__ # type: ignore \ No newline at end of file + __repr__ = ReprMixin.__repr__ + +__all__ = [ + 'SessionMixin', + 'InspectionMixin', + 'ActiveRecordMixin', + 'ModelNotFoundError', + 'ActiveRecordMixinAsync', + 'SmartQueryMixin', + 'smart_query', + 'EagerLoadMixin', + 'JOINED', + 'SUBQUERY', + 'ReprMixin', + 'SerializeMixin', + 'TimestampsMixin', + 'AllFeaturesMixin', +] \ No newline at end of file From 6da12c55c8784fbf53c3484f3b75e79fa47b3e61 Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 28 Jan 2024 16:55:26 +0200 Subject: [PATCH 07/10] patched SmaryQuery._get_root_cls --- sqlalchemy_mixins/activerecordasync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlalchemy_mixins/activerecordasync.py b/sqlalchemy_mixins/activerecordasync.py index 03e21bd..f5ee83f 100644 --- a/sqlalchemy_mixins/activerecordasync.py +++ b/sqlalchemy_mixins/activerecordasync.py @@ -5,17 +5,17 @@ from .activerecord import ModelNotFoundError from . import smartquery as SmaryQuery - +get_root_cls = SmaryQuery._get_root_cls def async_root_cls(query): """Monkey patch SmaryQuery to handle async queries.""" try: - return SmaryQuery._get_root_cls(query) + return get_root_cls(query) except ValueError: # Handle async queries if query.__dict__["_propagate_attrs"]["plugin_subject"].class_: return query.__dict__["_propagate_attrs"]["plugin_subject"].class_ raise - + SmaryQuery._get_root_cls = lambda query: async_root_cls(query) From c7a7c094518f50d2d86dcfefd5ddadaf447a54b6 Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Sun, 28 Jan 2024 18:06:02 +0200 Subject: [PATCH 08/10] added typing to async_root_cls patch --- sqlalchemy_mixins/activerecordasync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlalchemy_mixins/activerecordasync.py b/sqlalchemy_mixins/activerecordasync.py index f5ee83f..b01ef28 100644 --- a/sqlalchemy_mixins/activerecordasync.py +++ b/sqlalchemy_mixins/activerecordasync.py @@ -1,4 +1,5 @@ from sqlalchemy import select +from sqlalchemy.orm import Query from .utils import classproperty from .session import SessionMixin from .inspection import InspectionMixin @@ -6,7 +7,7 @@ from . import smartquery as SmaryQuery get_root_cls = SmaryQuery._get_root_cls -def async_root_cls(query): +def async_root_cls(query: Query): """Monkey patch SmaryQuery to handle async queries.""" try: return get_root_cls(query) From 60b4287c5f54cdcb659ff030af20a53d92acfcad Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Tue, 30 Jan 2024 08:33:34 +0200 Subject: [PATCH 09/10] added missing typing to fix run type checks --- sqlalchemy_mixins/activerecordasync.pyi | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/sqlalchemy_mixins/activerecordasync.pyi b/sqlalchemy_mixins/activerecordasync.pyi index 179455d..8517342 100644 --- a/sqlalchemy_mixins/activerecordasync.pyi +++ b/sqlalchemy_mixins/activerecordasync.pyi @@ -1,11 +1,10 @@ -from typing import List, Any, Optional +from typing import Dict, Iterable, List, Any, Optional from sqlalchemy_mixins.inspection import InspectionMixin from sqlalchemy_mixins.session import SessionMixin from sqlalchemy_mixins.utils import classproperty -from sqlalchemy.orm import Query +from sqlalchemy.orm import Query, QueryableAttribute -class ModelNotFoundError(ValueError): ... class ActiveRecordMixinAsync(InspectionMixin, SessionMixin): @@ -37,19 +36,24 @@ class ActiveRecordMixinAsync(InspectionMixin, SessionMixin): async def find_or_fail_async(cls, id_: Any) -> "ActiveRecordMixinAsync": ... @classmethod - async def select_async(cls, stmt=None, filters=None, sort_attrs=None, schema=None) -> "ActiveRecordMixinAsync": ... + async def select_async(cls, + stmt:str=None, + filters: Optional[Dict[str, Any]] = None, + sort_attrs: Optional[Iterable[str]] = None, + schema: Optional[dict] = None + ) -> "ActiveRecordMixinAsync": ... @classmethod - async def where_async(cls, **filters) -> Query: ... + async def where_async(cls, **filters: Any) -> Query: ... @classmethod - async def sort_async(cls, *columns) -> Query: ... + async def sort_async(cls, *columns: str) -> Query: ... @classmethod - async def with_async(cls, schema) -> Query: ... + async def with_async(cls, schema: dict) -> Query: ... @classmethod - async def with_joined_async(cls, *paths) -> Query: ... + async def with_joined_async(cls, *paths: List[QueryableAttribute]) -> Query: ... @classmethod - async def with_subquery_async(cls, *paths) -> Query: ... + async def with_subquery_async(cls, *paths: List[QueryableAttribute]) -> Query: ... From db446dc93d7b5efb3ba60ab76a3fabfe0bce6cd6 Mon Sep 17 00:00:00 2001 From: Aurthur Musendame Date: Tue, 30 Jan 2024 16:49:48 +0200 Subject: [PATCH 10/10] Added optional typing to select_async to fix -> (default has type 'None', argument has type 'str') --- sqlalchemy_mixins/activerecordasync.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_mixins/activerecordasync.pyi b/sqlalchemy_mixins/activerecordasync.pyi index 8517342..de80ce2 100644 --- a/sqlalchemy_mixins/activerecordasync.pyi +++ b/sqlalchemy_mixins/activerecordasync.pyi @@ -37,7 +37,7 @@ class ActiveRecordMixinAsync(InspectionMixin, SessionMixin): @classmethod async def select_async(cls, - stmt:str=None, + stmt:Optional[str] = None, filters: Optional[Dict[str, Any]] = None, sort_attrs: Optional[Iterable[str]] = None, schema: Optional[dict] = None