-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
445 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
WEATHER_API_KEY=api_key_from_https://www.weatherapi.com/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.