diff --git a/alembic/env.py b/alembic/env.py index 1340432..63515a4 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -4,6 +4,7 @@ from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context +from app.db.base import Base # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -16,7 +17,6 @@ if config.config_file_name is not None: # add your model's MetaData object here # for 'autogenerate' support -from app.db.base import Base target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, @@ -63,9 +63,7 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() @@ -74,4 +72,4 @@ def run_migrations_online() -> None: if context.is_offline_mode(): run_migrations_offline() else: - run_migrations_online() \ No newline at end of file + run_migrations_online() diff --git a/alembic/versions/001_initial_setup.py b/alembic/versions/001_initial_setup.py index 50fb018..b5ce5d1 100644 --- a/alembic/versions/001_initial_setup.py +++ b/alembic/versions/001_initial_setup.py @@ -1,16 +1,17 @@ """initial setup Revision ID: 001 -Revises: +Revises: Create Date: 2025-05-13 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '001' +revision = "001" down_revision = None branch_labels = None depends_on = None @@ -19,20 +20,27 @@ depends_on = None def upgrade() -> None: # Create calculation table op.create_table( - 'calculation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('operation', sa.String(), nullable=False), - sa.Column('first_number', sa.Float(), nullable=False), - sa.Column('second_number', sa.Float(), nullable=True), - sa.Column('result', sa.Float(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id') + "calculation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("operation", sa.String(), nullable=False), + sa.Column("first_number", sa.Float(), nullable=False), + sa.Column("second_number", sa.Float(), nullable=True), + sa.Column("result", sa.Float(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("CURRENT_TIMESTAMP"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_calculation_id"), "calculation", ["id"], unique=False) + op.create_index( + op.f("ix_calculation_operation"), "calculation", ["operation"], unique=False ) - op.create_index(op.f('ix_calculation_id'), 'calculation', ['id'], unique=False) - op.create_index(op.f('ix_calculation_operation'), 'calculation', ['operation'], unique=False) def downgrade() -> None: - op.drop_index(op.f('ix_calculation_operation'), table_name='calculation') - op.drop_index(op.f('ix_calculation_id'), table_name='calculation') - op.drop_table('calculation') \ No newline at end of file + op.drop_index(op.f("ix_calculation_operation"), table_name="calculation") + op.drop_index(op.f("ix_calculation_id"), table_name="calculation") + op.drop_table("calculation") diff --git a/app/api/endpoints/calculator.py b/app/api/endpoints/calculator.py index f29be9c..0d1c591 100644 --- a/app/api/endpoints/calculator.py +++ b/app/api/endpoints/calculator.py @@ -9,46 +9,59 @@ from app.crud import calculation router = APIRouter() + def perform_calculation(calc: CalculationCreate) -> float: """Perform the calculation based on the operation and numbers provided.""" if calc.operation == "add": if calc.second_number is None: - raise HTTPException(status_code=400, detail="Second number is required for addition") + raise HTTPException( + status_code=400, detail="Second number is required for addition" + ) return calc.first_number + calc.second_number - + elif calc.operation == "subtract": if calc.second_number is None: - raise HTTPException(status_code=400, detail="Second number is required for subtraction") + raise HTTPException( + status_code=400, detail="Second number is required for subtraction" + ) return calc.first_number - calc.second_number - + elif calc.operation == "multiply": if calc.second_number is None: - raise HTTPException(status_code=400, detail="Second number is required for multiplication") + raise HTTPException( + status_code=400, detail="Second number is required for multiplication" + ) return calc.first_number * calc.second_number - + elif calc.operation == "divide": if calc.second_number is None: - raise HTTPException(status_code=400, detail="Second number is required for division") + raise HTTPException( + status_code=400, detail="Second number is required for division" + ) if calc.second_number == 0: raise HTTPException(status_code=400, detail="Cannot divide by zero") return calc.first_number / calc.second_number - + elif calc.operation == "square_root": if calc.first_number < 0: - raise HTTPException(status_code=400, detail="Cannot calculate square root of a negative number") + raise HTTPException( + status_code=400, + detail="Cannot calculate square root of a negative number", + ) return math.sqrt(calc.first_number) - + else: raise HTTPException( - status_code=400, - detail="Invalid operation. Supported operations: add, subtract, multiply, divide, square_root" + status_code=400, + detail="Invalid operation. Supported operations: add, subtract, multiply, divide, square_root", ) + @router.post("/", response_model=CalculationResponse) def calculate(calc: CalculationCreate, db: Session = Depends(get_db)): """ Perform a calculation and store it in the database. - + - **operation**: add, subtract, multiply, divide, or square_root - **first_number**: First operand - **second_number**: Second operand (not required for square_root) @@ -57,14 +70,18 @@ def calculate(calc: CalculationCreate, db: Session = Depends(get_db)): db_calc = calculation.create_calculation(db=db, calc=calc, result=result) return db_calc + @router.get("/history", response_model=List[CalculationResponse]) -def get_calculation_history(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): +def get_calculation_history( + skip: int = 0, limit: int = 100, db: Session = Depends(get_db) +): """ Retrieve calculation history. """ calcs = calculation.get_calculations(db, skip=skip, limit=limit) return calcs + @router.get("/{calculation_id}", response_model=CalculationResponse) def get_calculation_by_id(calculation_id: int, db: Session = Depends(get_db)): """ @@ -73,4 +90,4 @@ def get_calculation_by_id(calculation_id: int, db: Session = Depends(get_db)): db_calc = calculation.get_calculation(db, calculation_id=calculation_id) if db_calc is None: raise HTTPException(status_code=404, detail="Calculation not found") - return db_calc \ No newline at end of file + return db_calc diff --git a/app/api/endpoints/health.py b/app/api/endpoints/health.py index 652d8a7..8eac7f4 100644 --- a/app/api/endpoints/health.py +++ b/app/api/endpoints/health.py @@ -4,6 +4,7 @@ from app.db.session import get_db router = APIRouter() + @router.get("/health") def health_check(db: Session = Depends(get_db)): """ @@ -17,9 +18,5 @@ def health_check(db: Session = Depends(get_db)): db_status = "healthy" except Exception as e: db_status = f"unhealthy: {str(e)}" - - return { - "status": "healthy", - "database": db_status, - "version": "1.0.0" - } \ No newline at end of file + + return {"status": "healthy", "database": db_status, "version": "1.0.0"} diff --git a/app/api/router.py b/app/api/router.py index f15a21b..7878a08 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -3,4 +3,4 @@ from app.api.endpoints import calculator, health router = APIRouter() router.include_router(calculator.router, prefix="/calculator", tags=["calculator"]) -router.include_router(health.router, tags=["health"]) \ No newline at end of file +router.include_router(health.router, tags=["health"]) diff --git a/app/core/config.py b/app/core/config.py index 8657087..8657a93 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,17 +1,19 @@ from pathlib import Path from pydantic_settings import BaseSettings + class Settings(BaseSettings): API_V1_STR: str = "/api/v1" PROJECT_NAME: str = "Calculator API" - + # Database settings DB_DIR = Path("/app") / "storage" / "db" DB_DIR.mkdir(parents=True, exist_ok=True) DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite" - + class Config: env_file = ".env" case_sensitive = True -settings = Settings() \ No newline at end of file + +settings = Settings() diff --git a/app/crud/calculation.py b/app/crud/calculation.py index 3ae4ef7..a6f5f6e 100644 --- a/app/crud/calculation.py +++ b/app/crud/calculation.py @@ -2,20 +2,31 @@ from sqlalchemy.orm import Session from app.models.calculation import Calculation from app.schemas.calculation import CalculationCreate -def create_calculation(db: Session, calc: CalculationCreate, result: float) -> Calculation: + +def create_calculation( + db: Session, calc: CalculationCreate, result: float +) -> Calculation: db_calculation = Calculation( operation=calc.operation, first_number=calc.first_number, second_number=calc.second_number, - result=result + result=result, ) db.add(db_calculation) db.commit() db.refresh(db_calculation) return db_calculation + def get_calculations(db: Session, skip: int = 0, limit: int = 100): - return db.query(Calculation).order_by(Calculation.created_at.desc()).offset(skip).limit(limit).all() + return ( + db.query(Calculation) + .order_by(Calculation.created_at.desc()) + .offset(skip) + .limit(limit) + .all() + ) + def get_calculation(db: Session, calculation_id: int): - return db.query(Calculation).filter(Calculation.id == calculation_id).first() \ No newline at end of file + return db.query(Calculation).filter(Calculation.id == calculation_id).first() diff --git a/app/db/base.py b/app/db/base.py index 799f08a..e69de29 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -1,2 +0,0 @@ -from app.db.base_class import Base -from app.models.calculation import Calculation \ No newline at end of file diff --git a/app/db/base_class.py b/app/db/base_class.py index f4d43d6..b6e3745 100644 --- a/app/db/base_class.py +++ b/app/db/base_class.py @@ -1,11 +1,12 @@ from typing import Any from sqlalchemy.ext.declarative import as_declarative, declared_attr + @as_declarative() class Base: id: Any __name__: str - + @declared_attr def __tablename__(cls) -> str: - return cls.__name__.lower() \ No newline at end of file + return cls.__name__.lower() diff --git a/app/db/session.py b/app/db/session.py index ab0db0b..e514fae 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -9,14 +9,14 @@ DB_DIR.mkdir(parents=True, exist_ok=True) SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite" engine = create_engine( - SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False} + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + def get_db(): db = SessionLocal() try: yield db finally: - db.close() \ No newline at end of file + db.close() diff --git a/app/models/calculation.py b/app/models/calculation.py index e21c19c..90389f2 100644 --- a/app/models/calculation.py +++ b/app/models/calculation.py @@ -2,10 +2,11 @@ from sqlalchemy import Column, Integer, String, Float, DateTime from sqlalchemy.sql import func from app.db.base_class import Base + class Calculation(Base): id = Column(Integer, primary_key=True, index=True) operation = Column(String, index=True) first_number = Column(Float) second_number = Column(Float, nullable=True) result = Column(Float) - created_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file + created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/app/schemas/calculation.py b/app/schemas/calculation.py index 35ac1db..3ef2551 100644 --- a/app/schemas/calculation.py +++ b/app/schemas/calculation.py @@ -1,19 +1,26 @@ from datetime import datetime from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict + class CalculationBase(BaseModel): - operation: str = Field(..., description="Mathematical operation: add, subtract, multiply, divide, or square_root") + operation: str = Field( + ..., + description="Mathematical operation: add, subtract, multiply, divide, or square_root", + ) first_number: float = Field(..., description="First operand") - second_number: Optional[float] = Field(None, description="Second operand (not required for square_root)") + second_number: Optional[float] = Field( + None, description="Second operand (not required for square_root)" + ) + class CalculationCreate(CalculationBase): pass + class CalculationResponse(CalculationBase): id: int result: float created_at: datetime - class Config: - orm_mode = True \ No newline at end of file + model_config = ConfigDict(from_attributes=True) diff --git a/main.py b/main.py index 255708b..820b0de 100644 --- a/main.py +++ b/main.py @@ -11,4 +11,4 @@ app = FastAPI( app.include_router(api_router, prefix=settings.API_V1_STR) if __name__ == "__main__": - uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/requirements.txt b/requirements.txt index 3d9fcc4..afbefc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ fastapi==0.110.1 uvicorn==0.29.0 sqlalchemy==2.0.29 pydantic==2.6.4 +pydantic-settings==2.2.1 alembic==1.13.1 ruff==0.3.2 python-dotenv==1.0.1 \ No newline at end of file