diff --git a/PROJET.md b/PROJET.md index e69de29..12fe38c 100644 --- a/PROJET.md +++ b/PROJET.md @@ -0,0 +1,54 @@ +# 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 -vvv . +``` + +## 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 +``` diff --git a/athlete/Dockerfile b/athlete/Dockerfile index bafe792..611747e 100644 --- a/athlete/Dockerfile +++ b/athlete/Dockerfile @@ -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"] \ No newline at end of file +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"] diff --git a/athlete/__pycache__/app.cpython-311.pyc b/athlete/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..99dcc32 Binary files /dev/null and b/athlete/__pycache__/app.cpython-311.pyc differ diff --git a/athlete/__pycache__/athlete.cpython-311.pyc b/athlete/__pycache__/athlete.cpython-311.pyc new file mode 100644 index 0000000..47a1317 Binary files /dev/null and b/athlete/__pycache__/athlete.cpython-311.pyc differ diff --git a/athlete/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc b/athlete/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..a5c712b Binary files /dev/null and b/athlete/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/app.py b/athlete/app.py index 11e292a..2bad487 100644 --- a/athlete/app.py +++ b/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('/', 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('/', 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('/', 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('/', 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 diff --git a/athlete/athlete.py b/athlete/athlete.py index d826389..7f216be 100644 --- a/athlete/athlete.py +++ b/athlete/athlete.py @@ -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("Fichier introuvable") + 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)) \ No newline at end of file diff --git a/athlete/conftest.py b/athlete/conftest.py index 882eddb..6ccefe3 100644 --- a/athlete/conftest.py +++ b/athlete/conftest.py @@ -1,24 +1,26 @@ import pytest -from ..app.app import create_app +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' / '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() \ No newline at end of file diff --git a/athlete/genData.py b/athlete/genData.py deleted file mode 100644 index e69de29..0000000 diff --git a/athlete/static/swagger.yaml b/athlete/static/swagger.yaml index 540ddfe..80998d4 100644 --- a/athlete/static/swagger.yaml +++ b/athlete/static/swagger.yaml @@ -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. diff --git a/athlete/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..6a8c86d Binary files /dev/null and b/athlete/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/__pycache__/test_delAthlete.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/test_delAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..f696723 Binary files /dev/null and b/athlete/tests/__pycache__/test_delAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/__pycache__/test_getAthlete.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/test_getAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..9a9eb02 Binary files /dev/null and b/athlete/tests/__pycache__/test_getAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/__pycache__/test_gettest.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/test_gettest.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..9e5b477 Binary files /dev/null and b/athlete/tests/__pycache__/test_gettest.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/__pycache__/test_listeAthlete.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/test_listeAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..9a21fcd Binary files /dev/null and b/athlete/tests/__pycache__/test_listeAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..4258ec0 Binary files /dev/null and b/athlete/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/__pycache__/test_ping_athlete.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/test_ping_athlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..7d52440 Binary files /dev/null and b/athlete/tests/__pycache__/test_ping_athlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/__pycache__/test_postAthlete.cpython-311-pytest-8.1.1.pyc b/athlete/tests/__pycache__/test_postAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..8678aae Binary files /dev/null and b/athlete/tests/__pycache__/test_postAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/athlete/tests/athletes.json b/athlete/tests/athletes.json deleted file mode 100644 index bd72eda..0000000 --- a/athlete/tests/athletes.json +++ /dev/null @@ -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": [] - } -] \ No newline at end of file diff --git a/athlete/tests/test_delAthlete.py b/athlete/tests/test_delAthlete.py new file mode 100644 index 0000000..0c50edd --- /dev/null +++ b/athlete/tests/test_delAthlete.py @@ -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 \ No newline at end of file diff --git a/athlete/tests/test_getAthlete.py b/athlete/tests/test_getAthlete.py index 052b47e..27cf864 100644 --- a/athlete/tests/test_getAthlete.py +++ b/athlete/tests/test_getAthlete.py @@ -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 diff --git a/athlete/tests/test_listeAthlete.py b/athlete/tests/test_listeAthlete.py index e69de29..1b3974f 100644 --- a/athlete/tests/test_listeAthlete.py +++ b/athlete/tests/test_listeAthlete.py @@ -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 != [] diff --git a/athlete/tests/test_ping.py b/athlete/tests/test_ping.py deleted file mode 100644 index 29b234a..0000000 --- a/athlete/tests/test_ping.py +++ /dev/null @@ -1,5 +0,0 @@ -import pytest - -def test_ping(client): - response = client.get("/ping") - assert b"{\"message\":\"pong\"}\n" in response.data \ No newline at end of file diff --git a/athlete/tests/test_ping_athlete.py b/athlete/tests/test_ping_athlete.py new file mode 100644 index 0000000..827718d --- /dev/null +++ b/athlete/tests/test_ping_athlete.py @@ -0,0 +1,3 @@ +def test_ping_athlete(client): + response = client.get("/ping") + assert b"{\"message\":\"pong\"}\n" in response.data diff --git a/athlete/tests/test_postAthlete.py b/athlete/tests/test_postAthlete.py new file mode 100644 index 0000000..5524968 --- /dev/null +++ b/athlete/tests/test_postAthlete.py @@ -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() \ No newline at end of file diff --git a/discipline/Dockerfile b/discipline/Dockerfile new file mode 100644 index 0000000..c8ddb39 --- /dev/null +++ b/discipline/Dockerfile @@ -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"] diff --git a/discipline/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc b/discipline/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..407abd7 Binary files /dev/null and b/discipline/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/__pycache__/discipline.cpython-311.pyc b/discipline/__pycache__/discipline.cpython-311.pyc new file mode 100644 index 0000000..02e6bcb Binary files /dev/null and b/discipline/__pycache__/discipline.cpython-311.pyc differ diff --git a/discipline/app.py b/discipline/app.py new file mode 100644 index 0000000..b8c9635 --- /dev/null +++ b/discipline/app.py @@ -0,0 +1,121 @@ +from flask import Flask, jsonify, request +from pathlib import Path +import json +from discipline 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') + +@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('/', 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('/', methods=["DELETE"]) +def deleteDiscipline(id: int): + """ + Supprime une discipline par son id + """ + listeDisciplines = ListeDiscipline() + listeDisciplines.loadFromJson(app.config['DISCIPLINE_FILE']) + for athlete in listeDisciplines.root: + if athlete.id == id: + listeDisciplines.root.remove(athlete) + 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('/', 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('/', 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([athlete.id for athlete 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() diff --git a/discipline/conftest.py b/discipline/conftest.py new file mode 100644 index 0000000..e617f65 --- /dev/null +++ b/discipline/conftest.py @@ -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() \ No newline at end of file diff --git a/discipline/discipline.py b/discipline/discipline.py new file mode 100644 index 0000000..96cc3bd --- /dev/null +++ b/discipline/discipline.py @@ -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 athlete in data: + self.root.append(Discipline(**athlete)) + except FileNotFoundError: + print("Fichier introuvable") + 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(Discipline(**athlete)) \ No newline at end of file diff --git a/discipline/static/swagger.yaml b/discipline/static/swagger.yaml new file mode 100644 index 0000000..258777d --- /dev/null +++ b/discipline/static/swagger.yaml @@ -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' diff --git a/discipline/swagger.yaml b/discipline/swagger.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/discipline/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..6a8c86d Binary files /dev/null and b/discipline/tests/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_delAthlete.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_delAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..f696723 Binary files /dev/null and b/discipline/tests/__pycache__/test_delAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_delDiscipline.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_delDiscipline.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..682f396 Binary files /dev/null and b/discipline/tests/__pycache__/test_delDiscipline.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_getAthlete.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_getAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..9a9eb02 Binary files /dev/null and b/discipline/tests/__pycache__/test_getAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_getDiscipline.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_getDiscipline.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..f797c82 Binary files /dev/null and b/discipline/tests/__pycache__/test_getDiscipline.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_gettest.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_gettest.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..9e5b477 Binary files /dev/null and b/discipline/tests/__pycache__/test_gettest.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_listeAthlete.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_listeAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..9a21fcd Binary files /dev/null and b/discipline/tests/__pycache__/test_listeAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_listeDiscipline.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_listeDiscipline.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..83b75ba Binary files /dev/null and b/discipline/tests/__pycache__/test_listeDiscipline.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..4258ec0 Binary files /dev/null and b/discipline/tests/__pycache__/test_ping.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_ping_discipline.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_ping_discipline.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..0b74945 Binary files /dev/null and b/discipline/tests/__pycache__/test_ping_discipline.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_postAthlete.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_postAthlete.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..8678aae Binary files /dev/null and b/discipline/tests/__pycache__/test_postAthlete.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/__pycache__/test_postDiscipline.cpython-311-pytest-8.1.1.pyc b/discipline/tests/__pycache__/test_postDiscipline.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..2a1d1e9 Binary files /dev/null and b/discipline/tests/__pycache__/test_postDiscipline.cpython-311-pytest-8.1.1.pyc differ diff --git a/discipline/tests/test_delDiscipline.py b/discipline/tests/test_delDiscipline.py new file mode 100644 index 0000000..02037c1 --- /dev/null +++ b/discipline/tests/test_delDiscipline.py @@ -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 \ No newline at end of file diff --git a/discipline/tests/test_getDiscipline.py b/discipline/tests/test_getDiscipline.py new file mode 100644 index 0000000..b69ed1f --- /dev/null +++ b/discipline/tests/test_getDiscipline.py @@ -0,0 +1,13 @@ +from discipline 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 diff --git a/discipline/tests/test_listeDiscipline.py b/discipline/tests/test_listeDiscipline.py new file mode 100644 index 0000000..778fb6a --- /dev/null +++ b/discipline/tests/test_listeDiscipline.py @@ -0,0 +1,9 @@ +from discipline import ListeDiscipline +def test_listeAthlete(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 == 0 + assert listeDiscipline.root[0].nom is not None diff --git a/discipline/tests/test_ping_discipline.py b/discipline/tests/test_ping_discipline.py new file mode 100644 index 0000000..a2695ea --- /dev/null +++ b/discipline/tests/test_ping_discipline.py @@ -0,0 +1,3 @@ +def test_ping_discipline(client): + response = client.get("/ping") + assert b"{\"message\":\"pong\"}\n" in response.data diff --git a/discipline/tests/test_postDiscipline.py b/discipline/tests/test_postDiscipline.py new file mode 100644 index 0000000..bd288e5 --- /dev/null +++ b/discipline/tests/test_postDiscipline.py @@ -0,0 +1,17 @@ +from discipline 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() \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index e69de29..068f708 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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" \ No newline at end of file diff --git a/medaille/Dockerfile b/medaille/Dockerfile new file mode 100644 index 0000000..78163f1 --- /dev/null +++ b/medaille/Dockerfile @@ -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"] diff --git a/medaille/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc b/medaille/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc new file mode 100644 index 0000000..32d37cf Binary files /dev/null and b/medaille/__pycache__/conftest.cpython-311-pytest-8.1.1.pyc differ diff --git a/medaille/app.py b/medaille/app.py new file mode 100644 index 0000000..ebbf072 --- /dev/null +++ b/medaille/app.py @@ -0,0 +1,117 @@ +from flask import Flask, jsonify, request +from pathlib import Path +import json +from athlete import ListeAthlete, Athlete +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 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['MEDAILLE_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('/', methods=["GET"]) +def getAthlete(id: int): + """ + Renvoie un athlète par son id + """ + listeAthletes = ListeAthlete() + listeAthletes.loadFromJson(app.config['MEDAILLE_FILE']) + for athlete in listeAthletes.root: + if athlete.id == id: + return jsonify(athlete.model_dump()), 200 + return jsonify({"message": "Athlete introuvable"}), 404 +@app.route('/', methods=["DELETE"]) +def deleteAthlete(id: int): + """ + Supprime un athlète par son id + """ + listeAthletes = ListeAthlete() + listeAthletes.loadFromJson(app.config['MEDAILLE_FILE']) + for athlete in listeAthletes.root: + if athlete.id == id: + listeAthletes.root.remove(athlete) + with open(app.config['MEDAILLE_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('/', methods=["PUT"]) +def updateAthlete(id: int): + """ + Met à jour un athlète par son id + """ + listeAthletes = ListeAthlete() + listeAthletes.loadFromJson(app.config['MEDAILLE_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['MEDAILLE_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=["PATCH"]) +def patchAthlete(id: int): + """ + Met à jour un athlète par son id + """ + listeAthletes = ListeAthlete() + listeAthletes.loadFromJson(app.config['MEDAILLE_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['MEDAILLE_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['MEDAILLE_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['MEDAILLE_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 + +if __name__ == '__main__': + app.run() diff --git a/medaille/conftest.py b/medaille/conftest.py new file mode 100644 index 0000000..ac0de99 --- /dev/null +++ b/medaille/conftest.py @@ -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() \ No newline at end of file diff --git a/medaille/static/swagger.yaml b/medaille/static/swagger.yaml new file mode 100644 index 0000000..7d18542 --- /dev/null +++ b/medaille/static/swagger.yaml @@ -0,0 +1,136 @@ +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 + disclipine: + type: integer + example: 1234567 + pays: + type: string + example: France + logo: + type: string + example: https://olympics.com/images/static/sports/pictograms/v2/kte.svg + requestBodies: + User: + description: Objet médaille à ajouter + content: + application/json: + schema: + $ref: '#/components/schemas/medaille' \ No newline at end of file diff --git a/medaille/swagger.yaml b/medaille/swagger.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/requirements.txt b/requirements.txt index b5661fc..70fbd72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flask==3.0.2 pydantic==2.6.4 pytest -flask_swagger_ui \ No newline at end of file +flask_swagger_ui +gunicorn \ No newline at end of file diff --git a/sample/athletes.json b/sample/athletes.json index bd72eda..9896058 100644 --- a/sample/athletes.json +++ b/sample/athletes.json @@ -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", diff --git a/sample/disciplines.json b/sample/disciplines.json new file mode 100644 index 0000000..c9684f1 --- /dev/null +++ b/sample/disciplines.json @@ -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", + } +] \ No newline at end of file