diff --git a/chart/database/server.py b/chart/database/server.py index 6c8934e..60c9328 100644 --- a/chart/database/server.py +++ b/chart/database/server.py @@ -1,9 +1,27 @@ -import asyncpg -import yaml +""" +This module initializes the database connection pool using asyncpg. -config = yaml.safe_load(open("config.yaml", "r")) +It reads database configuration from a YAML file and creates a connection pool +that can be used throughout the application for database operations. +""" + +import asyncpg + +from chart.utils.load_config import load_config + +config = load_config('config.yaml') async def create_pool() -> asyncpg.pool.Pool: + """ + Creates and returns a connection pool for the PostgreSQL database. + + The function uses configuration settings loaded from a YAML file to + establish the connection pool. The pool allows multiple database + connections to be reused efficiently across the application. + + Returns: + asyncpg.pool.Pool: The connection pool object. + """ pool = await asyncpg.create_pool( user=config['database']['user'], password=config['database']['password'], @@ -14,4 +32,4 @@ async def create_pool() -> asyncpg.pool.Pool: max_size=20 ) - return pool \ No newline at end of file + return pool diff --git a/chart/function/create_chart.py b/chart/function/create_chart.py index a376fa4..531abc8 100644 --- a/chart/function/create_chart.py +++ b/chart/function/create_chart.py @@ -1,3 +1,8 @@ +""" +This module provides functionality to generate currency rate charts +based on historical data retrieved from the database. +""" + from datetime import datetime from matplotlib import pyplot as plt @@ -5,7 +10,29 @@ from matplotlib import pyplot as plt from chart.function.gen_unique_name import generate_unique_name from ..database.server import create_pool -async def create_chart(from_currency: str, conv_currency: str, start_date: str, end_date: str) -> (str, None): +async def create_chart( + from_currency: str, + conv_currency: str, + start_date: str, + end_date: str +) -> (str, None): + """ + Generates a line chart of currency rates for a given date range. + + The chart shows the exchange rate trend between `from_currency` and + `conv_currency` within the specified `start_date` and `end_date` range. + The generated chart is saved as a PNG file, and the function returns the + file name. If data is invalid or insufficient, the function returns `None`. + + Args: + from_currency (str): The base currency (e.g., "USD"). + conv_currency (str): The target currency (e.g., "EUR"). + start_date (str): The start date in the format 'YYYY-MM-DD'. + end_date (str): The end date in the format 'YYYY-MM-DD'. + + Returns: + str | None: The name of the saved chart file, or `None` if the operation fails. + """ pool = await create_pool() if not validate_date(start_date) or not validate_date(end_date): @@ -58,6 +85,15 @@ async def create_chart(from_currency: str, conv_currency: str, start_date: str, def validate_date(date_str: str) -> bool: + """ + Validates whether the provided string is a valid date in the format 'YYYY-MM-DD'. + + Args: + date_str (str): The date string to validate. + + Returns: + bool: `True` if the string is a valid date, `False` otherwise. + """ try: datetime.strptime(date_str, '%Y-%m-%d') return True diff --git a/chart/function/gen_unique_name.py b/chart/function/gen_unique_name.py index 3f29e64..5f747d4 100644 --- a/chart/function/gen_unique_name.py +++ b/chart/function/gen_unique_name.py @@ -1,9 +1,23 @@ +""" +This module provides a function to generate a unique name for chart files. +""" import datetime import random import string async def generate_unique_name(currency_pair: str, date: datetime) -> str: + """ + Generates a unique name for a chart file based on the currency pair, + current date, and a random suffix. + + Args: + currency_pair (str): A string representing the currency pair (e.g., "USD_EUR"). + date (datetime.datetime): The current datetime object. + + Returns: + str: A unique name in the format "CURRENCYPAIR_YYYYMMDD_RANDOMSUFFIX". + """ date_str = date.strftime("%Y%m%d") random_suffix = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) unique_name = f"{currency_pair}_{date_str}_{random_suffix}" diff --git a/chart/main.py b/chart/main.py index 0d2eaf4..f2937cc 100644 --- a/chart/main.py +++ b/chart/main.py @@ -1,16 +1,22 @@ +""" +This is the main application file for the chart service using FastAPI. + +The application serves static files, provides endpoints for generating charts, +and integrates with Plausible Analytics for tracking usage. +""" + import os import uvicorn -import yaml - from fastapi import FastAPI from starlette.staticfiles import StaticFiles from chart.middleware.plausible_analytics import PlausibleAnalytics from chart.routes import get_chart, get_chart_period +from chart.utils.load_config import load_config app = FastAPI() -config = yaml.safe_load(open('config.yaml')) +config = load_config('config.yaml') if not os.path.exists('../charts'): os.mkdir('../charts') @@ -23,11 +29,16 @@ app.include_router(get_chart_period.router) if __name__ == '__main__': - uvicorn.run( app, host=config['server']['host'], port=3030, - ssl_keyfile=config['server']['ssl']['private_key'] if config['server']['ssl']['work'] else None, - ssl_certfile=config['server']['ssl']['cert'] if config['server']['ssl']['work'] else None + ssl_keyfile= + config['server']['ssl']['private_key'] + if config['server']['ssl']['work'] + else None, + ssl_certfile= + config['server']['ssl']['cert'] + if config['server']['ssl']['work'] + else None ) diff --git a/chart/middleware/plausible_analytics.py b/chart/middleware/plausible_analytics.py index b4c865b..16a802a 100644 --- a/chart/middleware/plausible_analytics.py +++ b/chart/middleware/plausible_analytics.py @@ -1,12 +1,25 @@ -import httpx -import yaml - -from user_agents import parse as ua_parse +""" +This module provides middleware for integrating Plausible Analytics +into a FastAPI application. +""" from http import HTTPStatus -config = yaml.safe_load(open('../config.yaml')) +import httpx +from user_agents import parse as ua_parse +from chart.utils.load_config import load_config + +config = load_config('config.yaml') + +# pylint: disable=too-few-public-methods class PlausibleAnalytics: + """ + Middleware for sending events to Plausible Analytics. + + This middleware intercepts each incoming request, collects metadata such as + user-agent, request path, and response status, and sends it as an event to + Plausible Analytics. + """ async def __call__(self, request, call_next): response = await call_next(request) @@ -23,8 +36,10 @@ class PlausibleAnalytics: "props": { "method": request.method, "statusCode": response.status_code, - "browser": f"{user_agent_parsed.browser.family} {user_agent_parsed.browser.version_string}", - "os": f"{user_agent_parsed.os.family} {user_agent_parsed.os.version_string}", + "browser": f"{user_agent_parsed.browser.family} " + f"{user_agent_parsed.browser.version_string}", + "os": f"{user_agent_parsed.os.family} " + f"{user_agent_parsed.os.version_string}", "source": request.headers.get('referer', 'direct'), }, } @@ -40,7 +55,12 @@ class PlausibleAnalytics: "User-Agent": request.headers.get('user-agent', 'unknown'), }, ) + except httpx.RequestError as e: + print(f"Request error while sending event to Plausible: {e}") + except httpx.HTTPStatusError as e: + print(f"HTTP status error while sending event to Plausible: {e}") + # pylint: disable=broad-exception-caught except Exception as e: - print(f"Error sending event to Plausible: {e}") + print(f"Unexpected error sending event to Plausible: {e}") return response diff --git a/chart/routes/get_chart.py b/chart/routes/get_chart.py index e471fd2..12c1d66 100644 --- a/chart/routes/get_chart.py +++ b/chart/routes/get_chart.py @@ -1,34 +1,60 @@ +""" +This module contains the route for retrieving a chart based on a given currency pair and date range. +It defines the `/api/getChart/` endpoint that processes requests for generating charts. +""" from fastapi import APIRouter, status, Request, Response +from pydantic import BaseModel from chart.function.create_chart import create_chart from chart.routes.get_chart_period import prepare_chart_response router = APIRouter() +class ChartRequestParams(BaseModel): + """ + A Pydantic model that represents the request parameters for generating a chart. + + This model is used to validate and group the request parameters: + from_currency, conv_currency, start_date, and end_date. + """ + from_currency: str + conv_currency: str + start_date: str + end_date: str + @router.get("/api/getChart/", status_code=status.HTTP_201_CREATED) async def get_chart( response: Response, request: Request, - from_currency: str = None, - conv_currency: str = None, - start_date: str = None, - end_date: str = None, + params: ChartRequestParams ): + """ + Fetches a chart for a given currency pair and date range. - if not from_currency or not conv_currency: + :param response: The response object used for returning the HTTP response. + :param request: The request object containing details about the incoming request. + :param params: Contains the request parameters: + from_currency, conv_currency, start_date, and end_date. + :return: A chart or an error message if the request is invalid. + """ + if not params.from_currency or not params.conv_currency: response.status_code = status.HTTP_400_BAD_REQUEST return { 'status': status.HTTP_400_BAD_REQUEST, 'message': 'The from_currency and conv_currency fields are required.', } - elif not start_date and not end_date: + if not params.start_date or not params.end_date: response.status_code = status.HTTP_400_BAD_REQUEST return { 'status': status.HTTP_400_BAD_REQUEST, 'message': 'The start_date and end_date fields are required.', } - - chart = await create_chart(from_currency, conv_currency, start_date, end_date) + chart = await create_chart( + params.from_currency, + params.conv_currency, + params.start_date, + params.end_date + ) return await prepare_chart_response(response, request, chart) diff --git a/chart/routes/get_chart_period.py b/chart/routes/get_chart_period.py index bc975fb..f59b48b 100644 --- a/chart/routes/get_chart_period.py +++ b/chart/routes/get_chart_period.py @@ -1,5 +1,12 @@ +""" +This module defines the route for fetching a chart for a specific currency pair and period. + +It includes the endpoint `/api/getChart/{period}` which allows users to request a chart for +a given currency pair (from_currency and conv_currency) +over a specified time period (week, month, quarter, or year). +""" from datetime import datetime -import dateutil.relativedelta +from dateutil.relativedelta import relativedelta from fastapi import APIRouter, status, Request, Response @@ -15,7 +22,20 @@ async def get_chart_period( conv_currency: str = None, period: str = None, ): + """ + Fetches a chart for a given currency pair and a specific period. + The period can be one of the following: 'week', 'month', 'quarter', 'year'. + Based on the selected period, it calculates the start date and retrieves the chart data. + + :param response: The response object used to set status and message. + :param request: The request object used to retrieve details of the incoming request. + :param from_currency: The base currency in the pair (e.g., 'USD'). + :param conv_currency: The target currency in the pair (e.g., 'EUR'). + :param period: The time period for which the chart is requested + (e.g., 'week', 'month', 'quarter', 'year'). + :return: A response containing the chart URL or an error message if parameters are invalid. + """ if not from_currency or not conv_currency: response.status_code = status.HTTP_400_BAD_REQUEST return { @@ -39,7 +59,7 @@ async def get_chart_period( years = -1 end_date = datetime.now() - start_date = end_date + dateutil.relativedelta.relativedelta(months=month, days=days, years=years) + start_date = end_date + relativedelta(months=month, days=days, years=years) chart = await create_chart( from_currency, @@ -51,7 +71,22 @@ async def get_chart_period( return await prepare_chart_response(response, request, chart) -async def prepare_chart_response(response: Response, request: Request, chart_name: str): +async def prepare_chart_response( + response: Response, + request: Request, + chart_name: str +): + """ + Prepares the response to return the URL of the generated chart. + + If the chart data is not found, it returns a 404 error with an appropriate message. + Otherwise, it returns a URL to access the chart image. + + :param response: The response object used to set status and message. + :param request: The request object used to retrieve details of the incoming request. + :param chart_name: The name of the generated chart (used to build the URL). + :return: A dictionary with the chart URL or an error message if no chart is found. + """ if not chart_name: response.status_code = status.HTTP_404_NOT_FOUND return {'message': 'No data found.', 'status_code': status.HTTP_404_NOT_FOUND}