diff --git a/.env b/.env new file mode 100644 index 0000000..b539844 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangAppAAAAAA" +ITEMS_PER_USER=2 + + +#ENV="development" +#DATABASE_PASSWORD="motDePasseSecret" +#THIRD_API_PRIVATE_KEY="cleSecrete" 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/README.md b/README.md index 1edc188..001a724 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ -# Python_OOP_Project \ No newline at end of file +# FastApiDecoratorBuilder + +"FastApiDecoratorBuilder" est un algorithme permettant de concevoir un décorateur Python qui transforme une fonction Python en une API FastAPI. + +## Installation + +1. Télécharger les fichiers Python +2. Installer l'application FastAPI + $ pip install fastapi +3. Installer le serveur ASGI + $ 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. + +## 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"). + +## 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'. + +1. Power + Description : Calcul de la puissance d'un nombre + 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 + 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 + Paramètres : + - 'x': Nombre à soustraire(integer) + - 'a': Nombre (integer) + - 'b': Nombre (integer) + + Exemple : '/sous/?x=10&a=3&b=2' pour soustraire 3 et 2 à 10 + + + + diff --git a/app.py b/app.py new file mode 100644 index 0000000..f7c7351 --- /dev/null +++ b/app.py @@ -0,0 +1,163 @@ +import asyncio +from typing import Annotated +from functools import lru_cache +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 + + +my_router = APIRouter() +app = FastAPI() + + +@lru_cache +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, type_args): + def decorator(func): + 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(**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="/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) +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)): + return {f"{x} to the power of {a}": int(x)**int(a)} + +@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()]): + 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("/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, + } + + +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.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 +""" +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/decorator.py b/decorator.py new file mode 100644 index 0000000..3e40809 --- /dev/null +++ b/decorator.py @@ -0,0 +1,18 @@ +# In this script we build the decorator and the function we want to transform into an API +from app import app, my_router + + +def fast_api_decorator(route): + def decorator(func): + def wrapper(x, a): + func(x, a) + return func(x, a) + my_router.add_api_route(path='/{func}/{x}/{a}', endpoint=wrapper) + app.include_router(my_router) + return wrapper + return decorator + + +@fast_api_decorator(route="") +def power_function(x, a): + return {f"{x} to the power of {a}": x**a} diff --git a/main.py b/main.py new file mode 100644 index 0000000..1fbe5b0 --- /dev/null +++ b/main.py @@ -0,0 +1,47 @@ +""" +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, rendement +import requests +import asyncio + + +def power(x, a): + print(requests.get(f"http://127.0.0.1:8000/power/?x={x}&a={a}").json()) + + +def add(x, a): + print(requests.get(f"http://127.0.0.1:8000/add/?x={x}&a={a}").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, 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])) 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")