SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

accounts: cli — seed depuis un fichier JSON ou HFMD_SEED_USERS, plus aucune adresse tierce en dur

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 18 days ago (Sep 6, 2026) parent fc98c35

1 changed file +40 −12

modified hfmarketdata/api/accounts/cli.py +40 −12
@@ -6,18 +6,25 @@
6 6 hfmd users disable <email> | enable <email>
7 7 hfmd users invite-resend <email> [--no-mail]
8 8 hfmd keys list <email>
9 − hfmd seed [--show-key] [--no-mail] # idempotent: 3 users + admin
9 + hfmd keys rotate <email> [--show-key]
10 + hfmd seed [seed.json] [--show-key] [--no-mail] # idempotent; users from the JSON file or $HFMD_SEED_USERS
10 11
11 12 Full API keys are printed ONLY at creation and ONLY with `--show-key`. Invitation links are printed when
12 13 the e-mail could not be delivered (no HFMD_RESEND_API_KEY) so you can forward them.
13 14
15 +Seed file format (never commit real addresses): `[{"name": "…", "email": "…", "tier": "free|high_usage|unlimited",
16 +"role": "user|admin"}, …]`. The same JSON may be passed inline through the `HFMD_SEED_USERS` environment variable.
17 +
14 18 Author: Simon-Pierre Boucher <contact@spboucher.ai>
15 19 """
16 20 from __future__ import annotations
17 21
18 22 import argparse
23 +import json
24 +import os
19 25 import sys
20 26 from collections.abc import Iterable
27 +from pathlib import Path
21 28
22 29 from core.config import settings
23 30 from core.db import session
@@ -25,13 +32,31 @@ from core.errors import ApiError
25 32
26 33 from . import models, service
27 34
28 −SEED_USERS = [
29 − {"name": "Luca Iannelli", "email": "ianez84@gmail.com", "tier": "free", "role": "user"},
30 − {"name": "Tim Erdmann", "email": "timerdmann.uni@gmail.com", "tier": "free", "role": "user"},
31 − {"name": "Javier Slaton", "email": "javier.slaton@gmail.com", "tier": "free", "role": "user"},
32 − {"name": "Simon-Pierre Boucher", "email": "contact@spboucher.ai", "tier": "high_usage", "role": "admin"},
33 −]
34 35 ACTOR = "cli"
36 +SEED_ENV = "HFMD_SEED_USERS"
37 +
38 +
39 +def load_seed(path: str | None) -> list[dict]:
40 + """Seed users from a JSON file (argument) or the HFMD_SEED_USERS environment variable (inline JSON)."""
41 + if path:
42 + raw = Path(path).read_text(encoding="utf-8")
43 + else:
44 + raw = os.environ.get(SEED_ENV, "")
45 + if not raw.strip():
46 + raise ApiError(400, "INVALID_PARAMETER", f"no seed: pass a JSON file or set {SEED_ENV}")
47 + try:
48 + users = json.loads(raw)
49 + except ValueError as exc:
50 + raise ApiError(400, "INVALID_PARAMETER", f"seed is not valid JSON: {exc}") from exc
51 + if not isinstance(users, list) or not users:
52 + raise ApiError(400, "INVALID_PARAMETER", "seed must be a non-empty JSON list of users")
53 + out = []
54 + for i, u in enumerate(users):
55 + if not isinstance(u, dict) or not u.get("email"):
56 + raise ApiError(400, "INVALID_PARAMETER", f"seed entry {i} needs at least an email")
57 + out.append({"name": str(u.get("name") or ""), "email": str(u["email"]), "tier": str(u.get("tier") or "free"),
58 + "role": str(u.get("role") or "user")})
59 + return out
35 60
36 61
37 62 def table(rows: Iterable[dict], columns: list[str], out=None) -> None:
@@ -70,7 +95,7 @@ def provision(s, name: str, email: str, *, tier: str, admin: bool, send_mail: bo
70 95 if u is None:
71 96 u, created_key, issued = service.invite(s, email, name, tier=tier, role="admin" if admin else "user",
72 97 actor=ACTOR, send_mail=send_mail)
73 − link = None if issued.delivery.delivered else issued.link
98 + link = None if (issued.delivery.delivered or issued.delivery.queued) else issued.link
74 99 else:
75 100 # existing user: never touch the tier (use `users set-tier`), only promote to admin when asked
76 101 if admin and u.role != "admin":
@@ -80,7 +105,7 @@ def provision(s, name: str, email: str, *, tier: str, admin: bool, send_mail: bo
80 105 created_key, _ = service.create_key(s, u, "default", actor=ACTOR, notify=False)
81 106 if u.status == "invited" and not service.pending_link(s, u, "invite"):
82 107 issued = service.issue_token(s, u, "invite", send_mail=send_mail, actor=ACTOR)
83 − link = None if issued.delivery.delivered else issued.link
108 + link = None if (issued.delivery.delivered or issued.delivery.queued) else issued.link
84 109 return u, created_key, link
85 110
86 111
@@ -133,7 +158,8 @@ def cmd_users_invite_resend(a) -> int:
133 158 if not service.active_keys(s, u):
134 159 service.create_key(s, u, "default", actor=ACTOR, notify=False)
135 160 issued = service.issue_token(s, u, "invite", send_mail=not a.no_mail, actor=ACTOR)
136 − print(f"{u.email}: invitation " + ("sent by e-mail" if issued.delivery.delivered else f"link → {issued.link}"))
161 + print(f"{u.email}: invitation " + ("queued for e-mail delivery" if issued.delivery.queued or issued.delivery.delivered
162 + else f"link → {issued.link}"))
137 163 return 0
138 164
139 165
@@ -163,8 +189,9 @@ def cmd_keys_list(a) -> int:
163 189
164 190 def cmd_seed(a) -> int:
165 191 rows = []
192 + seed_users = load_seed(a.file)
166 193 with session() as s:
167 − for spec in SEED_USERS:
194 + for spec in seed_users:
168 195 u, raw, link = provision(s, spec["name"], spec["email"], tier=spec["tier"], admin=spec["role"] == "admin",
169 196 send_mail=not a.no_mail)
170 197 rows.append(_row(s, u, raw, link, a.show_key))
@@ -200,7 +227,8 @@ def build_parser() -> argparse.ArgumentParser:
200 227 kl = keys.add_parser("list"); kl.add_argument("email"); kl.set_defaults(fn=cmd_keys_list)
201 228 kr = keys.add_parser("rotate", help="revoke + recreate the user's key"); kr.add_argument("email"); kr.add_argument("--show-key", action="store_true"); kr.set_defaults(fn=cmd_keys_rotate)
202 229
203 − seed = sub.add_parser("seed", help="create the initial users (idempotent)")
230 + seed = sub.add_parser("seed", help=f"create the initial users from a JSON file or ${SEED_ENV} (idempotent)")
231 + seed.add_argument("file", nargs="?", help="JSON list of {name, email, tier, role}")
204 232 seed.add_argument("--show-key", action="store_true")
205 233 seed.add_argument("--no-mail", action="store_true")
206 234 seed.set_defaults(fn=cmd_seed)
207 235