4 Commits

7 changed files with 18 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "fastapi-toolsets" name = "fastapi-toolsets"
version = "0.7.0" version = "0.7.1"
description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL" description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"

View File

@@ -21,4 +21,4 @@ Example usage:
return Response(data={"user": user.username}, message="Success") return Response(data={"user": user.username}, message="Success")
""" """
__version__ = "0.7.0" __version__ = "0.7.1"

View File

@@ -1,5 +1,7 @@
"""Generic async CRUD operations for SQLAlchemy models.""" """Generic async CRUD operations for SQLAlchemy models."""
from __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, ClassVar, Generic, Literal, Self, TypeVar, cast, overload from typing import Any, ClassVar, Generic, Literal, Self, TypeVar, cast, overload

View File

@@ -29,9 +29,14 @@ def get_obj_by_attr(
The first model instance where the attribute matches the given value. The first model instance where the attribute matches the given value.
Raises: Raises:
StopIteration: If no matching object is found. StopIteration: If no matching object is found in the fixture group.
""" """
return next(obj for obj in fixtures() if getattr(obj, attr_name) == value) try:
return next(obj for obj in fixtures() if getattr(obj, attr_name) == value)
except StopIteration:
raise StopIteration(
f"No object with {attr_name}={value} found in fixture '{getattr(fixtures, '__name__', repr(fixtures))}'"
) from None
async def load_fixtures( async def load_fixtures(

View File

@@ -10,6 +10,7 @@ __all__ = [
"ErrorResponse", "ErrorResponse",
"Pagination", "Pagination",
"PaginatedResponse", "PaginatedResponse",
"PydanticBase",
"Response", "Response",
"ResponseStatus", "ResponseStatus",
] ]

View File

@@ -744,8 +744,11 @@ class TestGetObjByAttr:
assert user.username == "alice" assert user.username == "alice"
def test_no_match_raises_stop_iteration(self): def test_no_match_raises_stop_iteration(self):
"""Raises StopIteration when no object matches.""" """Raises StopIteration with contextual message when no object matches."""
with pytest.raises(StopIteration): with pytest.raises(
StopIteration,
match="No object with name=nonexistent found in fixture 'roles'",
):
get_obj_by_attr(self.roles, "name", "nonexistent") get_obj_by_attr(self.roles, "name", "nonexistent")
def test_no_match_on_wrong_value_type(self): def test_no_match_on_wrong_value_type(self):

2
uv.lock generated
View File

@@ -220,7 +220,7 @@ wheels = [
[[package]] [[package]]
name = "fastapi-toolsets" name = "fastapi-toolsets"
version = "0.7.0" version = "0.7.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "asyncpg" }, { name = "asyncpg" },