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,73 +3,81 @@ 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.
Args: Args:
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):
method = "GET" if i % 2 == 0 else "POST" method = "GET" if i % 2 == 0 else "POST"
number = random.randint(1, 1000) # Generate a random number between 1 and 1000 number = random.randint(1, 1000) # Generate a random number between 1 and 1000
try: try:
if method == "GET": if method == "GET":
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
print("\nSample data generation completed.") print("\nSample data generation completed.")
print(f"Generated {num_samples} data points for the Number Divisibility API.") print(f"Generated {num_samples} data points for the Number Divisibility API.")
print(f"You can view the history at {base_url}/history") print(f"You can view the history at {base_url}/history")
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.
Use this when the API is not running. Use this when the API is not running.
Args: Args:
db_path (str): Path to the SQLite database db_path (str): Path to the SQLite database
num_samples (int): Number of sample data points to generate num_samples (int): Number of sample data points to generate
""" """
try: try:
import sqlite3 import sqlite3
from datetime import datetime
db_file = Path(db_path) db_file = Path(db_path)
# Check if database directory exists # Check if database directory exists
if not db_file.parent.exists(): if not db_file.parent.exists():
db_file.parent.mkdir(parents=True, exist_ok=True) db_file.parent.mkdir(parents=True, exist_ok=True)
# Connect to the database # Connect to the database
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
# Create table if it doesn't exist # Create table if it doesn't exist
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS number_checks ( 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 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 # Generate sample data
for i in range(num_samples): for i in range(num_samples):
number = random.randint(1, 1000) number = random.randint(1, 1000)
is_divisible = number % 2 == 0 is_divisible = number % 2 == 0
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()
conn.close() conn.close()
print("\nSample data generation completed.") print("\nSample data generation completed.")
print(f"Generated {num_samples} data points directly to the database.") print(f"Generated {num_samples} data points directly to the database.")
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()
if args.offline: if args.offline:
generate_offline_data(db_path=args.db_path, num_samples=args.samples) generate_offline_data(db_path=args.db_path, num_samples=args.samples)
else: else:
generate_sample_data(base_url=args.url, num_samples=args.samples) generate_sample_data(base_url=args.url, num_samples=args.samples)