diff --git a/README.md b/README.md index 8221fb4..26d8cc7 100644 --- a/README.md +++ b/README.md @@ -50,15 +50,24 @@ pip install -r requirements.txt 3. Run the application: ```bash -uvicorn main:app --reload +uvicorn main:app --host 0.0.0.0 --port 8001 --reload ``` -The API will be available at http://localhost:8000. +The API will be available at http://localhost:8001. + +### Health Check + +The application has a dedicated health check endpoint at `/health` that returns a simple JSON response: +```json +{"status": "ok"} +``` + +This endpoint can be used by load balancers, container orchestrators, or monitoring tools to verify that the application is running correctly. ## API Documentation -- Swagger UI: http://localhost:8000/docs -- ReDoc: http://localhost:8000/redoc +- Swagger UI: http://localhost:8001/docs +- ReDoc: http://localhost:8001/redoc ## API Endpoints diff --git a/app/api/base.py b/app/api/base.py index 874adbf..7519666 100644 --- a/app/api/base.py +++ b/app/api/base.py @@ -1,7 +1,6 @@ from fastapi import APIRouter -from app.api import health, items +from app.api import items api_router = APIRouter() -api_router.include_router(health.router, tags=["health"]) -api_router.include_router(items.router, prefix="/items", tags=["items"]) \ No newline at end of file +api_router.include_router(items.router, prefix="/items", tags=["items"]) diff --git a/app/api/health.py b/app/api/health.py index e7bd006..b2c698c 100644 --- a/app/api/health.py +++ b/app/api/health.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter +from fastapi import APIRouter, status router = APIRouter() -@router.get("/health", tags=["health"]) +@router.get("/health", tags=["health"], status_code=status.HTTP_200_OK) async def health_check(): """ - Health check endpoint + Health check endpoint that returns OK status. + This endpoint is used to verify the application is running correctly. """ - return {"status": "ok"} \ No newline at end of file + return {"status": "ok"} diff --git a/app/api/items.py b/app/api/items.py index 5978e60..93945e6 100644 --- a/app/api/items.py +++ b/app/api/items.py @@ -90,4 +90,4 @@ def delete_item( detail="Item not found", ) item = crud.item.remove(db=db, id=id) - return item \ No newline at end of file + return item diff --git a/app/core/config.py b/app/core/config.py index 0fac94e..bc0dfc0 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -7,7 +7,11 @@ from pydantic_settings import BaseSettings class Settings(BaseSettings): API_V1_STR: str = "/api/v1" PROJECT_NAME: str = "Generic REST API Service" - + + # Server Configuration + HOST: str = "0.0.0.0" + PORT: int = 8001 + # CORS Configuration BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] @@ -29,4 +33,4 @@ class Settings(BaseSettings): env_file = ".env" -settings = Settings() \ No newline at end of file +settings = Settings() diff --git a/app/crud/__init__.py b/app/crud/__init__.py index 24fc4a8..084bf36 100644 --- a/app/crud/__init__.py +++ b/app/crud/__init__.py @@ -1 +1 @@ -from app.crud.item import item # noqa \ No newline at end of file +from app.crud.item import item # noqa diff --git a/app/crud/base.py b/app/crud/base.py index 6e2c6e6..3a09880 100644 --- a/app/crud/base.py +++ b/app/crud/base.py @@ -14,9 +14,9 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): def __init__(self, model: Type[ModelType]): """ CRUD object with default methods to Create, Read, Update, Delete (CRUD). - + **Parameters** - + * `model`: A SQLAlchemy model class * `schema`: A Pydantic model (schema) class """ @@ -43,7 +43,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): db: Session, *, db_obj: ModelType, - obj_in: Union[UpdateSchemaType, Dict[str, Any]] + obj_in: Union[UpdateSchemaType, Dict[str, Any]], ) -> ModelType: obj_data = jsonable_encoder(db_obj) if isinstance(obj_in, dict): @@ -62,4 +62,4 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): obj = db.query(self.model).get(id) db.delete(obj) db.commit() - return obj \ No newline at end of file + return obj diff --git a/app/crud/item.py b/app/crud/item.py index 7bfcdb7..4710b48 100644 --- a/app/crud/item.py +++ b/app/crud/item.py @@ -12,4 +12,4 @@ class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]): return db.query(Item).filter(Item.title == title).first() -item = CRUDItem(Item) \ No newline at end of file +item = CRUDItem(Item) diff --git a/app/db/base.py b/app/db/base.py index 85a77c9..dcf5649 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -1,4 +1,4 @@ # Import all the models, so that Base has them before being # imported by Alembic from app.db.base_class import Base # noqa -from app.models.item import Item # noqa \ No newline at end of file +from app.models.item import Item # noqa diff --git a/app/db/base_class.py b/app/db/base_class.py index a474ab1..723e698 100644 --- a/app/db/base_class.py +++ b/app/db/base_class.py @@ -6,8 +6,8 @@ from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): id: Any __name__: str - + # Generate __tablename__ automatically @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 24b55ec..bbe647d 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -4,8 +4,7 @@ from sqlalchemy.orm import sessionmaker from app.core.config import settings engine = create_engine( - settings.SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False} + settings.SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -15,4 +14,4 @@ def get_db(): try: yield db finally: - db.close() \ No newline at end of file + db.close() diff --git a/app/models/item.py b/app/models/item.py index 663ab82..a1b8939 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -11,4 +11,6 @@ class Item(Base): description = Column(Text, nullable=True) is_active = Column(Boolean, default=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) \ No newline at end of file + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 52f577d..cec3c32 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -1 +1 @@ -from app.schemas.item import Item, ItemCreate, ItemUpdate, ItemInDBBase # noqa \ No newline at end of file +from app.schemas.item import Item, ItemCreate, ItemUpdate, ItemInDBBase # noqa diff --git a/app/schemas/item.py b/app/schemas/item.py index 254d30b..27f37a0 100644 --- a/app/schemas/item.py +++ b/app/schemas/item.py @@ -33,4 +33,4 @@ class ItemInDBBase(ItemBase): # Properties to return to client class Item(ItemInDBBase): - pass \ No newline at end of file + pass diff --git a/main.py b/main.py index c50b640..6946cff 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,11 @@ -from fastapi import FastAPI +from fastapi import FastAPI, status from fastapi.middleware.cors import CORSMiddleware from app.api.base import api_router from app.core.config import settings app = FastAPI( - title=settings.PROJECT_NAME, - openapi_url=f"{settings.API_V1_STR}/openapi.json" + title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json" ) # Set all CORS enabled origins @@ -19,17 +18,21 @@ if settings.BACKEND_CORS_ORIGINS: allow_headers=["*"], ) -app.include_router(api_router, prefix=settings.API_V1_STR) - -@app.get("/health", tags=["health"]) +# Add root health check endpoint +@app.get("/health", tags=["health"], status_code=status.HTTP_200_OK) async def health_check(): """ - Health check endpoint + Root health check endpoint. + This endpoint is used for server health monitoring. """ return {"status": "ok"} +# Include API routes +app.include_router(api_router, prefix=settings.API_V1_STR) + if __name__ == "__main__": import uvicorn - uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file + + uvicorn.run("main:app", host=settings.HOST, port=settings.PORT, reload=True) diff --git a/migrations/__init__.py b/migrations/__init__.py index 1b00133..95fa6f3 100644 --- a/migrations/__init__.py +++ b/migrations/__init__.py @@ -1 +1 @@ -# Alembic migrations package \ No newline at end of file +# Alembic migrations package diff --git a/migrations/env.py b/migrations/env.py index f62131e..606a507 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -64,9 +64,9 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - is_sqlite = connection.dialect.name == 'sqlite' + is_sqlite = connection.dialect.name == "sqlite" context.configure( - connection=connection, + connection=connection, target_metadata=target_metadata, render_as_batch=is_sqlite, ) @@ -78,4 +78,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/migrations/versions/initial_migration.py b/migrations/versions/initial_migration.py index 9c0cccd..5bf7060 100644 --- a/migrations/versions/initial_migration.py +++ b/migrations/versions/initial_migration.py @@ -1,16 +1,17 @@ """Initial migration Revision ID: 01234567890a -Revises: +Revises: Create Date: 2023-10-01 10:00:00.000000 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '01234567890a' +revision = "01234567890a" down_revision = None branch_labels = None depends_on = None @@ -18,23 +19,34 @@ depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('item', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "item", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=255), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=True, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=True, + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_item_id'), 'item', ['id'], unique=False) - op.create_index(op.f('ix_item_title'), 'item', ['title'], unique=False) + op.create_index(op.f("ix_item_id"), "item", ["id"], unique=False) + op.create_index(op.f("ix_item_title"), "item", ["title"], unique=False) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_item_title'), table_name='item') - op.drop_index(op.f('ix_item_id'), table_name='item') - op.drop_table('item') - # ### end Alembic commands ### \ No newline at end of file + op.drop_index(op.f("ix_item_title"), table_name="item") + op.drop_index(op.f("ix_item_id"), table_name="item") + op.drop_table("item") + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index f1e42a5..eb4c8ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,6 @@ alembic>=1.12.0,<1.13.0 pydantic>=2.3.0,<2.4.0 pydantic-settings>=2.0.3,<2.1.0 python-dotenv>=1.0.0,<1.1.0 +httpx>=0.24.1,<0.25.0 # Required for TestClient +pytest>=7.4.0,<7.5.0 ruff>=0.0.287,<0.1.0 \ No newline at end of file diff --git a/test_health.py b/test_health.py new file mode 100644 index 0000000..c651f0f --- /dev/null +++ b/test_health.py @@ -0,0 +1,15 @@ +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + + +def test_health_endpoint(): + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +if __name__ == "__main__": + test_result = test_health_endpoint() + print("Health endpoint test passed!") diff --git a/test_health_endpoint.sh b/test_health_endpoint.sh new file mode 100755 index 0000000..a5a03d3 --- /dev/null +++ b/test_health_endpoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo "Testing health endpoint on port 8001..." +echo "GET /health HTTP/1.1" +echo "Expected response: {'status': 'ok'}" +echo "" +echo "This script will help you verify that the health endpoint is working correctly." +echo "Run this script after starting the application with 'uvicorn main:app --host 0.0.0.0 --port 8001'" \ No newline at end of file