3 Commits

Author SHA1 Message Date
fe1ccabdd8 Version 0.2.0 2026-01-26 23:25:26 +01:00
d3vyce
9e7473fbf5 fix: pytest import when not using register_fixtures (#5) 2026-01-26 19:31:02 +01:00
d3vyce
d9d7f60e8e feat: add get_obj_by_attr fixture helper function (#4)
* feat: add get_obj_by_attr fixture helper function

* tests: add fixture utils
2026-01-26 18:58:30 +01:00
6 changed files with 97 additions and 4 deletions

View File

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

View File

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

View File

@@ -5,13 +5,23 @@ from .fixtures import (
load_fixtures,
load_fixtures_by_context,
)
from .pytest_plugin import register_fixtures
from .utils import get_obj_by_attr
__all__ = [
"Context",
"FixtureRegistry",
"LoadStrategy",
"get_obj_by_attr",
"load_fixtures",
"load_fixtures_by_context",
"register_fixtures",
]
# We lazy-load register_fixtures to avoid needing pytest when using fixtures CLI
def __getattr__(name: str):
if name == "register_fixtures":
from .pytest_plugin import register_fixtures
return register_fixtures
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@@ -0,0 +1,26 @@
from collections.abc import Callable, Sequence
from typing import Any, TypeVar
from sqlalchemy.orm import DeclarativeBase
T = TypeVar("T", bound=DeclarativeBase)
def get_obj_by_attr(
fixtures: Callable[[], Sequence[T]], attr_name: str, value: Any
) -> T:
"""Get a SQLAlchemy model instance by matching an attribute value.
Args:
fixtures: A fixture function registered via ``@registry.register``
that returns a sequence of SQLAlchemy model instances.
attr_name: Name of the attribute to match against.
value: Value to match.
Returns:
The first model instance where the attribute matches the given value.
Raises:
StopIteration: If no matching object is found.
"""
return next(obj for obj in fixtures() if getattr(obj, attr_name) == value)

View File

@@ -0,0 +1,57 @@
"""Tests for fastapi_toolsets.fixtures.utils."""
import pytest
from fastapi_toolsets.fixtures import FixtureRegistry
from fastapi_toolsets.fixtures.utils import get_obj_by_attr
from .conftest import Role, User
registry = FixtureRegistry()
@registry.register
def roles() -> list[Role]:
return [
Role(id=1, name="admin"),
Role(id=2, name="user"),
Role(id=3, name="moderator"),
]
@registry.register(depends_on=["roles"])
def users() -> list[User]:
return [
User(id=1, username="alice", email="alice@example.com", role_id=1),
User(id=2, username="bob", email="bob@example.com", role_id=1),
]
class TestGetObjByAttr:
"""Tests for get_obj_by_attr."""
def test_get_by_id(self):
"""Get an object by its id attribute."""
role = get_obj_by_attr(roles, "id", 1)
assert role.name == "admin"
def test_get_user_by_username(self):
"""Get a user by username."""
user = get_obj_by_attr(users, "username", "bob")
assert user.id == 2
assert user.email == "bob@example.com"
def test_returns_first_match(self):
"""Returns the first matching object when multiple could match."""
user = get_obj_by_attr(users, "role_id", 1)
assert user.username == "alice"
def test_no_match_raises_stop_iteration(self):
"""Raises StopIteration when no object matches."""
with pytest.raises(StopIteration):
get_obj_by_attr(roles, "name", "nonexistent")
def test_no_match_on_wrong_value_type(self):
"""Raises StopIteration when value type doesn't match."""
with pytest.raises(StopIteration):
get_obj_by_attr(roles, "id", "1")

2
uv.lock generated
View File

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