Skip to content

Commit

Permalink
create app
Browse files Browse the repository at this point in the history
  • Loading branch information
Parissai committed Aug 9, 2024
1 parent 2fde12c commit 68416f0
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 0 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WEATHER_API_KEY=api_key_from_https://www.weatherapi.com/
54 changes: 54 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI/CD Pipeline

on:
push:
branches:
- main
pull_request:

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Install Docker
run: |
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- name: Set environment variable
run: echo "WEATHER_API_KEY=${{ secrets.WEATHER_API_KEY }}" >> $GITHUB_ENV

- name: Build and run containers
run: |
docker-compose up -d
env:
WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }}

- name: Run tests
run: |
pytest
- name: Tear down
run: |
docker-compose down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Ignore SQLite database file
weather.db
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.10

# Set working directory
WORKDIR /app

# Copy requirements file
COPY requirements.txt .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . .

# Expose port
EXPOSE 8000

# Command to run the FastAPI application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Empty file added app/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions app/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from sqlalchemy.orm import Session
from datetime import datetime

from app import models, schemas

def get_weather_by_city_and_date(db: Session, city: str, date: datetime):
"""
Retrieve a weather record for a specific city and date from the database.
It returns the first matching record, or `None` if no match is found.
Args:
db (Session): The SQLAlchemy session object used for database operations.
city (str): The name of the city for which to retrieve the weather data.
date (datetime): The specific date for which to retrieve the weather data.
Returns:
models.Weather or None: The weather record as a SQLAlchemy model instance if found,
otherwise `None`.
"""

return db.query(models.Weather).filter(models.Weather.city == city, models.Weather.date == date).first()

def create_weather(db: Session, weather: schemas.WeatherCreate):
"""
Create a new weather record in the database.
This function takes a SQLAlchemy database session and a `WeatherCreate` schema object,
then creates and stores a new weather record in the database. The function commits
the transaction and refreshes the instance to ensure it contains any updates made by the
database (e.g., generated primary keys).
Args:
db (Session): The SQLAlchemy session object used for database operations.
weather (schemas.WeatherCreate): The Pydantic schema object containing the weather data to be saved.
Returns:
models.Weather: The newly created weather record as a SQLAlchemy model instance.
"""

db_weather = models.Weather(**weather.model_dump())
db.add(db_weather)
db.commit()
db.refresh(db_weather)

return db_weather
37 changes: 37 additions & 0 deletions app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./weather.db"

engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def init_db():
Base.metadata.create_all(bind=engine)

def get_db():
"""
Provides a database session for dependency injection in FastAPI.
This generator function yields a database session (`SessionLocal`)
for use in API endpoint functions. The session is automatically
closed after the request is completed, ensuring proper resource management.
Yields:
Session: A SQLAlchemy session object for interacting with the database.
Example:
This function is typically used as a dependency in FastAPI routes:
@app.get("/items/")
def read_items(db: Session = Depends(get_db)):
return db.query(Item).all()
"""

db = SessionLocal()
try:
yield db
finally:
db.close()
73 changes: 73 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from datetime import datetime
from pydantic import BaseModel, field_validator
import re
from app import weather, schemas, database


class WeatherRequest(BaseModel):
city: str
date: str

@field_validator("date")
def validate_date_format(cls, value):
"""
Validate the format of a date string.
This method checks if the provided date string matches the expected format
"YYYY-MM-DD". If the format is invalid, it raises a `ValueError`.
Args:
cls: The class that the validator is being applied to (used for class methods).
value (str): The date string to be validated.
Returns:
str: The original date string if it matches the required format.
Raises:
ValueError: If the date string does not match the format "YYYY-MM-DD",
a `ValueError` is raised with a message indicating the format requirement.
Example:
>>> validate_date_format("2024-08-08")
'2024-08-08'
>>> validate_date_format("08-08-2024")
ValueError: Invalid date format. Use 'YYYY-MM-DD'.
"""
if not re.match(r"^\d{4}-\d{2}-\d{2}$", value):
raise ValueError("Invalid date format. Use 'YYYY-MM-DD'.")
return value

# Initialize the database
database.init_db()

app = FastAPI()

@app.get("/weather/", response_model=schemas.WeatherResponse)
async def get_weather(city: str, date: str, db: Session = Depends(database.get_db)):
"""
Retrieve weather data for a specific city and date.
This function fetches weather data from the database for the specified city and date.
If the data is not found, it raises an HTTP 404 exception.
Args:
city (str): The name of the city for which to retrieve weather data.
date (str): The date for which to retrieve weather data, in the format "YYYY-MM-DD".
db (Session, optional): The database session dependency, automatically injected by FastAPI.
Returns:
dict: A dictionary containing the weather data for the specified city and date.
Raises:
HTTPException: If no weather data is found for the given city and date, a 404 error is raised.
"""

date_obj = datetime.strptime(date, "%Y-%m-%d")
weather_data = await weather.get_weather(db, city, date_obj)
if not weather_data:
raise HTTPException(status_code=404, detail="Weather data not found")

return weather_data
12 changes: 12 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sqlalchemy import Column, Integer, String, Float, DateTime
from app.database import Base

class Weather(Base):
__tablename__ = "weather"
id = Column(Integer, primary_key=True, index=True)
city = Column(String, index=True)
date = Column(DateTime, index=True)
min_temp = Column(Float)
max_temp = Column(Float)
avg_temp = Column(Float)
humidity = Column(Float)
18 changes: 18 additions & 0 deletions app/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pydantic import BaseModel, ConfigDict
from datetime import datetime

class WeatherBase(BaseModel):
city: str
date: datetime
min_temp: float
max_temp: float
avg_temp: float
humidity: float

class WeatherCreate(WeatherBase):
pass

class WeatherResponse(WeatherBase):
id: int

model_config = ConfigDict(from_attributes=True)
95 changes: 95 additions & 0 deletions app/weather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import httpx
from sqlalchemy.orm import Session
import os
from dotenv import load_dotenv
from app import crud, schemas
import logging

# Initialize logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

load_dotenv()

WEATHER_API_URL = "http://api.weatherapi.com/v1/history.json"
API_KEY = os.getenv("WEATHER_API_KEY")

if not API_KEY:
logger.error("Weather API key is not set. Please set WEATHER_API_KEY in the environment variables.")
raise ValueError("Weather API key is missing.")


async def fetch_weather_data(city: str, date: str) -> dict:
"""
Fetch weather data from the external API.
Args:
city (str): The name of the city to fetch weather data for.
date (str): The date to fetch weather data for, in 'YYYY-MM-DD' format.
Returns:
dict: The weather data in JSON format.
Raises:
httpx.HTTPStatusError: If the HTTP request to the weather API fails.
ValueError: If the response does not contain expected data.
"""
async with httpx.AsyncClient() as client:
try:
response = await client.get(WEATHER_API_URL, params={"key": API_KEY, "q": city, "dt": date})
response.raise_for_status()
data = response.json()

# Check if the response has the expected structure
if "forecast" not in data or "forecastday" not in data["forecast"]:
logger.error("Unexpected response structure: %s", data)
raise ValueError("Unexpected response structure from weather API.")

return data
except httpx.HTTPStatusError as e:
logger.error("HTTP request failed: %s", e)
raise
except ValueError as e:
logger.error("Data validation error: %s", e)
raise

async def get_weather(db: Session, city: str, date: str) -> schemas.WeatherResponse:
"""
Retrieve weather data from the database or fetch it from the API if not present.
Args:
db (Session): The database session object.
city (str): The name of the city to get weather data for.
date (str): The date to get weather data for, in 'YYYY-MM-DD' format.
Returns:
schemas.Weather: The weather data object.
Raises:
Exception: If there is an error fetching or storing weather data.
"""
# Fetch weather from the database
weather = crud.get_weather_by_city_and_date(db, city, date)
if weather:
return weather

# Fetch weather from the external API
data = await fetch_weather_data(city, date)
forecast = data["forecast"]["forecastday"][0]["day"]

# Create a weather data object
weather_data = schemas.WeatherCreate(
city=city,
date=date,
min_temp=forecast.get("mintemp_c"),
max_temp=forecast.get("maxtemp_c"),
avg_temp=forecast.get("avgtemp_c"),
humidity=forecast.get("avghumidity")
)

# Store weather data in the database
try:
return crud.create_weather(db, weather_data)
except Exception as e:
logger.error("Error saving weather data to database: %s", e)
raise
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "3.9"

services:
web:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
environment:
- DATABASE_URL=sqlite:///./weather.db
Loading

0 comments on commit 68416f0

Please sign in to comment.