Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ed6bf57
feat/annuaire: add YAML reading functions (load_clients, build_client…
May 11, 2026
c108b53
feat/annuaire: wire create_app to YAML source instead of CSV
May 11, 2026
17940a6
feat/annuaire: remove CSV functions and constants
May 11, 2026
41f85a8
feat/annuaire: move test fixture to fixtures/topology.yaml
May 11, 2026
2d6c249
feat/annuaire: format code & update uv.lock
May 11, 2026
f422feb
feat/annuaire: add error logging in load_clients and cover FileNotFou…
May 11, 2026
3e5d959
feat/annuaire: raise RuntimeError on invalid YAML structure and add t…
May 11, 2026
cc97044
feat/annuaire: open values file with explicit utf-8 encoding
May 12, 2026
e4d8813
feat/annuaire: add explicit structure validation in load_clients
May 12, 2026
984c6b3
feat/annuaire: pin PyYAML to 6.0.2
May 12, 2026
cce10ca
feat/annuaire: remove redundant test, add directCISU=true coverage
May 12, 2026
c88126e
feat/annuaire: rename DATA_KEY to CLIENTS_DATA_KEY and merge VALUES_D…
May 12, 2026
3c6f302
feat/annuaire: extract magic strings into named constants
May 12, 2026
3f0e63b
feat/annuaire: split structure validation into two distinct error mes…
May 12, 2026
f94a2d0
feat/annuaire: add test coverage for unexpected YAML parse error
May 12, 2026
dc333cf
feat/annuaire: remove unused VALUES_PATH import in tests
May 12, 2026
dd6621f
feat/annuaire: fix import order (stdlib before third-party)
May 12, 2026
4e27da9
feat/annuaire: rename c to client and log actual exception message
May 12, 2026
36ccb6c
feat/annuaire: move register_routes before create_app
May 12, 2026
ba28737
feat/annuaire: use TOPOLOGY constants in test assertions
May 12, 2026
d6e17b1
feat/annuaire: use yaml.dump for inline fixtures in tests
May 12, 2026
0fe1630
feat/annuaire: update uv.lock to match pyproject.toml
May 12, 2026
8ec31b9
feat/annuaire: use tempdir for file-not-found test instead of hardcod…
May 12, 2026
88f645e
feat/annuaire: add cisuPerimeterVersions to directCISU test fixture
May 12, 2026
30f97df
feat/annuaire: use 'annuaire' as YAML root key instead of 'hubsante-t…
May 12, 2026
1380908
feat/annuaire : format code
May 13, 2026
7dd0594
feat/annuaire: enrich fixture and strengthen test_api and test_load_c…
May 18, 2026
905e3c5
feat/annuaire: add full-perimeters+directCISU client to fixture for c…
May 18, 2026
7a73886
feat/annuaire: replace smur-only client with lrm-only client to match…
May 18, 2026
8c6647e
feat/annuaire: reference fixture directly via VALUES_PATH patch, remo…
May 18, 2026
a277b63
feat/annuaire: replace VALUES_DIR+VALUES_FILENAME with single VALUES_…
May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 50 additions & 44 deletions tools/annuaire/annuaire.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,79 @@
import logging
from flask import Flask, jsonify
import csv
import os

from flask import Flask, jsonify
import yaml

ENVIRONMENT = os.environ.get("ENVIRONMENT")

CSV_DIR = "/config"
CSV_DATA_KEY = "CSV_DATA"
CSV_FILENAME = "rabbitmq.clients-configuration.csv"
API_ENDPOINT = "/annuaire/api"
HEALTH_ENDPOINT = "/annuaire/health"
CSV_NOT_FOUND_MSG = "Fichier CSV introuvable"

HEADERS_COLUMNS_TO_KEEP = [
"client_id",
"editor",
"P: 15-15",
"P: 15-smur",
"P: 15-nexsis",
"P: 15-gps",
"directCISU",
]
VALUES_PATH = os.environ.get("VALUES_PATH", "/config/topology/values.yaml")
CLIENTS_DATA_KEY = "CLIENTS_DATA"

TOPOLOGY_ROOT_KEY = "annuaire"
TOPOLOGY_CLIENTS_KEY = "clients"

def create_app():
app = Flask(__name__)
register_routes(app)
csv_data = parse_csv(CSV_FILENAME)
if csv_data is None:
raise RuntimeError(
"Erreur : impossible de charger le fichier CSV au démarrage."
)
app.config[CSV_DATA_KEY] = select_columns(csv_data)
return app
TOPOLOGY_TO_LEGACY_KEY = {
"lrmPerimeterVersions": "P: 15-15",
"smurPerimeterVersions": "P: 15-smur",
"cisuPerimeterVersions": "P: 15-nexsis",
"gpsPerimeterVersions": "P: 15-gps",
}


def parse_csv(filename):
path = os.path.join(CSV_DIR, filename)
if not os.path.exists(path):
logging.error(f"Fichier CSV introuvable : {path}")
raise FileNotFoundError(f"{CSV_NOT_FOUND_MSG} : {filename}")
def load_clients(path: str) -> list[dict]:
try:
with open(path, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile, delimiter=";")
return list(reader)
with open(path, encoding="utf-8") as f:
data = yaml.safe_load(f)
if TOPOLOGY_ROOT_KEY not in data:
raise RuntimeError(f"Missing '{TOPOLOGY_ROOT_KEY}' key in {path}")
if TOPOLOGY_CLIENTS_KEY not in data[TOPOLOGY_ROOT_KEY]:
raise RuntimeError(
f"Missing '{TOPOLOGY_ROOT_KEY}.{TOPOLOGY_CLIENTS_KEY}' key in {path}"
)
return data[TOPOLOGY_ROOT_KEY][TOPOLOGY_CLIENTS_KEY]
except FileNotFoundError:
logging.error(f"Values file not found: {path}")
raise
except RuntimeError as e:
logging.error(f"Failed to load clients from {path}: {e}")
raise
except Exception as e:
logging.error(f"Erreur lors de la lecture du fichier CSV '{filename}': {e}")
raise RuntimeError(f"Erreur lors de la lecture du CSV: {e}")
logging.error(f"Failed to load clients from {path}: {e}")
raise RuntimeError(f"Failed to load clients from {path}: {e}") from e


def select_columns(data: list[dict]) -> list[dict]:
data_updated = []
for row in data:
row_updated = {
key: value for key, value in row.items() if key in HEADERS_COLUMNS_TO_KEEP
}
data_updated.append(row_updated)
return data_updated
def build_client_entry(client: dict) -> dict:
entry = {
"client_id": client["client_id"],
"editor": client.get("editor", ""),
"directCISU": client.get("directCISU", False),
}
for topo_key, legacy_key in TOPOLOGY_TO_LEGACY_KEY.items():
if topo_key in client:
entry[legacy_key] = client[topo_key]
return entry


def register_routes(app):
@app.get(API_ENDPOINT)
def get_json():
return jsonify(app.config[CSV_DATA_KEY])
return jsonify(app.config[CLIENTS_DATA_KEY])

@app.get(HEALTH_ENDPOINT)
def health_check():
return jsonify({"status": "UP", "service": "SAMU Hub Annuaire"}), 200


def create_app():
app = Flask(__name__)
register_routes(app)
clients = load_clients(VALUES_PATH)
app.config[CLIENTS_DATA_KEY] = [build_client_entry(c) for c in clients]
return app


if ENVIRONMENT == "production":
app = create_app()
28 changes: 28 additions & 0 deletions tools/annuaire/fixtures/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
annuaire:
clients:
# client with LRM + CISU + SMUR + GPS (full perimeters, no directCISU)
- client_id: fr.health.samu750
editor: Editeur A
lrmPerimeterVersions: ["2.1"]
cisuPerimeterVersions: ["1.9"]
smurPerimeterVersions: ["1.7"]
gpsPerimeterVersions: ["1.3"]
directCISU: false
# client with LRM only (partial perimeters, no directCISU)
- client_id: fr.health.test.samuv1
editor: ANS
lrmPerimeterVersions: ["1.5"]
directCISU: false
# client with CISU + directCISU (NexSIS-type)
- client_id: fr.health.fire
editor: NexSIS
cisuPerimeterVersions: ["1.9"]
directCISU: true
# client with all 4 perimeters + directCISU
- client_id: fr.health.test.samuC
editor: ANS
lrmPerimeterVersions: ["1.5", "2.0", "2.1"]
smurPerimeterVersions: ["1.7"]
cisuPerimeterVersions: ["1.9"]
gpsPerimeterVersions: ["1.3"]
directCISU: true
1 change: 1 addition & 0 deletions tools/annuaire/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"MarkupSafe==3.0.3",
"packaging==25.0",
"Werkzeug==3.1.3",
"PyYAML==6.0.2",
]

[dependency-groups]
Expand Down
Loading
Loading