Ajoute le store d’intrants et la CLI Life-NOC

This commit is contained in:
Daniel Allaire 2026-03-14 19:21:54 -04:00
parent 601bad94b0
commit a45f41bf03
9 changed files with 275 additions and 5 deletions

View file

@ -9,3 +9,12 @@
state: reloaded
when: life_noc_reload_icinga | bool
listen: validate and reload icinga
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart life-noc input api
ansible.builtin.service:
name: life-noc-input-api
state: restarted

View file

@ -221,3 +221,28 @@
when:
- life_noc_bpm_deploy_enabled | bool
- life_noc_bpm_destination_dir is defined
- name: install api dependencies for inputs service
ansible.builtin.apt:
name:
- python3-fastapi
- python3-uvicorn
state: present
update_cache: true
- name: deploy life-noc input api systemd unit
ansible.builtin.template:
src: life-noc-input-api.service.j2
dest: /etc/systemd/system/life-noc-input-api.service
owner: root
group: root
mode: "0644"
notify:
- reload systemd
- restart life-noc input api
- name: enable and start life-noc input api
ansible.builtin.service:
name: life-noc-input-api
enabled: true
state: started

View file

@ -0,0 +1,15 @@
[Unit]
Description=Life-NOC Input API
After=network.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory={{ life_noc_project_root }}
ExecStart=/usr/bin/python3 -m uvicorn scripts.life_noc_input_api:app --host 127.0.0.1 --port 8787
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target

View file

@ -1,9 +1,8 @@
revue-quotidienne-life-noc:
value: "2026-03-14"
captured_at: "2026-03-14T08:00:00Z"
value: '2026-03-14'
captured_at: '2026-03-14T08:00:00Z'
origin: manual
revue-hebdomadaire-priorites:
value: "2026-03-10"
captured_at: "2026-03-14T08:00:00Z"
value: '2026-03-14'
captured_at: '2026-03-14T23:16:41Z'
origin: manual

0
lib/__init__.py Normal file
View file

70
lib/life_noc_inputs.py Normal file
View file

@ -0,0 +1,70 @@
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
import yaml
INPUTS_DIR = Path("data/inputs")
def utc_now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def utc_today_date() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
def domain_file(domain: str) -> Path:
return INPUTS_DIR / f"{domain}.yaml"
def load_domain(domain: str) -> dict:
path = domain_file(domain)
if not path.exists():
return {}
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if data is None:
return {}
if not isinstance(data, dict):
raise ValueError(f"Fichier invalide: {path}")
return data
def save_domain(domain: str, data: dict) -> Path:
path = domain_file(domain)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
yaml.safe_dump(data, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
return path
def list_items(domain: str) -> list[str]:
return list(load_domain(domain).keys())
def get_item(domain: str, item_key: str) -> dict:
data = load_domain(domain)
if item_key not in data:
raise KeyError(item_key)
item = data[item_key]
if not isinstance(item, dict):
raise ValueError(f"Entrée invalide pour {item_key}")
return item
def set_item(domain: str, item_key: str, value: str, origin: str = "manual") -> dict:
data = load_domain(domain)
data[item_key] = {
"value": value,
"captured_at": utc_now_iso(),
"origin": origin,
}
save_domain(domain, data)
return data[item_key]
def complete_item(domain: str, item_key: str, origin: str = "manual") -> dict:
return set_item(domain, item_key, utc_today_date(), origin=origin)

View file

91
scripts/life_noc_input.py Executable file
View file

@ -0,0 +1,91 @@
#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
import sys
import argparse
import yaml
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from lib.life_noc_inputs import list_items, get_item, set_item, complete_item
def cmd_set(args: argparse.Namespace) -> int:
set_item(args.domain, args.item_key, args.value, origin=args.origin)
print(f"Mis à jour: {args.domain} -> {args.item_key}")
return 0
def cmd_complete(args: argparse.Namespace) -> int:
complete_item(args.domain, args.item_key, origin=args.origin)
print(f"Complété: {args.domain} -> {args.item_key}")
return 0
def cmd_get(args: argparse.Namespace) -> int:
try:
item = get_item(args.domain, args.item_key)
except KeyError:
print(f"Item introuvable: {args.item_key}", file=sys.stderr)
return 1
print(
yaml.safe_dump(
{args.item_key: item},
sort_keys=False,
allow_unicode=True,
).strip()
)
return 0
def cmd_list(args: argparse.Namespace) -> int:
for key in list_items(args.domain):
print(key)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Gestion des intrants Life-NOC")
sub = parser.add_subparsers(dest="command", required=True)
p_set = sub.add_parser("set", help="Met à jour un intrant")
p_set.add_argument("domain")
p_set.add_argument("item_key")
p_set.add_argument("value")
p_set.add_argument("--origin", default="manual")
p_set.set_defaults(func=cmd_set)
p_complete = sub.add_parser("complete", help="Marque un item comme complété maintenant")
p_complete.add_argument("domain")
p_complete.add_argument("item_key")
p_complete.add_argument("--origin", default="manual")
p_complete.set_defaults(func=cmd_complete)
p_get = sub.add_parser("get", help="Lit un intrant")
p_get.add_argument("domain")
p_get.add_argument("item_key")
p_get.set_defaults(func=cmd_get)
p_list = sub.add_parser("list", help="Liste les intrants dun domaine")
p_list.add_argument("domain")
p_list.set_defaults(func=cmd_list)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
try:
return args.func(args)
except ValueError as exc:
print(f"Erreur: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
import sys
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from lib.life_noc_inputs import list_items, get_item, set_item, complete_item # noqa: E402
class SetInputRequest(BaseModel):
value: str
origin: str = "manual"
class CompleteInputRequest(BaseModel):
origin: str = "manual"
app = FastAPI(title="Life-NOC Input API", version="1.0.0")
@app.get("/health")
def health() -> dict:
return {"status": "ok"}
@app.get("/inputs/{domain}")
def api_list_inputs(domain: str) -> dict:
return {"domain": domain, "items": list_items(domain)}
@app.get("/inputs/{domain}/{item_key}")
def api_get_input(domain: str, item_key: str) -> dict:
try:
item = get_item(domain, item_key)
except KeyError:
raise HTTPException(status_code=404, detail="item introuvable")
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"domain": domain, "item_key": item_key, **item}
@app.post("/inputs/{domain}/{item_key}")
def api_set_input(domain: str, item_key: str, req: SetInputRequest) -> dict:
item = set_item(domain, item_key, req.value, origin=req.origin)
return {"domain": domain, "item_key": item_key, **item}
@app.post("/inputs/{domain}/{item_key}/complete")
def api_complete_input(domain: str, item_key: str, req: CompleteInputRequest | None = None) -> dict:
origin = "manual" if req is None else req.origin
item = complete_item(domain, item_key, origin=origin)
return {"domain": domain, "item_key": item_key, **item}