v2 socle : core (config, erreurs uniformes, SQLite, DuckDB, réponses json/csv/parquet), OpenAPI 3.1, chargeur de modules v2, SPA fallback, tests de non-régression + lac synthétique, shell web (router, layout, tokens, client API), plan d'upgrade
38 changed files +3,248 −521
modified
.gitignore
+5 −0
@@ -22,3 +22,8 @@ hfmarketdata/venv/ | ||
| 22 | 22 | |
| 23 | 23 | # OS / editor |
| 24 | 24 | .DS_Store |
| 25 | + | |
| 26 | +# python env local | |
| 27 | +.venv/ | |
| 28 | +.pytest_cache/ | |
| 29 | +.ruff_cache/ | |
added
CLAUDE.md
+20 −0
@@ -0,0 +1,20 @@ | ||
| 1 | +# hfmarketdata — conventions de travail (lues par tous les agents) | |
| 2 | + | |
| 3 | +Plateforme : **www.hfmarketdata.io** — API FastAPI + DuckDB sur un lac Parquet (350 Go, FirstRate Data), site React/Vite servi par l'API. Plan complet : `docs/UPGRADE-PLAN.md` (lire en premier). Prod : nœud **M3U96b** (`~/hfmarketdata`, PM2 `hfmarketdata-api` :8090, lac `~/firstratedata`, Redis local), déployé par `mld` depuis la passerelle M1M32. Dépôt de vérité : spbgit `gitsrv:srv/git/hfmarketdata.git` (branche `main`). | |
| 4 | + | |
| 5 | +## Règles absolues | |
| 6 | +- **Aucun breaking change** : les endpoints `/v1/*` existants (main.py) gardent paths, paramètres, formes de réponse (`{"count","data"}`) et codes. Tout le neuf est additif sous `/v1/`. | |
| 7 | +- **Erreurs** : lever `core.errors.ApiError(status, CODE, message, type=…, details=…)` ; les codes vivent dans `core/errors.py::CODES` (en ajouter si besoin, jamais de code inconnu). Jamais de `HTTPException` dans le code v2. | |
| 8 | +- **Réponses v2** : `core.responses.frame_response(df, fmt, meta=…, request=…)` pour les tableaux (json/csv/parquet, enveloppe `{"data","meta"}`, `X-Row-Count`), `json_response(...)` pour le reste, `clamp_limit(...)` pour `limit`, curseurs via `encode_cursor/decode_cursor`. Timestamps UTC ISO 8601. | |
| 9 | +- **Données** : DuckDB via `core.duck.con()` (connexion par thread) + `core.duck.cached(key, builder)` pour les scans de répertoires. `settings.parquet` = racine du lac. **Jamais de valeur inventée** : absence → `null` + raison dans `coverage`. | |
| 10 | +- **Métadonnées** : SQLite via `core.db` (`Base`, `session()`, `get_session` dépendance FastAPI, `create_all()` idempotent au démarrage du module). Un module = un fichier `models.py`. | |
| 11 | +- **Config** : uniquement `core.config.settings` (variables `HFMD_*`). Secrets jamais en dur, jamais dans les exemples publics. | |
| 12 | +- **Structure** : `hfmarketdata/api/<module>/{routes.py,models.py,service.py,…}` ; `routes.py` expose `router` (APIRouter avec `prefix="/v1/…"`, `tags=[…]`) OU `install(app)` (middleware). `main.py` charge les modules listés dans `V2_MODULES` — ne pas éditer main.py au-delà de cette liste. Imports absolus depuis `hfmarketdata/api` (ex. `from core.errors import ApiError`). | |
| 13 | +- **Quota lignes** : toujours poser `X-Row-Count` (fait par `frame_response`). Endpoints coûteux : `request.state.request_cost = 2` (screener, frames). Bulk : `request.state.quota_exempt = True`. | |
| 14 | +- **Style** : Python 3.12+ typé, docstrings en anglais (le produit est en anglais), commentaires courts. Frontend : React 18 + Vite, dark par défaut, pas de framework CSS lourd (CSS modules / variables), composants dans `hfmarketdata/web/src`. | |
| 15 | +- **Tests** : pytest dans `tests/` (`pytest.ini` met `hfmarketdata/api` sur le path) ; lac synthétique `tests/fixtures/make_fixtures.py` (l'étendre si un module a besoin d'autres fichiers) ; Redis = `fakeredis` quand `HFMD_REDIS_URL=fakeredis://`. Chaque module livre ses tests unitaires + intégration (TestClient). `./.venv/bin/python -m pytest` doit rester vert. | |
| 16 | +- **OpenAPI 3.1** : chaque route a `summary`, `description` (markdown), `response_model` ou `responses={…}` avec exemples réels, et déclare ses erreurs possibles via `openapi_extra={"x-errors": ["CONTRACT_NOT_FOUND", …]}`. La doc du site est générée depuis `/openapi.json` : la qualité des descriptions EST la doc. | |
| 17 | +- **Git** : commits atomiques en français, préfixe du chantier (`futures:`, `accounts:`, `ratelimit:`, `fundamentals:`, `web:`, `mcp:`, `docs:`). Ne pas committer `.venv`, `node_modules`, `dist`, données. | |
| 18 | + | |
| 19 | +## Données réelles (référence, pas pour les tests) | |
| 20 | +Layout : `parquet/{stock|etf|crypto|index|fx}/{1min|5min|30min|1hour|1day}/{adj}/{TICKER}_{tf}.parquet` · `parquet/futures/{tf}/{contin_UNadj|contin_adj_ratio|contin_adj_absolute}/{ROOT}_{tf}.parquet` · `parquet/futures_contracts/{tf}/{archive|update}/{ROOT}_{MonthCode}{YY}_{tf}.parquet` (142 racines, ~15 000 contrats/tf, la colonne `ticker` = racine ; archive ≤ 2025, update ≥ 2025 avec chevauchement → dédupliquer sur `datetime`, priorité update) · `parquet/options/{yyyy}_{qN}/{TICKER}_month_option_chain.parquet` · `meta/futures/futures.csv` (Ticker, Name, First Date, Last Date). Colonnes barres : ticker, datetime (TIMESTAMP naïf, heure US/Eastern pour l'intraday), open, high, low, close, volume, open_interest (futures 1day). API publique pour vérifier : `https://www.hfmarketdata.io/v1/status`. Accès au nœud si indispensable : `ssh M3U96b` (lecture seule sur `~/firstratedata`). | |
added
docs/UPGRADE-PLAN.md
+164 −0
@@ -0,0 +1,164 @@ | ||
| 1 | +# HF Market Data — Upgrade majeur (v2) : plan d'exécution | |
| 2 | + | |
| 3 | +Date : 2026-09-04 · Auteur : Simon-Pierre Boucher · Statut : livrable 1 (schéma · plan de doc · maquettes), puis implémentation chantier par chantier. | |
| 4 | + | |
| 5 | +Contraintes retenues : aucun breaking change (`/v1/*` existants inchangés, erreurs enrichies mais `detail` conservé), UTC/ISO 8601, erreurs JSON uniformes `{"error":{"code","message","docs"}}`, p95 < 300 ms pour 10 000 barres, clés hashées, rate limit aussi sur l'inscription. | |
| 6 | + | |
| 7 | +## 0. Architecture cible | |
| 8 | + | |
| 9 | +``` | |
| 10 | +hfmarketdata/ | |
| 11 | + api/ FastAPI (Python 3.14, DuckDB sur le lac Parquet, SQLite métadonnées, Redis quotas) | |
| 12 | + main.py app + endpoints historiques (inchangés) + montage des routeurs v2 | |
| 13 | + core/config.py env (HFMD_*) · core/errors.py (format uniforme) · core/db.py (SQLite/SQLAlchemy) | |
| 14 | + futures/ chantier 1 : symbols.py (parsing), specs.py (référentiel racines), rolls.py, backfill.py, routes.py | |
| 15 | + accounts/ chantier 2 : models.py, security.py (hash/PAT), routes_auth.py, routes_me.py, routes_admin.py, mailer.py, cli.py | |
| 16 | + ratelimit/ chantier 2 : redis_limiter.py (Lua), middleware.py, tiers.py | |
| 17 | + openapi.py spec 3.1 enrichie (tags, exemples, codes d'erreur) = source unique de la doc | |
| 18 | + web/ Vite + React 18 + react-router · dark par défaut · SPA code-splittée (pré-rendu des pages statiques au build) | |
| 19 | + src/app/ shell, nav, thème, auth context | |
| 20 | + src/pages/{home,docs,playground,integrations,pricing,status,auth,dashboard,admin} | |
| 21 | + src/docs/ générateur 3 colonnes depuis /openapi.json + guides MDX (content/guides/*.mdx) + changelog | |
| 22 | + src/playground/ constructeur de requêtes, exécution, tableau, mini-graphique, export de code, headers quota | |
| 23 | + mcp/ chantier 5 : serveur MCP `hfmarketdata-mcp` (TypeScript, stdio) + README vitrine | |
| 24 | + skills/ chantier 5 : pack de skills (SKILL.md + scripts) → web/public/downloads/hfmarketdata-skills.zip | |
| 25 | + tests/ pytest (unit + intégration TestClient + Redis) · web/e2e (Playwright) | |
| 26 | + scripts/backfill_contracts.py chantier 1 : backfill idempotent des échéances (2010 →) | |
| 27 | +frd_downloader.py inchangé | |
| 28 | +``` | |
| 29 | + | |
| 30 | +Stockage : **SQLite** (`HFMD_STATE_DB`, défaut `~/firstratedata/state/hfmd.db`) pour comptes, clés, tiers, usage journalier et `futures_contracts` ; **Redis** (`HFMD_REDIS_URL`) pour les fenêtres glissantes de quotas (requêtes + lignes), scripts Lua atomiques. | |
| 31 | + | |
| 32 | +## 1. Schéma de base de données | |
| 33 | + | |
| 34 | +### 1.1 `futures_roots` (référentiel des produits) | |
| 35 | +| colonne | type | note | | |
| 36 | +|---|---|---| | |
| 37 | +| root | TEXT PK | ex. ES, CL, 6E (FirstRate : `E6` → alias `6E` accepté) | | |
| 38 | +| name | TEXT | « E-mini S&P 500 » | | |
| 39 | +| exchange | TEXT | CME, CBOT, NYMEX, COMEX, ICE, EUREX, … | | |
| 40 | +| asset_class | TEXT | equity_index, energy, metals, rates, ags, fx, crypto, volatility, softs, livestock | | |
| 41 | +| currency | TEXT | USD, EUR, … | | |
| 42 | +| contract_size | REAL / TEXT | « 50 × index », 1 000 bbl… (valeur numérique + `contract_size_unit`) | | |
| 43 | +| tick_size | REAL | | | |
| 44 | +| tick_value | REAL | | | |
| 45 | +| settlement_type | TEXT | cash / physical | | |
| 46 | +| expiry_rule | TEXT | clé de règle (ex. `third_friday`, `cl_rule`, `last_business_day`, `data`) | | |
| 47 | +| month_cycle | TEXT | ex. `HMUZ`, `FGHJKMNQUVXZ` | | |
| 48 | +| first_data_date, last_data_date | DATE | dérivés du lac (min/max sur tous les contrats) | | |
| 49 | +| contracts_count | INT | | | |
| 50 | +| source | TEXT | `reference` (table statique) / `derived` | | |
| 51 | + | |
| 52 | +### 1.2 `futures_contracts` | |
| 53 | +| colonne | type | note | | |
| 54 | +|---|---|---| | |
| 55 | +| symbol | TEXT PK | forme courte `ESZ25` ; forme longue `ESZ2025` acceptée en entrée | | |
| 56 | +| root | TEXT FK | | | |
| 57 | +| month_code | CHAR(1) | F G H J K M N Q U V X Z | | |
| 58 | +| contract_month | INT (1-12) · contract_year | INT | | | |
| 59 | +| expiration_date | DATE | règle par racine, sinon `last_data_date` ; `expiration_source` = rule / data | | |
| 60 | +| last_trading_date | DATE | | | |
| 61 | +| first_notice_date | DATE | NULL si cash-settled | | |
| 62 | +| settlement_type, contract_size, tick_size, tick_value, currency, exchange | | copiés de la racine | | |
| 63 | +| first_data_date, last_data_date | DATE | plage réelle (1day) | | |
| 64 | +| status | TEXT | active / expired (expired si `last_data_date` < aujourd'hui − 7 j ET mois d'échéance passé) | | |
| 65 | +| volume_avg_daily | REAL | 20 dernières séances | | |
| 66 | +| open_interest_last | REAL | dernier OI non nul | | |
| 67 | +| timeframes | TEXT | JSON des granularités disponibles | | |
| 68 | +| files | TEXT | JSON `{tf: [archive_path, update_path]}` | | |
| 69 | +| updated_at | TIMESTAMP | | | |
| 70 | +Index : (root, expiration_date), (status), (root, contract_year, contract_month). | |
| 71 | + | |
| 72 | +### 1.3 `futures_contract_gaps` — `symbol, timeframe, gap_start, gap_end, bars_missing` (gaps > 3 jours ouvrés, calculés au backfill). | |
| 73 | + | |
| 74 | +### 1.4 Comptes / clés / quotas | |
| 75 | +| table | colonnes | | |
| 76 | +|---|---| | |
| 77 | +| users | id PK, email UNIQUE, name, password_hash (argon2), email_verified_at, role (`user`/`admin`), tier (`free`/`high_usage`), status (`invited`/`active`/`disabled`), created_at, last_login_at | | |
| 78 | +| email_tokens | id, user_id FK, kind (`verify`/`reset`/`invite`), token_hash, expires_at, used_at | | |
| 79 | +| api_keys | id PK, user_id FK, name, prefix (8 car. affichés : `hfmd_live_ab12cd34`), key_hash (sha256 + sel serveur), tier_override (nullable), status (`active`/`revoked`), created_at, last_used_at, revoked_at | | |
| 80 | +| usage_daily | day, principal (`key:<id>` ou `ip:<hash>`), requests, rows, rows_parquet, bytes, status_2xx, status_429 — PK (day, principal) — agrégé depuis Redis toutes les minutes | | |
| 81 | +| usage_minute | minute, principal, requests, rows — 7 j glissants (graphes 24 h / 7 j) | | |
| 82 | +| audit_log | ts, actor, action, target, meta | | |
| 83 | + | |
| 84 | +Format d'une clé : `hfmd_live_<32 car. base62>` ; on n'en stocke que le hash et le préfixe ; affichée une seule fois à la création ; révocable ; « régénérer » = révoque + crée. | |
| 85 | + | |
| 86 | +### 1.5 Tiers (constantes `ratelimit/tiers.py`) | |
| 87 | +| tier | fenêtre | requêtes | lignes | lignes max / requête | | |
| 88 | +|---|---|---|---|---| | |
| 89 | +| keyless (IP) | 1 h | 30 | 100 000 | 5 000 | | |
| 90 | +| free (compte + clé) | 1 min | 120 | 1 000 000 | 50 000 | | |
| 91 | +| high_usage (sur demande à contact@spboucher.ai) | 1 min | 600 | 10 000 000 | 200 000 | | |
| 92 | + | |
| 93 | +Redis : clé `rl:{principal}:{req|rows}` — fenêtre glissante par buckets de 1 s (hash) évaluée en Lua : `EVALSHA limiter <key> <now_ms> <window_ms> <cost> <limit>` → `{allowed, remaining, reset_ms}` ; deux appels dans un même script pour requêtes et lignes. Parquet = coût lignes ÷ 2 ; `/v1/bulk/*` et réponses 304 = 0 ligne. Headers `X-RateLimit-*` sur toute réponse ; 429 `Retry-After` + `error.type`. | |
| 94 | + | |
| 95 | +## 2. Contrat d'API — nouveautés (toutes additives) | |
| 96 | + | |
| 97 | +Chantier 1 · `GET /v1/futures/roots` · `GET /v1/futures/{root}/contracts` · `GET /v1/futures/contract/{symbol}/bars` · `GET /v1/futures/contract/{symbol}/coverage` · `GET /v1/futures/{root}/chain?as_of=` · `GET /v1/futures/{root}/continuous?roll=&adjust=&depth=` · `GET /v1/futures/{root}/term-structure?as_of=`. | |
| 98 | +Paramètres communs : `interval` (`1m|5m|30m|1h|1d`, alias de `timeframe`), `from`/`to`, `session=rth|eth|all`, `cursor`/`limit`, `format=json|csv|parquet`. Réponses : `{"data":[...], "meta":{"symbol","interval","count","next_cursor","roll_dates":[...]}}`. | |
| 99 | +Erreurs : 400 `INVALID_CONTRACT_SYMBOL`, 404 `CONTRACT_NOT_FOUND`, 404 `ROOT_NOT_FOUND`, 400 `INVALID_PARAMETER`, 429 `RATE_LIMIT_EXCEEDED`, 401 `INVALID_API_KEY`. | |
| 100 | + | |
| 101 | +Chantier 2 · `/v1/auth/{signup,verify,login,logout,forgot,reset,accept-invite}` · `/v1/me` · `/v1/me/keys` (POST/GET/DELETE/POST …/rotate) · `/v1/me/usage?range=24h|7d|30d` · `/v1/admin/users` (CRUD, tier, invitation) · `/v1/admin/usage` · `/v1/limits` (public : tiers + limites courantes du principal). | |
| 102 | + | |
| 103 | +## 3. Plan de la documentation (`/docs`) | |
| 104 | + | |
| 105 | +Génération : `/openapi.json` (3.1, exemples réels capturés au build) → référence ; `content/guides/*.mdx` → guides ; `content/changelog.mdx`. Layout 3 colonnes (nav · texte · code curl/Python/JS/R avec langue persistante), Cmd+K, « Try it » → `/playground?ep=…¶ms…`, lien « Edit / Report issue » (mailto contact@spboucher.ai + lien repo), dark par défaut, mobile : nav en tiroir, colonne code repliable. | |
| 106 | + | |
| 107 | +``` | |
| 108 | +Getting started Guides Reference (OpenAPI) More | |
| 109 | + Quickstart (30 s) Futures individual contracts Meta & status Changelog | |
| 110 | + Authentication Options chains & Greeks Bars (stocks/ETF/crypto/…) Versioning & deprecation | |
| 111 | + Rate limits Time zones & sessions (RTH/ETH) Futures v2 (7 endpoints) Limits & pricing | |
| 112 | + Data formats Bulk downloads Options Integrations (MCP, skills) | |
| 113 | + Recipes: pandas backtest · Accounts & keys · Usage Status | |
| 114 | + custom continuous · CL term structure Errors | |
| 115 | +``` | |
| 116 | + | |
| 117 | +## 4. Maquettes | |
| 118 | + | |
| 119 | +### 4.1 Playground (`/playground`, aussi intégré au dashboard avec clé injectée) | |
| 120 | +``` | |
| 121 | +┌ HF Market Data ─ Docs · Playground · Integrations · Pricing · Status ─────────────── [Sign in] ┐ | |
| 122 | +│ ⓘ Keyless mode: 30 req/h. Create a free account for 120 req/min → [Create free account] │ | |
| 123 | +├──────────────────────────────┬─────────────────────────────────────────────────────────────────┤ | |
| 124 | +│ Request type ▾ │ GET https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=… │ [Copy] | |
| 125 | +│ ○ Stock/ETF bars │ ──────────────────────────────────────────────────────────────── │ | |
| 126 | +│ ○ Crypto · FX │ [Run ▶] 200 OK · 184 ms · 2 512 rows │ | |
| 127 | +│ ● Futures continuous │ ┌ Table ─┬ JSON ─┬ Chart ──────────────────────────────────────┐ │ | |
| 128 | +│ ○ Individual contract │ │ ▂▃▅▆▇█▇▆▅ candlesticks (lightweight-charts) roll markers ▲ │ │ | |
| 129 | +│ ○ Contract chain │ └─────────────────────────────────────────────────────────────┘ │ | |
| 130 | +│ ○ Term structure │ Rate limit: requests 29/30 · rows 97 488/100 000 · reset 41 min │ | |
| 131 | +│ ○ Options chain (Greeks) │ ┌ curl ─┬ Python (requests+pandas) ─┬ JavaScript (fetch) ─────┐ │ | |
| 132 | +│ ○ Symbols / roots │ │ import requests, pandas as pd … │ │ | |
| 133 | +│ ─ Form (dynamic) ─ │ └─────────────────────────────────────────────────────────────┘ │ | |
| 134 | +│ root [ES ▾ autocomplete] │ Examples: AAPL 1-min today · CL chain · ES continuous back-adj │ | |
| 135 | +│ roll [volume ▾] adjust [back_adjusted ▾] depth [1 ▾] 2015-2025 · NG term structure │ | |
| 136 | +│ from [2015-01-01] to [2025-12-31] interval [1d ▾] format [json ▾] session [all ▾] │ | |
| 137 | +└──────────────────────────────┴─────────────────────────────────────────────────────────────────┘ | |
| 138 | +``` | |
| 139 | + | |
| 140 | +### 4.2 Dashboard (`/dashboard`) | |
| 141 | +``` | |
| 142 | +┌ Sidebar: Overview · API keys · Usage · Playground · Account Tier: Free · Need more? contact@spboucher.ai ┐ | |
| 143 | +│ Overview ─ Requests today 1 240 / (120/min) · Rows today 3.1 M · Last request 2 min ago │ | |
| 144 | +│ API keys ─ ┌ name ─ prefix ─ created ─ last used ─ status ─ actions ┐ [+ Create key] │ | |
| 145 | +│ │ default hfmd_live_ab12cd34… 2026-09-04 just now active [Rotate] [Revoke] │ (clé montrée 1 fois) │ | |
| 146 | +│ Usage ─ [24h] [7d] [30d] ▁▂▃▅▆▇█ requests / min ▁▁▂▃▅▆ rows / min · table par jour · export CSV │ | |
| 147 | +│ Playground─ même composant qu'en public, clé injectée automatiquement, bandeau « authenticated as … » │ | |
| 148 | +│ Admin (rôle admin) ─ users (créer · inviter · tier · désactiver) · consommation globale · top principals │ | |
| 149 | +└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| 150 | +``` | |
| 151 | + | |
| 152 | +### 4.3 Pricing / Limits (`/pricing`) | |
| 153 | +Tableau 3 colonnes (Keyless · Free account · High usage) avec les 6 chiffres, CTA « Create free account » et « Request high usage → contact@spboucher.ai », encart « incitations » (Parquet = ½ coût, bulk hors quota, ETag/304 gratuits). | |
| 154 | + | |
| 155 | +### 4.4 Homepage | |
| 156 | +Hero (« Open high-frequency market data. 1-minute to daily, since 2010. ») · bloc « Get an API key » en 3 étapes (keyless → free → high usage) · aperçu live du playground (requête ES continu) · Integrations (Claude Code · Cursor · Codex · MCP) · datasets live (compteurs `/v1/status`). | |
| 157 | + | |
| 158 | +## 5. Ordre d'implémentation | |
| 159 | +1. Socle : config, erreurs uniformes, SQLite, OpenAPI 3.1 enrichie, tests de non-régression des endpoints v1. | |
| 160 | +2. Chantier 1 (futures) + backfill + tests unitaires symboles/rolls. | |
| 161 | +3. Chantier 2 (Redis limiter, clés, comptes, dashboard API, CLI `hfmd users add`, seed des 3 utilisateurs). | |
| 162 | +4. Chantier 6 + 3 + 4 (site, docs 3 colonnes, playground) — frontend. | |
| 163 | +5. Chantier 5 (MCP + skills + page Integrations). | |
| 164 | +6. Tests E2E Playwright, déploiement via `mld` sur M3U96b (Redis + backfill dans le manifeste). | |
added
hfmarketdata/api/core/__init__.py
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +"""Shared runtime for the HF Market Data API (config · errors · SQLite · DuckDB · responses).""" | |
added
hfmarketdata/api/core/config.py
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +"""Runtime configuration — every knob is an HFMD_* environment variable. | |
| 2 | + | |
| 3 | +Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 4 | +""" | |
| 5 | +from __future__ import annotations | |
| 6 | + | |
| 7 | +import os | |
| 8 | +from dataclasses import dataclass, field | |
| 9 | +from pathlib import Path | |
| 10 | + | |
| 11 | + | |
| 12 | +def _env(name: str, default: str | None = None) -> str | None: | |
| 13 | + v = os.environ.get(name) | |
| 14 | + return v if v not in (None, "") else default | |
| 15 | + | |
| 16 | + | |
| 17 | +@dataclass(frozen=True) | |
| 18 | +class Settings: | |
| 19 | + data_root: Path = field(default_factory=lambda: Path(_env("HFMD_DATA_ROOT", "/Volumes/ssd/firstratedata"))) | |
| 20 | + web_dist: Path = field(default_factory=lambda: Path(_env("HFMD_WEB_DIST", str(Path(__file__).resolve().parents[2] / "web" / "dist")))) | |
| 21 | + port: int = field(default_factory=lambda: int(_env("HFMD_PORT", "8090"))) | |
| 22 | + public_url: str = field(default_factory=lambda: _env("HFMD_PUBLIC_URL", "https://www.hfmarketdata.io")) | |
| 23 | + docs_url: str = field(default_factory=lambda: _env("HFMD_DOCS_URL", "https://www.hfmarketdata.io/docs")) | |
| 24 | + contact_email: str = field(default_factory=lambda: _env("HFMD_CONTACT_EMAIL", "contact@spboucher.ai")) | |
| 25 | + # metadata store (accounts, keys, usage, futures contracts, fundamentals) — SQLite, WAL mode | |
| 26 | + state_db: Path = field(default_factory=lambda: Path(_env("HFMD_STATE_DB", str(Path(_env("HFMD_DATA_ROOT", "/Volumes/ssd/firstratedata")) / "state" / "hfmd.db")))) | |
| 27 | + # rate limiting / sessions | |
| 28 | + redis_url: str = field(default_factory=lambda: _env("HFMD_REDIS_URL", "redis://127.0.0.1:6379/0")) | |
| 29 | + ratelimit_enabled: bool = field(default_factory=lambda: _env("HFMD_RATELIMIT", "1") not in ("0", "false", "no")) | |
| 30 | + # secrets | |
| 31 | + secret_key: str = field(default_factory=lambda: _env("HFMD_SECRET_KEY", "dev-only-change-me")) | |
| 32 | + key_hash_salt: str = field(default_factory=lambda: _env("HFMD_KEY_SALT", "hfmd-key-salt-dev")) | |
| 33 | + # e-mail (Resend) — invitations / verification / password reset | |
| 34 | + resend_api_key: str | None = field(default_factory=lambda: _env("HFMD_RESEND_API_KEY")) | |
| 35 | + mail_from: str = field(default_factory=lambda: _env("HFMD_MAIL_FROM", "HF Market Data <noreply@hfmarketdata.io>")) | |
| 36 | + # SEC EDGAR | |
| 37 | + sec_user_agent: str = field(default_factory=lambda: _env("HFMD_SEC_USER_AGENT", "HF Market Data (Simon-Pierre Boucher, contact@spboucher.ai)")) | |
| 38 | + env: str = field(default_factory=lambda: _env("HFMD_ENV", "production")) | |
| 39 | + | |
| 40 | + @property | |
| 41 | + def parquet(self) -> Path: | |
| 42 | + return self.data_root / "parquet" | |
| 43 | + | |
| 44 | + @property | |
| 45 | + def is_dev(self) -> bool: | |
| 46 | + return self.env != "production" | |
| 47 | + | |
| 48 | + | |
| 49 | +settings = Settings() | |
added
hfmarketdata/api/core/db.py
+73 −0
@@ -0,0 +1,73 @@ | ||
| 1 | +"""SQLite metadata store (accounts, API keys, usage, futures contracts, fundamentals). | |
| 2 | + | |
| 3 | +SQLAlchemy 2.x, WAL mode, foreign keys on. One engine per process; `session()` is a context manager. | |
| 4 | +Modules declare their tables on `Base` and call `create_all()` at import/startup (idempotent). | |
| 5 | + | |
| 6 | +Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 7 | +""" | |
| 8 | +from __future__ import annotations | |
| 9 | + | |
| 10 | +import contextlib | |
| 11 | +from collections.abc import Iterator | |
| 12 | +from pathlib import Path | |
| 13 | + | |
| 14 | +from sqlalchemy import create_engine, event, text | |
| 15 | +from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker | |
| 16 | + | |
| 17 | +from .config import settings | |
| 18 | + | |
| 19 | + | |
| 20 | +class Base(DeclarativeBase): | |
| 21 | + pass | |
| 22 | + | |
| 23 | + | |
| 24 | +def _make_engine(path: Path): | |
| 25 | + path.parent.mkdir(parents=True, exist_ok=True) | |
| 26 | + eng = create_engine(f"sqlite:///{path}", future=True, connect_args={"check_same_thread": False, "timeout": 30}) | |
| 27 | + | |
| 28 | + @event.listens_for(eng, "connect") | |
| 29 | + def _pragmas(dbapi_con, _): | |
| 30 | + cur = dbapi_con.cursor() | |
| 31 | + cur.execute("PRAGMA journal_mode=WAL") | |
| 32 | + cur.execute("PRAGMA synchronous=NORMAL") | |
| 33 | + cur.execute("PRAGMA foreign_keys=ON") | |
| 34 | + cur.execute("PRAGMA busy_timeout=30000") | |
| 35 | + cur.close() | |
| 36 | + | |
| 37 | + return eng | |
| 38 | + | |
| 39 | + | |
| 40 | +engine = _make_engine(settings.state_db) | |
| 41 | +SessionLocal = sessionmaker(bind=engine, expire_on_commit=False, future=True) | |
| 42 | + | |
| 43 | + | |
| 44 | +def create_all() -> None: | |
| 45 | + Base.metadata.create_all(engine) | |
| 46 | + | |
| 47 | + | |
| 48 | +@contextlib.contextmanager | |
| 49 | +def session() -> Iterator[Session]: | |
| 50 | + s = SessionLocal() | |
| 51 | + try: | |
| 52 | + yield s | |
| 53 | + s.commit() | |
| 54 | + except Exception: | |
| 55 | + s.rollback() | |
| 56 | + raise | |
| 57 | + finally: | |
| 58 | + s.close() | |
| 59 | + | |
| 60 | + | |
| 61 | +def get_session() -> Iterator[Session]: | |
| 62 | + """FastAPI dependency.""" | |
| 63 | + with session() as s: | |
| 64 | + yield s | |
| 65 | + | |
| 66 | + | |
| 67 | +def healthcheck() -> bool: | |
| 68 | + try: | |
| 69 | + with engine.connect() as c: | |
| 70 | + c.execute(text("SELECT 1")) | |
| 71 | + return True | |
| 72 | + except Exception: | |
| 73 | + return False | |
added
hfmarketdata/api/core/duck.py
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +"""DuckDB access to the Parquet lake — one read-only connection per thread + a small TTL cache. | |
| 2 | + | |
| 3 | +Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 4 | +""" | |
| 5 | +from __future__ import annotations | |
| 6 | + | |
| 7 | +import threading | |
| 8 | +import time | |
| 9 | +from collections.abc import Callable | |
| 10 | +from typing import Any | |
| 11 | + | |
| 12 | +import duckdb | |
| 13 | + | |
| 14 | +from .config import settings | |
| 15 | + | |
| 16 | +_tls = threading.local() | |
| 17 | +_cache: dict[str, tuple[float, Any]] = {} | |
| 18 | +_cache_lock = threading.Lock() | |
| 19 | +CACHE_TTL = 300 | |
| 20 | + | |
| 21 | + | |
| 22 | +def con() -> duckdb.DuckDBPyConnection: | |
| 23 | + if not hasattr(_tls, "con"): | |
| 24 | + c = duckdb.connect() | |
| 25 | + c.execute("SET threads TO 4") | |
| 26 | + c.execute("SET enable_object_cache=true") | |
| 27 | + _tls.con = c | |
| 28 | + return _tls.con | |
| 29 | + | |
| 30 | + | |
| 31 | +def cached(key: str, builder: Callable[[], Any], ttl: int = CACHE_TTL) -> Any: | |
| 32 | + now = time.time() | |
| 33 | + with _cache_lock: | |
| 34 | + hit = _cache.get(key) | |
| 35 | + if hit and now - hit[0] < ttl: | |
| 36 | + return hit[1] | |
| 37 | + value = builder() | |
| 38 | + with _cache_lock: | |
| 39 | + _cache[key] = (now, value) | |
| 40 | + return value | |
| 41 | + | |
| 42 | + | |
| 43 | +def invalidate(prefix: str = "") -> None: | |
| 44 | + with _cache_lock: | |
| 45 | + for k in [k for k in _cache if k.startswith(prefix)]: | |
| 46 | + del _cache[k] | |
| 47 | + | |
| 48 | + | |
| 49 | +PARQUET = settings.parquet | |
added
hfmarketdata/api/core/errors.py
+122 −0
@@ -0,0 +1,122 @@ | ||
| 1 | +"""Uniform error envelope for the whole API. | |
| 2 | + | |
| 3 | +Every error response has the shape: | |
| 4 | + | |
| 5 | + {"error": {"code": "CONTRACT_NOT_FOUND", "message": "…", "docs": "https://…/docs/errors#contract_not_found", | |
| 6 | + "type": "…" (optional), "details": {…} (optional)}, | |
| 7 | + "detail": "…"} # legacy field kept for the v1 endpoints (no breaking change) | |
| 8 | + | |
| 9 | +Raise `ApiError(status, code, message, **extra)` anywhere; FastAPI HTTPException and validation | |
| 10 | +errors are converted by the handlers registered in `install(app)`. | |
| 11 | + | |
| 12 | +Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 13 | +""" | |
| 14 | +from __future__ import annotations | |
| 15 | + | |
| 16 | +from typing import Any | |
| 17 | + | |
| 18 | +from fastapi import FastAPI, HTTPException, Request | |
| 19 | +from fastapi.exceptions import RequestValidationError | |
| 20 | +from fastapi.responses import JSONResponse | |
| 21 | + | |
| 22 | +from .config import settings | |
| 23 | + | |
| 24 | +# Canonical codes (documented in /docs/errors). Add new ones here so the docs stay exhaustive. | |
| 25 | +CODES: dict[str, str] = { | |
| 26 | + "INVALID_PARAMETER": "A query or path parameter is malformed or out of range.", | |
| 27 | + "VALIDATION_ERROR": "The request did not match the endpoint schema.", | |
| 28 | + "NOT_FOUND": "The requested resource does not exist.", | |
| 29 | + "TICKER_NOT_FOUND": "Unknown ticker for this asset type / timeframe / adjustment.", | |
| 30 | + "ASSET_NOT_FOUND": "Unknown asset type.", | |
| 31 | + "INVALID_CONTRACT_SYMBOL": "Futures contract symbol must look like ESZ25 or ESZ2025.", | |
| 32 | + "CONTRACT_NOT_FOUND": "No data for this futures contract.", | |
| 33 | + "ROOT_NOT_FOUND": "Unknown futures root.", | |
| 34 | + "OPTIONS_UNAVAILABLE": "Options dataset not available.", | |
| 35 | + "INVALID_API_KEY": "The API key is missing, malformed, revoked or unknown.", | |
| 36 | + "AUTH_REQUIRED": "Authentication is required for this endpoint.", | |
| 37 | + "FORBIDDEN": "Your tier or role does not allow this action.", | |
| 38 | + "RATE_LIMIT_EXCEEDED": "Request or row quota exhausted for the current window.", | |
| 39 | + "ROW_LIMIT_EXCEEDED": "`limit` is above the maximum rows per request for your tier.", | |
| 40 | + "CONFLICT": "The resource already exists or is in a conflicting state.", | |
| 41 | + "INTERNAL_ERROR": "Unexpected server error.", | |
| 42 | + "SERVICE_UNAVAILABLE": "A dependency (Redis, data lake) is unavailable.", | |
| 43 | +} | |
| 44 | + | |
| 45 | + | |
| 46 | +def docs_link(code: str) -> str: | |
| 47 | + return f"{settings.docs_url}/errors#{code.lower()}" | |
| 48 | + | |
| 49 | + | |
| 50 | +class ApiError(Exception): | |
| 51 | + def __init__(self, status: int, code: str, message: str, *, type: str | None = None, | |
| 52 | + details: dict[str, Any] | None = None, headers: dict[str, str] | None = None): | |
| 53 | + super().__init__(message) | |
| 54 | + self.status = status | |
| 55 | + self.code = code if code in CODES else "INTERNAL_ERROR" | |
| 56 | + self.message = message | |
| 57 | + self.type = type | |
| 58 | + self.details = details | |
| 59 | + self.headers = headers or {} | |
| 60 | + | |
| 61 | + def payload(self) -> dict[str, Any]: | |
| 62 | + err: dict[str, Any] = {"code": self.code, "message": self.message, "docs": docs_link(self.code)} | |
| 63 | + if self.type: | |
| 64 | + err["type"] = self.type | |
| 65 | + if self.details: | |
| 66 | + err["details"] = self.details | |
| 67 | + return {"error": err, "detail": self.message} | |
| 68 | + | |
| 69 | + def response(self) -> JSONResponse: | |
| 70 | + return JSONResponse(self.payload(), status_code=self.status, headers=self.headers) | |
| 71 | + | |
| 72 | + | |
| 73 | +def _code_for_http(status: int, message: str) -> str: | |
| 74 | + m = message.lower() | |
| 75 | + if status == 404: | |
| 76 | + if "ticker" in m or "not found in" in m: | |
| 77 | + return "TICKER_NOT_FOUND" | |
| 78 | + if "asset" in m: | |
| 79 | + return "ASSET_NOT_FOUND" | |
| 80 | + if "options" in m: | |
| 81 | + return "OPTIONS_UNAVAILABLE" | |
| 82 | + return "NOT_FOUND" | |
| 83 | + if status == 400: | |
| 84 | + return "INVALID_PARAMETER" | |
| 85 | + if status == 401: | |
| 86 | + return "INVALID_API_KEY" | |
| 87 | + if status == 403: | |
| 88 | + return "FORBIDDEN" | |
| 89 | + if status == 429: | |
| 90 | + return "RATE_LIMIT_EXCEEDED" | |
| 91 | + if status == 503: | |
| 92 | + return "SERVICE_UNAVAILABLE" | |
| 93 | + return "INTERNAL_ERROR" | |
| 94 | + | |
| 95 | + | |
| 96 | +def install(app: FastAPI) -> None: | |
| 97 | + @app.exception_handler(ApiError) | |
| 98 | + async def _api_error(_: Request, exc: ApiError): | |
| 99 | + return exc.response() | |
| 100 | + | |
| 101 | + @app.exception_handler(HTTPException) | |
| 102 | + async def _http_error(_: Request, exc: HTTPException): | |
| 103 | + message = exc.detail if isinstance(exc.detail, str) else str(exc.detail) | |
| 104 | + err = ApiError(exc.status_code, _code_for_http(exc.status_code, message), message, | |
| 105 | + headers=dict(exc.headers or {})) | |
| 106 | + return err.response() | |
| 107 | + | |
| 108 | + @app.exception_handler(RequestValidationError) | |
| 109 | + async def _validation_error(_: Request, exc: RequestValidationError): | |
| 110 | + errs = exc.errors() | |
| 111 | + first = errs[0] if errs else {} | |
| 112 | + loc = ".".join(str(x) for x in first.get("loc", []) if x not in ("query", "path", "body")) | |
| 113 | + message = f"{loc}: {first.get('msg', 'invalid value')}" if loc else "invalid request" | |
| 114 | + err = ApiError(422, "VALIDATION_ERROR", message, details={"errors": [ | |
| 115 | + {"loc": [str(x) for x in e.get("loc", [])], "msg": e.get("msg"), "type": e.get("type")} for e in errs]}) | |
| 116 | + return err.response() | |
| 117 | + | |
| 118 | + @app.exception_handler(Exception) | |
| 119 | + async def _unhandled(_: Request, exc: Exception): # pragma: no cover | |
| 120 | + err = ApiError(500, "INTERNAL_ERROR", "Unexpected server error. Please retry or contact " | |
| 121 | + f"{settings.contact_email}.") | |
| 122 | + return err.response() | |
added
hfmarketdata/api/core/responses.py
+109 −0
@@ -0,0 +1,109 @@ | ||
| 1 | +"""Response helpers shared by every v2 endpoint. | |
| 2 | + | |
| 3 | +* `Format = json | csv | parquet` — negotiated with the `format` query parameter. | |
| 4 | +* JSON envelope: `{"data": [...], "meta": {"count": n, "next_cursor": "...", ...}}`. | |
| 5 | +* Cursor pagination: opaque base64url of the last sort key (`encode_cursor` / `decode_cursor`). | |
| 6 | +* Row accounting: every response sets `X-Row-Count` so the rate limiter can charge the rows quota | |
| 7 | + (Parquet is charged at half price, see ratelimit/tiers.py). | |
| 8 | +* Timestamps are serialised as ISO 8601 strings (UTC, `Z` suffix when tz-aware). | |
| 9 | + | |
| 10 | +Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 11 | +""" | |
| 12 | +from __future__ import annotations | |
| 13 | + | |
| 14 | +import base64 | |
| 15 | +import io | |
| 16 | +import json | |
| 17 | +import math | |
| 18 | +from typing import Any, Literal | |
| 19 | + | |
| 20 | +import pandas as pd | |
| 21 | +from fastapi import Request, Response | |
| 22 | +from fastapi.responses import JSONResponse, PlainTextResponse | |
| 23 | + | |
| 24 | +from .errors import ApiError | |
| 25 | + | |
| 26 | +Format = Literal["json", "csv", "parquet"] | |
| 27 | +FORMATS = ("json", "csv", "parquet") | |
| 28 | +MEDIA = {"json": "application/json", "csv": "text/csv; charset=utf-8", "parquet": "application/vnd.apache.parquet"} | |
| 29 | + | |
| 30 | + | |
| 31 | +def parse_format(value: str | None) -> Format: | |
| 32 | + v = (value or "json").lower() | |
| 33 | + if v not in FORMATS: | |
| 34 | + raise ApiError(400, "INVALID_PARAMETER", f"format must be one of {', '.join(FORMATS)}") | |
| 35 | + return v # type: ignore[return-value] | |
| 36 | + | |
| 37 | + | |
| 38 | +def encode_cursor(value: Any) -> str: | |
| 39 | + raw = json.dumps(value, default=str).encode() | |
| 40 | + return base64.urlsafe_b64encode(raw).decode().rstrip("=") | |
| 41 | + | |
| 42 | + | |
| 43 | +def decode_cursor(cursor: str | None) -> Any: | |
| 44 | + if not cursor: | |
| 45 | + return None | |
| 46 | + try: | |
| 47 | + pad = "=" * (-len(cursor) % 4) | |
| 48 | + return json.loads(base64.urlsafe_b64decode(cursor + pad).decode()) | |
| 49 | + except Exception: | |
| 50 | + raise ApiError(400, "INVALID_PARAMETER", "cursor is not valid") | |
| 51 | + | |
| 52 | + | |
| 53 | +def _clean_frame(df: pd.DataFrame) -> pd.DataFrame: | |
| 54 | + df = df.copy() | |
| 55 | + for col in df.columns: | |
| 56 | + dt = str(df[col].dtype) | |
| 57 | + if dt.startswith("datetime64[ns, "): | |
| 58 | + df[col] = df[col].dt.tz_convert("UTC").dt.strftime("%Y-%m-%dT%H:%M:%SZ") | |
| 59 | + elif dt.startswith("datetime"): | |
| 60 | + df[col] = df[col].dt.strftime("%Y-%m-%dT%H:%M:%S").str.replace("T00:00:00", "", regex=False) | |
| 61 | + elif dt == "object": | |
| 62 | + df[col] = df[col].map(lambda v: v.isoformat() if hasattr(v, "isoformat") else v) | |
| 63 | + df = df.replace([math.inf, -math.inf], None) | |
| 64 | + return df.astype(object).where(df.notna(), None) | |
| 65 | + | |
| 66 | + | |
| 67 | +def frame_response(df: pd.DataFrame, fmt: Format, *, meta: dict[str, Any] | None = None, | |
| 68 | + request: Request | None = None, filename: str = "data") -> Response: | |
| 69 | + """Serialise a DataFrame in the requested format, with envelope + row accounting.""" | |
| 70 | + n = int(len(df)) | |
| 71 | + headers = {"X-Row-Count": str(n)} | |
| 72 | + if fmt == "csv": | |
| 73 | + buf = io.StringIO() | |
| 74 | + df.to_csv(buf, index=False) | |
| 75 | + return PlainTextResponse(buf.getvalue(), media_type=MEDIA["csv"], headers={ | |
| 76 | + **headers, "Content-Disposition": f'inline; filename="{filename}.csv"'}) | |
| 77 | + if fmt == "parquet": | |
| 78 | + buf = io.BytesIO() | |
| 79 | + df.to_parquet(buf, index=False, compression="zstd") | |
| 80 | + return Response(buf.getvalue(), media_type=MEDIA["parquet"], headers={ | |
| 81 | + **headers, "Content-Disposition": f'attachment; filename="{filename}.parquet"'}) | |
| 82 | + body = {"data": _clean_frame(df).to_dict(orient="records"), "meta": {"count": n, **(meta or {})}} | |
| 83 | + return JSONResponse(body, headers=headers) | |
| 84 | + | |
| 85 | + | |
| 86 | +def json_response(data: Any, *, meta: dict[str, Any] | None = None, rows: int | None = None, | |
| 87 | + status: int = 200) -> JSONResponse: | |
| 88 | + if rows is None: | |
| 89 | + rows = len(data) if isinstance(data, list) else 1 | |
| 90 | + body = {"data": data, "meta": {"count": rows, **(meta or {})}} | |
| 91 | + return JSONResponse(body, status_code=status, headers={"X-Row-Count": str(rows)}) | |
| 92 | + | |
| 93 | + | |
| 94 | +def clamp_limit(limit: int | None, default: int, hard_max: int, request: Request | None = None) -> int: | |
| 95 | + """Clamp `limit` to the hard maximum of the endpoint AND the tier's rows-per-request cap | |
| 96 | + (set by the rate-limit middleware in `request.state.max_rows`).""" | |
| 97 | + lim = default if limit is None else int(limit) | |
| 98 | + if lim < 1: | |
| 99 | + raise ApiError(400, "INVALID_PARAMETER", "limit must be >= 1") | |
| 100 | + cap = hard_max | |
| 101 | + tier_cap = getattr(getattr(request, "state", None), "max_rows", None) if request is not None else None | |
| 102 | + if tier_cap: | |
| 103 | + if lim > tier_cap: | |
| 104 | + raise ApiError(400, "ROW_LIMIT_EXCEEDED", | |
| 105 | + f"limit {lim} exceeds the maximum rows per request for your tier ({tier_cap}). " | |
| 106 | + "Create a free account or use Parquet/bulk endpoints for larger extracts.", | |
| 107 | + details={"max_rows": tier_cap}) | |
| 108 | + cap = min(cap, tier_cap) | |
| 109 | + return min(lim, cap) | |
modified
hfmarketdata/api/main.py
+47 −6
@@ -32,12 +32,15 @@ from pathlib import Path | ||
| 32 | 32 | import duckdb |
| 33 | 33 | from fastapi import FastAPI, HTTPException, Query |
| 34 | 34 | from fastapi.middleware.cors import CORSMiddleware |
| 35 | −from fastapi.responses import PlainTextResponse | |
| 35 | +from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse | |
| 36 | 36 | from fastapi.staticfiles import StaticFiles |
| 37 | 37 | |
| 38 | +from core import errors as _errors | |
| 39 | +from core.config import settings as _settings | |
| 40 | + | |
| 38 | 41 | __author__ = "Simon-Pierre Boucher" |
| 39 | 42 | __contact__ = "contact@spboucher.ai" |
| 40 | −__version__ = "1.0.0" | |
| 43 | +__version__ = "2.0.0" | |
| 41 | 44 | |
| 42 | 45 | DATA_ROOT = Path(os.environ.get("HFMD_DATA_ROOT", "/Volumes/ssd/firstratedata")) |
| 43 | 46 | PARQUET = DATA_ROOT / "parquet" |
@@ -68,7 +71,10 @@ app = FastAPI( | ||
| 68 | 71 | ) |
| 69 | 72 | app.add_middleware( |
| 70 | 73 | CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], |
| 74 | + expose_headers=["X-Row-Count", "X-RateLimit-Limit-Requests", "X-RateLimit-Remaining-Requests", | |
| 75 | + "X-RateLimit-Limit-Rows", "X-RateLimit-Remaining-Rows", "X-RateLimit-Reset", "Retry-After"], | |
| 71 | 76 | ) |
| 77 | +_errors.install(app) # uniform {"error": {code, message, docs}} envelope (legacy "detail" kept) | |
| 72 | 78 | |
| 73 | 79 | # ------------------------------------------------------------------------------ |
| 74 | 80 | # DuckDB — one connection per thread, read-only usage |
@@ -177,7 +183,8 @@ def _rows_to_response(rel, limit: int, fmt: str, order_col: str): | ||
| 177 | 183 | limit = min(limit, MAX_LIMIT_CSV) |
| 178 | 184 | rel = rel.limit(limit) |
| 179 | 185 | df = rel.df() |
| 180 | − return PlainTextResponse(df.to_csv(index=False), media_type="text/csv") | |
| 186 | + return PlainTextResponse(df.to_csv(index=False), media_type="text/csv", | |
| 187 | + headers={"X-Row-Count": str(len(df))}) | |
| 181 | 188 | limit = min(limit, MAX_LIMIT_JSON) |
| 182 | 189 | rel = rel.limit(limit) |
| 183 | 190 | df = rel.df() |
@@ -188,7 +195,8 @@ def _rows_to_response(rel, limit: int, fmt: str, order_col: str): | ||
| 188 | 195 | # sérialisables en JSON strict → null |
| 189 | 196 | df = df.replace([float("inf"), float("-inf")], None) |
| 190 | 197 | df = df.astype(object).where(df.notna(), None) |
| 191 | − return {"count": len(df), "data": df.to_dict(orient="records")} | |
| 198 | + return JSONResponse({"count": len(df), "data": df.to_dict(orient="records")}, | |
| 199 | + headers={"X-Row-Count": str(len(df))}) | |
| 192 | 200 | |
| 193 | 201 | |
| 194 | 202 | # ------------------------------------------------------------------------------ |
@@ -468,11 +476,44 @@ def opt_history(ticker: str, | ||
| 468 | 476 | |
| 469 | 477 | |
| 470 | 478 | # ------------------------------------------------------------------------------ |
| 471 | −# Static React platform (mounted last so it doesn't shadow the API) | |
| 479 | +# v2 modules (each one is optional at import time so a broken module never takes the API down) | |
| 480 | +# ------------------------------------------------------------------------------ | |
| 481 | + | |
| 482 | +import importlib | |
| 483 | +import logging | |
| 484 | + | |
| 485 | +_log = logging.getLogger("hfmarketdata") | |
| 486 | +V2_MODULES = ("ratelimit.middleware", "accounts.routes", "futures.routes", "fundamentals.routes", | |
| 487 | + "bulk.routes", "stream.routes", "openapi") | |
| 488 | +for _mod in V2_MODULES: | |
| 489 | + try: | |
| 490 | + m = importlib.import_module(_mod) | |
| 491 | + if hasattr(m, "install"): | |
| 492 | + m.install(app) | |
| 493 | + elif hasattr(m, "router"): | |
| 494 | + app.include_router(m.router) | |
| 495 | + except ModuleNotFoundError as e: # module not written yet | |
| 496 | + if e.name and e.name.split(".")[0] not in _mod: | |
| 497 | + _log.warning("module %s: missing dependency %s", _mod, e.name) | |
| 498 | + except Exception as e: # pragma: no cover | |
| 499 | + _log.exception("module %s failed to load: %s", _mod, e) | |
| 500 | + | |
| 501 | + | |
| 502 | +# ------------------------------------------------------------------------------ | |
| 503 | +# Static React platform (mounted last so it doesn't shadow the API) + SPA fallback | |
| 472 | 504 | # ------------------------------------------------------------------------------ |
| 473 | 505 | |
| 474 | 506 | if WEB_DIST.is_dir(): |
| 475 | − app.mount("/", StaticFiles(directory=str(WEB_DIST), html=True), name="web") | |
| 507 | + app.mount("/assets", StaticFiles(directory=str(WEB_DIST / "assets")), name="assets") | |
| 508 | + | |
| 509 | + @app.get("/{path:path}", include_in_schema=False) | |
| 510 | + def spa(path: str): | |
| 511 | + if path.startswith(("v1/", "health", "openapi", "docs/", "redoc")) and not path.startswith("docs/"): | |
| 512 | + raise HTTPException(404, "Not found") | |
| 513 | + candidate = (WEB_DIST / path).resolve() | |
| 514 | + if path and candidate.is_file() and str(candidate).startswith(str(WEB_DIST.resolve())): | |
| 515 | + return FileResponse(candidate) | |
| 516 | + return FileResponse(WEB_DIST / "index.html") | |
| 476 | 517 | |
| 477 | 518 | |
| 478 | 519 | if __name__ == "__main__": |
added
hfmarketdata/api/openapi.py
+111 −0
@@ -0,0 +1,111 @@ | ||
| 1 | +"""OpenAPI 3.1 customisation — the single source of truth for the reference documentation. | |
| 2 | + | |
| 3 | +Adds: servers, tag ordering/descriptions, the uniform error schema, per-endpoint `x-errors` | |
| 4 | +expansion into `responses`, and rate-limit headers on every operation. | |
| 5 | + | |
| 6 | +Author: Simon-Pierre Boucher <contact@spboucher.ai> | |
| 7 | +""" | |
| 8 | +from __future__ import annotations | |
| 9 | + | |
| 10 | +from fastapi import FastAPI | |
| 11 | +from fastapi.openapi.utils import get_openapi | |
| 12 | + | |
| 13 | +from core.config import settings | |
| 14 | +from core.errors import CODES, docs_link | |
| 15 | + | |
| 16 | +TAGS = [ | |
| 17 | + {"name": "meta", "description": "Health, live dataset inventory and your current limits."}, | |
| 18 | + {"name": "bars", "description": "Intraday & daily OHLCV bars for stocks, ETFs, crypto, indices, FX and continuous futures."}, | |
| 19 | + {"name": "futures", "description": "Individual futures contracts, expiry chains, continuous series, term structure."}, | |
| 20 | + {"name": "options", "description": "End-of-day options chains with quotes, implied volatility and Greeks (since 2010)."}, | |
| 21 | + {"name": "fundamentals", "description": "SEC EDGAR financial statements, ratios, screener — point-in-time, since 2010."}, | |
| 22 | + {"name": "stream", "description": "WebSocket streams (filings)."}, | |
| 23 | + {"name": "bulk", "description": "Whole-universe Parquet extracts, outside the rows quota."}, | |
| 24 | + {"name": "auth", "description": "Sign-up, e-mail verification, login, password reset."}, | |
| 25 | + {"name": "me", "description": "Your account: API keys, usage, tier."}, | |
| 26 | + {"name": "admin", "description": "Administration (admin role)."}, | |
| 27 | +] | |
| 28 | + | |
| 29 | +RATE_HEADERS = { | |
| 30 | + "X-RateLimit-Limit-Requests": {"schema": {"type": "integer"}, "description": "Requests allowed in the current window."}, | |
| 31 | + "X-RateLimit-Remaining-Requests": {"schema": {"type": "integer"}, "description": "Requests left in the current window."}, | |
| 32 | + "X-RateLimit-Limit-Rows": {"schema": {"type": "integer"}, "description": "Data rows allowed in the current window."}, | |
| 33 | + "X-RateLimit-Remaining-Rows": {"schema": {"type": "integer"}, "description": "Data rows left in the current window."}, | |
| 34 | + "X-RateLimit-Reset": {"schema": {"type": "integer"}, "description": "Unix timestamp (seconds) when the window resets."}, | |
| 35 | + "X-Row-Count": {"schema": {"type": "integer"}, "description": "Rows returned by this response."}, | |
| 36 | +} | |
| 37 | + | |
| 38 | +ERROR_SCHEMA = { | |
| 39 | + "type": "object", | |
| 40 | + "required": ["error", "detail"], | |
| 41 | + "properties": { | |
| 42 | + "error": {"type": "object", "required": ["code", "message", "docs"], "properties": { | |
| 43 | + "code": {"type": "string", "enum": sorted(CODES)}, | |
| 44 | + "message": {"type": "string"}, | |
| 45 | + "docs": {"type": "string", "format": "uri"}, | |
| 46 | + "type": {"type": "string", "description": "Sub-type, e.g. requests_per_hour / rows_per_minute for 429."}, | |
| 47 | + "details": {"type": "object", "additionalProperties": True}, | |
| 48 | + }}, | |
| 49 | + "detail": {"type": "string", "description": "Legacy field (same as error.message)."}, | |
| 50 | + }, | |
| 51 | + "example": {"error": {"code": "CONTRACT_NOT_FOUND", "message": "No data for contract ESZ19", | |
| 52 | + "docs": docs_link("CONTRACT_NOT_FOUND")}, "detail": "No data for contract ESZ19"}, | |
| 53 | +} | |
| 54 | + | |
| 55 | +STATUS_FOR = {"INVALID_PARAMETER": 400, "VALIDATION_ERROR": 422, "NOT_FOUND": 404, "TICKER_NOT_FOUND": 404, | |
| 56 | + "ASSET_NOT_FOUND": 404, "INVALID_CONTRACT_SYMBOL": 400, "CONTRACT_NOT_FOUND": 404, "ROOT_NOT_FOUND": 404, | |
| 57 | + "OPTIONS_UNAVAILABLE": 503, "INVALID_API_KEY": 401, "AUTH_REQUIRED": 401, "FORBIDDEN": 403, | |
| 58 | + "RATE_LIMIT_EXCEEDED": 429, "ROW_LIMIT_EXCEEDED": 400, "CONFLICT": 409, "INTERNAL_ERROR": 500, | |
| 59 | + "SERVICE_UNAVAILABLE": 503} | |
| 60 | + | |
| 61 | + | |
| 62 | +def build(app: FastAPI) -> dict: | |
| 63 | + schema = get_openapi(title=app.title, version=app.version, description=app.description, routes=app.routes, | |
| 64 | + tags=TAGS, servers=[{"url": settings.public_url, "description": "Production"}], | |
| 65 | + contact={"name": "Simon-Pierre Boucher", "email": settings.contact_email, "url": settings.public_url}, | |
| 66 | + license_info={"name": "Data: FirstRate Data & SEC EDGAR — see terms", "url": settings.public_url + "/docs/terms"}) | |
| 67 | + schema["openapi"] = "3.1.0" | |
| 68 | + comps = schema.setdefault("components", {}) | |
| 69 | + comps.setdefault("schemas", {})["Error"] = ERROR_SCHEMA | |
| 70 | + comps.setdefault("securitySchemes", {})["ApiKey"] = { | |
| 71 | + "type": "http", "scheme": "bearer", "bearerFormat": "hfmd_live_…", | |
| 72 | + "description": "Optional. `Authorization: Bearer <key>` or `?api_key=<key>`. Without a key you get the keyless " | |
| 73 | + "(per-IP, hourly) limits; a free account raises them a lot. High usage: contact@spboucher.ai."} | |
| 74 | + comps["securitySchemes"]["ApiKeyQuery"] = {"type": "apiKey", "in": "query", "name": "api_key"} | |
| 75 | + schema["security"] = [{}, {"ApiKey": []}, {"ApiKeyQuery": []}] | |
| 76 | + for path, ops in schema.get("paths", {}).items(): | |
| 77 | + for method, op in ops.items(): | |
| 78 | + if method not in ("get", "post", "put", "patch", "delete"): | |
| 79 | + continue | |
| 80 | + responses = op.setdefault("responses", {}) | |
| 81 | + for code in list(responses): | |
| 82 | + if code.startswith("2"): | |
| 83 | + responses[code].setdefault("headers", {}).update(RATE_HEADERS) | |
| 84 | + errs = set(op.pop("x-errors", []) or []) | |
| 85 | + errs |= {"RATE_LIMIT_EXCEEDED", "VALIDATION_ERROR", "INTERNAL_ERROR"} | |
| 86 | + for code in sorted(errs): | |
| 87 | + st = str(STATUS_FOR.get(code, 400)) | |
| 88 | + r = responses.setdefault(st, {"description": ""}) | |
| 89 | + r["description"] = (r.get("description") or "").strip() | |
| 90 | + desc = f"`{code}` — {CODES.get(code, '')}" | |
| 91 | + r["description"] = (r["description"] + "\n\n" + desc).strip() if r["description"] and code not in r["description"] else (r["description"] or desc) | |
| 92 | + r.setdefault("content", {})["application/json"] = {"schema": {"$ref": "#/components/schemas/Error"}} | |
| 93 | + if "422" in responses and "VALIDATION_ERROR" not in responses["422"].get("description", ""): | |
| 94 | + responses["422"]["description"] = "`VALIDATION_ERROR` — " + CODES["VALIDATION_ERROR"] | |
| 95 | + responses["422"]["content"] = {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} | |
| 96 | + schema["info"]["x-logo"] = {"url": settings.public_url + "/logo.svg"} | |
| 97 | + schema["info"]["x-tiers"] = { | |
| 98 | + "keyless": {"window": "1h", "requests": 30, "rows": 100_000, "max_rows_per_request": 5_000}, | |
| 99 | + "free": {"window": "1m", "requests": 120, "rows": 1_000_000, "max_rows_per_request": 50_000}, | |
| 100 | + "high_usage": {"window": "1m", "requests": 600, "rows": 10_000_000, "max_rows_per_request": 200_000, | |
| 101 | + "how": f"e-mail {settings.contact_email}"}, | |
| 102 | + } | |
| 103 | + return schema | |
| 104 | + | |
| 105 | + | |
| 106 | +def install(app: FastAPI) -> None: | |
| 107 | + def custom_openapi(): | |
| 108 | + if not app.openapi_schema: | |
| 109 | + app.openapi_schema = build(app) | |
| 110 | + return app.openapi_schema | |
| 111 | + app.openapi = custom_openapi # type: ignore[method-assign] | |
modified
hfmarketdata/requirements.txt
+10 −1
@@ -1,4 +1,13 @@ | ||
| 1 | 1 | fastapi>=0.115 |
| 2 | −uvicorn>=0.30 | |
| 2 | +uvicorn[standard]>=0.30 | |
| 3 | 3 | duckdb>=1.0 |
| 4 | 4 | pandas>=2.0 |
| 5 | +pyarrow>=17 | |
| 6 | +sqlalchemy>=2.0 | |
| 7 | +redis>=5.0 | |
| 8 | +argon2-cffi>=23 | |
| 9 | +httpx>=0.27 | |
| 10 | +websockets>=12 | |
| 11 | +python-multipart>=0.0.9 | |
| 12 | +itsdangerous>=2.2 | |
| 13 | +pydantic>=2.7 | |
modified
hfmarketdata/web/package-lock.json
+1978 −23
@@ -1,17 +1,22 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "hfmarketdata-web", |
| 3 | − "version": "1.0.0", | |
| 3 | + "version": "2.0.0", | |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | 8 | "name": "hfmarketdata-web", |
| 9 | − "version": "1.0.0", | |
| 9 | + "version": "2.0.0", | |
| 10 | 10 | "dependencies": { |
| 11 | + "lightweight-charts": "^4.2.0", | |
| 12 | + "prism-react-renderer": "^2.4.0", | |
| 11 | 13 | "react": "^18.3.1", |
| 12 | − "react-dom": "^18.3.1" | |
| 14 | + "react-dom": "^18.3.1", | |
| 15 | + "react-router-dom": "^6.28.0" | |
| 13 | 16 | }, |
| 14 | 17 | "devDependencies": { |
| 18 | + "@mdx-js/rollup": "^3.1.0", | |
| 19 | + "@playwright/test": "^1.49.0", | |
| 15 | 20 | "@vitejs/plugin-react": "^4.3.4", |
| 16 | 21 | "vite": "^6.0.0" |
| 17 | 22 | } |
@@ -790,6 +795,64 @@ | ||
| 790 | 795 | "@jridgewell/sourcemap-codec": "^1.4.14" |
| 791 | 796 | } |
| 792 | 797 | }, |
| 798 | + "node_modules/@mdx-js/mdx": { | |
| 799 | + "version": "3.1.1", | |
| 800 | + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", | |
| 801 | + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", | |
| 802 | + "dev": true, | |
| 803 | + "license": "MIT", | |
| 804 | + "dependencies": { | |
| 805 | + "@types/estree": "^1.0.0", | |
| 806 | + "@types/estree-jsx": "^1.0.0", | |
| 807 | + "@types/hast": "^3.0.0", | |
| 808 | + "@types/mdx": "^2.0.0", | |
| 809 | + "acorn": "^8.0.0", | |
| 810 | + "collapse-white-space": "^2.0.0", | |
| 811 | + "devlop": "^1.0.0", | |
| 812 | + "estree-util-is-identifier-name": "^3.0.0", | |
| 813 | + "estree-util-scope": "^1.0.0", | |
| 814 | + "estree-walker": "^3.0.0", | |
| 815 | + "hast-util-to-jsx-runtime": "^2.0.0", | |
| 816 | + "markdown-extensions": "^2.0.0", | |
| 817 | + "recma-build-jsx": "^1.0.0", | |
| 818 | + "recma-jsx": "^1.0.0", | |
| 819 | + "recma-stringify": "^1.0.0", | |
| 820 | + "rehype-recma": "^1.0.0", | |
| 821 | + "remark-mdx": "^3.0.0", | |
| 822 | + "remark-parse": "^11.0.0", | |
| 823 | + "remark-rehype": "^11.0.0", | |
| 824 | + "source-map": "^0.7.0", | |
| 825 | + "unified": "^11.0.0", | |
| 826 | + "unist-util-position-from-estree": "^2.0.0", | |
| 827 | + "unist-util-stringify-position": "^4.0.0", | |
| 828 | + "unist-util-visit": "^5.0.0", | |
| 829 | + "vfile": "^6.0.0" | |
| 830 | + }, | |
| 831 | + "funding": { | |
| 832 | + "type": "opencollective", | |
| 833 | + "url": "https://opencollective.com/unified" | |
| 834 | + } | |
| 835 | + }, | |
| 836 | + "node_modules/@mdx-js/rollup": { | |
| 837 | + "version": "3.1.1", | |
| 838 | + "resolved": "https://registry.npmjs.org/@mdx-js/rollup/-/rollup-3.1.1.tgz", | |
| 839 | + "integrity": "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==", | |
| 840 | + "dev": true, | |
| 841 | + "license": "MIT", | |
| 842 | + "dependencies": { | |
| 843 | + "@mdx-js/mdx": "^3.0.0", | |
| 844 | + "@rollup/pluginutils": "^5.0.0", | |
| 845 | + "source-map": "^0.7.0", | |
| 846 | + "vfile": "^6.0.0" | |
| 847 | + }, | |
| 848 | + "funding": { | |
| 849 | + "type": "opencollective", | |
| 850 | + "url": "https://opencollective.com/unified" | |
| 851 | + }, | |
| 852 | + "peerDependencies": { | |
| 853 | + "rollup": ">=2" | |
| 854 | + } | |
| 855 | + }, | |
| 793 | 856 | "node_modules/@napi-rs/lzma-linux-x64-gnu": { |
| 794 | 857 | "version": "1.5.1", |
| 795 | 858 | "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", |
@@ -810,6 +873,31 @@ | ||
| 810 | 873 | "node": "^22.20 || ^24.12 || >=25" |
| 811 | 874 | } |
| 812 | 875 | }, |
| 876 | + "node_modules/@playwright/test": { | |
| 877 | + "version": "1.63.0", | |
| 878 | + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", | |
| 879 | + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", | |
| 880 | + "dev": true, | |
| 881 | + "license": "Apache-2.0", | |
| 882 | + "dependencies": { | |
| 883 | + "playwright": "1.63.0" | |
| 884 | + }, | |
| 885 | + "bin": { | |
| 886 | + "playwright": "cli.js" | |
| 887 | + }, | |
| 888 | + "engines": { | |
| 889 | + "node": ">=20" | |
| 890 | + } | |
| 891 | + }, | |
| 892 | + "node_modules/@remix-run/router": { | |
| 893 | + "version": "1.23.4", | |
| 894 | + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", | |
| 895 | + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", | |
| 896 | + "license": "MIT", | |
| 897 | + "engines": { | |
| 898 | + "node": ">=14.0.0" | |
| 899 | + } | |
| 900 | + }, | |
| 813 | 901 | "node_modules/@rolldown/pluginutils": { |
| 814 | 902 | "version": "1.0.0-beta.27", |
| 815 | 903 | "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", |
@@ -817,6 +905,36 @@ | ||
| 817 | 905 | "dev": true, |
| 818 | 906 | "license": "MIT" |
| 819 | 907 | }, |
| 908 | + "node_modules/@rollup/pluginutils": { | |
| 909 | + "version": "5.4.0", | |
| 910 | + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", | |
| 911 | + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", | |
| 912 | + "dev": true, | |
| 913 | + "license": "MIT", | |
| 914 | + "dependencies": { | |
| 915 | + "@types/estree": "^1.0.0", | |
| 916 | + "estree-walker": "^2.0.2", | |
| 917 | + "picomatch": "^4.0.2" | |
| 918 | + }, | |
| 919 | + "engines": { | |
| 920 | + "node": ">=14.0.0" | |
| 921 | + }, | |
| 922 | + "peerDependencies": { | |
| 923 | + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" | |
| 924 | + }, | |
| 925 | + "peerDependenciesMeta": { | |
| 926 | + "rollup": { | |
| 927 | + "optional": true | |
| 928 | + } | |
| 929 | + } | |
| 930 | + }, | |
| 931 | + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { | |
| 932 | + "version": "2.0.2", | |
| 933 | + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", | |
| 934 | + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", | |
| 935 | + "dev": true, | |
| 936 | + "license": "MIT" | |
| 937 | + }, | |
| 820 | 938 | "node_modules/@rollup/rollup-android-arm-eabi": { |
| 821 | 939 | "version": "4.62.4", |
| 822 | 940 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", |
@@ -1251,6 +1369,16 @@ | ||
| 1251 | 1369 | "@babel/types": "^7.28.2" |
| 1252 | 1370 | } |
| 1253 | 1371 | }, |
| 1372 | + "node_modules/@types/debug": { | |
| 1373 | + "version": "4.1.13", | |
| 1374 | + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", | |
| 1375 | + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", | |
| 1376 | + "dev": true, | |
| 1377 | + "license": "MIT", | |
| 1378 | + "dependencies": { | |
| 1379 | + "@types/ms": "*" | |
| 1380 | + } | |
| 1381 | + }, | |
| 1254 | 1382 | "node_modules/@types/estree": { |
| 1255 | 1383 | "version": "1.0.9", |
| 1256 | 1384 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", |
@@ -1258,6 +1386,70 @@ | ||
| 1258 | 1386 | "dev": true, |
| 1259 | 1387 | "license": "MIT" |
| 1260 | 1388 | }, |
| 1389 | + "node_modules/@types/estree-jsx": { | |
| 1390 | + "version": "1.0.5", | |
| 1391 | + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", | |
| 1392 | + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", | |
| 1393 | + "dev": true, | |
| 1394 | + "license": "MIT", | |
| 1395 | + "dependencies": { | |
| 1396 | + "@types/estree": "*" | |
| 1397 | + } | |
| 1398 | + }, | |
| 1399 | + "node_modules/@types/hast": { | |
| 1400 | + "version": "3.0.5", | |
| 1401 | + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", | |
| 1402 | + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", | |
| 1403 | + "dev": true, | |
| 1404 | + "license": "MIT", | |
| 1405 | + "dependencies": { | |
| 1406 | + "@types/unist": "*" | |
| 1407 | + } | |
| 1408 | + }, | |
| 1409 | + "node_modules/@types/mdast": { | |
| 1410 | + "version": "4.0.4", | |
| 1411 | + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", | |
| 1412 | + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", | |
| 1413 | + "dev": true, | |
| 1414 | + "license": "MIT", | |
| 1415 | + "dependencies": { | |
| 1416 | + "@types/unist": "*" | |
| 1417 | + } | |
| 1418 | + }, | |
| 1419 | + "node_modules/@types/mdx": { | |
| 1420 | + "version": "2.0.14", | |
| 1421 | + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", | |
| 1422 | + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", | |
| 1423 | + "dev": true, | |
| 1424 | + "license": "MIT" | |
| 1425 | + }, | |
| 1426 | + "node_modules/@types/ms": { | |
| 1427 | + "version": "2.1.0", | |
| 1428 | + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", | |
| 1429 | + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", | |
| 1430 | + "dev": true, | |
| 1431 | + "license": "MIT" | |
| 1432 | + }, | |
| 1433 | + "node_modules/@types/prismjs": { | |
| 1434 | + "version": "1.26.6", | |
| 1435 | + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", | |
| 1436 | + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", | |
| 1437 | + "license": "MIT" | |
| 1438 | + }, | |
| 1439 | + "node_modules/@types/unist": { | |
| 1440 | + "version": "3.0.3", | |
| 1441 | + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", | |
| 1442 | + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", | |
| 1443 | + "dev": true, | |
| 1444 | + "license": "MIT" | |
| 1445 | + }, | |
| 1446 | + "node_modules/@ungap/structured-clone": { | |
| 1447 | + "version": "1.4.0", | |
| 1448 | + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", | |
| 1449 | + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", | |
| 1450 | + "dev": true, | |
| 1451 | + "license": "ISC" | |
| 1452 | + }, | |
| 1261 | 1453 | "node_modules/@vitejs/plugin-react": { |
| 1262 | 1454 | "version": "4.7.0", |
| 1263 | 1455 | "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", |
@@ -1279,6 +1471,50 @@ | ||
| 1279 | 1471 | "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" |
| 1280 | 1472 | } |
| 1281 | 1473 | }, |
| 1474 | + "node_modules/acorn": { | |
| 1475 | + "version": "8.18.0", | |
| 1476 | + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", | |
| 1477 | + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", | |
| 1478 | + "dev": true, | |
| 1479 | + "license": "MIT", | |
| 1480 | + "bin": { | |
| 1481 | + "acorn": "bin/acorn" | |
| 1482 | + }, | |
| 1483 | + "engines": { | |
| 1484 | + "node": ">=0.4.0" | |
| 1485 | + } | |
| 1486 | + }, | |
| 1487 | + "node_modules/acorn-jsx": { | |
| 1488 | + "version": "5.3.2", | |
| 1489 | + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", | |
| 1490 | + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", | |
| 1491 | + "dev": true, | |
| 1492 | + "license": "MIT", | |
| 1493 | + "peerDependencies": { | |
| 1494 | + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" | |
| 1495 | + } | |
| 1496 | + }, | |
| 1497 | + "node_modules/astring": { | |
| 1498 | + "version": "1.9.0", | |
| 1499 | + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", | |
| 1500 | + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", | |
| 1501 | + "dev": true, | |
| 1502 | + "license": "MIT", | |
| 1503 | + "bin": { | |
| 1504 | + "astring": "bin/astring" | |
| 1505 | + } | |
| 1506 | + }, | |
| 1507 | + "node_modules/bail": { | |
| 1508 | + "version": "2.0.2", | |
| 1509 | + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", | |
| 1510 | + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", | |
| 1511 | + "dev": true, | |
| 1512 | + "license": "MIT", | |
| 1513 | + "funding": { | |
| 1514 | + "type": "github", | |
| 1515 | + "url": "https://github.com/sponsors/wooorm" | |
| 1516 | + } | |
| 1517 | + }, | |
| 1282 | 1518 | "node_modules/baseline-browser-mapping": { |
| 1283 | 1519 | "version": "2.11.13", |
| 1284 | 1520 | "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", |
@@ -1347,6 +1583,92 @@ | ||
| 1347 | 1583 | ], |
| 1348 | 1584 | "license": "CC-BY-4.0" |
| 1349 | 1585 | }, |
| 1586 | + "node_modules/ccount": { | |
| 1587 | + "version": "2.0.1", | |
| 1588 | + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", | |
| 1589 | + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", | |
| 1590 | + "dev": true, | |
| 1591 | + "license": "MIT", | |
| 1592 | + "funding": { | |
| 1593 | + "type": "github", | |
| 1594 | + "url": "https://github.com/sponsors/wooorm" | |
| 1595 | + } | |
| 1596 | + }, | |
| 1597 | + "node_modules/character-entities": { | |
| 1598 | + "version": "2.0.2", | |
| 1599 | + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", | |
| 1600 | + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", | |
| 1601 | + "dev": true, | |
| 1602 | + "license": "MIT", | |
| 1603 | + "funding": { | |
| 1604 | + "type": "github", | |
| 1605 | + "url": "https://github.com/sponsors/wooorm" | |
| 1606 | + } | |
| 1607 | + }, | |
| 1608 | + "node_modules/character-entities-html4": { | |
| 1609 | + "version": "2.1.0", | |
| 1610 | + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", | |
| 1611 | + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", | |
| 1612 | + "dev": true, | |
| 1613 | + "license": "MIT", | |
| 1614 | + "funding": { | |
| 1615 | + "type": "github", | |
| 1616 | + "url": "https://github.com/sponsors/wooorm" | |
| 1617 | + } | |
| 1618 | + }, | |
| 1619 | + "node_modules/character-entities-legacy": { | |
| 1620 | + "version": "3.0.0", | |
| 1621 | + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", | |
| 1622 | + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", | |
| 1623 | + "dev": true, | |
| 1624 | + "license": "MIT", | |
| 1625 | + "funding": { | |
| 1626 | + "type": "github", | |
| 1627 | + "url": "https://github.com/sponsors/wooorm" | |
| 1628 | + } | |
| 1629 | + }, | |
| 1630 | + "node_modules/character-reference-invalid": { | |
| 1631 | + "version": "2.0.1", | |
| 1632 | + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", | |
| 1633 | + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", | |
| 1634 | + "dev": true, | |
| 1635 | + "license": "MIT", | |
| 1636 | + "funding": { | |
| 1637 | + "type": "github", | |
| 1638 | + "url": "https://github.com/sponsors/wooorm" | |
| 1639 | + } | |
| 1640 | + }, | |
| 1641 | + "node_modules/clsx": { | |
| 1642 | + "version": "2.1.1", | |
| 1643 | + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", | |
| 1644 | + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", | |
| 1645 | + "license": "MIT", | |
| 1646 | + "engines": { | |
| 1647 | + "node": ">=6" | |
| 1648 | + } | |
| 1649 | + }, | |
| 1650 | + "node_modules/collapse-white-space": { | |
| 1651 | + "version": "2.1.0", | |
| 1652 | + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", | |
| 1653 | + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", | |
| 1654 | + "dev": true, | |
| 1655 | + "license": "MIT", | |
| 1656 | + "funding": { | |
| 1657 | + "type": "github", | |
| 1658 | + "url": "https://github.com/sponsors/wooorm" | |
| 1659 | + } | |
| 1660 | + }, | |
| 1661 | + "node_modules/comma-separated-tokens": { | |
| 1662 | + "version": "2.0.3", | |
| 1663 | + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", | |
| 1664 | + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", | |
| 1665 | + "dev": true, | |
| 1666 | + "license": "MIT", | |
| 1667 | + "funding": { | |
| 1668 | + "type": "github", | |
| 1669 | + "url": "https://github.com/sponsors/wooorm" | |
| 1670 | + } | |
| 1671 | + }, | |
| 1350 | 1672 | "node_modules/convert-source-map": { |
| 1351 | 1673 | "version": "2.0.0", |
| 1352 | 1674 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", |
@@ -1372,6 +1694,44 @@ | ||
| 1372 | 1694 | } |
| 1373 | 1695 | } |
| 1374 | 1696 | }, |
| 1697 | + "node_modules/decode-named-character-reference": { | |
| 1698 | + "version": "1.3.0", | |
| 1699 | + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", | |
| 1700 | + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", | |
| 1701 | + "dev": true, | |
| 1702 | + "license": "MIT", | |
| 1703 | + "dependencies": { | |
| 1704 | + "character-entities": "^2.0.0" | |
| 1705 | + }, | |
| 1706 | + "funding": { | |
| 1707 | + "type": "github", | |
| 1708 | + "url": "https://github.com/sponsors/wooorm" | |
| 1709 | + } | |
| 1710 | + }, | |
| 1711 | + "node_modules/dequal": { | |
| 1712 | + "version": "2.0.3", | |
| 1713 | + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", | |
| 1714 | + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", | |
| 1715 | + "dev": true, | |
| 1716 | + "license": "MIT", | |
| 1717 | + "engines": { | |
| 1718 | + "node": ">=6" | |
| 1719 | + } | |
| 1720 | + }, | |
| 1721 | + "node_modules/devlop": { | |
| 1722 | + "version": "1.1.0", | |
| 1723 | + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", | |
| 1724 | + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", | |
| 1725 | + "dev": true, | |
| 1726 | + "license": "MIT", | |
| 1727 | + "dependencies": { | |
| 1728 | + "dequal": "^2.0.0" | |
| 1729 | + }, | |
| 1730 | + "funding": { | |
| 1731 | + "type": "github", | |
| 1732 | + "url": "https://github.com/sponsors/wooorm" | |
| 1733 | + } | |
| 1734 | + }, | |
| 1375 | 1735 | "node_modules/electron-to-chromium": { |
| 1376 | 1736 | "version": "1.5.403", |
| 1377 | 1737 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz", |
@@ -1379,6 +1739,40 @@ | ||
| 1379 | 1739 | "dev": true, |
| 1380 | 1740 | "license": "ISC" |
| 1381 | 1741 | }, |
| 1742 | + "node_modules/esast-util-from-estree": { | |
| 1743 | + "version": "2.0.0", | |
| 1744 | + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", | |
| 1745 | + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", | |
| 1746 | + "dev": true, | |
| 1747 | + "license": "MIT", | |
| 1748 | + "dependencies": { | |
| 1749 | + "@types/estree-jsx": "^1.0.0", | |
| 1750 | + "devlop": "^1.0.0", | |
| 1751 | + "estree-util-visit": "^2.0.0", | |
| 1752 | + "unist-util-position-from-estree": "^2.0.0" | |
| 1753 | + }, | |
| 1754 | + "funding": { | |
| 1755 | + "type": "opencollective", | |
| 1756 | + "url": "https://opencollective.com/unified" | |
| 1757 | + } | |
| 1758 | + }, | |
| 1759 | + "node_modules/esast-util-from-js": { | |
| 1760 | + "version": "2.0.1", | |
| 1761 | + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", | |
| 1762 | + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", | |
| 1763 | + "dev": true, | |
| 1764 | + "license": "MIT", | |
| 1765 | + "dependencies": { | |
| 1766 | + "@types/estree-jsx": "^1.0.0", | |
| 1767 | + "acorn": "^8.0.0", | |
| 1768 | + "esast-util-from-estree": "^2.0.0", | |
| 1769 | + "vfile-message": "^4.0.0" | |
| 1770 | + }, | |
| 1771 | + "funding": { | |
| 1772 | + "type": "opencollective", | |
| 1773 | + "url": "https://opencollective.com/unified" | |
| 1774 | + } | |
| 1775 | + }, | |
| 1382 | 1776 | "node_modules/esbuild": { |
| 1383 | 1777 | "version": "0.25.12", |
| 1384 | 1778 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", |
@@ -1431,6 +1825,117 @@ | ||
| 1431 | 1825 | "node": ">=6" |
| 1432 | 1826 | } |
| 1433 | 1827 | }, |
| 1828 | + "node_modules/estree-util-attach-comments": { | |
| 1829 | + "version": "3.0.0", | |
| 1830 | + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", | |
| 1831 | + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", | |
| 1832 | + "dev": true, | |
| 1833 | + "license": "MIT", | |
| 1834 | + "dependencies": { | |
| 1835 | + "@types/estree": "^1.0.0" | |
| 1836 | + }, | |
| 1837 | + "funding": { | |
| 1838 | + "type": "opencollective", | |
| 1839 | + "url": "https://opencollective.com/unified" | |
| 1840 | + } | |
| 1841 | + }, | |
| 1842 | + "node_modules/estree-util-build-jsx": { | |
| 1843 | + "version": "3.0.1", | |
| 1844 | + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", | |
| 1845 | + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", | |
| 1846 | + "dev": true, | |
| 1847 | + "license": "MIT", | |
| 1848 | + "dependencies": { | |
| 1849 | + "@types/estree-jsx": "^1.0.0", | |
| 1850 | + "devlop": "^1.0.0", | |
| 1851 | + "estree-util-is-identifier-name": "^3.0.0", | |
| 1852 | + "estree-walker": "^3.0.0" | |
| 1853 | + }, | |
| 1854 | + "funding": { | |
| 1855 | + "type": "opencollective", | |
| 1856 | + "url": "https://opencollective.com/unified" | |
| 1857 | + } | |
| 1858 | + }, | |
| 1859 | + "node_modules/estree-util-is-identifier-name": { | |
| 1860 | + "version": "3.0.0", | |
| 1861 | + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", | |
| 1862 | + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", | |
| 1863 | + "dev": true, | |
| 1864 | + "license": "MIT", | |
| 1865 | + "funding": { | |
| 1866 | + "type": "opencollective", | |
| 1867 | + "url": "https://opencollective.com/unified" | |
| 1868 | + } | |
| 1869 | + }, | |
| 1870 | + "node_modules/estree-util-scope": { | |
| 1871 | + "version": "1.0.1", | |
| 1872 | + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.1.tgz", | |
| 1873 | + "integrity": "sha512-B0np3dcdxqILX5e9nEi5/Fr4K7gL4oYFVPV1zRa2e9wRCbQoZZNWOZFYyoInvXUPJXBXjss+QXlWLJChDEHDkA==", | |
| 1874 | + "dev": true, | |
| 1875 | + "license": "MIT", | |
| 1876 | + "dependencies": { | |
| 1877 | + "@types/estree": "^1.0.0", | |
| 1878 | + "devlop": "^1.0.0" | |
| 1879 | + }, | |
| 1880 | + "funding": { | |
| 1881 | + "type": "opencollective", | |
| 1882 | + "url": "https://opencollective.com/unified" | |
| 1883 | + } | |
| 1884 | + }, | |
| 1885 | + "node_modules/estree-util-to-js": { | |
| 1886 | + "version": "2.0.0", | |
| 1887 | + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", | |
| 1888 | + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", | |
| 1889 | + "dev": true, | |
| 1890 | + "license": "MIT", | |
| 1891 | + "dependencies": { | |
| 1892 | + "@types/estree-jsx": "^1.0.0", | |
| 1893 | + "astring": "^1.8.0", | |
| 1894 | + "source-map": "^0.7.0" | |
| 1895 | + }, | |
| 1896 | + "funding": { | |
| 1897 | + "type": "opencollective", | |
| 1898 | + "url": "https://opencollective.com/unified" | |
| 1899 | + } | |
| 1900 | + }, | |
| 1901 | + "node_modules/estree-util-visit": { | |
| 1902 | + "version": "2.0.0", | |
| 1903 | + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", | |
| 1904 | + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", | |
| 1905 | + "dev": true, | |
| 1906 | + "license": "MIT", | |
| 1907 | + "dependencies": { | |
| 1908 | + "@types/estree-jsx": "^1.0.0", | |
| 1909 | + "@types/unist": "^3.0.0" | |
| 1910 | + }, | |
| 1911 | + "funding": { | |
| 1912 | + "type": "opencollective", | |
| 1913 | + "url": "https://opencollective.com/unified" | |
| 1914 | + } | |
| 1915 | + }, | |
| 1916 | + "node_modules/estree-walker": { | |
| 1917 | + "version": "3.0.3", | |
| 1918 | + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", | |
| 1919 | + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", | |
| 1920 | + "dev": true, | |
| 1921 | + "license": "MIT", | |
| 1922 | + "dependencies": { | |
| 1923 | + "@types/estree": "^1.0.0" | |
| 1924 | + } | |
| 1925 | + }, | |
| 1926 | + "node_modules/extend": { | |
| 1927 | + "version": "3.0.2", | |
| 1928 | + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", | |
| 1929 | + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", | |
| 1930 | + "dev": true, | |
| 1931 | + "license": "MIT" | |
| 1932 | + }, | |
| 1933 | + "node_modules/fancy-canvas": { | |
| 1934 | + "version": "2.1.0", | |
| 1935 | + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", | |
| 1936 | + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", | |
| 1937 | + "license": "MIT" | |
| 1938 | + }, | |
| 1434 | 1939 | "node_modules/fdir": { |
| 1435 | 1940 | "version": "6.5.0", |
| 1436 | 1941 | "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", |
@@ -1474,20 +1979,159 @@ | ||
| 1474 | 1979 | "node": ">=6.9.0" |
| 1475 | 1980 | } |
| 1476 | 1981 | }, |
| 1477 | − "node_modules/js-tokens": { | |
| 1478 | − "version": "4.0.0", | |
| 1479 | − "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", | |
| 1480 | − "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", | |
| 1481 | − "license": "MIT" | |
| 1482 | − }, | |
| 1483 | − "node_modules/jsesc": { | |
| 1484 | − "version": "3.1.0", | |
| 1485 | − "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", | |
| 1486 | − "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", | |
| 1982 | + "node_modules/hast-util-to-estree": { | |
| 1983 | + "version": "3.1.3", | |
| 1984 | + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", | |
| 1985 | + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", | |
| 1487 | 1986 | "dev": true, |
| 1488 | 1987 | "license": "MIT", |
| 1489 | − "bin": { | |
| 1490 | − "jsesc": "bin/jsesc" | |
| 1988 | + "dependencies": { | |
| 1989 | + "@types/estree": "^1.0.0", | |
| 1990 | + "@types/estree-jsx": "^1.0.0", | |
| 1991 | + "@types/hast": "^3.0.0", | |
| 1992 | + "comma-separated-tokens": "^2.0.0", | |
| 1993 | + "devlop": "^1.0.0", | |
| 1994 | + "estree-util-attach-comments": "^3.0.0", | |
| 1995 | + "estree-util-is-identifier-name": "^3.0.0", | |
| 1996 | + "hast-util-whitespace": "^3.0.0", | |
| 1997 | + "mdast-util-mdx-expression": "^2.0.0", | |
| 1998 | + "mdast-util-mdx-jsx": "^3.0.0", | |
| 1999 | + "mdast-util-mdxjs-esm": "^2.0.0", | |
| 2000 | + "property-information": "^7.0.0", | |
| 2001 | + "space-separated-tokens": "^2.0.0", | |
| 2002 | + "style-to-js": "^1.0.0", | |
| 2003 | + "unist-util-position": "^5.0.0", | |
| 2004 | + "zwitch": "^2.0.0" | |
| 2005 | + }, | |
| 2006 | + "funding": { | |
| 2007 | + "type": "opencollective", | |
| 2008 | + "url": "https://opencollective.com/unified" | |
| 2009 | + } | |
| 2010 | + }, | |
| 2011 | + "node_modules/hast-util-to-jsx-runtime": { | |
| 2012 | + "version": "2.3.6", | |
| 2013 | + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", | |
| 2014 | + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", | |
| 2015 | + "dev": true, | |
| 2016 | + "license": "MIT", | |
| 2017 | + "dependencies": { | |
| 2018 | + "@types/estree": "^1.0.0", | |
| 2019 | + "@types/hast": "^3.0.0", | |
| 2020 | + "@types/unist": "^3.0.0", | |
| 2021 | + "comma-separated-tokens": "^2.0.0", | |
| 2022 | + "devlop": "^1.0.0", | |
| 2023 | + "estree-util-is-identifier-name": "^3.0.0", | |
| 2024 | + "hast-util-whitespace": "^3.0.0", | |
| 2025 | + "mdast-util-mdx-expression": "^2.0.0", | |
| 2026 | + "mdast-util-mdx-jsx": "^3.0.0", | |
| 2027 | + "mdast-util-mdxjs-esm": "^2.0.0", | |
| 2028 | + "property-information": "^7.0.0", | |
| 2029 | + "space-separated-tokens": "^2.0.0", | |
| 2030 | + "style-to-js": "^1.0.0", | |
| 2031 | + "unist-util-position": "^5.0.0", | |
| 2032 | + "vfile-message": "^4.0.0" | |
| 2033 | + }, | |
| 2034 | + "funding": { | |
| 2035 | + "type": "opencollective", | |
| 2036 | + "url": "https://opencollective.com/unified" | |
| 2037 | + } | |
| 2038 | + }, | |
| 2039 | + "node_modules/hast-util-whitespace": { | |
| 2040 | + "version": "3.0.0", | |
| 2041 | + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", | |
| 2042 | + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", | |
| 2043 | + "dev": true, | |
| 2044 | + "license": "MIT", | |
| 2045 | + "dependencies": { | |
| 2046 | + "@types/hast": "^3.0.0" | |
| 2047 | + }, | |
| 2048 | + "funding": { | |
| 2049 | + "type": "opencollective", | |
| 2050 | + "url": "https://opencollective.com/unified" | |
| 2051 | + } | |
| 2052 | + }, | |
| 2053 | + "node_modules/inline-style-parser": { | |
| 2054 | + "version": "0.2.7", | |
| 2055 | + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", | |
| 2056 | + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", | |
| 2057 | + "dev": true, | |
| 2058 | + "license": "MIT" | |
| 2059 | + }, | |
| 2060 | + "node_modules/is-alphabetical": { | |
| 2061 | + "version": "2.0.1", | |
| 2062 | + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", | |
| 2063 | + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", | |
| 2064 | + "dev": true, | |
| 2065 | + "license": "MIT", | |
| 2066 | + "funding": { | |
| 2067 | + "type": "github", | |
| 2068 | + "url": "https://github.com/sponsors/wooorm" | |
| 2069 | + } | |
| 2070 | + }, | |
| 2071 | + "node_modules/is-alphanumerical": { | |
| 2072 | + "version": "2.0.1", | |
| 2073 | + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", | |
| 2074 | + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", | |
| 2075 | + "dev": true, | |
| 2076 | + "license": "MIT", | |
| 2077 | + "dependencies": { | |
| 2078 | + "is-alphabetical": "^2.0.0", | |
| 2079 | + "is-decimal": "^2.0.0" | |
| 2080 | + }, | |
| 2081 | + "funding": { | |
| 2082 | + "type": "github", | |
| 2083 | + "url": "https://github.com/sponsors/wooorm" | |
| 2084 | + } | |
| 2085 | + }, | |
| 2086 | + "node_modules/is-decimal": { | |
| 2087 | + "version": "2.0.1", | |
| 2088 | + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", | |
| 2089 | + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", | |
| 2090 | + "dev": true, | |
| 2091 | + "license": "MIT", | |
| 2092 | + "funding": { | |
| 2093 | + "type": "github", | |
| 2094 | + "url": "https://github.com/sponsors/wooorm" | |
| 2095 | + } | |
| 2096 | + }, | |
| 2097 | + "node_modules/is-hexadecimal": { | |
| 2098 | + "version": "2.0.1", | |
| 2099 | + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", | |
| 2100 | + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", | |
| 2101 | + "dev": true, | |
| 2102 | + "license": "MIT", | |
| 2103 | + "funding": { | |
| 2104 | + "type": "github", | |
| 2105 | + "url": "https://github.com/sponsors/wooorm" | |
| 2106 | + } | |
| 2107 | + }, | |
| 2108 | + "node_modules/is-plain-obj": { | |
| 2109 | + "version": "4.1.0", | |
| 2110 | + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", | |
| 2111 | + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", | |
| 2112 | + "dev": true, | |
| 2113 | + "license": "MIT", | |
| 2114 | + "engines": { | |
| 2115 | + "node": ">=12" | |
| 2116 | + }, | |
| 2117 | + "funding": { | |
| 2118 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 2119 | + } | |
| 2120 | + }, | |
| 2121 | + "node_modules/js-tokens": { | |
| 2122 | + "version": "4.0.0", | |
| 2123 | + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", | |
| 2124 | + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", | |
| 2125 | + "license": "MIT" | |
| 2126 | + }, | |
| 2127 | + "node_modules/jsesc": { | |
| 2128 | + "version": "3.1.0", | |
| 2129 | + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", | |
| 2130 | + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", | |
| 2131 | + "dev": true, | |
| 2132 | + "license": "MIT", | |
| 2133 | + "bin": { | |
| 2134 | + "jsesc": "bin/jsesc" | |
| 1491 | 2135 | }, |
| 1492 | 2136 | "engines": { |
| 1493 | 2137 | "node": ">=6" |
@@ -1506,6 +2150,26 @@ | ||
| 1506 | 2150 | "node": ">=6" |
| 1507 | 2151 | } |
| 1508 | 2152 | }, |
| 2153 | + "node_modules/lightweight-charts": { | |
| 2154 | + "version": "4.2.3", | |
| 2155 | + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.3.tgz", | |
| 2156 | + "integrity": "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw==", | |
| 2157 | + "license": "Apache-2.0", | |
| 2158 | + "dependencies": { | |
| 2159 | + "fancy-canvas": "2.1.0" | |
| 2160 | + } | |
| 2161 | + }, | |
| 2162 | + "node_modules/longest-streak": { | |
| 2163 | + "version": "3.1.0", | |
| 2164 | + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", | |
| 2165 | + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", | |
| 2166 | + "dev": true, | |
| 2167 | + "license": "MIT", | |
| 2168 | + "funding": { | |
| 2169 | + "type": "github", | |
| 2170 | + "url": "https://github.com/sponsors/wooorm" | |
| 2171 | + } | |
| 2172 | + }, | |
| 1509 | 2173 | "node_modules/loose-envify": { |
| 1510 | 2174 | "version": "1.4.0", |
| 1511 | 2175 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", |
@@ -1518,15 +2182,831 @@ | ||
| 1518 | 2182 | "loose-envify": "cli.js" |
| 1519 | 2183 | } |
| 1520 | 2184 | }, |
| 1521 | − "node_modules/lru-cache": { | |
| 1522 | − "version": "5.1.1", | |
| 1523 | − "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", | |
| 1524 | − "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", | |
| 2185 | + "node_modules/lru-cache": { | |
| 2186 | + "version": "5.1.1", | |
| 2187 | + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", | |
| 2188 | + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", | |
| 2189 | + "dev": true, | |
| 2190 | + "license": "ISC", | |
| 2191 | + "dependencies": { | |
| 2192 | + "yallist": "^3.0.2" | |
| 2193 | + } | |
| 2194 | + }, | |
| 2195 | + "node_modules/markdown-extensions": { | |
| 2196 | + "version": "2.0.0", | |
| 2197 | + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", | |
| 2198 | + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", | |
| 2199 | + "dev": true, | |
| 2200 | + "license": "MIT", | |
| 2201 | + "engines": { | |
| 2202 | + "node": ">=16" | |
| 2203 | + }, | |
| 2204 | + "funding": { | |
| 2205 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 2206 | + } | |
| 2207 | + }, | |
| 2208 | + "node_modules/mdast-util-from-markdown": { | |
| 2209 | + "version": "2.0.3", | |
| 2210 | + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", | |
| 2211 | + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", | |
| 2212 | + "dev": true, | |
| 2213 | + "license": "MIT", | |
| 2214 | + "dependencies": { | |
| 2215 | + "@types/mdast": "^4.0.0", | |
| 2216 | + "@types/unist": "^3.0.0", | |
| 2217 | + "decode-named-character-reference": "^1.0.0", | |
| 2218 | + "devlop": "^1.0.0", | |
| 2219 | + "mdast-util-to-string": "^4.0.0", | |
| 2220 | + "micromark": "^4.0.0", | |
| 2221 | + "micromark-util-decode-numeric-character-reference": "^2.0.0", | |
| 2222 | + "micromark-util-decode-string": "^2.0.0", | |
| 2223 | + "micromark-util-normalize-identifier": "^2.0.0", | |
| 2224 | + "micromark-util-symbol": "^2.0.0", | |
| 2225 | + "micromark-util-types": "^2.0.0", | |
| 2226 | + "unist-util-stringify-position": "^4.0.0" | |
| 2227 | + }, | |
| 2228 | + "funding": { | |
| 2229 | + "type": "opencollective", | |
| 2230 | + "url": "https://opencollective.com/unified" | |
| 2231 | + } | |
| 2232 | + }, | |
| 2233 | + "node_modules/mdast-util-mdx": { | |
| 2234 | + "version": "3.0.0", | |
| 2235 | + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", | |
| 2236 | + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", | |
| 2237 | + "dev": true, | |
| 2238 | + "license": "MIT", | |
| 2239 | + "dependencies": { | |
| 2240 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2241 | + "mdast-util-mdx-expression": "^2.0.0", | |
| 2242 | + "mdast-util-mdx-jsx": "^3.0.0", | |
| 2243 | + "mdast-util-mdxjs-esm": "^2.0.0", | |
| 2244 | + "mdast-util-to-markdown": "^2.0.0" | |
| 2245 | + }, | |
| 2246 | + "funding": { | |
| 2247 | + "type": "opencollective", | |
| 2248 | + "url": "https://opencollective.com/unified" | |
| 2249 | + } | |
| 2250 | + }, | |
| 2251 | + "node_modules/mdast-util-mdx-expression": { | |
| 2252 | + "version": "2.0.1", | |
| 2253 | + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", | |
| 2254 | + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", | |
| 2255 | + "dev": true, | |
| 2256 | + "license": "MIT", | |
| 2257 | + "dependencies": { | |
| 2258 | + "@types/estree-jsx": "^1.0.0", | |
| 2259 | + "@types/hast": "^3.0.0", | |
| 2260 | + "@types/mdast": "^4.0.0", | |
| 2261 | + "devlop": "^1.0.0", | |
| 2262 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2263 | + "mdast-util-to-markdown": "^2.0.0" | |
| 2264 | + }, | |
| 2265 | + "funding": { | |
| 2266 | + "type": "opencollective", | |
| 2267 | + "url": "https://opencollective.com/unified" | |
| 2268 | + } | |
| 2269 | + }, | |
| 2270 | + "node_modules/mdast-util-mdx-jsx": { | |
| 2271 | + "version": "3.2.0", | |
| 2272 | + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", | |
| 2273 | + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", | |
| 2274 | + "dev": true, | |
| 2275 | + "license": "MIT", | |
| 2276 | + "dependencies": { | |
| 2277 | + "@types/estree-jsx": "^1.0.0", | |
| 2278 | + "@types/hast": "^3.0.0", | |
| 2279 | + "@types/mdast": "^4.0.0", | |
| 2280 | + "@types/unist": "^3.0.0", | |
| 2281 | + "ccount": "^2.0.0", | |
| 2282 | + "devlop": "^1.1.0", | |
| 2283 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2284 | + "mdast-util-to-markdown": "^2.0.0", | |
| 2285 | + "parse-entities": "^4.0.0", | |
| 2286 | + "stringify-entities": "^4.0.0", | |
| 2287 | + "unist-util-stringify-position": "^4.0.0", | |
| 2288 | + "vfile-message": "^4.0.0" | |
| 2289 | + }, | |
| 2290 | + "funding": { | |
| 2291 | + "type": "opencollective", | |
| 2292 | + "url": "https://opencollective.com/unified" | |
| 2293 | + } | |
| 2294 | + }, | |
| 2295 | + "node_modules/mdast-util-mdxjs-esm": { | |
| 2296 | + "version": "2.0.1", | |
| 2297 | + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", | |
| 2298 | + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", | |
| 2299 | + "dev": true, | |
| 2300 | + "license": "MIT", | |
| 2301 | + "dependencies": { | |
| 2302 | + "@types/estree-jsx": "^1.0.0", | |
| 2303 | + "@types/hast": "^3.0.0", | |
| 2304 | + "@types/mdast": "^4.0.0", | |
| 2305 | + "devlop": "^1.0.0", | |
| 2306 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2307 | + "mdast-util-to-markdown": "^2.0.0" | |
| 2308 | + }, | |
| 2309 | + "funding": { | |
| 2310 | + "type": "opencollective", | |
| 2311 | + "url": "https://opencollective.com/unified" | |
| 2312 | + } | |
| 2313 | + }, | |
| 2314 | + "node_modules/mdast-util-phrasing": { | |
| 2315 | + "version": "4.1.0", | |
| 2316 | + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", | |
| 2317 | + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", | |
| 2318 | + "dev": true, | |
| 2319 | + "license": "MIT", | |
| 2320 | + "dependencies": { | |
| 2321 | + "@types/mdast": "^4.0.0", | |
| 2322 | + "unist-util-is": "^6.0.0" | |
| 2323 | + }, | |
| 2324 | + "funding": { | |
| 2325 | + "type": "opencollective", | |
| 2326 | + "url": "https://opencollective.com/unified" | |
| 2327 | + } | |
| 2328 | + }, | |
| 2329 | + "node_modules/mdast-util-to-hast": { | |
| 2330 | + "version": "13.2.1", | |
| 2331 | + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", | |
| 2332 | + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", | |
| 2333 | + "dev": true, | |
| 2334 | + "license": "MIT", | |
| 2335 | + "dependencies": { | |
| 2336 | + "@types/hast": "^3.0.0", | |
| 2337 | + "@types/mdast": "^4.0.0", | |
| 2338 | + "@ungap/structured-clone": "^1.0.0", | |
| 2339 | + "devlop": "^1.0.0", | |
| 2340 | + "micromark-util-sanitize-uri": "^2.0.0", | |
| 2341 | + "trim-lines": "^3.0.0", | |
| 2342 | + "unist-util-position": "^5.0.0", | |
| 2343 | + "unist-util-visit": "^5.0.0", | |
| 2344 | + "vfile": "^6.0.0" | |
| 2345 | + }, | |
| 2346 | + "funding": { | |
| 2347 | + "type": "opencollective", | |
| 2348 | + "url": "https://opencollective.com/unified" | |
| 2349 | + } | |
| 2350 | + }, | |
| 2351 | + "node_modules/mdast-util-to-markdown": { | |
| 2352 | + "version": "2.1.2", | |
| 2353 | + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", | |
| 2354 | + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", | |
| 2355 | + "dev": true, | |
| 2356 | + "license": "MIT", | |
| 2357 | + "dependencies": { | |
| 2358 | + "@types/mdast": "^4.0.0", | |
| 2359 | + "@types/unist": "^3.0.0", | |
| 2360 | + "longest-streak": "^3.0.0", | |
| 2361 | + "mdast-util-phrasing": "^4.0.0", | |
| 2362 | + "mdast-util-to-string": "^4.0.0", | |
| 2363 | + "micromark-util-classify-character": "^2.0.0", | |
| 2364 | + "micromark-util-decode-string": "^2.0.0", | |
| 2365 | + "unist-util-visit": "^5.0.0", | |
| 2366 | + "zwitch": "^2.0.0" | |
| 2367 | + }, | |
| 2368 | + "funding": { | |
| 2369 | + "type": "opencollective", | |
| 2370 | + "url": "https://opencollective.com/unified" | |
| 2371 | + } | |
| 2372 | + }, | |
| 2373 | + "node_modules/mdast-util-to-string": { | |
| 2374 | + "version": "4.0.0", | |
| 2375 | + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", | |
| 2376 | + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", | |
| 2377 | + "dev": true, | |
| 2378 | + "license": "MIT", | |
| 2379 | + "dependencies": { | |
| 2380 | + "@types/mdast": "^4.0.0" | |
| 2381 | + }, | |
| 2382 | + "funding": { | |
| 2383 | + "type": "opencollective", | |
| 2384 | + "url": "https://opencollective.com/unified" | |
| 2385 | + } | |
| 2386 | + }, | |
| 2387 | + "node_modules/micromark": { | |
| 2388 | + "version": "4.0.2", | |
| 2389 | + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", | |
| 2390 | + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", | |
| 2391 | + "dev": true, | |
| 2392 | + "funding": [ | |
| 2393 | + { | |
| 2394 | + "type": "GitHub Sponsors", | |
| 2395 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2396 | + }, | |
| 2397 | + { | |
| 2398 | + "type": "OpenCollective", | |
| 2399 | + "url": "https://opencollective.com/unified" | |
| 2400 | + } | |
| 2401 | + ], | |
| 2402 | + "license": "MIT", | |
| 2403 | + "dependencies": { | |
| 2404 | + "@types/debug": "^4.0.0", | |
| 2405 | + "debug": "^4.0.0", | |
| 2406 | + "decode-named-character-reference": "^1.0.0", | |
| 2407 | + "devlop": "^1.0.0", | |
| 2408 | + "micromark-core-commonmark": "^2.0.0", | |
| 2409 | + "micromark-factory-space": "^2.0.0", | |
| 2410 | + "micromark-util-character": "^2.0.0", | |
| 2411 | + "micromark-util-chunked": "^2.0.0", | |
| 2412 | + "micromark-util-combine-extensions": "^2.0.0", | |
| 2413 | + "micromark-util-decode-numeric-character-reference": "^2.0.0", | |
| 2414 | + "micromark-util-encode": "^2.0.0", | |
| 2415 | + "micromark-util-normalize-identifier": "^2.0.0", | |
| 2416 | + "micromark-util-resolve-all": "^2.0.0", | |
| 2417 | + "micromark-util-sanitize-uri": "^2.0.0", | |
| 2418 | + "micromark-util-subtokenize": "^2.0.0", | |
| 2419 | + "micromark-util-symbol": "^2.0.0", | |
| 2420 | + "micromark-util-types": "^2.0.0" | |
| 2421 | + } | |
| 2422 | + }, | |
| 2423 | + "node_modules/micromark-core-commonmark": { | |
| 2424 | + "version": "2.0.3", | |
| 2425 | + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", | |
| 2426 | + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", | |
| 2427 | + "dev": true, | |
| 2428 | + "funding": [ | |
| 2429 | + { | |
| 2430 | + "type": "GitHub Sponsors", | |
| 2431 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2432 | + }, | |
| 2433 | + { | |
| 2434 | + "type": "OpenCollective", | |
| 2435 | + "url": "https://opencollective.com/unified" | |
| 2436 | + } | |
| 2437 | + ], | |
| 2438 | + "license": "MIT", | |
| 2439 | + "dependencies": { | |
| 2440 | + "decode-named-character-reference": "^1.0.0", | |
| 2441 | + "devlop": "^1.0.0", | |
| 2442 | + "micromark-factory-destination": "^2.0.0", | |
| 2443 | + "micromark-factory-label": "^2.0.0", | |
| 2444 | + "micromark-factory-space": "^2.0.0", | |
| 2445 | + "micromark-factory-title": "^2.0.0", | |
| 2446 | + "micromark-factory-whitespace": "^2.0.0", | |
| 2447 | + "micromark-util-character": "^2.0.0", | |
| 2448 | + "micromark-util-chunked": "^2.0.0", | |
| 2449 | + "micromark-util-classify-character": "^2.0.0", | |
| 2450 | + "micromark-util-html-tag-name": "^2.0.0", | |
| 2451 | + "micromark-util-normalize-identifier": "^2.0.0", | |
| 2452 | + "micromark-util-resolve-all": "^2.0.0", | |
| 2453 | + "micromark-util-subtokenize": "^2.0.0", | |
| 2454 | + "micromark-util-symbol": "^2.0.0", | |
| 2455 | + "micromark-util-types": "^2.0.0" | |
| 2456 | + } | |
| 2457 | + }, | |
| 2458 | + "node_modules/micromark-extension-mdx-expression": { | |
| 2459 | + "version": "3.0.1", | |
| 2460 | + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", | |
| 2461 | + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", | |
| 2462 | + "dev": true, | |
| 2463 | + "funding": [ | |
| 2464 | + { | |
| 2465 | + "type": "GitHub Sponsors", | |
| 2466 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2467 | + }, | |
| 2468 | + { | |
| 2469 | + "type": "OpenCollective", | |
| 2470 | + "url": "https://opencollective.com/unified" | |
| 2471 | + } | |
| 2472 | + ], | |
| 2473 | + "license": "MIT", | |
| 2474 | + "dependencies": { | |
| 2475 | + "@types/estree": "^1.0.0", | |
| 2476 | + "devlop": "^1.0.0", | |
| 2477 | + "micromark-factory-mdx-expression": "^2.0.0", | |
| 2478 | + "micromark-factory-space": "^2.0.0", | |
| 2479 | + "micromark-util-character": "^2.0.0", | |
| 2480 | + "micromark-util-events-to-acorn": "^2.0.0", | |
| 2481 | + "micromark-util-symbol": "^2.0.0", | |
| 2482 | + "micromark-util-types": "^2.0.0" | |
| 2483 | + } | |
| 2484 | + }, | |
| 2485 | + "node_modules/micromark-extension-mdx-jsx": { | |
| 2486 | + "version": "3.0.2", | |
| 2487 | + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", | |
| 2488 | + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", | |
| 2489 | + "dev": true, | |
| 2490 | + "license": "MIT", | |
| 2491 | + "dependencies": { | |
| 2492 | + "@types/estree": "^1.0.0", | |
| 2493 | + "devlop": "^1.0.0", | |
| 2494 | + "estree-util-is-identifier-name": "^3.0.0", | |
| 2495 | + "micromark-factory-mdx-expression": "^2.0.0", | |
| 2496 | + "micromark-factory-space": "^2.0.0", | |
| 2497 | + "micromark-util-character": "^2.0.0", | |
| 2498 | + "micromark-util-events-to-acorn": "^2.0.0", | |
| 2499 | + "micromark-util-symbol": "^2.0.0", | |
| 2500 | + "micromark-util-types": "^2.0.0", | |
| 2501 | + "vfile-message": "^4.0.0" | |
| 2502 | + }, | |
| 2503 | + "funding": { | |
| 2504 | + "type": "opencollective", | |
| 2505 | + "url": "https://opencollective.com/unified" | |
| 2506 | + } | |
| 2507 | + }, | |
| 2508 | + "node_modules/micromark-extension-mdx-md": { | |
| 2509 | + "version": "2.0.0", | |
| 2510 | + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", | |
| 2511 | + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", | |
| 2512 | + "dev": true, | |
| 2513 | + "license": "MIT", | |
| 2514 | + "dependencies": { | |
| 2515 | + "micromark-util-types": "^2.0.0" | |
| 2516 | + }, | |
| 2517 | + "funding": { | |
| 2518 | + "type": "opencollective", | |
| 2519 | + "url": "https://opencollective.com/unified" | |
| 2520 | + } | |
| 2521 | + }, | |
| 2522 | + "node_modules/micromark-extension-mdxjs": { | |
| 2523 | + "version": "3.0.0", | |
| 2524 | + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", | |
| 2525 | + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", | |
| 2526 | + "dev": true, | |
| 2527 | + "license": "MIT", | |
| 2528 | + "dependencies": { | |
| 2529 | + "acorn": "^8.0.0", | |
| 2530 | + "acorn-jsx": "^5.0.0", | |
| 2531 | + "micromark-extension-mdx-expression": "^3.0.0", | |
| 2532 | + "micromark-extension-mdx-jsx": "^3.0.0", | |
| 2533 | + "micromark-extension-mdx-md": "^2.0.0", | |
| 2534 | + "micromark-extension-mdxjs-esm": "^3.0.0", | |
| 2535 | + "micromark-util-combine-extensions": "^2.0.0", | |
| 2536 | + "micromark-util-types": "^2.0.0" | |
| 2537 | + }, | |
| 2538 | + "funding": { | |
| 2539 | + "type": "opencollective", | |
| 2540 | + "url": "https://opencollective.com/unified" | |
| 2541 | + } | |
| 2542 | + }, | |
| 2543 | + "node_modules/micromark-extension-mdxjs-esm": { | |
| 2544 | + "version": "3.0.0", | |
| 2545 | + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", | |
| 2546 | + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", | |
| 2547 | + "dev": true, | |
| 2548 | + "license": "MIT", | |
| 2549 | + "dependencies": { | |
| 2550 | + "@types/estree": "^1.0.0", | |
| 2551 | + "devlop": "^1.0.0", | |
| 2552 | + "micromark-core-commonmark": "^2.0.0", | |
| 2553 | + "micromark-util-character": "^2.0.0", | |
| 2554 | + "micromark-util-events-to-acorn": "^2.0.0", | |
| 2555 | + "micromark-util-symbol": "^2.0.0", | |
| 2556 | + "micromark-util-types": "^2.0.0", | |
| 2557 | + "unist-util-position-from-estree": "^2.0.0", | |
| 2558 | + "vfile-message": "^4.0.0" | |
| 2559 | + }, | |
| 2560 | + "funding": { | |
| 2561 | + "type": "opencollective", | |
| 2562 | + "url": "https://opencollective.com/unified" | |
| 2563 | + } | |
| 2564 | + }, | |
| 2565 | + "node_modules/micromark-factory-destination": { | |
| 2566 | + "version": "2.0.1", | |
| 2567 | + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", | |
| 2568 | + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", | |
| 2569 | + "dev": true, | |
| 2570 | + "funding": [ | |
| 2571 | + { | |
| 2572 | + "type": "GitHub Sponsors", | |
| 2573 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2574 | + }, | |
| 2575 | + { | |
| 2576 | + "type": "OpenCollective", | |
| 2577 | + "url": "https://opencollective.com/unified" | |
| 2578 | + } | |
| 2579 | + ], | |
| 2580 | + "license": "MIT", | |
| 2581 | + "dependencies": { | |
| 2582 | + "micromark-util-character": "^2.0.0", | |
| 2583 | + "micromark-util-symbol": "^2.0.0", | |
| 2584 | + "micromark-util-types": "^2.0.0" | |
| 2585 | + } | |
| 2586 | + }, | |
| 2587 | + "node_modules/micromark-factory-label": { | |
| 2588 | + "version": "2.0.1", | |
| 2589 | + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", | |
| 2590 | + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", | |
| 2591 | + "dev": true, | |
| 2592 | + "funding": [ | |
| 2593 | + { | |
| 2594 | + "type": "GitHub Sponsors", | |
| 2595 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2596 | + }, | |
| 2597 | + { | |
| 2598 | + "type": "OpenCollective", | |
| 2599 | + "url": "https://opencollective.com/unified" | |
| 2600 | + } | |
| 2601 | + ], | |
| 2602 | + "license": "MIT", | |
| 2603 | + "dependencies": { | |
| 2604 | + "devlop": "^1.0.0", | |
| 2605 | + "micromark-util-character": "^2.0.0", | |
| 2606 | + "micromark-util-symbol": "^2.0.0", | |
| 2607 | + "micromark-util-types": "^2.0.0" | |
| 2608 | + } | |
| 2609 | + }, | |
| 2610 | + "node_modules/micromark-factory-mdx-expression": { | |
| 2611 | + "version": "2.0.3", | |
| 2612 | + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", | |
| 2613 | + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", | |
| 2614 | + "dev": true, | |
| 2615 | + "funding": [ | |
| 2616 | + { | |
| 2617 | + "type": "GitHub Sponsors", | |
| 2618 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2619 | + }, | |
| 2620 | + { | |
| 2621 | + "type": "OpenCollective", | |
| 2622 | + "url": "https://opencollective.com/unified" | |
| 2623 | + } | |
| 2624 | + ], | |
| 2625 | + "license": "MIT", | |
| 2626 | + "dependencies": { | |
| 2627 | + "@types/estree": "^1.0.0", | |
| 2628 | + "devlop": "^1.0.0", | |
| 2629 | + "micromark-factory-space": "^2.0.0", | |
| 2630 | + "micromark-util-character": "^2.0.0", | |
| 2631 | + "micromark-util-events-to-acorn": "^2.0.0", | |
| 2632 | + "micromark-util-symbol": "^2.0.0", | |
| 2633 | + "micromark-util-types": "^2.0.0", | |
| 2634 | + "unist-util-position-from-estree": "^2.0.0", | |
| 2635 | + "vfile-message": "^4.0.0" | |
| 2636 | + } | |
| 2637 | + }, | |
| 2638 | + "node_modules/micromark-factory-space": { | |
| 2639 | + "version": "2.0.1", | |
| 2640 | + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", | |
| 2641 | + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", | |
| 2642 | + "dev": true, | |
| 2643 | + "funding": [ | |
| 2644 | + { | |
| 2645 | + "type": "GitHub Sponsors", | |
| 2646 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2647 | + }, | |
| 2648 | + { | |
| 2649 | + "type": "OpenCollective", | |
| 2650 | + "url": "https://opencollective.com/unified" | |
| 2651 | + } | |
| 2652 | + ], | |
| 2653 | + "license": "MIT", | |
| 2654 | + "dependencies": { | |
| 2655 | + "micromark-util-character": "^2.0.0", | |
| 2656 | + "micromark-util-types": "^2.0.0" | |
| 2657 | + } | |
| 2658 | + }, | |
| 2659 | + "node_modules/micromark-factory-title": { | |
| 2660 | + "version": "2.0.1", | |
| 2661 | + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", | |
| 2662 | + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", | |
| 2663 | + "dev": true, | |
| 2664 | + "funding": [ | |
| 2665 | + { | |
| 2666 | + "type": "GitHub Sponsors", | |
| 2667 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2668 | + }, | |
| 2669 | + { | |
| 2670 | + "type": "OpenCollective", | |
| 2671 | + "url": "https://opencollective.com/unified" | |
| 2672 | + } | |
| 2673 | + ], | |
| 2674 | + "license": "MIT", | |
| 2675 | + "dependencies": { | |
| 2676 | + "micromark-factory-space": "^2.0.0", | |
| 2677 | + "micromark-util-character": "^2.0.0", | |
| 2678 | + "micromark-util-symbol": "^2.0.0", | |
| 2679 | + "micromark-util-types": "^2.0.0" | |
| 2680 | + } | |
| 2681 | + }, | |
| 2682 | + "node_modules/micromark-factory-whitespace": { | |
| 2683 | + "version": "2.0.1", | |
| 2684 | + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", | |
| 2685 | + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", | |
| 2686 | + "dev": true, | |
| 2687 | + "funding": [ | |
| 2688 | + { | |
| 2689 | + "type": "GitHub Sponsors", | |
| 2690 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2691 | + }, | |
| 2692 | + { | |
| 2693 | + "type": "OpenCollective", | |
| 2694 | + "url": "https://opencollective.com/unified" | |
| 2695 | + } | |
| 2696 | + ], | |
| 2697 | + "license": "MIT", | |
| 2698 | + "dependencies": { | |
| 2699 | + "micromark-factory-space": "^2.0.0", | |
| 2700 | + "micromark-util-character": "^2.0.0", | |
| 2701 | + "micromark-util-symbol": "^2.0.0", | |
| 2702 | + "micromark-util-types": "^2.0.0" | |
| 2703 | + } | |
| 2704 | + }, | |
| 2705 | + "node_modules/micromark-util-character": { | |
| 2706 | + "version": "2.1.1", | |
| 2707 | + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", | |
| 2708 | + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", | |
| 2709 | + "dev": true, | |
| 2710 | + "funding": [ | |
| 2711 | + { | |
| 2712 | + "type": "GitHub Sponsors", | |
| 2713 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2714 | + }, | |
| 2715 | + { | |
| 2716 | + "type": "OpenCollective", | |
| 2717 | + "url": "https://opencollective.com/unified" | |
| 2718 | + } | |
| 2719 | + ], | |
| 2720 | + "license": "MIT", | |
| 2721 | + "dependencies": { | |
| 2722 | + "micromark-util-symbol": "^2.0.0", | |
| 2723 | + "micromark-util-types": "^2.0.0" | |
| 2724 | + } | |
| 2725 | + }, | |
| 2726 | + "node_modules/micromark-util-chunked": { | |
| 2727 | + "version": "2.0.1", | |
| 2728 | + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", | |
| 2729 | + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", | |
| 2730 | + "dev": true, | |
| 2731 | + "funding": [ | |
| 2732 | + { | |
| 2733 | + "type": "GitHub Sponsors", | |
| 2734 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2735 | + }, | |
| 2736 | + { | |
| 2737 | + "type": "OpenCollective", | |
| 2738 | + "url": "https://opencollective.com/unified" | |
| 2739 | + } | |
| 2740 | + ], | |
| 2741 | + "license": "MIT", | |
| 2742 | + "dependencies": { | |
| 2743 | + "micromark-util-symbol": "^2.0.0" | |
| 2744 | + } | |
| 2745 | + }, | |
| 2746 | + "node_modules/micromark-util-classify-character": { | |
| 2747 | + "version": "2.0.1", | |
| 2748 | + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", | |
| 2749 | + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", | |
| 2750 | + "dev": true, | |
| 2751 | + "funding": [ | |
| 2752 | + { | |
| 2753 | + "type": "GitHub Sponsors", | |
| 2754 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2755 | + }, | |
| 2756 | + { | |
| 2757 | + "type": "OpenCollective", | |
| 2758 | + "url": "https://opencollective.com/unified" | |
| 2759 | + } | |
| 2760 | + ], | |
| 2761 | + "license": "MIT", | |
| 2762 | + "dependencies": { | |
| 2763 | + "micromark-util-character": "^2.0.0", | |
| 2764 | + "micromark-util-symbol": "^2.0.0", | |
| 2765 | + "micromark-util-types": "^2.0.0" | |
| 2766 | + } | |
| 2767 | + }, | |
| 2768 | + "node_modules/micromark-util-combine-extensions": { | |
| 2769 | + "version": "2.0.1", | |
| 2770 | + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", | |
| 2771 | + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", | |
| 2772 | + "dev": true, | |
| 2773 | + "funding": [ | |
| 2774 | + { | |
| 2775 | + "type": "GitHub Sponsors", | |
| 2776 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2777 | + }, | |
| 2778 | + { | |
| 2779 | + "type": "OpenCollective", | |
| 2780 | + "url": "https://opencollective.com/unified" | |
| 2781 | + } | |
| 2782 | + ], | |
| 2783 | + "license": "MIT", | |
| 2784 | + "dependencies": { | |
| 2785 | + "micromark-util-chunked": "^2.0.0", | |
| 2786 | + "micromark-util-types": "^2.0.0" | |
| 2787 | + } | |
| 2788 | + }, | |
| 2789 | + "node_modules/micromark-util-decode-numeric-character-reference": { | |
| 2790 | + "version": "2.0.2", | |
| 2791 | + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", | |
| 2792 | + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", | |
| 2793 | + "dev": true, | |
| 2794 | + "funding": [ | |
| 2795 | + { | |
| 2796 | + "type": "GitHub Sponsors", | |
| 2797 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2798 | + }, | |
| 2799 | + { | |
| 2800 | + "type": "OpenCollective", | |
| 2801 | + "url": "https://opencollective.com/unified" | |
| 2802 | + } | |
| 2803 | + ], | |
| 2804 | + "license": "MIT", | |
| 2805 | + "dependencies": { | |
| 2806 | + "micromark-util-symbol": "^2.0.0" | |
| 2807 | + } | |
| 2808 | + }, | |
| 2809 | + "node_modules/micromark-util-decode-string": { | |
| 2810 | + "version": "2.0.1", | |
| 2811 | + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", | |
| 2812 | + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", | |
| 2813 | + "dev": true, | |
| 2814 | + "funding": [ | |
| 2815 | + { | |
| 2816 | + "type": "GitHub Sponsors", | |
| 2817 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2818 | + }, | |
| 2819 | + { | |
| 2820 | + "type": "OpenCollective", | |
| 2821 | + "url": "https://opencollective.com/unified" | |
| 2822 | + } | |
| 2823 | + ], | |
| 2824 | + "license": "MIT", | |
| 2825 | + "dependencies": { | |
| 2826 | + "decode-named-character-reference": "^1.0.0", | |
| 2827 | + "micromark-util-character": "^2.0.0", | |
| 2828 | + "micromark-util-decode-numeric-character-reference": "^2.0.0", | |
| 2829 | + "micromark-util-symbol": "^2.0.0" | |
| 2830 | + } | |
| 2831 | + }, | |
| 2832 | + "node_modules/micromark-util-encode": { | |
| 2833 | + "version": "2.0.1", | |
| 2834 | + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", | |
| 2835 | + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", | |
| 2836 | + "dev": true, | |
| 2837 | + "funding": [ | |
| 2838 | + { | |
| 2839 | + "type": "GitHub Sponsors", | |
| 2840 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2841 | + }, | |
| 2842 | + { | |
| 2843 | + "type": "OpenCollective", | |
| 2844 | + "url": "https://opencollective.com/unified" | |
| 2845 | + } | |
| 2846 | + ], | |
| 2847 | + "license": "MIT" | |
| 2848 | + }, | |
| 2849 | + "node_modules/micromark-util-events-to-acorn": { | |
| 2850 | + "version": "2.0.3", | |
| 2851 | + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", | |
| 2852 | + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", | |
| 2853 | + "dev": true, | |
| 2854 | + "funding": [ | |
| 2855 | + { | |
| 2856 | + "type": "GitHub Sponsors", | |
| 2857 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2858 | + }, | |
| 2859 | + { | |
| 2860 | + "type": "OpenCollective", | |
| 2861 | + "url": "https://opencollective.com/unified" | |
| 2862 | + } | |
| 2863 | + ], | |
| 2864 | + "license": "MIT", | |
| 2865 | + "dependencies": { | |
| 2866 | + "@types/estree": "^1.0.0", | |
| 2867 | + "@types/unist": "^3.0.0", | |
| 2868 | + "devlop": "^1.0.0", | |
| 2869 | + "estree-util-visit": "^2.0.0", | |
| 2870 | + "micromark-util-symbol": "^2.0.0", | |
| 2871 | + "micromark-util-types": "^2.0.0", | |
| 2872 | + "vfile-message": "^4.0.0" | |
| 2873 | + } | |
| 2874 | + }, | |
| 2875 | + "node_modules/micromark-util-html-tag-name": { | |
| 2876 | + "version": "2.0.1", | |
| 2877 | + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", | |
| 2878 | + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", | |
| 2879 | + "dev": true, | |
| 2880 | + "funding": [ | |
| 2881 | + { | |
| 2882 | + "type": "GitHub Sponsors", | |
| 2883 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2884 | + }, | |
| 2885 | + { | |
| 2886 | + "type": "OpenCollective", | |
| 2887 | + "url": "https://opencollective.com/unified" | |
| 2888 | + } | |
| 2889 | + ], | |
| 2890 | + "license": "MIT" | |
| 2891 | + }, | |
| 2892 | + "node_modules/micromark-util-normalize-identifier": { | |
| 2893 | + "version": "2.0.1", | |
| 2894 | + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", | |
| 2895 | + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", | |
| 2896 | + "dev": true, | |
| 2897 | + "funding": [ | |
| 2898 | + { | |
| 2899 | + "type": "GitHub Sponsors", | |
| 2900 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2901 | + }, | |
| 2902 | + { | |
| 2903 | + "type": "OpenCollective", | |
| 2904 | + "url": "https://opencollective.com/unified" | |
| 2905 | + } | |
| 2906 | + ], | |
| 2907 | + "license": "MIT", | |
| 2908 | + "dependencies": { | |
| 2909 | + "micromark-util-symbol": "^2.0.0" | |
| 2910 | + } | |
| 2911 | + }, | |
| 2912 | + "node_modules/micromark-util-resolve-all": { | |
| 2913 | + "version": "2.0.1", | |
| 2914 | + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", | |
| 2915 | + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", | |
| 2916 | + "dev": true, | |
| 2917 | + "funding": [ | |
| 2918 | + { | |
| 2919 | + "type": "GitHub Sponsors", | |
| 2920 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2921 | + }, | |
| 2922 | + { | |
| 2923 | + "type": "OpenCollective", | |
| 2924 | + "url": "https://opencollective.com/unified" | |
| 2925 | + } | |
| 2926 | + ], | |
| 2927 | + "license": "MIT", | |
| 2928 | + "dependencies": { | |
| 2929 | + "micromark-util-types": "^2.0.0" | |
| 2930 | + } | |
| 2931 | + }, | |
| 2932 | + "node_modules/micromark-util-sanitize-uri": { | |
| 2933 | + "version": "2.0.1", | |
| 2934 | + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", | |
| 2935 | + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", | |
| 2936 | + "dev": true, | |
| 2937 | + "funding": [ | |
| 2938 | + { | |
| 2939 | + "type": "GitHub Sponsors", | |
| 2940 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2941 | + }, | |
| 2942 | + { | |
| 2943 | + "type": "OpenCollective", | |
| 2944 | + "url": "https://opencollective.com/unified" | |
| 2945 | + } | |
| 2946 | + ], | |
| 2947 | + "license": "MIT", | |
| 2948 | + "dependencies": { | |
| 2949 | + "micromark-util-character": "^2.0.0", | |
| 2950 | + "micromark-util-encode": "^2.0.0", | |
| 2951 | + "micromark-util-symbol": "^2.0.0" | |
| 2952 | + } | |
| 2953 | + }, | |
| 2954 | + "node_modules/micromark-util-subtokenize": { | |
| 2955 | + "version": "2.1.0", | |
| 2956 | + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", | |
| 2957 | + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", | |
| 2958 | + "dev": true, | |
| 2959 | + "funding": [ | |
| 2960 | + { | |
| 2961 | + "type": "GitHub Sponsors", | |
| 2962 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2963 | + }, | |
| 2964 | + { | |
| 2965 | + "type": "OpenCollective", | |
| 2966 | + "url": "https://opencollective.com/unified" | |
| 2967 | + } | |
| 2968 | + ], | |
| 2969 | + "license": "MIT", | |
| 2970 | + "dependencies": { | |
| 2971 | + "devlop": "^1.0.0", | |
| 2972 | + "micromark-util-chunked": "^2.0.0", | |
| 2973 | + "micromark-util-symbol": "^2.0.0", | |
| 2974 | + "micromark-util-types": "^2.0.0" | |
| 2975 | + } | |
| 2976 | + }, | |
| 2977 | + "node_modules/micromark-util-symbol": { | |
| 2978 | + "version": "2.0.1", | |
| 2979 | + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", | |
| 2980 | + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", | |
| 2981 | + "dev": true, | |
| 2982 | + "funding": [ | |
| 2983 | + { | |
| 2984 | + "type": "GitHub Sponsors", | |
| 2985 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 2986 | + }, | |
| 2987 | + { | |
| 2988 | + "type": "OpenCollective", | |
| 2989 | + "url": "https://opencollective.com/unified" | |
| 2990 | + } | |
| 2991 | + ], | |
| 2992 | + "license": "MIT" | |
| 2993 | + }, | |
| 2994 | + "node_modules/micromark-util-types": { | |
| 2995 | + "version": "2.0.2", | |
| 2996 | + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", | |
| 2997 | + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", | |
| 1525 | 2998 | "dev": true, |
| 1526 | − "license": "ISC", | |
| 1527 | − "dependencies": { | |
| 1528 | − "yallist": "^3.0.2" | |
| 1529 | − } | |
| 2999 | + "funding": [ | |
| 3000 | + { | |
| 3001 | + "type": "GitHub Sponsors", | |
| 3002 | + "url": "https://github.com/sponsors/unifiedjs" | |
| 3003 | + }, | |
| 3004 | + { | |
| 3005 | + "type": "OpenCollective", | |
| 3006 | + "url": "https://opencollective.com/unified" | |
| 3007 | + } | |
| 3008 | + ], | |
| 3009 | + "license": "MIT" | |
| 1530 | 3010 | }, |
| 1531 | 3011 | "node_modules/ms": { |
| 1532 | 3012 | "version": "2.1.3", |
@@ -1564,6 +3044,33 @@ | ||
| 1564 | 3044 | "node": ">=18" |
| 1565 | 3045 | } |
| 1566 | 3046 | }, |
| 3047 | + "node_modules/parse-entities": { | |
| 3048 | + "version": "4.0.2", | |
| 3049 | + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", | |
| 3050 | + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", | |
| 3051 | + "dev": true, | |
| 3052 | + "license": "MIT", | |
| 3053 | + "dependencies": { | |
| 3054 | + "@types/unist": "^2.0.0", | |
| 3055 | + "character-entities-legacy": "^3.0.0", | |
| 3056 | + "character-reference-invalid": "^2.0.0", | |
| 3057 | + "decode-named-character-reference": "^1.0.0", | |
| 3058 | + "is-alphanumerical": "^2.0.0", | |
| 3059 | + "is-decimal": "^2.0.0", | |
| 3060 | + "is-hexadecimal": "^2.0.0" | |
| 3061 | + }, | |
| 3062 | + "funding": { | |
| 3063 | + "type": "github", | |
| 3064 | + "url": "https://github.com/sponsors/wooorm" | |
| 3065 | + } | |
| 3066 | + }, | |
| 3067 | + "node_modules/parse-entities/node_modules/@types/unist": { | |
| 3068 | + "version": "2.0.11", | |
| 3069 | + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", | |
| 3070 | + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", | |
| 3071 | + "dev": true, | |
| 3072 | + "license": "MIT" | |
| 3073 | + }, | |
| 1567 | 3074 | "node_modules/picocolors": { |
| 1568 | 3075 | "version": "1.1.1", |
| 1569 | 3076 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", |
@@ -1584,6 +3091,35 @@ | ||
| 1584 | 3091 | "url": "https://github.com/sponsors/jonschlinkert" |
| 1585 | 3092 | } |
| 1586 | 3093 | }, |
| 3094 | + "node_modules/playwright": { | |
| 3095 | + "version": "1.63.0", | |
| 3096 | + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", | |
| 3097 | + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", | |
| 3098 | + "dev": true, | |
| 3099 | + "license": "Apache-2.0", | |
| 3100 | + "dependencies": { | |
| 3101 | + "playwright-core": "1.63.0" | |
| 3102 | + }, | |
| 3103 | + "bin": { | |
| 3104 | + "playwright": "cli.js" | |
| 3105 | + }, | |
| 3106 | + "engines": { | |
| 3107 | + "node": ">=20" | |
| 3108 | + } | |
| 3109 | + }, | |
| 3110 | + "node_modules/playwright-core": { | |
| 3111 | + "version": "1.63.0", | |
| 3112 | + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", | |
| 3113 | + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", | |
| 3114 | + "dev": true, | |
| 3115 | + "license": "Apache-2.0", | |
| 3116 | + "bin": { | |
| 3117 | + "playwright-core": "cli.js" | |
| 3118 | + }, | |
| 3119 | + "engines": { | |
| 3120 | + "node": ">=20" | |
| 3121 | + } | |
| 3122 | + }, | |
| 1587 | 3123 | "node_modules/postcss": { |
| 1588 | 3124 | "version": "8.5.26", |
| 1589 | 3125 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", |
@@ -1613,6 +3149,30 @@ | ||
| 1613 | 3149 | "node": "^10 || ^12 || >=14" |
| 1614 | 3150 | } |
| 1615 | 3151 | }, |
| 3152 | + "node_modules/prism-react-renderer": { | |
| 3153 | + "version": "2.4.1", | |
| 3154 | + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", | |
| 3155 | + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", | |
| 3156 | + "license": "MIT", | |
| 3157 | + "dependencies": { | |
| 3158 | + "@types/prismjs": "^1.26.0", | |
| 3159 | + "clsx": "^2.0.0" | |
| 3160 | + }, | |
| 3161 | + "peerDependencies": { | |
| 3162 | + "react": ">=16.0.0" | |
| 3163 | + } | |
| 3164 | + }, | |
| 3165 | + "node_modules/property-information": { | |
| 3166 | + "version": "7.2.0", | |
| 3167 | + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", | |
| 3168 | + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", | |
| 3169 | + "dev": true, | |
| 3170 | + "license": "MIT", | |
| 3171 | + "funding": { | |
| 3172 | + "type": "github", | |
| 3173 | + "url": "https://github.com/sponsors/wooorm" | |
| 3174 | + } | |
| 3175 | + }, | |
| 1616 | 3176 | "node_modules/react": { |
| 1617 | 3177 | "version": "18.3.1", |
| 1618 | 3178 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", |
@@ -1648,6 +3208,175 @@ | ||
| 1648 | 3208 | "node": ">=0.10.0" |
| 1649 | 3209 | } |
| 1650 | 3210 | }, |
| 3211 | + "node_modules/react-router": { | |
| 3212 | + "version": "6.30.6", | |
| 3213 | + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", | |
| 3214 | + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", | |
| 3215 | + "license": "MIT", | |
| 3216 | + "dependencies": { | |
| 3217 | + "@remix-run/router": "1.23.4" | |
| 3218 | + }, | |
| 3219 | + "engines": { | |
| 3220 | + "node": ">=14.0.0" | |
| 3221 | + }, | |
| 3222 | + "peerDependencies": { | |
| 3223 | + "react": ">=16.8" | |
| 3224 | + } | |
| 3225 | + }, | |
| 3226 | + "node_modules/react-router-dom": { | |
| 3227 | + "version": "6.30.6", | |
| 3228 | + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", | |
| 3229 | + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", | |
| 3230 | + "license": "MIT", | |
| 3231 | + "dependencies": { | |
| 3232 | + "@remix-run/router": "1.23.4", | |
| 3233 | + "react-router": "6.30.6" | |
| 3234 | + }, | |
| 3235 | + "engines": { | |
| 3236 | + "node": ">=14.0.0" | |
| 3237 | + }, | |
| 3238 | + "peerDependencies": { | |
| 3239 | + "react": ">=16.8", | |
| 3240 | + "react-dom": ">=16.8" | |
| 3241 | + } | |
| 3242 | + }, | |
| 3243 | + "node_modules/recma-build-jsx": { | |
| 3244 | + "version": "1.0.0", | |
| 3245 | + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", | |
| 3246 | + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", | |
| 3247 | + "dev": true, | |
| 3248 | + "license": "MIT", | |
| 3249 | + "dependencies": { | |
| 3250 | + "@types/estree": "^1.0.0", | |
| 3251 | + "estree-util-build-jsx": "^3.0.0", | |
| 3252 | + "vfile": "^6.0.0" | |
| 3253 | + }, | |
| 3254 | + "funding": { | |
| 3255 | + "type": "opencollective", | |
| 3256 | + "url": "https://opencollective.com/unified" | |
| 3257 | + } | |
| 3258 | + }, | |
| 3259 | + "node_modules/recma-jsx": { | |
| 3260 | + "version": "1.0.1", | |
| 3261 | + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", | |
| 3262 | + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", | |
| 3263 | + "dev": true, | |
| 3264 | + "license": "MIT", | |
| 3265 | + "dependencies": { | |
| 3266 | + "acorn-jsx": "^5.0.0", | |
| 3267 | + "estree-util-to-js": "^2.0.0", | |
| 3268 | + "recma-parse": "^1.0.0", | |
| 3269 | + "recma-stringify": "^1.0.0", | |
| 3270 | + "unified": "^11.0.0" | |
| 3271 | + }, | |
| 3272 | + "funding": { | |
| 3273 | + "type": "opencollective", | |
| 3274 | + "url": "https://opencollective.com/unified" | |
| 3275 | + }, | |
| 3276 | + "peerDependencies": { | |
| 3277 | + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" | |
| 3278 | + } | |
| 3279 | + }, | |
| 3280 | + "node_modules/recma-parse": { | |
| 3281 | + "version": "1.0.0", | |
| 3282 | + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", | |
| 3283 | + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", | |
| 3284 | + "dev": true, | |
| 3285 | + "license": "MIT", | |
| 3286 | + "dependencies": { | |
| 3287 | + "@types/estree": "^1.0.0", | |
| 3288 | + "esast-util-from-js": "^2.0.0", | |
| 3289 | + "unified": "^11.0.0", | |
| 3290 | + "vfile": "^6.0.0" | |
| 3291 | + }, | |
| 3292 | + "funding": { | |
| 3293 | + "type": "opencollective", | |
| 3294 | + "url": "https://opencollective.com/unified" | |
| 3295 | + } | |
| 3296 | + }, | |
| 3297 | + "node_modules/recma-stringify": { | |
| 3298 | + "version": "1.0.0", | |
| 3299 | + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", | |
| 3300 | + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", | |
| 3301 | + "dev": true, | |
| 3302 | + "license": "MIT", | |
| 3303 | + "dependencies": { | |
| 3304 | + "@types/estree": "^1.0.0", | |
| 3305 | + "estree-util-to-js": "^2.0.0", | |
| 3306 | + "unified": "^11.0.0", | |
| 3307 | + "vfile": "^6.0.0" | |
| 3308 | + }, | |
| 3309 | + "funding": { | |
| 3310 | + "type": "opencollective", | |
| 3311 | + "url": "https://opencollective.com/unified" | |
| 3312 | + } | |
| 3313 | + }, | |
| 3314 | + "node_modules/rehype-recma": { | |
| 3315 | + "version": "1.0.0", | |
| 3316 | + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", | |
| 3317 | + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", | |
| 3318 | + "dev": true, | |
| 3319 | + "license": "MIT", | |
| 3320 | + "dependencies": { | |
| 3321 | + "@types/estree": "^1.0.0", | |
| 3322 | + "@types/hast": "^3.0.0", | |
| 3323 | + "hast-util-to-estree": "^3.0.0" | |
| 3324 | + }, | |
| 3325 | + "funding": { | |
| 3326 | + "type": "opencollective", | |
| 3327 | + "url": "https://opencollective.com/unified" | |
| 3328 | + } | |
| 3329 | + }, | |
| 3330 | + "node_modules/remark-mdx": { | |
| 3331 | + "version": "3.1.1", | |
| 3332 | + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", | |
| 3333 | + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", | |
| 3334 | + "dev": true, | |
| 3335 | + "license": "MIT", | |
| 3336 | + "dependencies": { | |
| 3337 | + "mdast-util-mdx": "^3.0.0", | |
| 3338 | + "micromark-extension-mdxjs": "^3.0.0" | |
| 3339 | + }, | |
| 3340 | + "funding": { | |
| 3341 | + "type": "opencollective", | |
| 3342 | + "url": "https://opencollective.com/unified" | |
| 3343 | + } | |
| 3344 | + }, | |
| 3345 | + "node_modules/remark-parse": { | |
| 3346 | + "version": "11.0.0", | |
| 3347 | + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", | |
| 3348 | + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", | |
| 3349 | + "dev": true, | |
| 3350 | + "license": "MIT", | |
| 3351 | + "dependencies": { | |
| 3352 | + "@types/mdast": "^4.0.0", | |
| 3353 | + "mdast-util-from-markdown": "^2.0.0", | |
| 3354 | + "micromark-util-types": "^2.0.0", | |
| 3355 | + "unified": "^11.0.0" | |
| 3356 | + }, | |
| 3357 | + "funding": { | |
| 3358 | + "type": "opencollective", | |
| 3359 | + "url": "https://opencollective.com/unified" | |
| 3360 | + } | |
| 3361 | + }, | |
| 3362 | + "node_modules/remark-rehype": { | |
| 3363 | + "version": "11.1.2", | |
| 3364 | + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", | |
| 3365 | + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", | |
| 3366 | + "dev": true, | |
| 3367 | + "license": "MIT", | |
| 3368 | + "dependencies": { | |
| 3369 | + "@types/hast": "^3.0.0", | |
| 3370 | + "@types/mdast": "^4.0.0", | |
| 3371 | + "mdast-util-to-hast": "^13.0.0", | |
| 3372 | + "unified": "^11.0.0", | |
| 3373 | + "vfile": "^6.0.0" | |
| 3374 | + }, | |
| 3375 | + "funding": { | |
| 3376 | + "type": "opencollective", | |
| 3377 | + "url": "https://opencollective.com/unified" | |
| 3378 | + } | |
| 3379 | + }, | |
| 1651 | 3380 | "node_modules/rollup": { |
| 1652 | 3381 | "version": "4.62.4", |
| 1653 | 3382 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", |
@@ -1713,6 +3442,16 @@ | ||
| 1713 | 3442 | "semver": "bin/semver.js" |
| 1714 | 3443 | } |
| 1715 | 3444 | }, |
| 3445 | + "node_modules/source-map": { | |
| 3446 | + "version": "0.7.6", | |
| 3447 | + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", | |
| 3448 | + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", | |
| 3449 | + "dev": true, | |
| 3450 | + "license": "BSD-3-Clause", | |
| 3451 | + "engines": { | |
| 3452 | + "node": ">= 12" | |
| 3453 | + } | |
| 3454 | + }, | |
| 1716 | 3455 | "node_modules/source-map-js": { |
| 1717 | 3456 | "version": "1.2.1", |
| 1718 | 3457 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", |
@@ -1723,6 +3462,52 @@ | ||
| 1723 | 3462 | "node": ">=0.10.0" |
| 1724 | 3463 | } |
| 1725 | 3464 | }, |
| 3465 | + "node_modules/space-separated-tokens": { | |
| 3466 | + "version": "2.0.2", | |
| 3467 | + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", | |
| 3468 | + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", | |
| 3469 | + "dev": true, | |
| 3470 | + "license": "MIT", | |
| 3471 | + "funding": { | |
| 3472 | + "type": "github", | |
| 3473 | + "url": "https://github.com/sponsors/wooorm" | |
| 3474 | + } | |
| 3475 | + }, | |
| 3476 | + "node_modules/stringify-entities": { | |
| 3477 | + "version": "4.0.4", | |
| 3478 | + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", | |
| 3479 | + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", | |
| 3480 | + "dev": true, | |
| 3481 | + "license": "MIT", | |
| 3482 | + "dependencies": { | |
| 3483 | + "character-entities-html4": "^2.0.0", | |
| 3484 | + "character-entities-legacy": "^3.0.0" | |
| 3485 | + }, | |
| 3486 | + "funding": { | |
| 3487 | + "type": "github", | |
| 3488 | + "url": "https://github.com/sponsors/wooorm" | |
| 3489 | + } | |
| 3490 | + }, | |
| 3491 | + "node_modules/style-to-js": { | |
| 3492 | + "version": "1.1.21", | |
| 3493 | + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", | |
| 3494 | + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", | |
| 3495 | + "dev": true, | |
| 3496 | + "license": "MIT", | |
| 3497 | + "dependencies": { | |
| 3498 | + "style-to-object": "1.0.14" | |
| 3499 | + } | |
| 3500 | + }, | |
| 3501 | + "node_modules/style-to-object": { | |
| 3502 | + "version": "1.0.14", | |
| 3503 | + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", | |
| 3504 | + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", | |
| 3505 | + "dev": true, | |
| 3506 | + "license": "MIT", | |
| 3507 | + "dependencies": { | |
| 3508 | + "inline-style-parser": "0.2.7" | |
| 3509 | + } | |
| 3510 | + }, | |
| 1726 | 3511 | "node_modules/tinyglobby": { |
| 1727 | 3512 | "version": "0.2.17", |
| 1728 | 3513 | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", |
@@ -1740,6 +3525,135 @@ | ||
| 1740 | 3525 | "url": "https://github.com/sponsors/SuperchupuDev" |
| 1741 | 3526 | } |
| 1742 | 3527 | }, |
| 3528 | + "node_modules/trim-lines": { | |
| 3529 | + "version": "3.0.1", | |
| 3530 | + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", | |
| 3531 | + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", | |
| 3532 | + "dev": true, | |
| 3533 | + "license": "MIT", | |
| 3534 | + "funding": { | |
| 3535 | + "type": "github", | |
| 3536 | + "url": "https://github.com/sponsors/wooorm" | |
| 3537 | + } | |
| 3538 | + }, | |
| 3539 | + "node_modules/trough": { | |
| 3540 | + "version": "2.2.0", | |
| 3541 | + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", | |
| 3542 | + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", | |
| 3543 | + "dev": true, | |
| 3544 | + "license": "MIT", | |
| 3545 | + "funding": { | |
| 3546 | + "type": "github", | |
| 3547 | + "url": "https://github.com/sponsors/wooorm" | |
| 3548 | + } | |
| 3549 | + }, | |
| 3550 | + "node_modules/unified": { | |
| 3551 | + "version": "11.0.5", | |
| 3552 | + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", | |
| 3553 | + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", | |
| 3554 | + "dev": true, | |
| 3555 | + "license": "MIT", | |
| 3556 | + "dependencies": { | |
| 3557 | + "@types/unist": "^3.0.0", | |
| 3558 | + "bail": "^2.0.0", | |
| 3559 | + "devlop": "^1.0.0", | |
| 3560 | + "extend": "^3.0.0", | |
| 3561 | + "is-plain-obj": "^4.0.0", | |
| 3562 | + "trough": "^2.0.0", | |
| 3563 | + "vfile": "^6.0.0" | |
| 3564 | + }, | |
| 3565 | + "funding": { | |
| 3566 | + "type": "opencollective", | |
| 3567 | + "url": "https://opencollective.com/unified" | |
| 3568 | + } | |
| 3569 | + }, | |
| 3570 | + "node_modules/unist-util-is": { | |
| 3571 | + "version": "6.0.1", | |
| 3572 | + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", | |
| 3573 | + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", | |
| 3574 | + "dev": true, | |
| 3575 | + "license": "MIT", | |
| 3576 | + "dependencies": { | |
| 3577 | + "@types/unist": "^3.0.0" | |
| 3578 | + }, | |
| 3579 | + "funding": { | |
| 3580 | + "type": "opencollective", | |
| 3581 | + "url": "https://opencollective.com/unified" | |
| 3582 | + } | |
| 3583 | + }, | |
| 3584 | + "node_modules/unist-util-position": { | |
| 3585 | + "version": "5.0.0", | |
| 3586 | + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", | |
| 3587 | + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", | |
| 3588 | + "dev": true, | |
| 3589 | + "license": "MIT", | |
| 3590 | + "dependencies": { | |
| 3591 | + "@types/unist": "^3.0.0" | |
| 3592 | + }, | |
| 3593 | + "funding": { | |
| 3594 | + "type": "opencollective", | |
| 3595 | + "url": "https://opencollective.com/unified" | |
| 3596 | + } | |
| 3597 | + }, | |
| 3598 | + "node_modules/unist-util-position-from-estree": { | |
| 3599 | + "version": "2.0.0", | |
| 3600 | + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", | |
| 3601 | + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", | |
| 3602 | + "dev": true, | |
| 3603 | + "license": "MIT", | |
| 3604 | + "dependencies": { | |
| 3605 | + "@types/unist": "^3.0.0" | |
| 3606 | + }, | |
| 3607 | + "funding": { | |
| 3608 | + "type": "opencollective", | |
| 3609 | + "url": "https://opencollective.com/unified" | |
| 3610 | + } | |
| 3611 | + }, | |
| 3612 | + "node_modules/unist-util-stringify-position": { | |
| 3613 | + "version": "4.0.0", | |
| 3614 | + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", | |
| 3615 | + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", | |
| 3616 | + "dev": true, | |
| 3617 | + "license": "MIT", | |
| 3618 | + "dependencies": { | |
| 3619 | + "@types/unist": "^3.0.0" | |
| 3620 | + }, | |
| 3621 | + "funding": { | |
| 3622 | + "type": "opencollective", | |
| 3623 | + "url": "https://opencollective.com/unified" | |
| 3624 | + } | |
| 3625 | + }, | |
| 3626 | + "node_modules/unist-util-visit": { | |
| 3627 | + "version": "5.1.0", | |
| 3628 | + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", | |
| 3629 | + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", | |
| 3630 | + "dev": true, | |
| 3631 | + "license": "MIT", | |
| 3632 | + "dependencies": { | |
| 3633 | + "@types/unist": "^3.0.0", | |
| 3634 | + "unist-util-is": "^6.0.0", | |
| 3635 | + "unist-util-visit-parents": "^6.0.0" | |
| 3636 | + }, | |
| 3637 | + "funding": { | |
| 3638 | + "type": "opencollective", | |
| 3639 | + "url": "https://opencollective.com/unified" | |
| 3640 | + } | |
| 3641 | + }, | |
| 3642 | + "node_modules/unist-util-visit-parents": { | |
| 3643 | + "version": "6.0.2", | |
| 3644 | + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", | |
| 3645 | + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", | |
| 3646 | + "dev": true, | |
| 3647 | + "license": "MIT", | |
| 3648 | + "dependencies": { | |
| 3649 | + "@types/unist": "^3.0.0", | |
| 3650 | + "unist-util-is": "^6.0.0" | |
| 3651 | + }, | |
| 3652 | + "funding": { | |
| 3653 | + "type": "opencollective", | |
| 3654 | + "url": "https://opencollective.com/unified" | |
| 3655 | + } | |
| 3656 | + }, | |
| 1743 | 3657 | "node_modules/update-browserslist-db": { |
| 1744 | 3658 | "version": "1.3.0", |
| 1745 | 3659 | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", |
@@ -1771,6 +3685,36 @@ | ||
| 1771 | 3685 | "browserslist": ">= 4.21.0" |
| 1772 | 3686 | } |
| 1773 | 3687 | }, |
| 3688 | + "node_modules/vfile": { | |
| 3689 | + "version": "6.0.3", | |
| 3690 | + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", | |
| 3691 | + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", | |
| 3692 | + "dev": true, | |
| 3693 | + "license": "MIT", | |
| 3694 | + "dependencies": { | |
| 3695 | + "@types/unist": "^3.0.0", | |
| 3696 | + "vfile-message": "^4.0.0" | |
| 3697 | + }, | |
| 3698 | + "funding": { | |
| 3699 | + "type": "opencollective", | |
| 3700 | + "url": "https://opencollective.com/unified" | |
| 3701 | + } | |
| 3702 | + }, | |
| 3703 | + "node_modules/vfile-message": { | |
| 3704 | + "version": "4.0.3", | |
| 3705 | + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", | |
| 3706 | + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", | |
| 3707 | + "dev": true, | |
| 3708 | + "license": "MIT", | |
| 3709 | + "dependencies": { | |
| 3710 | + "@types/unist": "^3.0.0", | |
| 3711 | + "unist-util-stringify-position": "^4.0.0" | |
| 3712 | + }, | |
| 3713 | + "funding": { | |
| 3714 | + "type": "opencollective", | |
| 3715 | + "url": "https://opencollective.com/unified" | |
| 3716 | + } | |
| 3717 | + }, | |
| 1774 | 3718 | "node_modules/vite": { |
| 1775 | 3719 | "version": "6.4.3", |
| 1776 | 3720 | "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", |
@@ -1852,6 +3796,17 @@ | ||
| 1852 | 3796 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", |
| 1853 | 3797 | "dev": true, |
| 1854 | 3798 | "license": "ISC" |
| 3799 | + }, | |
| 3800 | + "node_modules/zwitch": { | |
| 3801 | + "version": "2.0.4", | |
| 3802 | + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", | |
| 3803 | + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", | |
| 3804 | + "dev": true, | |
| 3805 | + "license": "MIT", | |
| 3806 | + "funding": { | |
| 3807 | + "type": "github", | |
| 3808 | + "url": "https://github.com/sponsors/wooorm" | |
| 3809 | + } | |
| 1855 | 3810 | } |
| 1856 | 3811 | } |
| 1857 | 3812 | } |
modified
hfmarketdata/web/package.json
+10 −4
@@ -1,21 +1,27 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "hfmarketdata-web", |
| 3 | 3 | "private": true, |
| 4 | − "version": "1.0.0", | |
| 5 | − "description": "HF Market Data — open high-frequency market data API platform", | |
| 4 | + "version": "2.0.0", | |
| 5 | + "description": "HF Market Data — open high-frequency market data API platform (site, docs, playground, dashboard)", | |
| 6 | 6 | "author": "Simon-Pierre Boucher <contact@spboucher.ai>", |
| 7 | 7 | "type": "module", |
| 8 | 8 | "scripts": { |
| 9 | 9 | "dev": "vite", |
| 10 | 10 | "build": "vite build", |
| 11 | − "preview": "vite preview" | |
| 11 | + "preview": "vite preview", | |
| 12 | + "test:e2e": "playwright test" | |
| 12 | 13 | }, |
| 13 | 14 | "dependencies": { |
| 14 | 15 | "react": "^18.3.1", |
| 15 | − "react-dom": "^18.3.1" | |
| 16 | + "react-dom": "^18.3.1", | |
| 17 | + "react-router-dom": "^6.28.0", | |
| 18 | + "lightweight-charts": "^4.2.0", | |
| 19 | + "prism-react-renderer": "^2.4.0" | |
| 16 | 20 | }, |
| 17 | 21 | "devDependencies": { |
| 18 | 22 | "@vitejs/plugin-react": "^4.3.4", |
| 23 | + "@mdx-js/rollup": "^3.1.0", | |
| 24 | + "@playwright/test": "^1.49.0", | |
| 19 | 25 | "vite": "^6.0.0" |
| 20 | 26 | } |
| 21 | 27 | } |
modified
hfmarketdata/web/src/App.jsx
+36 −481
@@ -1,486 +1,41 @@ | ||
| 1 | −import React, { useEffect, useMemo, useState } from 'react' | |
| 2 | −import { | |
| 3 | − ASSET_TYPES, BAR_FIELDS, BASE_URL, CAPABILITIES, ENDPOINTS, ERRORS, | |
| 4 | − OPTION_FIELDS, TIMEFRAMES, | |
| 5 | − buildUrl, curlSnippet, defaultValues, jsSnippet, paramOptions, pythonSnippet, | |
| 6 | −} from './spec.js' | |
| 7 | − | |
| 8 | −// ---------------------------------------------------------------- atoms | |
| 9 | − | |
| 10 | −function Kicker({ children }) { | |
| 11 | − return <div className="kicker">{children}</div> | |
| 12 | −} | |
| 13 | − | |
| 14 | −function MethodChip({ method }) { | |
| 15 | − return <span className="method">{method}</span> | |
| 16 | −} | |
| 17 | − | |
| 18 | −function CopyButton({ text, label = 'Copy' }) { | |
| 19 | − const [copied, setCopied] = useState(false) | |
| 20 | − const copy = () => { | |
| 21 | − navigator.clipboard.writeText(text).then(() => { | |
| 22 | − setCopied(true) | |
| 23 | − setTimeout(() => setCopied(false), 1200) | |
| 24 | − }) | |
| 25 | − } | |
| 26 | − return <button className="tab" onClick={copy}>{copied ? 'Copied ✓' : label}</button> | |
| 27 | −} | |
| 28 | − | |
| 29 | −// ---------------------------------------------------------------- playground | |
| 30 | − | |
| 31 | −function ParamInput({ p, values, onChange }) { | |
| 32 | − const id = `pg-${p.name}-${Math.random().toString(36).slice(2, 6)}` | |
| 33 | − const common = { id, value: values[p.name] ?? '' } | |
| 34 | − return ( | |
| 35 | − <label className="pg-field" htmlFor={id}> | |
| 36 | − <span className="pg-label"> | |
| 37 | − {p.name} | |
| 38 | − {p.req ? <em className="pg-req">required</em> : null} | |
| 39 | − <span className="pg-in">{p.in}</span> | |
| 40 | − </span> | |
| 41 | − {p.t === 'select' ? ( | |
| 42 | − <select {...common} onChange={e => onChange(p.name, e.target.value)}> | |
| 43 | − {paramOptions(p, values).map(o => ( | |
| 44 | − <option key={o} value={o}>{o === '' ? '(default)' : o}</option> | |
| 45 | − ))} | |
| 46 | − </select> | |
| 47 | − ) : ( | |
| 48 | − <input {...common} type="text" placeholder={p.ph || ''} | |
| 49 | − onChange={e => onChange(p.name, e.target.value)} /> | |
| 50 | − )} | |
| 51 | − </label> | |
| 52 | − ) | |
| 53 | −} | |
| 54 | − | |
| 55 | −function Playground({ ep }) { | |
| 56 | − const [values, setValues] = useState(() => defaultValues(ep)) | |
| 57 | − const [resp, setResp] = useState(null) // {status, ms, body, ok} | |
| 58 | − const [busy, setBusy] = useState(false) | |
| 59 | − const [lang, setLang] = useState('curl') | |
| 60 | − | |
| 61 | − const onChange = (name, v) => { | |
| 62 | − setValues(prev => { | |
| 63 | − const next = { ...prev, [name]: v } | |
| 64 | − // keep adjustment valid when the asset changes | |
| 65 | − if (name === 'asset') next.adjustment = '' | |
| 66 | − return next | |
| 67 | − }) | |
| 68 | − } | |
| 69 | − | |
| 70 | − const url = buildUrl(ep, values) | |
| 71 | − const snippets = { curl: curlSnippet(url), Python: pythonSnippet(url), JavaScript: jsSnippet(url) } | |
| 72 | − | |
| 73 | − const run = async () => { | |
| 74 | − setBusy(true) | |
| 75 | − const t0 = performance.now() | |
| 76 | − try { | |
| 77 | − const r = await fetch(url) | |
| 78 | − const ms = Math.round(performance.now() - t0) | |
| 79 | − const text = await r.text() | |
| 80 | − let body = text | |
| 81 | − try { body = JSON.stringify(JSON.parse(text), null, 2) } catch { /* csv */ } | |
| 82 | − if (body.length > 6000) body = body.slice(0, 6000) + '\n… (truncated)' | |
| 83 | − setResp({ status: r.status, ok: r.ok, ms, body }) | |
| 84 | − } catch (e) { | |
| 85 | − setResp({ status: '—', ok: false, ms: Math.round(performance.now() - t0), body: `Request failed: ${e.message}` }) | |
| 86 | − } finally { | |
| 87 | − setBusy(false) | |
| 88 | − } | |
| 89 | − } | |
| 90 | − | |
| 91 | − return ( | |
| 92 | − <div className="playground"> | |
| 93 | − <div className="pg-head"> | |
| 94 | − <span className="pg-title">Playground</span> | |
| 95 | − <span className="pg-hint">edit the parameters, the request updates live</span> | |
| 96 | − </div> | |
| 97 | − | |
| 98 | − {ep.params.length > 0 && ( | |
| 99 | − <div className="pg-grid"> | |
| 100 | − {ep.params.map(p => ( | |
| 101 | − <ParamInput key={p.name} p={p} values={values} onChange={onChange} /> | |
| 102 | − ))} | |
| 103 | − </div> | |
| 104 | − )} | |
| 105 | − | |
| 106 | − <div className="pg-url"> | |
| 107 | − <span className="pg-url-method">GET</span> | |
| 108 | − <code>{BASE_URL}{url}</code> | |
| 109 | − </div> | |
| 110 | − | |
| 111 | − <div className="code-tabs-bar"> | |
| 112 | − <button className="tab run" onClick={run} disabled={busy}> | |
| 113 | − {busy ? 'Running…' : '▶ Send request'} | |
| 114 | − </button> | |
| 115 | − {Object.keys(snippets).map(t => ( | |
| 116 | − <button key={t} className={t === lang ? 'tab active' : 'tab'} | |
| 117 | − onClick={() => setLang(t)}>{t}</button> | |
| 118 | − ))} | |
| 119 | − <span className="pg-spacer" /> | |
| 120 | − <CopyButton text={`${BASE_URL}${url}`} label="Copy URL" /> | |
| 121 | − <CopyButton text={snippets[lang]} label="Copy code" /> | |
| 122 | − </div> | |
| 123 | − <pre><code>{snippets[lang]}</code></pre> | |
| 124 | − | |
| 125 | − {resp ? ( | |
| 126 | − <div className="pg-response"> | |
| 127 | − <div className="pg-resp-bar"> | |
| 128 | − <span className={resp.ok ? 'pg-status ok' : 'pg-status err'}> | |
| 129 | − {resp.status} | |
| 130 | − </span> | |
| 131 | − <span className="pg-ms">{resp.ms} ms</span> | |
| 132 | − <span className="pg-resp-label">live response</span> | |
| 133 | − </div> | |
| 134 | − <pre className="response"><code>{resp.body}</code></pre> | |
| 135 | − </div> | |
| 136 | − ) : ( | |
| 137 | − <div className="pg-response"> | |
| 138 | − <div className="pg-resp-bar"> | |
| 139 | − <span className="pg-resp-label">example response</span> | |
| 140 | − </div> | |
| 141 | − <pre className="response"><code>{ep.response}</code></pre> | |
| 142 | − </div> | |
| 143 | − )} | |
| 144 | − </div> | |
| 145 | − ) | |
| 146 | −} | |
| 147 | − | |
| 148 | −// ---------------------------------------------------------------- endpoint card | |
| 149 | − | |
| 150 | −function FieldsTable({ kind }) { | |
| 151 | − const fields = kind === 'options' ? OPTION_FIELDS : BAR_FIELDS | |
| 152 | − const [open, setOpen] = useState(false) | |
| 153 | − return ( | |
| 154 | − <div className="fields"> | |
| 155 | − <button className="fields-toggle" onClick={() => setOpen(!open)}> | |
| 156 | − {open ? '−' : '+'} Response fields <span className="fields-n">{fields.length}</span> | |
| 157 | − </button> | |
| 158 | − {open && ( | |
| 159 | − <table className="params"> | |
| 160 | − <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead> | |
| 161 | − <tbody> | |
| 162 | − {fields.map(f => ( | |
| 163 | − <tr key={f.name}> | |
| 164 | − <td><code>{f.name}</code></td> | |
| 165 | − <td className="type">{f.type}</td> | |
| 166 | − <td>{f.desc}</td> | |
| 167 | − </tr> | |
| 168 | − ))} | |
| 169 | − </tbody> | |
| 170 | − </table> | |
| 171 | − )} | |
| 172 | − </div> | |
| 173 | − ) | |
| 174 | −} | |
| 175 | − | |
| 176 | −function Endpoint({ ep, index }) { | |
| 177 | − return ( | |
| 178 | − <article className="endpoint" id={ep.id}> | |
| 179 | − <div className="endpoint-index">{String(index + 1).padStart(2, '0')}</div> | |
| 180 | − <div className="endpoint-body"> | |
| 181 | − <div className="endpoint-head"> | |
| 182 | − <MethodChip method={ep.method} /> | |
| 183 | − <code className="path">{ep.path}</code> | |
| 184 | − </div> | |
| 185 | − <h3>{ep.title}</h3> | |
| 186 | − <p className="endpoint-desc">{ep.desc}</p> | |
| 187 | − {ep.params.length > 0 && ( | |
| 188 | − <table className="params"> | |
| 189 | − <thead> | |
| 190 | − <tr><th>Parameter</th><th>In</th><th></th><th>Description</th></tr> | |
| 191 | − </thead> | |
| 192 | − <tbody> | |
| 193 | − {ep.params.map(p => ( | |
| 194 | − <tr key={p.name}> | |
| 195 | − <td><code>{p.name}</code></td> | |
| 196 | − <td className="type">{p.in}</td> | |
| 197 | − <td>{p.req ? <span className="req">required</span> : <span className="opt">optional</span>}</td> | |
| 198 | − <td>{p.desc}</td> | |
| 199 | − </tr> | |
| 200 | − ))} | |
| 201 | − </tbody> | |
| 202 | − </table> | |
| 203 | − )} | |
| 204 | − <Playground ep={ep} /> | |
| 205 | − {ep.fields && <FieldsTable kind={ep.fields} />} | |
| 206 | − </div> | |
| 207 | − </article> | |
| 208 | − ) | |
| 209 | −} | |
| 210 | − | |
| 211 | −// ---------------------------------------------------------------- live status | |
| 212 | − | |
| 213 | −function LiveStatus() { | |
| 214 | − const [status, setStatus] = useState(null) | |
| 215 | − const [err, setErr] = useState(null) | |
| 216 | − useEffect(() => { | |
| 217 | − fetch('/v1/status') | |
| 218 | − .then(r => r.json()) | |
| 219 | − .then(d => setStatus(d.datasets)) | |
| 220 | − .catch(e => setErr(e.message)) | |
| 221 | − }, []) | |
| 222 | − if (err) return <p className="muted">Live status unavailable ({err}).</p> | |
| 223 | − if (!status) return <p className="muted">Loading live dataset status…</p> | |
| 224 | − const rows = [] | |
| 225 | − for (const [asset, tfs] of Object.entries(status)) { | |
| 226 | − if (asset === 'options') continue | |
| 227 | − for (const [tf, adjs] of Object.entries(tfs)) { | |
| 228 | − for (const [adj, n] of Object.entries(adjs)) { | |
| 229 | − rows.push({ asset, tf, adj, n }) | |
| 230 | − } | |
| 231 | − } | |
| 232 | − } | |
| 233 | − const total = rows.reduce((s, r) => s + r.n, 0) | |
| 234 | − return ( | |
| 235 | − <div> | |
| 236 | − <div className="stat-chips"> | |
| 237 | − <div className="chip"><b>{total.toLocaleString('en-US')}</b><span>instrument files live</span></div> | |
| 238 | − {status.options && ( | |
| 239 | − <div className="chip"><b>{status.options.quarters.length}</b><span>options quarters ({status.options.quarters[0]} → {status.options.quarters.at(-1)})</span></div> | |
| 240 | − )} | |
| 241 | − </div> | |
| 242 | − <div className="table-scroll"> | |
| 243 | − <table className="params"> | |
| 244 | − <thead> | |
| 245 | − <tr><th>Asset</th><th>Timeframe</th><th>Adjustment</th><th>Instruments</th></tr> | |
| 246 | − </thead> | |
| 247 | − <tbody> | |
| 248 | − {rows.map((r, i) => ( | |
| 249 | − <tr key={i}> | |
| 250 | − <td>{r.asset}</td><td>{r.tf}</td><td><code>{r.adj}</code></td> | |
| 251 | − <td className="num">{r.n.toLocaleString('en-US')}</td> | |
| 252 | − </tr> | |
| 253 | − ))} | |
| 254 | − </tbody> | |
| 255 | − </table> | |
| 256 | − </div> | |
| 257 | − </div> | |
| 258 | − ) | |
| 259 | −} | |
| 260 | − | |
| 261 | −// ---------------------------------------------------------------- app | |
| 1 | +// Route table — one lazy chunk per area. Owners: web-core (home, pricing, docs, status, layout), | |
| 2 | +// web-app (playground, auth, dashboard, admin), mcp-skills (integrations). | |
| 3 | +import React, { Suspense, lazy } from 'react' | |
| 4 | +import { Route, Routes } from 'react-router-dom' | |
| 5 | +import Layout from './app/Layout.jsx' | |
| 6 | + | |
| 7 | +const Home = lazy(() => import('./pages/home/Home.jsx')) | |
| 8 | +const Docs = lazy(() => import('./pages/docs/Docs.jsx')) | |
| 9 | +const Playground = lazy(() => import('./pages/playground/Playground.jsx')) | |
| 10 | +const Integrations = lazy(() => import('./pages/integrations/Integrations.jsx')) | |
| 11 | +const Pricing = lazy(() => import('./pages/pricing/Pricing.jsx')) | |
| 12 | +const Status = lazy(() => import('./pages/status/Status.jsx')) | |
| 13 | +const Auth = lazy(() => import('./pages/auth/Auth.jsx')) | |
| 14 | +const Dashboard = lazy(() => import('./pages/dashboard/Dashboard.jsx')) | |
| 15 | +const Admin = lazy(() => import('./pages/admin/Admin.jsx')) | |
| 16 | +const NotFound = () => <main className="page narrow"><h1>404</h1><p className="muted">This page does not exist.</p></main> | |
| 262 | 17 | |
| 263 | 18 | export default function App() { |
| 264 | − const grouped = useMemo(() => { | |
| 265 | − const g = {} | |
| 266 | − for (const e of ENDPOINTS) (g[e.tag] ||= []).push(e) | |
| 267 | − return g | |
| 268 | − }, []) | |
| 269 | − | |
| 270 | 19 | return ( |
| 271 | − <> | |
| 272 | − <div className="layout"> | |
| 273 | − <aside className="sidebar"> | |
| 274 | − <a className="brand" href="#top"> | |
| 275 | − <span className="logo">HF</span> | |
| 276 | − <span className="brand-text"> | |
| 277 | − <span className="brand-name">HF Market Data</span> | |
| 278 | − <span className="brand-sub">Open API · v1</span> | |
| 279 | − </span> | |
| 280 | − </a> | |
| 281 | − <nav> | |
| 282 | − <Kicker>Getting started</Kicker> | |
| 283 | − <a href="#overview">Overview</a> | |
| 284 | − <a href="#capabilities">What you can do</a> | |
| 285 | − <a href="#quickstart">Quick start</a> | |
| 286 | − <a href="#conventions">Conventions</a> | |
| 287 | − <a href="#coverage">Data coverage</a> | |
| 288 | − <Kicker>Endpoints — live playground</Kicker> | |
| 289 | − {ENDPOINTS.map(e => ( | |
| 290 | − <a key={e.id} href={`#${e.id}`} className="ep-link"> | |
| 291 | − <span className="ep-method">{e.method}</span>{e.title} | |
| 292 | − </a> | |
| 293 | − ))} | |
| 294 | − <Kicker>Resources</Kicker> | |
| 295 | − <a href="#errors">Errors</a> | |
| 296 | − <a href="#live">Live status</a> | |
| 297 | − <a href="#about">About</a> | |
| 298 | − <a href="/docs" target="_blank" rel="noreferrer">OpenAPI / Swagger ↗</a> | |
| 299 | − </nav> | |
| 300 | − </aside> | |
| 301 | − | |
| 302 | − <main id="top"> | |
| 303 | − <section className="hero" id="overview"> | |
| 304 | − <Kicker>Open high-frequency market data</Kicker> | |
| 305 | − <h1> | |
| 306 | − Market data,<br /> | |
| 307 | − <em className="accent-word">wide open.</em> | |
| 308 | − </h1> | |
| 309 | − <p className="lede"> | |
| 310 | − A free, keyless REST API over a <strong>26.5-billion-row</strong> historical | |
| 311 | − archive: <strong>1-minute to daily bars</strong> for stocks, ETFs, futures, | |
| 312 | − crypto, indices and FX — and <strong>complete end-of-day options | |
| 313 | − chains</strong> with quotes, implied volatility and Greeks back to 2010. | |
| 314 | − Every endpoint below is a live playground. | |
| 315 | − </p> | |
| 316 | − <div className="hero-actions"> | |
| 317 | − <a className="btn" href="#bars">Try the playground</a> | |
| 318 | − <a className="btn ghost" href="/docs" target="_blank" rel="noreferrer">OpenAPI spec ↗</a> | |
| 319 | − </div> | |
| 320 | − <div className="stat-chips hero-chips"> | |
| 321 | − <div className="chip"><b>26.5B</b><span>rows in the lake</span></div> | |
| 322 | − <div className="chip"><b>7</b><span>asset classes</span></div> | |
| 323 | − <div className="chip"><b>5</b><span>timeframes — 1min → 1day</span></div> | |
| 324 | − <div className="chip"><b>5,800+</b><span>options underlyings</span></div> | |
| 325 | − <div className="chip"><b>66</b><span>options quarters since 2010</span></div> | |
| 326 | − </div> | |
| 327 | − </section> | |
| 328 | − | |
| 329 | − <section id="capabilities"> | |
| 330 | − <Kicker>01 — Getting started</Kicker> | |
| 331 | − <h2>What you can do</h2> | |
| 332 | − <div className="conv-grid"> | |
| 333 | − {CAPABILITIES.map(c => ( | |
| 334 | − <div className="conv" key={c.title}> | |
| 335 | − <h4>{c.title}</h4> | |
| 336 | − <p>{c.desc}</p> | |
| 337 | − </div> | |
| 338 | − ))} | |
| 339 | − </div> | |
| 340 | − </section> | |
| 341 | − | |
| 342 | − <section id="quickstart"> | |
| 343 | − <Kicker>02 — Getting started</Kicker> | |
| 344 | − <h2>Quick start</h2> | |
| 345 | − <p> | |
| 346 | − No key, no registration, no SDK required. Every endpoint is a plain | |
| 347 | − GET on <code>{BASE_URL}</code> returning JSON — or CSV when you ask for it. | |
| 348 | − </p> | |
| 349 | − <pre><code>{`# Daily AAPL bars, split+dividend adjusted | |
| 350 | −curl "${BASE_URL}/v1/bars/stock/AAPL?timeframe=1day&start=2024-01-01" | |
| 351 | − | |
| 352 | −# A precise 4-hour window of 1-minute bars, across three tickers at once | |
| 353 | −curl "${BASE_URL}/v1/bars/stock?tickers=AAPL,MSFT,NVDA&timeframe=1min\\ | |
| 354 | − &start=2024-06-03%2009:30:00&end=2024-06-03%2013:30:00" | |
| 355 | − | |
| 356 | −# The whole watchlist at one exact moment | |
| 357 | −curl "${BASE_URL}/v1/snapshot/stock?tickers=AAPL,MSFT,NVDA&at=2024-06-03%2010:35:00" | |
| 358 | − | |
| 359 | −# Full AAPL option chain — quotes, IV, Greeks | |
| 360 | −curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre> | |
| 361 | − <div className="callout"> | |
| 362 | − <b>Response envelope.</b> Every JSON data endpoint returns | |
| 363 | − <code>{'{ "count": n, "data": [ … ] }'}</code>. CSV responses stream a | |
| 364 | − header row followed by the records — ideal for <code>pandas.read_csv</code> straight from the URL. | |
| 365 | − </div> | |
| 366 | − </section> | |
| 367 | − | |
| 368 | − <section id="conventions"> | |
| 369 | − <Kicker>03 — Getting started</Kicker> | |
| 370 | − <h2>Conventions</h2> | |
| 371 | − <div className="conv-grid"> | |
| 372 | − <div className="conv"> | |
| 373 | − <h4>Timeframes</h4> | |
| 374 | − <p>{TIMEFRAMES.join(' · ')}. Intraday timestamps are <b>US Eastern Time</b>; daily bars carry the session date. Zero-volume bars are excluded.</p> | |
| 375 | − </div> | |
| 376 | − <div className="conv"> | |
| 377 | − <h4>Adjustments</h4> | |
| 378 | − <p>Stocks & ETFs ship in three variants — <code>adj_split</code>, <code>adj_splitdiv</code> (default), <code>UNADJUSTED</code> (1min & 1day only). Futures continuous series come unadjusted, ratio-adjusted or absolute-adjusted for roll dates.</p> | |
| 379 | − </div> | |
| 380 | − <div className="conv"> | |
| 381 | − <h4>Formats & limits</h4> | |
| 382 | − <p><code>format=json</code> (default, 50k-row cap) or <code>format=csv</code> (2M-row cap) on every data endpoint. Filter with <code>start</code>/<code>end</code>, page with <code>limit</code> + <code>order</code>.</p> | |
| 383 | − </div> | |
| 384 | − <div className="conv"> | |
| 385 | − <h4>Options snapshots</h4> | |
| 386 | − <p>End-of-day chains sampled <b>30 seconds before the close</b> to avoid rebalancing-order noise — with bid/ask IV and the full Greek set on every row.</p> | |
| 387 | − </div> | |
| 388 | − </div> | |
| 389 | − </section> | |
| 390 | − | |
| 391 | − <section id="coverage"> | |
| 392 | − <Kicker>04 — Getting started</Kicker> | |
| 393 | − <h2>Data coverage</h2> | |
| 394 | − <div className="cards"> | |
| 395 | − {ASSET_TYPES.map(a => ( | |
| 396 | − <div className="card" key={a.id}> | |
| 397 | − <div className="card-head"> | |
| 398 | − <h3>{a.name}</h3> | |
| 399 | − <code className="card-ex">{a.example}</code> | |
| 400 | − </div> | |
| 401 | − <p>{a.desc}</p> | |
| 402 | − <p className="muted small">{a.adjustments.map(x => <code key={x}>{x}</code>)}</p> | |
| 403 | − </div> | |
| 404 | − ))} | |
| 405 | − <div className="card accent"> | |
| 406 | − <div className="card-head"> | |
| 407 | − <h3>Options</h3> | |
| 408 | − <code className="card-ex">AAPL 200c</code> | |
| 409 | − </div> | |
| 410 | − <p>End-of-day chains for 5,800+ US equities & indices since 2010 — | |
| 411 | − last price, bid/ask, bid/ask IV, open interest, volume, | |
| 412 | − delta, gamma, vega, theta, rho.</p> | |
| 413 | − </div> | |
| 414 | − </div> | |
| 415 | − </section> | |
| 416 | − | |
| 417 | − <section id="reference"> | |
| 418 | − {Object.entries(grouped).map(([tag, eps]) => ( | |
| 419 | − <div key={tag} className="ep-group"> | |
| 420 | − <Kicker>API reference — live playground</Kicker> | |
| 421 | − <h2>{tag}</h2> | |
| 422 | − {eps.map(e => ( | |
| 423 | − <Endpoint key={e.id} ep={e} | |
| 424 | − index={ENDPOINTS.findIndex(x => x.id === e.id)} /> | |
| 425 | − ))} | |
| 426 | − </div> | |
| 427 | − ))} | |
| 428 | − </section> | |
| 429 | − | |
| 430 | − <section id="errors"> | |
| 431 | − <Kicker>Resources</Kicker> | |
| 432 | − <h2>Errors</h2> | |
| 433 | − <p> | |
| 434 | − Errors are plain HTTP status codes with a JSON body: | |
| 435 | − <code>{'{ "detail": "…" }'}</code>. The detail message always names the | |
| 436 | − invalid parameter and lists the valid values. | |
| 437 | − </p> | |
| 438 | − <table className="params"> | |
| 439 | − <thead><tr><th>Code</th><th>Meaning</th><th>When</th></tr></thead> | |
| 440 | − <tbody> | |
| 441 | − {ERRORS.map(e => ( | |
| 442 | − <tr key={e.code}> | |
| 443 | − <td><code>{e.code}</code></td> | |
| 444 | − <td>{e.meaning}</td> | |
| 445 | − <td>{e.desc}</td> | |
| 446 | − </tr> | |
| 447 | − ))} | |
| 448 | − </tbody> | |
| 449 | − </table> | |
| 450 | − </section> | |
| 451 | − | |
| 452 | − <section id="live"> | |
| 453 | − <Kicker>Resources</Kicker> | |
| 454 | − <h2>Live dataset status</h2> | |
| 455 | − <LiveStatus /> | |
| 456 | − </section> | |
| 457 | − | |
| 458 | − <section id="about" className="about"> | |
| 459 | − <Kicker>Resources</Kicker> | |
| 460 | − <h2>About</h2> | |
| 461 | − <p> | |
| 462 | − HF Market Data is built and maintained by <strong>Simon-Pierre | |
| 463 | − Boucher</strong>. Under the hood it is a DuckDB-over-Parquet data | |
| 464 | − lake: every instrument is a zstd-compressed Parquet file queried | |
| 465 | − in place with predicate pushdown — no database server between you | |
| 466 | − and the data, which is what keeps responses fast across 26.5 | |
| 467 | − billion rows. | |
| 468 | − </p> | |
| 469 | − <p> | |
| 470 | − Contact — <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a> | |
| 471 | − </p> | |
| 472 | − </section> | |
| 473 | − | |
| 474 | − <footer> | |
| 475 | − <div className="foot-brand">HF Market Data</div> | |
| 476 | − <div> | |
| 477 | − © {new Date().getFullYear()} Simon-Pierre Boucher ·{' '} | |
| 478 | − <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a> ·{' '} | |
| 479 | − <a href="/docs" target="_blank" rel="noreferrer">OpenAPI</a> | |
| 480 | − </div> | |
| 481 | − </footer> | |
| 482 | − </main> | |
| 483 | − </div> | |
| 484 | − </> | |
| 20 | + <Layout> | |
| 21 | + <Suspense fallback={<div className="page-loading" aria-busy="true" />}> | |
| 22 | + <Routes> | |
| 23 | + <Route path="/" element={<Home />} /> | |
| 24 | + <Route path="/docs/*" element={<Docs />} /> | |
| 25 | + <Route path="/playground" element={<Playground />} /> | |
| 26 | + <Route path="/integrations/*" element={<Integrations />} /> | |
| 27 | + <Route path="/pricing" element={<Pricing />} /> | |
| 28 | + <Route path="/status" element={<Status />} /> | |
| 29 | + <Route path="/signin" element={<Auth mode="signin" />} /> | |
| 30 | + <Route path="/signup" element={<Auth mode="signup" />} /> | |
| 31 | + <Route path="/verify" element={<Auth mode="verify" />} /> | |
| 32 | + <Route path="/reset" element={<Auth mode="reset" />} /> | |
| 33 | + <Route path="/invite" element={<Auth mode="invite" />} /> | |
| 34 | + <Route path="/dashboard/*" element={<Dashboard />} /> | |
| 35 | + <Route path="/admin/*" element={<Admin />} /> | |
| 36 | + <Route path="*" element={<NotFound />} /> | |
| 37 | + </Routes> | |
| 38 | + </Suspense> | |
| 39 | + </Layout> | |
| 485 | 40 | ) |
| 486 | 41 | } |
added
hfmarketdata/web/src/app/Layout.jsx
+36 −0
@@ -0,0 +1,36 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +import { Link, NavLink } from 'react-router-dom' | |
| 3 | +import { useAuth } from './auth.jsx' | |
| 4 | +import { CONTACT_EMAIL } from './api.js' | |
| 5 | + | |
| 6 | +const NAV = [ | |
| 7 | + ['/docs', 'Docs'], ['/playground', 'Playground'], ['/integrations', 'Integrations'], | |
| 8 | + ['/pricing', 'Pricing / Limits'], ['/status', 'Status'], | |
| 9 | +] | |
| 10 | + | |
| 11 | +export default function Layout({ children }) { | |
| 12 | + const { user } = useAuth() | |
| 13 | + return ( | |
| 14 | + <div className="shell"> | |
| 15 | + <header className="topbar"> | |
| 16 | + <Link to="/" className="brand" aria-label="HF Market Data home"> | |
| 17 | + <span className="brand-mark">HF</span><span className="brand-name">Market Data</span> | |
| 18 | + </Link> | |
| 19 | + <nav className="topnav" aria-label="Main"> | |
| 20 | + {NAV.map(([to, label]) => ( | |
| 21 | + <NavLink key={to} to={to} className={({ isActive }) => (isActive ? 'active' : '')}>{label}</NavLink> | |
| 22 | + ))} | |
| 23 | + </nav> | |
| 24 | + <div className="topbar-right"> | |
| 25 | + {user ? <Link to="/dashboard" className="btn btn-ghost">Dashboard</Link> | |
| 26 | + : <Link to="/signin" className="btn btn-ghost">Sign in</Link>} | |
| 27 | + </div> | |
| 28 | + </header> | |
| 29 | + {children} | |
| 30 | + <footer className="footer"> | |
| 31 | + <div>© {new Date().getFullYear()} HF Market Data · Simon-Pierre Boucher · <a href={`mailto:${CONTACT_EMAIL}`}>{CONTACT_EMAIL}</a></div> | |
| 32 | + <div className="muted">Data: FirstRate Data · SEC EDGAR · UTC / ISO 8601 everywhere</div> | |
| 33 | + </footer> | |
| 34 | + </div> | |
| 35 | + ) | |
| 36 | +} | |
added
hfmarketdata/web/src/app/api.js
+53 −0
@@ -0,0 +1,53 @@ | ||
| 1 | +// Tiny API client shared by every page. Injects the session key (dashboard playground) when present, | |
| 2 | +// exposes rate-limit headers on every result, never logs keys. | |
| 3 | +export const BASE_URL = import.meta.env.VITE_API_BASE || '' | |
| 4 | +export const PUBLIC_BASE = 'https://www.hfmarketdata.io' | |
| 5 | +export const CONTACT_EMAIL = 'contact@spboucher.ai' | |
| 6 | + | |
| 7 | +export const TIERS = [ | |
| 8 | + { id: 'keyless', name: 'Keyless (per IP)', window: 'hour', requests: 30, rows: 100_000, maxRows: 5_000 }, | |
| 9 | + { id: 'free', name: 'Free account + API key', window: 'minute', requests: 120, rows: 1_000_000, maxRows: 50_000 }, | |
| 10 | + { id: 'high_usage', name: 'High usage (on request)', window: 'minute', requests: 600, rows: 10_000_000, maxRows: 200_000 }, | |
| 11 | +] | |
| 12 | + | |
| 13 | +export function rateHeaders(res) { | |
| 14 | + const h = res.headers | |
| 15 | + const num = k => (h.get(k) == null ? null : Number(h.get(k))) | |
| 16 | + return { | |
| 17 | + limitRequests: num('x-ratelimit-limit-requests'), | |
| 18 | + remainingRequests: num('x-ratelimit-remaining-requests'), | |
| 19 | + limitRows: num('x-ratelimit-limit-rows'), | |
| 20 | + remainingRows: num('x-ratelimit-remaining-rows'), | |
| 21 | + reset: num('x-ratelimit-reset'), | |
| 22 | + rowCount: num('x-row-count'), | |
| 23 | + retryAfter: num('retry-after'), | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | |
| 27 | +export async function api(path, { method = 'GET', body, apiKey, headers = {}, raw = false, signal } = {}) { | |
| 28 | + const h = { Accept: 'application/json', ...headers } | |
| 29 | + if (body !== undefined) h['Content-Type'] = 'application/json' | |
| 30 | + if (apiKey) h.Authorization = `Bearer ${apiKey}` | |
| 31 | + const t0 = performance.now() | |
| 32 | + const res = await fetch(BASE_URL + path, { method, headers: h, body: body === undefined ? undefined : JSON.stringify(body), credentials: 'include', signal }) | |
| 33 | + const ms = Math.round(performance.now() - t0) | |
| 34 | + const rate = rateHeaders(res) | |
| 35 | + if (raw) return { res, ms, rate } | |
| 36 | + const ct = res.headers.get('content-type') || '' | |
| 37 | + const data = ct.includes('json') ? await res.json() : await res.text() | |
| 38 | + if (!res.ok) { | |
| 39 | + const err = new Error(data?.error?.message || data?.detail || `HTTP ${res.status}`) | |
| 40 | + err.status = res.status | |
| 41 | + err.code = data?.error?.code | |
| 42 | + err.body = data | |
| 43 | + err.rate = rate | |
| 44 | + throw err | |
| 45 | + } | |
| 46 | + return { data, ms, rate, status: res.status } | |
| 47 | +} | |
| 48 | + | |
| 49 | +export function buildUrl(path, params = {}) { | |
| 50 | + const u = new URL(path, PUBLIC_BASE) | |
| 51 | + Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') u.searchParams.set(k, v) }) | |
| 52 | + return u.toString() | |
| 53 | +} | |
added
hfmarketdata/web/src/app/auth.jsx
+29 −0
@@ -0,0 +1,29 @@ | ||
| 1 | +// Session context: who is signed in, their tier, and the API key to inject in the authenticated playground. | |
| 2 | +// Backed by cookie session endpoints under /v1/auth and /v1/me (owned by the accounts module). | |
| 3 | +import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' | |
| 4 | +import { api } from './api.js' | |
| 5 | + | |
| 6 | +const Ctx = createContext({ user: null, loading: true, refresh: () => {}, signout: () => {} }) | |
| 7 | + | |
| 8 | +export function AuthProvider({ children }) { | |
| 9 | + const [user, setUser] = useState(null) | |
| 10 | + const [loading, setLoading] = useState(true) | |
| 11 | + const refresh = useCallback(async () => { | |
| 12 | + try { | |
| 13 | + const { data } = await api('/v1/me') | |
| 14 | + setUser(data.data || data) | |
| 15 | + } catch { | |
| 16 | + setUser(null) | |
| 17 | + } finally { | |
| 18 | + setLoading(false) | |
| 19 | + } | |
| 20 | + }, []) | |
| 21 | + const signout = useCallback(async () => { | |
| 22 | + try { await api('/v1/auth/logout', { method: 'POST' }) } catch { /* ignore */ } | |
| 23 | + setUser(null) | |
| 24 | + }, []) | |
| 25 | + useEffect(() => { refresh() }, [refresh]) | |
| 26 | + return <Ctx.Provider value={{ user, loading, refresh, signout }}>{children}</Ctx.Provider> | |
| 27 | +} | |
| 28 | + | |
| 29 | +export const useAuth = () => useContext(Ctx) | |
added
hfmarketdata/web/src/app/theme.css
+41 −0
@@ -0,0 +1,41 @@ | ||
| 1 | +/* HF Market Data design tokens — dark by default, one accent, mono for code. Owner: web-core. */ | |
| 2 | +:root { | |
| 3 | + color-scheme: dark; | |
| 4 | + --bg: #0b0d10; --bg-1: #12151a; --bg-2: #181c23; --line: #262b34; --line-2: #333a46; | |
| 5 | + --fg: #e6e9ef; --fg-1: #aeb6c2; --fg-2: #7c8594; | |
| 6 | + --accent: #5ee7a5; --accent-2: #37b3ff; --danger: #ff6b6b; --warn: #ffc857; | |
| 7 | + --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | |
| 8 | + --sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| 9 | + --radius: 8px; --max: 1240px; | |
| 10 | +} | |
| 11 | +[data-theme="light"] { color-scheme: light; --bg: #ffffff; --bg-1: #f6f7f9; --bg-2: #eef0f4; --line: #dfe3ea; --line-2: #c9cfd9; --fg: #14181f; --fg-1: #3d4654; --fg-2: #6b7482; --accent: #0f8f5a; --accent-2: #0b6fb8; } | |
| 12 | +* { box-sizing: border-box; } | |
| 13 | +html { font-family: var(--sans); background: var(--bg); color: var(--fg); -webkit-font-smoothing: antialiased; font-size: 15px; } | |
| 14 | +body { margin: 0; } | |
| 15 | +a { color: var(--accent-2); text-decoration: none; } | |
| 16 | +a:hover { text-decoration: underline; } | |
| 17 | +code, pre, kbd, .mono { font-family: var(--mono); font-size: 0.92em; } | |
| 18 | +pre { background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; overflow: auto; } | |
| 19 | +.muted { color: var(--fg-2); } | |
| 20 | +.shell { min-height: 100dvh; display: flex; flex-direction: column; } | |
| 21 | +.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; gap: 24px; padding: 0 20px; height: 56px; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); } | |
| 22 | +.brand { display: inline-flex; align-items: center; gap: 8px; color: var(--fg); font-weight: 600; } | |
| 23 | +.brand-mark { font-family: var(--mono); background: var(--accent); color: #05140c; border-radius: 6px; padding: 2px 6px; font-size: 13px; } | |
| 24 | +.topnav { display: flex; gap: 4px; flex: 1; } | |
| 25 | +.topnav a { color: var(--fg-1); padding: 6px 10px; border-radius: 6px; } | |
| 26 | +.topnav a.active, .topnav a:hover { color: var(--fg); background: var(--bg-2); text-decoration: none; } | |
| 27 | +.btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: var(--radius); border: 1px solid var(--line-2); background: var(--bg-1); color: var(--fg); cursor: pointer; font: inherit; } | |
| 28 | +.btn:hover { border-color: var(--fg-2); text-decoration: none; } | |
| 29 | +.btn-primary { background: var(--accent); color: #05140c; border-color: var(--accent); font-weight: 600; } | |
| 30 | +.btn-ghost { background: transparent; } | |
| 31 | +.page { width: 100%; max-width: var(--max); margin: 0 auto; padding: 40px 20px 80px; flex: 1; } | |
| 32 | +.page.narrow { max-width: 760px; } | |
| 33 | +.page-loading { flex: 1; min-height: 40vh; } | |
| 34 | +.footer { border-top: 1px solid var(--line); padding: 24px 20px; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; color: var(--fg-1); font-size: 13px; } | |
| 35 | +.card { background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px; } | |
| 36 | +table { border-collapse: collapse; width: 100%; } | |
| 37 | +th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; } | |
| 38 | +th { color: var(--fg-1); font-weight: 600; font-size: 13px; } | |
| 39 | +input, select, textarea { font: inherit; color: var(--fg); background: var(--bg-1); border: 1px solid var(--line-2); border-radius: 6px; padding: 8px 10px; } | |
| 40 | +input:focus, select:focus, textarea:focus { outline: 2px solid color-mix(in srgb, var(--accent) 50%, transparent); border-color: var(--accent); } | |
| 41 | +@media (max-width: 800px) { .topnav { display: none; } .topbar { gap: 12px; } } | |
renamed
hfmarketdata/web/src/spec.js → hfmarketdata/web/src/legacy-spec.js
+0 −0
modified
hfmarketdata/web/src/main.jsx
+10 −4
@@ -1,10 +1,16 @@ | ||
| 1 | 1 | import React from 'react' |
| 2 | −import ReactDOM from 'react-dom/client' | |
| 2 | +import { createRoot } from 'react-dom/client' | |
| 3 | +import { BrowserRouter } from 'react-router-dom' | |
| 3 | 4 | import App from './App.jsx' |
| 4 | −import './styles.css' | |
| 5 | +import { AuthProvider } from './app/auth.jsx' | |
| 6 | +import './app/theme.css' | |
| 5 | 7 | |
| 6 | −ReactDOM.createRoot(document.getElementById('root')).render( | |
| 8 | +createRoot(document.getElementById('root')).render( | |
| 7 | 9 | <React.StrictMode> |
| 8 | − <App /> | |
| 10 | + <BrowserRouter> | |
| 11 | + <AuthProvider> | |
| 12 | + <App /> | |
| 13 | + </AuthProvider> | |
| 14 | + </BrowserRouter> | |
| 9 | 15 | </React.StrictMode>, |
| 10 | 16 | ) |
added
hfmarketdata/web/src/pages/admin/Admin.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Admin() { | |
| 4 | + return <main className="page"><h1>Admin</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/auth/Auth.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by web-app. Props: mode = signin | signup | verify | reset | invite | |
| 3 | +export default function Auth({ mode }) { | |
| 4 | + return <main className="page narrow"><h1>{mode}</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/dashboard/Dashboard.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Dashboard() { | |
| 4 | + return <main className="page"><h1>Dashboard</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/docs/Docs.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Docs() { | |
| 4 | + return <main className="page"><h1>Docs</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/home/Home.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Home() { | |
| 4 | + return <main className="page"><h1>Home</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/integrations/Integrations.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Integrations() { | |
| 4 | + return <main className="page"><h1>Integrations</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/playground/Playground.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Playground() { | |
| 4 | + return <main className="page"><h1>Playground</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/pricing/Pricing.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Pricing() { | |
| 4 | + return <main className="page"><h1>Pricing</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
added
hfmarketdata/web/src/pages/status/Status.jsx
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +import React from 'react' | |
| 2 | +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name. | |
| 3 | +export default function Status() { | |
| 4 | + return <main className="page"><h1>Status</h1><p className="muted">Coming soon.</p></main> | |
| 5 | +} | |
modified
hfmarketdata/web/vite.config.js
+17 −2
@@ -1,12 +1,27 @@ | ||
| 1 | 1 | import { defineConfig } from 'vite' |
| 2 | 2 | import react from '@vitejs/plugin-react' |
| 3 | +import mdx from '@mdx-js/rollup' | |
| 3 | 4 | |
| 5 | +// Dev proxy → local API (uvicorn on :8090). In production the API serves dist/ itself (SPA fallback in main.py). | |
| 4 | 6 | export default defineConfig({ |
| 5 | − plugins: [react()], | |
| 7 | + plugins: [{ enforce: 'pre', ...mdx({ providerImportSource: '@mdx-js/react' }) }, react()], | |
| 8 | + build: { | |
| 9 | + target: 'es2020', | |
| 10 | + rollupOptions: { | |
| 11 | + output: { | |
| 12 | + manualChunks: { | |
| 13 | + vendor: ['react', 'react-dom', 'react-router-dom'], | |
| 14 | + charts: ['lightweight-charts'], | |
| 15 | + prism: ['prism-react-renderer'], | |
| 16 | + }, | |
| 17 | + }, | |
| 18 | + }, | |
| 19 | + }, | |
| 6 | 20 | server: { |
| 7 | 21 | proxy: { |
| 8 | − '/v1': 'http://localhost:8090', | |
| 22 | + '/v1': { target: 'http://localhost:8090', ws: true }, | |
| 9 | 23 | '/health': 'http://localhost:8090', |
| 24 | + '/openapi.json': 'http://localhost:8090', | |
| 10 | 25 | }, |
| 11 | 26 | }, |
| 12 | 27 | }) |
added
pytest.ini
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +[pytest] | |
| 2 | +testpaths = tests | |
| 3 | +pythonpath = hfmarketdata/api | |
| 4 | +asyncio_mode = auto | |
| 5 | +filterwarnings = | |
| 6 | + ignore::DeprecationWarning | |
| 7 | +addopts = -q | |
added
requirements-dev.txt
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +-r hfmarketdata/requirements.txt | |
| 2 | +pytest>=8 | |
| 3 | +pytest-asyncio>=0.23 | |
| 4 | +fakeredis>=2.23 | |
| 5 | +freezegun>=1.5 | |
| 6 | +respx>=0.21 | |
| 7 | +ruff>=0.6 | |
added
tests/__init__.py
+0 −0
added
tests/conftest.py
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +"""Shared pytest fixtures. | |
| 2 | + | |
| 3 | +The tests never touch the real 350 GB lake: `tests/fixtures/make_fixtures.py` builds a tiny synthetic | |
| 4 | +Parquet lake (same layout as frd_downloader.py) in a temp directory, and the API is imported with | |
| 5 | +HFMD_DATA_ROOT pointing at it. Redis is replaced by fakeredis; the SQLite state DB lives in the temp dir. | |
| 6 | +""" | |
| 7 | +from __future__ import annotations | |
| 8 | + | |
| 9 | +import importlib | |
| 10 | +import os | |
| 11 | +import sys | |
| 12 | +from pathlib import Path | |
| 13 | + | |
| 14 | +import pytest | |
| 15 | + | |
| 16 | +ROOT = Path(__file__).resolve().parents[1] | |
| 17 | +API_DIR = ROOT / "hfmarketdata" / "api" | |
| 18 | +if str(API_DIR) not in sys.path: | |
| 19 | + sys.path.insert(0, str(API_DIR)) | |
| 20 | + | |
| 21 | + | |
| 22 | +@pytest.fixture(scope="session") | |
| 23 | +def lake(tmp_path_factory) -> Path: | |
| 24 | + from tests.fixtures.make_fixtures import build_lake | |
| 25 | + root = tmp_path_factory.mktemp("lake") | |
| 26 | + build_lake(root) | |
| 27 | + return root | |
| 28 | + | |
| 29 | + | |
| 30 | +@pytest.fixture(scope="session") | |
| 31 | +def app(lake): | |
| 32 | + os.environ["HFMD_DATA_ROOT"] = str(lake) | |
| 33 | + os.environ["HFMD_STATE_DB"] = str(lake / "state" / "hfmd.db") | |
| 34 | + os.environ["HFMD_WEB_DIST"] = str(lake / "no-web") | |
| 35 | + os.environ["HFMD_ENV"] = "test" | |
| 36 | + os.environ["HFMD_REDIS_URL"] = "fakeredis://" | |
| 37 | + os.environ["HFMD_SECRET_KEY"] = "test-secret" | |
| 38 | + os.environ["HFMD_KEY_SALT"] = "test-salt" | |
| 39 | + for m in [m for m in list(sys.modules) if m.split(".")[0] in ("core", "main", "futures", "accounts", "ratelimit", "fundamentals", "bulk", "stream", "openapi")]: | |
| 40 | + del sys.modules[m] | |
| 41 | + main = importlib.import_module("main") | |
| 42 | + return main.app | |
| 43 | + | |
| 44 | + | |
| 45 | +@pytest.fixture(scope="session") | |
| 46 | +def client(app): | |
| 47 | + from fastapi.testclient import TestClient | |
| 48 | + with TestClient(app) as c: | |
| 49 | + yield c | |
added
tests/fixtures/__init__.py
+0 −0
added
tests/fixtures/make_fixtures.py
+118 −0
@@ -0,0 +1,118 @@ | ||
| 1 | +"""Build a tiny synthetic Parquet lake with the exact layout produced by frd_downloader.py. | |
| 2 | + | |
| 3 | + parquet/{stock|etf|crypto|index|fx}/{timeframe}/{adjustment}/{TICKER}_{timeframe}.parquet | |
| 4 | + parquet/futures/{timeframe}/{contin_UNadj|contin_adj_ratio|contin_adj_absolute}/{ROOT}_{timeframe}.parquet | |
| 5 | + parquet/futures_contracts/{timeframe}/{archive|update}/{ROOT}_{MonthCode}{YY}_{timeframe}.parquet | |
| 6 | + parquet/options/{year}_{quarter}/{TICKER}_month_option_chain.parquet | |
| 7 | + meta/futures/futures.csv | |
| 8 | + | |
| 9 | +Every bar file carries a `ticker` column (root only for futures contracts — the contract identity is | |
| 10 | +in the file name, exactly like the real lake). | |
| 11 | +""" | |
| 12 | +from __future__ import annotations | |
| 13 | + | |
| 14 | +import csv | |
| 15 | +from datetime import date, datetime, timedelta | |
| 16 | +from pathlib import Path | |
| 17 | + | |
| 18 | +import numpy as np | |
| 19 | +import pandas as pd | |
| 20 | + | |
| 21 | +TIMEFRAMES = ["1min", "5min", "30min", "1hour", "1day"] | |
| 22 | +TICKERS = {"stock": ["AAPL", "MSFT", "SMCP"], "etf": ["SPY"], "crypto": ["BTCUSD"], "index": ["SPX"], "fx": ["EURUSD"]} | |
| 23 | +ADJ = {"stock": ["adj_split", "adj_splitdiv", "UNADJUSTED"], "etf": ["adj_split", "adj_splitdiv", "UNADJUSTED"], | |
| 24 | + "crypto": ["none"], "index": ["none"], "fx": ["none"]} | |
| 25 | +ROOTS = ["ES", "CL", "NG"] | |
| 26 | +MONTHS = {"F": 1, "G": 2, "H": 3, "J": 4, "K": 5, "M": 6, "N": 7, "Q": 8, "U": 9, "V": 10, "X": 11, "Z": 12} | |
| 27 | +CYCLE = {"ES": "HMUZ", "CL": "FGHJKMNQUVXZ", "NG": "FGHJKMNQUVXZ"} | |
| 28 | + | |
| 29 | + | |
| 30 | +def _bars(ticker: str, start: date, end: date, tf: str, seed: int, oi: bool = False) -> pd.DataFrame: | |
| 31 | + rng = np.random.default_rng(seed) | |
| 32 | + if tf == "1day": | |
| 33 | + idx = pd.bdate_range(start, end) | |
| 34 | + else: | |
| 35 | + step = {"1min": 1, "5min": 5, "30min": 30, "1hour": 60}[tf] | |
| 36 | + days = pd.bdate_range(start, end)[-5:] # intraday: last 5 sessions only, RTH 09:30-16:00 ET | |
| 37 | + idx = pd.DatetimeIndex([d + timedelta(hours=9, minutes=30) + timedelta(minutes=step * i) | |
| 38 | + for d in days for i in range(int(390 / step))]) | |
| 39 | + n = len(idx) | |
| 40 | + close = 100 + np.cumsum(rng.normal(0, 1, n)) | |
| 41 | + df = pd.DataFrame({"ticker": ticker, "datetime": idx, "open": close + rng.normal(0, .2, n), | |
| 42 | + "high": close + abs(rng.normal(0, .5, n)), "low": close - abs(rng.normal(0, .5, n)), | |
| 43 | + "close": close, "volume": rng.integers(100, 10_000, n).astype(float)}) | |
| 44 | + if oi: | |
| 45 | + df["open_interest"] = rng.integers(1_000, 100_000, n).astype(float) | |
| 46 | + return df | |
| 47 | + | |
| 48 | + | |
| 49 | +def build_lake(root: Path, start: date = date(2023, 1, 2), end: date = date(2025, 6, 30)) -> None: | |
| 50 | + pq = root / "parquet" | |
| 51 | + seed = 1 | |
| 52 | + for asset, tickers in TICKERS.items(): | |
| 53 | + for tf in TIMEFRAMES: | |
| 54 | + for adj in ADJ[asset]: | |
| 55 | + if tf not in ("1min", "1day") and adj == "UNADJUSTED": | |
| 56 | + continue | |
| 57 | + d = pq / asset / tf / adj | |
| 58 | + d.mkdir(parents=True, exist_ok=True) | |
| 59 | + for t in tickers: | |
| 60 | + seed += 1 | |
| 61 | + _bars(t, start, end, tf, seed).to_parquet(d / f"{t}_{tf}.parquet", index=False) | |
| 62 | + for tf in TIMEFRAMES: | |
| 63 | + for adj in ("contin_UNadj", "contin_adj_ratio", "contin_adj_absolute"): | |
| 64 | + d = pq / "futures" / tf / adj | |
| 65 | + d.mkdir(parents=True, exist_ok=True) | |
| 66 | + for r in ROOTS: | |
| 67 | + seed += 1 | |
| 68 | + _bars(r, start, end, tf, seed, oi=(tf == "1day")).to_parquet(d / f"{r}_{tf}.parquet", index=False) | |
| 69 | + # individual contracts: archive = up to 2024, update = 2025+ (with overlap for 2025 contracts) | |
| 70 | + for tf in TIMEFRAMES: | |
| 71 | + for r in ROOTS: | |
| 72 | + for yy in (23, 24, 25, 26): | |
| 73 | + for mc in CYCLE[r][:: (1 if r == "ES" else 3)]: | |
| 74 | + exp_month = MONTHS[mc] | |
| 75 | + exp = date(2000 + yy, exp_month, 15) | |
| 76 | + first = exp - timedelta(days=400) | |
| 77 | + last = min(exp, end) | |
| 78 | + if first > end: | |
| 79 | + continue | |
| 80 | + bucket = "update" if yy >= 25 else "archive" | |
| 81 | + d = pq / "futures_contracts" / tf / bucket | |
| 82 | + d.mkdir(parents=True, exist_ok=True) | |
| 83 | + seed += 1 | |
| 84 | + _bars(r, max(first, date(2022, 1, 3)), last, tf, seed, oi=(tf == "1day")).to_parquet( | |
| 85 | + d / f"{r}_{mc}{yy}_{tf}.parquet", index=False) | |
| 86 | + if bucket == "update" and yy == 25: # archive also holds the first half of 2025 contracts | |
| 87 | + d2 = pq / "futures_contracts" / tf / "archive" | |
| 88 | + d2.mkdir(parents=True, exist_ok=True) | |
| 89 | + _bars(r, max(first, date(2022, 1, 3)), min(last, date(2024, 12, 31)), tf, seed, oi=(tf == "1day")).to_parquet( | |
| 90 | + d2 / f"{r}_{mc}{yy}_{tf}.parquet", index=False) | |
| 91 | + # options: one quarter, one ticker | |
| 92 | + d = pq / "options" / "2025_q2" | |
| 93 | + d.mkdir(parents=True, exist_ok=True) | |
| 94 | + rows = [] | |
| 95 | + for td in pd.bdate_range("2025-04-01", "2025-04-10"): | |
| 96 | + for k in (180, 190, 200, 210): | |
| 97 | + for cp in ("c", "p"): | |
| 98 | + rows.append({"ticker": "AAPL", "trade_date": td.date(), "strike": float(k), "expiry": date(2025, 6, 20), | |
| 99 | + "call_put": cp, "bid": 1.0, "ask": 1.2, "last": 1.1, "volume": 10.0, "open_interest": 100.0, | |
| 100 | + "iv": 0.25, "delta": 0.5 if cp == "c" else -0.5, "gamma": 0.01, "theta": -0.02, "vega": 0.1, | |
| 101 | + "rho": 0.01, "underlying_price": 195.0}) | |
| 102 | + pd.DataFrame(rows).to_parquet(d / "AAPL_month_option_chain.parquet", index=False) | |
| 103 | + # metadata | |
| 104 | + m = root / "meta" / "futures" | |
| 105 | + m.mkdir(parents=True, exist_ok=True) | |
| 106 | + with open(m / "futures.csv", "w", newline="") as f: | |
| 107 | + w = csv.writer(f) | |
| 108 | + w.writerow(["Ticker", "Name", "First Date", "Last Date"]) | |
| 109 | + w.writerow(["ES", "E-mini S&P 500 (CME) ", "2008-01-02", str(end)]) | |
| 110 | + w.writerow(["CL", "Crude Oil WTI (NYMEX) ", "2008-01-02", str(end)]) | |
| 111 | + w.writerow(["NG", "Natural Gas (NYMEX) ", "2008-01-02", str(end)]) | |
| 112 | + (root / "state").mkdir(exist_ok=True) | |
| 113 | + | |
| 114 | + | |
| 115 | +if __name__ == "__main__": | |
| 116 | + import sys | |
| 117 | + build_lake(Path(sys.argv[1] if len(sys.argv) > 1 else "/tmp/hfmd-lake")) | |
| 118 | + print("fixture lake built") | |
added
tests/test_v1_regression.py
+52 −0
@@ -0,0 +1,52 @@ | ||
| 1 | +"""Non-regression of the historical v1 surface: same paths, same shapes, same status codes.""" | |
| 2 | + | |
| 3 | + | |
| 4 | +def test_health(client): | |
| 5 | + r = client.get("/health") | |
| 6 | + assert r.status_code == 200 | |
| 7 | + assert r.json()["status"] == "ok" | |
| 8 | + | |
| 9 | + | |
| 10 | +def test_status_lists_assets(client): | |
| 11 | + r = client.get("/v1/status") | |
| 12 | + assert r.status_code == 200 | |
| 13 | + body = r.json() | |
| 14 | + assert "datasets" in body and "stock" in body["datasets"] | |
| 15 | + | |
| 16 | + | |
| 17 | +def test_tickers_and_bars_shape(client): | |
| 18 | + r = client.get("/v1/stock/tickers?timeframe=1day") | |
| 19 | + assert r.status_code == 200 | |
| 20 | + assert "AAPL" in r.json()["tickers"] | |
| 21 | + r = client.get("/v1/bars/stock/AAPL?timeframe=1day&limit=5") | |
| 22 | + assert r.status_code == 200 | |
| 23 | + body = r.json() | |
| 24 | + assert body["count"] == 5 and set(body["data"][0]) >= {"ticker", "datetime", "open", "high", "low", "close", "volume"} | |
| 25 | + assert r.headers["X-Row-Count"] == "5" | |
| 26 | + | |
| 27 | + | |
| 28 | +def test_csv_format(client): | |
| 29 | + r = client.get("/v1/bars/stock/AAPL?timeframe=1day&limit=3&format=csv") | |
| 30 | + assert r.status_code == 200 and r.headers["content-type"].startswith("text/csv") | |
| 31 | + assert r.text.splitlines()[0].startswith("ticker,datetime") | |
| 32 | + | |
| 33 | + | |
| 34 | +def test_uniform_error_envelope_keeps_legacy_detail(client): | |
| 35 | + r = client.get("/v1/bars/stock/NOPE?timeframe=1day") | |
| 36 | + assert r.status_code == 404 | |
| 37 | + body = r.json() | |
| 38 | + assert body["error"]["code"] == "TICKER_NOT_FOUND" | |
| 39 | + assert body["error"]["docs"].endswith("#ticker_not_found") | |
| 40 | + assert body["detail"] # legacy field still present | |
| 41 | + | |
| 42 | + | |
| 43 | +def test_validation_error_is_uniform(client): | |
| 44 | + r = client.get("/v1/bars/stock/AAPL?timeframe=1day&limit=0") | |
| 45 | + assert r.status_code == 422 | |
| 46 | + assert r.json()["error"]["code"] == "VALIDATION_ERROR" | |
| 47 | + | |
| 48 | + | |
| 49 | +def test_options_chain(client): | |
| 50 | + r = client.get("/v1/options/chain/AAPL?limit=4") | |
| 51 | + assert r.status_code == 200 | |
| 52 | + assert r.json()["count"] == 4 | |
| 53 | ||