duplicate apis

This commit is contained in:
Mael G. 2024-03-27 14:17:05 +01:00
parent b56019c694
commit 935b404338
60 changed files with 1065 additions and 80 deletions

View file

@ -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
```

View file

@ -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"]

Binary file not shown.

Binary file not shown.

View file

@ -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

View file

@ -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))

View file

@ -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()

View file

View file

@ -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.

View file

@ -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": []
}
]

View 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

View file

@ -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

View file

@ -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 != []

View file

@ -1,5 +0,0 @@
import pytest
def test_ping(client):
response = client.get("/ping")
assert b"{\"message\":\"pong\"}\n" in response.data

View file

@ -0,0 +1,3 @@
def test_ping_athlete(client):
response = client.get("/ping")
assert b"{\"message\":\"pong\"}\n" in response.data

View 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
View 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"]

Binary file not shown.

121
discipline/app.py Normal file
View file

@ -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('/<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 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('/<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([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()

26
discipline/conftest.py Normal file
View 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/discipline.py Normal file
View 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 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))

View 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'

View 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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
def test_ping_discipline(client):
response = client.get("/ping")
assert b"{\"message\":\"pong\"}\n" in response.data

View file

@ -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()

View file

@ -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
View 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"]

117
medaille/app.py Normal file
View file

@ -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('/<int:id>', 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('/<int:id>', 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('/<int:id>', 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('/<int:id>', 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()

26
medaille/conftest.py Normal file
View 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()

View file

@ -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'

View file

View file

@ -1,4 +1,5 @@
flask==3.0.2
pydantic==2.6.4
pytest
flask_swagger_ui
flask_swagger_ui
gunicorn

View file

@ -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
View 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",
}
]