commit 26020ecbc1088269ccb583403643b8323797743c Author: Pierre Berthe Date: Thu Apr 11 16:05:53 2024 +0200 Ajouter les fichiers de configuration pour VS Code et le fichier .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f27d21d --- /dev/null +++ b/.gitignore @@ -0,0 +1,199 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# Sqlite database files +*.sqlite3 +*.db + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..61c9825 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.autoImportCompletions": true +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..0e57540 --- /dev/null +++ b/main.py @@ -0,0 +1,140 @@ +from contextlib import asynccontextmanager +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Session, select + +from models import ( + Hero, + HeroCreate, + HeroRead, + HeroReadWithTeam, + HeroUpdate, + Team, + TeamCreate, + TeamRead, + TeamReadWithHeroes, + create_db_and_tables, + get_session, +) +from security import hash_password + + +@asynccontextmanager +async def lifespan(app: FastAPI): + create_db_and_tables() + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + hashed_password = hash_password(hero.password) + extra_data = {"hashed_password": hashed_password} + db_hero = Hero.model_validate(hero, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + return HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + return HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + extra_data = {} + if "password" in hero_data: + password = hero_data["password"] + hashed_password = hash_password(password) + extra_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + return HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} + + +@app.post("/teams/", response_model=TeamRead) +def create_team(*, session: Session = Depends(get_session), team: TeamCreate): + db_team = Team.model_validate(team) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.get("/teams/", response_model=list[TeamRead]) +def read_teams( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + teams = session.exec(select(Team).offset(offset).limit(limit)).all() + return teams + + +@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes) +def read_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + return HTTPException(status_code=404, detail="Team not found") + return team + + +@app.patch("/teams/{team_id}", response_model=TeamRead) +def update_team( + *, session: Session = Depends(get_session), team_id: int, team: TeamCreate +): + db_team = session.get(Team, team_id) + if not db_team: + return HTTPException(status_code=404, detail="Team not found") + team_data = team.model_dump(exclude_unset=True) + db_team.sqlmodel_update(team_data) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.delete("/teams/{team_id}") +def delete_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + return HTTPException(status_code=404, detail="Team not found") + session.delete(team) + session.commit() + return {"ok": True} diff --git a/models.py b/models.py new file mode 100644 index 0000000..76fe029 --- /dev/null +++ b/models.py @@ -0,0 +1,73 @@ +from sqlmodel import Field, Relationship, SQLModel, Session, create_engine + + +class TeamBase(SQLModel): + name: str = Field(index=True) + headquarters: str + + +class Team(TeamBase, table=True): + id: int | None = Field(default=None, primary_key=True) + heroes: list["Hero"] = Relationship(back_populates="team") + + +class TeamCreate(TeamBase): + pass + + +class TeamRead(TeamBase): + name: str | None = None + headquarters: str | None = None + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(index=True, default=None) + team_id: int | None = Field(default=None, foreign_key="team.id") + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + hashed_password: str = Field() + team: Team | None = Relationship(back_populates="heroes") + + +class HeroCreate(HeroBase): + password: str + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + password: str | None = None + team_id: int | None = None + + +class TeamReadWithHeroes(TeamRead): + heroes: list[HeroRead] = [] + + +class HeroReadWithTeam(HeroRead): + team: TeamRead | None = None + + +SQLITE_FILE_NAME = "database.db" +DATABASE_URL = f"sqlite:///{SQLITE_FILE_NAME}" + +connect_args = {"check_same_thread": False} +engine = create_engine(DATABASE_URL, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session diff --git a/security.py b/security.py new file mode 100644 index 0000000..6629f2f --- /dev/null +++ b/security.py @@ -0,0 +1,11 @@ +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["argon2"]) + + +def hash_password(password: str | bytes): + return pwd_context.hash(password) + + +def verify_password(password: str | bytes, hashed_password: str): + return pwd_context.verify(password, hashed_password)