From 0bd6630a703dfcec2f39e015563d5c5146ed1c06 Mon Sep 17 00:00:00 2001
From: Cody Fincher <204685+cofin@users.noreply.github.com>
Date: Thu, 14 Nov 2024 18:09:25 -0600
Subject: [PATCH] feat: remove lambda statement usage (#288)
* feat: remove lambda statement usage
---
.gitignore | 1 +
.pre-commit-config.yaml | 10 +-
README.md | 44 ++--
advanced_alchemy/filters.py | 221 +++-------------
advanced_alchemy/repository/_async.py | 253 ++++++-------------
advanced_alchemy/repository/_sync.py | 251 ++++++------------
advanced_alchemy/repository/_util.py | 111 ++++----
advanced_alchemy/repository/memory/_async.py | 6 +-
advanced_alchemy/repository/memory/_sync.py | 6 +-
advanced_alchemy/service/_async.py | 18 +-
advanced_alchemy/service/_sync.py | 18 +-
docs/conf.py | 1 +
pyproject.toml | 1 +
tests/integration/test_lambda_stmt.py | 115 ---------
tests/unit/test_repository.py | 35 ++-
uv.lock | 48 ++--
16 files changed, 357 insertions(+), 782 deletions(-)
delete mode 100644 tests/integration/test_lambda_stmt.py
diff --git a/.gitignore b/.gitignore
index c033e6d3..b98b7b61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -171,3 +171,4 @@ cython_debug/
/docs/changelog.md
.cursorrules
.cursorignore
+.zed
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e890363d..c9b69077 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,9 +24,13 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.7.3"
hooks:
- - id: ruff
- args: ["--fix","--unsafe-fixes"]
- - id: ruff-format
+ # Run the linter.
+ - id: ruff
+ types_or: [ python, pyi ]
+ args: [ --fix ]
+ # Run the formatter.
+ - id: ruff-format
+ types_or: [ python, pyi ]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
diff --git a/README.md b/README.md
index 99d69e52..77db42bc 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,8 @@
diff --git a/advanced_alchemy/filters.py b/advanced_alchemy/filters.py
index 20ff2169..8682d374 100644
--- a/advanced_alchemy/filters.py
+++ b/advanced_alchemy/filters.py
@@ -9,14 +9,15 @@
from operator import attrgetter
from typing import TYPE_CHECKING, Any, Generic, Literal, cast
-from sqlalchemy import BinaryExpression, and_, any_, or_, text
+from sqlalchemy import BinaryExpression, Delete, Select, Update, and_, any_, or_, text
from typing_extensions import TypeVar
if TYPE_CHECKING:
from typing import Callable
- from sqlalchemy import ColumnElement, Select, StatementLambdaElement
+ from sqlalchemy import ColumnElement
from sqlalchemy.orm import InstrumentedAttribute
+ from sqlalchemy.sql.dml import ReturningDelete, ReturningUpdate
from typing_extensions import TypeAlias
from advanced_alchemy import base
@@ -36,32 +37,29 @@
"InAnyFilter",
"StatementFilter",
"StatementFilterT",
+ "StatementTypeT",
)
T = TypeVar("T")
ModelT = TypeVar("ModelT", bound="base.ModelProtocol")
StatementFilterT = TypeVar("StatementFilterT", bound="StatementFilter")
+StatementTypeT = TypeVar(
+ "StatementTypeT",
+ bound="ReturningDelete[tuple[Any]] | ReturningUpdate[tuple[Any]] | Select[tuple[Any]] | Select[Any] | Update | Delete",
+)
FilterTypes: TypeAlias = "BeforeAfter | OnBeforeAfter | CollectionFilter[Any] | LimitOffset | OrderBy | SearchFilter | NotInCollectionFilter[Any] | NotInSearchFilter"
"""Aggregate type alias of the types supported for collection filtering."""
class StatementFilter(ABC):
@abstractmethod
- def append_to_statement(self, statement: Select[tuple[ModelT]], model: type[ModelT]) -> Select[tuple[ModelT]]:
- return statement
-
- @abstractmethod
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
- *args: Any,
- **kwargs: Any,
- ) -> StatementLambdaElement:
+ def append_to_statement(
+ self, statement: StatementTypeT, model: type[ModelT], *args: Any, **kwargs: Any
+ ) -> StatementTypeT:
return statement
@staticmethod
def _get_instrumented_attr(model: Any, key: str | InstrumentedAttribute[Any]) -> InstrumentedAttribute[Any]:
- # copy this here to avoid a circular import of `get_instrumented_attribute`. Maybe we move that function somewhere else?
if isinstance(key, str):
return cast("InstrumentedAttribute[Any]", getattr(model, key))
return key
@@ -78,36 +76,12 @@ class BeforeAfter(StatementFilter):
after: datetime | None
"""Filter results where field later than this."""
- def append_to_statement(self, statement: Select[tuple[ModelT]], model: type[ModelT]) -> Select[tuple[ModelT]]:
- field = self._get_instrumented_attr(model, self.field_name)
- if self.before is not None:
- statement = statement.where(field < self.before) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
- if self.after is not None:
- statement = statement.where(field > self.after) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
- return statement
-
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
- model: type[ModelT],
- ) -> StatementLambdaElement:
+ def append_to_statement(self, statement: StatementTypeT, model: type[ModelT]) -> StatementTypeT:
field = self._get_instrumented_attr(model, self.field_name)
if self.before is not None:
- before = self.before
- statement = statement.add_criteria(
- lambda s: s.where(field < before),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, model, self.field_name, self.before],
- )
+ statement = cast("StatementTypeT", statement.where(field < self.before)) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
if self.after is not None:
- after = self.after
- statement = statement.add_criteria(
- lambda s: s.where(field > after),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, model, self.field_name, self.after],
- )
+ statement = cast("StatementTypeT", statement.where(field > self.after)) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
return statement
@@ -122,36 +96,12 @@ class OnBeforeAfter(StatementFilter):
on_or_after: datetime | None
"""Filter results where field on or later than this."""
- def append_to_statement(self, statement: Select[tuple[ModelT]], model: type[ModelT]) -> Select[tuple[ModelT]]:
+ def append_to_statement(self, statement: StatementTypeT, model: type[ModelT]) -> StatementTypeT:
field = self._get_instrumented_attr(model, self.field_name)
if self.on_or_before is not None:
- statement = statement.where(field <= self.on_or_before) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
+ statement = cast("StatementTypeT", statement.where(field <= self.on_or_before)) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
if self.on_or_after is not None:
- statement = statement.where(field >= self.on_or_after) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
- return statement
-
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
- model: type[ModelT],
- ) -> StatementLambdaElement:
- field = self._get_instrumented_attr(model, self.field_name)
- if self.on_or_before is not None:
- on_or_before = self.on_or_before
- statement = statement.add_criteria(
- lambda s: s.where(field <= on_or_before),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, model.__name__, self.field_name, self.on_or_before],
- )
- if self.on_or_after is not None:
- on_or_after = self.on_or_after
- statement = statement.add_criteria(
- lambda s: s.where(field >= on_or_after),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, model.__name__, self.field_name, self.on_or_after],
- )
+ statement = cast("StatementTypeT", statement.where(field >= self.on_or_after)) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
return statement
@@ -172,45 +122,18 @@ class CollectionFilter(InAnyFilter, Generic[T]):
def append_to_statement(
self,
- statement: Select[tuple[ModelT]],
- model: type[ModelT],
- prefer_any: bool = False,
- ) -> Select[tuple[ModelT]]:
- field = self._get_instrumented_attr(model, self.field_name)
- if self.values is None:
- return statement
- if not self.values:
- return statement.where(text("1=-1"))
- if prefer_any:
- return statement.where(any_(self.values) == field) # type: ignore[arg-type]
- return statement.where(field.in_(self.values))
-
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
+ statement: StatementTypeT,
model: type[ModelT],
prefer_any: bool = False,
- ) -> StatementLambdaElement:
+ ) -> StatementTypeT:
field = self._get_instrumented_attr(model, self.field_name)
if self.values is None:
return statement
if not self.values:
- return statement.add_criteria(lambda s: s.where(text("1=-1")), enable_tracking=False)
+ return cast("StatementTypeT", statement.where(text("1=-1")))
if prefer_any:
- values = self.values
- return statement.add_criteria(
- lambda s: s.where(any_(values) == field), # type: ignore[arg-type]
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, self.field_name, model.__name__],
- )
- values = self.values
- return statement.add_criteria(
- lambda s: s.where(field.in_(values)),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, self.field_name, model.__name__],
- )
+ return cast("StatementTypeT", statement.where(any_(self.values) == field)) # type: ignore[arg-type]
+ return cast("StatementTypeT", statement.where(field.in_(self.values)))
@dataclass
@@ -226,41 +149,16 @@ class NotInCollectionFilter(InAnyFilter, Generic[T]):
def append_to_statement(
self,
- statement: Select[tuple[ModelT]],
+ statement: StatementTypeT,
model: type[ModelT],
prefer_any: bool = False,
- ) -> Select[tuple[ModelT]]:
+ ) -> StatementTypeT:
field = self._get_instrumented_attr(model, self.field_name)
if not self.values:
return statement
if prefer_any:
- return statement.where(any_(self.values) == field) # type: ignore[arg-type]
- return statement.where(field.in_(self.values))
-
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
- model: type[ModelT],
- prefer_any: bool = False,
- ) -> StatementLambdaElement:
- field = self._get_instrumented_attr(model, self.field_name)
- if not self.values:
- return statement
- if prefer_any:
- values = self.values
- return statement.add_criteria(
- lambda s: s.where(any_(values) != field), # type: ignore[arg-type]
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, self.field_name, model.__name__],
- )
- values = self.values
- return statement.add_criteria(
- lambda s: s.where(field.notin_(values)),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, self.field_name, model.__name__],
- )
+ return cast("StatementTypeT", statement.where(any_(self.values) != field)) # type: ignore[arg-type]
+ return cast("StatementTypeT", statement.where(field.notin_(self.values)))
class PaginationFilter(StatementFilter, ABC):
@@ -276,22 +174,10 @@ class LimitOffset(PaginationFilter):
offset: int
"""Value for ``OFFSET`` clause of query."""
- def append_to_statement(self, statement: Select[tuple[ModelT]], model: type[ModelT]) -> Select[tuple[ModelT]]:
- return statement.limit(self.limit).offset(self.offset)
-
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
- model: type[ModelT],
- ) -> StatementLambdaElement:
- limit = self.limit
- offset = self.offset
- return statement.add_criteria(
- lambda s: s.limit(limit).offset(offset),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, limit, offset, model.__name__],
- )
+ def append_to_statement(self, statement: StatementTypeT, model: type[ModelT]) -> StatementTypeT:
+ if isinstance(statement, Select):
+ return cast("StatementTypeT", statement.limit(self.limit).offset(self.offset))
+ return statement
@dataclass
@@ -303,25 +189,13 @@ class OrderBy(StatementFilter):
sort_order: Literal["asc", "desc"] = "asc"
"""Sort ascending or descending"""
- def append_to_statement(self, statement: Select[tuple[ModelT]], model: type[ModelT]) -> Select[tuple[ModelT]]:
+ def append_to_statement(self, statement: StatementTypeT, model: type[ModelT]) -> StatementTypeT:
+ if not isinstance(statement, Select):
+ return statement
field = self._get_instrumented_attr(model, self.field_name)
if self.sort_order == "desc":
- return statement.order_by(field.desc())
- return statement.order_by(field.asc())
-
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
- model: type[ModelT],
- ) -> StatementLambdaElement:
- field = self._get_instrumented_attr(model, self.field_name)
- fragment = field.desc() if self.sort_order == "desc" else field.asc()
- return statement.add_criteria(
- lambda s: s.order_by(fragment),
- track_bound_values=False,
- track_closure_variables=False,
- track_on=[self.__class__.__name__, model.__name__, self.field_name, self.sort_order],
- )
+ return cast("StatementTypeT", statement.order_by(field.desc()))
+ return cast("StatementTypeT", statement.order_by(field.asc()))
@dataclass
@@ -357,30 +231,11 @@ def get_search_clauses(self, model: type[ModelT]) -> list[BinaryExpression[bool]
def append_to_statement(
self,
- statement: Select[tuple[ModelT]],
- model: type[ModelT],
- ) -> Select[tuple[ModelT]]:
- where_clause = self._operator(*self.get_search_clauses(model))
- return statement.where(where_clause)
-
- def append_to_lambda_statement(
- self,
- statement: StatementLambdaElement,
+ statement: StatementTypeT,
model: type[ModelT],
- ) -> StatementLambdaElement:
+ ) -> StatementTypeT:
where_clause = self._operator(*self.get_search_clauses(model))
- return statement.add_criteria(
- lambda s: s.where(where_clause),
- track_bound_values=True,
- track_closure_variables=True,
- track_on=[
- self.__class__.__name__,
- model.__name__,
- str(self.normalized_field_names),
- self.value,
- self.ignore_case,
- ],
- )
+ return cast("StatementTypeT", statement.where(where_clause))
@dataclass
diff --git a/advanced_alchemy/repository/_async.py b/advanced_alchemy/repository/_async.py
index 99bc15f2..40a4d345 100644
--- a/advanced_alchemy/repository/_async.py
+++ b/advanced_alchemy/repository/_async.py
@@ -18,14 +18,14 @@
)
from sqlalchemy import (
+ Delete,
Result,
RowMapping,
Select,
- StatementLambdaElement,
TextClause,
+ Update,
any_,
delete,
- lambda_stmt,
over,
select,
text,
@@ -53,8 +53,9 @@
from sqlalchemy.ext.asyncio.scoping import async_scoped_session
from sqlalchemy.orm.strategy_options import _AbstractLoad # pyright: ignore[reportPrivateUsage]
from sqlalchemy.sql import ColumnElement
+ from sqlalchemy.sql.dml import ReturningDelete, ReturningUpdate
- from advanced_alchemy.filters import StatementFilter
+ from advanced_alchemy.filters import StatementFilter, StatementTypeT
DEFAULT_INSERTMANYVALUES_MAX_PARAMETERS: Final = 950
@@ -67,7 +68,7 @@ class SQLAlchemyAsyncRepositoryProtocol(FilterableRepositoryProtocol[ModelT], Pr
id_attribute: Any
match_fields: list[str] | str | None = None
- statement: Select[tuple[ModelT]] | StatementLambdaElement
+ statement: Select[tuple[ModelT]]
session: AsyncSession | async_scoped_session[AsyncSession]
auto_expunge: bool
auto_refresh: bool
@@ -78,7 +79,7 @@ class SQLAlchemyAsyncRepositoryProtocol(FilterableRepositoryProtocol[ModelT], Pr
def __init__(
self,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
session: AsyncSession | async_scoped_session[AsyncSession],
auto_expunge: bool = False,
auto_refresh: bool = True,
@@ -178,7 +179,7 @@ async def get(
item_id: Any,
*,
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
id_attribute: str | InstrumentedAttribute[Any] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -189,7 +190,7 @@ async def get_one(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -200,7 +201,7 @@ async def get_one_or_none(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -241,7 +242,7 @@ async def get_and_update(
async def count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
load: LoadSpec | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
execution_options: dict[str, Any] | None = None,
@@ -280,7 +281,7 @@ def _get_update_many_statement(
supports_returning: bool,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement: ...
+ ) -> Update | ReturningUpdate[tuple[ModelT]]: ...
async def upsert(
self,
@@ -314,7 +315,7 @@ async def list_and_count(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
force_basic_query_mode: bool | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -327,7 +328,7 @@ async def list(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -378,7 +379,7 @@ class SQLAlchemyAsyncRepository(SQLAlchemyAsyncRepositoryProtocol[ModelT], Filte
def __init__(
self,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
session: AsyncSession | async_scoped_session[AsyncSession],
auto_expunge: bool = False,
auto_refresh: bool = True,
@@ -752,17 +753,16 @@ async def delete_where(
execution_options = self._get_execution_options(execution_options)
loader_options, _loader_options_have_wildcard = self._get_loader_options(load)
model_type = self.model_type
- statement = lambda_stmt(lambda: delete(model_type)) # pyright: ignore[reportUnknownLambdaType]
- if loader_options:
- statement = statement.options(*loader_options)
- if execution_options:
- statement = statement.execution_options(**execution_options)
+ statement = self._get_base_stmt(
+ statement=delete(model_type),
+ loader_options=loader_options,
+ execution_options=execution_options,
+ )
statement = self._filter_select_by_kwargs(statement=statement, kwargs=kwargs)
statement = self._apply_filters(*filters, statement=statement, apply_pagination=False)
instances: list[ModelT] = []
if self._dialect.delete_executemany_returning:
- statement += lambda s: s.returning(model_type) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
- instances.extend(await self.session.scalars(statement))
+ instances.extend(await self.session.scalars(statement.returning(model_type)))
else:
instances.extend(
await self.list(
@@ -779,7 +779,7 @@ async def delete_where(
# backends will return a -1 if they can't determine impacted rowcount
# only compare length of selected instances to results if it's >= 0
await self.session.rollback()
- raise RepositoryError(detail="Deleted count does not match fetched count. Rollback issued.")
+ raise RepositoryError(detail="Deleted count does not match fetched count. Rollback issued.")
await self._flush_or_commit(auto_commit=auto_commit)
for instance in instances:
@@ -821,52 +821,27 @@ async def exists(
)
return existing > 0
- def _to_lambda_stmt(
- self,
- statement: Select[tuple[ModelT]] | StatementLambdaElement,
- global_track_bound_values: bool = True,
- track_closure_variables: bool = True,
- enable_tracking: bool = True,
- track_bound_values: bool = True,
- track_on: object | None = None,
- ) -> StatementLambdaElement:
- if isinstance(statement, Select):
- statement = lambda_stmt(
- lambda: statement,
- track_bound_values=track_bound_values,
- global_track_bound_values=global_track_bound_values,
- track_closure_variables=track_closure_variables,
- enable_tracking=enable_tracking,
- track_on=track_on,
- )
- return statement
-
def _get_base_stmt(
self,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement,
- global_track_bound_values: bool = True,
- track_closure_variables: bool = True,
- enable_tracking: bool = True,
- track_bound_values: bool = True,
+ statement: StatementTypeT,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- track_on: object | None = None,
- ) -> StatementLambdaElement:
- statement = self._to_lambda_stmt(
- statement=statement,
- global_track_bound_values=global_track_bound_values,
- track_closure_variables=track_closure_variables,
- enable_tracking=enable_tracking,
- track_bound_values=track_bound_values,
- track_on=track_on,
- )
+ ) -> StatementTypeT:
+ """Get base statement with options applied.
+
+ Args:
+ statement: The select statement to modify
+ loader_options: Options for loading relationships
+ execution_options: Options for statement execution
+
+ Returns:
+ Modified select statement
+ """
if loader_options:
- statement = statement.add_criteria(lambda s: s.options(*loader_options), enable_tracking=False)
+ statement = cast("StatementTypeT", statement.options(*loader_options))
if execution_options:
- statement = statement.add_criteria(
- lambda s: s.execution_options(**execution_options), enable_tracking=False
- )
+ statement = cast("StatementTypeT", statement.execution_options(**execution_options))
return statement
def _get_delete_many_statement(
@@ -879,38 +854,27 @@ def _get_delete_many_statement(
statement_type: Literal["delete", "select"] = "delete",
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
+ ) -> Select[tuple[ModelT]] | Delete | ReturningDelete[tuple[ModelT]]:
# Base statement is static
- statement = delete(model_type) if statement_type == "delete" else select(model_type)
- if loader_options:
- statement = statement.options(*loader_options)
+ statement = self._get_base_stmt(
+ statement=delete(model_type) if statement_type == "delete" else select(model_type),
+ loader_options=loader_options,
+ execution_options=execution_options,
+ )
if execution_options:
statement = statement.execution_options(**execution_options)
if supports_returning and statement_type != "select":
- statement = statement.returning(model_type) # type: ignore[union-attr,assignment] # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownVariableType]
+ statement = cast("ReturningDelete[tuple[ModelT]]", statement.returning(model_type)) # type: ignore[union-attr,assignment] # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownVariableType]
if self._prefer_any:
- statement = statement.where(any_(id_chunk) == id_attribute) # type: ignore[arg-type]
- else:
- statement = statement.where(id_attribute.in_(id_chunk)) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
- return lambda_stmt(
- lambda: statement, # pyright: ignore[reportUnknownLambdaType]
- track_bound_values=True,
- track_on=[
- self._dialect.name,
- statement_type,
- model_type,
- id_attribute,
- statement, # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
- )
+ return statement.where(any_(id_chunk) == id_attribute) # type: ignore[arg-type]
+ return statement.where(id_attribute.in_(id_chunk)) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
async def get(
self,
item_id: Any,
*,
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
id_attribute: str | InstrumentedAttribute[Any] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -948,15 +912,6 @@ async def get(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_bound_values=False,
- track_closure_variables=False,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id_attribute,
- id(statement), # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._filter_select_by_kwargs(statement, [(id_attribute, item_id)])
instance = (await self._execute(statement, uniquify=loader_options_have_wildcard)).scalar_one_or_none()
@@ -968,7 +923,7 @@ async def get_one(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -1004,12 +959,6 @@ async def get_one(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
@@ -1022,7 +971,7 @@ async def get_one_or_none(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -1055,12 +1004,6 @@ async def get_one_or_none(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
@@ -1236,7 +1179,7 @@ async def get_and_update(
async def count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -1268,21 +1211,15 @@ async def count(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- enable_tracking=False,
- global_track_bound_values=False,
- track_bound_values=False,
- track_closure_variables=False,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement), # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
- statement = self._get_count_stmt(statement, loader_options, execution_options)
- results = await self._execute(statement, uniquify=loader_options_have_wildcard)
+ results = await self._execute(
+ statement=self._get_count_stmt(
+ statement=statement, loader_options=loader_options, execution_options=execution_options
+ ),
+ uniquify=loader_options_have_wildcard,
+ )
return cast(int, results.scalar_one())
async def update(
@@ -1419,29 +1356,20 @@ def _get_update_many_statement(
supports_returning: bool,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
+ ) -> Update | ReturningUpdate[tuple[ModelT]]:
# Base update statement is static
- statement = update(model_type)
- if supports_returning:
- statement = statement.returning(model_type)
- if loader_options:
- statement = statement.options(*loader_options)
- if execution_options:
- statement = statement.execution_options(**execution_options)
- return lambda_stmt(
- lambda: statement,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- statement,
- f"{loader_options!s}:{execution_options!s}",
- ],
+ statement = self._get_base_stmt(
+ statement=update(table=model_type), loader_options=loader_options, execution_options=execution_options
)
+ if supports_returning:
+ return statement.returning(model_type)
+
+ return statement
async def list_and_count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
force_basic_query_mode: bool | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
@@ -1529,7 +1457,7 @@ async def _list_and_count_window(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -1564,23 +1492,15 @@ async def _list_and_count_window(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
if order_by is None:
order_by = self.order_by or []
statement = self._apply_order_by(statement=statement, order_by=order_by)
- statement = statement.add_criteria(
- lambda s: s.add_columns(over(sql_func.count())),
- enable_tracking=False,
- )
statement = self._apply_filters(*filters, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
- result = await self._execute(statement, uniquify=loader_options_have_wildcard)
+ result = await self._execute(
+ statement.add_columns(over(sql_func.count())), uniquify=loader_options_have_wildcard
+ )
count: int = 0
instances: list[ModelT] = []
for i, (instance, count_value) in enumerate(result):
@@ -1594,7 +1514,7 @@ async def _list_and_count_basic(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -1629,12 +1549,6 @@ async def _list_and_count_basic(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
if order_by is None:
order_by = self.order_by or []
@@ -1658,22 +1572,12 @@ async def _list_and_count_basic(
def _get_count_stmt(
self,
- statement: StatementLambdaElement,
+ statement: Select[tuple[ModelT]],
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
+ ) -> Select[tuple[int]]:
# Count statement transformations are static
- return statement.add_criteria(
- lambda s: s.with_only_columns(sql_func.count(text("1")), maintain_column_froms=True).order_by(None),
- track_bound_values=False,
- track_closure_variables=False,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
- )
+ return statement.with_only_columns(sql_func.count(text("1")), maintain_column_froms=True).order_by(None)
async def upsert(
self,
@@ -1890,7 +1794,7 @@ async def list(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -1925,12 +1829,6 @@ async def list(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement), # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
)
if order_by is None:
order_by = self.order_by or []
@@ -1999,7 +1897,7 @@ async def _attach_to_session(
async def _execute(
self,
- statement: Select[Any] | StatementLambdaElement,
+ statement: Select[Any],
uniquify: bool = False,
) -> Result[Any]:
result = await self.session.execute(statement)
@@ -2278,6 +2176,11 @@ def check_not_found(item_or_none: T | None) -> T:
async def execute(
self,
- statement: Select[Any] | StatementLambdaElement,
+ statement: ReturningDelete[tuple[Any]]
+ | ReturningUpdate[tuple[Any]]
+ | Select[tuple[Any]]
+ | Update
+ | Delete
+ | Select[Any],
) -> Result[Any]:
return await self.session.execute(statement)
diff --git a/advanced_alchemy/repository/_sync.py b/advanced_alchemy/repository/_sync.py
index 63a05087..f00562a6 100644
--- a/advanced_alchemy/repository/_sync.py
+++ b/advanced_alchemy/repository/_sync.py
@@ -20,14 +20,14 @@
)
from sqlalchemy import (
+ Delete,
Result,
RowMapping,
Select,
- StatementLambdaElement,
TextClause,
+ Update,
any_,
delete,
- lambda_stmt,
over,
select,
text,
@@ -54,8 +54,9 @@
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.strategy_options import _AbstractLoad # pyright: ignore[reportPrivateUsage]
from sqlalchemy.sql import ColumnElement
+ from sqlalchemy.sql.dml import ReturningDelete, ReturningUpdate
- from advanced_alchemy.filters import StatementFilter
+ from advanced_alchemy.filters import StatementFilter, StatementTypeT
DEFAULT_INSERTMANYVALUES_MAX_PARAMETERS: Final = 950
@@ -68,7 +69,7 @@ class SQLAlchemySyncRepositoryProtocol(FilterableRepositoryProtocol[ModelT], Pro
id_attribute: Any
match_fields: list[str] | str | None = None
- statement: Select[tuple[ModelT]] | StatementLambdaElement
+ statement: Select[tuple[ModelT]]
session: Session | scoped_session[Session]
auto_expunge: bool
auto_refresh: bool
@@ -79,7 +80,7 @@ class SQLAlchemySyncRepositoryProtocol(FilterableRepositoryProtocol[ModelT], Pro
def __init__(
self,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
session: Session | scoped_session[Session],
auto_expunge: bool = False,
auto_refresh: bool = True,
@@ -179,7 +180,7 @@ def get(
item_id: Any,
*,
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
id_attribute: str | InstrumentedAttribute[Any] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -190,7 +191,7 @@ def get_one(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -201,7 +202,7 @@ def get_one_or_none(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -242,7 +243,7 @@ def get_and_update(
def count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
load: LoadSpec | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
execution_options: dict[str, Any] | None = None,
@@ -281,7 +282,7 @@ def _get_update_many_statement(
supports_returning: bool,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement: ...
+ ) -> Update | ReturningUpdate[tuple[ModelT]]: ...
def upsert(
self,
@@ -315,7 +316,7 @@ def list_and_count(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
force_basic_query_mode: bool | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -328,7 +329,7 @@ def list(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -379,7 +380,7 @@ class SQLAlchemySyncRepository(SQLAlchemySyncRepositoryProtocol[ModelT], Filtera
def __init__(
self,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
session: Session | scoped_session[Session],
auto_expunge: bool = False,
auto_refresh: bool = True,
@@ -753,17 +754,16 @@ def delete_where(
execution_options = self._get_execution_options(execution_options)
loader_options, _loader_options_have_wildcard = self._get_loader_options(load)
model_type = self.model_type
- statement = lambda_stmt(lambda: delete(model_type)) # pyright: ignore[reportUnknownLambdaType]
- if loader_options:
- statement = statement.options(*loader_options)
- if execution_options:
- statement = statement.execution_options(**execution_options)
+ statement = self._get_base_stmt(
+ statement=delete(model_type),
+ loader_options=loader_options,
+ execution_options=execution_options,
+ )
statement = self._filter_select_by_kwargs(statement=statement, kwargs=kwargs)
statement = self._apply_filters(*filters, statement=statement, apply_pagination=False)
instances: list[ModelT] = []
if self._dialect.delete_executemany_returning:
- statement += lambda s: s.returning(model_type) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType]
- instances.extend(self.session.scalars(statement))
+ instances.extend(self.session.scalars(statement.returning(model_type)))
else:
instances.extend(
self.list(
@@ -780,7 +780,7 @@ def delete_where(
# backends will return a -1 if they can't determine impacted rowcount
# only compare length of selected instances to results if it's >= 0
self.session.rollback()
- raise RepositoryError(detail="Deleted count does not match fetched count. Rollback issued.")
+ raise RepositoryError(detail="Deleted count does not match fetched count. Rollback issued.")
self._flush_or_commit(auto_commit=auto_commit)
for instance in instances:
@@ -822,52 +822,27 @@ def exists(
)
return existing > 0
- def _to_lambda_stmt(
- self,
- statement: Select[tuple[ModelT]] | StatementLambdaElement,
- global_track_bound_values: bool = True,
- track_closure_variables: bool = True,
- enable_tracking: bool = True,
- track_bound_values: bool = True,
- track_on: object | None = None,
- ) -> StatementLambdaElement:
- if isinstance(statement, Select):
- statement = lambda_stmt(
- lambda: statement,
- track_bound_values=track_bound_values,
- global_track_bound_values=global_track_bound_values,
- track_closure_variables=track_closure_variables,
- enable_tracking=enable_tracking,
- track_on=track_on,
- )
- return statement
-
def _get_base_stmt(
self,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement,
- global_track_bound_values: bool = True,
- track_closure_variables: bool = True,
- enable_tracking: bool = True,
- track_bound_values: bool = True,
+ statement: StatementTypeT,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- track_on: object | None = None,
- ) -> StatementLambdaElement:
- statement = self._to_lambda_stmt(
- statement=statement,
- global_track_bound_values=global_track_bound_values,
- track_closure_variables=track_closure_variables,
- enable_tracking=enable_tracking,
- track_bound_values=track_bound_values,
- track_on=track_on,
- )
+ ) -> StatementTypeT:
+ """Get base statement with options applied.
+
+ Args:
+ statement: The select statement to modify
+ loader_options: Options for loading relationships
+ execution_options: Options for statement execution
+
+ Returns:
+ Modified select statement
+ """
if loader_options:
- statement = statement.add_criteria(lambda s: s.options(*loader_options), enable_tracking=False)
+ statement = cast("StatementTypeT", statement.options(*loader_options))
if execution_options:
- statement = statement.add_criteria(
- lambda s: s.execution_options(**execution_options), enable_tracking=False
- )
+ statement = cast("StatementTypeT", statement.execution_options(**execution_options))
return statement
def _get_delete_many_statement(
@@ -880,38 +855,27 @@ def _get_delete_many_statement(
statement_type: Literal["delete", "select"] = "delete",
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
+ ) -> Select[tuple[ModelT]] | Delete | ReturningDelete[tuple[ModelT]]:
# Base statement is static
- statement = delete(model_type) if statement_type == "delete" else select(model_type)
- if loader_options:
- statement = statement.options(*loader_options)
+ statement = self._get_base_stmt(
+ statement=delete(model_type) if statement_type == "delete" else select(model_type),
+ loader_options=loader_options,
+ execution_options=execution_options,
+ )
if execution_options:
statement = statement.execution_options(**execution_options)
if supports_returning and statement_type != "select":
- statement = statement.returning(model_type) # type: ignore[union-attr,assignment] # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownVariableType]
+ statement = cast("ReturningDelete[tuple[ModelT]]", statement.returning(model_type)) # type: ignore[union-attr,assignment] # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownVariableType]
if self._prefer_any:
- statement = statement.where(any_(id_chunk) == id_attribute) # type: ignore[arg-type]
- else:
- statement = statement.where(id_attribute.in_(id_chunk)) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
- return lambda_stmt(
- lambda: statement, # pyright: ignore[reportUnknownLambdaType]
- track_bound_values=True,
- track_on=[
- self._dialect.name,
- statement_type,
- model_type,
- id_attribute,
- statement, # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
- )
+ return statement.where(any_(id_chunk) == id_attribute) # type: ignore[arg-type]
+ return statement.where(id_attribute.in_(id_chunk)) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
def get(
self,
item_id: Any,
*,
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
id_attribute: str | InstrumentedAttribute[Any] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -949,15 +913,6 @@ def get(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_bound_values=False,
- track_closure_variables=False,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id_attribute,
- id(statement), # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._filter_select_by_kwargs(statement, [(id_attribute, item_id)])
instance = (self._execute(statement, uniquify=loader_options_have_wildcard)).scalar_one_or_none()
@@ -969,7 +924,7 @@ def get_one(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -1005,12 +960,6 @@ def get_one(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
@@ -1023,7 +972,7 @@ def get_one_or_none(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -1056,12 +1005,6 @@ def get_one_or_none(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
@@ -1237,7 +1180,7 @@ def get_and_update(
def count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -1269,21 +1212,15 @@ def count(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- enable_tracking=False,
- global_track_bound_values=False,
- track_bound_values=False,
- track_closure_variables=False,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement), # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
)
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
- statement = self._get_count_stmt(statement, loader_options, execution_options)
- results = self._execute(statement, uniquify=loader_options_have_wildcard)
+ results = self._execute(
+ statement=self._get_count_stmt(
+ statement=statement, loader_options=loader_options, execution_options=execution_options
+ ),
+ uniquify=loader_options_have_wildcard,
+ )
return cast(int, results.scalar_one())
def update(
@@ -1420,29 +1357,20 @@ def _get_update_many_statement(
supports_returning: bool,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
+ ) -> Update | ReturningUpdate[tuple[ModelT]]:
# Base update statement is static
- statement = update(model_type)
- if supports_returning:
- statement = statement.returning(model_type)
- if loader_options:
- statement = statement.options(*loader_options)
- if execution_options:
- statement = statement.execution_options(**execution_options)
- return lambda_stmt(
- lambda: statement,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- statement,
- f"{loader_options!s}:{execution_options!s}",
- ],
+ statement = self._get_base_stmt(
+ statement=update(table=model_type), loader_options=loader_options, execution_options=execution_options
)
+ if supports_returning:
+ return statement.returning(model_type)
+
+ return statement
def list_and_count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
force_basic_query_mode: bool | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
@@ -1530,7 +1458,7 @@ def _list_and_count_window(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -1565,23 +1493,13 @@ def _list_and_count_window(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
if order_by is None:
order_by = self.order_by or []
statement = self._apply_order_by(statement=statement, order_by=order_by)
- statement = statement.add_criteria(
- lambda s: s.add_columns(over(sql_func.count())),
- enable_tracking=False,
- )
statement = self._apply_filters(*filters, statement=statement)
statement = self._filter_select_by_kwargs(statement, kwargs)
- result = self._execute(statement, uniquify=loader_options_have_wildcard)
+ result = self._execute(statement.add_columns(over(sql_func.count())), uniquify=loader_options_have_wildcard)
count: int = 0
instances: list[ModelT] = []
for i, (instance, count_value) in enumerate(result):
@@ -1595,7 +1513,7 @@ def _list_and_count_basic(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -1630,12 +1548,6 @@ def _list_and_count_basic(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
)
if order_by is None:
order_by = self.order_by or []
@@ -1659,22 +1571,12 @@ def _list_and_count_basic(
def _get_count_stmt(
self,
- statement: StatementLambdaElement,
+ statement: Select[tuple[ModelT]],
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
+ ) -> Select[tuple[int]]:
# Count statement transformations are static
- return statement.add_criteria(
- lambda s: s.with_only_columns(sql_func.count(text("1")), maintain_column_froms=True).order_by(None),
- track_bound_values=False,
- track_closure_variables=False,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement),
- f"{loader_options!s}:{execution_options!s}",
- ],
- )
+ return statement.with_only_columns(sql_func.count(text("1")), maintain_column_froms=True).order_by(None)
def upsert(
self,
@@ -1891,7 +1793,7 @@ def list(
self,
*filters: StatementFilter | ColumnElement[bool],
auto_expunge: bool | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -1926,12 +1828,6 @@ def list(
statement=statement,
loader_options=loader_options,
execution_options=execution_options,
- track_on=[
- self._dialect.name,
- f"{self.model_type.__name__}",
- id(statement), # pyright: ignore[reportUnknownArgumentType]
- f"{loader_options!s}:{execution_options!s}",
- ],
)
if order_by is None:
order_by = self.order_by or []
@@ -2000,7 +1896,7 @@ def _attach_to_session(
def _execute(
self,
- statement: Select[Any] | StatementLambdaElement,
+ statement: Select[Any],
uniquify: bool = False,
) -> Result[Any]:
result = self.session.execute(statement)
@@ -2279,6 +2175,11 @@ def check_not_found(item_or_none: T | None) -> T:
def execute(
self,
- statement: Select[Any] | StatementLambdaElement,
+ statement: ReturningDelete[tuple[Any]]
+ | ReturningUpdate[tuple[Any]]
+ | Select[tuple[Any]]
+ | Update
+ | Delete
+ | Select[Any],
) -> Result[Any]:
return self.session.execute(statement)
diff --git a/advanced_alchemy/repository/_util.py b/advanced_alchemy/repository/_util.py
index 04cee2c5..5cf24d52 100644
--- a/advanced_alchemy/repository/_util.py
+++ b/advanced_alchemy/repository/_util.py
@@ -1,7 +1,10 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Iterable, Literal, Protocol, Sequence, Union, cast
+from typing import TYPE_CHECKING, Any, Iterable, Literal, Protocol, Sequence, Union, cast, overload
+from sqlalchemy import (
+ Select,
+)
from sqlalchemy.orm import InstrumentedAttribute, MapperProperty, RelationshipProperty, joinedload, selectinload
from sqlalchemy.orm.strategy_options import (
_AbstractLoad, # pyright: ignore[reportPrivateUsage] # pyright: ignore[reportPrivateUsage]
@@ -16,14 +19,17 @@
InAnyFilter,
PaginationFilter,
StatementFilter,
+ StatementTypeT,
)
from advanced_alchemy.repository.typing import ModelT, OrderingPair
if TYPE_CHECKING:
from sqlalchemy import (
+ Delete,
Dialect,
- StatementLambdaElement,
+ Update,
)
+ from sqlalchemy.sql.dml import ReturningDelete, ReturningUpdate
from advanced_alchemy.base import ModelProtocol
@@ -135,12 +141,44 @@ class FilterableRepository(FilterableRepositoryProtocol[ModelT]):
order_by: list[OrderingPair] | OrderingPair | None = None
"""List of ordering pairs to use for sorting."""
+ @overload
+ def _apply_filters(
+ self,
+ *filters: StatementFilter | ColumnElement[bool],
+ apply_pagination: bool = True,
+ statement: Select[tuple[ModelT]],
+ ) -> Select[tuple[ModelT]]: ...
+
+ @overload
def _apply_filters(
self,
*filters: StatementFilter | ColumnElement[bool],
apply_pagination: bool = True,
- statement: StatementLambdaElement,
- ) -> StatementLambdaElement:
+ statement: Delete,
+ ) -> Delete: ...
+
+ @overload
+ def _apply_filters(
+ self,
+ *filters: StatementFilter | ColumnElement[bool],
+ apply_pagination: bool = True,
+ statement: ReturningDelete[tuple[ModelT]] | ReturningUpdate[tuple[ModelT]],
+ ) -> ReturningDelete[tuple[ModelT]] | ReturningUpdate[tuple[ModelT]]: ...
+
+ @overload
+ def _apply_filters(
+ self,
+ *filters: StatementFilter | ColumnElement[bool],
+ apply_pagination: bool = True,
+ statement: Update,
+ ) -> Update: ...
+
+ def _apply_filters(
+ self,
+ *filters: StatementFilter | ColumnElement[bool],
+ apply_pagination: bool = True,
+ statement: StatementTypeT,
+ ) -> StatementTypeT:
"""Apply filters to a select statement.
Args:
@@ -148,67 +186,36 @@ def _apply_filters(
apply_pagination: applies pagination filters if true
statement: select statement to apply filters
- Keyword Args:
- select: select to apply filters against
-
Returns:
The select with filters applied.
"""
for filter_ in filters:
if isinstance(filter_, (PaginationFilter,)):
if apply_pagination:
- statement = filter_.append_to_lambda_statement(statement, self.model_type)
+ statement = filter_.append_to_statement(statement, self.model_type)
elif isinstance(filter_, (InAnyFilter,)):
- statement = filter_.append_to_lambda_statement(statement, self.model_type, prefer_any=self._prefer_any)
+ statement = filter_.append_to_statement(statement, self.model_type)
elif isinstance(filter_, ColumnElement):
- statement = self._filter_by_expression(expression=filter_, statement=statement)
+ statement = cast("StatementTypeT", statement.where(filter_))
else:
- statement = filter_.append_to_lambda_statement(statement, self.model_type)
+ statement = filter_.append_to_statement(statement, self.model_type)
return statement
def _filter_select_by_kwargs(
self,
- statement: StatementLambdaElement,
+ statement: StatementTypeT,
kwargs: dict[Any, Any] | Iterable[tuple[Any, Any]],
- ) -> StatementLambdaElement:
+ ) -> StatementTypeT:
for key, val in dict(kwargs).items():
- statement = self._filter_by_where(statement=statement, field_name=key, value=val)
+ field = get_instrumented_attr(self.model_type, key)
+ statement = cast("StatementTypeT", statement.where(field == val))
return statement
- def _filter_by_expression(
- self,
- statement: StatementLambdaElement,
- expression: ColumnElement[bool],
- ) -> StatementLambdaElement:
- """Add a where clause to the statement."""
- # Static WHERE clause - no need to track
- return statement.add_criteria(
- lambda s: s.where(expression),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self._dialect.name, self.model_type.__name__, id(expression)],
- )
-
- def _filter_by_where(
- self,
- statement: StatementLambdaElement,
- field_name: str | InstrumentedAttribute[Any],
- value: Any,
- ) -> StatementLambdaElement:
- field = get_instrumented_attr(self.model_type, field_name)
- # Track only the value parameter since it's dynamic
- return statement.add_criteria(
- lambda s: s.where(field == value),
- track_bound_values=True,
- track_closure_variables=False,
- track_on=[self._dialect.name, self.model_type.__name__, field, id(value)],
- )
-
def _apply_order_by(
self,
- statement: StatementLambdaElement,
+ statement: StatementTypeT,
order_by: list[tuple[str | InstrumentedAttribute[Any], bool]] | tuple[str | InstrumentedAttribute[Any], bool],
- ) -> StatementLambdaElement:
+ ) -> StatementTypeT:
if not isinstance(order_by, list):
order_by = [order_by]
for order_field, is_desc in order_by:
@@ -218,14 +225,10 @@ def _apply_order_by(
def _order_by_attribute(
self,
- statement: StatementLambdaElement,
+ statement: StatementTypeT,
field: InstrumentedAttribute[Any],
is_desc: bool,
- ) -> StatementLambdaElement:
- fragment = field.desc() if is_desc else field.asc()
- # Static ORDER BY - no need to track
- return statement.add_criteria(
- lambda s: s.order_by(fragment),
- track_closure_variables=False,
- track_on=[self._dialect.name, self.model_type.__name__, field, is_desc],
- )
+ ) -> StatementTypeT:
+ if not isinstance(statement, Select):
+ return statement
+ return cast("StatementTypeT", statement.order_by(field.desc() if is_desc else field.asc()))
diff --git a/advanced_alchemy/repository/memory/_async.py b/advanced_alchemy/repository/memory/_async.py
index e6d69f16..37fbfb8b 100644
--- a/advanced_alchemy/repository/memory/_async.py
+++ b/advanced_alchemy/repository/memory/_async.py
@@ -11,6 +11,7 @@
Dialect,
Select,
StatementLambdaElement,
+ Update,
)
from sqlalchemy.orm import InstrumentedAttribute
@@ -46,6 +47,7 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio.scoping import async_scoped_session
from sqlalchemy.orm.strategy_options import _AbstractLoad # pyright: ignore[reportPrivateUsage]
+ from sqlalchemy.sql.dml import ReturningUpdate
from advanced_alchemy.repository._util import (
LoadSpec,
@@ -399,8 +401,8 @@ def _get_update_many_statement(
supports_returning: bool,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
- return cast("StatementLambdaElement", self.statement)
+ ) -> Update | ReturningUpdate[tuple[ModelT]]:
+ return self.statement # type: ignore[no-any-return] # pyright: ignore[reportReturnType]
@classmethod
async def check_health(cls, session: AsyncSession | async_scoped_session[AsyncSession]) -> bool:
diff --git a/advanced_alchemy/repository/memory/_sync.py b/advanced_alchemy/repository/memory/_sync.py
index dc3c514e..4ace515e 100644
--- a/advanced_alchemy/repository/memory/_sync.py
+++ b/advanced_alchemy/repository/memory/_sync.py
@@ -13,6 +13,7 @@
Dialect,
Select,
StatementLambdaElement,
+ Update,
)
from sqlalchemy.orm import InstrumentedAttribute, Session
@@ -47,6 +48,7 @@
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.strategy_options import _AbstractLoad # pyright: ignore[reportPrivateUsage]
+ from sqlalchemy.sql.dml import ReturningUpdate
from advanced_alchemy.repository._util import (
LoadSpec,
@@ -400,8 +402,8 @@ def _get_update_many_statement(
supports_returning: bool,
loader_options: list[_AbstractLoad] | None,
execution_options: dict[str, Any] | None,
- ) -> StatementLambdaElement:
- return cast("StatementLambdaElement", self.statement)
+ ) -> Update | ReturningUpdate[tuple[ModelT]]:
+ return self.statement # type: ignore[no-any-return] # pyright: ignore[reportReturnType]
@classmethod
def check_health(cls, session: Session | scoped_session[Session]) -> bool:
diff --git a/advanced_alchemy/service/_async.py b/advanced_alchemy/service/_async.py
index 1b94ed53..e5ca5a27 100644
--- a/advanced_alchemy/service/_async.py
+++ b/advanced_alchemy/service/_async.py
@@ -36,7 +36,7 @@
from advanced_alchemy.utils.dataclass import Empty, EmptyType
if TYPE_CHECKING:
- from sqlalchemy import Select, StatementLambdaElement
+ from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio.scoping import async_scoped_session
from sqlalchemy.orm import InstrumentedAttribute
@@ -104,7 +104,7 @@ class SQLAlchemyAsyncRepositoryReadService(Generic[ModelT], ResultConverter):
def __init__(
self,
session: AsyncSession | async_scoped_session[AsyncSession],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool = False,
auto_refresh: bool = True,
auto_commit: bool = False,
@@ -146,7 +146,7 @@ def __init__(
async def count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -208,7 +208,7 @@ async def get(
self,
item_id: Any,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
id_attribute: str | InstrumentedAttribute[Any] | None = None,
auto_expunge: bool | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
@@ -246,7 +246,7 @@ async def get(
async def get_one(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
load: LoadSpec | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
@@ -281,7 +281,7 @@ async def get_one(
async def get_one_or_none(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -349,7 +349,7 @@ async def to_model(
async def list_and_count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
force_basic_query_mode: bool | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
@@ -392,7 +392,7 @@ async def list_and_count(
async def new(
cls,
session: AsyncSession | async_scoped_session[AsyncSession] | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
config: SQLAlchemyAsyncConfig | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -429,7 +429,7 @@ async def new(
async def list(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
diff --git a/advanced_alchemy/service/_sync.py b/advanced_alchemy/service/_sync.py
index c971d0d1..20251695 100644
--- a/advanced_alchemy/service/_sync.py
+++ b/advanced_alchemy/service/_sync.py
@@ -38,7 +38,7 @@
from advanced_alchemy.utils.dataclass import Empty, EmptyType
if TYPE_CHECKING:
- from sqlalchemy import Select, StatementLambdaElement
+ from sqlalchemy import Select
from sqlalchemy.orm import InstrumentedAttribute, Session
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.sql import ColumnElement
@@ -105,7 +105,7 @@ class SQLAlchemySyncRepositoryReadService(Generic[ModelT], ResultConverter):
def __init__(
self,
session: Session | scoped_session[Session],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool = False,
auto_refresh: bool = True,
auto_commit: bool = False,
@@ -147,7 +147,7 @@ def __init__(
def count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
execution_options: dict[str, Any] | None = None,
@@ -209,7 +209,7 @@ def get(
self,
item_id: Any,
*,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
id_attribute: str | InstrumentedAttribute[Any] | None = None,
auto_expunge: bool | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
@@ -247,7 +247,7 @@ def get(
def get_one(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
load: LoadSpec | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
@@ -282,7 +282,7 @@ def get_one(
def get_one_or_none(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -350,7 +350,7 @@ def to_model(
def list_and_count(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
force_basic_query_mode: bool | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
@@ -393,7 +393,7 @@ def list_and_count(
def new(
cls,
session: Session | scoped_session[Session] | None = None,
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
config: SQLAlchemySyncConfig | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
load: LoadSpec | None = None,
@@ -430,7 +430,7 @@ def new(
def list(
self,
*filters: StatementFilter | ColumnElement[bool],
- statement: Select[tuple[ModelT]] | StatementLambdaElement | None = None,
+ statement: Select[tuple[ModelT]] | None = None,
auto_expunge: bool | None = None,
order_by: list[OrderingPair] | OrderingPair | None = None,
error_messages: ErrorMessages | None | EmptyType = Empty,
diff --git a/docs/conf.py b/docs/conf.py
index 0170888d..0997ce3c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -149,6 +149,7 @@
(PY_CLASS, "Litestar"),
(PY_CLASS, "DTOFieldDefinition"),
(PY_CLASS, "advanced_alchemy.extensions.litestar.plugins._slots_base.SlotsBase"),
+ (PY_CLASS, "advanced_alchemy.filters.StatementTypeT"),
(PY_CLASS, "Default"),
(PY_CLASS, "bytes-like"),
(PY_CLASS, "scoped_session"),
diff --git a/pyproject.toml b/pyproject.toml
index 23cb786d..f47a77bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -421,6 +421,7 @@ venvPath = "."
add_editors_note = true
cache = true
ruff_fix = true
+ruff_format = true
update_docstrings = true
[tool.unasyncd.files]
diff --git a/tests/integration/test_lambda_stmt.py b/tests/integration/test_lambda_stmt.py
deleted file mode 100644
index 24a10ac6..00000000
--- a/tests/integration/test_lambda_stmt.py
+++ /dev/null
@@ -1,115 +0,0 @@
-from __future__ import annotations
-
-from pathlib import Path
-from typing import TYPE_CHECKING
-
-import pytest
-from sqlalchemy import ForeignKey, String, create_engine, func, select
-from sqlalchemy.orm import Mapped, Session, mapped_column, relationship, sessionmaker
-
-from advanced_alchemy.base import UUIDBase
-from advanced_alchemy.repository import SQLAlchemySyncRepository
-
-if TYPE_CHECKING:
- from pytest import MonkeyPatch
-
-xfail = pytest.mark.xfail
-
-
-# This test does not work when run in group for some reason.
-# If you run individually, it'll pass.
-@pytest.mark.xdist_group("lambda")
-@xfail()
-def test_lambda_statement_quirks(monkeypatch: MonkeyPatch, tmp_path: Path) -> None:
- from sqlalchemy.orm import DeclarativeBase
-
- from advanced_alchemy import base
-
- orm_registry = base.create_registry()
-
- class NewUUIDBase(base.UUIDPrimaryKey, base.CommonTableAttributes, DeclarativeBase):
- registry = orm_registry
-
- class NewBigIntBase(base.BigIntPrimaryKey, base.CommonTableAttributes, DeclarativeBase):
- registry = orm_registry
-
- monkeypatch.setattr(base, "UUIDBase", NewUUIDBase)
-
- monkeypatch.setattr(base, "BigIntBase", NewBigIntBase)
-
- class Country(UUIDBase):
- name: Mapped[str] = mapped_column(String(length=50)) # pyright: ignore
-
- class State(UUIDBase):
- name: Mapped[str] = mapped_column(String(length=50)) # pyright: ignore
- country_id: Mapped[str] = mapped_column(ForeignKey(Country.id))
-
- country = relationship(Country)
-
- class USStateRepository(SQLAlchemySyncRepository[State]):
- model_type = State
-
- engine = create_engine("sqlite:///:memory:", echo=True)
- session_factory: sessionmaker[Session] = sessionmaker(engine, expire_on_commit=False)
-
- with engine.begin() as conn:
- State.metadata.create_all(conn)
-
- engine.clear_compiled_cache()
- with session_factory() as db_session:
- usa = Country(name="United States of America")
- france = Country(name="France")
- db_session.add(usa)
- db_session.add(france)
-
- california = State(name="California", country=usa)
- oregon = State(name="Oregon", country=usa)
- ile_de_france = State(name="Île-de-France", country=france)
-
- repo = USStateRepository(session=db_session)
- repo.add_many([california, oregon, ile_de_france], auto_commit=True)
-
- # Using only the ORM, this works fine:
-
- stmt = select(State).where(State.country_id == usa.id).with_only_columns(func.count())
- count = db_session.execute(stmt).scalar_one()
- assert count == 2, f"Expected 2, got {count}"
- count = db_session.execute(stmt).scalar_one()
- assert count == 2, f"Expected 2, got {count}"
-
- stmt = select(State).where(State.country == usa).with_only_columns(func.count(), maintain_column_froms=True)
- count = db_session.execute(stmt).scalar_one()
- assert count == 2, f"Expected 2, got {count}"
- count = db_session.execute(stmt).scalar_one()
- assert count == 2, f"Expected 2, got {count}"
-
- # Using the repository, this works:
- stmt1 = select(State).where(State.country_id == usa.id)
-
- count = repo.count(statement=stmt1)
- assert count == 2, f"Expected 2, got {count}"
-
- count = repo.count(statement=stmt1)
- assert count == 2, f"Expected 2, got {count}"
-
- # But this would fail (only after the second query) (lambda caching test):
- stmt2 = select(State).where(State.country == usa)
-
- count = repo.count(statement=stmt2)
- assert count == 2, f"Count Expected 2, got {count}"
-
- count = repo.count(State.country == usa)
- assert count == 2, f"Count Expression Expected 2, got {count}"
-
- count = repo.count(statement=stmt2)
- assert count == 2, f"Recount Statement Expected 2, got {count}"
-
- # It also failed with
- states = repo.list(statement=stmt2)
- count = len(states)
- assert count == 2, f"List Statement Expected 2, got {count}"
-
- _states, count = repo.list_and_count(statement=stmt2)
- assert count == 2, f"List and Count Expected 2, got {count}"
- _states, count = repo.list_and_count(statement=stmt2, force_basic_query_mode=True)
- assert count == 2, f"List and Count (force_basic_query_mode) Expected 2, got {count}"
diff --git a/tests/unit/test_repository.py b/tests/unit/test_repository.py
index 48f8bd47..04bf835f 100644
--- a/tests/unit/test_repository.py
+++ b/tests/unit/test_repository.py
@@ -598,7 +598,7 @@ async def test_sqlalchemy_repo_list_with_pagination(
"""Test list operation with pagination."""
statement = MagicMock()
mock_repo_execute.return_value = MagicMock()
- mocker.patch.object(LimitOffset, "append_to_lambda_statement", return_value=statement)
+ mocker.patch.object(LimitOffset, "append_to_statement", return_value=statement)
mock_repo_execute.return_value = MagicMock()
await maybe_async(mock_repo.list(LimitOffset(2, 3)))
mock_repo._execute.assert_called_with(statement, uniquify=False) # pyright: ignore[reportFunctionMemberAccess,reportPrivateUsage]
@@ -613,7 +613,7 @@ async def test_sqlalchemy_repo_list_with_before_after_filter(
statement = MagicMock()
mocker.patch.object(mock_repo.model_type.updated_at, "__lt__", return_value="lt")
mocker.patch.object(mock_repo.model_type.updated_at, "__gt__", return_value="gt")
- mocker.patch.object(BeforeAfter, "append_to_lambda_statement", return_value=statement)
+ mocker.patch.object(BeforeAfter, "append_to_statement", return_value=statement)
mock_repo_execute.return_value = MagicMock()
await maybe_async(mock_repo.list(BeforeAfter("updated_at", datetime.max, datetime.min)))
mock_repo._execute.assert_called_with(statement, uniquify=False) # pyright: ignore[reportFunctionMemberAccess,reportPrivateUsage]
@@ -629,7 +629,7 @@ async def test_sqlalchemy_repo_list_with_on_before_after_filter(
statement = MagicMock()
mocker.patch.object(mock_repo.model_type.updated_at, "__le__", return_value="le")
mocker.patch.object(mock_repo.model_type.updated_at, "__ge__", return_value="ge")
- mocker.patch.object(OnBeforeAfter, "append_to_lambda_statement", return_value=statement)
+ mocker.patch.object(OnBeforeAfter, "append_to_statement", return_value=statement)
mock_repo_execute.return_value = MagicMock()
await maybe_async(mock_repo.list(OnBeforeAfter("updated_at", datetime.max, datetime.min)))
@@ -646,7 +646,7 @@ async def test_sqlalchemy_repo_list_with_collection_filter(
field_name = "id"
mock_repo_execute.return_value = MagicMock()
mock_repo.statement.where.return_value = mock_repo.statement # pyright: ignore[reportFunctionMemberAccess]
- mocker.patch.object(CollectionFilter, "append_to_lambda_statement", return_value=mock_repo.statement)
+ mocker.patch.object(CollectionFilter, "append_to_statement", return_value=mock_repo.statement)
values = [1, 2, 3]
await maybe_async(mock_repo.list(CollectionFilter(field_name, values)))
mock_repo._execute.assert_called_with(mock_repo.statement, uniquify=False) # pyright: ignore[reportFunctionMemberAccess,reportPrivateUsage]
@@ -664,7 +664,7 @@ async def test_sqlalchemy_repo_list_with_null_collection_filter(
mock_repo.statement.where.return_value = mock_repo.statement # pyright: ignore[reportFunctionMemberAccess]
monkeypatch.setattr(
CollectionFilter,
- "append_to_lambda_statement",
+ "append_to_statement",
MagicMock(return_value=mock_repo.statement),
)
await maybe_async(mock_repo.list(CollectionFilter(field_name, None))) # pyright: ignore[reportFunctionMemberAccess,reportUnknownArgumentType]
@@ -685,7 +685,7 @@ async def test_sqlalchemy_repo_empty_list_with_collection_filter(
await maybe_async(mock_repo.list(CollectionFilter(field_name, values)))
monkeypatch.setattr(
CollectionFilter,
- "append_to_lambda_statement",
+ "append_to_statement",
MagicMock(return_value=mock_repo.statement),
)
await maybe_async(mock_repo.list(CollectionFilter(field_name, values)))
@@ -704,7 +704,7 @@ async def test_sqlalchemy_repo_list_with_not_in_collection_filter(
mock_repo.statement.where.return_value = mock_repo.statement # pyright: ignore[reportFunctionMemberAccess]
monkeypatch.setattr(
NotInCollectionFilter,
- "append_to_lambda_statement",
+ "append_to_statement",
MagicMock(return_value=mock_repo.statement),
)
values = [1, 2, 3]
@@ -724,7 +724,7 @@ async def test_sqlalchemy_repo_list_with_null_not_in_collection_filter(
mock_repo.statement.where.return_value = mock_repo.statement # pyright: ignore[reportFunctionMemberAccess]
monkeypatch.setattr(
NotInCollectionFilter,
- "append_to_lambda_statement",
+ "append_to_statement",
MagicMock(return_value=mock_repo.statement),
)
await maybe_async(mock_repo.list(NotInCollectionFilter[str](field_name, None))) # pyright: ignore[reportFunctionMemberAccess]
@@ -801,7 +801,7 @@ def test_filter_in_collection_noop_if_collection_empty(mock_repo: SQLAlchemyAsyn
"""Ensures we don't filter on an empty collection."""
statement = MagicMock()
filter = CollectionFilter(field_name="id", values=[]) # type:ignore[var-annotated]
- statement = filter.append_to_lambda_statement(statement, MagicMock()) # type:ignore[assignment]
+ statement = filter.append_to_statement(statement, MagicMock()) # type:ignore[assignment]
mock_repo.statement.where.assert_not_called() # pyright: ignore[reportFunctionMemberAccess]
@@ -813,12 +813,23 @@ def test_filter_in_collection_noop_if_collection_empty(mock_repo: SQLAlchemyAsyn
(datetime.max, None),
],
)
-def test_filter_on_datetime_field(before: datetime, after: datetime, mock_repo: SQLAlchemyAsyncRepository[Any]) -> None:
+def test_filter_on_datetime_field(
+ before: datetime,
+ after: datetime,
+ mock_repo: SQLAlchemyAsyncRepository[Any],
+ mocker: MockerFixture,
+ monkeypatch: MonkeyPatch,
+) -> None:
"""Test through branches of _filter_on_datetime_field()"""
- field_mock = MagicMock()
+ field_mock = MagicMock(return_value=before or after)
statement = MagicMock()
field_mock.__gt__ = field_mock.__lt__ = lambda self, other: True # pyright: ignore[reportFunctionMemberAccess,reportUnknownLambdaType]
+ monkeypatch.setattr(
+ BeforeAfter,
+ "append_to_statement",
+ MagicMock(return_value=mock_repo.statement),
+ )
filter = BeforeAfter(field_name="updated_at", before=before, after=after)
- statement = filter.append_to_lambda_statement(statement, MagicMock()) # type:ignore[assignment]
+ statement = filter.append_to_statement(statement, MagicMock(return_value=before or after)) # type:ignore[assignment]
mock_repo.model_type.updated_at = field_mock
mock_repo.statement.where.assert_not_called() # pyright: ignore[reportFunctionMemberAccess]
diff --git a/uv.lock b/uv.lock
index c9d96d4e..f79c0732 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1285,29 +1285,29 @@ wheels = [
[[package]]
name = "faker"
-version = "30.8.2"
+version = "33.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c1/df/7574c0d13f0bbab725e52bec4b00783aaa14163fe9093dde11a928a4c638/faker-30.8.2.tar.gz", hash = "sha256:aa31b52cdae3673d6a78b4857c7bcdc0e98f201a5cb77d7827fa9e6b5876da94", size = 1808329 }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/c7/0782eb872b96ee571701237be0dd85aef3713a15045ba42752a8aac7c8ce/faker-33.0.0.tar.gz", hash = "sha256:9b01019c1ddaf2253ca2308c0472116e993f4ad8fc9905f82fa965e0c6f932e9", size = 1850076 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/64/82/f7d0c0a4ab512fd1572a315eec903d50a578c75d5aa894cf3f5cc04025e5/Faker-30.8.2-py3-none-any.whl", hash = "sha256:4a82b2908cd19f3bba1a4da2060cc4eb18a40410ccdf9350d071d79dc92fe3ce", size = 1846458 },
+ { url = "https://files.pythonhosted.org/packages/c0/c3/0451555e7a9a233bc17f128cff7654ec60036d4ccbb8397dd949f28df176/Faker-33.0.0-py3-none-any.whl", hash = "sha256:68e5580cb6b4226710886e595eabc13127149d6e71e9d1db65506a7fbe2c7fce", size = 1889118 },
]
[[package]]
name = "fastapi"
-version = "0.115.4"
+version = "0.115.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a9/db/5781f19bd30745885e0737ff3fdd4e63e7bc691710f9da691128bb0dc73b/fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349", size = 300737 }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/29/f71316b9273b6552a263748e49cd7b83898dc9499a663d30c7b9cb853cb8/fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", size = 301047 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/99/f6/af0d1f58f86002be0cf1e2665cdd6f7a4a71cdc8a7a9438cdc9e3b5375fe/fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742", size = 94732 },
+ { url = "https://files.pythonhosted.org/packages/54/c4/148d5046a96c428464557264877ae5a9338a83bbe0df045088749ec89820/fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796", size = 94866 },
]
[package.optional-dependencies]
@@ -1553,7 +1553,7 @@ wheels = [
[[package]]
name = "google-cloud-spanner"
-version = "3.50.0"
+version = "3.50.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", extra = ["grpc"] },
@@ -1564,21 +1564,21 @@ dependencies = [
{ name = "protobuf" },
{ name = "sqlparse" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/de/c2/46b5401d253b096712ed3a3fb4c66d667561ea7efff6d2b7b0016fc410c6/google_cloud_spanner-3.50.0.tar.gz", hash = "sha256:d585aa9c2adca1ec494db93706ea0a51bce924a4e154800e049e5599adc9deb5", size = 575252 }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/42/1459e6677381389cd96c098b5f515b4c6da28c899389d84043e9ac11b096/google_cloud_spanner-3.50.1.tar.gz", hash = "sha256:82937ea03b55de86bddf622f555aeae65ae86bb4f28ab35bd920ac505917c9bf", size = 575566 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/a9/fde441a9f46ff072c5d9c9b91a6b4a2b114acf1eeb5853fd0080f9e9d6b7/google_cloud_spanner-3.50.0-py2.py3-none-any.whl", hash = "sha256:6410e15d0f2b9229c0d23592ea98ab6b7387eff9e609b770b3ee0ecab0ce1fed", size = 416374 },
+ { url = "https://files.pythonhosted.org/packages/1c/ce/35abda83da7eacf458a55f628d350bb7c8f91215767b45a5cd2da4528e9b/google_cloud_spanner-3.50.1-py2.py3-none-any.whl", hash = "sha256:9d399aa53fae58816023a4eb31fa267333c3a879a9221229e7f06fdda543884a", size = 416477 },
]
[[package]]
name = "googleapis-common-protos"
-version = "1.65.0"
+version = "1.66.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/53/3b/1599ceafa875ffb951480c8c74f4b77646a6b80e80970698f2aa93c216ce/googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0", size = 113657 }
+sdist = { url = "https://files.pythonhosted.org/packages/ff/a7/8e9cccdb1c49870de6faea2a2764fa23f627dd290633103540209f03524c/googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c", size = 114376 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/08/49bfe7cf737952cc1a9c43e80cc258ed45dad7f183c5b8276fc94cb3862d/googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", size = 220890 },
+ { url = "https://files.pythonhosted.org/packages/a0/0f/c0713fb2b3d28af4b2fded3291df1c4d4f79a00d15c2374a9e010870016c/googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed", size = 221682 },
]
[package.optional-dependencies]
@@ -3009,15 +3009,15 @@ wheels = [
[[package]]
name = "pyright"
-version = "1.1.388"
+version = "1.1.389"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9c/83/e9867538a794638d2d20ac3ab3106a31aca1d9cfea530c9b2921809dae03/pyright-1.1.388.tar.gz", hash = "sha256:0166d19b716b77fd2d9055de29f71d844874dbc6b9d3472ccd22df91db3dfa34", size = 21939 }
+sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/03/57/7fb00363b7f267a398c5bdf4f55f3e64f7c2076b2e7d2901b3373d52b6ff/pyright-1.1.388-py3-none-any.whl", hash = "sha256:c7068e9f2c23539c6ac35fc9efac6c6c1b9aa5a0ce97a9a8a6cf0090d7cbf84c", size = 18579 },
+ { url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 },
]
[[package]]
@@ -3255,16 +3255,16 @@ wheels = [
[[package]]
name = "rich-click"
-version = "1.8.3"
+version = "1.8.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3a/a9/a1f1af87e83832d794342fbc09c96cc7cd6798b8dfb8adfbe6ccbef8d70c/rich_click-1.8.3.tar.gz", hash = "sha256:6d75bdfa7aa9ed2c467789a0688bc6da23fbe3a143e19aa6ad3f8bac113d2ab3", size = 38209 }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f4/e48dc2850662526a26fb0961aacb0162c6feab934312b109b748ae4efee2/rich_click-1.8.4.tar.gz", hash = "sha256:0f49471f04439269d0e66a6f43120f52d11d594869a2a0be600cfb12eb0616b9", size = 38247 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c6/ea/5a0c5a8e6532e971983d1b0fc99268eb66a10f489da35d9022ce01044191/rich_click-1.8.3-py3-none-any.whl", hash = "sha256:636d9c040d31c5eee242201b5bf4f2d358bfae4db14bb22ec1cafa717cfd02cd", size = 35032 },
+ { url = "https://files.pythonhosted.org/packages/84/f3/72f93d8494ee641bde76bfe1208cf4abc44c6f9448673762f6077bc162d6/rich_click-1.8.4-py3-none-any.whl", hash = "sha256:2d2841b3cebe610d5682baa1194beaf78ab00c4fa31931533261b5eba2ee80b7", size = 35071 },
]
[[package]]
@@ -4024,11 +4024,11 @@ wheels = [
[[package]]
name = "sqlparse"
-version = "0.5.1"
+version = "0.5.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502 }
+sdist = { url = "https://files.pythonhosted.org/packages/57/61/5bc3aff85dc5bf98291b37cf469dab74b3d0aef2dd88eade9070a200af05/sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f", size = 84951 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156 },
+ { url = "https://files.pythonhosted.org/packages/7a/13/5f6654c9d915077fae255686ca6fa42095b62b7337e3e1aa9e82caa6f43a/sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e", size = 44407 },
]
[[package]]
@@ -4298,11 +4298,11 @@ wheels = [
[[package]]
name = "types-setuptools"
-version = "75.3.0.20241107"
+version = "75.3.0.20241112"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2b/8b/626019697000a97e7eddf7d4f5a6951ac72aa167b6d29f06954a901229d4/types-setuptools-75.3.0.20241107.tar.gz", hash = "sha256:f66710e1cd4a936e5fcc12d4e49be1a67c34372cf753e87ebe704426451b4012", size = 43666 }
+sdist = { url = "https://files.pythonhosted.org/packages/39/01/8027bb0285d5c65747b53b9d3b11805447bd8df794164a63ac470efbefed/types-setuptools-75.3.0.20241112.tar.gz", hash = "sha256:f9e1ebd17a56f606e16395c4ee4efa1cdc394b9a2a0ee898a624058b4b62ef8f", size = 43723 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/df/29/71f63f64bbdca142995ac816d1fa72fae298886c73f21086eed05596fc95/types_setuptools-75.3.0.20241107-py3-none-any.whl", hash = "sha256:bc6de6e2bcb6d610556304d0a69fe4ca208ac4896162647314ecfd9fd73d8550", size = 67584 },
+ { url = "https://files.pythonhosted.org/packages/35/6a/cbe08e52046544198a69feefc998c0ec198099e96f4254dfb6581e7b482f/types_setuptools-75.3.0.20241112-py3-none-any.whl", hash = "sha256:78cb5fef4a6056d2f37114d27da90f4655a306e4e38042d7034a8a880bc3f5dd", size = 67639 },
]
[[package]]