Skip to content

Commit 090c112

Browse files
committed
test commit
1 parent a2d0d6c commit 090c112

11 files changed

Lines changed: 348 additions & 45 deletions

File tree

.pre-commit-config.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
repos:
2-
- repo: https://github.com/psf/black
3-
rev: 24.1.0
4-
hooks:
5-
- id: black
6-
72
- repo: https://github.com/pre-commit/mirrors-isort
83
rev: v5.10.1
94
hooks:
105
- id: isort
6+
7+
- repo: https://github.com/psf/black
8+
rev: 24.1.0
9+
hooks:
10+
- id: black
1111

1212
- repo: https://github.com/charliermarsh/ruff-pre-commit
1313
rev: v0.14.7

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,26 @@ dependencies = [
2020
"psycopg-binary>=3.2.13",
2121
"pydantic-settings>=2.12.0",
2222
"pydantic[email]>=2.12.5",
23+
"pytest>=9.0.1",
24+
"pytest-asyncio>=1.3.0",
25+
"pytest-cov>=7.0.0",
2326
"python-dotenv>=1.2.1",
2427
"python-jose[cryptography]>=3.5.0",
2528
"ruff>=0.14.7",
29+
"types-python-jose>=3.5.0.20250531",
2630
"uvicorn[standard]>=0.38.0",
2731
]
32+
33+
[tool.isort]
34+
profile = "black"
35+
line_length = 88 # тот же, что в black
36+
multi_line_output = 3
37+
include_trailing_comma = true
38+
39+
[tool.black]
40+
line-length = 88
41+
target-version = ['py312']
42+
skip-string-normalization = true
43+
44+
[tool.mypy]
45+
ignore_missing_imports = true

src/core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class BaseAppSettings(BaseSettings):
1818
DEBUG: bool = True
1919
RELOAD: bool = True
2020

21-
SECRET_KEY: str
21+
SECRET_KEY: str = "SECRET_KEY"
2222
LOG_LEVEL: str = "INFO"
2323

2424
CORS_ORIGINS: list[str] = ["*"]

src/core/exceptions.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,22 @@ def __init__(
2424
super().__init__(self.detail)
2525

2626

27-
def api_exception_handler(request: Request, exc: APIException) -> JSONResponse:
28-
content = {
29-
"code": exc.code,
30-
"detail": exc.detail,
31-
"values": exc.values,
32-
}
33-
return JSONResponse(status_code=exc.status_code, content=content)
27+
async def api_exception_handler(request: Request, exc: Exception) -> JSONResponse:
28+
"""
29+
Обработчик для FastAPI, принимает Exception (требование Starlette),
30+
проверяет на APIException и возвращает JSONResponse.
31+
"""
32+
if isinstance(exc, APIException):
33+
content = {
34+
"code": exc.code,
35+
"detail": exc.detail,
36+
"values": exc.values,
37+
}
38+
return JSONResponse(status_code=exc.status_code, content=content)
39+
40+
# fallback для остальных исключений
41+
return JSONResponse(status_code=500, content={"code": "error", "detail": str(exc)})
3442

3543

3644
def register_exception_handlers(app: FastAPI) -> None:
37-
app.add_exception_handler(APIException, api_exception_handler)
45+
app.add_exception_handler(Exception, api_exception_handler)

src/db/crud/category.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Sequence
2+
13
from fastapi import Depends
24
from sqlalchemy import select
35
from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,7 +13,7 @@ class CategoryCRUD:
1113
def __init__(self, session: AsyncSession = Depends(get_db_session)):
1214
self.session = session
1315

14-
async def get_all(self) -> list[Category]:
16+
async def get_all(self) -> Sequence[Category]:
1517
stmt = select(Category)
1618
result = await self.session.execute(stmt)
1719
return result.scalars().all()

src/db/migrations/env.py

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,25 @@
22
from logging.config import fileConfig
33

44
from alembic import context
5-
from sqlalchemy.ext.asyncio.engine import create_async_engine
5+
from sqlalchemy.ext.asyncio import create_async_engine
66
from sqlalchemy.future import Connection
77

88
from core.config import settings
99
from db.meta import meta
1010
from db.models import load_all_models
1111

12-
# this is the Alembic Config object, which provides
13-
# access to the values within the .ini file in use.
1412
config = context.config
1513

1614
# Load models
1715
load_all_models()
1816

19-
# Interpret the config file for Python logging.
20-
# This line sets up loggers basically.
17+
# Logging
2118
if config.config_file_name is not None:
2219
fileConfig(config.config_file_name)
2320

24-
# add your model's MetaData object here
25-
# for 'autogenerate' support
26-
# from myapp import mymodel
27-
# target_metadata = mymodel.Base.metadata
21+
# Target metadata
2822
target_metadata = meta
2923

30-
# other values from the config, defined by the needs of env.py,
31-
# can be acquired:
32-
# my_important_option = config.get_main_option("my_important_option")
33-
# ... etc.
34-
3524

3625
def run_migrations_offline() -> None:
3726
context.configure(
@@ -46,11 +35,7 @@ def run_migrations_offline() -> None:
4635

4736

4837
def do_run_migrations(connection: Connection) -> None:
49-
"""
50-
Run actual sync migrations.
51-
52-
:param connection: connection to the database.
53-
"""
38+
"""Run sync migrations in the async context."""
5439
context.configure(connection=connection, target_metadata=target_metadata)
5540

5641
with context.begin_transaction():
@@ -65,8 +50,6 @@ async def run_migrations_online() -> None:
6550

6651

6752
if context.is_offline_mode():
68-
task = run_migrations_offline()
53+
run_migrations_offline()
6954
else:
70-
task = run_migrations_online()
71-
72-
asyncio.run(task)
55+
asyncio.run(run_migrations_online())

src/schemas/user.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional
22

3-
from pydantic import BaseModel, EmailStr, Field
3+
from pydantic import BaseModel, ConfigDict, EmailStr, Field
44

55
from db.models.users import UserRole
66

@@ -31,8 +31,7 @@ class UserOut(UserBaseScheme):
3131
is_active: bool = Field(..., description="Indicates if the user is active")
3232
role: UserRole = Field(..., description="The role assigned to the user")
3333

34-
class Config:
35-
from_attributes = True
34+
model_config = ConfigDict(from_attributes=True)
3635

3736

3837
class UserResponse(BaseModel):
@@ -53,17 +52,15 @@ class UserUpdate(UserBaseScheme):
5352
None, description="Indicates if the user is active"
5453
)
5554

56-
class Config:
57-
from_attributes = True
55+
model_config = ConfigDict(from_attributes=True)
5856

5957

6058
class UserInDB(UserBaseScheme):
6159
id: int = Field(..., description="The unique identifier of the user")
6260
hashed_password: str = Field(..., description="The hashed password of the user")
6361
is_active: bool = Field(..., description="Indicates if the user is active")
6462

65-
class Config:
66-
from_attributes = True
63+
model_config = ConfigDict(from_attributes=True)
6764

6865

6966
__all__ = (

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from collections.abc import AsyncGenerator
2+
from typing import Any
3+
4+
import pytest
5+
from fastapi import FastAPI
6+
from httpx import ASGITransport, AsyncClient
7+
from sqlalchemy.ext.asyncio import (
8+
AsyncEngine,
9+
AsyncSession,
10+
async_sessionmaker,
11+
create_async_engine,
12+
)
13+
14+
from src.app import create_app
15+
from src.core.config import get_app_settings
16+
from src.db.dependencies import get_db_session
17+
from src.db.meta import meta
18+
from src.db.models import load_all_models
19+
20+
21+
@pytest.fixture(scope="session")
22+
async def _engine() -> AsyncGenerator[AsyncEngine, None]:
23+
"""
24+
Create engine and databases.
25+
26+
:yield: new engine.
27+
"""
28+
settings = get_app_settings()
29+
30+
load_all_models()
31+
32+
engine = create_async_engine(str(settings.DATABASE_URL))
33+
async with engine.begin() as conn:
34+
await conn.run_sync(meta.create_all)
35+
36+
try:
37+
yield engine
38+
finally:
39+
await engine.dispose()
40+
41+
42+
@pytest.fixture
43+
async def dbsession(
44+
_engine: AsyncEngine,
45+
) -> AsyncGenerator[AsyncSession, None]:
46+
"""
47+
Get session to database.
48+
49+
Fixture that returns a SQLAlchemy session with a SAVEPOINT, and the rollback to it
50+
after the test completes.
51+
52+
:param _engine: current engine.
53+
:yields: async session.
54+
"""
55+
connection = await _engine.connect()
56+
trans = await connection.begin()
57+
58+
session_maker = async_sessionmaker(
59+
connection,
60+
expire_on_commit=False,
61+
)
62+
session = session_maker()
63+
64+
try:
65+
yield session
66+
finally:
67+
await session.close()
68+
await trans.rollback()
69+
await connection.close()
70+
71+
72+
@pytest.fixture
73+
async def fastapi_app(
74+
dbsession: AsyncSession,
75+
) -> FastAPI:
76+
"""
77+
Fixture for creating FastAPI app.
78+
79+
:return: fastapi app with mocked dependencies.
80+
"""
81+
application = create_app()
82+
application.dependency_overrides[get_db_session] = lambda: dbsession
83+
return application
84+
85+
86+
@pytest.fixture(scope="session")
87+
def anyio_backend() -> str:
88+
"""
89+
Backend for anyio pytest plugin.
90+
91+
:return: backend name.
92+
"""
93+
return "asyncio"
94+
95+
96+
@pytest.fixture
97+
async def client(
98+
fastapi_app: FastAPI,
99+
anyio_backend: Any,
100+
) -> AsyncGenerator[AsyncClient, None]:
101+
"""
102+
Fixture that creates client for requesting server.
103+
104+
:param fastapi_app: the application.
105+
:yield: client for the app.
106+
"""
107+
transport = ASGITransport(fastapi_app)
108+
async with AsyncClient(
109+
transport=transport,
110+
base_url="http://test",
111+
) as client:
112+
yield client

tests/users_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import pytest
2+
from httpx import AsyncClient
3+
4+
5+
@pytest.mark.anyio
6+
async def test_default(
7+
client: AsyncClient,
8+
) -> None:
9+
"""Tests users instance creation."""
10+
assert 201 == 201

0 commit comments

Comments
 (0)