Skip to content

Commit e336aa9

Browse files
Revert "feat: Добавил обертки для получения сесии и записии в контекст. Убрал временно mypy из pre-commit-а."
This reverts commit fa0f33e.
1 parent fa0f33e commit e336aa9

File tree

11 files changed

+111
-114
lines changed

11 files changed

+111
-114
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ repos:
2525
language: system
2626
types: [python]
2727
args: [--ini, .bandit]
28-
# - id: mypy
29-
# name: "mypy"
30-
# entry: mypy
31-
# args: ["--config-file", "pyproject.toml"]
32-
# types: [python]
33-
# language: system
34-
# exclude: tests
28+
- id: mypy
29+
name: "mypy"
30+
entry: mypy
31+
args: ["--config-file", "pyproject.toml"]
32+
types: [python]
33+
language: system
34+
exclude: tests
3535

3636
- repo: https://github.com/pre-commit/pre-commit-hooks
3737
rev: v4.6.0

README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Вспомогательная библиотека для разработки приложений на FastAPI.
66

7-
Реализует функционал для наиболее распространных ситуаций (создание экзмепляра приложения FastAPI по заданным настройкам,
7+
Реализует функционал для наиболее распространных ситуаций (создание экзмепляра приложения FastAPI по заданным настройкам,
88
работа с БД, фильтрация, пагинация, авторизация, аутентификация).
99

1010
- [Названия библиотеки](#названия-библиотеки)
@@ -33,7 +33,7 @@ fastapi-django - рабочее название (не финальное).
3333
## TODO
3434

3535
1. множественные БД (в одну БД (мастер) пишется, в другую синхронизируется и из нее читается.)
36-
2. генерация шаблона проекта как в django (также генерируется файл manage.py, в котором дополняются переменные окружения
36+
2. генерация шаблона проекта как в django (также генерируется файл manage.py, в котором дополняются переменные окружения
3737
и который является входной точкой в приложение)
3838
2. прикинуть, какие еще консольные команды могут пригодиться (напр., миграции)
3939
3. репозитории
@@ -52,7 +52,7 @@ fastapi-django - рабочее название (не финальное).
5252

5353
## Создание приложения
5454

55-
Библиотека предоставляет функцию `fastapi_django.app.get_default_app()`, которая создает экземпляр приложения FastAPI
55+
Библиотека предоставляет функцию `fastapi_django.app.get_default_app()`, которая создает экземпляр приложения FastAPI
5656
с параметрами, указанными в настройках в settings.py:
5757

5858
```python
@@ -92,13 +92,13 @@ UVICORN_APP = "web.app:app"
9292
python manage.py runserver
9393
```
9494

95-
Это запустит экземпляр указанного в UVICORN_APP приложения при помощи uvicorn.
95+
Это запустит экземпляр указанного в UVICORN_APP приложения при помощи uvicorn.
9696

9797
## Работа с БД
9898

9999
### Сессии SQLAlchemy
100100

101-
Работа с базами данных происходит через слой репозиториев. Для этого разработан [базовый класс репозитория BaseRepository](fastapi_django/db/repositories/base.py#L13),
101+
Работа с базами данных происходит через слой репозиториев. Для этого разработан [базовый класс репозитория BaseRepository](fastapi_django/db/repositories/base.py#L13),
102102
который предоставляет возможность работать с данными в стиле Django ORM:
103103

104104
```python
@@ -115,9 +115,9 @@ async with contextified_autocommit_session():
115115
users = await repository.objects.filter(name="Иван").all()
116116
```
117117

118-
Обратите внимание, что сессия SQLAlchemy не передается при инициализации репозитория. Вместо этого она инициализируется
119-
контекстным менеджером contextified_autocommit_session() и помещается в ContextVars. Репозитории (все в пределах действия
120-
контекстного менеджера) затем берут инициализированную сессию оттуда. contextified_autocommit_session() также управляет
118+
Обратите внимание, что сессия SQLAlchemy не передается при инициализации репозитория. Вместо этого она инициализируется
119+
контекстным менеджером contextified_autocommit_session() и помещается в ContextVars. Репозитории (все в пределах действия
120+
контекстного менеджера) затем берут инициализированную сессию оттуда. contextified_autocommit_session() также управляет
121121
жизненным циклом сессии.
122122

123123
Настройки базы данных задаются в settings(.py) в DATABASE. Пример:
@@ -148,10 +148,10 @@ DATABASE = {
148148

149149
### fastapi-sqla
150150

151-
Движки, сессии конфигурируются библиотекой. Параметры через энвы, для наименования которых необходимо придерживаться
152-
некоторых правил.
151+
Движки, сессии конфигурируются библиотекой. Параметры через энвы, для наименования которых необходимо придерживаться
152+
некоторых правил.
153153

154-
Сессия создается в миддлварях и [записывается в fastapi.Request.state](https://github.com/dialoguemd/fastapi-sqla/blob/master/fastapi_sqla/async_sqla.py#L137).
154+
Сессия создается в миддлварях и [записывается в fastapi.Request.state](https://github.com/dialoguemd/fastapi-sqla/blob/master/fastapi_sqla/async_sqla.py#L137).
155155
Затем эту сессию депенденси AsyncSessionDependency возвращает.
156156

157157
Сессия не хранится в contextvars.
@@ -324,3 +324,4 @@ def get_users():
324324
Паттерн active record.
325325

326326
Не поддерживается.
327+

fastapi_django/db/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi_django.conf import settings
55
from fastapi_django.exceptions import ImproperlyConfigured
66

7+
78
# поддерживаемые диалекты SQLAlchemy https://docs.sqlalchemy.org/en/20/dialects/index.html
89
# SQLAlchemy как безоговорочный вариант для взаимодействия с БД
910

@@ -26,9 +27,7 @@ def __init__(self):
2627
database=settings.DATABASE["DATABASE"],
2728
)
2829
kw = settings.DATABASE.get("OPTIONS", {})
29-
self.__dict__["_engine"] = create_async_engine(
30-
url, **kw
31-
) # иначе будет RecursionError: maximum recursion depth exceeded
30+
self.__dict__["_engine"] = create_async_engine(url, **kw) # иначе будет RecursionError: maximum recursion depth exceeded
3231

3332
def __getattr__(self, item):
3433
return getattr(self._engine, item)

fastapi_django/db/dependencies.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
1-
from collections.abc import AsyncIterator, Callable
2-
from typing import Annotated, Any
1+
from typing import Any, AsyncIterator, Callable, Annotated
32

43
from fastapi import Depends
54
from sqlalchemy.ext.asyncio import AsyncSession
65

7-
from fastapi_django.db.sessions import contextified_autocommit_session, contextified_transactional_session
6+
from fastapi_django.db.sessions import contextified_transactional_session, contextified_autocommit_session
87

98

109
def contextify_transactional_session(**kw: Any) -> Callable:
1110
async def wrapper() -> AsyncIterator[AsyncSession]:
1211
async with contextified_transactional_session(**kw) as session:
1312
yield session
14-
1513
return wrapper
1614

1715

1816
def contextify_autocommit_session(**kw: Any) -> Callable:
1917
async def wrapper() -> AsyncIterator[AsyncSession]:
2018
async with contextified_autocommit_session(**kw) as session:
2119
yield session
22-
2320
return wrapper
2421

2522

fastapi_django/db/exceptions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
class ColumnNotFoundError(Exception):
22
def __init__(self, model, column_name: str):
3-
error = f"Столбец `{column_name}` не найден в модели {model.__name__}"
3+
error = (
4+
f"Столбец `{column_name}` не найден в модели {model.__name__}"
5+
)
46
super().__init__(error)

fastapi_django/db/repositories/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from itertools import islice
3-
from typing import Any, Generic, Self
3+
from typing import Generic, Any, Type, Self
44

55
from fastapi_django.db.repositories.queryset import QuerySet
66
from fastapi_django.db.sessions import session_context_var
@@ -11,7 +11,7 @@
1111

1212

1313
class BaseRepository(Generic[Model]):
14-
model_cls: type[Model]
14+
model_cls: Type[Model]
1515

1616
def __init__(self):
1717
if not self.model_cls:

fastapi_django/db/repositories/builder.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,49 @@
11
import logging
2-
from typing import Any, Self
2+
from typing import Any, Type, Self
33

4-
from sqlalchemy import Delete, Select, Update, delete, func, select, update
5-
from sqlalchemy.orm import aliased, contains_eager
4+
from sqlalchemy import Select, select, func, delete, Delete, update, Update
5+
from sqlalchemy.orm import contains_eager, aliased
66
from sqlalchemy.sql.operators import eq
77

88
from fastapi_django.db.repositories.constants import LOOKUP_SEP
99
from fastapi_django.db.repositories.lookups import lookups
1010
from fastapi_django.db.types import Model
11-
from fastapi_django.db.utils import get_annotations, get_column, get_columns, get_pk, get_relationships
11+
from fastapi_django.db.utils import get_column, get_pk, get_relationships, get_columns, get_annotations
1212

1313
logger = logging.getLogger(__name__)
1414

1515

1616
class InvalidFilterFieldError(Exception):
17+
1718
def __init__(self, filter_field: str):
1819
error = f"Некорректное поле для фильтрации - {filter_field}"
1920
super().__init__(error)
2021

2122

2223
class InvalidOrderByFieldError(Exception):
24+
2325
def __init__(self, ordering_field: str):
2426
error = f"Некорректное поле для сортировки - {ordering_field}"
2527
super().__init__(error)
2628

2729

2830
class InvalidOptionFieldError(Exception):
31+
2932
def __init__(self, option_field: str):
3033
error = f"Некорректное поле для options - {option_field}"
3134
super().__init__(error)
3235

3336

3437
class InvalidJoinFieldError(Exception):
38+
3539
def __init__(self, join_field: str):
3640
error = f"Некорректное поле для join - {join_field}"
3741
super().__init__(error)
3842

3943

4044
class QueryBuilder:
41-
"""Обертка над запросом SQLAlchemy.
42-
43-
Хранит параметры запроса. Предоставляет методы для
45+
"""
46+
Обертка над запросом SQLAlchemy. Хранит параметры запроса. Предоставляет методы для
4447
создания конечных методов
4548
4649
Собирает параметры запроса и в конце генерирует запрос
@@ -131,7 +134,7 @@ class QueryBuilder:
131134
132135
"""
133136

134-
def __init__(self, model_cls: type[Model]):
137+
def __init__(self, model_cls: Type[Model]):
135138
self._model_cls = model_cls
136139
self._where: dict = {}
137140
self._order_by: dict = {}
@@ -223,7 +226,9 @@ def order_by(self, *args: str) -> None:
223226
raise InvalidOrderByFieldError(ordering_field)
224227
if column_name is None:
225228
raise InvalidOrderByFieldError(ordering_field)
226-
order_by[column_name] = {"direction": "desc" if ordering_field.startswith("-") else "asc"}
229+
order_by[column_name] = {
230+
"direction": "desc" if ordering_field.startswith("-") else "asc"
231+
}
227232

228233
def options(self, *args: str) -> None:
229234
for option_field in args:
@@ -234,7 +239,7 @@ def options(self, *args: str) -> None:
234239
if attr in relationships:
235240
model_cls = getattr(model_cls, attr).property.mapper.class_
236241
joins = joins.setdefault("children", {}).setdefault(attr, {})
237-
joins["model_cls"] = model_cls
242+
joins['model_cls'] = model_cls
238243
relationships = get_relationships(model_cls)
239244
else:
240245
raise InvalidOptionFieldError(option_field)
@@ -272,7 +277,7 @@ def join(self, *args: str, isouter: bool) -> None:
272277
if attr in relationships:
273278
model_cls = getattr(model_cls, attr).property.mapper.class_
274279
joins = joins.setdefault("children", {}).setdefault(attr, {})
275-
joins["model_cls"] = model_cls
280+
joins['model_cls'] = model_cls
276281
relationships = get_relationships(model_cls)
277282
else:
278283
raise InvalidJoinFieldError(join_field)
@@ -295,7 +300,10 @@ def build_count_stmt(self) -> Select:
295300
if self._options:
296301
raise ValueError("Удалите options")
297302
pk = get_pk(self._model_cls)
298-
stmt = select(func.count(func.distinct(pk))).select_from(self._model_cls)
303+
stmt = (
304+
select(func.count(func.distinct(pk)))
305+
.select_from(self._model_cls)
306+
)
299307
stmt = self._apply_joins(stmt)
300308
stmt = self._apply_where(stmt)
301309
return stmt
@@ -335,7 +343,8 @@ def _apply_distinct(self, stmt: Select) -> Select:
335343
return stmt
336344

337345
def build_select_stmt(self) -> Select:
338-
"""Возвращает запрос на выборку.
346+
"""
347+
Возвращает запрос на выборку
339348
340349
Лимитированные запросы с options приходится составлять при помощи подзапроса, чтобы
341350
гарантировать правильность применений OFFSET и LIMIT, так как связные модели join-ятся
@@ -415,17 +424,17 @@ def _apply_limit(self, stmt: Select) -> Select:
415424
def _apply_where(self, stmt, model_cls=None) -> Select:
416425
model_cls = model_cls or self._model_cls
417426
for attr, value in self._where.items():
418-
op = value["op"]
427+
op = value['op']
419428
column = getattr(model_cls, attr)
420-
stmt = stmt.where(op(column, value["value"]))
429+
stmt = stmt.where(op(column, value['value']))
421430
return stmt
422431

423432
def _apply_order_by(self, stmt: Select, model_cls=None):
424433
model_cls = model_cls or self._model_cls
425434
for attr, value in self._order_by.items():
426-
direction = value["direction"]
435+
direction = value['direction']
427436
column = getattr(model_cls, attr) # напр., aliased(Section).name или Section.name
428-
column = column.asc() if direction == "asc" else column.desc()
437+
column = column.asc() if direction == 'asc' else column.desc()
429438
stmt = stmt.order_by(column)
430439
return stmt
431440

@@ -435,10 +444,9 @@ def _apply_joins(
435444
apply_where: bool = True,
436445
apply_order_by: bool = True,
437446
apply_options: bool = True,
438-
parent_model_cls=None,
447+
parent_model_cls=None
439448
) -> Select:
440-
"""Метод для работы с join-ами.
441-
449+
"""
442450
как сейчас:
443451
444452
{
@@ -508,7 +516,7 @@ def _apply_joins_recursively(self, stmt, joins, where, order_by, parent_model_cl
508516
for attr, value in joins.get("children", {}).items():
509517
target = aliased(value["model_cls"])
510518
onclause = getattr(parent_model_cls, attr)
511-
attr_root = f"{root}{LOOKUP_SEP}{attr}".removesuffix(LOOKUP_SEP).removeprefix(LOOKUP_SEP)
519+
attr_root = f"{root}__{attr}".strip("__")
512520
tree[attr_root] = {"attr": onclause, "alias": target}
513521
isouter = value.get("isouter", False)
514522
stmt = stmt.join(target, onclause, isouter=isouter)
@@ -519,9 +527,15 @@ def _apply_joins_recursively(self, stmt, joins, where, order_by, parent_model_cl
519527
for name, item in value.get("order_by", {}).items():
520528
direction = item["direction"]
521529
column = getattr(target, name)
522-
order_by.append(column.asc() if direction == "asc" else column.desc())
530+
order_by.append(column.asc() if direction == 'asc' else column.desc())
523531
stmt = self._apply_joins_recursively(
524-
stmt, joins=value, order_by=order_by, where=where, parent_model_cls=target, tree=tree, root=attr_root
532+
stmt,
533+
joins=value,
534+
order_by=order_by,
535+
where=where,
536+
parent_model_cls=target,
537+
tree=tree,
538+
root=attr_root
525539
)
526540
return stmt
527541

0 commit comments

Comments
 (0)