diff --git a/README.md b/README.md index 774a8aa..118c764 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Number Divisibility API -A simple REST API built with FastAPI that checks if a number is divisible by 2. +A simple REST API built with FastAPI that checks if a number is divisible by 2 and by 3. ## Features -- Check if a number is divisible by 2 via GET or POST requests +- Check if a number is divisible by 2 and by 3 via GET or POST requests +- Dedicated endpoints for divisibility by 2 and by 3 - History endpoint to view all past checks - Database integration with SQLAlchemy and SQLite - Database migrations managed by Alembic @@ -19,7 +20,7 @@ Welcome message for the API. ### GET /divisibility/{number} -Check if a number is divisible by 2 using a path parameter. +Check if a number is divisible by 2 and by 3 using a path parameter. **Parameters:** - `number` (integer): The number to check @@ -28,13 +29,14 @@ Check if a number is divisible by 2 using a path parameter. ```json { "number": 42, - "is_divisible_by_2": true + "is_divisible_by_2": true, + "is_divisible_by_3": false } ``` ### POST /divisibility -Check if a number is divisible by 2 using a JSON request body. +Check if a number is divisible by 2 and by 3 using a JSON request body. **Request Body:** ```json @@ -47,7 +49,44 @@ Check if a number is divisible by 2 using a JSON request body. ```json { "number": 42, - "is_divisible_by_2": true + "is_divisible_by_2": true, + "is_divisible_by_3": false +} +``` + +### GET /divisibility/by3/{number} + +Check if a number is divisible by 3 using a path parameter. + +**Parameters:** +- `number` (integer): The number to check + +**Response:** +```json +{ + "number": 9, + "is_divisible_by_2": false, + "is_divisible_by_3": true +} +``` + +### POST /divisibility/by3 + +Check if a number is divisible by 3 using a JSON request body. + +**Request Body:** +```json +{ + "number": 9 +} +``` + +**Response:** +```json +{ + "number": 9, + "is_divisible_by_2": false, + "is_divisible_by_3": true } ``` @@ -61,14 +100,23 @@ Get the history of all divisibility checks performed. { "number": 42, "is_divisible_by_2": true, + "is_divisible_by_3": false, "id": 1, "created_at": "2025-05-14T12:34:56.789Z" }, { "number": 7, "is_divisible_by_2": false, + "is_divisible_by_3": false, "id": 2, "created_at": "2025-05-14T12:35:01.234Z" + }, + { + "number": 9, + "is_divisible_by_2": false, + "is_divisible_by_3": true, + "id": 3, + "created_at": "2025-05-14T12:36:05.678Z" } ] ``` diff --git a/alembic/env.py b/alembic/env.py index 7ce2189..867a5cd 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -67,7 +67,12 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) + is_sqlite = connection.dialect.name == "sqlite" + context.configure( + connection=connection, + target_metadata=target_metadata, + render_as_batch=is_sqlite, # Enable batch mode for SQLite + ) with context.begin_transaction(): context.run_migrations() diff --git a/alembic/versions/002_add_divisible_by_3.py b/alembic/versions/002_add_divisible_by_3.py new file mode 100644 index 0000000..b5e1f5c --- /dev/null +++ b/alembic/versions/002_add_divisible_by_3.py @@ -0,0 +1,33 @@ +"""Add is_divisible_by_3 column + +Revision ID: 002 +Revises: 001 +Create Date: 2023-05-15 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "002" +down_revision = "001" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add is_divisible_by_3 column with default value of False + with op.batch_alter_table("number_checks") as batch_op: + batch_op.add_column( + sa.Column( + "is_divisible_by_3", sa.Boolean(), nullable=False, server_default="0" + ) + ) + + +def downgrade() -> None: + # Remove is_divisible_by_3 column + with op.batch_alter_table("number_checks") as batch_op: + batch_op.drop_column("is_divisible_by_3") diff --git a/app/api/endpoints.py b/app/api/endpoints.py index 7a29d9c..8c81988 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -25,19 +25,28 @@ def check_divisibility( db: Session = Depends(get_db), ): """ - Check if a number is divisible by 2. + Check if a number is divisible by 2 and by 3. - Returns a JSON response with the number and a boolean indicating if it's divisible by 2. + Returns a JSON response with the number and booleans indicating if it's divisible by 2 and by 3. """ - is_divisible = number % 2 == 0 + is_divisible_by_2 = number % 2 == 0 + is_divisible_by_3 = number % 3 == 0 # Log the check in the database - number_check = NumberCheck(number=number, is_divisible_by_2=is_divisible) + number_check = NumberCheck( + number=number, + is_divisible_by_2=is_divisible_by_2, + is_divisible_by_3=is_divisible_by_3, + ) db.add(number_check) db.commit() db.refresh(number_check) - return {"number": number, "is_divisible_by_2": is_divisible} + return { + "number": number, + "is_divisible_by_2": is_divisible_by_2, + "is_divisible_by_3": is_divisible_by_3, + } @router.post( @@ -47,19 +56,28 @@ def check_divisibility( ) def check_divisibility_post(request: NumberRequest, db: Session = Depends(get_db)): """ - Check if a number is divisible by 2 using POST request. + Check if a number is divisible by 2 and by 3 using POST request. - Returns a JSON response with the number and a boolean indicating if it's divisible by 2. + Returns a JSON response with the number and booleans indicating if it's divisible by 2 and by 3. """ - is_divisible = request.number % 2 == 0 + is_divisible_by_2 = request.number % 2 == 0 + is_divisible_by_3 = request.number % 3 == 0 # Log the check in the database - number_check = NumberCheck(number=request.number, is_divisible_by_2=is_divisible) + number_check = NumberCheck( + number=request.number, + is_divisible_by_2=is_divisible_by_2, + is_divisible_by_3=is_divisible_by_3, + ) db.add(number_check) db.commit() db.refresh(number_check) - return {"number": request.number, "is_divisible_by_2": is_divisible} + return { + "number": request.number, + "is_divisible_by_2": is_divisible_by_2, + "is_divisible_by_3": is_divisible_by_3, + } @router.get( @@ -74,6 +92,71 @@ def get_check_history(db: Session = Depends(get_db)): return db.query(NumberCheck).all() +@router.get( + "/divisibility/by3/{number}", + response_model=DivisibilityResponse, + summary="Check divisibility by 3 (GET)", +) +def check_divisibility_by3( + number: int = PathParam(..., description="The number to check divisibility for"), + db: Session = Depends(get_db), +): + """ + Check if a number is divisible by 3. + + Returns a JSON response with the number and booleans indicating if it's divisible by 2 and by 3. + """ + is_divisible_by_2 = number % 2 == 0 + is_divisible_by_3 = number % 3 == 0 + + # Log the check in the database + number_check = NumberCheck( + number=number, + is_divisible_by_2=is_divisible_by_2, + is_divisible_by_3=is_divisible_by_3, + ) + db.add(number_check) + db.commit() + db.refresh(number_check) + + return { + "number": number, + "is_divisible_by_2": is_divisible_by_2, + "is_divisible_by_3": is_divisible_by_3, + } + + +@router.post( + "/divisibility/by3", + response_model=DivisibilityResponse, + summary="Check divisibility by 3 (POST)", +) +def check_divisibility_by3_post(request: NumberRequest, db: Session = Depends(get_db)): + """ + Check if a number is divisible by 3 using POST request. + + Returns a JSON response with the number and booleans indicating if it's divisible by 2 and by 3. + """ + is_divisible_by_2 = request.number % 2 == 0 + is_divisible_by_3 = request.number % 3 == 0 + + # Log the check in the database + number_check = NumberCheck( + number=request.number, + is_divisible_by_2=is_divisible_by_2, + is_divisible_by_3=is_divisible_by_3, + ) + db.add(number_check) + db.commit() + db.refresh(number_check) + + return { + "number": request.number, + "is_divisible_by_2": is_divisible_by_2, + "is_divisible_by_3": is_divisible_by_3, + } + + @router.get("/health", summary="Health check") def health_check(): """ diff --git a/app/models/number.py b/app/models/number.py index 47e6cc0..1a602ee 100644 --- a/app/models/number.py +++ b/app/models/number.py @@ -9,4 +9,5 @@ class NumberCheck(Base): id = Column(Integer, primary_key=True, index=True) number = Column(Integer, nullable=False) is_divisible_by_2 = Column(Boolean, nullable=False) + is_divisible_by_3 = Column(Boolean, nullable=False, default=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/app/schemas/number.py b/app/schemas/number.py index a09aa4d..3a20706 100644 --- a/app/schemas/number.py +++ b/app/schemas/number.py @@ -9,6 +9,7 @@ class NumberRequest(BaseModel): class DivisibilityResponse(BaseModel): number: int is_divisible_by_2: bool + is_divisible_by_3: bool class NumberCheckResponse(DivisibilityResponse): diff --git a/generate_sample_data.py b/generate_sample_data.py index d9218af..b06fb24 100644 --- a/generate_sample_data.py +++ b/generate_sample_data.py @@ -3,73 +3,81 @@ import requests import time from pathlib import Path + def generate_sample_data(base_url="http://localhost:8000", num_samples=100): """ Generate sample data for the divisibility endpoint by making requests to the API. - + Args: base_url (str): Base URL of the API num_samples (int): Number of sample data points to generate """ - print(f"Generating {num_samples} sample data points for the Number Divisibility API...") - + print( + f"Generating {num_samples} sample data points for the Number Divisibility API..." + ) + # Use both GET and POST endpoints to create diverse sample data for i in range(num_samples): method = "GET" if i % 2 == 0 else "POST" number = random.randint(1, 1000) # Generate a random number between 1 and 1000 - + try: if method == "GET": response = requests.get(f"{base_url}/divisibility/{number}") else: response = requests.post( - f"{base_url}/divisibility", - json={"number": number} + f"{base_url}/divisibility", json={"number": number} ) - + if response.status_code == 200: result = response.json() - print(f"Added: Number {number} is{'' if result['is_divisible_by_2'] else ' not'} divisible by 2") + print( + f"Added: Number {number} is{'' if result['is_divisible_by_2'] else ' not'} divisible by 2" + ) else: - print(f"Error: Failed to add number {number}. Status code: {response.status_code}") - + print( + f"Error: Failed to add number {number}. Status code: {response.status_code}" + ) + # Add a small delay to avoid hammering the API time.sleep(0.1) - + except requests.RequestException as e: - print(f"Error: Failed to connect to API at {base_url}. Make sure the API is running.") + print( + f"Error: Failed to connect to API at {base_url}. Make sure the API is running." + ) print(f"Exception: {e}") return False - + print("\nSample data generation completed.") print(f"Generated {num_samples} data points for the Number Divisibility API.") print(f"You can view the history at {base_url}/history") - + return True + def generate_offline_data(db_path="/app/storage/db/db.sqlite", num_samples=100): """ Generate sample data directly to the database without making API requests. Use this when the API is not running. - + Args: db_path (str): Path to the SQLite database num_samples (int): Number of sample data points to generate """ try: import sqlite3 - from datetime import datetime - + db_file = Path(db_path) - + # Check if database directory exists if not db_file.parent.exists(): db_file.parent.mkdir(parents=True, exist_ok=True) - + # Connect to the database conn = sqlite3.connect(db_path) cursor = conn.cursor() - + # Create table if it doesn't exist cursor.execute(""" CREATE TABLE IF NOT EXISTS number_checks ( @@ -79,47 +87,70 @@ def generate_offline_data(db_path="/app/storage/db/db.sqlite", num_samples=100): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) - - print(f"Generating {num_samples} sample data points directly to the database...") - + + print( + f"Generating {num_samples} sample data points directly to the database..." + ) + # Generate sample data for i in range(num_samples): number = random.randint(1, 1000) is_divisible = number % 2 == 0 - + cursor.execute( "INSERT INTO number_checks (number, is_divisible_by_2) VALUES (?, ?)", - (number, is_divisible) + (number, is_divisible), ) - - print(f"Added: Number {number} is{'' if is_divisible else ' not'} divisible by 2") - + + print( + f"Added: Number {number} is{'' if is_divisible else ' not'} divisible by 2" + ) + # Commit the changes conn.commit() conn.close() - + print("\nSample data generation completed.") print(f"Generated {num_samples} data points directly to the database.") - + return True - + except Exception as e: - print(f"Error: Failed to generate offline data.") + print("Error: Failed to generate offline data.") print(f"Exception: {e}") return False + if __name__ == "__main__": import argparse - - parser = argparse.ArgumentParser(description="Generate sample data for the Number Divisibility API") - parser.add_argument("--samples", type=int, default=100, help="Number of sample data points to generate") - parser.add_argument("--url", type=str, default="http://localhost:8000", help="Base URL of the API") - parser.add_argument("--offline", action="store_true", help="Generate data directly to the database without API requests") - parser.add_argument("--db-path", type=str, default="/app/storage/db/db.sqlite", help="Path to the SQLite database file (for offline mode)") - + + parser = argparse.ArgumentParser( + description="Generate sample data for the Number Divisibility API" + ) + parser.add_argument( + "--samples", + type=int, + default=100, + help="Number of sample data points to generate", + ) + parser.add_argument( + "--url", type=str, default="http://localhost:8000", help="Base URL of the API" + ) + parser.add_argument( + "--offline", + action="store_true", + help="Generate data directly to the database without API requests", + ) + parser.add_argument( + "--db-path", + type=str, + default="/app/storage/db/db.sqlite", + help="Path to the SQLite database file (for offline mode)", + ) + args = parser.parse_args() - + if args.offline: generate_offline_data(db_path=args.db_path, num_samples=args.samples) else: - generate_sample_data(base_url=args.url, num_samples=args.samples) \ No newline at end of file + generate_sample_data(base_url=args.url, num_samples=args.samples)