Commit 9884e87d authored by ZeinabRm13's avatar ZeinabRm13

Add migrations

parent 8d7cb029
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
# this is typically a path given in POSIX (e.g. forward slashes) # this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this # format, relative to the token %(here)s which refers to the location of this
# ini file # ini file
script_location = %(here)s/alembic script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time # Uncomment the line below if you want the files to be prepended with date and time
...@@ -16,7 +16,7 @@ script_location = %(here)s/alembic ...@@ -16,7 +16,7 @@ script_location = %(here)s/alembic
# sys.path path, will be prepended to sys.path if present. # sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator # defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below. # is defined by "path_separator" below.
prepend_sys_path = . prepend_sys_path = src
# timezone to use when rendering the date within the migration file # timezone to use when rendering the date within the migration file
...@@ -84,7 +84,7 @@ path_separator = os ...@@ -84,7 +84,7 @@ path_separator = os
# database URL. This is consumed by the user-maintained env.py script only. # database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py # other means of configuring database URLs may be customized within the env.py
# file. # file.
sqlalchemy.url = postgresql+asyncpg://chart_analyzer_user:chartanalyzer13@localhost:5432/chart_analyzer sqlalchemy.url = "postgresql+psycopg://chart_analyzer_user:chartanalyzer13@localhost:5432/chart_analyzer"
[post_write_hooks] [post_write_hooks]
......
# alembic/env.py # alembic/env.py
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
from sqlalchemy.engine import Connection from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
from alembic import context from alembic import context
import asyncio
import sys
import os
# Add the project root to sys.path to import your models import os
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) import sys
if project_root not in sys.path:
sys.path.insert(0, project_root)
# Import your Base from your app # Add the project root directory to the Python path
from src.infrastructure.adapters.sqlserver import Base sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# Import your settings and models
from src.config import settings
from src.infrastructure.adapters.sqlserver.models import Base
# ------------------------------ # ------------------------------
# Configure Alembic # Configure Alembic
# ------------------------------ # ------------------------------
config = context.config config = context.config
# Setup logging (if you have it in alembic.ini) # Setup logging
if config.config_file_name is not None: if config.config_file_name is not None:
fileConfig(config.config_file_name) fileConfig(config.config_file_name)
# Set target metadata for autogenerate # Set target metadata for autogenerate
target_metadata = Base.metadata target_metadata = Base.metadata
# Set the database URL from settings
config.set_main_option('sqlalchemy.url', settings.DATABASE_URL)
url = config.get_main_option("sqlalchemy.url")
# ------------------------------ # ------------------------------
# Offline Mode (no DB connection) # Offline Mode
# ------------------------------ # ------------------------------
def run_migrations_offline() -> None: def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.""" """Run migrations in 'offline' mode."""
url = config.get_main_option("sqlalchemy.url")
context.configure( context.configure(
url=url, url=url,
target_metadata=target_metadata, target_metadata=target_metadata,
literal_binds=True, literal_binds=True,
dialect_opts={"paramstyle": "named"}, dialect_opts={"paramstyle": "named"},
compare_type=True,
compare_server_default=True,
) )
with context.begin_transaction(): with context.begin_transaction():
...@@ -49,37 +49,27 @@ def run_migrations_offline() -> None: ...@@ -49,37 +49,27 @@ def run_migrations_offline() -> None:
# ------------------------------ # ------------------------------
# Online Mode (with async engine) # Online Mode (with sync engine)
# ------------------------------ # ------------------------------
async def run_async_migrations() -> None: def run_migrations_online() -> None:
"""Create and run async migrations.""" """Run migrations in 'online' mode."""
connectable: AsyncEngine = create_async_engine( connectable = engine_from_config(
config.get_main_option("sqlalchemy.url"), config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool, poolclass=pool.NullPool,
) )
async with connectable.connect() as connection: with connectable.connect() as connection:
# ✅ Correct: Configure context inside run_sync context.configure(
await connection.run_sync(lambda conn: context.configure( connection=connection, target_metadata=target_metadata
connection=conn, )
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
))
# ✅ Correct: Wrap run_migrations in lambda to avoid extra arg
await connection.run_sync(lambda conn: context.run_migrations())
await connectable.dispose()
with context.begin_transaction():
def run_migrations_online() -> None: context.run_migrations()
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
# ------------------------------ # ------------------------------
# Choose mode # Run the appropriate mode
# ------------------------------ # ------------------------------
if context.is_offline_mode(): if context.is_offline_mode():
run_migrations_offline() run_migrations_offline()
......
"""Initial migration
Revision ID: b052cb31f4a8
Revises:
Create Date: 2025-07-23 08:29:32.710203
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b052cb31f4a8'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('password_hash', sa.String(length=255), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('last_login', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('chart_images',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('user_id', sa.UUID(), nullable=False),
sa.Column('image_data', sa.LargeBinary(), nullable=False),
sa.Column('uploaded_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('chart_analyses',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('chart_image_id', sa.UUID(), nullable=False),
sa.Column('question', sa.Text(), nullable=False),
sa.Column('answer', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['chart_image_id'], ['chart_images.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('chart_analyses')
op.drop_table('chart_images')
op.drop_table('users')
# ### end Alembic commands ###
from fastapi import FastAPI
from src.infrastructure.api.fastapi.routes import auth, charts
app = FastAPI(
title="Chart Analyzer API",
description="API for analyzing charts and managing users.",
version="1.0.0",
)
app.include_router(auth.router, prefix="/auth")
app.include_router(charts.router, prefix="/charts")
@app.get("/")
async def root():
return {"message": "Welcome to Chart Analyzer"}
from .config import Settings
\ No newline at end of file
from authentication import ( from .authentication import (
RegisterRequestDTO, RegisterRequestDTO,
LoginRequestDTO, LoginRequestDTO,
UserResponseDTO, UserResponseDTO,
TokenResponseDTO TokenResponseDTO
) )
from analysis import AnalysisRequestDTO, AnalysisResponseDTO from .analysis import AnalysisRequestDTO, AnalysisResponseDTO
\ No newline at end of file \ No newline at end of file
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from domain.entities.user import User from src.domain.entities.user import User
from application.dtos.authentication import ( from src.application.dtos.authentication import (
RegisterRequest, RegisterRequestDTO,
LoginRequest, LoginRequestDTO,
UserResponse, UserResponseDTO,
TokenResponse TokenResponseDTO
) )
# src/domain/ports/auth_service.py
class AuthServicePort(ABC): class AuthServicePort(ABC):
@abstractmethod @abstractmethod
async def register(self, request: RegisterRequest) -> UserResponse: ... async def register(self, email, password) -> User: ...
@abstractmethod @abstractmethod
async def login(self, request: LoginRequest) -> TokenResponse: # Returns JWT async def login(self, email, password) -> str: ...
... \ No newline at end of file
\ No newline at end of file
...@@ -2,15 +2,15 @@ ...@@ -2,15 +2,15 @@
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from jose import jwt from jose import jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from domain.ports import UserRepositoryPort from src.domain.ports.repositories.user_repository import UserRepositoryPort
from domain.entities.user import User from src.domain.entities.user import User
import uuid import uuid
from application.ports import AuthServicePort from src.application.ports.authentication_service_port import AuthServicePort
from application.dtos.authentication import ( from src.application.dtos.authentication import (
RegisterRequest, RegisterRequestDTO,
LoginRequest, LoginRequestDTO,
UserResponse, UserResponseDTO,
TokenResponse TokenResponseDTO
) )
class AuthService(AuthServicePort): class AuthService(AuthServicePort):
...@@ -27,27 +27,24 @@ class AuthService(AuthServicePort): ...@@ -27,27 +27,24 @@ class AuthService(AuthServicePort):
self._expires_minutes = expires_minutes self._expires_minutes = expires_minutes
self._pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") self._pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def register(self, request: RegisterRequest) -> UserResponse: async def register(self, email, password) -> User:
if await self._user_repo.get_by_email(request.email): if await self._user_repo.get_by_email(email):
raise ValueError("Email already registered") raise ValueError("Email already registered")
user = User( user = User(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
email=request.email, email=email,
password_hash=self._hash_password(request.password) password_hash=self._hash_password(password)
) )
await self._user_repo.create_user(user) await self._user_repo.create_user(user)
return self._user_to_dto(user) return user
async def login(self, request: LoginRequest) -> TokenResponse: async def login(self, email, password) -> str:
user = await self._user_repo.get_by_email(request.email) user = await self._user_repo.get_by_email(email)
if not user or not self._verify_password(request.password, user.password_hash): if not user or not self._verify_password(password, user.password_hash):
raise ValueError("Invalid credentials") raise ValueError("Invalid credentials")
return TokenResponse( return self._create_access_token(user.email)
access_token=self._create_access_token(user.email),
token_type="bearer"
)
def _hash_password(self, password: str) -> str: def _hash_password(self, password: str) -> str:
return self._pwd_context.hash(password) return self._pwd_context.hash(password)
...@@ -63,8 +60,8 @@ class AuthService(AuthServicePort): ...@@ -63,8 +60,8 @@ class AuthService(AuthServicePort):
algorithm=self._algorithm algorithm=self._algorithm
) )
def _user_to_dto(self, user: User) -> UserResponse: def _user_to_dto(self, user: User) -> UserResponseDTO:
return UserResponse( return UserResponseDTO(
id=user.id, id=user.id,
email=user.email, email=user.email,
is_active=user.is_active is_active=user.is_active
......
from domain.entities import ChartImage from src.domain.entities.chart_image import ChartImage
from domain.ports import ChartsRepositoryPort from src.domain.ports.repositories.charts_repository import ChartsRepositoryPort
import uuid import uuid
class UploadChartUseCase: class UploadChartUseCase:
...@@ -12,4 +12,5 @@ class UploadChartUseCase: ...@@ -12,4 +12,5 @@ class UploadChartUseCase:
user_id=user_id, user_id=user_id,
image_data=image_bytes image_data=image_bytes
) )
return self._image_repo.save(chart_image) # Returns image ID self._image_repo.save(chart_image)
\ No newline at end of file return chart_image.id
\ No newline at end of file
# src/config.py
from pydantic import ConfigDict
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://chart_analyzer_user:chartanalyzer13@localhost:5432/chart_analyzer" DATABASE_URL: str = "postgresql+psycopg://chart_analyzer_user:chartanalyzer13@localhost:5432/chart_analyzer"
JWT_SECRET: str = "a_very_secret_key" JWT_SECRET: str = "a_very_secret_key"
JWT_ALGORITHM: str = "HS256" JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_MINUTES: int = 30 JWT_EXPIRE_MINUTES: int = 30
class Config: # Use model_config instead of inner Config class
env_file = ".env" model_config = ConfigDict(
env_file_encoding = "utf-8" env_file=".env",
env_file_encoding="utf-8",
extra="ignore" # or "allow" if you want to allow extra fields
)
settings = Settings() settings = Settings()
\ No newline at end of file
...@@ -2,10 +2,13 @@ from fastapi import Depends ...@@ -2,10 +2,13 @@ from fastapi import Depends
from typing import AsyncGenerator from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from application.services import AuthService from src.application.services.authentication_service import AuthService
from domain.ports import UserRepositoryPort from src.application.use_cases.upload_chart import UploadChartUseCase
from infrastructure.adapters.sqlserver.sql_user_repository import SqlUserRepository from src.domain.ports.repositories.user_repository import UserRepositoryPort
from config import settings from src.domain.ports.repositories.charts_repository import ChartsRepositoryPort
from src.infrastructure.adapters.sqlserver.sql_user_repository import SqlUserRepository
from src.infrastructure.adapters.sqlserver.sql_charts_repository import SqlChartsRepository
from src.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=True) engine = create_async_engine(settings.DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
...@@ -18,6 +21,9 @@ async def get_db_session() -> AsyncGenerator[AsyncSession, None]: ...@@ -18,6 +21,9 @@ async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
def get_user_repository(session: AsyncSession = Depends(get_db_session)) -> UserRepositoryPort: def get_user_repository(session: AsyncSession = Depends(get_db_session)) -> UserRepositoryPort:
return SqlUserRepository(session) return SqlUserRepository(session)
def get_charts_repository(session: AsyncSession = Depends(get_db_session)) -> ChartsRepositoryPort:
return SqlChartsRepository(session)
def get_auth_service( def get_auth_service(
user_repo: UserRepositoryPort = Depends(get_user_repository), user_repo: UserRepositoryPort = Depends(get_user_repository),
) -> AuthService: ) -> AuthService:
...@@ -26,4 +32,7 @@ def get_auth_service( ...@@ -26,4 +32,7 @@ def get_auth_service(
secret_key=settings.JWT_SECRET, secret_key=settings.JWT_SECRET,
algorithm=settings.JWT_ALGORITHM, algorithm=settings.JWT_ALGORITHM,
expires_minutes=settings.JWT_EXPIRE_MINUTES, expires_minutes=settings.JWT_EXPIRE_MINUTES,
) )
\ No newline at end of file
def get_upload_use_case(charts_repo: ChartsRepositoryPort = Depends(get_charts_repository)) -> UploadChartUseCase:
return UploadChartUseCase(charts_repo)
\ No newline at end of file
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from domain.entities import ChartAnalysis from src.domain.entities import ChartAnalysis
class AnalysisRepositoryPort(ABC): class AnalysisRepositoryPort(ABC):
@abstractmethod @abstractmethod
......
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from domain.entities import ChartImage from src.domain.entities.chart_image import ChartImage
class ChartsRepositoryPort(ABC): class ChartsRepositoryPort(ABC):
@abstractmethod @abstractmethod
......
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from domain.entities.user import User from src.domain.entities.user import User
class UserRepositoryPort(ABC): class UserRepositoryPort(ABC):
@abstractmethod @abstractmethod
......
from jose import jwt
from datetime import datetime, timedelta
from src.config import settings
class JwtTokenGenerator:
def create_token(self, user_id: int) -> str:
payload = {
"sub": str(user_id),
"exp": datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
}
return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
def decode_token(self, token: str) -> int:
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
return int(payload["sub"])
\ No newline at end of file
# src/infrastructure/database/models.py # src/infrastructure/database/models.py
from sqlalchemy import Column, String, Boolean, DateTime, LargeBinary, ForeignKey, Text from sqlalchemy import Column, String, Boolean, DateTime, LargeBinary, ForeignKey, Text
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.dialects.mssql import UNIQUEIDENTIFIER # For SQL Server UUID from sqlalchemy.orm import declarative_base
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
import uuid
# 1. Create a Base class that all your models will inherit from. # 1. Create a Base class that all your models will inherit from.
Base = declarative_base() Base = declarative_base()
...@@ -11,7 +12,7 @@ Base = declarative_base() ...@@ -11,7 +12,7 @@ Base = declarative_base()
class User(Base): class User(Base):
__tablename__ = 'users' __tablename__ = 'users'
id = Column(UNIQUEIDENTIFIER, primary_key=True, server_default=func.newid()) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), unique=True, nullable=False) email = Column(String(255), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False) # Store only hashed passwords password_hash = Column(String(255), nullable=False) # Store only hashed passwords
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
...@@ -27,8 +28,8 @@ class User(Base): ...@@ -27,8 +28,8 @@ class User(Base):
class ChartImage(Base): class ChartImage(Base):
__tablename__ = 'chart_images' __tablename__ = 'chart_images'
id = Column(UNIQUEIDENTIFIER, primary_key=True, server_default=func.newid()) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UNIQUEIDENTIFIER, ForeignKey('users.id'), nullable=False) user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False)
image_data = Column(LargeBinary, nullable=False) # For storing binary data image_data = Column(LargeBinary, nullable=False) # For storing binary data
uploaded_at = Column(DateTime(timezone=True), server_default=func.now()) uploaded_at = Column(DateTime(timezone=True), server_default=func.now())
...@@ -41,8 +42,8 @@ class ChartImage(Base): ...@@ -41,8 +42,8 @@ class ChartImage(Base):
class ChartAnalysis(Base): class ChartAnalysis(Base):
__tablename__ = 'chart_analyses' __tablename__ = 'chart_analyses'
id = Column(UNIQUEIDENTIFIER, primary_key=True, server_default=func.newid()) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
chart_image_id = Column(UNIQUEIDENTIFIER, ForeignKey('chart_images.id'), nullable=False) chart_image_id = Column(UUID(as_uuid=True), ForeignKey('chart_images.id'), nullable=False)
question = Column(Text, nullable=False) question = Column(Text, nullable=False)
answer = Column(Text, nullable=False) answer = Column(Text, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
......
from sqlalchemy import create_engine from sqlalchemy import create_engine
from domain.ports import AnalysisRepositoryPort from src.domain.ports import AnalysisRepositoryPort
from domain.entities import ChartAnalysis, ChartImage from src.domain.entities import ChartAnalysis, ChartImage
class SqlAnalysisRepository(AnalysisRepositoryPort): class SqlAnalysisRepository(AnalysisRepositoryPort):
def __init__(self, connection_string: str): def __init__(self, connection_string: str):
self._engine = create_engine(connection_string) self._engine = create_engine(connection_string)
......
from sqlalchemy import create_engine from sqlalchemy import create_engine
from domain.ports import ChartsRepositoryPort from src.domain.ports.repositories.charts_repository import ChartsRepositoryPort
from domain.entities import ChartImage from src.domain.entities.chart_image import ChartImage
from sqlalchemy.orm import Session
class SqlChartsRepository(ChartsRepositoryPort): class SqlChartsRepository(ChartsRepositoryPort):
def __init__(self, connection_string: str): def __init__(self, session: Session):
self._engine = create_engine(connection_string) self._session = session
def save(self, image: ChartImage) -> str: def save(self, image: ChartImage) -> str:
with self._engine.connect() as conn: self._session.add(image)
conn.execute( self._session.commit()
"INSERT INTO Charts (Id, UserId, ImageData, UploadedAt) " return image.id
"VALUES (?, ?, ?, ?)", \ No newline at end of file
(image.id, image.user_id, image.image_data, image.uploaded_at)
)
conn.commit()
return image.id # Return the ID instead of a file path
\ No newline at end of file
# src/infrastructure/adapters/sql_user_repository.py # src/infrastructure/adapters/sql_user_repository.py
from sqlalchemy import select from sqlalchemy import select
from domain.ports import UserRepositoryPort from src.domain.ports import UserRepositoryPort
from domain.entities.user import User from src.domain.entities.user import User
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import EmailStr from pydantic import EmailStr
......
from fastapi import APIRouter, UploadFile, File, Depends
from application.use_cases.upload_chart import UploadChartUseCase
from fastapi import APIRouter, Depends, HTTPException, status
from application.dtos import RegisterRequestDTO, LoginRequestDTO, TokenResponseDTO
from application.ports import AuthServicePort
from dependencies import get_auth_service, get_upload_use_case
router = APIRouter(tags=["auth"])
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(
request: RegisterRequestDTO,
auth_service: AuthServicePort = Depends(get_auth_service)
):
try:
user = await auth_service.register(request.email, request.password)
return {"id": user.id, "email": user.email}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/login", response_model=TokenResponseDTO)
async def login(
request: LoginRequestDTO,
auth_service: AuthServicePort = Depends(get_auth_service)
):
try:
token = await auth_service.login(request.email, request.password)
return {"access_token": token, "token_type": "bearer"}
except ValueError as e:
raise HTTPException(status_code=401, detail=str(e))
@router.post("/upload")
async def upload_chart(
file: UploadFile = File(...),
use_case: UploadChartUseCase = Depends(get_upload_use_case)
) -> dict:
image_bytes = await file.read()
image_id = use_case.execute(image_bytes, "user123") # Replace with real user ID
return {"image_id": image_id}
\ No newline at end of file
# src/infrastructure/api/fastapi/routes/auth.py
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from application.dtos.authentication import RegisterRequest, LoginRequest, TokenResponse, UserResponse from src.application.dtos.authentication import RegisterRequestDTO, LoginRequestDTO, TokenResponseDTO
from application.services import AuthService from src.application.ports.authentication_service_port import AuthServicePort
from dependencies import get_auth_service from src.dependencies import get_auth_service
router = APIRouter(tags=["auth"]) router = APIRouter(tags=["auth"])
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED) @router.post("/register", status_code=status.HTTP_201_CREATED)
async def register( async def register(
request: RegisterRequest, request: RegisterRequestDTO,
auth_service: AuthService = Depends(get_auth_service), auth_service: AuthServicePort = Depends(get_auth_service)
): ):
try: try:
return await auth_service.register(request) user = await auth_service.register(request.email, request.password)
return {"id": user.id, "email": user.email}
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.post("/login", response_model=TokenResponse) @router.post("/login", response_model=TokenResponseDTO)
async def login( async def login(
request: LoginRequest, request: LoginRequestDTO,
auth_service: AuthService = Depends(get_auth_service), auth_service: AuthServicePort = Depends(get_auth_service)
): ):
try: try:
return await auth_service.login(request) token = await auth_service.login(request.email, request.password)
return {"access_token": token, "token_type": "bearer"}
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(status_code=401, detail=str(e))
status_code=status.HTTP_401_UNAUTHORIZED, \ No newline at end of file
detail=str(e),
headers={"WWW-Authenticate": "Bearer"},
)
\ No newline at end of file
from fastapi import APIRouter, UploadFile, File, Depends
from src.application.use_cases.upload_chart import UploadChartUseCase
from src.dependencies import get_upload_use_case
router = APIRouter()
@router.post("/upload")
async def upload_chart(
file: UploadFile = File(...),
use_case: UploadChartUseCase = Depends(get_upload_use_case)
) -> dict:
image_bytes = await file.read()
image_id = use_case.execute(image_bytes, "user123")
return {"image_id": image_id}
\ No newline at end of file
...@@ -25,44 +25,3 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]: ...@@ -25,44 +25,3 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
yield session yield session
finally: finally:
await session.close() await session.close()
chart_analyzer/ # Project root
├── docker-compose.yml
├── Dockerfile
├── generate.sh
├── requirements.txt
├── alembic.ini # ✅ At root
├── alembic/ # ✅ At root
├── versions/
├── env.py
└── script.py.mako
├── src/ # ✅ Only one src
├── application/ # Use cases, services
├── domain/ # Entities, business logic
├── infrastructure/ # DB, adapters, external services
├── adapters/
├── sqlmodel.py # Your Base, models
└── __init__.py
└── persistence/ # DB session, engine
├── database.py
└── __init__.py
├── presentation/ # FastAPI routers, DTOs
├── api/
├── v1/
├── users.py
└── charts.py
└── __init__.py
└── __init__.py
├── main.py # FastAPI app factory
└── config.py # Config, settings
├── tests/
├── unit/
├── integration/
└── conftest.py
├── .env
├── .gitignore
└── README.md
\ No newline at end of file
from fastapi import FastAPI
from infrastructure.api.fastapi.routes import auth
app = FastAPI(
title="Chart Analyzer API",
description="API for analyzing charts and managing users.",
version="1.0.0",
)
app.include_router(auth.router, prefix="/auth")
@app.get("/")
async def root():
return {"message": "Welcome to Chart Analyzer"}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment