Compare commits
12 Commits
b9a9e4e094
...
2471c2981f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2471c2981f | ||
|
|
9c20b7690c | ||
|
|
6657f55545 | ||
|
|
45ebed40bc | ||
|
|
c8e1863ee6 | ||
|
|
c61db4bc9d | ||
|
|
93cf7b2d24 | ||
|
|
acb3eefcbe | ||
|
|
a6c00e20c3 | ||
|
|
0ab17f3a99 | ||
|
|
22ee887238 | ||
|
|
cdee74210e |
0
__init__.py
Normal file
0
__init__.py
Normal file
@@ -30,3 +30,64 @@ dependencies = [
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
# Exclude commonly ignored directories and files.
|
||||||
|
exclude = [
|
||||||
|
".bzr",
|
||||||
|
".direnv",
|
||||||
|
".eggs",
|
||||||
|
".git",
|
||||||
|
".git-rewrite",
|
||||||
|
".hg",
|
||||||
|
".ipynb_checkpoints",
|
||||||
|
".mypy_cache",
|
||||||
|
".nox",
|
||||||
|
".pants.d",
|
||||||
|
".pyenv",
|
||||||
|
".pytest_cache",
|
||||||
|
".pytype",
|
||||||
|
".ruff_cache",
|
||||||
|
".svn",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
".vscode",
|
||||||
|
"__pypackages__",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"site-packages",
|
||||||
|
"venv",
|
||||||
|
]
|
||||||
|
# Set the maximum line length for both linting and formatting.
|
||||||
|
line-length = 88
|
||||||
|
# Assume Python 3.9 for compatibility checks.
|
||||||
|
target-version = "py39"
|
||||||
|
# Enable preview features for early access to new rules and formatting changes.
|
||||||
|
preview = true
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# Select specific rule groups to enable.
|
||||||
|
# 'E' for pycodestyle, 'F' for Pyflakes, 'I' for isort, 'B' for flake8-bugbear.
|
||||||
|
select = [
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"I",
|
||||||
|
"B",
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
]
|
||||||
|
# Ignore specific rules within the selected groups.
|
||||||
|
ignore = [
|
||||||
|
"UP035",
|
||||||
|
"B903",
|
||||||
|
"B904",
|
||||||
|
"E501",
|
||||||
|
"B008",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
# Enable Ruff's formatter.
|
||||||
|
docstring-code-format = true
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
@@ -1,14 +1,14 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import HTTPException, Depends, Path
|
from fastapi import Depends, HTTPException, Path
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from jwt import InvalidTokenError
|
from jwt import InvalidTokenError
|
||||||
|
|
||||||
|
from src.api.dependacies.db_dep import sessionDep
|
||||||
from src.core.auth_manager import AuthManager
|
from src.core.auth_manager import AuthManager
|
||||||
from src.core.settings import settings
|
from src.core.settings import settings
|
||||||
from src.schemas.auth import TokenData
|
from src.schemas.auth import TokenData
|
||||||
from src.services.users import UserService
|
from src.services.users import UserService
|
||||||
from src.api.dependacies.db_dep import sessionDep
|
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login")
|
||||||
|
|
||||||
@@ -51,14 +51,6 @@ async def get_admin_user(db: sessionDep, current_user: ActiveUser):
|
|||||||
AdminUser = Annotated[TokenData, Depends(get_admin_user)]
|
AdminUser = Annotated[TokenData, Depends(get_admin_user)]
|
||||||
|
|
||||||
|
|
||||||
async def user_or_admin_path(db: sessionDep, id: int, current_user: ActiveUser):
|
|
||||||
if current_user.id == id:
|
|
||||||
return current_user
|
|
||||||
else:
|
|
||||||
admin = await get_admin_user(db, current_user)
|
|
||||||
return admin
|
|
||||||
|
|
||||||
|
|
||||||
async def user_or_admin(
|
async def user_or_admin(
|
||||||
db: sessionDep, current_user: ActiveUser, id: Annotated[int, Path()]
|
db: sessionDep, current_user: ActiveUser, id: Annotated[int, Path()]
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from src.api.v1.auth import router as auth_router
|
from src.api.v1.auth import router as auth_router
|
||||||
from src.api.v1.users import router as users_router
|
|
||||||
from src.api.v1.tasks import router as tasks_router
|
from src.api.v1.tasks import router as tasks_router
|
||||||
|
from src.api.v1.users import router as users_router
|
||||||
from src.core.settings import settings
|
from src.core.settings import settings
|
||||||
|
|
||||||
router = APIRouter(prefix=settings.api.v1.prefix)
|
router = APIRouter(prefix=settings.api.v1.prefix)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from fastapi import APIRouter, Depends
|
|||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
|
||||||
from src.api.dependacies.db_dep import sessionDep
|
from src.api.dependacies.db_dep import sessionDep
|
||||||
from src.schemas.users import UserRequestADD
|
|
||||||
from src.core.settings import settings
|
from src.core.settings import settings
|
||||||
|
from src.schemas.users import UserRequestADD
|
||||||
from src.services.auth import AuthService
|
from src.services.auth import AuthService
|
||||||
|
|
||||||
router = APIRouter(prefix=settings.api.v1.auth, tags=["Auth"])
|
router = APIRouter(prefix=settings.api.v1.auth, tags=["Auth"])
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from src.api.dependacies.db_dep import sessionDep
|
||||||
|
from src.api.dependacies.user_dep import ActiveUser
|
||||||
|
from src.models.tasks import TasksORM
|
||||||
|
|
||||||
router = APIRouter(prefix="/tasks", tags=["Tasks"])
|
router = APIRouter(prefix="/tasks", tags=["Tasks"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def get_tasks(): ...
|
async def get_tasks(db: sessionDep, user: ActiveUser):
|
||||||
|
query = select(TasksORM.id, TasksORM.description).where(TasksORM.user_id == user.id)
|
||||||
|
tasks = await db.session.execute(query)
|
||||||
|
result = tasks.scalars().all()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{task_id}")
|
@router.get("/{task_id}")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Body
|
||||||
|
|
||||||
from src.api.dependacies.user_dep import ActiveUser, AdminUser, CurrentOrAdmin
|
|
||||||
from src.api.dependacies.db_dep import sessionDep
|
from src.api.dependacies.db_dep import sessionDep
|
||||||
|
from src.api.dependacies.user_dep import ActiveUser, AdminUser, CurrentOrAdmin
|
||||||
from src.core.settings import settings
|
from src.core.settings import settings
|
||||||
|
from src.schemas.users import UserUpdate
|
||||||
from src.services.users import UserService
|
from src.services.users import UserService
|
||||||
|
|
||||||
router = APIRouter(prefix=settings.api.v1.users, tags=["Users"])
|
router = APIRouter(prefix=settings.api.v1.users, tags=["Users"])
|
||||||
@@ -23,3 +24,17 @@ async def get_all_users(db: sessionDep, _: AdminUser):
|
|||||||
async def get_user_by_id(db: sessionDep, id: int, _: CurrentOrAdmin):
|
async def get_user_by_id(db: sessionDep, id: int, _: CurrentOrAdmin):
|
||||||
user = await UserService(db).get_user_by_filter_or_raise(id=id)
|
user = await UserService(db).get_user_by_filter_or_raise(id=id)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{id}")
|
||||||
|
async def patch_user(
|
||||||
|
db: sessionDep, id: int, _: CurrentOrAdmin, user_update: UserUpdate = Body()
|
||||||
|
):
|
||||||
|
updated_user = await UserService(db).update_user(id=id, update_data=user_update)
|
||||||
|
return updated_user
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}")
|
||||||
|
async def delete_user(db: sessionDep, id: int, _: AdminUser):
|
||||||
|
await UserService(db).delete_user(id)
|
||||||
|
return {"message": "User deleted successfully"}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import timedelta, datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import TIMESTAMP, func
|
from sqlalchemy import TIMESTAMP, event, func
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
from src.core.settings import settings
|
from src.core.settings import settings
|
||||||
|
|
||||||
engine = create_async_engine(settings.db.url, echo=True)
|
engine = create_async_engine(settings.db.url, echo=True)
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(engine.sync_engine, "connect")
|
||||||
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
|
if "sqlite" in settings.db.url:
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
|
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from src.repository.tasks import TasksRepo
|
||||||
from src.repository.users import UsersRepo
|
from src.repository.users import UsersRepo
|
||||||
|
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ class DBManager:
|
|||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self.session = self.session_factory()
|
self.session = self.session_factory()
|
||||||
self.user = UsersRepo(self.session)
|
self.user = UsersRepo(self.session)
|
||||||
|
self.task = TasksRepo(self.session)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
|
from src.repository.tasks import TasksRepo
|
||||||
from src.repository.users import UsersRepo
|
from src.repository.users import UsersRepo
|
||||||
|
|
||||||
|
|
||||||
class IUOWDB(Protocol):
|
class IUOWDB(Protocol):
|
||||||
user: UsersRepo
|
user: UsersRepo
|
||||||
|
task: TasksRepo
|
||||||
|
|
||||||
async def __aenter__(self) -> "IUOWDB": ...
|
async def __aenter__(self) -> "IUOWDB": ...
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class AccessToken(BaseSettings):
|
|||||||
expire_minutes: int = 15
|
expire_minutes: int = 15
|
||||||
secret_key: str
|
secret_key: str
|
||||||
algorithm: str = "HS256"
|
algorithm: str = "HS256"
|
||||||
|
token_type: str = "bearer" # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
import uvicorn
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).parent.parent))
|
sys.path.append(str(Path(__file__).parent.parent))
|
||||||
from src.api import router
|
from src.api import router
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
|
||||||
from sqlalchemy import pool
|
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
from sqlalchemy import engine_from_config, event, pool
|
||||||
|
|
||||||
from src.core.database import Base
|
from src.core.database import Base
|
||||||
from src.models import * # noqa
|
from src.models import * # noqa
|
||||||
@@ -67,6 +65,14 @@ def run_migrations_online() -> None:
|
|||||||
poolclass=pool.NullPool,
|
poolclass=pool.NullPool,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Enable foreign keys for SQLite in migrations
|
||||||
|
@event.listens_for(connectable, "connect")
|
||||||
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
|
print("⚙️ Enabling PRAGMA foreign_keys=ON for Alembic")
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
context.configure(connection=connection, target_metadata=target_metadata)
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ Create Date: 2025-07-06 00:02:09.254907
|
|||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "a2fdd0ec4a96"
|
revision: str = "a2fdd0ec4a96"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""add_cascade_delete_to_tasks
|
||||||
|
|
||||||
|
Revision ID: 197b195208e8
|
||||||
|
Revises: a2fdd0ec4a96
|
||||||
|
Create Date: 2025-08-06 23:41:56.778423
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "197b195208e8"
|
||||||
|
down_revision: Union[str, None] = "a2fdd0ec4a96"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
"""Upgrade schema."""
|
||||||
|
op.execute("PRAGMA foreign_keys=ON")
|
||||||
|
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
connection = op.get_bind()
|
||||||
|
inspector = sa.inspect(connection)
|
||||||
|
|
||||||
|
foreign_keys = inspector.get_foreign_keys("tasks")
|
||||||
|
constraint_name = None
|
||||||
|
|
||||||
|
for fk in foreign_keys:
|
||||||
|
if "user_id" in fk["constrained_columns"]:
|
||||||
|
constraint_name = fk["name"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if constraint_name:
|
||||||
|
try: # noqa: SIM105
|
||||||
|
batch_op.drop_constraint(constraint_name, type_="foreignkey")
|
||||||
|
except: # noqa E722
|
||||||
|
pass
|
||||||
|
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
"fk_tasks_user_id_users", "users", ["user_id"], ["id"], ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
"""Downgrade schema."""
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
try: # noqa: SIM105
|
||||||
|
batch_op.drop_constraint("fk_tasks_user_id_users", type_="foreignkey")
|
||||||
|
except: # noqa E722
|
||||||
|
pass
|
||||||
|
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
"fk_tasks_user_id_users", "users", ["user_id"], ["id"]
|
||||||
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""fix_duplicate_foreign_keys
|
||||||
|
|
||||||
|
Revision ID: 4b0f3ea2fd26
|
||||||
|
Revises: 197b195208e8
|
||||||
|
Create Date: 2025-08-06 23:54:24.308488
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union # noqa: UP035
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "4b0f3ea2fd26"
|
||||||
|
down_revision: Union[str, None] = "197b195208e8"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
"""Upgrade schema."""
|
||||||
|
op.execute("PRAGMA foreign_keys=ON")
|
||||||
|
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
connection = op.get_bind()
|
||||||
|
inspector = sa.inspect(connection)
|
||||||
|
|
||||||
|
foreign_keys = inspector.get_foreign_keys("tasks")
|
||||||
|
|
||||||
|
for fk in foreign_keys:
|
||||||
|
if "user_id" in fk["constrained_columns"]:
|
||||||
|
try: # noqa: SIM105
|
||||||
|
batch_op.drop_constraint(fk["name"], type_="foreignkey")
|
||||||
|
except: # noqa E722
|
||||||
|
pass
|
||||||
|
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
"fk_tasks_user_id_users", "users", ["user_id"], ["id"], ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
"""Downgrade schema."""
|
||||||
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
|
try: # noqa: SIM105
|
||||||
|
batch_op.drop_constraint("fk_tasks_user_id_users", type_="foreignkey")
|
||||||
|
except: # noqa E722
|
||||||
|
pass
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
"fk_tasks_user_id_users", "users", ["user_id"], ["id"]
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from src.models.users import UsersORM
|
|
||||||
from src.models.tasks import TasksORM
|
from src.models.tasks import TasksORM
|
||||||
|
from src.models.users import UsersORM
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"UsersORM",
|
"UsersORM",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, Text, Date, Enum, String
|
from sqlalchemy import Date, Enum, ForeignKey, String, Text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from src.core.database import Base
|
from src.core.database import Base
|
||||||
@@ -16,7 +16,7 @@ priority_enum = Enum("low", "medium", "high", "critical", name="priority_enum")
|
|||||||
class TasksORM(Base):
|
class TasksORM(Base):
|
||||||
__tablename__ = "tasks"
|
__tablename__ = "tasks"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||||
title: Mapped[str] = mapped_column(String(100))
|
title: Mapped[str] = mapped_column(String(100))
|
||||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Optional, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from sqlalchemy import String, BigInteger, Integer, Boolean
|
from sqlalchemy import BigInteger, Boolean, Integer, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from src.core.database import Base
|
from src.core.database import Base
|
||||||
@@ -23,4 +23,6 @@ class UsersORM(Base):
|
|||||||
avatar_path: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
avatar_path: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||||
is_superuser: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
is_superuser: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
tasks: Mapped[list["TasksORM"]] = relationship(back_populates="user")
|
tasks: Mapped[list["TasksORM"]] = relationship(
|
||||||
|
back_populates="user", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|||||||
6
src/repository/tasks.py
Normal file
6
src/repository/tasks.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from src.models.tasks import TasksORM
|
||||||
|
from src.repository.base import BaseRepo
|
||||||
|
|
||||||
|
|
||||||
|
class TasksRepo(BaseRepo):
|
||||||
|
model = TasksORM
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import delete, select, update
|
||||||
|
|
||||||
from src.models import UsersORM
|
from src.models import UsersORM
|
||||||
from src.repository.base import BaseRepo
|
from src.repository.base import BaseRepo
|
||||||
@@ -7,8 +7,22 @@ from src.repository.base import BaseRepo
|
|||||||
class UsersRepo(BaseRepo):
|
class UsersRepo(BaseRepo):
|
||||||
model = UsersORM
|
model = UsersORM
|
||||||
|
|
||||||
async def get_all_users(self):
|
async def get_all_users(self) -> list[UsersORM]:
|
||||||
query = select(self.model)
|
query = select(self.model)
|
||||||
result = await self.session.execute(query)
|
result = await self.session.execute(query)
|
||||||
models = result.scalars().all()
|
models = result.scalars().all()
|
||||||
return models
|
return models
|
||||||
|
|
||||||
|
async def delete_one(self, id: int) -> None:
|
||||||
|
await self.session.execute(delete(self.model).where(self.model.id == id))
|
||||||
|
|
||||||
|
async def update_one(self, id: int, data: dict) -> UsersORM:
|
||||||
|
stmt = (
|
||||||
|
update(self.model)
|
||||||
|
.where(self.model.id == id)
|
||||||
|
.values(data.model_dump(exclude_unset=True))
|
||||||
|
.returning(self.model)
|
||||||
|
)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
model = result.scalar_one()
|
||||||
|
return model
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from pydantic import BaseModel, EmailStr, ConfigDict, BeforeValidator
|
from pydantic import BaseModel, BeforeValidator, ConfigDict, EmailStr
|
||||||
|
|
||||||
from src.schemas.validators import ensure_password
|
from src.schemas.validators import ensure_password
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
email: EmailStr | None = None
|
||||||
|
username: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
email: EmailStr | None
|
email: EmailStr | None
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from src.schemas.auth import Token
|
|
||||||
from src.schemas.users import UserRequestADD, User, UserAdd, UserWithHashedPass
|
|
||||||
from src.services.base import BaseService
|
|
||||||
from src.core.auth_manager import AuthManager
|
from src.core.auth_manager import AuthManager
|
||||||
|
from src.core.settings import settings
|
||||||
|
from src.schemas.auth import Token
|
||||||
|
from src.schemas.users import User, UserAdd, UserRequestADD, UserWithHashedPass
|
||||||
|
from src.services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
class AuthService(BaseService):
|
class AuthService(BaseService):
|
||||||
@@ -37,4 +38,6 @@ class AuthService(BaseService):
|
|||||||
access_token = AuthManager.create_access_token(
|
access_token = AuthManager.create_access_token(
|
||||||
data={"id": user.id, "sub": user.username, "is_active": user.is_active}
|
data={"id": user.id, "sub": user.username, "is_active": user.is_active}
|
||||||
)
|
)
|
||||||
return Token(access_token=access_token, token_type="bearer")
|
return Token(
|
||||||
|
access_token=access_token, token_type=settings.access_token.token_type
|
||||||
|
)
|
||||||
|
|||||||
4
src/services/tasks.py
Normal file
4
src/services/tasks.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from src.services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
|
class TasksService(BaseService): ...
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from src.schemas.users import User
|
from src.schemas.users import User, UserUpdate
|
||||||
from src.services.base import BaseService
|
from src.services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
@@ -26,3 +26,13 @@ class UserService(BaseService):
|
|||||||
async def get_all_users(self) -> list[User]:
|
async def get_all_users(self) -> list[User]:
|
||||||
users = await self.session.user.get_all_users()
|
users = await self.session.user.get_all_users()
|
||||||
return [User.model_validate(user) for user in users]
|
return [User.model_validate(user) for user in users]
|
||||||
|
|
||||||
|
async def delete_user(self, id: int) -> None:
|
||||||
|
await self.session.user.delete_one(id=id)
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
|
async def update_user(self, id: int, update_data: UserUpdate) -> User:
|
||||||
|
await self.get_user_by_filter_or_raise(id=id)
|
||||||
|
user = await self.session.user.update_one(id=id, data=update_data)
|
||||||
|
await self.session.commit()
|
||||||
|
return User.model_validate(user)
|
||||||
|
|||||||
Reference in New Issue
Block a user