Compare commits
3 commits
b56019c694
...
c84d872927
Author | SHA1 | Date | |
---|---|---|---|
Mael G. | c84d872927 | ||
Mael G. | 89c17187ba | ||
Mael G. | 935b404338 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -0,0 +1,3 @@
|
|||
.venv/
|
||||
data/
|
||||
__pycache__/
|
9
Makefile
Normal file
9
Makefile
Normal file
|
@ -0,0 +1,9 @@
|
|||
.PHONY: test
|
||||
|
||||
test:
|
||||
pytest -v ./athlete/tests
|
||||
pytest -v ./medaille/tests
|
||||
pytest -v ./discipline/tests
|
||||
|
||||
clear_before_rendreTP:
|
||||
rm -rf ./data ./__pycache__ ./athlete/__pycache__ ./medaille/__pycache__ ./discipline/__pycache__
|
56
PROJET.md
56
PROJET.md
|
@ -0,0 +1,56 @@
|
|||
# API JO 2024
|
||||
|
||||
Todo :
|
||||
- API Athletes (in progress), API Medaille, API Discipline
|
||||
- Simple FrontEnd
|
||||
- Dockerize
|
||||
- Data generation script for testing purpose
|
||||
|
||||
Done :
|
||||
- Swagger definitions
|
||||
- Base model (on Athletes) with unitests (pytest)
|
||||
|
||||
## Architecture
|
||||
3 uServices in their respective subfolders:
|
||||
- ``/athlete``: Athletes API
|
||||
- ``/medaille``: Medals API
|
||||
- ``/discipline``: Disciplines API
|
||||
|
||||
A ``swagger.yaml`` definition is available in each uService definition in the api folder.
|
||||
|
||||
The ``data`` folder contains the data used by the API in dev. It is a shared folder in the dev environement.
|
||||
The ``sample`` folder for default values inside the containers. It should be mounted in the docker container in seperated volumes as they shouldn't talk to each other.
|
||||
|
||||
## How to run (dev)
|
||||
Go in the right API folder. Start the flask application
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
cd athlete/
|
||||
python app.py
|
||||
```
|
||||
|
||||
## How to test
|
||||
```bash
|
||||
pytest -v athlete/tests
|
||||
pytest -v discipline/tests
|
||||
pytest -v medaille/tests
|
||||
```
|
||||
|
||||
## How to build the docker images
|
||||
|
||||
You can build using the rootfolder context and the Dockerfile in the subfolder.
|
||||
|
||||
```bash
|
||||
docker build . -f athlete/Dockerfile
|
||||
docker build . -f discipline/Dockerfile
|
||||
docker build . -f medaille/Dockerfile
|
||||
```
|
||||
|
||||
However, the compose file is already set up to build the images for you.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
|
@ -3,9 +3,17 @@ FROM python:alpine3.19
|
|||
WORKDIR /app
|
||||
|
||||
COPY /requirements.txt requirements.txt
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
COPY athlete/ .
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
RUN apk update && apk add --no-cache curl
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=3 CMD curl --fail http://localhost:8000/ping || exit 1
|
||||
|
||||
COPY sample/athletes.json ./data/
|
||||
|
||||
EXPOSE 8000
|
||||
ENV ATHLETE_FILE=data/athletes.json
|
||||
|
||||
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
|
||||
|
|
119
athlete/app.py
119
athlete/app.py
|
@ -1,22 +1,115 @@
|
|||
from flask import Flask, jsonify
|
||||
import athlete
|
||||
from flask import Flask, jsonify, request
|
||||
from pathlib import Path
|
||||
import json
|
||||
from athlete import ListeAthlete, Athlete
|
||||
# noinspection PyUnresolvedReferences
|
||||
from flask_swagger_ui import get_swaggerui_blueprint
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['ATHLETE_FILE'] = os.getenv('ATHLETE_FILE', Path(__file__).parent.parent / 'data' / 'athletes.json')
|
||||
@app.route('/ping', methods=["GET"])
|
||||
def ping():
|
||||
return jsonify({"message": "pong"}), 200
|
||||
@app.route('/', methods=["GET"])
|
||||
def hello_world():
|
||||
return jsonify(athlete.Athlete(
|
||||
id=1,
|
||||
prenom="john",
|
||||
nom="doe",
|
||||
pays="France",
|
||||
sexe="Homme",
|
||||
image="localhost",
|
||||
disciplines=[123],
|
||||
).model_dump())
|
||||
def listeAthlete():
|
||||
"""
|
||||
Renvoie la liste des athlètes
|
||||
"""
|
||||
# Offset / Limit
|
||||
offset = request.args.get('offset', 0)
|
||||
limit = request.args.get('limit', 10)
|
||||
listeAthletes = ListeAthlete()
|
||||
listeAthletes.loadFromJson(app.config['ATHLETE_FILE'])
|
||||
|
||||
if limit != 0:
|
||||
listeAthletes.root = listeAthletes.root[int(offset):int(offset)+int(limit)]
|
||||
else:
|
||||
listeAthletes.root = listeAthletes.root[int(offset):]
|
||||
|
||||
return jsonify(listeAthletes.model_dump()), 200
|
||||
|
||||
@app.route('/<int:id>', methods=["GET"])
|
||||
def getAthlete(id: int):
|
||||
"""
|
||||
Renvoie un athlète par son id
|
||||
"""
|
||||
listeAthletes = ListeAthlete()
|
||||
listeAthletes.loadFromJson(app.config['ATHLETE_FILE'])
|
||||
for athlete in listeAthletes.root:
|
||||
if athlete.id == id:
|
||||
return jsonify(athlete.model_dump()), 200
|
||||
return jsonify({"message": "Athlete introuvable"}), 404
|
||||
@app.route('/<int:id>', methods=["DELETE"])
|
||||
def deleteAthlete(id: int):
|
||||
"""
|
||||
Supprime un athlète par son id
|
||||
"""
|
||||
listeAthletes = ListeAthlete()
|
||||
listeAthletes.loadFromJson(app.config['ATHLETE_FILE'])
|
||||
for athlete in listeAthletes.root:
|
||||
if athlete.id == id:
|
||||
listeAthletes.root.remove(athlete)
|
||||
with open(app.config['ATHLETE_FILE'], 'w') as f:
|
||||
json.dump(listeAthletes.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Athlete supprimé"}), 200
|
||||
return jsonify({"message": "Athlete introuvable"}), 404
|
||||
|
||||
@app.route('/<int:id>', methods=["PUT"])
|
||||
def updateAthlete(id: int):
|
||||
"""
|
||||
Met à jour un athlète par son id
|
||||
"""
|
||||
listeAthletes = ListeAthlete()
|
||||
listeAthletes.loadFromJson(app.config['ATHLETE_FILE'])
|
||||
for athlete in listeAthletes.root:
|
||||
if athlete.id == id:
|
||||
data = json.loads(request.data)
|
||||
for key, value in data.items():
|
||||
setattr(athlete, key, value)
|
||||
with open(app.config['ATHLETE_FILE'], 'w') as f:
|
||||
json.dump(listeAthletes.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Athlete mis à jour"}), 200
|
||||
return jsonify({"message": "Athlete introuvable"}), 404
|
||||
|
||||
@app.route('/<int:id>', methods=["PATCH"])
|
||||
def patchAthlete(id: int):
|
||||
"""
|
||||
Met à jour un athlète par son id
|
||||
"""
|
||||
listeAthletes = ListeAthlete()
|
||||
listeAthletes.loadFromJson(app.config['ATHLETE_FILE'])
|
||||
for athlete in listeAthletes.root:
|
||||
if athlete.id == id:
|
||||
data = json.loads(request.data)
|
||||
data["id"] = athlete.id # On ne peut pas changer l'id
|
||||
for key, value in data.items():
|
||||
if hasattr(athlete, key):
|
||||
setattr(athlete, key, value)
|
||||
with open(app.config['ATHLETE_FILE'], 'w') as f:
|
||||
json.dump(listeAthletes.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Athlete mis à jour"}), 200
|
||||
return jsonify({"message": "Athlete introuvable"}), 404
|
||||
|
||||
@app.route('/', methods=["POST"])
|
||||
def addAthlete():
|
||||
"""
|
||||
Ajoute un athlète
|
||||
"""
|
||||
listeAthletes = ListeAthlete()
|
||||
listeAthletes.loadFromJson(app.config['ATHLETE_FILE'])
|
||||
athlete = Athlete(**json.loads(request.data))
|
||||
athlete.id = max([athlete.id for athlete in listeAthletes.root]) + 1
|
||||
listeAthletes.root.append(athlete)
|
||||
with open(app.config['ATHLETE_FILE'], 'w') as f:
|
||||
json.dump(listeAthletes.model_dump(), f, indent=4)
|
||||
return jsonify(athlete.model_dump()), 200
|
||||
|
||||
swaggerui_blueprint = get_swaggerui_blueprint(
|
||||
"/swagger",
|
||||
"/static/swagger.yaml"
|
||||
)
|
||||
app.register_blueprint(swaggerui_blueprint)
|
||||
|
||||
def create_app():
|
||||
return app
|
||||
|
|
|
@ -1,15 +1,53 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, RootModel
|
||||
from typing import Optional, List
|
||||
import json
|
||||
|
||||
class Athlete(BaseModel):
|
||||
"""
|
||||
Modèle Athlète
|
||||
"""
|
||||
id: Optional[int]
|
||||
id: Optional[int] = 0
|
||||
prenom: str
|
||||
nom: str
|
||||
pays: str
|
||||
sexe: str = "N/A"
|
||||
image: str = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_640.png"
|
||||
disciplines: Optional[list[int]] = None
|
||||
records: Optional[list[int]] = None
|
||||
disciplines: Optional[List[int]] = None
|
||||
records: Optional[List[int]] = None
|
||||
|
||||
def loadFromJsonData(self, data: str):
|
||||
"""
|
||||
Charge les données depuis une chaine json
|
||||
|
||||
:param data: Données json
|
||||
:return: None
|
||||
"""
|
||||
data = json.loads(data)
|
||||
for key, value in data.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
class ListeAthlete(RootModel):
|
||||
root: List[Athlete] = []
|
||||
def loadFromJson(self, path: str):
|
||||
"""
|
||||
Charge les données depuis un fichier json
|
||||
|
||||
:param path: Chemin du fichier json
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
for athlete in data:
|
||||
self.root.append(Athlete(**athlete))
|
||||
except FileNotFoundError:
|
||||
print(f"Le fichier {path} n'existe pas")
|
||||
def loadFromJsonData(self, data: str):
|
||||
"""
|
||||
Charge les données depuis une chaine json
|
||||
:param data: Données json
|
||||
:return: None
|
||||
"""
|
||||
data = json.loads(data)
|
||||
for athlete in data:
|
||||
self.root.append(Athlete(**athlete))
|
|
@ -1,24 +1,27 @@
|
|||
import pytest
|
||||
from ..app.app import create_app
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app import create_app
|
||||
from flask import request
|
||||
|
||||
@pytest.fixture()
|
||||
def app():
|
||||
app = create_app()
|
||||
# Copy the sample to the test folder using python
|
||||
shutil.copy(Path(__file__).parent.parent / 'sample' / 'athletes.json', Path(__file__).parent / 'tests' / 'athletes.json')
|
||||
app.config.update({
|
||||
"TESTING": True,
|
||||
"ATHLETE_FILE": Path(__file__).parent / 'tests' / 'athletes.json'
|
||||
})
|
||||
|
||||
# other setup can go here
|
||||
|
||||
yield app
|
||||
|
||||
# clean up / reset resources here
|
||||
# Remove the file after the test
|
||||
(Path(__file__).parent / 'tests' / 'athletes.json').unlink()
|
||||
|
||||
@pytest.fixture()
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def runner(app):
|
||||
return app.test_cli_runner()
|
|
@ -19,11 +19,13 @@ paths:
|
|||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
description: Le nombre d'éléments à ignorer avant de commencer à collecter l'ensemble de résultats
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
description: Le nombre d'éléments à ignorer
|
||||
summary: Liste l'ensemble des athlètes
|
||||
description: Affiche la liste des athlètes enregistrés sur les J.O. 2024.
|
||||
|
|
BIN
athlete/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc
Normal file
BIN
athlete/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
athlete/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc
Normal file
BIN
athlete/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,38 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"prenom": "Riner",
|
||||
"nom": "Teddy",
|
||||
"pays": "France",
|
||||
"sexe": "H",
|
||||
"image": "https://upload.wikimedia.org/wikipedia/commons/4/4e/Teddy_Riner_2012.jpg",
|
||||
"disciplines": [
|
||||
1
|
||||
],
|
||||
"records": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prenom": "Flessel",
|
||||
"nom": "Laura",
|
||||
"pays": "France",
|
||||
"sexe": "H",
|
||||
"image": "https://plusquedusport.files.wordpress.com/2012/04/124936207sl015_laura_flesse.jpg",
|
||||
"disciplines": [
|
||||
2
|
||||
],
|
||||
"records": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prenom": "Monfils",
|
||||
"nom": "Ga\u00ebl",
|
||||
"pays": "France",
|
||||
"sexe": "H",
|
||||
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/Monfils_RG19_%2813%29_%2848199149362%29.jpg/1200px-Monfils_RG19_%2813%29_%2848199149362%29.jpg",
|
||||
"disciplines": [
|
||||
3
|
||||
],
|
||||
"records": []
|
||||
}
|
||||
]
|
6
athlete/tests/test_delAthlete.py
Normal file
6
athlete/tests/test_delAthlete.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
def test_delAthlete(client):
|
||||
response = client.delete("/1")
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.get("/1")
|
||||
assert response.status_code == 404
|
|
@ -1,8 +1,17 @@
|
|||
import unittest
|
||||
from athlete import Athlete
|
||||
def test_getAthlete(client):
|
||||
response = client.get("/1")
|
||||
# Check if can be mapped to Athlete RootModel object
|
||||
|
||||
class MyTestCase(unittest.TestCase):
|
||||
def test_something(self):
|
||||
self.assertEqual(True, False) # add assertion here
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
athlete = Athlete(
|
||||
id=1,
|
||||
prenom="Teddy",
|
||||
nom="Riner",
|
||||
pays="France",
|
||||
sexe="H",
|
||||
image="https://upload.wikimedia.org/wikipedia/commons/4/4e/Teddy_Riner_2012.jpg",
|
||||
disciplines=[1],
|
||||
records=[],
|
||||
)
|
||||
assert athlete.model_dump() == response.json
|
||||
assert response.status_code == 200
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from athlete import ListeAthlete
|
||||
def test_listeAthlete(client):
|
||||
response = client.get("/")
|
||||
listeAthlete = ListeAthlete()
|
||||
listeAthlete.loadFromJsonData(response.data)
|
||||
assert listeAthlete.root is not None
|
||||
assert len(listeAthlete.root) > 0
|
||||
assert listeAthlete.root[0].prenom is not None
|
||||
assert listeAthlete.root[0].nom is not None
|
||||
assert listeAthlete.root[0].pays is not None
|
||||
assert listeAthlete.root[0].sexe is not None
|
||||
assert listeAthlete.root[0].image is not None
|
||||
assert listeAthlete.root[0].disciplines is not None
|
||||
assert listeAthlete.root[0].records is not None
|
||||
assert listeAthlete.root[0].id is not None
|
||||
assert listeAthlete.root[0].id > 0
|
||||
assert listeAthlete.root[0].prenom != ""
|
||||
assert listeAthlete.root[0].nom != ""
|
||||
assert listeAthlete.root[0].pays != ""
|
||||
assert listeAthlete.root[0].sexe != ""
|
||||
assert listeAthlete.root[0].image != ""
|
||||
assert listeAthlete.root[0].disciplines != []
|
|
@ -1,5 +0,0 @@
|
|||
import pytest
|
||||
|
||||
def test_ping(client):
|
||||
response = client.get("/ping")
|
||||
assert b"{\"message\":\"pong\"}\n" in response.data
|
3
athlete/tests/test_ping_athlete.py
Normal file
3
athlete/tests/test_ping_athlete.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
def test_ping_athlete(client):
|
||||
response = client.get("/ping")
|
||||
assert b"{\"message\":\"pong\"}\n" in response.data
|
19
athlete/tests/test_postAthlete.py
Normal file
19
athlete/tests/test_postAthlete.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from athlete import Athlete
|
||||
def test_postAthlete(client):
|
||||
athlete = Athlete(
|
||||
id=1,
|
||||
prenom="Jean",
|
||||
nom="Dupont",
|
||||
pays="URSS",
|
||||
sexe="H",
|
||||
image="http://demo.example",
|
||||
disciplines=[0],
|
||||
records=[1],
|
||||
)
|
||||
|
||||
response = client.post("/", json=athlete.model_dump())
|
||||
assert response.status_code == 200
|
||||
athlete.id = response.json["id"]
|
||||
response = client.get(f"/{athlete.id}")
|
||||
|
||||
assert response.json == athlete.model_dump()
|
19
discipline/Dockerfile
Normal file
19
discipline/Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
|||
FROM python:alpine3.19
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY /requirements.txt requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY discipline/ .
|
||||
|
||||
RUN apk update && apk add --no-cache curl
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=3 CMD curl --fail http://localhost:8000/ping || exit 1
|
||||
|
||||
COPY sample/disciplines.json ./data/
|
||||
|
||||
EXPOSE 8000
|
||||
ENV DISCIPLINE_FILE=data/disciplines.json
|
||||
|
||||
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
|
122
discipline/app.py
Normal file
122
discipline/app.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from flask import Flask, jsonify, request
|
||||
from pathlib import Path
|
||||
import json
|
||||
from models import Discipline, ListeDiscipline
|
||||
# noinspection PyUnresolvedReferences
|
||||
from flask_swagger_ui import get_swaggerui_blueprint
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['DISCIPLINE_FILE'] = os.getenv('DISCIPLINE_FILE', Path(__file__).parent.parent / 'data' / 'disciplines.json')
|
||||
print(app.config['DISCIPLINE_FILE'])
|
||||
|
||||
@app.route('/ping', methods=["GET"])
|
||||
def ping():
|
||||
return jsonify({"message": "pong"}), 200
|
||||
|
||||
@app.route('/', methods=["GET"])
|
||||
def listeDiscipline():
|
||||
"""
|
||||
Renvoie la liste des disciplines
|
||||
"""
|
||||
# Offset / Limit
|
||||
offset = request.args.get('offset', 0)
|
||||
limit = request.args.get('limit', 10)
|
||||
listeDisciplines = ListeDiscipline()
|
||||
listeDisciplines.loadFromJson(app.config['DISCIPLINE_FILE'])
|
||||
|
||||
if limit != 0:
|
||||
listeDisciplines.root = listeDisciplines.root[int(offset):int(offset)+int(limit)]
|
||||
else:
|
||||
listeDisciplines.root = listeDisciplines.root[int(offset):]
|
||||
|
||||
return jsonify(listeDisciplines.model_dump()), 200
|
||||
|
||||
@app.route('/<int:id>', methods=["GET"])
|
||||
def getDiscipline(id: int):
|
||||
"""
|
||||
Renvoie une discipline par son id
|
||||
"""
|
||||
listeDisciplines = ListeDiscipline()
|
||||
listeDisciplines.loadFromJson(app.config['DISCIPLINE_FILE'])
|
||||
for discipline in listeDisciplines.root:
|
||||
if discipline.id == id:
|
||||
return jsonify(discipline.model_dump()), 200
|
||||
return jsonify({"message": "Discipline introuvable"}), 404
|
||||
|
||||
@app.route('/<int:id>', methods=["DELETE"])
|
||||
def deleteDiscipline(id: int):
|
||||
"""
|
||||
Supprime une discipline par son id
|
||||
"""
|
||||
listeDisciplines = ListeDiscipline()
|
||||
listeDisciplines.loadFromJson(app.config['DISCIPLINE_FILE'])
|
||||
for discipline in listeDisciplines.root:
|
||||
if discipline.id == id:
|
||||
listeDisciplines.root.remove(discipline)
|
||||
with open(app.config['DISCIPLINE_FILE'], 'w') as f:
|
||||
json.dump(listeDisciplines.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Discipline supprimé"}), 200
|
||||
return jsonify({"message": "Discipline introuvable"}), 404
|
||||
|
||||
@app.route('/<int:id>', methods=["PUT"])
|
||||
def updateDiscipline(id: int):
|
||||
"""
|
||||
Met à jour une discipline par son id
|
||||
"""
|
||||
listeDisciplines = ListeDiscipline()
|
||||
listeDisciplines.loadFromJson(app.config['DISCIPLINE_FILE'])
|
||||
for discipline in listeDisciplines.root:
|
||||
if discipline.id == id:
|
||||
data = json.loads(request.data)
|
||||
for key, value in data.items():
|
||||
setattr(discipline, key, value)
|
||||
with open(app.config['DISCIPLINE_FILE'], 'w') as f:
|
||||
json.dump(listeDisciplines.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Discipline mise à jour"}), 200
|
||||
return jsonify({"message": "Discipline introuvable"}), 404
|
||||
|
||||
@app.route('/<int:id>', methods=["PATCH"])
|
||||
def patchDiscipline(id: int):
|
||||
"""
|
||||
Met à jour une discipline par son id
|
||||
"""
|
||||
listeDisciplines = ListeDiscipline()
|
||||
listeDisciplines.loadFromJson(app.config['DISCIPLINE_FILE'])
|
||||
for discipline in listeDisciplines.root:
|
||||
if discipline.id == id:
|
||||
data = json.loads(request.data)
|
||||
data["id"] = discipline.id # On ne peut pas changer l'id
|
||||
for key, value in data.items():
|
||||
if hasattr(discipline, key):
|
||||
setattr(discipline, key, value)
|
||||
with open(app.config['DISCIPLINE_FILE'], 'w') as f:
|
||||
json.dump(listeDisciplines.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Discipline mis à jour"}), 200
|
||||
return jsonify({"message": "Discipline introuvable"}), 404
|
||||
|
||||
@app.route('/', methods=["POST"])
|
||||
def addDiscipline():
|
||||
"""
|
||||
Ajoute une discipline
|
||||
"""
|
||||
listeDisciplines = ListeDiscipline()
|
||||
listeDisciplines.loadFromJson(app.config['DISCIPLINE_FILE'])
|
||||
discipline = Discipline(**json.loads(request.data))
|
||||
discipline.id = max([discipline.id for discipline in listeDisciplines.root]) + 1
|
||||
listeDisciplines.root.append(discipline)
|
||||
with open(app.config['DISCIPLINE_FILE'], 'w') as f:
|
||||
json.dump(listeDisciplines.model_dump(), f, indent=4)
|
||||
return jsonify(discipline.model_dump()), 200
|
||||
|
||||
swaggerui_blueprint = get_swaggerui_blueprint(
|
||||
"/swagger",
|
||||
"/static/swagger.yaml"
|
||||
)
|
||||
app.register_blueprint(swaggerui_blueprint)
|
||||
|
||||
def create_app():
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
26
discipline/conftest.py
Normal file
26
discipline/conftest.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
import pytest
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app import create_app
|
||||
|
||||
@pytest.fixture()
|
||||
def app():
|
||||
app = create_app()
|
||||
# Copy the sample to the test folder using python
|
||||
shutil.copy(Path(__file__).parent.parent / 'sample' / 'disciplines.json', Path(__file__).parent / 'tests' / 'disciplines.json')
|
||||
app.config.update({
|
||||
"TESTING": True,
|
||||
"DISCIPLINE_FILE": Path(__file__).parent / 'tests' / 'disciplines.json'
|
||||
})
|
||||
yield app
|
||||
|
||||
# Remove the file after the test
|
||||
(Path(__file__).parent / 'tests' / 'disciplines.json').unlink()
|
||||
|
||||
@pytest.fixture()
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture()
|
||||
def runner(app):
|
||||
return app.test_cli_runner()
|
50
discipline/models.py
Normal file
50
discipline/models.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from pydantic import BaseModel, RootModel
|
||||
from typing import Optional, List
|
||||
import json
|
||||
|
||||
class Discipline(BaseModel):
|
||||
"""
|
||||
Modèle Discipline
|
||||
"""
|
||||
id: Optional[int] = 0
|
||||
intitule: str
|
||||
type: str
|
||||
description: str
|
||||
logo: str
|
||||
|
||||
def loadFromJsonData(self, data: str):
|
||||
"""
|
||||
Charge les données depuis une chaine json
|
||||
|
||||
:param data: Données json
|
||||
:return: None
|
||||
"""
|
||||
data = json.loads(data)
|
||||
for key, value in data.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
class ListeDiscipline(RootModel):
|
||||
root: List[Discipline] = []
|
||||
def loadFromJson(self, path: str):
|
||||
"""
|
||||
Charge les données depuis un fichier json
|
||||
|
||||
:param path: Chemin du fichier json
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
for discipline in data:
|
||||
self.root.append(Discipline(**discipline))
|
||||
except FileNotFoundError:
|
||||
print(f"Le fichier {path} n'existe pas")
|
||||
def loadFromJsonData(self, data: str):
|
||||
"""
|
||||
Charge les données depuis une chaine json
|
||||
:param data: Données json
|
||||
:return: None
|
||||
"""
|
||||
data = json.loads(data)
|
||||
for discipline in data:
|
||||
self.root.append(Discipline(**discipline))
|
133
discipline/static/swagger.yaml
Normal file
133
discipline/static/swagger.yaml
Normal file
|
@ -0,0 +1,133 @@
|
|||
openapi: 3.0.1
|
||||
info:
|
||||
title: API Disciplines des J.O. 2024
|
||||
description: |-
|
||||
Cette API uService sert à afficher les informations sur les Disciplines des JO 2024.
|
||||
version: 0.0.1
|
||||
servers:
|
||||
- url: /disciplines/
|
||||
tags:
|
||||
- name: discipline
|
||||
description: discipline des J.O.
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
tags:
|
||||
- discipline
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
description: Le nombre d'éléments à ignorer avant de commencer à collecter l'ensemble de résultats
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
description: Le nombre d'éléments à ignorer
|
||||
summary: Liste l'ensemble des disciplines
|
||||
description: Affiche la liste des disciplines enregistrés sur les J.O. 2024.
|
||||
operationId: listedisciplines
|
||||
responses:
|
||||
'200':
|
||||
description: discipline
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/discipline'
|
||||
post:
|
||||
tags:
|
||||
- discipline
|
||||
summary: Créer une discipline
|
||||
operationId: createAthelte
|
||||
requestBody:
|
||||
description: Objet discipline a créer
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/discipline'
|
||||
responses:
|
||||
default:
|
||||
description: Opération avec succès
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/discipline'
|
||||
/{id}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: ID de l'discipline à récupérer
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
get:
|
||||
tags:
|
||||
- discipline
|
||||
summary: Récupération d'une discipline selon son id
|
||||
operationId: getdiscipline
|
||||
responses:
|
||||
'200':
|
||||
description: Opération avec succès
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/discipline'
|
||||
'400':
|
||||
description: ID donné invalide
|
||||
'404':
|
||||
description: discipline introuvable
|
||||
patch:
|
||||
tags:
|
||||
- discipline
|
||||
summary: Mettre à jour une discipline
|
||||
operationId: updatediscipline
|
||||
requestBody:
|
||||
description: Mettre à jour une discipline existant
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/discipline'
|
||||
responses:
|
||||
default:
|
||||
description: Opération avec succès
|
||||
delete:
|
||||
tags:
|
||||
- discipline
|
||||
summary: Supprimer une discipline
|
||||
operationId: deletediscipline
|
||||
responses:
|
||||
'400':
|
||||
description: ID donné invalide
|
||||
'404':
|
||||
description: discipline introuvable
|
||||
components:
|
||||
schemas:
|
||||
discipline:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: uuid
|
||||
example: 123456789
|
||||
intitule:
|
||||
type: string
|
||||
example: Judo
|
||||
type:
|
||||
type: string
|
||||
example: terrestre
|
||||
description:
|
||||
type: string
|
||||
example: Le judo est un sport de combat issu du pays du soleil levant
|
||||
logo:
|
||||
type: string
|
||||
example: https://olympics.com/images/static/sports/pictograms/v2/kte.svg
|
||||
requestBodies:
|
||||
User:
|
||||
description: Objet discipline à ajouter
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/discipline'
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6
discipline/tests/test_delDiscipline.py
Normal file
6
discipline/tests/test_delDiscipline.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
def test_delDiscipline(client):
|
||||
response = client.delete("/1")
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.get("/1")
|
||||
assert response.status_code == 404
|
13
discipline/tests/test_getDiscipline.py
Normal file
13
discipline/tests/test_getDiscipline.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from models import Discipline
|
||||
|
||||
def test_getDiscipline(client):
|
||||
response = client.get("/1")
|
||||
discipline = Discipline(
|
||||
id=1,
|
||||
intitule="Judo",
|
||||
type="Combat",
|
||||
description="Le judo est un art martial japonais, fondé par Jigoro Kano en 1882. Il se compose pour l'essentiel de techniques de projection, de contrôle au sol, d'étranglements et de clefs.",
|
||||
logo="https://upload.wikimedia.org/wikipedia/commons/4/4b/Judo_pictogram.svg"
|
||||
)
|
||||
assert discipline.model_dump() == response.json
|
||||
assert response.status_code == 200
|
9
discipline/tests/test_listeDiscipline.py
Normal file
9
discipline/tests/test_listeDiscipline.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from models import ListeDiscipline
|
||||
def test_listeDiscipline(client):
|
||||
response = client.get("/")
|
||||
listeDiscipline = ListeDiscipline()
|
||||
listeDiscipline.loadFromJsonData(response.data)
|
||||
assert listeDiscipline.root is not None
|
||||
assert len(listeDiscipline.root) > 0
|
||||
assert listeDiscipline.root[0].id == 1
|
||||
assert listeDiscipline.root[0].intitule is not None
|
3
discipline/tests/test_ping_discipline.py
Normal file
3
discipline/tests/test_ping_discipline.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
def test_ping_discipline(client):
|
||||
response = client.get("/ping")
|
||||
assert b"{\"message\":\"pong\"}\n" in response.data
|
17
discipline/tests/test_postDiscipline.py
Normal file
17
discipline/tests/test_postDiscipline.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from models import Discipline
|
||||
|
||||
def test_postDiscipline(client):
|
||||
discipline = Discipline(
|
||||
id=1,
|
||||
intitule="Karaté",
|
||||
type="Combat",
|
||||
description="Le karaté est un art martial d'origine japonaise, dont la pratique est à la fois un sport, un moyen de self-défense et un art de vivre.",
|
||||
logo="https://upload.wikimedia.org/wikipedia/commons/4/4b/Karate_pictogram.svg"
|
||||
)
|
||||
|
||||
response = client.post("/", json=discipline.model_dump())
|
||||
assert response.status_code == 200
|
||||
discipline.id = response.json["id"]
|
||||
response = client.get(f"/{discipline.id}")
|
||||
|
||||
assert response.json == discipline.model_dump()
|
|
@ -0,0 +1,46 @@
|
|||
version: "3.3"
|
||||
|
||||
services:
|
||||
|
||||
traefik:
|
||||
image: "traefik:v2.11"
|
||||
container_name: "proxy"
|
||||
command:
|
||||
- "--log.level=DEBUG"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:8082"
|
||||
ports:
|
||||
- "8082:8082"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
|
||||
athlete:
|
||||
image: "jo2024/athlete"
|
||||
container_name: "athlete-service"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./athlete/Dockerfile
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.joueurs-service.entrypoints=web"
|
||||
- "traefik.http.routers.joueurs.rule=PathPrefix(`/joueurs{regex:$$|/.*}`)"
|
||||
- "traefik.http.routers.joueurs.middlewares=joueurs-stripprefix"
|
||||
- "traefik.http.middlewares.joueurs-stripprefix.stripprefix.prefixes=/joueurs"
|
||||
volumes:
|
||||
- "athlete:/app/data"
|
||||
|
||||
discipline:
|
||||
image: "jo2024/discipline"
|
||||
container_name: "discipline-service"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./discipline/Dockerfile
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.disciplines-service.entrypoints=web"
|
||||
- "traefik.http.routers.disciplines.rule=PathPrefix(`/disciplines{regex:$$|/.*}`)"
|
||||
- "traefik.http.routers.disciplines.middlewares=disciplines-stripprefix"
|
||||
- "traefik.http.middlewares.disciplines-stripprefix.stripprefix.prefixes=/disciplines"
|
||||
volumes:
|
||||
- "athlete:/app/data"
|
19
medaille/Dockerfile
Normal file
19
medaille/Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
|||
FROM python:alpine3.19
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY /requirements.txt requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY medaille/ .
|
||||
|
||||
RUN apk update && apk add --no-cache curl
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=3 CMD curl --fail http://localhost:8000/ping || exit 1
|
||||
|
||||
COPY sample/medailles.json ./data/
|
||||
|
||||
EXPOSE 8000
|
||||
ENV MEDAILLE_FILE=data/medailles.json
|
||||
|
||||
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
|
121
medaille/app.py
Normal file
121
medaille/app.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
from flask import Flask, jsonify, request
|
||||
from pathlib import Path
|
||||
import json
|
||||
from models import ListeMedaille, Medaille
|
||||
from flask_swagger_ui import get_swaggerui_blueprint
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['MEDAILLE_FILE'] = os.getenv('MEDAILLE_FILE', Path(__file__).parent.parent / 'data' / 'medailles.json')
|
||||
@app.route('/ping', methods=["GET"])
|
||||
def ping():
|
||||
return jsonify({"message": "pong"}), 200
|
||||
@app.route('/', methods=["GET"])
|
||||
def listeMedaille():
|
||||
"""
|
||||
Renvoie la liste des médilles
|
||||
"""
|
||||
# Offset / Limit
|
||||
offset = request.args.get('offset', 0)
|
||||
limit = request.args.get('limit', 10)
|
||||
listeMedailles = ListeMedaille()
|
||||
listeMedailles.loadFromJson(app.config['MEDAILLE_FILE'])
|
||||
|
||||
if limit != 0:
|
||||
listeMedailles.root = listeMedailles.root[int(offset):int(offset)+int(limit)]
|
||||
else:
|
||||
listeMedailles.root = listeMedailles.root[int(offset):]
|
||||
|
||||
return jsonify(listeMedailles.model_dump()), 200
|
||||
|
||||
@app.route('/<int:id>', methods=["GET"])
|
||||
def getMedaille(id: int):
|
||||
"""
|
||||
Renvoie un médille par son id
|
||||
"""
|
||||
listeMedailles = ListeMedaille()
|
||||
listeMedailles.loadFromJson(app.config['MEDAILLE_FILE'])
|
||||
for medaille in listeMedailles.root:
|
||||
if medaille.id == id:
|
||||
return jsonify(medaille.model_dump()), 200
|
||||
return jsonify({"message": "Medaille introuvable"}), 404
|
||||
@app.route('/<int:id>', methods=["DELETE"])
|
||||
def deleteMedaille(id: int):
|
||||
"""
|
||||
Supprime un médille par son id
|
||||
"""
|
||||
listeMedailles = ListeMedaille()
|
||||
listeMedailles.loadFromJson(app.config['MEDAILLE_FILE'])
|
||||
for medaille in listeMedailles.root:
|
||||
if medaille.id == id:
|
||||
listeMedailles.root.remove(medaille)
|
||||
with open(app.config['MEDAILLE_FILE'], 'w') as f:
|
||||
json.dump(listeMedailles.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Medaille supprimé"}), 200
|
||||
return jsonify({"message": "Medaille introuvable"}), 404
|
||||
|
||||
@app.route('/<int:id>', methods=["PUT"])
|
||||
def updateMedaille(id: int):
|
||||
"""
|
||||
Met à jour un médille par son id
|
||||
"""
|
||||
listeMedailles = ListeMedaille()
|
||||
listeMedailles.loadFromJson(app.config['MEDAILLE_FILE'])
|
||||
for medaille in listeMedailles.root:
|
||||
if medaille.id == id:
|
||||
data = json.loads(request.data)
|
||||
for key, value in data.items():
|
||||
setattr(medaille, key, value)
|
||||
with open(app.config['MEDAILLE_FILE'], 'w') as f:
|
||||
json.dump(listeMedailles.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Medaille mis à jour"}), 200
|
||||
return jsonify({"message": "Medaille introuvable"}), 404
|
||||
|
||||
@app.route('/<int:id>', methods=["PATCH"])
|
||||
def patchMedaille(id: int):
|
||||
"""
|
||||
Met à jour un médille par son id
|
||||
"""
|
||||
listeMedailles = ListeMedaille()
|
||||
listeMedailles.loadFromJson(app.config['MEDAILLE_FILE'])
|
||||
for medaille in listeMedailles.root:
|
||||
if medaille.id == id:
|
||||
data = json.loads(request.data)
|
||||
data["id"] = medaille.id # On ne peut pas changer l'id
|
||||
for key, value in data.items():
|
||||
if hasattr(medaille, key):
|
||||
setattr(medaille, key, value)
|
||||
if not medaille.validate():
|
||||
return jsonify({"message": "Données invalides"}), 400
|
||||
with open(app.config['MEDAILLE_FILE'], 'w') as f:
|
||||
json.dump(listeMedailles.model_dump(), f, indent=4)
|
||||
return jsonify({"message": "Medaille mis à jour"}), 200
|
||||
return jsonify({"message": "Medaille introuvable"}), 404
|
||||
|
||||
@app.route('/', methods=["POST"])
|
||||
def addMedaille():
|
||||
"""
|
||||
Ajoute un médille
|
||||
"""
|
||||
listeMedailles = ListeMedaille()
|
||||
listeMedailles.loadFromJson(app.config['MEDAILLE_FILE'])
|
||||
medaille = Medaille(**json.loads(request.data))
|
||||
if not medaille.validate():
|
||||
return jsonify({"message": "Données invalides"}), 400
|
||||
medaille.id = max([medaille.id for medaille in listeMedailles.root]) + 1
|
||||
listeMedailles.root.append(medaille)
|
||||
with open(app.config['MEDAILLE_FILE'], 'w') as f:
|
||||
json.dump(listeMedailles.model_dump(), f, indent=4)
|
||||
return jsonify(medaille.model_dump()), 200
|
||||
|
||||
swaggerui_blueprint = get_swaggerui_blueprint(
|
||||
"/swagger",
|
||||
"/static/swagger.yaml"
|
||||
)
|
||||
app.register_blueprint(swaggerui_blueprint)
|
||||
|
||||
def create_app():
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
26
medaille/conftest.py
Normal file
26
medaille/conftest.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
import pytest
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app import create_app
|
||||
|
||||
@pytest.fixture()
|
||||
def app():
|
||||
app = create_app()
|
||||
# Copy the sample to the test folder using python
|
||||
shutil.copy(Path(__file__).parent.parent / 'sample' / 'medailles.json', Path(__file__).parent / 'tests' / 'medailles.json')
|
||||
app.config.update({
|
||||
"TESTING": True,
|
||||
"MEDAILLE_FILE": Path(__file__).parent / 'tests' / 'medailles.json'
|
||||
})
|
||||
yield app
|
||||
|
||||
# Remove the file after the test
|
||||
(Path(__file__).parent / 'tests' / 'medailles.json').unlink()
|
||||
|
||||
@pytest.fixture()
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture()
|
||||
def runner(app):
|
||||
return app.test_cli_runner()
|
60
medaille/models.py
Normal file
60
medaille/models.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
from pydantic import BaseModel, RootModel
|
||||
from typing import Optional, List
|
||||
import json
|
||||
|
||||
class Medaille(BaseModel):
|
||||
"""
|
||||
Modèle Medaille
|
||||
"""
|
||||
id: Optional[int] = 0
|
||||
type: str # Or, Argent, Bronze
|
||||
sport: str
|
||||
categorie: str # ex : 80kg pour le judo
|
||||
pays: str # Nom FR du pays
|
||||
|
||||
def loadFromJsonData(self, data: str):
|
||||
"""
|
||||
Charge les données depuis une chaine json
|
||||
|
||||
:param data: Données json
|
||||
:return: None
|
||||
"""
|
||||
data = json.loads(data)
|
||||
for key, value in data.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""
|
||||
Valide les données
|
||||
|
||||
:return: True si les données sont valides, False sinon
|
||||
"""
|
||||
if self.type not in ["Or", "Argent", "Bronze"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ListeMedaille(RootModel):
|
||||
root: List[Medaille] = []
|
||||
def loadFromJson(self, path: str):
|
||||
"""
|
||||
Charge les données depuis un fichier json
|
||||
|
||||
:param path: Chemin du fichier json
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
for discipline in data:
|
||||
self.root.append(Medaille(**discipline))
|
||||
except FileNotFoundError:
|
||||
print(f"Le fichier {path} n'existe pas")
|
||||
def loadFromJsonData(self, data: str):
|
||||
"""
|
||||
Charge les données depuis une chaine json
|
||||
:param data: Données json
|
||||
:return: None
|
||||
"""
|
||||
data = json.loads(data)
|
||||
for discipline in data:
|
||||
self.root.append(Medaille(**discipline))
|
133
medaille/static/swagger.yaml
Normal file
133
medaille/static/swagger.yaml
Normal file
|
@ -0,0 +1,133 @@
|
|||
openapi: 3.0.1
|
||||
info:
|
||||
title: API Médailles des J.O. 2024
|
||||
description: |-
|
||||
Cette API uService sert à afficher les informations sur les médailles des JO 2024.
|
||||
version: 0.0.1
|
||||
servers:
|
||||
- url: /medailles/
|
||||
tags:
|
||||
- name: medaille
|
||||
description: Medaille des J.O.
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
tags:
|
||||
- medaille
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
description: Le nombre d'éléments à ignorer avant de commencer à collecter l'ensemble de résultats
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
description: Le nombre d'éléments à ignorer
|
||||
summary: Liste l'ensemble des médailles
|
||||
description: Affiche la liste des médailles enregistrés sur les J.O. 2024.
|
||||
operationId: listemédailles
|
||||
responses:
|
||||
'200':
|
||||
description: medaille
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/medaille'
|
||||
post:
|
||||
tags:
|
||||
- medaille
|
||||
summary: Créer une médaille
|
||||
operationId: createAthelte
|
||||
requestBody:
|
||||
description: Objet médaille a créer
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/medaille'
|
||||
responses:
|
||||
default:
|
||||
description: Opération avec succès
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/medaille'
|
||||
/{id}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: ID de l'médaille à récupérer
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
get:
|
||||
tags:
|
||||
- medaille
|
||||
summary: Récupération d'une médaille selon son id
|
||||
operationId: getmedaille
|
||||
responses:
|
||||
'200':
|
||||
description: Opération avec succès
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/medaille'
|
||||
'400':
|
||||
description: ID donné invalide
|
||||
'404':
|
||||
description: Médaille introuvable
|
||||
patch:
|
||||
tags:
|
||||
- medaille
|
||||
summary: Mettre à jour une médaille
|
||||
operationId: updatemedaille
|
||||
requestBody:
|
||||
description: Mettre à jour une médaille existant
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/medaille'
|
||||
responses:
|
||||
default:
|
||||
description: Opération avec succès
|
||||
delete:
|
||||
tags:
|
||||
- medaille
|
||||
summary: Supprimer une médaille
|
||||
operationId: deleteMedaille
|
||||
responses:
|
||||
'400':
|
||||
description: ID donné invalide
|
||||
'404':
|
||||
description: Medaille introuvable
|
||||
components:
|
||||
schemas:
|
||||
medaille:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: uuid
|
||||
example: 123456789
|
||||
type:
|
||||
type: string
|
||||
example: Or
|
||||
sport:
|
||||
type: integer
|
||||
example: 1234567
|
||||
categorie:
|
||||
type: str
|
||||
example: +80kg
|
||||
pays:
|
||||
type: string
|
||||
example: France
|
||||
requestBodies:
|
||||
User:
|
||||
description: Objet médaille à ajouter
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/medaille'
|
6
medaille/tests/test_delMedaille.py
Normal file
6
medaille/tests/test_delMedaille.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
def test_delMedaille(client):
|
||||
response = client.delete("/1")
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.get("/1")
|
||||
assert response.status_code == 404
|
13
medaille/tests/test_getMedaille.py
Normal file
13
medaille/tests/test_getMedaille.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from models import Medaille
|
||||
|
||||
def test_getMedaille(client):
|
||||
response = client.get("/1")
|
||||
medaille = Medaille(
|
||||
id=1,
|
||||
type="Or",
|
||||
sport="Judo",
|
||||
categorie="80kg",
|
||||
pays="France"
|
||||
)
|
||||
assert medaille.model_dump() == response.json
|
||||
assert response.status_code == 200
|
8
medaille/tests/test_listeMedaille.py
Normal file
8
medaille/tests/test_listeMedaille.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from models import ListeMedaille
|
||||
def test_listeMedaille(client):
|
||||
response = client.get("/")
|
||||
listeMedailles = ListeMedaille()
|
||||
listeMedailles.loadFromJsonData(response.data)
|
||||
assert listeMedailles.root is not None
|
||||
assert len(listeMedailles.root) > 0
|
||||
assert listeMedailles.root[0].id == 1
|
3
medaille/tests/test_ping_medaille.py
Normal file
3
medaille/tests/test_ping_medaille.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
def test_ping_medaille(client):
|
||||
response = client.get("/ping")
|
||||
assert b"{\"message\":\"pong\"}\n" in response.data
|
30
medaille/tests/test_postMedaille.py
Normal file
30
medaille/tests/test_postMedaille.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from models import Medaille
|
||||
|
||||
def test_postMedaille(client):
|
||||
medaille = Medaille(
|
||||
id=1,
|
||||
type="Argent",
|
||||
sport="Escrime",
|
||||
categorie="Fleuret",
|
||||
pays="Italie"
|
||||
)
|
||||
|
||||
response = client.post("/", json=medaille.model_dump())
|
||||
assert response.status_code == 200
|
||||
medaille.id = response.json["id"]
|
||||
response = client.get(f"/{medaille.id}")
|
||||
|
||||
assert response.json == medaille.model_dump()
|
||||
|
||||
def test_postMedaille_erreur(client):
|
||||
medaille = Medaille(
|
||||
id=1,
|
||||
type="NexistePas",
|
||||
sport="Escrime",
|
||||
categorie="Fleuret",
|
||||
pays="Italie"
|
||||
)
|
||||
|
||||
response = client.post("/", json=medaille.model_dump())
|
||||
assert response.status_code == 400
|
||||
assert response.json == {'message': 'Données invalides'}
|
|
@ -1,4 +1,5 @@
|
|||
flask==3.0.2
|
||||
pydantic==2.6.4
|
||||
pytest
|
||||
flask_swagger_ui
|
||||
flask_swagger_ui
|
||||
gunicorn
|
|
@ -1,8 +1,8 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"prenom": "Riner",
|
||||
"nom": "Teddy",
|
||||
"prenom": "Teddy",
|
||||
"nom": "Riner",
|
||||
"pays": "France",
|
||||
"sexe": "H",
|
||||
"image": "https://upload.wikimedia.org/wikipedia/commons/4/4e/Teddy_Riner_2012.jpg",
|
||||
|
|
30
sample/disciplines.json
Normal file
30
sample/disciplines.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"intitule": "Judo",
|
||||
"type": "Combat",
|
||||
"description": "Le judo est un art martial japonais, fondé par Jigoro Kano en 1882. Il se compose pour l'essentiel de techniques de projection, de contrôle au sol, d'étranglements et de clefs.",
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/4/4b/Judo_pictogram.svg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"intitule": "Karate",
|
||||
"type": "Combat",
|
||||
"description": "Le karaté est un art martial d'origine japonaise, dont le développement s'est essentiellement effectué à Okinawa. Il est principalement constitué de techniques de percussions à mains nues, de coups de pieds, de projections, de balayages et de luxations.",
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/2/2d/Karate_pictogram.svg"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"intitule": "100m",
|
||||
"type": "Course",
|
||||
"description": "Le 100 mètres est une épreuve de sprint en athlétisme. C'est l'épreuve reine des sprinteurs.",
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/1/1b/Athletics_pictogram.svg"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"intitule": "Natation",
|
||||
"type": "Nage",
|
||||
"description": "La natation est un sport consistant à parcourir une certaine distance dans l'eau, en utilisant les bras et les jambes.",
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/0/0d/Swimming_pictogram.svg"
|
||||
}
|
||||
]
|
44
sample/medailles.json
Normal file
44
sample/medailles.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Or",
|
||||
"sport": "Judo",
|
||||
"categorie": "80kg",
|
||||
"pays": "France"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Argent",
|
||||
"sport": "Judo",
|
||||
"categorie": "80kg",
|
||||
"pays": "France"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "Bronze",
|
||||
"sport": "Judo",
|
||||
"categorie": "80kg",
|
||||
"pays": "France"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "Or",
|
||||
"sport": "Judo",
|
||||
"categorie": "80kg",
|
||||
"pays": "France"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "Argent",
|
||||
"sport": "Judo",
|
||||
"categorie": "80kg",
|
||||
"pays": "France"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "Bronze",
|
||||
"sport": "Judo",
|
||||
"categorie": "80kg",
|
||||
"pays": "France"
|
||||
}
|
||||
]
|
Loading…
Reference in a new issue