Add divisibility by 3 functionality

- Add is_divisible_by_3 field to database model and schema
- Create migration script for the new field
- Update existing endpoints to check divisibility by 3
- Add dedicated endpoints for divisibility by 3
- Update README with new endpoint documentation
- Fix linting issues with ruff
This commit is contained in:
Automated Action 2025-05-17 15:52:39 +00:00
parent 1254ceb807
commit 3d02858340
7 changed files with 260 additions and 58 deletions

View File

@ -1,10 +1,11 @@
# Number Divisibility API # 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 ## 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 - History endpoint to view all past checks
- Database integration with SQLAlchemy and SQLite - Database integration with SQLAlchemy and SQLite
- Database migrations managed by Alembic - Database migrations managed by Alembic
@ -19,7 +20,7 @@ Welcome message for the API.
### GET /divisibility/{number} ### 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:** **Parameters:**
- `number` (integer): The number to check - `number` (integer): The number to check
@ -28,13 +29,14 @@ Check if a number is divisible by 2 using a path parameter.
```json ```json
{ {
"number": 42, "number": 42,
"is_divisible_by_2": true "is_divisible_by_2": true,
"is_divisible_by_3": false
} }
``` ```
### POST /divisibility ### 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:** **Request Body:**
```json ```json
@ -47,7 +49,44 @@ Check if a number is divisible by 2 using a JSON request body.
```json ```json
{ {
"number": 42, "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, "number": 42,
"is_divisible_by_2": true, "is_divisible_by_2": true,
"is_divisible_by_3": false,
"id": 1, "id": 1,
"created_at": "2025-05-14T12:34:56.789Z" "created_at": "2025-05-14T12:34:56.789Z"
}, },
{ {
"number": 7, "number": 7,
"is_divisible_by_2": false, "is_divisible_by_2": false,
"is_divisible_by_3": false,
"id": 2, "id": 2,
"created_at": "2025-05-14T12:35:01.234Z" "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"
} }
] ]
``` ```

View File

@ -67,7 +67,12 @@ def run_migrations_online() -> None:
) )
with connectable.connect() as connection: 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(): with context.begin_transaction():
context.run_migrations() context.run_migrations()

View File

@ -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")

View File

@ -25,19 +25,28 @@ def check_divisibility(
db: Session = Depends(get_db), 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 # 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.add(number_check)
db.commit() db.commit()
db.refresh(number_check) 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( @router.post(
@ -47,19 +56,28 @@ def check_divisibility(
) )
def check_divisibility_post(request: NumberRequest, db: Session = Depends(get_db)): 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 # 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.add(number_check)
db.commit() db.commit()
db.refresh(number_check) 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( @router.get(
@ -74,6 +92,71 @@ def get_check_history(db: Session = Depends(get_db)):
return db.query(NumberCheck).all() 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") @router.get("/health", summary="Health check")
def health_check(): def health_check():
""" """

View File

@ -9,4 +9,5 @@ class NumberCheck(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
number = Column(Integer, nullable=False) number = Column(Integer, nullable=False)
is_divisible_by_2 = Column(Boolean, 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()) created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@ -9,6 +9,7 @@ class NumberRequest(BaseModel):
class DivisibilityResponse(BaseModel): class DivisibilityResponse(BaseModel):
number: int number: int
is_divisible_by_2: bool is_divisible_by_2: bool
is_divisible_by_3: bool
class NumberCheckResponse(DivisibilityResponse): class NumberCheckResponse(DivisibilityResponse):

View File

@ -3,6 +3,7 @@ import requests
import time import time
from pathlib import Path from pathlib import Path
def generate_sample_data(base_url="http://localhost:8000", num_samples=100): 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. Generate sample data for the divisibility endpoint by making requests to the API.
@ -11,7 +12,9 @@ def generate_sample_data(base_url="http://localhost:8000", num_samples=100):
base_url (str): Base URL of the API base_url (str): Base URL of the API
num_samples (int): Number of sample data points to generate 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 # Use both GET and POST endpoints to create diverse sample data
for i in range(num_samples): for i in range(num_samples):
@ -23,21 +26,26 @@ def generate_sample_data(base_url="http://localhost:8000", num_samples=100):
response = requests.get(f"{base_url}/divisibility/{number}") response = requests.get(f"{base_url}/divisibility/{number}")
else: else:
response = requests.post( response = requests.post(
f"{base_url}/divisibility", f"{base_url}/divisibility", json={"number": number}
json={"number": number}
) )
if response.status_code == 200: if response.status_code == 200:
result = response.json() 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: 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 # Add a small delay to avoid hammering the API
time.sleep(0.1) time.sleep(0.1)
except requests.RequestException as e: 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}") print(f"Exception: {e}")
return False return False
@ -47,6 +55,7 @@ def generate_sample_data(base_url="http://localhost:8000", num_samples=100):
return True return True
def generate_offline_data(db_path="/app/storage/db/db.sqlite", num_samples=100): 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. Generate sample data directly to the database without making API requests.
@ -58,7 +67,6 @@ def generate_offline_data(db_path="/app/storage/db/db.sqlite", num_samples=100):
""" """
try: try:
import sqlite3 import sqlite3
from datetime import datetime
db_file = Path(db_path) db_file = Path(db_path)
@ -80,7 +88,9 @@ def generate_offline_data(db_path="/app/storage/db/db.sqlite", num_samples=100):
) )
""") """)
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 # Generate sample data
for i in range(num_samples): for i in range(num_samples):
@ -89,10 +99,12 @@ def generate_offline_data(db_path="/app/storage/db/db.sqlite", num_samples=100):
cursor.execute( cursor.execute(
"INSERT INTO number_checks (number, is_divisible_by_2) VALUES (?, ?)", "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 # Commit the changes
conn.commit() conn.commit()
@ -104,18 +116,37 @@ def generate_offline_data(db_path="/app/storage/db/db.sqlite", num_samples=100):
return True return True
except Exception as e: except Exception as e:
print(f"Error: Failed to generate offline data.") print("Error: Failed to generate offline data.")
print(f"Exception: {e}") print(f"Exception: {e}")
return False return False
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
parser = argparse.ArgumentParser(description="Generate sample data for the Number Divisibility API") parser = argparse.ArgumentParser(
parser.add_argument("--samples", type=int, default=100, help="Number of sample data points to generate") description="Generate sample data for the Number Divisibility API"
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(
parser.add_argument("--db-path", type=str, default="/app/storage/db/db.sqlite", help="Path to the SQLite database file (for offline mode)") "--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() args = parser.parse_args()