Skip to content

Commit 4376890

Browse files
authored
fix: implement library installed check (#217)
* fix: implement library installed check * fix: populate load and execution_options for update * fix: remove runtime checkable * feat: `count` doesn't not expect to have an argument here. * fix: align count syntax
1 parent 3b5c243 commit 4376890

File tree

9 files changed

+55
-11
lines changed

9 files changed

+55
-11
lines changed

advanced_alchemy/repository/_async.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ async def update(
218218
auto_expunge: bool | None = None,
219219
auto_refresh: bool | None = None,
220220
id_attribute: str | InstrumentedAttribute[Any] | None = None,
221+
load: LoadSpec | None = None,
222+
execution_options: dict[str, Any] | None = None,
221223
) -> ModelT: ...
222224

223225
async def update_many(
@@ -1109,7 +1111,6 @@ async def count(
11091111
"""
11101112
with wrap_sqlalchemy_exception():
11111113
statement = self.statement if statement is None else statement
1112-
fragment = self.get_id_attribute_value(self.model_type)
11131114
loader_options, loader_options_have_wildcard = self._get_loader_options(load)
11141115
statement = self._get_base_stmt(
11151116
statement=statement,
@@ -1119,7 +1120,7 @@ async def count(
11191120
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
11201121
statement = self._filter_select_by_kwargs(statement, kwargs)
11211122
statement = statement.add_criteria(
1122-
lambda s: s.with_only_columns(sql_func.count(fragment), maintain_column_froms=True).order_by(None),
1123+
lambda s: s.with_only_columns(sql_func.count(text("1")), maintain_column_froms=True).order_by(None),
11231124
)
11241125
results = await self._execute(statement, uniquify=loader_options_have_wildcard)
11251126
return cast(int, results.scalar_one())
@@ -1133,6 +1134,8 @@ async def update(
11331134
auto_expunge: bool | None = None,
11341135
auto_refresh: bool | None = None,
11351136
id_attribute: str | InstrumentedAttribute[Any] | None = None,
1137+
load: LoadSpec | None = None,
1138+
execution_options: dict[str, Any] | None = None,
11361139
) -> ModelT:
11371140
"""Update instance with the attribute values present on `data`.
11381141
@@ -1152,6 +1155,8 @@ async def update(
11521155
:class:`SQLAlchemyAsyncRepository.auto_commit <SQLAlchemyAsyncRepository>`
11531156
id_attribute: Allows customization of the unique identifier to use for model fetching.
11541157
Defaults to `id`, but can reference any surrogate or candidate key for the table.
1158+
load: Set relationships to be loaded
1159+
execution_options: Set default execution options
11551160
11561161
Returns:
11571162
The updated instance.
@@ -1165,7 +1170,7 @@ async def update(
11651170
id_attribute=id_attribute,
11661171
)
11671172
# this will raise for not found, and will put the item in the session
1168-
await self.get(item_id, id_attribute=id_attribute)
1173+
await self.get(item_id, id_attribute=id_attribute, load=load, execution_options=execution_options)
11691174
# this will merge the inbound data to the instance we just put in the session
11701175
instance = await self._attach_to_session(data, strategy="merge")
11711176
await self._flush_or_commit(auto_commit=auto_commit)

advanced_alchemy/repository/_sync.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ def update(
219219
auto_expunge: bool | None = None,
220220
auto_refresh: bool | None = None,
221221
id_attribute: str | InstrumentedAttribute[Any] | None = None,
222+
load: LoadSpec | None = None,
223+
execution_options: dict[str, Any] | None = None,
222224
) -> ModelT: ...
223225

224226
def update_many(
@@ -1110,7 +1112,6 @@ def count(
11101112
"""
11111113
with wrap_sqlalchemy_exception():
11121114
statement = self.statement if statement is None else statement
1113-
fragment = self.get_id_attribute_value(self.model_type)
11141115
loader_options, loader_options_have_wildcard = self._get_loader_options(load)
11151116
statement = self._get_base_stmt(
11161117
statement=statement,
@@ -1120,7 +1121,7 @@ def count(
11201121
statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
11211122
statement = self._filter_select_by_kwargs(statement, kwargs)
11221123
statement = statement.add_criteria(
1123-
lambda s: s.with_only_columns(sql_func.count(fragment), maintain_column_froms=True).order_by(None),
1124+
lambda s: s.with_only_columns(sql_func.count(text("1")), maintain_column_froms=True).order_by(None),
11241125
)
11251126
results = self._execute(statement, uniquify=loader_options_have_wildcard)
11261127
return cast(int, results.scalar_one())
@@ -1134,6 +1135,8 @@ def update(
11341135
auto_expunge: bool | None = None,
11351136
auto_refresh: bool | None = None,
11361137
id_attribute: str | InstrumentedAttribute[Any] | None = None,
1138+
load: LoadSpec | None = None,
1139+
execution_options: dict[str, Any] | None = None,
11371140
) -> ModelT:
11381141
"""Update instance with the attribute values present on `data`.
11391142
@@ -1153,6 +1156,8 @@ def update(
11531156
:class:`SQLAlchemyAsyncRepository.auto_commit <SQLAlchemyAsyncRepository>`
11541157
id_attribute: Allows customization of the unique identifier to use for model fetching.
11551158
Defaults to `id`, but can reference any surrogate or candidate key for the table.
1159+
load: Set relationships to be loaded
1160+
execution_options: Set default execution options
11561161
11571162
Returns:
11581163
The updated instance.
@@ -1166,7 +1171,7 @@ def update(
11661171
id_attribute=id_attribute,
11671172
)
11681173
# this will raise for not found, and will put the item in the session
1169-
self.get(item_id, id_attribute=id_attribute)
1174+
self.get(item_id, id_attribute=id_attribute, load=load, execution_options=execution_options)
11701175
# this will merge the inbound data to the instance we just put in the session
11711176
instance = self._attach_to_session(data, strategy="merge")
11721177
self._flush_or_commit(auto_commit=auto_commit)

advanced_alchemy/repository/memory/_async.py

+2
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ async def update(
535535
auto_expunge: bool | None = None,
536536
auto_refresh: bool | None = None,
537537
id_attribute: str | InstrumentedAttribute[Any] | None = None,
538+
load: LoadSpec | None = None,
539+
execution_options: dict[str, Any] | None = None,
538540
) -> ModelT:
539541
self._find_or_raise_not_found(self.__collection__().key(data))
540542
return self.__collection__().update(data)

advanced_alchemy/repository/memory/_sync.py

+2
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,8 @@ def update(
536536
auto_expunge: bool | None = None,
537537
auto_refresh: bool | None = None,
538538
id_attribute: str | InstrumentedAttribute[Any] | None = None,
539+
load: LoadSpec | None = None,
540+
execution_options: dict[str, Any] | None = None,
539541
) -> ModelT:
540542
self._find_or_raise_not_found(self.__collection__().key(data))
541543
return self.__collection__().update(data)

advanced_alchemy/service/_async.py

+2
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,8 @@ async def update(
503503
auto_expunge=auto_expunge,
504504
auto_refresh=auto_refresh,
505505
id_attribute=id_attribute,
506+
load=load,
507+
execution_options=execution_options,
506508
)
507509

508510
async def update_many(

advanced_alchemy/service/_sync.py

+2
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,8 @@ def update(
504504
auto_expunge=auto_expunge,
505505
auto_refresh=auto_refresh,
506506
id_attribute=id_attribute,
507+
load=load,
508+
execution_options=execution_options,
507509
)
508510

509511
def update_many(

advanced_alchemy/service/_util.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
from advanced_alchemy.filters import LimitOffset
2323
from advanced_alchemy.repository.typing import ModelOrRowMappingT
2424
from advanced_alchemy.service.pagination import OffsetPagination
25-
from advanced_alchemy.service.typing import ( # type: ignore[attr-defined]
25+
from advanced_alchemy.service.typing import (
26+
MSGSPEC_INSTALLED,
27+
PYDANTIC_INSTALLED,
2628
BaseModel,
2729
ModelDTOT,
2830
Struct,
@@ -185,7 +187,7 @@ def to_schema(
185187
offset=limit_offset.offset,
186188
total=total,
187189
)
188-
if issubclass(schema_type, Struct):
190+
if MSGSPEC_INSTALLED and issubclass(schema_type, Struct):
189191
if not isinstance(data, Sequence):
190192
return cast(
191193
"ModelDTOT",
@@ -221,7 +223,7 @@ def to_schema(
221223
total=total,
222224
)
223225

224-
if issubclass(schema_type, BaseModel):
226+
if PYDANTIC_INSTALLED and issubclass(schema_type, BaseModel):
225227
if not isinstance(data, Sequence):
226228
return cast("ModelDTOT", TypeAdapter(schema_type).validate_python(data, from_attributes=True)) # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType,reportAttributeAccessIssue,reportCallIssue]
227229
limit_offset = find_filter(LimitOffset, filters=filters)

advanced_alchemy/service/typing.py

+24
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from typing import (
1010
Any,
11+
Final,
1112
Generic,
1213
Protocol,
1314
TypeVar,
@@ -21,6 +22,9 @@
2122

2223
try:
2324
from msgspec import Struct, convert # pyright: ignore[reportAssignmentType,reportUnusedImport]
25+
26+
MSGSPEC_INSTALLED: Final[bool] = True
27+
2428
except ImportError: # pragma: nocover
2529

2630
class Struct(Protocol): # type: ignore[no-redef] # pragma: nocover
@@ -30,10 +34,14 @@ def convert(*args: Any, **kwargs: Any) -> Any: # type: ignore[no-redef] # noqa:
3034
"""Placeholder implementation"""
3135
return {}
3236

37+
MSGSPEC_INSTALLED: Final[bool] = False # type: ignore # pyright: ignore[reportConstantRedefinition,reportGeneralTypeIssues] # noqa: PGH003
38+
3339

3440
try:
3541
from pydantic import BaseModel # pyright: ignore[reportAssignmentType]
3642
from pydantic.type_adapter import TypeAdapter # pyright: ignore[reportUnusedImport, reportAssignmentType]
43+
44+
PYDANTIC_INSTALLED: Final[bool] = True
3745
except ImportError: # pragma: nocover
3846

3947
class BaseModel(Protocol): # type: ignore[no-redef] # pragma: nocover
@@ -51,10 +59,26 @@ def validate_python(self, data: Any, *args: Any, **kwargs: Any) -> T: # pragma:
5159
"""Stub"""
5260
return cast("T", data)
5361

62+
PYDANTIC_INSTALLED: Final[bool] = False # type: ignore # pyright: ignore[reportConstantRedefinition,reportGeneralTypeIssues] # noqa: PGH003
5463

5564
ModelDictT: TypeAlias = "dict[str, Any] | ModelT"
5665
ModelDictListT: TypeAlias = "list[ModelT | dict[str, Any]] | list[dict[str, Any]]"
5766
FilterTypeT = TypeVar("FilterTypeT", bound="StatementFilter")
5867
ModelDTOT = TypeVar("ModelDTOT", bound="Struct | BaseModel")
5968
PydanticModelDTOT = TypeVar("PydanticModelDTOT", bound="BaseModel")
6069
StructModelDTOT = TypeVar("StructModelDTOT", bound="Struct")
70+
71+
__all__ = (
72+
"ModelDictT",
73+
"ModelDictListT",
74+
"FilterTypeT",
75+
"ModelDTOT",
76+
"PydanticModelDTOT",
77+
"StructModelDTOT",
78+
"PYDANTIC_INSTALLED",
79+
"MSGSPEC_INSTALLED",
80+
"BaseModel",
81+
"TypeAdapter",
82+
"Struct",
83+
"convert",
84+
)

tests/fixtures/bigint/services.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ class SlugBookSyncService(SQLAlchemySyncRepositoryService[BigIntSlugBook]):
237237
match_fields = ["title"]
238238

239239
def __init__(self, **repo_kwargs: Any) -> None:
240-
self.repository = SlugBookSyncRepository(**repo_kwargs)
240+
self.repository: SlugBookSyncRepository = self.repository_type(**repo_kwargs) # pyright: ignore
241241

242242
def to_model(self, data: BigIntSlugBook | dict[str, Any], operation: str | None = None) -> BigIntSlugBook:
243243
if isinstance(data, dict) and "slug" not in data and operation == "create":
@@ -254,7 +254,7 @@ class SlugBookAsyncMockService(SQLAlchemyAsyncRepositoryService[BigIntSlugBook])
254254
match_fields = ["title"]
255255

256256
def __init__(self, **repo_kwargs: Any) -> None:
257-
self.repository = SlugBookAsyncMockRepository(**repo_kwargs)
257+
self.repository: SlugBookAsyncMockRepository = self.repository_type(**repo_kwargs) # pyright: ignore
258258

259259
async def to_model(self, data: BigIntSlugBook | dict[str, Any], operation: str | None = None) -> BigIntSlugBook:
260260
if isinstance(data, dict) and "slug" not in data and operation == "create":

0 commit comments

Comments
 (0)