Skip to content

Commit

Permalink
v4.0.0: remove loads strings, fix fastapi extention, fix search_by an…
Browse files Browse the repository at this point in the history
…d order_by interface.
  • Loading branch information
ALittleMoron committed Jul 9, 2024
1 parent ccc5353 commit 02b4959
Show file tree
Hide file tree
Showing 13 changed files with 545 additions and 481 deletions.
37 changes: 26 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,19 @@ class YourModelAsyncRepository(BaseAsyncRepository[YourModel]):
## Configuration

sqlrepo Repository classes provide many options, which you can configure to make repositories
work like you need:
work like you need. To configure your repository class, You can use `RepositoryConfig` and init
it in class body like this:

```python
from sqlrepo import BaseSyncRepository, RepositoryConfig

from your_package.models import YourModel

class YourModelSyncRepository(BaseSyncRepository[YourModel]):
config = RepositoryConfig(...)
```

Config params are the following:

### `model_class`

Expand Down Expand Up @@ -126,7 +138,12 @@ strings.
from my_package.models import Admin

class AdminRepository(BaseSyncRepository[Admin]):
specific_column_mapping = {"custom_field": Admin.id, "other_field": Admin.name}
config =RepositoryConfig(
specific_column_mapping={
"custom_field": Admin.id,
"other_field": Admin.name,
}
)


admins = AdminRepository(session).list(
Expand Down Expand Up @@ -236,12 +253,6 @@ List of operators: `=, >, <, >=, <=, is, is_not, between, contains`.
* `django-like` - `key-value` dict with django-like lookups system. See django docs for
more info.

### `load_strategy`

Uses as choice of SQLAlchemy load strategies.

By default selectinload, because it makes less errors.

## Unit of work

sqlrepo provides unit of work base implementation to work with all your repositories in one place
Expand Down Expand Up @@ -313,6 +324,10 @@ supported.
FastAPI extensions implements base classes for services and container, so you can work with your
code easier.

Attention! Container is good solution, if you want to simplify your work with services and
repositories, but it cause situation, when you can access any services in any routes. It's not
safe, so be careful.

First of all You need to prepare all to work with plugin:

```python
Expand Down Expand Up @@ -349,8 +364,8 @@ then you should use plugin like this:
```python
# your prepared code below

from sqlrepo.ext.fastapi import add_container_overrides
add_container_overrides(app, get_session)
from sqlrepo.ext.fastapi import add_session_stub_overrides
add_session_stub_overrides(app, get_session)
```

then you can implements containers and services like this:
Expand Down Expand Up @@ -394,7 +409,7 @@ class Container(BaseSyncContainer):

@cached_property
def your_model_service(self):
return YourModelService(self.session, self.request)
return YourModelService(self.request, self.session)
```

and finally you can use Container in your routes like this:
Expand Down
550 changes: 274 additions & 276 deletions pdm.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,15 @@ dev = [

[project]
name = "sqlrepo"
version = "3.0.0"
version = "4.0.0"
description = "sqlalchemy repositories with crud operations and other utils for it."
authors = [{ name = "Dmitriy Lunev", email = "dima.lunev14@gmail.com" }]
requires-python = ">=3.11"
readme = "README.md"
license = { text = "MIT" }
dependencies = [
"sqlalchemy>=2.0.29",
"python-dev-utils[sqlalchemy_filters]>=2.2.0",
"python-dev-utils[sqlalchemy_filters]>=2.3.0",
]

[project.optional-dependencies]
Expand Down
12 changes: 1 addition & 11 deletions sqlrepo/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import datetime
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias

Expand All @@ -10,22 +9,20 @@
SimpleFilterConverter,
)
from dev_utils.sqlalchemy.filters.types import FilterConverterStrategiesLiteral
from sqlalchemy.orm import selectinload

StrField: TypeAlias = str


if TYPE_CHECKING:
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.strategy_options import _AbstractLoad # type: ignore


filter_convert_classes: Final[dict[FilterConverterStrategiesLiteral, type[BaseFilterConverter]]] = {
"simple": SimpleFilterConverter,
"advanced": AdvancedOperatorFilterConverter,
"django": DjangoLikeFilterConverter,
}
"""Final convert class filters mapping."""
"""Convert class filters mapping."""


@dataclass(slots=True)
Expand Down Expand Up @@ -140,13 +137,6 @@ class RepositoryConfig:
``django-like`` - ``key-value`` dict with django-like lookups system. See django docs for
more info.
"""
# FIXME: remove it. Will cause many errors. Just pass _AbstractLoad instances itself. Not str
default_load_strategy: Callable[..., "_AbstractLoad"] = field(default=selectinload)
"""
Uses as choice of SQLAlchemy load strategies.
By default selectinload, because it makes less errors.
"""

def get_filter_convert_class(self) -> type[BaseFilterConverter]:
"""Get filter convert class from passed strategy."""
Expand Down
2 changes: 1 addition & 1 deletion sqlrepo/ext/fastapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .containers import BaseAsyncContainer as BaseAsyncContainer
from .containers import BaseSyncContainer as BaseSyncContainer
from .containers import add_container_overrides as add_container_overrides
from .services import BaseAsyncService as BaseAsyncService
from .services import BaseService as BaseService
from .services import BaseSyncService as BaseSyncService
from .stubs import add_session_stub_overrides as add_session_stub_overrides
54 changes: 3 additions & 51 deletions sqlrepo/ext/fastapi/containers.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,13 @@
from typing import TYPE_CHECKING, Protocol, TypeAlias
from typing import TYPE_CHECKING

from fastapi import Depends, Request

if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Generator
from sqlrepo.ext.fastapi.stubs import _get_session_stub # type: ignore

from fastapi import FastAPI
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.session import Session

class SyncSessionGeneratorDependsProtocol(Protocol):
"""Sync session depends protocol for FastAPI framework."""

@staticmethod
def __call__() -> Generator[Session, None, None]: ... # noqa: D102

class SyncSessionDependsProtocol(Protocol):
"""Sync session depends protocol for FastAPI framework."""

@staticmethod
def __call__() -> Session: ... # noqa: D102

class AsyncSessionGeneratorDependsProtocol(Protocol):
"""Async session depends protocol for FastAPI framework."""

@staticmethod
async def __call__() -> AsyncGenerator[AsyncSession, None]: ... # noqa: D102

class AsyncSessionDependsProtocol(Protocol):
"""Async session depends protocol for FastAPI framework."""

@staticmethod
async def __call__() -> AsyncSession: ... # noqa: D102

SessionDepends: TypeAlias = (
SyncSessionDependsProtocol
| AsyncSessionDependsProtocol
| SyncSessionGeneratorDependsProtocol
| AsyncSessionGeneratorDependsProtocol
)


def _get_session_stub() -> None:
"""Stub function, that will be overridden by main plug functions."""


def add_container_overrides(
app: "FastAPI",
session_depends: "SessionDepends",
) -> "FastAPI":
"""Container plugin function.
Add dependency override for user-defined SQLAlchemy session (sync or async) and return app back.
"""
app.dependency_overrides[_get_session_stub] = session_depends
return app


class BaseSyncContainer:
"""Base container class with sync interface."""
Expand Down
16 changes: 12 additions & 4 deletions sqlrepo/ext/fastapi/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
from typing import TYPE_CHECKING, Any, ForwardRef, Generic, TypeVar, get_args

from dev_utils.verbose_http_exceptions import BaseVerboseHTTPException
from fastapi import HTTPException, status
from fastapi import Depends, HTTPException, Request, status
from pydantic import BaseModel, TypeAdapter
from sqlalchemy.orm.decl_api import DeclarativeBase

from sqlrepo.ext.fastapi.helpers import NotSet, NotSetType
from sqlrepo.ext.fastapi.pagination import PaginatedResult, PaginationMeta
from sqlrepo.ext.fastapi.stubs import _get_session_stub # type: ignore
from sqlrepo.logging import logger

if TYPE_CHECKING:
from collections.abc import Sequence

from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.session import Session

Expand Down Expand Up @@ -179,7 +179,11 @@ def init_repositories(self, session: "AsyncSession") -> None:
"""
raise NotImplementedError()

def __init__(self, session: "AsyncSession", request: "Request") -> None: # pragma: no coverage
def __init__(
self,
request: "Request",
session: "AsyncSession" = Depends(_get_session_stub),
) -> None: # pragma: no coverage
self.session = session
self.request = request
self.init_repositories(session)
Expand All @@ -197,7 +201,11 @@ def init_repositories(self, session: "Session") -> None:
"""
raise NotImplementedError()

def __init__(self, session: "Session", request: "Request") -> None: # pragma: no coverage
def __init__(
self,
request: Request,
session: "Session" = Depends(_get_session_stub),
) -> None: # pragma: no coverage
self.session = session
self.request = request
self.init_repositories(session)
55 changes: 55 additions & 0 deletions sqlrepo/ext/fastapi/stubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from typing import TYPE_CHECKING, Protocol, TypeAlias

if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Generator

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.session import Session

class SyncSessionGeneratorDependsProtocol(Protocol):
"""Sync session depends protocol for FastAPI framework."""

@staticmethod
def __call__() -> Generator[Session, None, None]: ... # noqa: D102

class SyncSessionDependsProtocol(Protocol):
"""Sync session depends protocol for FastAPI framework."""

@staticmethod
def __call__() -> Session: ... # noqa: D102

class AsyncSessionGeneratorDependsProtocol(Protocol):
"""Async session depends protocol for FastAPI framework."""

@staticmethod
async def __call__() -> AsyncGenerator[AsyncSession, None]: ... # noqa: D102

class AsyncSessionDependsProtocol(Protocol):
"""Async session depends protocol for FastAPI framework."""

@staticmethod
async def __call__() -> AsyncSession: ... # noqa: D102

SessionDepends: TypeAlias = (
SyncSessionDependsProtocol
| AsyncSessionDependsProtocol
| SyncSessionGeneratorDependsProtocol
| AsyncSessionGeneratorDependsProtocol
)


def _get_session_stub() -> None:
"""Stub function, that will be overridden by main plug functions."""


def add_session_stub_overrides(
app: "FastAPI",
session_depends: "SessionDepends",
) -> "FastAPI":
"""Container plugin function.
Add dependency override for user-defined SQLAlchemy session (sync or async) and return app back.
"""
app.dependency_overrides[_get_session_stub] = session_depends
return app
1 change: 1 addition & 0 deletions sqlrepo/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@


logging.config.dictConfig(LOGGER_CONFIG)
# TODO: replace to structlog
logger = logging.getLogger("sqlrepo")
Loading

0 comments on commit 02b4959

Please sign in to comment.