From 7874dba704ae05c7a348ab4d24c6206ccef78d14 Mon Sep 17 00:00:00 2001 From: Spydie78 <61595590+Spydie78@users.noreply.github.com> Date: Sun, 19 Nov 2023 19:44:36 +0100 Subject: [PATCH] Readme et commentaire --- .env | 21 +++++++---- Authentification.py | 59 +++++++++++++++++++++++------ README.md | 81 ++++++++++++++++++++++++++++++++++------ app.py | 90 ++++++++++++++++++++++++++++++++------------- main.py | 4 +- settings.py | 16 ++++---- 6 files changed, 206 insertions(+), 65 deletions(-) diff --git a/.env b/.env index 5842ce8..25f1fc7 100644 --- a/.env +++ b/.env @@ -1,12 +1,17 @@ +# TITLE: The title of the FastAPI application TITLE="FastApi Decorator Builder" -DESCRIPTION="Description de votre API" -ADMIN_EMAIL="deadpool@example.com" -APP_NAME="ChimichangAppAAAAAA" -ITEMS_PER_USER=2 +# DESCRIPTION: A description of the API +DESCRIPTION="FastApi Decorator Builder est un algorithme permettant de concevoir un décorateur Python qui transforme +une fonction Python en une API FastAPI. Les fonctionnalités sont accessible suivant une authentification." +# ADMIN_EMAIL: The email address for the admin user of the application +ADMIN_EMAIL="admin@fastApi.com" +# COMMAND_LOAD: The command to start the FastAPI application using uvicorn with automatic reloading COMMAND_LOAD="python -m uvicorn main:app --reload" +# URL: The base URL of the FastAPI application URL="http://127.0.0.1:8000" -SAVE_COUNT_FILE="saved_count.txt" +# SAVE_COUNT_FILE: The file name to save the count data +SAVE_COUNT_FILE="saved_count.pki" +# HASH_PASSWORD: A fake hashed password +HASH_PASSWORD="fakehashed" + -#ENV="development" -#DATABASE_PASSWORD="motDePasseSecret" -#THIRD_API_PRIVATE_KEY="cleSecrete" \ No newline at end of file diff --git a/Authentification.py b/Authentification.py index 109c8db..db96b92 100644 --- a/Authentification.py +++ b/Authentification.py @@ -4,12 +4,12 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from pydantic import BaseModel - +# Users database with 2 regular users and 1 administrator fake_users_db = { - "john": { - "username": "john", - "full_name": "John Doe", - "email": "johndoe@example.com", + "admin": { + "username": "admin", + "full_name": "admin", + "email": "admin@fastApi.com", "hashed_password": "fakehashedsecret", "disabled": False, "admin": True, @@ -22,16 +22,29 @@ "disabled": True, "admin": False, }, + "bod": { + "username": "bod", + "full_name": "bod Wonderson", + "email": "bod@example.com", + "hashed_password": "fakehashedsecret1", + "disabled": False, + "admin": False, + }, } def fake_hash_password(password: str): + """ + Password hashing function. + :param password: The input password to be hashed. + :return: The hashed password. + """ return "fakehashed" + password - +# OAuth2 password bearer scheme oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - +# Pydantic class to represent the structure of a user class User(BaseModel): username: str email: str = None @@ -45,19 +58,33 @@ class UserInDB(User): def get_user(db, username: str): + """ + Get user from the database. + :param db: The user database. + :param username: The username of the user to retrieve. + :return: An instance of UserInDB if the user exists, otherwise None. + """ if username in db: user_dict = db[username] return UserInDB(**user_dict) def fake_decode_token(token): - # This doesn't provide any security at all - # Check the next version + """ + function to decode a token. + :param token: The token to be decoded. + :return: An instance of UserInDB if the user exists, otherwise None. + """ user = get_user(fake_users_db, token) return user async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + """ + Get the current user based on the provided token. + :param token: The OAuth2 token. + :return: An instance of UserInDB if the user exists, otherwise raise HTTPException. + """ user = fake_decode_token(token) if not user: raise HTTPException( @@ -69,12 +96,22 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): async def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)]): + """ + Get the current active user based on the current user. + :param current_user: The current user. + :return: The current user if active, otherwise raise HTTPException. + """ if current_user.disabled: raise HTTPException(status_code=400, detail="Function not available because you are an Inactive user") return current_user async def get_current_admin_user(current_user: Annotated[User, Depends(get_current_user)]): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Function not available because you are an Active user") + """ + Get the current admin user based on the current user. + :param current_user: The current user. + :return:The current user if an admin, otherwise raise HTTPException. + """ + if not current_user.admin: + raise HTTPException(status_code=400, detail="Function not available because you are not an Administrator user") return current_user diff --git a/README.md b/README.md index bec3cd3..e622831 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# FastApiDecoratorBuilder +# FastApi Decorator Builder -"FastApiDecoratorBuilder" est un algorithme permettant de concevoir un décorateur Python qui transforme -une fonction Python en une API FastAPI. +"FastApi Decorator Builder" est un algorithme permettant de concevoir un décorateur Python qui transforme +une fonction Python en une API FastAPI. Les fonctionnalités sont accessible suivant une authentification. +L'API permet de collecter des données statisques. ## Installation @@ -16,6 +17,7 @@ une fonction Python en une API FastAPI. 6. Un aperçu des requêtes implémentées est disponible à l'adresse suivante : http://127.0.0.1:8000/docs. ## Utilisation +1. Le décorateur Le décorateur 'fast_api_decorator' ajoute une route avec un endpoint correspondant aux paramètres de la requête (paramètres de la fonction). Le décorateur appliqué à une fonction puis exécutée sur le même script de l'instance FastAPI (app) permet de rendre utilisable l'API avec une route qui dépend de la fonction et de ses propres paramètres. @@ -25,33 +27,88 @@ L'API est configurée directement grâce aux paramètres du décorateur avec les les méthodes HTTP (ex: "GET", "POST", "PUT", "DELETE") et une liste de type correspondants aux types des arguments de la fonction décorée. -## Test -Dans le code suivant, il y a trois fonctions qui ont été implémenté dans l'API. Il suffit d'entrer les points de -terminaison suivants après le lien 'http://127.0.0.1:8000'. +2. L'authentification +Cet API utilise une authentification OAuth2 Password Bearer pour assurer la sécurité des endpoints. +Trois types d'utilisateurs sont définis: administrateur (username : "admin" & password : "secret"), utilisateur régulier +(username : "bod" & password : "secret1") et utilisateur inactif (username : "alice" & password : "secret2"). +L'administrateur a accès à toutes les fonctionnalitées proposées par l'API dont les statistiques. +L'utilisateur actif peut utiliser les fonctions mathématiques alors que l'utilisateur inactif ne peut qu'accèder qu'à la +fonctionnalité 'info' de l'API. +Si un token invalide ou si une fonctionnalité réservée à un type d'utilisateur spécifique est renseigné +sans les permissions nécessaires, l'API renverra une erreur : + - 401 Unauthorized: Le token fourni est invalide. + - 400 Bad Request: Vous n'avez pas les permissions nécessaires pour accéder à cette fonctionnalité. + + +3. Suivi des Statistiques +Les informations telles que le nombre d'appels par route, le temps moyen d'exécution par route sont collectées +automatiquement à chaque appel d'API. -> enregistré dans un fichier .pki + +## Fonctionnalités +Dans le code suivant, il y a plusieurs fonctions qui ont été implémenté dans l'API. Certaines fonctions nécéssitent des authentifications. + +### Information API + Info + Description : Obtenir des informations sur l'API + Route: '/info/' + Accès : Pas d'accès spécifique + +### Opérations Mathématiques 1. Power Description : Calcul de la puissance d'un nombre + Route: '/power/' + Accès via : Un token d'authentification Paramètres : - 'x': Nombre (integer) - 'a': La puissance (integer) - - Exemple : '/power/?x=2&a=3' retournera le résultat de 2 à la puissance 3 + 2. Add Description : Calcul l'addition de deux nombres + Route: '/add/' + Accès via : Un token d'authentification Paramètres : - 'x': Nombre (integer) - 'a': Nombre (integer) - - Exemple : '/add/?x=2&a=3' retournera la somme de 2 et 3 + 3. Sous Description : Calcul la soustraction de trois nombres + Route: '/sous/' + Accès via : Un token d'authentification Paramètres : - 'x': Nombre à soustraire (integer) - [a, b] : liste de nombre (List[int]) - - Exemple : '/sous/?x=10&lst=3&lst=2' pour soustraire 3 et 2 à 10 + + +4. Div + Description : Calcul la division d'un nombre + Route: '/div/' + Accès via : Un token d'authentification + Paramètres: + - 'x': Numérateur(integer) + - item: Modèle Pydantic contenant le diviseur + +### Gestion des Utilisateurs + User + Description : Obtenir des informations sur l'utilisateur actuel + Route: '/users/me' + Accès via : Un token d'authentification + +### Obtention d'un token + Token + Description : Obtenir le jeton d'accès + Route: '/token/' + Paramètres: + - 'username': Nom d'utilisateur + - 'password': Mot de passe + +### Statistiques + Stats + Description : Obtenir le jeton d'accès + Route: '/stats/' + Accès via : Un token d'authentification d'administrateur ## Compte rendu du projet diff --git a/app.py b/app.py index 9c1d237..ee2bf61 100644 --- a/app.py +++ b/app.py @@ -1,23 +1,23 @@ -import asyncio -from functools import lru_cache from fastapi import FastAPI, APIRouter, Depends, HTTPException, Query, Request from dotenv import load_dotenv from fastapi.security import OAuth2PasswordRequestForm from settings import Settings from typing import Annotated from Authentification import User, get_current_active_user, fake_users_db, UserInDB, fake_hash_password, \ - get_current_admin_user + get_current_admin_user, get_current_user from pydantic import BaseModel import pickle from collections import Counter import os import time +# Load environment variables from a .env file if present load_dotenv() -settings1 = Settings() +# Create an instance of the Settings class to load environment variables +sett_Env = Settings() my_router = APIRouter() -app = FastAPI(title=settings1.title, description=settings1.description) +app = FastAPI(title=sett_Env.title, description=sett_Env.description) route_request_counter = Counter() route_time_counter = Counter() @@ -86,12 +86,6 @@ def check_args_type(dict_args, type_args): f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}" ) - -@lru_cache -def get_settings(): - return Settings() - - def count_func_call(func): """ Increment the number of call by fonction @@ -125,6 +119,13 @@ def wrapper(**kwargs): def power_function(x: Annotated[int, Query(description="Int we'll compute the power")], a: Annotated[int, Query(description="Power of the calculation")], current_user: User = Depends(get_current_active_user)): + """ + Calculates the power of a given number. + :param x: The base number + :param a: The power to raise the base number to. + :param current_user: The current user. + :return: The result of the power calculation. + """ return {f"{x} to the power of {a}": x ** a} @@ -132,6 +133,13 @@ def power_function(x: Annotated[int, Query(description="Int we'll compute the po def add_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")], current_user: User = Depends(get_current_active_user)): + """ + Adds two numbers. + :param x:The first number. + :param a:The second number to add. + :param current_user: The current user. + :return:The result of the addition. + """ return {f"{x} + {a} equals": x + a} @@ -139,6 +147,13 @@ def add_function(x: Annotated[int, Query(description="Int we'll add something")] def sous_function(x: Annotated[int, Query(description="Int we'll substract something")], lst: Annotated[list[int], Query(description="List of 2 int that will be substracted")], current_user: User = Depends(get_current_active_user)): + """ + Subtracts two numbers. + :param x:The first number. + :param lst: The list containing two numbers to subtract from the first. + :param current_user: The current user. + :return: The result of the subtraction. + """ return {f"{x} - {lst[0]} - {lst[1]} equals": x - lst[0] - lst[1]} @@ -149,12 +164,25 @@ class InputDiv(BaseModel): # Pour faire une requête avec un argument "Body" ou un json avec des arguments il faut passer # par une méthode "POST" et pas "GET" @fast_api_decorator(route="/div/", method=["POST"], type_args=[int, InputDiv]) -def div_function(x: Annotated[int, Query(description="Int we will divide something")], item: InputDiv): +def div_function(x: Annotated[int, Query(description="Int we will divide something")], item: InputDiv, + current_user: User = Depends(get_current_active_user)): + """ + Divides two numbers. + :param x: The numerator. + :param item: The Pydantic model containing the divisor. + :param current_user: The current user. + :return: The result of the division. + """ return {f"{x} / {item.div} equals": item.div} @app.get("/stats") async def get_stats(current_user: User = Depends(get_current_admin_user)): + """ + Get statistics about API usage. + :param current_user: The current admin user. + :return: Statistics including the number of API calls per route and average execution time. + """ avg_time = dict() for key in route_request_counter.keys(): avg_time[key] = route_time_counter[key] * 1000 / route_request_counter[key] @@ -164,19 +192,23 @@ async def get_stats(current_user: User = Depends(get_current_admin_user)): @fast_api_decorator(route="/users/me", method=["GET"], type_args=[]) -def read_users_me(current_user: User = Depends(get_current_active_user)): +def read_users_me(current_user: User = Depends(get_current_user)): + """ + Get information about the current user. + :param current_user: The current user. + :return:Information about the current user. + """ return current_user -@fast_api_decorator(route="/rendement/", method=["POST"], type_args=[int, float]) -def rendement(x: Annotated[int, Query(description="Int we'll add something")], - r: Annotated[float, Query(description="float added")], - current_user: User = Depends(get_current_active_user)): - return {f"{x} * (1 + {r}) equals": int(x) * (1 + float(r))} - - @app.post("/token") async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + """ + Log in and retrieve an access token. + + :param form_data: The OAuth2 password request form containing username and password. + :return: The access token and token type. + """ user_dict = fake_users_db.get(form_data.username) if not user_dict: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -187,18 +219,26 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): return {"access_token": user.username, "token_type": "bearer"} -@app.get("/info") +@fast_api_decorator(route="/info/me", method=["GET"], type_args=[]) async def info(): + """ + Get information about the application. + + :return: Information about the application. + """ return { - "app_name": settings1.app_name, - "admin_email": settings1.admin_email, - "items_per_user": settings1.items_per_user, + "API_name": sett_Env.title, + "API_description": sett_Env.description, + "API_url": sett_Env.url, + "admin_email": sett_Env.admin_email, + "command_to_load": sett_Env.command_load, + } # On "lance" les fonctions pour qu'elles soient visibles par l'app FastAPI read_users_me() -rendement(x=0, r=0.0) +info() power_function(x=0, a=0) add_function(x=0, a=0) sous_function(x=0, lst=[0, 0]) diff --git a/main.py b/main.py index d04719b..ce43683 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -from app import app, power_function, add_function, sous_function, rendement +from app import app, power_function, add_function, sous_function import requests @@ -21,4 +21,4 @@ def sous(x, lst): print(power_function(x=9, a=2)) print(add_function(x=9, a=2)) print(sous_function(x=9, lst=[2, 1])) - print(rendement(x=0, r=0.0)) + diff --git a/settings.py b/settings.py index 179e4ff..bb9f27a 100644 --- a/settings.py +++ b/settings.py @@ -1,10 +1,12 @@ from pydantic_settings import BaseSettings, SettingsConfigDict - +# This Pydantic settings class is used to load the configuration parameters from the .env file class Settings(BaseSettings): - title: str = "FastAPIBuilder" - description: str = "default" - app_name: str = "Awesome" - admin_email: str = "Et" - items_per_user: int = 50 - #model_config = SettingsConfigDict(env_file=".env") + title: str = None + description: str = None + admin_email: str = None + command_load:str = None + url:str = None + save_count_file:str =None + hash_password:str =None +