From b64dbfc655d15073f5f3e28057c9668787b8965f Mon Sep 17 00:00:00 2001 From: Spydie78 <61595590+Spydie78@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:53:46 +0100 Subject: [PATCH 01/20] Authentification implementation --- Authentification.py | 80 +++++++++++++++++++++++++++++++++++++++++++++ app.py | 57 +++++++++++++++++++++++++++----- main.py | 2 +- 3 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 Authentification.py diff --git a/Authentification.py b/Authentification.py new file mode 100644 index 0000000..87a26ff --- /dev/null +++ b/Authentification.py @@ -0,0 +1,80 @@ +from __future__ import annotations +from typing import Annotated +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + + + +fake_users_db = { + "john": { + "username": "john", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str = None + full_name: str = None + disabled: bool = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + 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 + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_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 Inactive user") + return current_user + +async def get_current_inactive_user(current_user: Annotated[User, Depends(get_current_user)]): + if not current_user.disabled: + raise HTTPException(status_code=400, detail="Function not available because you are an Active user") + return current_user + + + diff --git a/app.py b/app.py index d201419..c8df22f 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,14 @@ -from fastapi import FastAPI, APIRouter - +from typing import Annotated +from functools import lru_cache +from fastapi import FastAPI, APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm +from Authentification import User, get_current_active_user, fake_users_db, UserInDB, fake_hash_password, \ + get_current_inactive_user + +from config import Settings +@lru_cache +def get_settings(): + return Settings() my_router = APIRouter() app = FastAPI() @@ -8,6 +17,7 @@ def fast_api_decorator(route, method): def decorator(func): def wrapper(*args, **kwargs): + #if current_user in kwargs.values(): my_router.add_api_route(path=route, endpoint=func, methods=method) app.include_router(my_router) return func(*args, **kwargs) @@ -15,22 +25,53 @@ def wrapper(*args, **kwargs): return decorator -@fast_api_decorator(route="/power/", method=["GET"]) -def power_function(x: str, a: str): - return {f"{x} to the power of {a}": int(x)**int(a)} - @fast_api_decorator(route="/add/", method=["GET"]) def add_function(x: str, a: str): return {f"{x} + {a} equals": int(x) + int(a)} - @fast_api_decorator(route="/sous/", method=["GET"]) def sous_function(x: str, lst): return {f"{x} - {lst[0]} - {lst[1]} equals": int(x) - int(lst[0]) - int(lst[1])} -# On "lance" les fonctions pour qu'elles soient lisibles par l'app FastAPI + +@fast_api_decorator(route="/users/me", method=["GET"]) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + +@fast_api_decorator(route="/power/", method=["POST"]) +async def power_function(x: float, a: float, current_user: User = Depends(get_current_inactive_user)): + return {f"{x} to the power of {a}": float(x)**float(a)} + +@fast_api_decorator(route="/rendement/", method=["POST"]) +async def rendement(x: int, r: float, 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()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + +@app.get("/env") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "default variable": settings.DEFAULT_VAR, + "api key": settings.API_KEY, + "app max integer": settings.APP_MAX, + } + + +# On "lance" les fonctions pour qu'elles soient visibles par l'app FastAPI +read_users_me() +rendement(x="0", r="0") 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 79dc920..d656bd3 100644 --- a/main.py +++ b/main.py @@ -41,4 +41,4 @@ def sous(x, a, b): sous(x=9, a=2, b=1) print(power_function(x="9", a="2")) print(add_function(x="9", a="2")) - print(sous_function(x="9", a="2", b="1")) + print(sous_function(x="9", lst = "[2, 1]")) From 208d1055ed5e1aebbade6d52ac071f686c4f6002 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 12 Nov 2023 19:49:43 +0100 Subject: [PATCH 02/20] handle list, objects and json parameters --- app.py | 53 ++++++++++++++++++++++++++++++++++++++--------------- main.py | 10 +++++----- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/app.py b/app.py index 89f0a58..16c0d78 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,11 @@ -from fastapi import FastAPI, APIRouter -import ast +from fastapi import FastAPI, APIRouter, Query +from typing import Annotated +from pydantic import BaseModel my_router = APIRouter() app = FastAPI() +request_count = 0 def fast_api_decorator(route, method, type_args): @@ -14,6 +16,10 @@ def wrapper(**kwargs): if not isinstance(value, expected_type): raise TypeError(f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}") + # Count the number of request + global request_count + request_count += 1 + # add endpoint to the API my_router.add_api_route(path=route, endpoint=func, methods=method) app.include_router(my_router) @@ -22,27 +28,44 @@ def wrapper(**kwargs): return decorator -@fast_api_decorator(route="/power/", method=["GET"], type_args=[str, str]) -def power_function(x: str, a: str): - return {f"{x} to the power of {a}": int(x)**int(a)} +@fast_api_decorator(route="/power/", method=["GET"], type_args=[int, int]) +def power_function(x: Annotated[int, Query(description="Int we'll compute the power")], a: Annotated[int, Query(description="Power of the calculation")]): + return {f"{x} to the power of {a}": x ** a} -@fast_api_decorator(route="/add/", method=["GET"], type_args=[str, str]) -def add_function(x: str, a: str): - return {f"{x} + {a} equals": int(x) + int(a)} +@fast_api_decorator(route="/add/", method=["GET"], type_args=[int, int]) +def add_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")]): + return {f"{x} + {a} equals": x + a} -@fast_api_decorator(route="/sous/", method=["GET"], type_args=[str, list]) -def sous_function(x, lst): - x = ast.literal_eval(x) - # lst = ast.literal_eval(lst) +@fast_api_decorator(route="/sous/", method=["GET"], type_args=[int, list]) +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")]): return {f"{x} - {lst[0]} - {lst[1]} equals": x - lst[0] - lst[1]} +class InputDiv(BaseModel): + div: int + + +# 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): + return {f"{x} / {item.div} equals": item.div} + + +@app.get("/stats") +async def get_stats(): + global request_count + return {"request_count": request_count} + + # On "lance" les fonctions pour qu'elles soient lisibles par l'app FastAPI -power_function(x="0", a="0") -add_function(x="0", a="0") -sous_function(x="0", lst=[0, 0]) +power_function(x=0, a=0) +add_function(x=0, a=0) +sous_function(x=0, lst=[0, 0]) +input_item = InputDiv(div=10) +div_function(x=100, item=input_item) # résolution pb de lancement des fonctions """ diff --git a/main.py b/main.py index 55c669f..1e6fc04 100644 --- a/main.py +++ b/main.py @@ -32,13 +32,13 @@ def add(x, a): def sous(x, lst): - print(requests.get(f"http://127.0.0.1:8000/sous/?x={x}&lst=[{lst[0]},{lst[1]}]").json()) + print(requests.get(f"http://127.0.0.1:8000/sous/?x={x}&lst={lst[0]}&lst={lst[1]}").json()) if __name__ == "__main__": power(x=9, a=2) add(x=9, a=2) - #sous(x=9, lst=[2, 1]) - print(power_function(x="9", a="2")) - print(add_function(x="9", a="2")) - print(sous_function(x="9", lst=[2, 1])) + sous(x=9, lst=[2, 1]) + print(power_function(x=9, a=2)) + print(add_function(x=9, a=2)) + print(sous_function(x=9, lst=[2, 1])) From 99b6a1e97fa241e50507c53cc18315819e9bc939 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 12 Nov 2023 19:58:40 +0100 Subject: [PATCH 03/20] count in func --- app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.py b/app.py index 16c0d78..1aa0103 100644 --- a/app.py +++ b/app.py @@ -30,6 +30,8 @@ def wrapper(**kwargs): @fast_api_decorator(route="/power/", method=["GET"], type_args=[int, int]) def power_function(x: Annotated[int, Query(description="Int we'll compute the power")], a: Annotated[int, Query(description="Power of the calculation")]): + global request_count + request_count += 1 return {f"{x} to the power of {a}": x ** a} From 0bdd5a92cf498f6ea71ef3fa78201e64406b98d0 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 13 Nov 2023 21:48:56 +0100 Subject: [PATCH 04/20] count ok --- app.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 1aa0103..044b236 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,25 @@ my_router = APIRouter() app = FastAPI() -request_count = 0 + + +def get_saved_value(): + try: + with open("saved_count.txt", "r") as file: + value = int(file.read()) + except FileNotFoundError: + with open("saved_count.txt", 'w') as file: + file.write('0') + value = 0 + return value + + +request_count = get_saved_value() + + +def save_value(value): + with open("saved_count.txt", "w") as file: + file.write(str(value)) def fast_api_decorator(route, method, type_args): @@ -19,6 +37,7 @@ def wrapper(**kwargs): # Count the number of request global request_count request_count += 1 + save_value(request_count) # add endpoint to the API my_router.add_api_route(path=route, endpoint=func, methods=method) @@ -30,8 +49,6 @@ def wrapper(**kwargs): @fast_api_decorator(route="/power/", method=["GET"], type_args=[int, int]) def power_function(x: Annotated[int, Query(description="Int we'll compute the power")], a: Annotated[int, Query(description="Power of the calculation")]): - global request_count - request_count += 1 return {f"{x} to the power of {a}": x ** a} @@ -58,7 +75,7 @@ def div_function(x: Annotated[int, Query(description="Int we will divide somethi @app.get("/stats") async def get_stats(): - global request_count + request_count = get_saved_value() return {"request_count": request_count} From 6ffdd078079d6ae9839e87d1d746372a84e95160 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 17 Nov 2023 12:21:02 +0100 Subject: [PATCH 05/20] readme update --- README.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 001a724..bec3cd3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # FastApiDecoratorBuilder -"FastApiDecoratorBuilder" est un algorithme permettant de concevoir un décorateur Python qui transforme une fonction Python en une API FastAPI. +"FastApiDecoratorBuilder" est un algorithme permettant de concevoir un décorateur Python qui transforme +une fonction Python en une API FastAPI. ## Installation @@ -11,14 +12,22 @@ $ pip install "uvicorn[standard]" 4. Lancer le serveur dans le terminal de l'application Python $ python -m uvicorn main:app --reload -5. Le serveur API démarrera et sera accessible à http://127.0.0.1:8000. +5. Le serveur API démarrera et sera accessible à l'adresse suivante : http://127.0.0.1:8000. +6. Un aperçu des requêtes implémentées est disponible à l'adresse suivante : http://127.0.0.1:8000/docs. ## Utilisation -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 "lancée" sur le même script de l'instance FastAPI (app) permet de requêter l'API avec une route qui dépend de la fonction et de ses propres paramètres. -L'API est configurée directement grâce aux paramètres du décorateur avec les routes ("/power/", "/add/" et "/sous/") et les méthodes HTTP ("GET", "POST", "PUT", "DELETE"). +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. +Ainsi, une fois cette étape réalisée, il est possible de requêter l'API de la fonction à laquelle on a appliqué le +décorateur avec n'importe quels arguments. La réponse de l'API est évidemment l'output de cette fonction. +L'API est configurée directement grâce aux paramètres du décorateur avec les routes (ex: "/power/", "/add/" ou "/sous/"), +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'. +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'. 1. Power Description : Calcul de la puissance d'un nombre @@ -39,12 +48,19 @@ Dans le code suivant, il y a trois fonctions qui ont été implémenté dans l'A 3. Sous Description : Calcul la soustraction de trois nombres Paramètres : - - 'x': Nombre à soustraire(integer) - - 'a': Nombre (integer) - - 'b': Nombre (integer) + - 'x': Nombre à soustraire (integer) + - [a, b] : liste de nombre (List[int]) - Exemple : '/sous/?x=10&a=3&b=2' pour soustraire 3 et 2 à 10 + Exemple : '/sous/?x=10&lst=3&lst=2' pour soustraire 3 et 2 à 10 +## Compte rendu du projet - +Dans FastAPI, les routes doivent être enregistrées auprès de l'instance d'application pour qu'elles soient +accessibles lorsque le serveur est en cours d'exécution. Dans notre code, les routes sont définies à l'aide de +décorateurs qui configurent essentiellement le comportement de routage. Cependant, jusqu'à ce qu'on invoque +les fonctions décorées avec le décorateur @fast_api_decorator, les routes ne sont pas ajoutées à l'application +FastAPI. En effet, lors du lancement de l'application, le programme app.py est exécuté mais les routes ne sont pas +ajoutées tant que le décorateur n'est pas appelé, c'est-à-dire que les fonctions décorées ne sont pas exécutées une +première fois. Il est alors nécessaire d'appeler chaque fonction dans le programme app afin de rendre utilisable +leur route. From ea33057d05c21ee1eb904aae136637d74f081e1e Mon Sep 17 00:00:00 2001 From: Spydie78 <61595590+Spydie78@users.noreply.github.com> Date: Sat, 18 Nov 2023 14:59:08 +0100 Subject: [PATCH 06/20] .env implementation --- .env | 3 +++ app.py | 65 ++++++++++++++++++++++++++++++++++++++++++++--------- settings.py | 6 +++++ 3 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 .env create mode 100644 settings.py diff --git a/.env b/.env new file mode 100644 index 0000000..c42a131 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" + diff --git a/app.py b/app.py index c8df22f..948f9e7 100644 --- a/app.py +++ b/app.py @@ -5,15 +5,65 @@ from Authentification import User, get_current_active_user, fake_users_db, UserInDB, fake_hash_password, \ get_current_inactive_user -from config import Settings + +from settings import Settings + + +my_router = APIRouter() +app = FastAPI() + + @lru_cache def get_settings(): return Settings() -my_router = APIRouter() -app = FastAPI() + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } + + + + +# @app.get("/info") +# async def info(): +# return { +# "app_name": settings.app_name, +# "admin_email": settings.admin_email, +# "items_per_user": settings.items_per_user, +# } + +# class Settings(BaseSettings): +# DEFAULT_VAR: str = "some default string value" # default value if env variable does not exist +# API_KEY: str = "ma key" +# APP_MAX: int = 100 # default value if env variable does not exist +# +# model_config = SettingsConfigDict(env_file=".env") + + + +#def get_settings(): +# return Settings() + + +# async def info(settings: Settings = Depends(get_settings)): +# return { +# "default variable": settings.DEFAULT_VAR, +# "api key": settings.API_KEY, +# "app max integer": settings.APP_MAX, +# } +# +# @app.get("/") +# @app.get("/env") +# async def root(): +# return {"settings": settings} + def fast_api_decorator(route, method): def decorator(func): def wrapper(*args, **kwargs): @@ -60,13 +110,7 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): return {"access_token": user.username, "token_type": "bearer"} -@app.get("/env") -async def info(settings: Annotated[Settings, Depends(get_settings)]): - return { - "default variable": settings.DEFAULT_VAR, - "api key": settings.API_KEY, - "app max integer": settings.APP_MAX, - } + # On "lance" les fonctions pour qu'elles soient visibles par l'app FastAPI @@ -76,6 +120,7 @@ async def info(settings: Annotated[Settings, Depends(get_settings)]): add_function(x="0", a="0") sous_function(x="0", lst=[0, 0]) + # résolution pb de lancement des fonctions """ from fastapi import FastAPI, APIRouter diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..a40fc45 --- /dev/null +++ b/settings.py @@ -0,0 +1,6 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +class Settings(BaseSettings): + app_name: str = "Awesome" + admin_email: str = "Et" + items_per_user: int = 50 + model_config = SettingsConfigDict(env_file=".env") From c6d805d2509628e9b57a0c5f2e42fda9f7f65f3b Mon Sep 17 00:00:00 2001 From: Spydie78 <61595590+Spydie78@users.noreply.github.com> Date: Sat, 18 Nov 2023 15:21:21 +0100 Subject: [PATCH 07/20] .env implementation --- .env | 7 ++++++- app.py | 51 +++++++-------------------------------------------- 2 files changed, 13 insertions(+), 45 deletions(-) diff --git a/.env b/.env index c42a131..b539844 100644 --- a/.env +++ b/.env @@ -1,3 +1,8 @@ ADMIN_EMAIL="deadpool@example.com" -APP_NAME="ChimichangApp" +APP_NAME="ChimichangAppAAAAAA" +ITEMS_PER_USER=2 + +#ENV="development" +#DATABASE_PASSWORD="motDePasseSecret" +#THIRD_API_PRIVATE_KEY="cleSecrete" diff --git a/app.py b/app.py index 948f9e7..ed016c9 100644 --- a/app.py +++ b/app.py @@ -18,51 +18,7 @@ def get_settings(): return Settings() -@app.get("/info") -async def info(settings: Annotated[Settings, Depends(get_settings)]): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } - - - - - - -# @app.get("/info") -# async def info(): -# return { -# "app_name": settings.app_name, -# "admin_email": settings.admin_email, -# "items_per_user": settings.items_per_user, -# } -# class Settings(BaseSettings): -# DEFAULT_VAR: str = "some default string value" # default value if env variable does not exist -# API_KEY: str = "ma key" -# APP_MAX: int = 100 # default value if env variable does not exist -# -# model_config = SettingsConfigDict(env_file=".env") - - - -#def get_settings(): -# return Settings() - - -# async def info(settings: Settings = Depends(get_settings)): -# return { -# "default variable": settings.DEFAULT_VAR, -# "api key": settings.API_KEY, -# "app max integer": settings.APP_MAX, -# } -# -# @app.get("/") -# @app.get("/env") -# async def root(): -# return {"settings": settings} def fast_api_decorator(route, method): def decorator(func): @@ -111,6 +67,13 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): return {"access_token": user.username, "token_type": "bearer"} +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } # On "lance" les fonctions pour qu'elles soient visibles par l'app FastAPI From ed94d684035b0e30a1b80a7e6b2fb74051f742e3 Mon Sep 17 00:00:00 2001 From: Spydie78 <61595590+Spydie78@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:36:12 +0100 Subject: [PATCH 08/20] add Matthieu work --- app.py | 109 ++++++++++++++++++++++++++++++++++++++++++-------------- main.py | 17 +++++---- 2 files changed, 92 insertions(+), 34 deletions(-) diff --git a/app.py b/app.py index ed016c9..59f99e4 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,11 @@ +import asyncio from typing import Annotated from functools import lru_cache -from fastapi import FastAPI, APIRouter, Depends, HTTPException +from fastapi import FastAPI, APIRouter, Depends, HTTPException, Query from fastapi.security import OAuth2PasswordRequestForm from Authentification import User, get_current_active_user, fake_users_db, UserInDB, fake_hash_password, \ get_current_inactive_user - +from pydantic import BaseModel from settings import Settings @@ -17,42 +18,78 @@ def get_settings(): return Settings() +def get_saved_value(): + try: + with open("saved_count.txt", "r") as file: + value = int(file.read()) + except FileNotFoundError: + with open("saved_count.txt", 'w') as file: + file.write('0') + value = 0 + return value + + +request_count = get_saved_value() + +def save_value(value): + with open("saved_count.txt", "w") as file: + file.write(str(value)) -def fast_api_decorator(route, method): +# def fast_api_decorator(route, method): +# def decorator(func): +# def wrapper(*args, **kwargs): +# #if current_user in kwargs.values(): +# my_router.add_api_route(path=route, endpoint=func, methods=method) +# app.include_router(my_router) +# return func(*args, **kwargs) +# return wrapper +# return decorator + +def fast_api_decorator(route, method, type_args): def decorator(func): - def wrapper(*args, **kwargs): - #if current_user in kwargs.values(): + def wrapper(**kwargs): + # Handle argument type error if type_args is not None + if type_args is not None: + for value, expected_type in zip(kwargs.values(), type_args): + if not isinstance(value, expected_type): + raise TypeError(f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}") + + # Count the number of request + global request_count + request_count += 1 + save_value(request_count) + + # add endpoint to the API my_router.add_api_route(path=route, endpoint=func, methods=method) app.include_router(my_router) - return func(*args, **kwargs) + return func(**kwargs) return wrapper return decorator +@fast_api_decorator(route="/add/", method=["GET"], type_args=[int, int]) +def add_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")]): + return {f"{x} + {a} equals": x + a} -@fast_api_decorator(route="/add/", method=["GET"]) -def add_function(x: str, a: str): - return {f"{x} + {a} equals": int(x) + int(a)} - -@fast_api_decorator(route="/sous/", method=["GET"]) -def sous_function(x: str, lst): - return {f"{x} - {lst[0]} - {lst[1]} equals": int(x) - int(lst[0]) - int(lst[1])} +@fast_api_decorator(route="/sous/", method=["GET"], type_args=[int, list]) +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")]): + return {f"{x} - {lst[0]} - {lst[1]} equals": x - lst[0] - lst[1]} -@fast_api_decorator(route="/users/me", method=["GET"]) -async def read_users_me(current_user: User = Depends(get_current_active_user)): - return current_user +@fast_api_decorator(route="/users/me", method=["GET"],type_args=None) +def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user -@fast_api_decorator(route="/power/", method=["POST"]) -async def power_function(x: float, a: float, current_user: User = Depends(get_current_inactive_user)): - return {f"{x} to the power of {a}": float(x)**float(a)} +@fast_api_decorator(route="/power/", method=["POST"],type_args=[int, int]) +def power_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")], current_user: User = Depends(get_current_inactive_user)): + return {f"{x} to the power of {a}": int(x)**int(a)} -@fast_api_decorator(route="/rendement/", method=["POST"]) -async def rendement(x: int, r: float, current_user: User = Depends(get_current_active_user)): - return {f"{x} * (1 + {r}) equals": int(x) * (1 + float(r))} +@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()]): @@ -76,13 +113,31 @@ async def info(settings: Annotated[Settings, Depends(get_settings)]): } +class InputDiv(BaseModel): + div: int + + +# 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): + return {f"{x} / {item.div} equals": item.div} + + +@app.get("/stats") +async def get_stats(): + request_count = get_saved_value() + return {"request_count": request_count} + + # On "lance" les fonctions pour qu'elles soient visibles par l'app FastAPI read_users_me() -rendement(x="0", r="0") -power_function(x="0", a="0") -add_function(x="0", a="0") -sous_function(x="0", lst=[0, 0]) - +rendement(x=0, r=0.0) +power_function(x=0, a=0) +add_function(x=0, a=0) +sous_function(x=0, lst=[0, 0]) +input_item = InputDiv(div=10) +div_function(x=100, item=input_item) # résolution pb de lancement des fonctions """ diff --git a/main.py b/main.py index d656bd3..1fbe5b0 100644 --- a/main.py +++ b/main.py @@ -19,8 +19,9 @@ # Use command : "python -m uvicorn main:app --reload" to lauch server and be able to request the "app" API. -from app import app, power_function, add_function, sous_function +from app import app, power_function, add_function, sous_function, rendement import requests +import asyncio def power(x, a): @@ -31,14 +32,16 @@ def add(x, a): print(requests.get(f"http://127.0.0.1:8000/add/?x={x}&a={a}").json()) -def sous(x, a, b): - print(requests.post(f"http://127.0.0.1:8000/sous/?x={x}&a={a}&b={b}").json()) +def sous(x, lst): + print(requests.get(f"http://127.0.0.1:8000/sous/?x={x}&lst={lst[0]}&lst={lst[1]}").json()) + if __name__ == "__main__": power(x=9, a=2) add(x=9, a=2) - sous(x=9, a=2, b=1) - print(power_function(x="9", a="2")) - print(add_function(x="9", a="2")) - print(sous_function(x="9", lst = "[2, 1]")) + sous(x=9, lst=[2, 1]) + print(rendement(x=0, r=0.0)) + print(power_function(x=9, a=2)) + print(add_function(x=9, a=2)) + print(sous_function(x=9, lst=[2, 1])) From a87b733dd9085f273ce0707d564473c7f21f4807 Mon Sep 17 00:00:00 2001 From: Spydie78 <61595590+Spydie78@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:36:59 +0100 Subject: [PATCH 09/20] delete comments --- app.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app.py b/app.py index 59f99e4..f7c7351 100644 --- a/app.py +++ b/app.py @@ -37,16 +37,6 @@ def save_value(value): file.write(str(value)) -# def fast_api_decorator(route, method): -# def decorator(func): -# def wrapper(*args, **kwargs): -# #if current_user in kwargs.values(): -# my_router.add_api_route(path=route, endpoint=func, methods=method) -# app.include_router(my_router) -# return func(*args, **kwargs) -# return wrapper -# return decorator - def fast_api_decorator(route, method, type_args): def decorator(func): def wrapper(**kwargs): From e232cd0599ad78130b443919997de0915198621a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sat, 18 Nov 2023 18:45:12 +0100 Subject: [PATCH 10/20] indentation et espace --- Authentification.py | 5 +---- app.py | 49 +++++++++------------------------------------ main.py | 1 - settings.py | 2 ++ 4 files changed, 13 insertions(+), 44 deletions(-) diff --git a/Authentification.py b/Authentification.py index 87a26ff..0521ba0 100644 --- a/Authentification.py +++ b/Authentification.py @@ -5,7 +5,6 @@ from pydantic import BaseModel - fake_users_db = { "john": { "username": "john", @@ -71,10 +70,8 @@ async def get_current_active_user(current_user: Annotated[User, Depends(get_curr raise HTTPException(status_code=400, detail="Function not available because you are an Inactive user") return current_user + async def get_current_inactive_user(current_user: Annotated[User, Depends(get_current_user)]): if not current_user.disabled: raise HTTPException(status_code=400, detail="Function not available because you are an Active user") return current_user - - - diff --git a/app.py b/app.py index f7c7351..7409600 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,7 @@ def get_settings(): return Settings() + def get_saved_value(): try: with open("saved_count.txt", "r") as file: @@ -63,23 +64,26 @@ def wrapper(**kwargs): def add_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")]): return {f"{x} + {a} equals": x + a} + @fast_api_decorator(route="/sous/", method=["GET"], type_args=[int, list]) 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")]): return {f"{x} - {lst[0]} - {lst[1]} equals": x - lst[0] - lst[1]} - -@fast_api_decorator(route="/users/me", method=["GET"],type_args=None) +@fast_api_decorator(route="/users/me", method=["GET"], type_args=None) def read_users_me(current_user: User = Depends(get_current_active_user)): - return current_user + return current_user + -@fast_api_decorator(route="/power/", method=["POST"],type_args=[int, int]) +@fast_api_decorator(route="/power/", method=["POST"], type_args=[int, int]) def power_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")], current_user: User = Depends(get_current_inactive_user)): return {f"{x} to the power of {a}": int(x)**int(a)} -@fast_api_decorator(route="/rendement/", method=["POST"],type_args=[int, float]) + +@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))} + return {f"{x} * (1 + {r}) equals": int(x) * (1 + float(r))} + @app.post("/token") async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): @@ -128,36 +132,3 @@ async def get_stats(): sous_function(x=0, lst=[0, 0]) input_item = InputDiv(div=10) div_function(x=100, item=input_item) - -# résolution pb de lancement des fonctions -""" -from fastapi import FastAPI, APIRouter - -app = FastAPI() - -class PowerEndpoint: - router = APIRouter() - - @router.get("/power/") - async def power_function(self, x: str, a: str): - return {f"{x} to the power of {a}": int(x)**int(a)} - -class AddEndpoint: - router = APIRouter() - - @router.get("/add/") - async def add_function(self, x: str, a: str): - return {f"{x} + {a} equals": int(x) + int(a)} - -class SousEndpoint: - router = APIRouter() - - @router.get("/sous/") - async def sous_function(self, x: str, lst): - return {f"{x} - {lst[0]} - {lst[1]} equals": int(x) - int(lst[0]) - int(lst[1])} - -# Including the routers directly in the main app -app.include_router(PowerEndpoint.router, tags=["power"]) -app.include_router(AddEndpoint.router, tags=["add"]) -app.include_router(SousEndpoint.router, tags=["sous"]) -""" \ No newline at end of file diff --git a/main.py b/main.py index 1fbe5b0..5a4c9e5 100644 --- a/main.py +++ b/main.py @@ -36,7 +36,6 @@ def sous(x, lst): print(requests.get(f"http://127.0.0.1:8000/sous/?x={x}&lst={lst[0]}&lst={lst[1]}").json()) - if __name__ == "__main__": power(x=9, a=2) add(x=9, a=2) diff --git a/settings.py b/settings.py index a40fc45..10e85c9 100644 --- a/settings.py +++ b/settings.py @@ -1,4 +1,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict + + class Settings(BaseSettings): app_name: str = "Awesome" admin_email: str = "Et" From 8dfe8db44b9174cc447ee542b009d1bb83f7b657 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sat, 18 Nov 2023 18:47:09 +0100 Subject: [PATCH 11/20] update readme --- README.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 001a724..bec3cd3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # FastApiDecoratorBuilder -"FastApiDecoratorBuilder" est un algorithme permettant de concevoir un décorateur Python qui transforme une fonction Python en une API FastAPI. +"FastApiDecoratorBuilder" est un algorithme permettant de concevoir un décorateur Python qui transforme +une fonction Python en une API FastAPI. ## Installation @@ -11,14 +12,22 @@ $ pip install "uvicorn[standard]" 4. Lancer le serveur dans le terminal de l'application Python $ python -m uvicorn main:app --reload -5. Le serveur API démarrera et sera accessible à http://127.0.0.1:8000. +5. Le serveur API démarrera et sera accessible à l'adresse suivante : http://127.0.0.1:8000. +6. Un aperçu des requêtes implémentées est disponible à l'adresse suivante : http://127.0.0.1:8000/docs. ## Utilisation -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 "lancée" sur le même script de l'instance FastAPI (app) permet de requêter l'API avec une route qui dépend de la fonction et de ses propres paramètres. -L'API est configurée directement grâce aux paramètres du décorateur avec les routes ("/power/", "/add/" et "/sous/") et les méthodes HTTP ("GET", "POST", "PUT", "DELETE"). +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. +Ainsi, une fois cette étape réalisée, il est possible de requêter l'API de la fonction à laquelle on a appliqué le +décorateur avec n'importe quels arguments. La réponse de l'API est évidemment l'output de cette fonction. +L'API est configurée directement grâce aux paramètres du décorateur avec les routes (ex: "/power/", "/add/" ou "/sous/"), +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'. +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'. 1. Power Description : Calcul de la puissance d'un nombre @@ -39,12 +48,19 @@ Dans le code suivant, il y a trois fonctions qui ont été implémenté dans l'A 3. Sous Description : Calcul la soustraction de trois nombres Paramètres : - - 'x': Nombre à soustraire(integer) - - 'a': Nombre (integer) - - 'b': Nombre (integer) + - 'x': Nombre à soustraire (integer) + - [a, b] : liste de nombre (List[int]) - Exemple : '/sous/?x=10&a=3&b=2' pour soustraire 3 et 2 à 10 + Exemple : '/sous/?x=10&lst=3&lst=2' pour soustraire 3 et 2 à 10 +## Compte rendu du projet - +Dans FastAPI, les routes doivent être enregistrées auprès de l'instance d'application pour qu'elles soient +accessibles lorsque le serveur est en cours d'exécution. Dans notre code, les routes sont définies à l'aide de +décorateurs qui configurent essentiellement le comportement de routage. Cependant, jusqu'à ce qu'on invoque +les fonctions décorées avec le décorateur @fast_api_decorator, les routes ne sont pas ajoutées à l'application +FastAPI. En effet, lors du lancement de l'application, le programme app.py est exécuté mais les routes ne sont pas +ajoutées tant que le décorateur n'est pas appelé, c'est-à-dire que les fonctions décorées ne sont pas exécutées une +première fois. Il est alors nécessaire d'appeler chaque fonction dans le programme app afin de rendre utilisable +leur route. From f32fa5ae32114fa6469dcfd6dc45fe09b166df0d Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sat, 18 Nov 2023 23:14:57 +0100 Subject: [PATCH 12/20] finish sur les stats --- app.py | 75 +++++++++++++++++++++------------------------------------ main.py | 21 ---------------- 2 files changed, 27 insertions(+), 69 deletions(-) diff --git a/app.py b/app.py index 044b236..f464aa4 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,8 @@ -from fastapi import FastAPI, APIRouter, Query +from fastapi import FastAPI, APIRouter, Query, Request from typing import Annotated from pydantic import BaseModel - +import pickle +from collections import Counter my_router = APIRouter() app = FastAPI() @@ -9,21 +10,32 @@ def get_saved_value(): try: - with open("saved_count.txt", "r") as file: - value = int(file.read()) + with open("saved_count.pkl", "rb") as file: + values = pickle.load(file) except FileNotFoundError: - with open("saved_count.txt", 'w') as file: - file.write('0') - value = 0 - return value + with open("saved_count.pkl", 'wb') as file: + values = {"power_function": 0, "add_function": 0, "sous_function": 0, "div_function": 0} + pickle.dump(values, file) + return values request_count = get_saved_value() -def save_value(value): - with open("saved_count.txt", "w") as file: - file.write(str(value)) +def save_value(values): + with open("saved_count.pkl", "wb") as file: + pickle.dump(values, file) + + +route_request_counter = Counter() + + +@app.middleware("http") +async def count_requests(request: Request, call_next): + route = request.url.path + route_request_counter[route] += 1 + response = await call_next(request) + return response def fast_api_decorator(route, method, type_args): @@ -35,8 +47,8 @@ def wrapper(**kwargs): raise TypeError(f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}") # Count the number of request - global request_count - request_count += 1 + request_count = get_saved_value() + request_count[func.__name__] += 1 save_value(request_count) # add endpoint to the API @@ -75,8 +87,8 @@ def div_function(x: Annotated[int, Query(description="Int we will divide somethi @app.get("/stats") async def get_stats(): - request_count = get_saved_value() - return {"request_count": request_count} + return {"Nombre d'appels aux fonctions décorées" : get_saved_value(), + "Nombre d'appels totaux des API par route" : route_request_counter} # On "lance" les fonctions pour qu'elles soient lisibles par l'app FastAPI @@ -85,36 +97,3 @@ async def get_stats(): sous_function(x=0, lst=[0, 0]) input_item = InputDiv(div=10) div_function(x=100, item=input_item) - -# résolution pb de lancement des fonctions -""" -from fastapi import FastAPI, APIRouter - -app = FastAPI() - -class PowerEndpoint: - router = APIRouter() - - @router.get("/power/") - async def power_function(self, x: str, a: str): - return {f"{x} to the power of {a}": int(x)**int(a)} - -class AddEndpoint: - router = APIRouter() - - @router.get("/add/") - async def add_function(self, x: str, a: str): - return {f"{x} + {a} equals": int(x) + int(a)} - -class SousEndpoint: - router = APIRouter() - - @router.get("/sous/") - async def sous_function(self, x: str, lst): - return {f"{x} - {lst[0]} - {lst[1]} equals": int(x) - int(lst[0]) - int(lst[1])} - -# Including the routers directly in the main app -app.include_router(PowerEndpoint.router, tags=["power"]) -app.include_router(AddEndpoint.router, tags=["add"]) -app.include_router(SousEndpoint.router, tags=["sous"]) -""" \ No newline at end of file diff --git a/main.py b/main.py index 1e6fc04..cda75a2 100644 --- a/main.py +++ b/main.py @@ -1,24 +1,3 @@ -""" -FastApiDecoratorBuilder - -Description du Projet : -Votre défi dans le projet "FastApiDecoratorBuilder" est de concevoir un décorateur Python qui -transforme une fonction Python en une API FastAPI basée sur la fonction et des configurations -définies. - -Objectifs du Projet : -- Création de Décorateur: Construire un décorateur qui transforme une fonction Python en API FastAPI. -- Gestion de Configurations: Implanter un mécanisme de configuration pour l’API. - -Consignes : -1) Développement du Décorateur : Elaborez un décorateur qui, appliqué à une fonction, génère une API FastAPI -correspondante. -2) Configuration de l'API : Intégrez une méthode pour configurer les propriétés de l’API générée, telles que -les routes et les méthodes HTTP acceptées. -""" - -# Use command : "python -m uvicorn main:app --reload" to lauch server and be able to request the "app" API. - from app import app, power_function, add_function, sous_function import requests From 15cdb61fd07a389af78a25912f02c391e4b1b3a9 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 19 Nov 2023 11:58:25 +0100 Subject: [PATCH 13/20] =?UTF-8?q?finish=20sur=20les=20stats=20de=20compte?= =?UTF-8?q?=20de=20requ=C3=AAtes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 52 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/app.py b/app.py index f464aa4..0fe6d1c 100644 --- a/app.py +++ b/app.py @@ -3,33 +3,37 @@ from pydantic import BaseModel import pickle from collections import Counter +import os my_router = APIRouter() app = FastAPI() +route_request_counter = Counter() + + +@app.on_event("shutdown") +def shutdown_event(): + try: + os.remove('saved_count.pkl') + except Exception as e: + print(f"Error deleting statistics file: {e}") -def get_saved_value(): +def get_saved_values(): try: with open("saved_count.pkl", "rb") as file: values = pickle.load(file) except FileNotFoundError: - with open("saved_count.pkl", 'wb') as file: - values = {"power_function": 0, "add_function": 0, "sous_function": 0, "div_function": 0} + with open('saved_count.pkl', 'wb') as file: + values = dict() pickle.dump(values, file) return values -request_count = get_saved_value() - - def save_value(values): with open("saved_count.pkl", "wb") as file: pickle.dump(values, file) -route_request_counter = Counter() - - @app.middleware("http") async def count_requests(request: Request, call_next): route = request.url.path @@ -38,19 +42,29 @@ async def count_requests(request: Request, call_next): return response +def check_args_type(dict_args, type_args): + for value, expected_type in zip(dict_args.values(), type_args): + if not isinstance(value, expected_type): + raise TypeError(f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}") + + +def count_func_call(func): + request_count = get_saved_values() + key_func = func.__name__ + if key_func in request_count: + request_count[key_func] += 1 + else: + request_count[key_func] = 1 + save_value(request_count) + + def fast_api_decorator(route, method, type_args): def decorator(func): def wrapper(**kwargs): # Handle argument type error - for value, expected_type in zip(kwargs.values(), type_args): - if not isinstance(value, expected_type): - raise TypeError(f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}") - + check_args_type(dict_args=kwargs, type_args=type_args) # Count the number of request - request_count = get_saved_value() - request_count[func.__name__] += 1 - save_value(request_count) - + count_func_call(func=func) # add endpoint to the API my_router.add_api_route(path=route, endpoint=func, methods=method) app.include_router(my_router) @@ -87,8 +101,8 @@ def div_function(x: Annotated[int, Query(description="Int we will divide somethi @app.get("/stats") async def get_stats(): - return {"Nombre d'appels aux fonctions décorées" : get_saved_value(), - "Nombre d'appels totaux des API par route" : route_request_counter} + return {"Nombre d'appels aux fonctions décorées": get_saved_values(), + "Nombre d'appels totaux des API par route": route_request_counter} # On "lance" les fonctions pour qu'elles soient lisibles par l'app FastAPI From b209a7f2d131ec2f5075462268a704c5e20569fc Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 19 Nov 2023 12:27:30 +0100 Subject: [PATCH 14/20] finish temps moyen d'API --- app.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 0fe6d1c..3360f2c 100644 --- a/app.py +++ b/app.py @@ -4,10 +4,12 @@ import pickle from collections import Counter import os +import time my_router = APIRouter() app = FastAPI() route_request_counter = Counter() +route_time_counter = Counter() @app.on_event("shutdown") @@ -38,7 +40,10 @@ def save_value(values): async def count_requests(request: Request, call_next): route = request.url.path route_request_counter[route] += 1 + start_time = time.time() response = await call_next(request) + end_time = time.time() + route_time_counter[route] += end_time - start_time return response @@ -101,8 +106,12 @@ def div_function(x: Annotated[int, Query(description="Int we will divide somethi @app.get("/stats") async def get_stats(): + avg_time = dict() + for key in route_request_counter.keys(): + avg_time[key] = route_time_counter[key] * 1000 / route_request_counter[key] return {"Nombre d'appels aux fonctions décorées": get_saved_values(), - "Nombre d'appels totaux des API par route": route_request_counter} + "Nombre d'appels totaux des API par route": route_request_counter, + "Temps moyen d'exécution par route en ms": avg_time} # On "lance" les fonctions pour qu'elles soient lisibles par l'app FastAPI From 7d4eb216b31acc413d3f5f5caa9b597d82af5905 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 19 Nov 2023 12:52:02 +0100 Subject: [PATCH 15/20] commentaires --- app.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app.py b/app.py index 3360f2c..e88ff6e 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,9 @@ @app.on_event("shutdown") def shutdown_event(): + """ + Remove the statistics file when the app shutdown + """ try: os.remove('saved_count.pkl') except Exception as e: @@ -21,6 +24,10 @@ def shutdown_event(): def get_saved_values(): + """ + Retrieve the statistics data saved in the file + :return: dict with statistics values of all API routes + """ try: with open("saved_count.pkl", "rb") as file: values = pickle.load(file) @@ -32,12 +39,22 @@ def get_saved_values(): def save_value(values): + """ + Save the current API statistics in a file + :param values: dict with statistics values of all API routes + """ with open("saved_count.pkl", "wb") as file: pickle.dump(values, file) @app.middleware("http") async def count_requests(request: Request, call_next): + """ + Increment the number of request by route and calculate the time while processing + :param request: incoming HTTP request + :param call_next: function that represents the next middleware or request handler in the processing pipeline + :return: response of the API request + """ route = request.url.path route_request_counter[route] += 1 start_time = time.time() @@ -48,12 +65,21 @@ async def count_requests(request: Request, call_next): def check_args_type(dict_args, type_args): + """ + Check if args in the dictionary corresponds to the expected type, raise an error if not + :param dict_args: dict of arguments to check + :param type_args: list of expected types + """ for value, expected_type in zip(dict_args.values(), type_args): if not isinstance(value, expected_type): raise TypeError(f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}") def count_func_call(func): + """ + Increment the number of call by fonction + :param func: methode to increment the count + """ request_count = get_saved_values() key_func = func.__name__ if key_func in request_count: From 3199f99ad004e02d1f912acc8da2d142b7fe0cf3 Mon Sep 17 00:00:00 2001 From: Spydie78 <61595590+Spydie78@users.noreply.github.com> Date: Sun, 19 Nov 2023 12:53:38 +0100 Subject: [PATCH 16/20] add .env and admin --- .env | 8 ++++++-- Authentification.py | 7 +++++-- app.py | 28 ++++++++++++++++------------ settings.py | 4 +++- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/.env b/.env index b539844..5842ce8 100644 --- a/.env +++ b/.env @@ -1,8 +1,12 @@ +TITLE="FastApi Decorator Builder" +DESCRIPTION="Description de votre API" ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangAppAAAAAA" ITEMS_PER_USER=2 - +COMMAND_LOAD="python -m uvicorn main:app --reload" +URL="http://127.0.0.1:8000" +SAVE_COUNT_FILE="saved_count.txt" #ENV="development" #DATABASE_PASSWORD="motDePasseSecret" -#THIRD_API_PRIVATE_KEY="cleSecrete" +#THIRD_API_PRIVATE_KEY="cleSecrete" \ No newline at end of file diff --git a/Authentification.py b/Authentification.py index 87a26ff..ad99af3 100644 --- a/Authentification.py +++ b/Authentification.py @@ -13,6 +13,7 @@ "email": "johndoe@example.com", "hashed_password": "fakehashedsecret", "disabled": False, + "admin": True, }, "alice": { "username": "alice", @@ -20,6 +21,7 @@ "email": "alice@example.com", "hashed_password": "fakehashedsecret2", "disabled": True, + "admin": False, }, } @@ -36,6 +38,7 @@ class User(BaseModel): email: str = None full_name: str = None disabled: bool = None + admin: bool = None class UserInDB(User): @@ -71,8 +74,8 @@ async def get_current_active_user(current_user: Annotated[User, Depends(get_curr raise HTTPException(status_code=400, detail="Function not available because you are an Inactive user") return current_user -async def get_current_inactive_user(current_user: Annotated[User, Depends(get_current_user)]): - if not current_user.disabled: +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") return current_user diff --git a/app.py b/app.py index f7c7351..2fbf4d2 100644 --- a/app.py +++ b/app.py @@ -1,17 +1,21 @@ import asyncio from typing import Annotated from functools import lru_cache +from dotenv import load_dotenv from fastapi import FastAPI, APIRouter, Depends, HTTPException, Query from fastapi.security import OAuth2PasswordRequestForm from Authentification import User, get_current_active_user, fake_users_db, UserInDB, fake_hash_password, \ - get_current_inactive_user + get_current_admin_user from pydantic import BaseModel - from settings import Settings +load_dotenv() +settings1 = Settings() my_router = APIRouter() -app = FastAPI() +app = FastAPI(title=settings1.title, + description= settings1.description, + ) @lru_cache @@ -60,11 +64,11 @@ def wrapper(**kwargs): @fast_api_decorator(route="/add/", method=["GET"], type_args=[int, int]) -def add_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")]): +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)): return {f"{x} + {a} equals": x + a} @fast_api_decorator(route="/sous/", method=["GET"], type_args=[int, list]) -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")]): +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)): return {f"{x} - {lst[0]} - {lst[1]} equals": x - lst[0] - lst[1]} @@ -74,7 +78,7 @@ def read_users_me(current_user: User = Depends(get_current_active_user)): return current_user @fast_api_decorator(route="/power/", method=["POST"],type_args=[int, int]) -def power_function(x: Annotated[int, Query(description="Int we'll add something")], a: Annotated[int, Query(description="Int added")], current_user: User = Depends(get_current_inactive_user)): +def power_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)): return {f"{x} to the power of {a}": int(x)**int(a)} @fast_api_decorator(route="/rendement/", method=["POST"],type_args=[int, float]) @@ -95,11 +99,11 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): @app.get("/info") -async def info(settings: Annotated[Settings, Depends(get_settings)]): +async def info(): return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, + "app_name": settings1.app_name, + "admin_email": settings1.admin_email, + "items_per_user": settings1.items_per_user, } @@ -110,12 +114,12 @@ 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)): return {f"{x} / {item.div} equals": item.div} @app.get("/stats") -async def get_stats(): +async def get_stats(current_user: User = Depends(get_current_admin_user)): request_count = get_saved_value() return {"request_count": request_count} diff --git a/settings.py b/settings.py index a40fc45..cc033bd 100644 --- a/settings.py +++ b/settings.py @@ -1,6 +1,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict 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") + #model_config = SettingsConfigDict(env_file=".env") From d2264424a0dc77c86fc67f67f48893d1c32afcc6 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 19 Nov 2023 15:01:31 +0100 Subject: [PATCH 17/20] merge --- app.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app.py b/app.py index c3cd3c5..f9674cc 100644 --- a/app.py +++ b/app.py @@ -1,15 +1,9 @@ import asyncio -from typing import Annotated from functools import lru_cache -from fastapi import FastAPI, APIRouter, Depends, HTTPException, Query +from fastapi import FastAPI, APIRouter, Depends, HTTPException, Query,Request from fastapi.security import OAuth2PasswordRequestForm -from Authentification import User, get_current_active_user, fake_users_db, UserInDB, fake_hash_password, \ - get_current_inactive_user -from pydantic import BaseModel - +from Authentification import User, get_current_active_user, fake_users_db, UserInDB, fake_hash_password, get_current_inactive_user from settings import Settings - -from fastapi import FastAPI, APIRouter, Query, Request from typing import Annotated from pydantic import BaseModel import pickle From c08794b76a0e8587a6fda12f7931a20d05b9943e Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 19 Nov 2023 15:23:30 +0100 Subject: [PATCH 18/20] update --- app.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index e3e4443..0d23ba4 100644 --- a/app.py +++ b/app.py @@ -82,7 +82,9 @@ def check_args_type(dict_args, type_args): """ for value, expected_type in zip(dict_args.values(), type_args): if not isinstance(value, expected_type): - raise TypeError(f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}") + raise TypeError( + f"Type d'argument incorrect. Attendu : {expected_type.__name__}, Reçu : {type(value).__name__}" + ) @lru_cache @@ -120,17 +122,23 @@ def wrapper(**kwargs): @fast_api_decorator(route="/power/", method=["GET"], type_args=[int, int]) -def power_function(x: Annotated[int, Query(description="Int we'll compute the power")], a: Annotated[int, Query(description="Power of the calculation")]): +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)): return {f"{x} to the power of {a}": x ** a} @fast_api_decorator(route="/add/", method=["GET"], type_args=[int, int]) -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)): +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)): return {f"{x} + {a} equals": x + a} @fast_api_decorator(route="/sous/", method=["GET"], type_args=[int, list]) -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)): +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)): return {f"{x} - {lst[0]} - {lst[1]} equals": x - lst[0] - lst[1]} @@ -146,7 +154,7 @@ def div_function(x: Annotated[int, Query(description="Int we will divide somethi @app.get("/stats") -async def get_stats(): +async def get_stats(current_user: User = Depends(get_current_admin_user)): avg_time = dict() for key in route_request_counter.keys(): avg_time[key] = route_time_counter[key] * 1000 / route_request_counter[key] @@ -161,7 +169,9 @@ def read_users_me(current_user: User = Depends(get_current_active_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)): +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))} From d3dd56934e4ad0b3fe7f80acc3850d173ca3516b Mon Sep 17 00:00:00 2001 From: Matthieu Date: Sun, 19 Nov 2023 15:33:56 +0100 Subject: [PATCH 19/20] round ms time --- app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 0d23ba4..9c1d237 100644 --- a/app.py +++ b/app.py @@ -70,7 +70,7 @@ async def count_requests(request: Request, call_next): start_time = time.time() response = await call_next(request) end_time = time.time() - route_time_counter[route] += end_time - start_time + route_time_counter[route] += round(end_time - start_time, 6) return response @@ -163,7 +163,7 @@ async def get_stats(current_user: User = Depends(get_current_admin_user)): "Temps moyen d'exécution par route en ms": avg_time} -@fast_api_decorator(route="/users/me", method=["GET"], type_args=None) +@fast_api_decorator(route="/users/me", method=["GET"], type_args=[]) def read_users_me(current_user: User = Depends(get_current_active_user)): return current_user 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 20/20] 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 +