correctifs prod : fond opaque de repli (Safari sans color-mix) pour topbar/tiroir/nav docs, thème sombre par défaut ; clés utilisables pour les comptes invités ; normalisation EDGAR tolère les dates manquantes (NaT) ; import robuste des modèles dans l'agrégation d'usage
8 changed files +40 −23
modified
hfmarketdata/api/fundamentals/normalize.py
+11 −1
@@ -222,8 +222,18 @@ class FiscalCalendar: | ||
| 222 | 222 | return fe - timedelta(days=round(91.3125 * (4 - fq))) |
| 223 | 223 | |
| 224 | 224 | |
| 225 | +def _is_missing(d) -> bool: | |
| 226 | + """None or pandas NaT / NaN (companyfacts leaves `start` empty on instant facts and on some odd rows).""" | |
| 227 | + if d is None: | |
| 228 | + return True | |
| 229 | + try: | |
| 230 | + return bool(d != d) # NaT/NaN are not equal to themselves | |
| 231 | + except Exception: | |
| 232 | + return False | |
| 233 | + | |
| 234 | + | |
| 225 | 235 | def span_type(start: date | None, end: date) -> str | None: |
| 226 | − if start is None: | |
| 236 | + if _is_missing(start) or _is_missing(end): | |
| 227 | 237 | return None |
| 228 | 238 | days = (end - start).days |
| 229 | 239 | if 75 <= days <= 105: |
modified
hfmarketdata/api/ratelimit/middleware.py
+5 −1
@@ -158,7 +158,7 @@ def resolve_principal(scope: dict) -> Principal: | ||
| 158 | 158 | if info is None or info.key_status != "active": |
| 159 | 159 | raise ApiError(401, "INVALID_API_KEY", "The API key is unknown, malformed or revoked. Check it in your " |
| 160 | 160 | f"dashboard ({settings.public_url}/dashboard) or omit it to use keyless access.") |
| 161 | − if info.user_status != "active": | |
| 161 | + if info.user_status == "disabled": # `invited` accounts (created by an admin) can use their key right away | |
| 162 | 162 | raise ApiError(403, "ACCOUNT_DISABLED", "This API key belongs to a disabled account. " |
| 163 | 163 | f"Contact {settings.contact_email}.") |
| 164 | 164 | return Principal(f"key:{info.key_id}", "key", tier_for(info.tier), info.user_id, info.key_id) |
@@ -410,6 +410,10 @@ def stream_charger(principal: str, rows: int) -> None: | ||
| 410 | 410 | |
| 411 | 411 | def install(app: FastAPI) -> None: |
| 412 | 412 | """Mount the middleware INSIDE the CORS layer so 429/401 responses keep their CORS headers.""" |
| 413 | + try: # import the account models once, on the main thread (avoids a duplicate-definition race in the usage folder thread) | |
| 414 | + import accounts.models # noqa: F401 | |
| 415 | + except Exception: | |
| 416 | + pass | |
| 413 | 417 | try: # stream module (fundamentals filings WebSocket) → charge delivered messages to the rows quota |
| 414 | 418 | from stream import accounting as _stream_accounting |
| 415 | 419 | _stream_accounting.set_charger(stream_charger) |
modified
hfmarketdata/api/ratelimit/usage.py
+5 −2
@@ -161,8 +161,11 @@ def fold(now_s: float | None = None) -> int: | ||
| 161 | 161 | |
| 162 | 162 | |
| 163 | 163 | def _models(): |
| 164 | − from accounts import models | |
| 165 | − return models | |
| 164 | + import sys | |
| 165 | + mod = sys.modules.get("accounts.models") | |
| 166 | + if mod is None: | |
| 167 | + from accounts import models as mod # type: ignore[no-redef] | |
| 168 | + return mod | |
| 166 | 169 | |
| 167 | 170 | |
| 168 | 171 | async def _folder_loop(interval_s: int = 60) -> None: |
modified
hfmarketdata/web/index.html
+1 −1
@@ -20,7 +20,7 @@ | ||
| 20 | 20 | // Theme is resolved before first paint (no flash): localStorage → system preference → dark. |
| 21 | 21 | try { |
| 22 | 22 | var t = localStorage.getItem('hfmd.theme'); |
| 23 | − if (t !== 'light' && t !== 'dark') t = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; | |
| 23 | + if (t !== 'light' && t !== 'dark') t = 'dark'; | |
| 24 | 24 | document.documentElement.setAttribute('data-theme', t); |
| 25 | 25 | } catch (e) {} |
| 26 | 26 | </script> |
modified
hfmarketdata/web/src/app/theme.css
+6 −6
@@ -60,7 +60,7 @@ button { font: inherit; } | ||
| 60 | 60 | |
| 61 | 61 | /* ---- shell ---- */ |
| 62 | 62 | .shell { min-height: 100dvh; display: flex; flex-direction: column; } |
| 63 | −.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; gap: 20px; padding: 0 20px; height: var(--topbar-h); background: color-mix(in srgb, var(--bg) 86%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); } | |
| 63 | +.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; gap: 20px; padding: 0 20px; height: var(--topbar-h); background: var(--bg); background: color-mix(in srgb, var(--bg) 86%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); } | |
| 64 | 64 | .brand { display: inline-flex; align-items: center; gap: 9px; color: var(--fg); font-weight: 650; letter-spacing: -0.01em; white-space: nowrap; } |
| 65 | 65 | .brand:hover { text-decoration: none; } |
| 66 | 66 | .brand-mark { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 7px; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 55%, var(--accent-2))); color: var(--accent-ink); font-family: var(--mono); font-size: 12px; font-weight: 700; } |
@@ -80,7 +80,7 @@ button { font: inherit; } | ||
| 80 | 80 | .btn:hover { border-color: var(--line-3); text-decoration: none; background: var(--bg-2); } |
| 81 | 81 | .btn:active { transform: translateY(1px); } |
| 82 | 82 | .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 650; } |
| 83 | −.btn-primary:hover { background: color-mix(in srgb, var(--accent) 88%, white); border-color: color-mix(in srgb, var(--accent) 88%, white); } | |
| 83 | +.btn-primary:hover { background: var(--accent); background: color-mix(in srgb, var(--accent) 88%, white); border-color: color-mix(in srgb, var(--accent) 88%, white); } | |
| 84 | 84 | .btn-ghost { background: transparent; border-color: transparent; color: var(--fg-1); } |
| 85 | 85 | .btn-ghost:hover { background: var(--bg-2); color: var(--fg); border-color: transparent; } |
| 86 | 86 | .btn-lg { padding: 12px 20px; font-size: 15px; border-radius: 10px; } |
@@ -109,7 +109,7 @@ button { font: inherit; } | ||
| 109 | 109 | table { border-collapse: collapse; width: 100%; font-size: 14px; } |
| 110 | 110 | th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; } |
| 111 | 111 | th { color: var(--fg-1); font-weight: 600; font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; } |
| 112 | −tbody tr:hover { background: color-mix(in srgb, var(--bg-2) 60%, transparent); } | |
| 112 | +tbody tr:hover { background: var(--bg-2); background: color-mix(in srgb, var(--bg-2) 60%, transparent); } | |
| 113 | 113 | .table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); } |
| 114 | 114 | .table-wrap table { min-width: 520px; } |
| 115 | 115 | .table-wrap th { background: var(--bg-1); position: sticky; top: 0; white-space: nowrap; overflow-wrap: normal; } |
@@ -118,12 +118,12 @@ tbody tr:hover { background: color-mix(in srgb, var(--bg-2) 60%, transparent); } | ||
| 118 | 118 | |
| 119 | 119 | input, select, textarea { font: inherit; color: var(--fg); background: var(--bg-1); border: 1px solid var(--line-2); border-radius: 6px; padding: 8px 10px; } |
| 120 | 120 | input:focus, select:focus, textarea:focus { outline: 2px solid color-mix(in srgb, var(--accent) 50%, transparent); border-color: var(--accent); } |
| 121 | −::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); } | |
| 121 | +::selection { background: var(--accent); background: color-mix(in srgb, var(--accent) 35%, transparent); } | |
| 122 | 122 | @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } |
| 123 | 123 | |
| 124 | 124 | /* ---- mobile drawer ---- */ |
| 125 | −.drawer-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, .55); } | |
| 126 | −.drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 61; width: min(320px, 86vw); background: var(--bg-1); border-left: 1px solid var(--line); padding: 16px; display: flex; flex-direction: column; gap: 6px; box-shadow: var(--shadow); } | |
| 125 | +.drawer-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, .62); } | |
| 126 | +.drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 61; width: min(320px, 86vw); background: var(--bg-1); background-color: var(--bg-1); isolation: isolate; overflow-y: auto; -webkit-overflow-scrolling: touch; border-left: 1px solid var(--line); padding: 16px; display: flex; flex-direction: column; gap: 6px; box-shadow: var(--shadow); } | |
| 127 | 127 | .drawer-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } |
| 128 | 128 | .drawer a.navlink { display: block; padding: 10px 12px; border-radius: 8px; color: var(--fg-1); font-weight: 500; } |
| 129 | 129 | .drawer a.navlink.active, .drawer a.navlink:hover { background: var(--bg-2); color: var(--fg); text-decoration: none; } |
modified
hfmarketdata/web/src/components/components.css
+1 −1
@@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | /* Code */ |
| 4 | 4 | .code { border: 1px solid var(--line); border-radius: var(--radius); background: var(--code-bg); overflow: hidden; margin: 0 0 16px; min-width: 0; max-width: 100%; } |
| 5 | −.code-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px 6px 12px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--bg-1) 70%, var(--code-bg)); min-height: 34px; flex-wrap: wrap; } | |
| 5 | +.code-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px 6px 12px; border-bottom: 1px solid var(--line); background: var(--bg-1); background: color-mix(in srgb, var(--bg-1) 70%, var(--code-bg)); min-height: 34px; flex-wrap: wrap; } | |
| 6 | 6 | .code-title { font-family: var(--mono); font-size: 12px; color: var(--fg-2); letter-spacing: .02em; } |
| 7 | 7 | /* contain: inline-size → the (scrolling) pre never inflates the intrinsic width of grid/flex ancestors on narrow screens */ |
| 8 | 8 | .code-pre { margin: 0; border: 0; border-radius: 0; background: transparent; padding: 12px 14px; font-size: 13px; line-height: 1.55; tab-size: 2; contain: inline-size; } |
modified
hfmarketdata/web/src/pages/docs/docs.css
+1 −1
@@ -126,7 +126,7 @@ | ||
| 126 | 126 | @media (max-width: 900px) { |
| 127 | 127 | .docs { grid-template-columns: 1fr; } |
| 128 | 128 | .docs-nav { display: none; } |
| 129 | − .docs-nav.open { display: block; position: fixed; inset: var(--topbar-h) 0 0 0; z-index: 45; height: auto; background: var(--bg); border-right: 0; padding: 16px; } | |
| 129 | + .docs-nav.open { display: block; position: fixed; inset: var(--topbar-h) 0 0 0; z-index: 45; height: auto; background: var(--bg); background-color: var(--bg); isolation: isolate; border-right: 0; padding: 16px; } | |
| 130 | 130 | .docs-nav-toggle { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-bottom: 1px solid var(--line); background: var(--bg-1); position: sticky; top: var(--topbar-h); z-index: 40; font-size: 13.5px; color: var(--fg-1); } |
| 131 | 131 | .docs-nav-toggle button { display: inline-flex; align-items: center; gap: 6px; } |
| 132 | 132 | .docs-content { padding: 24px 16px 60px; } |
modified
hfmarketdata/web/src/playground/playground.css
+10 −10
@@ -20,9 +20,9 @@ | ||
| 20 | 20 | .pg-type.active { background: var(--bg-1); border-color: var(--line-2); color: var(--fg); } |
| 21 | 21 | .pg-type-title { font-weight: 500; } |
| 22 | 22 | .pg-type-path { grid-column: 2; font-size: 11px; color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 23 | −.pg-method { font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; align-self: center; background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); } | |
| 24 | −.pg-method.ws { background: color-mix(in srgb, var(--accent-2) 18%, transparent); color: var(--accent-2); } | |
| 25 | −.pg-method.post, .pg-method.patch, .pg-method.delete { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); } | |
| 23 | +.pg-method { font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; align-self: center; background: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); } | |
| 24 | +.pg-method.ws { background: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 18%, transparent); color: var(--accent-2); } | |
| 25 | +.pg-method.post, .pg-method.patch, .pg-method.delete { background: var(--warn); background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); } | |
| 26 | 26 | |
| 27 | 27 | .pg-form-section { border-top: 1px solid var(--line); padding-top: 14px; } |
| 28 | 28 | .pg-desc { font-size: 13px; margin: 0 0 12px; } |
@@ -55,8 +55,8 @@ | ||
| 55 | 55 | .pg-kbd { font-size: 12px; } |
| 56 | 56 | .pg-status { display: inline-flex; align-items: center; gap: 10px; font-size: 13px; } |
| 57 | 57 | .pg-code-badge { padding: 2px 8px; border-radius: 4px; font-family: var(--mono); font-size: 12px; font-weight: 600; } |
| 58 | −.pg-code-badge.ok { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); } | |
| 59 | −.pg-code-badge.bad { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); } | |
| 58 | +.pg-code-badge.ok { background: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); } | |
| 59 | +.pg-code-badge.bad { background: var(--danger); background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); } | |
| 60 | 60 | .pg-err-details { margin: 8px 0 0; font-size: 12px; } |
| 61 | 61 | |
| 62 | 62 | .pg-results { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-1); overflow: hidden; } |
@@ -111,7 +111,7 @@ | ||
| 111 | 111 | .pg-rl-fill.warn { background: var(--warn); } |
| 112 | 112 | .pg-rl-fill.danger { background: var(--danger); } |
| 113 | 113 | .pg-rl-foot { font-size: 12px; margin: 0; } |
| 114 | −.pg-rl-429 { font-size: 13px; padding: 8px 10px; border-radius: 6px; background: color-mix(in srgb, var(--danger) 15%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent); } | |
| 114 | +.pg-rl-429 { font-size: 13px; padding: 8px 10px; border-radius: 6px; background: var(--danger); background: color-mix(in srgb, var(--danger) 15%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent); } | |
| 115 | 115 | |
| 116 | 116 | .pg-code { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: #011627; } |
| 117 | 117 | .pg-code-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; background: var(--bg-2); border-bottom: 1px solid var(--line); font-size: 12px; color: var(--fg-1); } |
@@ -124,10 +124,10 @@ | ||
| 124 | 124 | .pg-preset:hover { color: var(--fg); border-color: var(--fg-2); } |
| 125 | 125 | |
| 126 | 126 | .pg-callout { display: flex; justify-content: space-between; gap: 14px; align-items: center; padding: 12px 14px; border-radius: var(--radius); border: 1px solid var(--line); background: var(--bg-1); font-size: 13.5px; flex-wrap: wrap; } |
| 127 | −.pg-callout-info { border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); background: color-mix(in srgb, var(--accent-2) 8%, var(--bg-1)); } | |
| 128 | −.pg-callout-warn { border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: color-mix(in srgb, var(--warn) 8%, var(--bg-1)); } | |
| 129 | −.pg-callout-danger { border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: color-mix(in srgb, var(--danger) 8%, var(--bg-1)); } | |
| 130 | −.pg-callout-success { border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: color-mix(in srgb, var(--accent) 8%, var(--bg-1)); } | |
| 127 | +.pg-callout-info { border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); background: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 8%, var(--bg-1)); } | |
| 128 | +.pg-callout-warn { border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: var(--warn); background: color-mix(in srgb, var(--warn) 8%, var(--bg-1)); } | |
| 129 | +.pg-callout-danger { border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger); background: color-mix(in srgb, var(--danger) 8%, var(--bg-1)); } | |
| 130 | +.pg-callout-success { border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--bg-1)); } | |
| 131 | 131 | .pg-callout-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 220px; } |
| 132 | 132 | .pg-callout-title { font-size: 14px; } |
| 133 | 133 | |
| 134 | 134 | |