Skip to content

Storage Abstraction

NeoZen uses protocol-based storage so any database backend can be used. The primary implementation is SQLAlchemy 2.0 with async support.

Repository Protocols

The repository abstraction is split into three levels:

ReadableRepositoryProtocol

Components that only need lookups depend on this protocol:

class ReadableRepositoryProtocol[Model](Protocol):
    async def get_one_by(self, **filters) -> Model:
        """Get one object. Raises DoesNotExist if not found."""

    async def get_many_by(
        self, *, offset: int = 0, limit: int | None = None, **filters
    ) -> list[Model]:
        """Get multiple objects with pagination."""

    async def count(self, **filters) -> int:
        """Count matching objects."""

    async def exists(self, **filters) -> bool:
        """Check existence."""

WritableRepositoryProtocol

Components that need mutations depend on this protocol:

class WritableRepositoryProtocol[Model](Protocol):
    async def create(self, **data) -> Model: ...
    async def update_obj(self, obj: Model, **data) -> Model: ...
    async def delete(self, obj: Model) -> None: ...

RepositoryProtocol

This protocol combines the two, and stays backwards compatible:

class RepositoryProtocol[Model](
    ReadableRepositoryProtocol[Model],
    WritableRepositoryProtocol[Model],
    Protocol,
):
    model_type: type[Model]

All three are @runtime_checkable, so you can use isinstance() checks at runtime.

Why Split?

The split addresses a common integration pain point: PasswordFactor only calls get_one_by and update_obj, but the old monolithic protocol required implementing 7 methods. Now consumers can implement just ReadableRepositoryProtocol or WritableRepositoryProtocol depending on what's needed.

SQLAlchemy Implementation

NeoZen ships with a full SQLAlchemy implementation using Advanced Alchemy's repository pattern:

from neozen.storage.sqlalchemy.repositories import (
    SubjectRepository,
    SessionRepository,
    RoleRepository,
)
from neozen.storage.sqlalchemy.base import Base

# Create tables
async with engine.begin() as conn:
    await conn.run_sync(Base.metadata.create_all)

# Use repositories
async with session_factory() as db_session:
    repo = SubjectRepository(session=db_session)
    subject = await repo.get_one_by(email="alice@example.com")

Available Repositories

Repository Model Table
SubjectRepository Subject subjects
SessionRepository Session sessions
RoleRepository Role roles
RoleAssignmentRepository RoleAssignment role_assignments
EmailVerificationTokenRepository EmailVerificationToken email_verification_tokens
PasswordResetTokenRepository PasswordResetToken password_reset_tokens
MagicLinkTokenRepository MagicLinkToken magic_link_tokens
LoginAttemptRepository LoginAttempt login_attempts
AuditLogRepository AuditLog audit_logs

Writing an Adapter

If you have your own ORM models, write an adapter:

from neozen.core.repository import DoesNotExist

class UserRepoAdapter:
    def __init__(self, my_repo):
        self._repo = my_repo

    async def get_one_by(self, **filters):
        result = await self._repo.find_one(**filters)
        if result is None:
            raise DoesNotExist()
        return result

    async def update_obj(self, obj, **data):
        for k, v in data.items():
            setattr(obj, k, v)
        await self._repo.save(obj)
        return obj

See Bring Your Own Model for a complete example.