diff --git a/.devcontainer/.devcontainer.json b/.devcontainer/.devcontainer.json index 53cf50f1..9174ddad 100644 --- a/.devcontainer/.devcontainer.json +++ b/.devcontainer/.devcontainer.json @@ -30,17 +30,9 @@ } } }, - "features": { - "ghcr.io/devcontainers/features/postgres:1": { - "version": "15" - }, - "ghcr.io/devcontainers/features/redis:1": { - "version": "latest" - } - }, "forwardPorts": [ 8000 ], "postCreateCommand": "make install", "remoteUser": "vscode" -} \ No newline at end of file +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 53cf50f1..9174ddad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -30,17 +30,9 @@ } } }, - "features": { - "ghcr.io/devcontainers/features/postgres:1": { - "version": "15" - }, - "ghcr.io/devcontainers/features/redis:1": { - "version": "latest" - } - }, "forwardPorts": [ 8000 ], "postCreateCommand": "make install", "remoteUser": "vscode" -} \ No newline at end of file +} diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..e4e5eb32 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203, W503 diff --git a/benchmark.json b/benchmark.json new file mode 100644 index 00000000..4de491da --- /dev/null +++ b/benchmark.json @@ -0,0 +1,562 @@ +{ + "meta": { + "xcore_version": "2.1.2", + "python_version": "3.14.4", + "platform": "linux", + "timestamp": "2026-04-29T11:04:53.472846", + "cpu_count": 8, + "total_duration_s": 90.74864666199937, + "system_info": { + "cpu_count": 8, + "cpu_freq_mhz": 1287.0510000000002, + "total_ram_gb": 15.3, + "available_ram_gb": 7.4 + } + }, + "results": [ + { + "name": "single_plugin_load", + "category": "plugin_lifecycle", + "iterations": 1000, + "total_ms": 1653.8354479707778, + "mean_ms": 1.6538354479707778, + "median_ms": 1.0576224995020311, + "p95_ms": 2.2267910007940372, + "p99_ms": 18.81043200046406, + "min_ms": 0.8524500008206815, + "max_ms": 61.980675000086194, + "std_ms": 3.700601990736283, + "throughput_ops_sec": 604.655076916497, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "1000 runs" + }, + { + "name": "single_plugin_unload", + "category": "plugin_lifecycle", + "iterations": 1000, + "total_ms": 262.9234420037392, + "mean_ms": 0.2629234420037392, + "median_ms": 0.2353315003347234, + "p95_ms": 0.36601899955712724, + "p99_ms": 1.0726239997893572, + "min_ms": 0.20125400078541134, + "max_ms": 2.1960899994155625, + "std_ms": 0.12980287681590708, + "throughput_ops_sec": 3803.3885163643126, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "1000 runs" + }, + { + "name": "plugin_reload", + "category": "plugin_lifecycle", + "iterations": 1000, + "total_ms": 619.2345519848459, + "mean_ms": 0.6192345519848459, + "median_ms": 0.5641699999614502, + "p95_ms": 0.7921790002001217, + "p99_ms": 1.595661000465043, + "min_ms": 0.47513000026810914, + "max_ms": 19.42086199960613, + "std_ms": 0.6220941984513612, + "throughput_ops_sec": 1614.8969672229664, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "batch_load_5_plugins", + "category": "plugin_lifecycle", + "iterations": 5, + "total_ms": 278.03283799948986, + "mean_ms": 55.60656759989797, + "median_ms": 55.60656759989797, + "p95_ms": 83.40985139984696, + "p99_ms": 111.21313519979594, + "min_ms": 0, + "max_ms": 278.03283799948986, + "std_ms": 0, + "throughput_ops_sec": 17.983487259908394, + "memory_delta_mb": 8.79296875, + "errors": -1, + "notes": "6/5 loaded, 1.76MB/plugin" + }, + { + "name": "batch_load_20_plugins", + "category": "plugin_lifecycle", + "iterations": 20, + "total_ms": 280.6563129997812, + "mean_ms": 14.032815649989061, + "median_ms": 14.032815649989061, + "p95_ms": 21.04922347498359, + "p99_ms": 28.065631299978122, + "min_ms": 0, + "max_ms": 280.6563129997812, + "std_ms": 0, + "throughput_ops_sec": 71.26153616938447, + "memory_delta_mb": 0.01953125, + "errors": -1, + "notes": "21/20 loaded, 0.00MB/plugin" + }, + { + "name": "batch_load_50_plugins", + "category": "plugin_lifecycle", + "iterations": 50, + "total_ms": 456.491613000253, + "mean_ms": 9.12983226000506, + "median_ms": 9.12983226000506, + "p95_ms": 13.69474839000759, + "p99_ms": 18.25966452001012, + "min_ms": 0, + "max_ms": 456.491613000253, + "std_ms": 0, + "throughput_ops_sec": 109.53103753950522, + "memory_delta_mb": 0.0078125, + "errors": -1, + "notes": "51/50 loaded, 0.00MB/plugin" + }, + { + "name": "sequential_call_ping", + "category": "plugin_calls", + "iterations": 1000, + "total_ms": 549.8288080034399, + "mean_ms": 0.5498288080034399, + "median_ms": 0.5462260005515418, + "p95_ms": 0.6609619995288085, + "p99_ms": 0.7332519999181386, + "min_ms": 0.37818799864908215, + "max_ms": 18.01899300153309, + "std_ms": 0.5595776948326894, + "throughput_ops_sec": 1818.7479183406913, + "memory_delta_mb": 0.0, + "errors": 1000, + "notes": "single plugin, 1000 calls" + }, + { + "name": "sequential_call_echo_payload", + "category": "plugin_calls", + "iterations": 1000, + "total_ms": 531.5483460144605, + "mean_ms": 0.5315483460144605, + "median_ms": 0.5326665004758979, + "p95_ms": 0.6623869994655252, + "p99_ms": 0.7814060008968227, + "min_ms": 0.37976399835315533, + "max_ms": 0.9061159998964285, + "std_ms": 0.08243008392172911, + "throughput_ops_sec": 1881.2964192212826, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "concurrent_10_calls", + "category": "plugin_calls", + "iterations": 10, + "total_ms": 6.712553000397747, + "mean_ms": 0.6712553000397747, + "median_ms": 0.6712553000397747, + "p95_ms": 0.8726318900517072, + "p99_ms": 1.006882950059662, + "min_ms": 0, + "max_ms": 6.712553000397747, + "std_ms": 0, + "throughput_ops_sec": 1489.7461516367107, + "memory_delta_mb": 0.0, + "errors": 10, + "notes": "asyncio.gather(10)" + }, + { + "name": "concurrent_50_calls", + "category": "plugin_calls", + "iterations": 50, + "total_ms": 39.98580899860826, + "mean_ms": 0.7997161799721653, + "median_ms": 0.7997161799721653, + "p95_ms": 1.0396310339638148, + "p99_ms": 1.199574269958248, + "min_ms": 0, + "max_ms": 39.98580899860826, + "std_ms": 0, + "throughput_ops_sec": 1250.4436261809856, + "memory_delta_mb": 0.0, + "errors": 50, + "notes": "asyncio.gather(50)" + }, + { + "name": "concurrent_100_calls", + "category": "plugin_calls", + "iterations": 100, + "total_ms": 93.88338000098884, + "mean_ms": 0.9388338000098884, + "median_ms": 0.9388338000098884, + "p95_ms": 1.2204839400128549, + "p99_ms": 1.4082507000148325, + "min_ms": 0, + "max_ms": 93.88338000098884, + "std_ms": 0, + "throughput_ops_sec": 1065.1512546623985, + "memory_delta_mb": 0.0, + "errors": 100, + "notes": "asyncio.gather(100)" + }, + { + "name": "routing_5_plugins", + "category": "plugin_calls", + "iterations": 200, + "total_ms": 42.57786499147187, + "mean_ms": 0.21288932495735935, + "median_ms": 0.07892800022091251, + "p95_ms": 0.09425499956705607, + "p99_ms": 0.2733080000325572, + "min_ms": 0.053650999689125456, + "max_ms": 27.0948600009433, + "std_ms": 1.9104920019930742, + "throughput_ops_sec": 4697.2763909148325, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "6 loaded, 200 calls round-robin" + }, + { + "name": "routing_20_plugins", + "category": "plugin_calls", + "iterations": 200, + "total_ms": 13.121534997480921, + "mean_ms": 0.06560767498740461, + "median_ms": 0.06274550014495617, + "p95_ms": 0.08325999988301191, + "p99_ms": 0.12489999971876387, + "min_ms": 0.055982000048970804, + "max_ms": 0.21046499932708684, + "std_ms": 0.013054642492955359, + "throughput_ops_sec": 15242.119160479022, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "21 loaded, 200 calls round-robin" + }, + { + "name": "pipeline_0_middlewares", + "category": "middleware", + "iterations": 2000, + "total_ms": 6.521591951241135, + "mean_ms": 0.0032607959756205673, + "median_ms": 0.0025069994080695324, + "p95_ms": 0.004637000529328361, + "p99_ms": 0.004888999683316797, + "min_ms": 0.0021790001483168453, + "max_ms": 0.025078001272049733, + "std_ms": 0.001183324370529185, + "throughput_ops_sec": 306673.58751560294, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "pipeline_4_middlewares", + "category": "middleware", + "iterations": 2000, + "total_ms": 23.53243399193161, + "mean_ms": 0.011766216995965806, + "median_ms": 0.011150001228088513, + "p95_ms": 0.015556001017102972, + "p99_ms": 0.016503001461387612, + "min_ms": 0.009588000466465019, + "max_ms": 0.0949909990595188, + "std_ms": 0.003427922820594405, + "throughput_ops_sec": 84989.0836062995, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_emit_no_handlers", + "category": "events", + "iterations": 2000, + "total_ms": 9.18424001247331, + "mean_ms": 0.004592120006236655, + "median_ms": 0.0044720000005327165, + "p95_ms": 0.004659001206164248, + "p99_ms": 0.004847999662160873, + "min_ms": 0.004166000508121215, + "max_ms": 0.0911999995878432, + "std_ms": 0.002467249294366055, + "throughput_ops_sec": 217764.34384159796, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_emit_1_handler", + "category": "events", + "iterations": 2000, + "total_ms": 79.13077196644736, + "mean_ms": 0.03956538598322368, + "median_ms": 0.043199999709031545, + "p95_ms": 0.04741500015370548, + "p99_ms": 0.0628220004728064, + "min_ms": 0.027657999453367665, + "max_ms": 0.3801000002567889, + "std_ms": 0.01203053598454082, + "throughput_ops_sec": 25274.617576687237, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_emit_10_handlers", + "category": "events", + "iterations": 2000, + "total_ms": 202.94466797167843, + "mean_ms": 0.10147233398583921, + "median_ms": 0.09965049957827432, + "p95_ms": 0.11356899995007552, + "p99_ms": 0.15094599984877277, + "min_ms": 0.089948000095319, + "max_ms": 0.1985950002563186, + "std_ms": 0.010555282168575956, + "throughput_ops_sec": 9854.902915109386, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_wildcard_matching", + "category": "events", + "iterations": 2000, + "total_ms": 62.355292961001396, + "mean_ms": 0.031177646480500698, + "median_ms": 0.03010399996128399, + "p95_ms": 0.03481399835436605, + "p99_ms": 0.049606000175117515, + "min_ms": 0.02903099994000513, + "max_ms": 0.10097499944095034, + "std_ms": 0.003929790401716499, + "throughput_ops_sec": 32074.261943582744, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "hookmanager_5_priority_hooks", + "category": "events", + "iterations": 1000, + "total_ms": 618.4966150176479, + "mean_ms": 0.6184966150176479, + "median_ms": 0.6060270006855717, + "p95_ms": 0.8415030006290181, + "p99_ms": 1.1398449987609638, + "min_ms": 0.47312200149463024, + "max_ms": 2.049806998911663, + "std_ms": 0.13544462934080997, + "throughput_ops_sec": 1616.8237233949396, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "permission_check_cold", + "category": "permissions", + "iterations": 30000, + "total_ms": 94.13857905383338, + "mean_ms": 0.003137952635127779, + "median_ms": 0.0024870005290722474, + "p95_ms": 0.0028720005502691492, + "p99_ms": 0.005308998879627325, + "min_ms": 0.002061000486719422, + "max_ms": 16.65858600063075, + "std_ms": 0.09616561230116766, + "throughput_ops_sec": 318679.1249828025, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "permission_check_cached", + "category": "permissions", + "iterations": 30000, + "total_ms": 46.59688580795773, + "mean_ms": 0.0015532295269319245, + "median_ms": 0.001526999767520465, + "p95_ms": 0.001670001438469626, + "p99_ms": 0.002586999471532181, + "min_ms": 0.0013760000001639128, + "max_ms": 0.038047999623813666, + "std_ms": 0.0003534346220896545, + "throughput_ops_sec": 643819.8493272839, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_set_single", + "category": "cache", + "iterations": 3000, + "total_ms": 8.480978016450536, + "mean_ms": 0.0028269926721501784, + "median_ms": 0.0026470006559975445, + "p95_ms": 0.0031969993870006874, + "p99_ms": 0.004724999598693103, + "min_ms": 0.0022599997464567423, + "max_ms": 0.14715299948875327, + "std_ms": 0.003119987371805583, + "throughput_ops_sec": 353732.78815024707, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_get_hot", + "category": "cache", + "iterations": 3000, + "total_ms": 4.004473043096368, + "mean_ms": 0.0013348243476987893, + "median_ms": 0.0012429991329554468, + "p95_ms": 0.0016499998309882358, + "p99_ms": 0.0021579999156529084, + "min_ms": 0.0011350002750987187, + "max_ms": 0.020790001144632697, + "std_ms": 0.0006348142416934207, + "throughput_ops_sec": 749162.2412521768, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_mset_100_keys", + "category": "cache", + "iterations": 300, + "total_ms": 30.281595991255017, + "mean_ms": 0.10093865330418339, + "median_ms": 0.09859700003289618, + "p95_ms": 0.1105069986806484, + "p99_ms": 0.1468110003770562, + "min_ms": 0.09195600068778731, + "max_ms": 0.1541719993838342, + "std_ms": 0.00817448206686219, + "throughput_ops_sec": 9907.007546320763, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_mget_100_keys", + "category": "cache", + "iterations": 300, + "total_ms": 12.505856009738636, + "mean_ms": 0.04168618669912879, + "median_ms": 0.041201000385626685, + "p95_ms": 0.04277799962437712, + "p99_ms": 0.06069599839975126, + "min_ms": 0.040077999074128456, + "max_ms": 0.07302099947992247, + "std_ms": 0.00281464258349221, + "throughput_ops_sec": 23988.761726217075, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + } + ], + "capacity": { + "100": { + "n_requested": 100, + "n_loaded": 101, + "load_time_ms": 973.9, + "ms_per_plugin": 9.64, + "mem_delta_mb": 0.18, + "mb_per_plugin": 0.002, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 146.5 + }, + "200": { + "n_requested": 200, + "n_loaded": 201, + "load_time_ms": 2032.6, + "ms_per_plugin": 10.11, + "mem_delta_mb": 0.0, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 367.0 + }, + "300": { + "n_requested": 300, + "n_loaded": 301, + "load_time_ms": 3269.6, + "ms_per_plugin": 10.86, + "mem_delta_mb": 0.01, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 376.2 + }, + "400": { + "n_requested": 400, + "n_loaded": 401, + "load_time_ms": 4360.4, + "ms_per_plugin": 10.87, + "mem_delta_mb": 0.01, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 482.3 + }, + "500": { + "n_requested": 500, + "n_loaded": 501, + "load_time_ms": 5700.2, + "ms_per_plugin": 11.38, + "mem_delta_mb": 0.01, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 481.4 + }, + "600": { + "n_requested": 600, + "n_loaded": 601, + "load_time_ms": 7051.5, + "ms_per_plugin": 11.73, + "mem_delta_mb": 0.01, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 487.4 + }, + "700": { + "n_requested": 700, + "n_loaded": 701, + "load_time_ms": 8113.9, + "ms_per_plugin": 11.57, + "mem_delta_mb": 0.04, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 552.6 + }, + "800": { + "n_requested": 800, + "n_loaded": 801, + "load_time_ms": 7394.8, + "ms_per_plugin": 9.23, + "mem_delta_mb": 0.02, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 377.1 + }, + "900": { + "n_requested": 900, + "n_loaded": 901, + "load_time_ms": 8846.2, + "ms_per_plugin": 9.82, + "mem_delta_mb": 0.08, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 529.2 + }, + "1000": { + "n_requested": 1000, + "n_loaded": 1001, + "load_time_ms": 10968.7, + "ms_per_plugin": 10.96, + "mem_delta_mb": 0.43, + "mb_per_plugin": 0.0, + "concurrent_calls_ok": 0, + "concurrent_call_ms": 550.5 + } + } +} diff --git a/doc/architecture/overview.md b/doc/architecture/overview.md index 06eecf25..b9fcc0bb 100644 --- a/doc/architecture/overview.md +++ b/doc/architecture/overview.md @@ -1,530 +1,205 @@ -# Architecture Overview +# Architecture -Plongée approfondie dans l'architecture interne de XCore : comment le kernel, les services et les plugins interagissent. +XCore suit le pattern **Modular Monolith** : tous les plugins s'exécutent dans un environnement orchestré unifié, isolés via des frontières logiques (permissions, injection de contexte) et physiques (sandbox OS pour les plugins `sandboxed`). --- -## 1. Vue d'Ensemble Haute Niveau - -XCore suit le pattern **Modular Monolith**. Tous les plugins s'exécutent dans un environnement orchestré unifié, mais sont strictement isolés via des frontières logiques (injection de contexte) et physiques (sandbox au niveau processus). +## Vue d'ensemble ```mermaid flowchart TB - subgraph Core["🎯 XCore Kernel"] - direction TB - X[⚙️ Xcore Engine
Point d'entrée principal] - PS[🔄 Plugin Supervisor
Gestion cycle de vie] - SC[🔧 Service Container
Registry services] - EB[📡 Event Bus
Messagerie async] - PE[🛡️ Permission Engine
Contrôle d'accès] + subgraph App["Application FastAPI"] + FA[Routes HTTP] end - subgraph Infra["🏗️ Infrastructure Partagée"] - DB[(🗄️ Database
SQL/NoSQL)] - CACHE[(⚡ Cache
Redis/Memory)] - SCHED[⏰ Scheduler
APScheduler] - LOG[📝 Logger
Structuré] + subgraph Kernel["XCore Kernel"] + direction TB + X[Xcore Engine] + PS[PluginSupervisor] + SC[ServiceContainer] + EB[EventBus] + PE[PermissionEngine] + RM[MiddlewarePipeline
Tracing · RateLimit · Permissions · Retry] end - subgraph Plugins["🧩 Écosystème Plugins"] + subgraph Plugins["Plugins"] direction LR - TP[🔐 Trusted Plugins
Processus principal] - SP[📦 Sandboxed Plugins
Processus isolé] + TP[Trusted
processus principal] + SP[Sandboxed
processus isolé] end - subgraph App["🌐 Application"] - FA[FastAPI
Routes HTTP] - WS[WebSocket
Real-time] + subgraph Services["Services partagés"] + DB[(Database
SQL/NoSQL)] + CACHE[(Cache
Redis/Memory)] + SCHED[Scheduler
APScheduler] end App --> X - X --> PS - X --> SC - X --> EB - X --> PE - - SC --> Infra - PS --> TP - PS --> SP - - EB -.->|Pub/Sub| TP - EB -.->|Pub/Sub| SP - PE -.->|Audit| PS - - style Core fill:#E3F2FD,stroke:#1976D2,stroke-width:2px - style Infra fill:#E8F5E9,stroke:#388E3C,stroke-width:2px - style Plugins fill:#FFF3E0,stroke:#F57C00,stroke-width:2px - style App fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px + X --> PS & SC & EB & PE + PS --> RM --> TP & SP + SC --> DB & CACHE & SCHED + EB -.->|pub/sub| TP & SP + PE -.->|audit| RM + + style Kernel fill:#E3F2FD,stroke:#1976D2 + style Plugins fill:#FFF3E0,stroke:#F57C00 + style Services fill:#E8F5E9,stroke:#388E3C ``` -### Composants Clés +--- -| Composant | Rôle | Analogie | -| :--- | :--- | :--- | -| **Xcore Engine** | Point d'entrée, coordonne le boot | Chef d'orchestre | -| **PluginSupervisor** | Lifecycle, hot-reload, IPC | Gestionnaire de processus | -| **ServiceContainer** | Registry DB, Cache, Scheduler | Boîte à outils partagée | -| **EventBus** | Dispatcher async Observer pattern | Système de notification | -| **PermissionEngine** | Policy-based access control | Garde de sécurité | +## Composants principaux + +| Composant | Module | Rôle | +|:----------|:-------|:-----| +| `Xcore` | `xcore/__init__.py` | Point d'entrée — orchestre le boot/shutdown | +| `PluginSupervisor` | `kernel/runtime/supervisor.py` | Lifecycle plugins + pipeline d'appels | +| `PluginLoader` | `kernel/runtime/loader.py` | Découverte, import, résolution du DAG | +| `StateMachine` | `kernel/runtime/state_machine.py` | FSM par plugin (UNLOADED→LOADING→READY…) | +| `ServiceContainer` | `services/container.py` | Registry DB, Cache, Scheduler | +| `EventBus` | `kernel/events/bus.py` | Messagerie async pub/sub avec wildcards | +| `HookManager` | `kernel/events/hooks.py` | Hooks before/after sur le cycle de vie | +| `PermissionEngine` | `kernel/permissions/engine.py` | PBAC avec cache LRU | +| `PluginRegistry` | `registry/index.py` | Métadonnées et dépendances | +| `ManifestValidator` | `kernel/security/validation.py` | Validation manifeste + scan AST | --- -## 2. Boot Sequence Détaillée - -Le framework suit une séquence d'initialisation stricte pour garantir que toutes les dépendances sont résolues avant le démarrage des plugins. +## Séquence de boot ```mermaid sequenceDiagram - participant Dev as Développeur participant Main as main.py participant X as Xcore Engine - participant Cfg as Config Loader - participant S as Services + participant Cfg as ConfigLoader + participant SC as ServiceContainer participant L as PluginLoader - participant DAG as Dependency Graph - participant P as Plugins - participant R as Router + participant PS as PluginSupervisor - Dev->>Main: uvicorn app:app Main->>X: boot(app) - Note over X: 📋 Phase 1: Configuration - X->>Cfg: Charger xcore.yaml - X->>Cfg: Charger .env - Cfg-->>X: Config validée - - Note over X: 🔧 Phase 2: Services - X->>S: Initialiser DB - X->>S: Initialiser Cache - X->>S: Initialiser Scheduler - S-->>X: ✅ Services Ready - - Note over X: 🧩 Phase 3: Plugins - X->>L: Découvrir plugins/ - L->>L: Parser plugin.yaml - L->>DAG: Build dependency graph - DAG->>DAG: Topological sort - DAG-->>L: Ordre de chargement - - loop Chaque vague de plugins - L->>P: Load plugin (trusted/sandboxed) - P->>P: on_load() - P-->>L: ✅ Ready - end + Note over X: Phase 1 — Config + X->>Cfg: load(xcore.yaml + .env) + Cfg-->>X: XcoreConfig - Note over X: 🌐 Phase 4: Routage - X->>R: Monter routes FastAPI - R->>R: Plugin routes - R->>R: API routes - R-->>X: ✅ Routes montées + Note over X: Phase 2 — Services + X->>SC: init() [DB → Cache → Scheduler → Extensions] + SC-->>X: services ready - X-->>Main: Application prête - Main-->>Dev: 🚀 Running on :8000 -``` + Note over X: Phase 3 — Plugins + X->>L: discover(plugins/) + L->>L: parse plugin.yaml × N + L->>L: build dependency DAG + L->>L: topological sort → vagues de chargement -### Détails des Phases + loop chaque vague + L->>PS: load(plugin) + PS->>PS: AST scan (sandboxed) + PS->>PS: validate manifest + PS->>PS: inject context + PS->>PS: on_load() + PS-->>L: READY + end -```mermaid -gantt - title Boot Sequence Timeline - dateFormat X - axisFormat %L - - section Configuration - Charger YAML :a1, 0, 10 - Charger ENV :a2, after a1, 5 - Validation :a3, after a2, 5 - - section Services - Database :b1, after a3, 20 - Cache :b2, after a3, 15 - Scheduler :b3, after a3, 10 - - section Plugins - Discovery :c1, after b1, 10 - DAG Build :c2, after c1, 15 - Load Wave 1 :c3, after c2, 30 - Load Wave 2 :c4, after c3, 30 - - section Routing - Mount Routes :d1, after c4, 10 + Note over X: Phase 4 — Routage + X->>Main: mount plugin routes on FastAPI app + Main-->>Main: server ready on :8000 ``` --- -## 3. Machine d'État des Plugins - -Chaque plugin est géré par une **Finite State Machine (FSM)** pour assurer des transitions sûres pendant les hot-reloads ou en cas d'échec. +## Machine d'états des plugins ```mermaid stateDiagram-v2 - [*] --> UNLOADED: Découverte + [*] --> UNLOADED - UNLOADED --> LOADING: supervisor.load() + UNLOADED --> LOADING : load() + LOADING --> READY : ok + LOADING --> FAILED : error - state LOADING { - [*] --> SCANNING: AST Scan - SCANNING --> VALIDATING: Scan OK - VALIDATING --> INSTANTIATING: Manifest OK - INSTANTIATING --> INITIALIZING: Entry point OK - INITIALIZING --> READY: on_load() OK - end + READY --> UNLOADING : unload() + READY --> RELOADING : reload() + READY --> FAILED : error - LOADING --> FAILED: Erreur AST - LOADING --> FAILED: Erreur Manifest - LOADING --> FAILED: Erreur on_load() + UNLOADING --> UNLOADED : ok + UNLOADING --> FAILED : error - READY --> RELOADING: supervisor.reload() + RELOADING --> READY : ok + RELOADING --> FAILED : error - state RELOADING { - [*] --> UNLOADING: on_unload() - UNLOADING --> RELOADING_CODE: Recharger code - RELOADING_CODE --> RE_INITIALIZING: Nouveau on_load() - RE_INITIALIZING --> READY: Succès - end - - RELOADING --> FAILED: Échec reload - - READY --> UNLOADED: supervisor.unload() - FAILED --> UNLOADED: Cleanup - FAILED --> RELOADING: Retry (max 3) - - note right of READY - 🟢 État stable - - Routes actives - - Événements abonnés - - Services connectés - end note - - note right of FAILED - 🔴 État terminal - - Erreur enregistrée - - Cleanup requis - - Notification envoyée - end note - - note right of UNLOADED - ⚪ État initial/final - - Code non chargé - - Aucune ressource - end note + FAILED --> UNLOADED : reset ``` -### Codes d'État - -| État | Signification | Actions Possibles | -| :--- | :--- | :--- | -| `UNLOADED` | Plugin découvert mais non chargé | `load()` | -| `LOADING` | Initialisation en cours | Attendre | -| `READY` | Plugin opérationnel | `reload()`, `unload()`, `call()` | -| `RELOADING` | Mise à jour à chaud | Attendre | -| `FAILED` | Erreur irrécupérable | `unload()`, `reload()` (retry) | +Transitions invalides lèvent `InvalidTransition`. --- -## 4. Communication Inter-Plugins (IPC) - -Quand le **Plugin A** appelle le **Plugin B**, l'appel n'est **jamais direct**. Il transite par le **Supervisor Pipeline** du kernel. - -### Architecture du Pipeline - -```mermaid -flowchart LR - subgraph Caller["📤 Plugin A (Appelant)"] - A[Code métier] - end - - subgraph Kernel["🎯 XCore Supervisor Pipeline"] - direction TB - M1[📍 Tracing
Génère span] - M2[🛑 Rate Limiting
Vérifie quota] - M3[🔐 Permission Audit
Valide autorisation] - M4[🔄 Retry Logic
Gère échecs] - end - - subgraph Target["📥 Plugin B (Cible)"] - B[Handler @action] - end +## Pipeline d'appel inter-plugins - A -- "ctx.plugins.call()" --> M1 - M1 --> M2 - M2 --> M3 - M3 --> M4 - M4 -- "execute" --> B - B -- "result" --> A +Quand le Plugin A appelle le Plugin B via `call_plugin()`, l'appel transite par le `MiddlewarePipeline` : - style Kernel fill:#FFEBEE,stroke:#C62828,stroke-width:2px - style M3 fill:#C62828,color:#fff ``` - -### Détail des Étapes du Pipeline - -```mermaid -sequenceDiagram - participant A as Plugin A - participant T as Tracing - participant R as RateLimit - participant P as PermissionEngine - participant L as RetryLogic - participant B as Plugin B - - A->>T: call("plugin_b", "action", payload) - - Note over T: 1. Tracing - T->>T: Créer span ID - T->>T: Injecter context - - Note over R: 2. Rate Limiting - R->>R: Vérifier quota A - R-->>T: ✅ Dans la limite - - Note over P: 3. Permission Audit - P->>P: Charger policies - P->>P: Vérifier A → B.action - P-->>T: ✅ Autorisé - - Note over L: 4. Retry Logic - L->>B: Exécuter action - alt Succès - B-->>L: Résultat - L-->>A: ✅ Résultat - else Échec transitoire - B-->>L: Erreur - L->>L: Retry (max 3) - L-->>A: ✅ Résultat (retry) - else Échec permanent - L-->>A: ❌ Erreur - end +Plugin A → TracingMiddleware → RateLimitMiddleware → PermissionMiddleware → RetryMiddleware → Plugin B ``` -### Middleware Stack Détaillé - -| Étape | Rôle | Détails | -| :--- | :--- | :--- | -| **Tracing** | Observabilité | Génère un span OpenTelemetry, propage le context | -| **Rate Limiting** | Protection | Vérifie le quota du caller (calls/période) | -| **Permission Audit** | Sécurité | Valide la policy `resource: action` | -| **Retry Logic** | Résilience | Retry exponentiel sur erreurs transitoires | +| Middleware | Rôle | +|:-----------|:-----| +| `TracingMiddleware` | Crée un span, propage le contexte | +| `RateLimitMiddleware` | Vérifie le quota de l'appelant (calls/période) | +| `PermissionMiddleware` | Vérifie la policy via `PermissionEngine` | +| `RetryMiddleware` | Retry exponentiel sur erreurs transitoires | --- -## 5. Modèle de Sandboxing - -Les plugins sandboxed s'exécutent dans un **processus OS dédié**. C'est le niveau d'isolation le plus élevé. - -### Architecture du Sandbox - -```mermaid -flowchart TB - subgraph Kernel["🎯 Kernel Process"] - K[XCore Engine] - PS[PluginSupervisor] - WR[Worker Manager] - end - - subgraph IPC["📡 JSON-RPC 2.0 Channel"] - direction LR - PIPE1[Pipe stdout] - PIPE2[Pipe stdin] - end - - subgraph Sandbox["📦 Sandbox Process"] - SW[Sandbox Worker] - AST[🛡️ AST Scanner] - EX[Code Exécuté] - end +## Sandboxing (plugins `sandboxed`) - K --> PS - PS --> WR - WR --> IPC - IPC --> Sandbox +Trois couches de protection en cascade : - style Sandbox fill:#FFEBEE,stroke:#C62828 - style AST fill:#C62828,color:#fff ``` - -### Les 3 Couches de Protection - -```mermaid -flowchart LR - subgraph L1["Couche 1: AST Scan"] - A1[Parser AST] --> A2[Forbidden Names] - A2 --> A3[Import Whitelist] - end - - subgraph L2["Couche 2: Process Isolation"] - B1[Processus dédié] --> B2[JSON-RPC sur pipes] - B2 --> B3[Pas de pickle] - end - - subgraph L3["Couche 3: Resource Limits"] - C1[Memory Monitor] --> C2[Timeout Monitor] - C2 --> C3[Kill si dépassement] - end - - L1 --> L2 --> L3 - - style L1 fill:#FFE0B2,stroke:#F57C00 - style L2 fill:#C8E6C9,stroke:#388E3C - style L3 fill:#BBDEFB,stroke:#1976D2 +Couche 1 — AST Scan (statique, avant import) + └─ modules interdits : os, subprocess, … + └─ builtins interdits : eval, exec, … + └─ attributs dangereux : __globals__, __subclasses__, … + +Couche 2 — Isolation processus + └─ processus OS dédié + └─ IPC JSON-RPC 2.0 sur pipes stdin/stdout (pas de pickle) + +Couche 3 — Limites de ressources + └─ timeout CPU (kill si dépassé) + └─ mémoire RSS max (kill si dépassée) + └─ rate limit (429 si quota dépassé) ``` -#### Couche 1: Analyse Statique (AST) +--- -Avant exécution, l'`ASTScanner` parse l'arbre syntaxique du plugin : +## Service Container — ordre d'initialisation -```python -# Ce qui est BLOQUÉ : -import os # ❌ Forbidden module -import subprocess # ❌ Forbidden module -eval() # ❌ Forbidden name -exec() # ❌ Forbidden name -__class__ # ❌ Forbidden attribute -__globals__ # ❌ Forbidden attribute -__subclasses__() # ❌ Forbidden attribute (sandbox escape) ``` - -#### Couche 2: Isolation Processus - -```mermaid -sequenceDiagram - participant K as Kernel - participant P as Pipe (IPC) - participant S as Sandbox Worker - - K->>P: {"jsonrpc":"2.0","method":"call",...} - Note over P: JSON uniquement
Pas d'objets Python - P->>S: Désérialiser JSON - S->>S: Exécuter handler - S->>P: {"jsonrpc":"2.0","result":...} - P->>K: Résultat JSON +Database → Cache → Scheduler → Extensions ``` -#### Couche 3: Limites de Ressources - -| Ressource | Limite | Action | -| :--- | :--- | :--- | -| **Mémoire (RSS)** | `max_memory_mb` | Kill process si dépassement | -| **Temps CPU** | `timeout_seconds` | Kill + timeout error | -| **Appels** | `rate_limit` | 429 Too Many Requests | - ---- - -## 6. Service Scoping - -Les services enregistrés dans le `ServiceContainer` ont différents niveaux de visibilité : - -```mermaid -flowchart TB - subgraph Container["🔧 Service Container"] - direction TB - PUB[🟢 Public Services
db, cache, scheduler] - PRI[🔴 Private Services
Kernel interne] - SCO[🟡 Scoped Services
Plugins autorisés] - end - - subgraph Access["Accès"] - K[🎯 Kernel] - T[🔐 Trusted Plugins] - S[📦 Sandboxed Plugins] - end +Shutdown dans l'ordre inverse : - PUB --> K - PUB --> T - PUB --> S - - PRI --> K - - SCO --> K - SCO --> T - SCO -.->|Si autorisé| S - - style PUB fill:#C8E6C9,stroke:#388E3C - style PRI fill:#FFCDD2,stroke:#C62828 - style SCO fill:#FFF9C4,stroke:#F57F17 ``` - -### Matrice d'Accès - -| Scope | Description | Accès | Exemple | -| :--- | :--- | :--- | :--- | -| **Public** | Infrastructure générale | Kernel + Tous plugins | `db`, `cache`, `scheduler` | -| **Private** | Services kernel internes | Kernel uniquement | `permission_engine`, `plugin_supervisor` | -| **Scoped** | Restreint à plugins spécifiques | Plugins autorisés uniquement | `stripe_api` (plugin paiement) | - -### Exemple de Configuration - -```yaml -# xcore.yaml -services: - # Service public - accessible par tous - db: - scope: public - backend: postgresql - url: ${DATABASE_URL} - - # Service scoped - accessible uniquement par authorized plugins - payment_db: - scope: scoped - authorized_plugins: - - stripe_plugin - - billing_plugin - backend: postgresql - url: ${PAYMENT_DATABASE_URL} - - # Service private - kernel uniquement - audit_log: - scope: private - backend: sqlite +Extensions → Scheduler → Cache → Database ``` ---- +Les connexions multiples à la même base (`db`, `analytics`, `redis_db`) sont toutes enregistrées dans le container sous leurs clés respectives. La première connexion déclarée est aussi enregistrée sous `"db"` comme alias par défaut. -## 7. Flux de Données Complet +--- -Voici le flux complet d'une requête HTTP à travers XCore : +## Résolution des dépendances entre plugins -```mermaid -sequenceDiagram - participant Client as 🌐 Client HTTP - participant API as 📡 FastAPI Router - participant X as ⚙️ XCore Engine - participant P as 🛡️ Permission Engine - participant S as 🔄 Plugin Supervisor - participant PL as 🧩 Plugin Cible - participant DB as 🗄️ Database - - Client->>API: POST /plugins/call - API->>X: plugins.call(plugin, action, payload) - - Note over X,P: 1. Permission Check - X->>P: verify(plugin, action) - P-->>X: ✅ Autorisé - - Note over X,S: 2. Resolve Plugin - X->>S: get_handler(plugin, action) - S-->>X: Handler trouvé - - Note over S,PL: 3. Execute Action - S->>PL: @action(payload) - - alt Besoin DB - PL->>DB: Query - DB-->>PL: Résultats - end +`PluginLoader` construit un **DAG** (directed acyclic graph) à partir des champs `requires` de chaque manifeste. Un tri topologique détermine l'ordre de chargement par vagues : - PL-->>S: Résultat - S-->>X: Response - X-->>API: JSON - API-->>Client: HTTP 200 OK -``` +- Les plugins d'une même vague sont chargés en parallèle. +- Un cycle de dépendances lève `CyclicDependencyError` au démarrage. +- Une dépendance manquante lève `MissingDependencyError`. --- -## Prochaines Lectures +## Décisions architecturales -| 📚 Document | Objectif | -| :--- | :--- | -| [Technical Decisions](decisions.md) | Comprendre les choix architecturaux | -| [Security Deep Dive](../guides/security.md) | Détails du sandboxing | -| [Scaling Analysis](../guides/scaling.md) | Passage à l'échelle | +Voir [Decisions](decisions.md) pour les choix de design (pourquoi JSON-RPC plutôt que `multiprocessing.Queue`, pourquoi `fnmatch` pour les policies, etc.). diff --git a/doc/development/testing.md b/doc/development/testing.md index b45028ca..8d69ddd9 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -1,119 +1,215 @@ -# Guide des Tests XCore - -XCore a été conçu avec la testabilité comme priorité. Ce guide vous aide à tester vos plugins de manière robuste, tant en isolation qu'en intégration. +# Guide des Tests --- -## 1. Philosophie des Tests +## Structure des tests -Dans XCore, les tests se répartissent en trois catégories : -1. **Tests de Plugins (Isolation)** : Tester la logique métier sans le framework. -2. **Tests d'Intégration (Noyau)** : Tester le chargement, les services et l'IPC. -3. **Tests de Sécurité (Sandbox)** : Vérifier que les restrictions de la sandbox sont bien appliquées. +``` +tests/ +├── conftest.py # Fixtures globales +├── pytest.ini # Configuration pytest +├── unit/ +│ ├── kernel/ +│ │ ├── test_contract.py # TrustedBase, BasePlugin, ok/error +│ │ └── test_events.py # EventBus, HookManager +│ ├── services/ +│ │ └── test_cache.py # CacheService, MemoryBackend, RedisBackend +│ ├── plugins/ +│ │ ├── test_base.py # PluginManifest, PluginDependency +│ │ └── test_decorators.py # @action, @validate_payload, @route +│ ├── security/ +│ │ └── test_validation.py # ManifestValidator, ASTScanner +│ └── test_configuration.py # ConfigLoader, sections +└── integration/ + ├── test_xcore.py # boot/shutdown complet + └── test_plugin_lifecycle.py # load/call/reload/unload +``` --- -## 2. Tester un Plugin en Isolation +## Lancer les tests + +```bash +# Suite complète +poetry run pytest + +# Avec couverture +poetry run pytest --cov=xcore --cov-report=term-missing + +# Par marker +poetry run pytest -m unit +poetry run pytest -m integration +poetry run pytest -m "not slow" + +# Un test spécifique +poetry run pytest -k "test_permission_engine" + +# Un fichier +poetry run pytest tests/unit/kernel/test_events.py + +# Un répertoire +poetry run pytest tests/unit/kernel/ + +# Verbose avec traceback court +poetry run pytest -v --tb=short +``` -Le moyen le plus simple est de tester la classe `Plugin` directement en mockant ses services. +**Markers disponibles :** `unit`, `integration`, `slow`, `skip_ci` + +--- + +## Tester un plugin en isolation ```python import pytest -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock from plugins.mon_plugin.src.main import Plugin @pytest.mark.asyncio -async def test_mon_plugin_logic(): - # Instanciation manuelle - p = Plugin() +async def test_greet_action(): + plugin = Plugin() + + # Mock du contexte et des services + mock_cache = AsyncMock() + mock_cache.get.return_value = None - # Mock des services - p.cache = MagicMock() - p.cache.get.return_value = "ma_valeur" + plugin.ctx = MagicMock() + plugin.ctx.get_service.return_value = mock_cache - # Appel de l'action - result = await p.handle("get_data", {"key": "test"}) + await plugin.on_load() - # Vérifications + result = await plugin.handle("greet", {"name": "Alice"}) assert result["status"] == "ok" - assert result["data"] == "ma_valeur" + assert "Alice" in result["message"] + + # Vérifier que le cache a été écrit + mock_cache.set.assert_called_once() ``` --- -## 3. Tester via le Framework (Intégration) - -Utilisez le `Xcore` orchestrateur pour charger réellement vos plugins et tester les appels IPC réels. +## Tester via le framework (intégration) ```python -from xcore import Xcore import pytest +from xcore import Xcore -@pytest.mark.asyncio -async def test_full_plugin_cycle(): - # Démarrage du framework (config par défaut) - xcore = Xcore() +@pytest.fixture +async def xcore_instance(): + xcore = Xcore(config_path="tests/fixtures/xcore_test.yaml") await xcore.boot() + yield xcore + await xcore.shutdown() - # Appel IPC réel via le Supervisor - result = await xcore.plugins.call("mon_plugin", "ping", {}) - - # Vérifications +@pytest.mark.asyncio +async def test_plugin_call(xcore_instance): + result = await xcore_instance.plugins.call("mon_plugin", "ping", {}) assert result["status"] == "ok" - assert result["msg"] == "pong" - # Arrêt propre - await xcore.shutdown() +@pytest.mark.asyncio +async def test_plugin_reload(xcore_instance): + await xcore_instance.plugins.reload("mon_plugin") + result = await xcore_instance.plugins.call("mon_plugin", "ping", {}) + assert result["status"] == "ok" ``` --- -## 4. Tester l'API HTTP (FastAPI) - -Utilisez `TestClient` de FastAPI pour tester les endpoints HTTP exposés par vos plugins. +## Tester l'API HTTP ```python from fastapi.testclient import TestClient -from app import app # Votre application FastAPI intégrant Xcore +from main import app # votre application FastAPI client = TestClient(app) -def test_plugin_http_route(): - # Test de la route système IPC +def test_plugin_route(): response = client.post( - "/plugin/ipc/mon_plugin/action", - headers={"X-Plugin-Key": "change-me-in-production"}, - json={"payload": {"key": "val"}} + "/plugin/mon_plugin/action", + headers={"X-Plugin-Key": "test-key"}, + json={"action": "greet", "payload": {"name": "Test"}}, ) assert response.status_code == 200 + assert response.json()["status"] == "ok" - # Test de la route REST du plugin - response = client.get("/plugin/mon_plugin/status") - assert response.status_code == 200 - assert response.json()["active"] is True +def test_protected_route(): + # Sans token → 401 + r = client.get("/plugin/mon_plugin/items") + assert r.status_code == 401 + + # Avec token valide + r = client.get( + "/plugin/mon_plugin/items", + headers={"Authorization": "Bearer valid_token"}, + ) + assert r.status_code == 200 +``` + +--- + +## Tester les événements + +```python +import pytest +from xcore.kernel.events.bus import EventBus + +@pytest.mark.asyncio +async def test_event_received(): + bus = EventBus() + received = [] + + @bus.on("user.created") + async def handler(event): + received.append(event.data) + + await bus.emit("user.created", {"email": "test@example.com"}) + + assert len(received) == 1 + assert received[0]["email"] == "test@example.com" + +@pytest.mark.asyncio +async def test_wildcard_event(): + bus = EventBus() + received = [] + + @bus.on("order.*") + async def catch_all(event): + received.append(event.name) + + await bus.emit("order.created", {}) + await bus.emit("order.paid", {}) + await bus.emit("user.created", {}) # ne doit pas déclencher + + assert received == ["order.created", "order.paid"] ``` --- -## 5. Exécution des Tests +## Benchmarks -Le projet utilise **Pytest**. Vous pouvez lancer tous les tests du framework et des plugins avec : +XCore embarque des benchmarks dans `tests/benchmarks/` via `pytest-benchmark`. ```bash -# Lancer tous les tests -poetry run pytest +# Benchmarks kernel (permission engine, policy matching) +poetry run pytest tests/benchmarks/test_kernel_benchmarks.py \ + tests/benchmarks/test_permission_bench.py \ + --benchmark-only -v -# Lancer les tests d'un répertoire spécifique -poetry run pytest tests/unit/kernel/ +# Exporter les résultats pour comparaison historique +poetry run pytest tests/benchmarks/ --benchmark-only \ + --benchmark-json=bench_$(date +%Y%m%d).json -# Voir la couverture de code -poetry run pytest --cov=xcore +# Benchmarks cache (batch vs séquentiel) +poetry run python tests/benchmarks/cache_batch_perf.py ``` +Résultats de référence : voir [Benchmarks](../reference/benchmarks.md). + --- -## Bonnes Pratiques +## Bonnes pratiques -1. **Utiliser des bases de données de test** : Configurez `DATABASE_URL` pour pointer vers une base SQLite en mémoire (`sqlite:///:memory:`) ou une base de test dédiée pour vos tests d'intégration. -2. **Nettoyage après test** : Assurez-vous de décharger les plugins (`unload`) après chaque test pour éviter les fuites de mémoire ou les conflits de namespace. -3. **Tester le mode Sandboxed** : Si votre plugin est destiné à être sandboxed, testez-le avec la commande `xcore sandbox run ` pour vérifier qu'aucune `PermissionError` n'est levée. +- Utiliser SQLite en mémoire pour les tests d'intégration : `url: "sqlite+aiosqlite:///:memory:"`. +- Toujours appeler `shutdown()` après `boot()` (utiliser une fixture `yield`). +- Pour les plugins sandboxed, tester avec `execution_mode: trusted` en tests unitaires, puis valider le sandbox avec `xcore sandbox run`. +- `asyncio_mode = auto` est activé — les tests `async def` sont automatiquement reconnus sans `@pytest.mark.asyncio` explicite. diff --git a/doc/getting-started/installation.md b/doc/getting-started/installation.md index f959df38..9b4485d0 100644 --- a/doc/getting-started/installation.md +++ b/doc/getting-started/installation.md @@ -1,136 +1,84 @@ # Installation -Ce guide vous accompagne dans l'installation de XCore et la configuration de votre environnement de développement. - ## Prérequis -Avant d'installer XCore, assurez-vous de disposer des éléments suivants : +- **Python 3.11+** +- **Poetry 2.x** ou **uv** +- **Redis 7+** (optionnel — requis uniquement si `cache.backend: redis` ou `scheduler.backend: redis`) +- **PostgreSQL 15+** / **MySQL 8+** (optionnel — SQLite fonctionne sans installation) -- **Python 3.11** ou une version supérieure. -- **Poetry 1.7+** (gestionnaire de dépendances et d'environnements virtuels). -- **Git** (pour cloner le dépôt). -- **PostgreSQL 15+** et **Redis 7+** (recommandés pour les fonctionnalités de base de données et de cache). +--- -### Vérification de la version de Python +## Depuis le dépôt source ```bash -python --version -# Doit afficher : Python 3.11.x ou plus -``` - -### Installation de Poetry - -Si vous n'avez pas encore installé Poetry : +git clone https://github.com/traoreera/xcore +cd xcore -```bash -# Sur macOS/Linux -curl -sSL https://install.python-poetry.org | python3 - +# Avec Poetry (recommandé) +poetry install -# Sur Windows (PowerShell) -(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py - +# Ou avec uv +uv sync ``` -Ajoutez Poetry à votre PATH si nécessaire : +### Lancer le serveur de développement ```bash -export PATH="$HOME/.local/bin:$PATH" -``` - -## Étapes d'installation - -### 1. Cloner le dépôt +# Via Poetry +poetry run uvicorn main:app --reload --port 8000 -```bash -git clone https://github.com/traoreera/xcore.git -cd xcore +# Via uv +uv run uvicorn main:app --reload --port 8000 ``` -### 2. Installer les dépendances +--- -Utilisez Poetry pour installer toutes les dépendances du projet, y compris celles de développement : +## En tant que dépendance (uv) ```bash -poetry install -``` - -Cette commande va : -- Créer un environnement virtuel isolé. -- Installer les bibliothèques requises (FastAPI, SQLAlchemy, Pydantic, etc.). -- Installer les outils de développement (Pytest, Black, etc.). - -### 3. Vérifier l'installation - -```bash -poetry run xcore --version -# Doit afficher : xcore v2.0.0 +uv add "xcore @ git+https://github.com/traoreera/xcore" ``` -## Configuration de l'environnement +--- -### Fichier de configuration `.env` +## Variables d'environnement -Copiez le fichier d'exemple et générez vos clés secrètes : +XCore charge automatiquement un fichier `.env` à la racine du projet si `app.dotenv` est configuré dans `xcore.yaml`. +Toute clé de configuration peut être surchargée via une variable d'environnement au format : -```bash -cp .env.example .env ``` - -Modifiez le fichier `.env` pour y inclure vos paramètres : - -```bash -# Clé secrète de l'application (utilisée pour signer les tokens/sessions) -APP_SECRET_KEY=votre_cle_secrete_longue_et_aleatoire - -# Clé de signature des plugins (utilisée pour valider les plugins Trusted) -PLUGIN_SECRET_KEY=votre_cle_de_signature_plugin - -# URLs de connexion aux services -DATABASE_URL=postgresql+psycopg2://user:pass@localhost:5432/xcore -REDIS_URL=redis://localhost:6379/0 +XCORE__
__=valeur ``` -### Référence des variables d'environnement principales - -| Variable | Requis | Description | -|----------|----------|-------------| -| `APP_SECRET_KEY` | Oui | Clé de sécurité principale du framework. | -| `PLUGIN_SECRET_KEY` | Oui | Clé utilisée pour la signature cryptographique des plugins. | -| `DATABASE_URL` | Non* | URL de connexion SQLAlchemy (si le service DB est utilisé). | -| `REDIS_URL` | Non* | URL de connexion Redis (si le service de cache/scheduler est utilisé). | -| `LOG_LEVEL` | Non | Niveau de log (DEBUG, INFO, WARNING, ERROR). | - -## Configuration de la Base de Données et du Cache - -### Docker (Option rapide) - -Si vous ne souhaitez pas installer PostgreSQL et Redis localement, vous pouvez utiliser Docker : +Exemples : ```bash -# Lancer PostgreSQL -docker run -d --name xcore-db -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=xcore postgres:15 - -# Lancer Redis -docker run -d --name xcore-cache -p 6379:6379 redis:7-alpine +XCORE__APP__DEBUG=true +XCORE__SERVICES__CACHE__BACKEND=redis +XCORE__SERVICES__CACHE__URL=redis://localhost:6379/0 ``` -## Lancement en mode Développement +--- -Pour lancer le serveur avec le rechargement à chaud (hot-reload) : +## Structure minimale d'un projet -```bash -# Utilisation de la commande make (recommandé) -make run-dev - -# Ou manuellement via uvicorn -poetry run uvicorn app:app --reload --port 8082 +``` +mon-projet/ +├── xcore.yaml # Configuration principale +├── main.py # Point d'entrée FastAPI +└── plugins/ + └── mon_plugin/ + ├── plugin.yaml + └── src/ + └── main.py ``` -Le serveur sera accessible sur `http://localhost:8082`. Vous pouvez vérifier son état en consultant l'endpoint de santé : +--- + +## Vérifier l'installation ```bash -curl http://localhost:8082/plugin/ipc/health +poetry run xcore --version +# xcore v2.1.2 ``` - -## Prochaines étapes - -Maintenant que XCore est opérationnel, passez au **[Guide de démarrage rapide](quickstart.md)** pour créer votre premier plugin. diff --git a/doc/getting-started/quickstart.md b/doc/getting-started/quickstart.md index 57a27602..69559fd9 100644 --- a/doc/getting-started/quickstart.md +++ b/doc/getting-started/quickstart.md @@ -1,334 +1,158 @@ -# Quick Start Guide +# Quick Start -Créez votre première application XCore et votre premier plugin en **moins de 5 minutes**. +Créez votre première application XCore et votre premier plugin en **5 minutes**. --- -## Vue d'Ensemble du Flux +## Étape 1 — Configuration -```mermaid -flowchart LR - A[1. Initialiser
l'Application] --> B[2. Créer
le Plugin] - B --> C[3. Démarrer
le Serveur] - C --> D[4. Tester
l'Action] +Créez `xcore.yaml` à la racine : - style A fill:#4CAF50,color:#fff - style B fill:#2196F3,color:#fff - style C fill:#FF9800,color:#fff - style D fill:#9C27B0,color:#fff -``` - ---- +```yaml +app: + name: "mon-app" + debug: true + secret_key: "change-me-in-production" + plugin_prefix: "/plugin" -## Étape 1 : Initialiser l'Application +plugins: + directory: "./plugins" + secret_key: "change-me-in-production" + strict_trusted: false -Créez un fichier `app.py` qui servira de point d'entrée à votre serveur FastAPI : +services: + databases: + db: + type: sqlasync + url: sqlite+aiosqlite:///./app.db -```python -# app.py -from fastapi import FastAPI -from xcore import Xcore + cache: + backend: memory + ttl: 300 -# 📦 Initialisation -app = FastAPI(title="Mon App XCore") -core = Xcore(config_path="xcore.yaml") - -# 🚀 Démarrage du kernel -@app.on_event("startup") -async def startup(): - await core.boot(app) - # Le kernel initialise : - # 1. ✅ Les services (DB, Cache, Scheduler) - # 2. ✅ Les plugins découverts - # 3. ✅ Les routes HTTP - -# 🛑 Arrêt propre -@app.on_event("shutdown") -async def shutdown(): - await core.shutdown() + scheduler: + enabled: false ``` -### Ce qui se passe au démarrage : +--- -```mermaid -sequenceDiagram - participant You as Vous - participant App as app.py - participant Kernel as XCore Kernel - participant Services as Services - participant Plugins as Plugins +## Étape 2 — Point d'entrée FastAPI - You->>App: uvicorn app:app - App->>Kernel: boot() +```python +# main.py +from contextlib import asynccontextmanager +from fastapi import FastAPI +from xcore import Xcore - Note over Kernel: Initialisation... - Kernel->>Services: DB, Cache, Scheduler - Kernel->>Plugins: Chargement +xcore = Xcore(config_path="xcore.yaml") - Services-->>Kernel: ✅ Prêts - Plugins-->>Kernel: ✅ Chargés +@asynccontextmanager +async def lifespan(app: FastAPI): + await xcore.boot(app) # démarre kernel + services + plugins + yield + await xcore.shutdown() # arrêt propre - Kernel-->>App: ✅ Prêt - App-->>You: 🚀 Serveur sur :8000 +app = FastAPI(title="Mon App XCore", lifespan=lifespan) ``` --- -## Étape 2 : Créer Votre Premier Plugin - -### A. Structure du Dossier +## Étape 3 — Premier plugin -```bash -# Créez l'arborescence -mkdir -p plugins/hello_plugin/src - -# Créez les fichiers -touch plugins/hello_plugin/plugin.yaml -touch plugins/hello_plugin/src/main.py ``` - -``` -plugins/hello_plugin/ -├── plugin.yaml # 📋 Manifeste (métadonnées) -└── src/ - └── main.py # 💡 Logique métier +plugins/ +└── hello/ + ├── plugin.yaml + └── src/ + └── main.py ``` -### B. Le Manifeste (`plugin.yaml`) +**`plugins/hello/plugin.yaml`** : ```yaml -# 📋 Identification -name: hello_plugin +name: hello version: "1.0.0" -author: Votre Nom -description: "Mon premier plugin XCore" - -# ⚙️ Configuration -execution_mode: trusted # trusted | sandboxed +author: moi +description: Plugin de démonstration +execution_mode: trusted entry_point: src/main.py -# 🔐 Permissions (optionnel) permissions: - resource: "cache.*" actions: ["read", "write"] effect: allow - -# 📊 Ressources (limites) -resources: - timeout_seconds: 30 - rate_limit: - calls: 100 - period_seconds: 60 ``` -### C. La Logique (`src/main.py`) +**`plugins/hello/src/main.py`** : ```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok, error +from xcore.kernel.api.contract import TrustedBase, ok, error +from xcore.sdk.decorators import AutoDispatchMixin, action, validate_payload +from pydantic import BaseModel + +class GreetPayload(BaseModel): + name: str class Plugin(AutoDispatchMixin, TrustedBase): - """ - Plugin de démonstration qui salue les utilisateurs. - """ async def on_load(self): - """🎯 Appelé quand le plugin est chargé.""" - self.logger.info("✨ Hello plugin est prêt !") - - async def on_unload(self): - """🧹 Appelé quand le plugin est déchargé.""" - self.logger.info("👋 Hello plugin est déchargé") + self.cache = self.get_service("cache") @action("greet") - async def greet_user(self, payload: dict): - """ - 🎯 Action: greet - - Args: - payload: {"name": "Votre nom"} + @validate_payload(GreetPayload) + async def greet(self, payload: GreetPayload) -> dict: + cached = await self.cache.get(f"greet:{payload.name}") + if cached: + return ok(message=cached, from_cache=True) - Returns: - {"status": "ok", "message": "Hello, X !"} - """ - name = payload.get("name", "World") - self.logger.info(f"Salutation de: {name}") - return ok(message=f"Hello, {name} !") + msg = f"Bonjour, {payload.name} !" + await self.cache.set(f"greet:{payload.name}", msg, ttl=60) + return ok(message=msg) @action("ping") - async def ping(self, payload: dict): - """Test de connectivité.""" - return ok(status="alive") + async def ping(self, payload: dict) -> dict: + return ok(pong=True) ``` --- -## Étape 3 : Démarrer et Tester - -### Démarrer le Serveur +## Étape 4 — Lancer et tester ```bash -# Dans le terminal 1 -uvicorn app:app --reload --host 0.0.0.0 --port 8000 -``` +# Démarrer +poetry run uvicorn main:app --reload -``` -INFO: Started server process [12345] -INFO: Waiting for application startup. -INFO: XCore Kernel booting... -INFO: Services initialized: db, cache, scheduler -INFO: Plugins loaded: hello_plugin (v1.0.0) -INFO: Application startup complete. -INFO: Uvicorn running on http://0.0.0.0:8000 -``` +# Tester via curl +curl -X POST http://localhost:8000/plugin/hello/action \ + -H "Content-Type: application/json" \ + -d '{"action": "greet", "payload": {"name": "Dev"}}' -### Tester via CLI - -```bash -# Dans le terminal 2 -xcore plugin call hello_plugin greet '{"name": "Développeur"}' -``` - -**Réponse :** -```json -{ - "status": "ok", - "message": "Hello, Développeur !" -} -``` - -### Tester via HTTP - -Si vous avez ajouté des routes HTTP : - -```bash -curl http://localhost:8000/plugin/hello_plugin/hello/Dev -``` - -### Flux Complet d'un Appel - -```mermaid -sequenceDiagram - participant CLI as xcore CLI - participant API as API HTTP - participant Kernel as XCore Kernel - participant Perm as Permission Engine - participant Plugin as hello_plugin - - CLI->>API: POST /plugins/call - API->>Kernel: plugins.call("hello_plugin", "greet") - - Kernel->>Perm: Vérifier permissions - Perm-->>Kernel: ✅ Autorisé - - Kernel->>Plugin: @action("greet") - Plugin->>Plugin: greet_user(payload) - Plugin-->>Kernel: {"status": "ok", ...} - - Kernel-->>API: Résultat - API-->>CLI: Response JSON +# Réponse +{"status": "ok", "message": "Bonjour, Dev !"} ``` --- -## Étape 4 : Aller Plus Loin - -### Ajouter une Route HTTP - -```python -from xcore.sdk import TrustedBase, RoutedPlugin, route - -class Plugin(RoutedPlugin, TrustedBase): - - @route("/hello/{name}", methods=["GET"]) - async def hello_api(self, name: str): - """Route HTTP accessible directement.""" - return {"message": f"Hello, {name} !"} - - @route("/status", methods=["GET"]) - async def status(self): - """Endpoint de santé du plugin.""" - return { - "plugin": self.name, - "version": self.version, - "status": "healthy" - } -``` - -**Routes disponibles :** -- `GET /plugin/hello_plugin/hello/{name}` -- `GET /plugin/hello_plugin/status` - -### Utiliser le Cache - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok - -class Plugin(AutoDispatchMixin, TrustedBase): - - async def on_load(self): - self.cache = self.get_service("cache") - - @action("greet_cached") - async def greet_cached(self, payload: dict): - name = payload.get("name", "World") - cache_key = f"greeting:{name}" +## Étape 5 — CLI - # Vérifier le cache - cached = await self.cache.get(cache_key) - if cached: - return ok(message=cached, source="cache") - - # Générer et caster - message = f"Hello, {name} !" - await self.cache.set(cache_key, message, ttl=60) - - return ok(message=message, source="generated") -``` - -### Émettre un Événement - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok - -class Plugin(AutoDispatchMixin, TrustedBase): - - @action("create_user") - async def create_user(self, payload: dict): - # ... logique de création ... +```bash +# Lister les plugins chargés +poetry run xcore plugin list - # 📡 Émettre un événement - await self.ctx.events.emit("user.created", { - "id": user_id, - "email": payload.get("email") - }) +# Inspecter un plugin +poetry run xcore plugin info hello - return ok(user_id=user_id) +# Recharger à chaud +poetry run xcore plugin reload hello ``` --- -## Résumé des Commandes - -| Commande | Description | -| :--- | :--- | -| `xcore plugin list` | Lister tous les plugins chargés | -| `xcore plugin load hello_plugin` | Charger un plugin | -| `xcore plugin reload hello_plugin` | Recharger à chaud | -| `xcore plugin unload hello_plugin` | Décharger un plugin | -| `xcore plugin call hello_plugin greet '{}'` | Appeler une action | -| `xcore health` | Vérifier la santé du système | - ---- - -## Prochaines Étapes - -| 📚 Guide | Objectif | -| :--- | :--- | -| [📖 SDK Reference](../reference/sdk.md) | Découvrir tous les décorateurs | -| [🛡️ Sécurité](../guides/security.md) | Comprendre le sandboxing | -| [📡 Événements](../guides/events.md) | Maîtriser le système d'événements | -| [💾 Services](../guides/services.md) | Utiliser DB et Cache | - ---- +## Prochaines étapes -> 💡 **Astuce** : Pour le développement, utilisez `execution_mode: trusted` pour un débogage plus facile. En production, passez à `sandboxed` pour une isolation maximale. +| Objectif | Doc | +|:---------|:----| +| Ajouter des routes HTTP au plugin | [Créer un plugin](../guides/creating-plugins.md) | +| Connecter une base de données | [Services](../guides/services.md) | +| Mettre en place la sécurité | [Sécurité](../guides/security.md) | +| Comprendre l'architecture | [Architecture](../architecture/overview.md) | diff --git a/doc/guides/events.md b/doc/guides/events.md index 494b0dca..c7b05937 100644 --- a/doc/guides/events.md +++ b/doc/guides/events.md @@ -1,567 +1,167 @@ -# Event & Hook System +# Événements & Hooks -XCore propose deux mécanismes de communication découplée : les **Événements** (asynchrones, side-effects) et les **Hooks** (synchrones, transformation de données). +XCore utilise un `EventBus` asynchrone pour la communication découplée entre le kernel et les plugins. --- -## Vue d'Ensemble Comparative - -```mermaid -flowchart LR - subgraph Events["📡 Événements (Async)"] - E1[Fire-and-forget] - E2[Handlers parallèles] - E3[Pas de retour] - E4[Side effects] - end - - subgraph Hooks["🪝 Hooks (Sync)"] - H1[Transformation] - H2[Séquentiel] - H3[Retour valeur] - H4[Inline logic] - end - - Events -.->|Découplage fort| Hooks - Hooks -.->|Transformation| Events - - style Events fill:#E3F2FD,stroke:#1976D2 - style Hooks fill:#FFF3E0,stroke:#F57C00 -``` - ---- - -## 1. Système d'Événements Asynchrones - -L'Event Bus est utilisé pour la communication **"fire-and-forget"**. Idéal pour déclencher des side effects (logging, emails, cache) après une action. - -### Architecture de l'Event Bus - -```mermaid -flowchart TB - subgraph Publisher["📤 Publishers"] - P1[Plugin A] - P2[Plugin B] - K[Kernel] - end - - subgraph EventBus["📡 Event Bus"] - direction TB - Q[Queue Async] - D[Dispatcher] - R[Registry Handlers] - end - - subgraph Subscriber["📥 Subscribers"] - S1[Handler 1] - S2[Handler 2] - S3[Handler 3] - end - - P1 --> EventBus - P2 --> EventBus - K --> EventBus - - EventBus --> S1 - EventBus --> S2 - EventBus --> S3 - - style EventBus fill:#E8F5E9,stroke:#388E3C -``` - -### Concepts Clés +## EventBus -| Concept | Description | Usage | -| :--- | :--- | :--- | -| **Priority** | Les handlers avec priorité haute (100) s'exécutent avant les bas (50) | Ordonnancer l'exécution | -| **Propagation** | Un handler peut appeler `event.stop_propagation()` | Stopper la chaîne | -| **Gather** | Par défaut, handlers exécutés en parallèle via `asyncio.gather` | Performance | - ---- - -### S'abonner à un Événement +### S'abonner à un événement ```python -from xcore.sdk import TrustedBase, on, Event - -class Plugin(TrustedBase): +from xcore.kernel.events.bus import EventBus - async def on_load(self): - # Méthode 1: Décorateur (recommandé) - # Le handler est automatiquement abonné +# Depuis un plugin — accès via self.ctx.events +async def on_load(self): + bus: EventBus = self.ctx.events - # Méthode 2: Subscription manuelle - self.ctx.events.on("user.created", self._on_user_created, priority=100) - - @on("user.created", priority=100) - async def _on_user_created(self, event: Event): - """ - Handler pour l'événement user.created - - Args: - event: Contient data, timestamp, source - """ - user_id = event.data.get("id") + @bus.on("user.created") + async def welcome_user(event): email = event.data.get("email") + await self._send_welcome(email) - self.logger.info(f"Nouvel utilisateur créé: {user_id}") - - # Side effect: envoyer email de bienvenue - await self._send_welcome_email(email) - - # Optionnel: stopper la propagation - # event.stop_propagation() - - async def _send_welcome_email(self, email: str): - # ... logique d'envoi ... - pass + # Handler one-shot (se désabonne après le premier appel) + @bus.once("system.ready") + async def on_ready(event): + await self._init_data() ``` -### Émettre un Événement +### Émettre un événement ```python -from xcore.sdk import TrustedBase, action, ok - -class Plugin(TrustedBase): - - @action("create_user") - async def create_user(self, payload: dict): - # 1. Créer l'utilisateur - user_id = await self._create_in_db(payload) +# Parallel (gather=True par défaut) — tous les handlers en parallèle +await self.ctx.events.emit("order.created", { + "order_id": 42, + "user_id": 7, + "amount": 99.90, +}) - # 2. Émettre l'événement - await self.ctx.events.emit("user.created", { - "id": user_id, - "email": payload.get("email"), - "created_at": self._now() - }) +# Séquentiel avec propagation contrôlable +await self.ctx.events.emit("payment.processing", data, gather=False) - # 3. Retourner résultat (l'événement est fire-and-forget) - return ok(user_id=user_id) +# Fire-and-forget depuis du code synchrone +self.ctx.events.emit_sync("metrics.tick", {"ts": time.time()}) ``` -### Flux d'un Événement - -```mermaid -sequenceDiagram - participant P as Plugin Publisher - participant EB as Event Bus - participant H1 as Handler 1 (priority=100) - participant H2 as Handler 2 (priority=50) - participant H3 as Handler 3 (priority=50) - - P->>EB: emit("user.created", data) - - Note over EB: Registry handlers par priority - EB->>H1: _on_user_created(event) - - par Exécution parallèle (asyncio.gather) - EB->>H2: handler_2(event) - EB->>H3: handler_3(event) - end - - H1-->>EB: ✅ Done - H2-->>EB: ✅ Done - H3-->>EB: ✅ Done - - EB-->>P: ✅ Tous handlers exécutés - - Note over P,H3: ⚠️ Le publisher n'attend pas
les résultats des handlers -``` - ---- - -## 2. Événements Système Built-in - -XCore émet plusieurs événements système que vous pouvez subscriber. - -### Tableau des Événements Système - -| Événement | Quand | Payload | Usage Typique | -| :--- | :--- | :--- | :--- | -| `xcore.plugins.booted` | Tous plugins chargés | `{"report": {...}}` | Initialisation globale | -| `plugin..loaded` | Plugin chargé | `{"name", "version"}` | Logging, metrics | -| `plugin..reloaded` | Plugin rechargé | `{"name"}` | Refresh cache | -| `plugin..unloaded` | Plugin déchargé | `{"name"}` | Cleanup | -| `permission.deny` | Permission refusée | `{"plugin", "resource", "action"}` | Audit sécurité | -| `security.violation` | Scan AST échoué | `{"plugin", "errors"}` | Alertes sécurité | -| `user.created` | Custom: utilisateur créé | `{"id", "email"}` | Welcome email | -| `user.deleted` | Custom: utilisateur supprimé | `{"id"}` | Cleanup données | - -### Exemple: Subscriber aux Événements Système +### Wildcards ```python -from xcore.sdk import TrustedBase, on, Event - -class Plugin(TrustedBase): - - @on("xcore.plugins.booted", priority=200) - async def _on_system_boot(self, event: Event): - """Exécuté quand TOUS les plugins sont chargés.""" - report = event.data.get("report", {}) - loaded_count = report.get("loaded", 0) - self.logger.info(f"🚀 Système prêt : {loaded_count} plugins chargés") - - @on("plugin.auth_plugin.loaded", priority=100) - async def _on_auth_ready(self, event: Event): - """Exécuté quand le plugin auth est prêt.""" - self.logger.info("✅ Plugin auth disponible") - # Initialiser la connexion avec le plugin auth - - @on("permission.deny", priority=50) - async def _on_permission_denied(self, event: Event): - """Logging de toutes les permissions refusées.""" - plugin = event.data.get("plugin") - resource = event.data.get("resource") - self.logger.warning(f"🚫 Permission refusée: {plugin} → {resource}") -``` +# S'abonner à tous les événements "order.*" +@bus.on("order.*") +async def handle_all_orders(event): + logger.info(f"Ordre: {event.name} — {event.data}") ---- - -## 3. Best Practices pour les Événements - -```mermaid -flowchart TB - subgraph Do["✅ Bonnes Pratiques"] - D1[Handlers légers] - D2[Priorités claires] - D3[Cleanup handlers] - D4[Événements nommés] - end - - subgraph Dont["❌ Erreurs à Éviter"] - A1[Tâches lourdes] - A2[Priorités magiques] - A3[Memory leaks] - A4[Noms obscurs] - end - - Do -.->|Au lieu de| Dont - - style Do fill:#E8F5E9,stroke:#388E3C - style Dont fill:#FFEBEE,stroke:#C62828 +# S'abonner à tous les événements +@bus.on("*") +async def log_all(event): + logger.debug(f"Event: {event.name}") ``` -### 1. Handlers Légers +### Priorités -```python -# ❌ MAUVAIS: Handler lourd (bloque l'event loop) -@on("user.created") -async def bad_handler(self, event): - # Envoi email synchrone (lent !) - send_email_sync(event.data["email"]) - - # Traitement lourd - for i in range(1000000): - process(i) - -# ✅ BON: Déléguer au scheduler -@on("user.created") -async def good_handler(self, event): - # Quick: queue la tâche - await self.scheduler.add_job( - self._send_welcome_email, - args=[event.data["email"]] - ) -``` - -### 2. Priorités Explicites +Les handlers sont appelés par ordre de priorité décroissante (défaut : 50). ```python -# ❌ MAUVAIS: Priorités arbitraires -@on("user.created", priority=42) -@on("user.created", priority=99) - -# ✅ BON: Priorités sémantiques -PRIORITY_LOGGING = 100 # Logging en premier -PRIORITY_BUSINESS = 50 # Logique métier -PRIORITY_CLEANUP = 10 # Cleanup en dernier +@bus.on("request.incoming", priority=100) +async def auth_check(event): + # Exécuté en premier + ... -@on("user.created", priority=PRIORITY_LOGGING) -@on("user.created", priority=PRIORITY_BUSINESS) +@bus.on("request.incoming", priority=10) +async def log_request(event): + # Exécuté en dernier + ... ``` -### 3. Nommage des Événements +### Introspection ```python -# ❌ MAUVAIS: Noms obscurs -await self.ctx.events.emit("stuff.happened", data) -await self.ctx.events.emit("do_thing", data) - -# ✅ BON: Noms sémantiques (domain.event) -await self.ctx.events.emit("user.created", data) -await self.ctx.events.emit("user.deleted", data) -await self.ctx.events.emit("order.completed", data) -``` +# Lister tous les événements abonnés +events = bus.list_events() +# {"user.created": ["welcome_user", "audit_log"], "order.*": ["handle_all_orders"]} -### 4. Stop Propagation (Usage Avancé) +# Nombre de handlers pour un événement +count = bus.handler_count("user.created") -```python -from xcore.sdk import TrustedBase, on, Event - -class Plugin(TrustedBase): - - @on("user.created", priority=100) - async def validate_user(self, event: Event): - """Validation en premier, peut bloquer les autres handlers.""" - if not self._is_valid(event.data): - self.logger.error("Utilisateur invalide") - event.stop_propagation() # ⚠️ Stop les handlers suivants - return - - @on("user.created", priority=50) - async def send_welcome(self, event: Event): - """Ne s'exécute que si validation passe.""" - if event.propagation_stopped: - self.logger.debug("Propagation stoppée, skip welcome") - return - - # Envoyer email... -``` +# Désabonner un handler +bus.unsubscribe("user.created", welcome_user) ---- - -## 4. Hooks Synchrones - -Les hooks permettent de modifier des données ou d'exécuter de la logique synchrone pendant un processus spécifique. - -### Architecture du HookManager - -```mermaid -flowchart LR - subgraph Filters["🔄 Filters (Transformation)"] - F1[Value] --> F2[Hook 1] - F2 --> F3[Hook 2] - F3 --> F4[Hook 3] - F4 --> F5[Modified Value] - end - - subgraph Actions["⚡ Actions (Side Effects)"] - A1[Trigger] --> A2[Hook 1] - A2 --> A3[Hook 2] - A3 --> A4[Hook 3] - A4 --> A5[Done] - end - - style Filters fill:#E3F2FD,stroke:#1976D2 - style Actions fill:#FFF3E0,stroke:#F57C00 -``` - -### A. Filters (Transformation de Données) - -Les filters permettent de "faire passer une valeur dans une chaîne" pour laisser d'autres plugins la modifier. - -```mermaid -flowchart LR - V[Valeur Originale] --> H1[Hook 1] - H1 -->|Modifie| H2[Hook 2] - H2 -->|Modifie| H3[Hook 3] - H3 -->|Modifie| R[Valeur Finale] - - style V fill:#E8F5E9 - style R fill:#C8E6C9 +# Vider le bus +bus.clear() # tout +bus.clear("user.created") # un événement spécifique ``` -```python -# Côté Kernel ou Plugin -class Plugin(TrustedBase): - - async def render_page(self): - # 1. Valeur de base - title = "Bienvenue sur XCore" +--- - # 2. Appliquer les filters - title = self.ctx.hooks.apply_filters("page_title", title) +## Événements système - # title peut maintenant être modifié par d'autres plugins - # Ex: "Bienvenue sur XCore | Dashboard" +XCore émet des événements internes auxquels les plugins peuvent s'abonner : - return f"

{title}

" +| Événement | Données | Déclenchement | +|:----------|:--------|:--------------| +| `plugin.loaded` | `{"name": str, "version": str}` | Après chargement réussi | +| `plugin.unloaded` | `{"name": str}` | Après déchargement | +| `plugin.failed` | `{"name": str, "error": str}` | Après échec de chargement | +| `plugin.reloaded` | `{"name": str}` | Après rechargement à chaud | +| `service.ready` | `{"name": str}` | Après init d'un service | +| `system.shutdown` | `{}` | Avant arrêt du kernel | - # 3. Enregistrer un filter - @filter("page_title") - def modify_title(self, title: str) -> str: - """Ajoute le suffixe au titre.""" - return f"{title} | Mon App" -``` +--- -### B. Actions (Side Effects Sync) +## HookManager -Les actions sont des side effects synchrones qui ne retournent pas de valeur. +Les hooks permettent d'intercepter des points précis du cycle de vie sans passer par le bus. ```python -# Côté Kernel ou Plugin -class Plugin(TrustedBase): - - async def render_template(self, template_name: str): - # 1. Trigger action avant rendu - self.ctx.hooks.do_action("before_render", template=template_name) - - # 2. Rendu du template - result = self._render(template_name) +# Accès via self.ctx.hooks +hooks = self.ctx.hooks - # 3. Trigger action après rendu - self.ctx.hooks.do_action("after_render", template=template_name, result=result) +# Enregistrer un hook avant une action +@hooks.before("plugin.call") +async def before_call(context): + context["start_time"] = time.monotonic() - return result - - # 4. Enregistrer un action hook - @action_hook("before_render") - def on_before_render(self, template: str): - """Log chaque rendu de template.""" - self.logger.debug(f"Rendu du template: {template}") +# Enregistrer un hook après une action +@hooks.after("plugin.call") +async def after_call(context): + duration = time.monotonic() - context.get("start_time", 0) + logger.info(f"Appel terminé en {duration:.3f}s") ``` --- -## 5. Différence Entre Events et Hooks - -| Caractéristique | Events | Hooks | -| :--- | :--- | :--- | -| **Exécution** | Asynchrone (`async def`) | Synchrone (`def`) | -| **Retourne Valeur** | ❌ Non | ✅ Oui (Filters) | -| **Parallèle** | ✅ Oui (`asyncio.gather`) | ❌ Non (Séquentiel) | -| **Propagation** | ✅ `stop_propagation()` | ❌ N/A | -| **Priorités** | ✅ Oui | ⚠️ Ordre d'enregistrement | -| **Use Case** | Side effects découplés | Transformation de données | - -### Quand Utiliser Quoi ? - -```mermaid -flowchart TB - Start{Besoin ?} - - Start -->|Modifier données| Hooks - Start -->|Side effect| Events - - Start -->|Synchrone requis| Hooks - Start -->|Async OK| Events - - Start -->|Plusieurs listeners| Events - Start -->|Chaîne transformation| Hooks - - Hooks --> H[🪝 Hooks] - Events --> E[📡 Events] - - style H fill:#FFF3E0,stroke:#F57C00 - style E fill:#E3F2FD,stroke:#1976D2 -``` +## Patterns communs -### Exemple Comparatif +### Communication entre plugins via événements ```python -# 📡 EVENT: Pour side effect découplé -@on("user.created") -async def send_welcome_email(self, event): - # Envoi email async (ne modifie pas les données) - await self.email_service.send(event.data["email"]) - -# 🪝 HOOK FILTER: Pour modifier des données -@filter("user.profile_data") -def add_premium_badge(self, profile_data: dict) -> dict: - # Modifie les données avant retour - if self._is_premium(user_id): - profile_data["badge"] = "premium" - return profile_data - -# 🪝 HOOK ACTION: Pour side effect synchrone -@action_hook("before_user_save") -def log_user_change(self, user_data: dict): - # Logging synchrone - self.logger.debug(f"User update: {user_data}") -``` - ---- - -## 6. Exemple Complet: Système de Notification +# Plugin A — émet +@action("create_order") +async def create_order(self, payload: dict) -> dict: + order = await self._save_order(payload) + await self.ctx.events.emit("order.created", { + "order_id": order.id, + "user_id": payload["user_id"], + "total": order.total, + }) + return ok(order_id=order.id) + +# Plugin B — réagit +async def on_load(self): + @self.ctx.events.on("order.created") + async def send_confirmation(event): + await self._send_email(event.data["user_id"], event.data["order_id"]) +``` + +### Invalidation de cache sur événement ```python -from xcore.sdk import TrustedBase, on, action, ok, filter - -class NotificationPlugin(TrustedBase): - """ - Plugin de notification qui démontre Events + Hooks. - """ - - async def on_load(self): - # S'abonner aux événements système - self.ctx.events.on("user.created", self._on_user_created, priority=50) - self.ctx.events.on("order.completed", self._on_order_completed, priority=50) +async def on_load(self): + self.cache = self.get_service("cache") - @on("user.created") - async def _on_user_created(self, event): - """Envoyer email de bienvenue.""" - email = event.data.get("email") - await self._send_email(email, "Bienvenue !") - - @on("order.completed") - async def _on_order_completed(self, event): - """Envoyer confirmation de commande.""" - email = event.data.get("email") - await self._send_email(email, "Commande confirmée !") - - @filter("notification.message") - def add_disclaimer(self, message: str) -> str: - """Ajouter un disclaimer à tous les messages.""" - return f"{message}\n\n-- Ceci est un message automatique" - - @action("send_notification") - async def send_notification(self, payload: dict): - """Action publique pour envoyer une notification.""" - # Appliquer les filters - message = self.ctx.hooks.apply_filters( - "notification.message", - payload.get("message", "") - ) - - # Émettre un événement pour logging - await self.ctx.events.emit("notification.sent", { - "to": payload.get("to"), - "message": message - }) - - return ok(status="sent") + @self.ctx.events.on("user.updated") + async def invalidate_user_cache(event): + user_id = event.data.get("user_id") + await self.cache.delete(f"user:{user_id}") + await self.cache.delete(f"profile:{user_id}") ``` - ---- - -## 7. Debugging et Monitoring - -### Voir les Handlers Enregistrés - -```bash -# Lister tous les handlers d'événements -xcore events list - -# Lister tous les hooks -xcore hooks list -``` - -### Tracing des Événements - -```python -# Activer le debug logging -# xcore.yaml -logging: - level: DEBUG - events: true # Log tous les événements émis -``` - -### Diagnostic des Problèmes - -| Problème | Cause Possible | Solution | -| :--- | :--- | :--- | -| Handler non appelé | Mauvais nom d'événement | Vérifier le nom exact | -| Handler appelé trop tard | Priorité trop basse | Augmenter priority | -| Memory leak | Handler non unsubscribed | Unsubscribe dans `on_unload` | -| Event loop bloquée | Handler trop lourd | Déléguer au scheduler | - ---- - -## Prochaines Lectures - -| 📚 Guide | Objectif | -| :--- | :--- | -| [Creating Plugins](creating-plugins.md) | Développer des plugins | -| [Services](services.md) | Utiliser les services partagés | -| [Monitoring](monitoring.md) | Observer les événements en prod | diff --git a/doc/guides/monitoring.md b/doc/guides/monitoring.md index 0f1df4e4..f9d9ed9c 100644 --- a/doc/guides/monitoring.md +++ b/doc/guides/monitoring.md @@ -1,140 +1,178 @@ -# Surveillance et Observabilité XCore +# Monitoring & Observabilité -XCore intègre des outils de surveillance pour vous aider à comprendre les performances de vos plugins et à diagnostiquer les problèmes en temps réel. +XCore embarque logging structuré, métriques et tracing. Tous sont configurables via `xcore.yaml`. --- -## 1. Journaux de Bord (Logs) - -Les logs sont structurés pour faciliter le débogage. Chaque plugin possède son propre logger, préfixé par son nom. - -### Niveaux de logs configurables - -Modifiez le niveau de détail dans `xcore.yaml` : +## Configuration ```yaml observability: logging: - level: "DEBUG" # INFO, WARNING, ERROR, DEBUG + level: "INFO" # DEBUG | INFO | WARNING | ERROR format: "%(asctime)s [%(levelname)s] %(name)s: %(message)s" - file: "logs/xcore.log" # Sortie vers un fichier (optionnel) -``` + file: "log/app.log" # null = stdout uniquement + max_bytes: 10485760 # 10 MB + backup_count: 5 -### Utiliser le logger dans un plugin + metrics: + enabled: true + backend: memory # memory | prometheus | statsd + prefix: "xcore" -```python -class MonPlugin(TrustedBase): - async def on_load(self): - self.logger.info("Démarrage du plugin...") - self.logger.debug("Données de configuration : %s", self.ctx.config) + tracing: + enabled: false + backend: noop # noop | opentelemetry | jaeger + service_name: "xcore" + endpoint: null # ex: http://jaeger:14268/api/traces ``` --- -## 2. Métriques (Metrics) +## Logging -Le `MetricsRegistry` collecte des compteurs, des jauges et des histogrammes pour le framework et ses plugins. +### Dans un plugin -### Consulter les métriques +```python +from xcore.kernel.observability import get_logger -```bash -# Via la CLI -xcore health +class Plugin(TrustedBase): -# Via l'API HTTP (Snapshot JSON) -curl http://localhost:8082/plugin/ipc/metrics + async def on_load(self): + self.logger = get_logger(f"plugin.{self.ctx.name}") + + @action("process") + async def process(self, payload: dict) -> dict: + self.logger.info("Traitement démarré", extra={"payload_size": len(payload)}) + try: + result = await self._do_work(payload) + self.logger.debug("Succès", extra={"result_keys": list(result.keys())}) + return ok(**result) + except Exception as e: + self.logger.error(f"Échec: {e}", exc_info=True) + return error(str(e), "processing_error") ``` -### Ajouter des métriques personnalisées +### Configuration du niveau par module ```python -class MonPlugin(TrustedBase): - async def on_load(self): - # Création d'un compteur personnalisé - self.metrics.counter("processed_items_total", description="Nombre total d'items traités") - - async def process(self, item): - # Incrémenter le compteur - self.metrics.inc("processed_items_total", labels={"type": item.type}) +import logging +logging.getLogger("xcore.runtime").setLevel(logging.DEBUG) +logging.getLogger("xcore.permissions").setLevel(logging.WARNING) ``` --- -## 3. Traces Distribuées (Tracing) +## Métriques -XCore supporte le tracing via **OpenTelemetry** pour suivre le cheminement d'une requête à travers plusieurs plugins. +```python +from xcore.kernel.observability import MetricsRegistry, Counter, Gauge, Histogram -### Configuration du Tracing +class Plugin(TrustedBase): -```yaml -observability: - tracing: - enabled: true - backend: "jaeger" # noop, opentelemetry, jaeger - endpoint: "http://localhost:14268/api/traces" - service_name: "xcore-production" + async def on_load(self): + self.metrics: MetricsRegistry = self.ctx.metrics + + # Compteur d'appels + self.requests = self.metrics.counter( + "plugin_requests_total", + labels={"plugin": "mon_plugin"}, + ) + # Gauge — valeur instantanée + self.active_sessions = self.metrics.gauge("active_sessions") + + # Histogram — distribution de latences + self.latency = self.metrics.histogram( + "request_duration_seconds", + buckets=[0.01, 0.05, 0.1, 0.5, 1.0], + ) + + @action("process") + async def process(self, payload: dict) -> dict: + import time + start = time.monotonic() + self.requests.inc() + + result = await self._work(payload) + + self.latency.observe(time.monotonic() - start) + return ok(**result) ``` -### Utiliser le Tracer dans un plugin +--- + +## Tracing ```python -class MonPlugin(TrustedBase): - async def handle(self, action, payload): - with self.tracer.start_span(f"action_{action}") as span: - span.set_attribute("payload_size", len(payload)) - # ... logique métier ... - return ok() +from xcore.kernel.observability import Tracer, Span + +class Plugin(TrustedBase): + + async def on_load(self): + self.tracer: Tracer = self.ctx.tracer + + @action("complex_operation") + async def complex_operation(self, payload: dict) -> dict: + with self.tracer.start_span("complex_operation") as span: + span.set_attribute("user_id", payload.get("user_id")) + + with self.tracer.start_span("db_query") as db_span: + result = await self._query_db(payload) + db_span.set_attribute("rows_returned", len(result)) + + return ok(data=result) ``` --- -## 4. Bilan de Santé (Health Check) +## Health Checks -Le `HealthChecker` centralise l'état de santé de tous les composants : -- **Framework** (Boot, Registry). -- **Services** (DB, Cache, Scheduler). -- **Plugins** (Statut, Sandboxes). - -### Consulter l'état global +```python +from xcore.kernel.observability import HealthChecker, HealthStatus -```bash -# Rapport détaillé (CLI) -xcore health +class Plugin(TrustedBase): -# Rapport global (JSON) -xcore health --json + async def on_load(self): + health: HealthChecker = self.ctx.health + + # Enregistrer un check custom + health.register("mon_plugin.db", self._check_db) + health.register("mon_plugin.external_api", self._check_api) + + async def _check_db(self) -> HealthStatus: + try: + async with self.db.session() as s: + await s.execute("SELECT 1") + return HealthStatus(healthy=True, message="DB OK") + except Exception as e: + return HealthStatus(healthy=False, message=str(e)) ``` -### Ajouter un Health Check personnalisé dans un plugin +```bash +# Vérifier l'état depuis le CLI +poetry run xcore health +poetry run xcore health --json -```python -class MonPlugin(TrustedBase): - async def on_init(self): - # Enregistrer un check périodique - self.ctx.health.register_check("api_connection", self.check_api) - - async def check_api(self): - # Retourne (bool, message) - is_up = await ping_api() - return is_up, "API accessible" if is_up else "API hors ligne" +# État des services uniquement +poetry run xcore services status ``` --- -## 5. Middleware de Surveillance (Performance) +## Logs en temps réel -Le `MiddlewarePipeline` mesure automatiquement le temps d'exécution de chaque appel IPC vers un plugin. Si un appel dépasse un certain seuil, un warning est émis dans les logs. +Les logs structurés sont écrits dans `log/app.log` (configurable). Pour les filtrer : -- **Métriques automatiques** : `plugin_call_duration_seconds`, `plugin_call_total`. -- **Labels** : `plugin_name`, `action`, `status` (ok/error). +```bash +# Tous les logs en live +tail -f log/app.log ---- +# Erreurs uniquement +grep '"level":"ERROR"' log/app.log | jq . -## Résumé +# Logs d'un plugin spécifique +grep 'plugin.mon_plugin' log/app.log -| Outil | Usage Principal | Configuration | -|-------|-----------------|---------------| -| **Logs** | Débogage détaillé | `logging` | -| **Metrics** | Tableaux de bord (Prometheus) | `metrics` | -| **Tracing** | Analyse de latence (Jaeger) | `tracing` | -| **Health** | Monitoring de disponibilité | Intégré | +# Logs de permissions +grep 'xcore.permissions' log/app.log +``` diff --git a/doc/guides/security.md b/doc/guides/security.md index 3ba8354e..21caf1a9 100644 --- a/doc/guides/security.md +++ b/doc/guides/security.md @@ -1,639 +1,214 @@ -# Security Deep Dive +# Sécurité -XCore suit un modèle **"Zero Trust"** pour les plugins. Même les plugins trusted sont restreints de modifier l'état du kernel, tandis que les plugins sandboxed sont isolés aux niveaux OS et Runtime. +XCore implémente une sécurité en profondeur (defense in depth) à plusieurs couches. --- -## Vue d'Ensemble de la Sécurité - -```mermaid -flowchart TB - subgraph Defense["Défense en Profondeur"] - direction TB - L1[🛡️ Couche 1: AST Scan
Analyse statique] - L2[🛡️ Couche 2: Process Isolation
Sandbox OS] - L3[🛡️ Couche 3: Resource Limits
Memory/CPU] - L4[🛡️ Couche 4: Permissions
Policy Engine] - L5[🛡️ Couche 5: Signature
HMAC Verification] - end - - subgraph Attaques["Attaques Bloquées"] - A1[Code Injection] - A2[Sandbox Escape] - A3[DoS Resource] - A4[Accès Non Autorisé] - A5[Tampering] - end - - L1 -.->|Bloque| A1 - L2 -.->|Bloque| A2 - L3 -.->|Bloque| A3 - L4 -.->|Bloque| A4 - L5 -.->|Bloque| A5 - - style Defense fill:#E8F5E9,stroke:#388E3C - style L1 fill:#C8E6C9 - style L2 fill:#C8E6C9 - style L3 fill:#C8E6C9 - style L4 fill:#C8E6C9 - style L5 fill:#C8E6C9 -``` - ---- - -## 1. Le Multi-Layer Sandbox - -Quand `execution_mode: sandboxed` est défini, le kernel applique **3 couches de protection** successives. - -### Vue d'Ensemble du Flux - -```mermaid -flowchart LR - subgraph PreExec["Pré-Exécution"] - A[Code Source] --> B[🔍 AST Scanner] - B --> C{Scan OK ?} - C -->|Non| D[❌ Rejeté] - C -->|Oui| E[📦 Spawn Worker] - end - - subgraph Exec["Exécution"] - E --> F[📡 JSON-RPC IPC] - F --> G[⏱️ Timeout Monitor] - G --> H[🧠 Memory Monitor] - end - - subgraph PostExec["Post-Exécution"] - H --> I{Limites OK ?} - I -->|Non| J[❌ Kill Worker] - I -->|Oui| K[✅ Résultat] - end +## 1. Moteur de Permissions - style PreExec fill:#FFF3E0,stroke:#F57C00 - style Exec fill:#E3F2FD,stroke:#1976D2 - style PostExec fill:#E8F5E9,stroke:#388E3C -``` - ---- - -### Couche 1: Analyse Statique (AST) - -Avant l'exécution, l'`ASTScanner` parse l'arbre syntaxique du plugin avec un `ast.NodeVisitor`. - -```mermaid -flowchart TB - subgraph AST["🔍 AST Scanner"] - S[Code Source] --> P[Parser AST] - P --> N[NodeVisitor] - - N --> F1[Forbidden Names] - N --> F2[Forbidden Attributes] - N --> F3[Import Whitelist] - - F1 --> B1[❌ eval, exec] - F1 --> B2[❌ globals, locals] - F1 --> B3[❌ __import__] - - F2 --> B4[❌ __class__] - F2 --> B5[❌ __globals__] - F2 --> B6[❌ __subclasses__] - - F3 --> B7[❌ os, subprocess] - F3 --> B8[❌ socket, requests] - end - - style AST fill:#FFEBEE,stroke:#C62828 - style B1 fill:#FFCDD2 - style B2 fill:#FFCDD2 - style B3 fill:#FFCDD2 - style B4 fill:#FFCDD2 - style B5 fill:#FFCDD2 - style B6 fill:#FFCDD2 - style B7 fill:#FFCDD2 - style B8 fill:#FFCDD2 -``` - -#### Liste Complète des Restrictions - -```python -# ❌ Forbidden Names (bloqués par l'AST) -FORBIDDEN_NAMES = { - 'eval', 'exec', 'compile', - 'globals', 'locals', 'vars', - '__import__', 'open', 'input', - 'breakpoint', 'delattr', 'setattr' -} - -# ❌ Forbidden Attributes (sandboxes escapes courants) -FORBIDDEN_ATTRIBUTES = { - '__class__', '__bases__', '__subclasses__', - '__globals__', '__code__', '__closure__', - '__builtins__', '__import__', 'gi_frame', - 'cr_frame', 'f_globals', 'f_locals' -} - -# ❌ Forbidden Modules (nécessite allowed_imports) -FORBIDDEN_MODULES = { - 'os', 'sys', 'subprocess', 'multiprocessing', - 'socket', 'requests', 'urllib', 'http', - 'pickle', 'marshal', 'shelve' -} -``` +Chaque plugin déclare ses accès dans `plugin.yaml`. Le `PermissionEngine` évalue les requêtes inter-plugins à chaque appel. -#### Exemple de Configuration +### Déclaration des permissions ```yaml # plugin.yaml -# Pour utiliser des modules normalement restreints -allowed_imports: - - datetime # ✅ Autorisé - - json # ✅ Autorisé - - hashlib # ✅ Autorisé (si besoin) - - re # ✅ Autorisé - -# Tentative d'import bloqué : -# import os # ❌ Erreur: Module 'os' non autorisé -``` - ---- - -### Couche 2: Isolation Processus - -Les plugins sandboxed s'exécutent dans un **processus OS dédié** via le module `multiprocessing`. - -```mermaid -sequenceDiagram - participant K as 🎯 Kernel Process - participant M as 📡 Multiprocessing Manager - participant W as 📦 Sandbox Worker - - Note over K: Boot du kernel - K->>M: spawn_worker(plugin_path) - - Note over M: Création processus - M->>W: fork()/spawn() - W->>W: Charger code plugin - - Note over K,W: Communication via pipes - K->>W: {"jsonrpc":"2.0","method":"call",...} - Note over K,W: JSON uniquement
⚠️ Pas de pickle ! - - W->>W: Exécuter handler - W->>K: {"jsonrpc":"2.0","result":...} - - Note over K: Désérialiser résultat -``` - -#### Transport: JSON-RPC 2.0 sur Pipes OS - -```python -# Format des messages Kernel → Worker -{ - "jsonrpc": "2.0", - "id": "uuid-1234-5678", - "method": "call", - "params": { - "action": "greet", - "payload": {"name": "World"} - } -} - -# Format des messages Worker → Kernel -{ - "jsonrpc": "2.0", - "id": "uuid-1234-5678", - "result": {"status": "ok", "message": "Hello !"} -} - -# Ou en cas d'erreur -{ - "jsonrpc": "2.0", - "id": "uuid-1234-5678", - "error": { - "code": -32000, - "message": "Action not found" - } -} -``` - -#### Pourquoi JSON-RPC ? - -| Avantage | Description | -| :--- | :--- | -| **Sérialisation sûre** | Seulement types JSON (pas de `pickle` → pas de RCE) | -| **Langage-agnostique** | Pourrait être implémenté dans tout langage | -| **Debug-friendly** | Messages lisibles en clair | -| **Standardisé** | Spécification JSON-RPC 2.0 | - ---- - -### Couche 3: Limites de Ressources OS - -Le kernel monitorle RSS (Resident Set Size) et le temps CPU du worker. - -```mermaid -flowchart LR - subgraph Monitors["📊 Resource Monitors"] - MM[🧠 Memory Monitor
RSS Tracker] - TM[⏱️ Timeout Monitor
Execution Timer] - CM[📈 CPU Monitor
CPU Time Tracker] - end - - subgraph Limits["⚠️ Limites"] - ML[Max: max_memory_mb] - TL[Max: timeout_seconds] - CL[Max: cpu_seconds] - end - - subgraph Actions["🔥 Actions"] - MA[⚠️ Warning] - KA[❌ Kill Process] - RA[📝 Log Violation] - end - - MM --> ML --> MA --> KA - TM --> TL --> KA - CM --> CL --> KA - - style Monitors fill:#E3F2FD,stroke:#1976D2 - style Limits fill:#FFF3E0,stroke:#F57C00 - style Actions fill:#FFEBEE,stroke:#C62828 -``` - -#### Configuration des Limites - -```yaml -# plugin.yaml -resources: - # Mémoire maximale (Mo) - max_memory_mb: 256 - - # Timeout d'exécution (secondes) - timeout_seconds: 30 - - # Rate limiting - rate_limit: - calls: 1000 - period_seconds: 60 -``` - -#### Comportement en Cas de Dépassement - -| Ressource | Limite | Comportement | -| :--- | :--- | :--- | -| **Mémoire** | `max_memory_mb` | Warning à 80%, Kill à 100% | -| **Timeout** | `timeout_seconds` | Kill immédiat + erreur timeout | -| **Rate Limit** | `calls/period` | Erreur 429 Too Many Requests | - ---- - -## 2. Permission Engine - -Chaque appel inter-plugin et accès service est audité par le **PermissionEngine**. - -### Modèle d'Évaluation - -```mermaid -flowchart TB - subgraph Evaluation["🔍 Évaluation Permission"] - Start[Début Évaluation] - CheckDeny[Vérifier DENY explicite ?] - CheckAllow[Vérifier ALLOW explicite ?] - DefaultDeny[❌ DENY par défaut] - Allow[✅ ALLOW] - Deny[❌ DENY] - end - - Start --> CheckDeny - CheckDeny -->|Oui| Deny - CheckDeny -->|Non| CheckAllow - CheckAllow -->|Oui| Allow - CheckAllow -->|Non| DefaultDeny - - style Deny fill:#FFCDD2,stroke:#C62828 - style Allow fill:#C8E6C9,stroke:#388E3C - style DefaultDeny fill:#FFCDD2,stroke:#C62828 -``` - -### Modèle "Fail-Closed" - -XCore utilise un modèle **"Fail-Closed"** : si aucune policy n'autorise explicitement une action, elle est **refusée**. - -``` -1. Vérifier DENY explicite → Si trouvé: ❌ REFUSÉ -2. Vérifier ALLOW explicite → Si trouvé: ✅ AUTORISÉ -3. Aucun match → ❌ REFUSÉ (défaut) -``` - -### Patterns de Permissions - -#### Wildcards sur les Ressources - -```yaml permissions: - # ✅ Autorise lecture sur TOUTES les tables 'users_*' + # Autoriser la lecture/écriture sur toutes les tables users - resource: "db.users.*" - actions: ["read"] - effect: allow - - # ✅ Autorise lecture/écriture sur cache global - - resource: "cache.global" actions: ["read", "write"] effect: allow - # ✅ Autorise TOUS les actions sur une resource - - resource: "logs.*" - actions: ["*"] - effect: allow - - # ❌ DENY explicite (priorité haute) + # Interdire l'accès aux données admin (priorité sur les règles suivantes) - resource: "db.admin.*" actions: ["*"] effect: deny -``` - -#### Exemple Complet de Policy - -```yaml -# plugin.yaml -permissions: - # Lecture seule sur la DB principale - - resource: "db.main.users" - actions: ["read"] - effect: allow - - - resource: "db.main.posts" - actions: ["read"] - effect: allow - # Écriture sur cache - - resource: "cache.global" + # Cache en lecture/écriture + - resource: "cache.*" actions: ["read", "write"] effect: allow - # Accès scheduler - - resource: "scheduler.jobs" - actions: ["create", "delete"] + # Autoriser uniquement l'écriture sur le filesystem temporaire + - resource: "fs.tmp.*" + actions: ["write"] effect: allow - - # Interdiction explicite (sécurité défense) - - resource: "db.admin.*" - actions: ["*"] - effect: deny - - - resource: "kernel.*" - actions: ["*"] - effect: deny ``` -### Flux d'Évaluation - -```mermaid -sequenceDiagram - participant A as Plugin A - participant S as Supervisor - participant P as PermissionEngine - participant L as Policy Loader - - A->>S: call("plugin_b", "get_user") - S->>P: evaluate("plugin_a", "plugin_b.get_user") +### Règles d'évaluation - P->>L: Charger policies de plugin_a - L-->>P: Retourner rules +- Les règles sont évaluées **dans l'ordre** — première correspondance gagne. +- Aucune règle correspondante → **deny** par défaut (fail-closed). +- Les patterns `resource` supportent les globs (`*`, `?`, `[abc]`). +- Le champ `actions` accepte `["*"]` pour toutes les actions. - P->>P: Vérifier DENY explicite - Note over P: ❌ Aucun DENY trouvé +### Performances - P->>P: Vérifier ALLOW explicite - Note over P: ✅ Rule trouvée ! - - P-->>S: ✅ Autorisé - S->>A: Procéder à l'appel -``` +Le moteur utilise un **cache LRU** par triplet `(plugin, resource, action)`. Latence mesurée : **113 µs** (avec cache) vs 152 µs (sans cache) pour 6 vérifications. Voir [Benchmarks](../reference/benchmarks.md). --- -## 3. Signature des Plugins Trusted - -En production, le mode `strict_trusted` garantit que les plugins trusted n'ont pas été altérés. - -### Architecture de Signature +## 2. Signature des plugins Trusted -```mermaid -flowchart TB - subgraph Sign["📝 Signature (Développement)"] - D1[Fichiers Plugin] --> D2[Calcul HMAC-SHA256] - D2 --> D3[Générer plugin.sig] - end +En production, activer `strict_trusted: true` dans `xcore.yaml` force la vérification de la signature HMAC-SHA256 avant le chargement de tout plugin `trusted`. - subgraph Verify["🔍 Vérification (Production)"] - V1[Charger plugin.sig] --> V2[Re-calculer hashes] - V2 --> V3{Match ?} - V3 -->|Oui| V4[✅ Charger] - V3 -->|Non| V5[❌ Rejeté] - end - - Sign --> Verify - - style Sign fill:#E8F5E9,stroke:#388E3C - style Verify fill:#FFEBEE,stroke:#C62828 +```yaml +plugins: + strict_trusted: true + secret_key: "${PLUGIN_SECRET_KEY}" # clé HMAC partagée ``` -### Générer une Signature - ```bash -# Signer un plugin trusted -xcore plugin sign plugins/my_plugin/ +# Signer un plugin avant déploiement +poetry run xcore plugin sign plugins/mon_plugin --key "ma_clé" -# Le fichier plugin.sig est créé/mis à jour -``` - -### Structure de `plugin.sig` - -```json -{ - "plugin": "my_plugin", - "version": "1.0.0", - "timestamp": "2024-01-15T10:30:00Z", - "files": { - "plugin.yaml": "a1b2c3d4e5f6...", - "src/main.py": "f6e5d4c3b2a1...", - "src/utils.py": "1234567890ab..." - }, - "signature": "hmac-sha256-signature-here" -} +# Cela génère plugins/mon_plugin/plugin.sig +# Vérifier +poetry run xcore plugin verify plugins/mon_plugin --key "ma_clé" ``` -### Vérification au Boot - -```mermaid -sequenceDiagram - participant K as Kernel - participant S as SignatureVerifier - participant F as Fichiers Plugin - participant Sig as plugin.sig - - K->>S: verify_trusted("my_plugin") - - S->>Sig: Charger signature - S->>F: Lire fichiers actuels - S->>S: Calculer hashes actuels - - S->>S: Comparer avec plugin.sig - alt Hashes correspondent - S-->>K: ✅ Vérifié - K->>K: Charger plugin - else Hashes différents - S-->>K: ❌ Tampering détecté - K->>K: ❌ Refuser chargement - end -``` +Un plugin `trusted` sans `plugin.sig` valide sera **refusé au chargement** si `strict_trusted: true`. --- -## 4. Bonnes Pratiques pour les Développeurs +## 3. Sandbox multi-couches (plugins Sandboxed) -### Checklist de Sécurité +Les plugins `sandboxed` s'exécutent dans un **processus OS séparé** avec trois niveaux de protection : -```mermaid -flowchart LR - subgraph Do["✅ À Faire"] - D1[Sandboxed par défaut] - D2[Permissions minimales] - D3[Valider inputs] - D4[Pas d'état local] - D5[Signer en prod] - end +### Couche 1 — Analyse statique AST - subgraph Dont["❌ À Éviter"] - A1[Trusted sans besoin] - A2[Permissions larges] - A3[Inputs non validés] - A4[État dans le plugin] - A5[Code non signé] - end +Avant toute exécution, l'`ASTScanner` parse l'arbre syntaxique du plugin et bloque : - Do -.->|Au lieu de| Dont - - style Do fill:#E8F5E9,stroke:#388E3C - style Dont fill:#FFEBEE,stroke:#C62828 +```python +# BLOQUÉ par le scanner AST +import os # ❌ module interdit +import subprocess # ❌ module interdit +eval("code") # ❌ builtin interdit +exec("code") # ❌ builtin interdit +obj.__class__ # ❌ attribut dangereux +obj.__globals__ # ❌ sandbox escape +type.__subclasses__() # ❌ sandbox escape ``` -### 1. Utiliser le Sandboxing par Défaut +Les modules autorisés se déclarent dans `allowed_imports` du manifeste : ```yaml -# ✅ RECOMMANDÉ: Sandboxed pour isolation maximale -execution_mode: sandboxed - -# ⚠️ UNIQUEMENT SI NÉCESSAIRE: Trusted pour accès kernel -execution_mode: trusted -# Nécessite: accès FastAPI hooks, objets kernel complexes +allowed_imports: + - httpx + - pydantic + - json + - datetime ``` -### 2. Demander des Permissions Minimales +### Couche 2 — Isolation processus (JSON-RPC 2.0) -```yaml -# ❌ TROP LARGE -permissions: - - resource: "db.*" - actions: ["*"] - effect: allow +La communication kernel ↔ sandbox se fait exclusivement via **pipes stdin/stdout** en JSON. Aucun objet Python natif ne passe la frontière (pas de `pickle`). -# ✅ MINIMAL -permissions: - - resource: "db.main.users" - actions: ["read"] - effect: allow - - resource: "db.main.posts" - actions: ["read", "write"] - effect: allow +``` +Kernel Pipe (JSON) Sandbox Worker + │ │ │ + │── {"method":"call"} ──►│──────────────────────►│ + │ │ │── exécute handler + │◄── {"result": ...} ───│◄──────────────────────│ ``` -### 3. Valider Tous les Inputs +### Couche 3 — Limites de ressources -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok -from pydantic import BaseModel, Field - -# ✅ Schema de validation -class GreetInput(BaseModel): - name: str = Field(min_length=1, max_length=100) - language: str = Field(default="fr") +```yaml +resources: + timeout_seconds: 10 # Kill si dépasse + max_memory_mb: 128 # Kill si RSS dépasse + rate_limit: + calls: 100 + period_seconds: 60 # 429 si quota dépassé +``` -class Plugin(AutoDispatchMixin, TrustedBase): +--- - @action("greet") - async def greet(self, payload: dict): - # Valider l'input - try: - validated = GreetInput(**payload) - except ValidationError as e: - return error(msg=str(e), code="invalid_input") +## 4. Authentification HTTP (AuthBackend) - return ok(message=f"Bonjour, {validated.name} !") -``` +XCore fournit un système d'authentification pluggable via l'`AuthBackend` protocol. -### 4. Pas d'État Local +### Implémenter un backend ```python -# ❌ MAUVAIS: État dans le plugin -class Plugin(TrustedBase): - def __init__(self): - self.cache_local = {} # ⚠️ Perdu au reload ! +from xcore.kernel.api.auth import AuthBackend, AuthPayload, register_auth_backend -# ✅ BON: État dans les services +class JWTBackend: + async def extract_token(self, request) -> str | None: + auth = request.headers.get("Authorization", "") + if auth.startswith("Bearer "): + return auth[7:] + return None + + async def decode_token(self, token: str) -> AuthPayload | None: + try: + payload = jwt.decode(token, SECRET, algorithms=["HS256"]) + return AuthPayload( + sub=payload["sub"], + roles=payload.get("roles", []), + permissions=payload.get("permissions", []), + ) + except jwt.InvalidTokenError: + return None + + async def has_permission(self, payload: AuthPayload, permission: str) -> bool: + return permission in (payload.get("permissions") or []) + +# Dans on_load() du plugin auth class Plugin(TrustedBase): async def on_load(self): - self.cache = self.get_service("cache") # ✅ Persistant + register_auth_backend(JWTBackend()) - async def store(self, key, value): - await self.cache.set(key, value) # ✅ Sauvegardé + async def on_unload(self): + from xcore.kernel.api.auth import unregister_auth_backend + unregister_auth_backend() ``` -### 5. Signer en Production +### Protéger des routes FastAPI -```bash -# Développement -# Pas de signature requise +```python +from xcore.kernel.api.auth import get_current_user +from fastapi import Depends -# Production (strict_trusted activé) -xcore plugin sign plugins/my_plugin/ +@router.get("/profile") +async def profile(user = Depends(get_current_user)): + return {"sub": user["sub"], "roles": user.get("roles", [])} +``` -# Vérifier -xcore plugin validate plugins/my_plugin/ +### RBAC déclaratif sur les routes plugins + +```python +from xcore.sdk.decorators import route + +@route("/admin/users", method="GET", permissions=["admin"]) +async def list_all_users(self): + # Accessible uniquement si l'utilisateur a la permission "admin" + ... ``` --- -## 5. matrice de Sécurité - -### Comparaison Trusted vs Sandboxed +## 5. Validation des manifestes -| Caractéristique | Trusted | Sandboxed | -| :--- | :--- | :--- | -| **Processus** | Principal | Isolé (OS) | -| **Performance** | ⚡ Rapide | 🐌 IPC overhead | -| **Isolation** | 🔶 Logique | 🔒 Physique | -| **Accès Kernel** | ✅ Complet | ❌ Via IPC uniquement | -| **AST Scan** | ⚠️ Partiel | ✅ Complet | -| **Usage** | Dev / Confiance | Prod / Tiers | +Le `ManifestValidator` vérifie chaque manifeste au chargement : -### Tableau des Permissions par Défaut +- Champs requis (`name`, `version`) +- `execution_mode` valide (`trusted` | `sandboxed`) +- `framework_version` compatible avec la version installée +- `allowed_imports` contient uniquement des noms de modules (pas de symboles) +- `plugin.sig` présent si `strict_trusted: true` -| Action | Trusted | Sandboxed | -| :--- | :--- | :--- | -| Appeler autre plugin | ✅ (avec permission) | ✅ (avec permission) | -| Accéder DB publique | ✅ | ✅ | -| Accéder DB privée | ❌ | ❌ | -| Modifier kernel | ❌ | ❌ | -| Import restreint | ⚠️ (whitelist) | ❌ (bloqué AST) | -| Route HTTP | ✅ | ✅ (via IPC) | +```bash +# Valider manuellement avant déploiement +poetry run xcore plugin validate plugins/mon_plugin +``` --- -## Prochaines Lectures +## 6. Bonnes pratiques -| 📚 Guide | Objectif | -| :--- | :--- | -| [Event System](events.md) | Messagerie sécurisée | -| [Plugin Creation](creating-plugins.md) | Développer des plugins | -| [Troubleshooting](troubleshooting.md) | Déboguer les erreurs | +- Utiliser `strict_trusted: true` en production et signer tous les plugins. +- Privilegier `sandboxed` pour les plugins tiers ou non audités. +- Appliquer le **principe du moindre privilège** : ne déclarer que les permissions réellement utilisées. +- Ne jamais mettre `secret_key` en clair dans `xcore.yaml` — utiliser `"${SECRET_KEY}"` et `.env`. +- Auditer régulièrement avec `poetry run bandit -r xcore/ plugins/`. diff --git a/doc/guides/services.md b/doc/guides/services.md index e6807f31..f0ee9a98 100644 --- a/doc/guides/services.md +++ b/doc/guides/services.md @@ -1,167 +1,225 @@ -# Utilisation des Services XCore +# Services -Les services XCore fournissent une infrastructure technique partagée (Base de données, Cache, Planificateur de tâches) disponible pour tous les plugins. +XCore expose trois services partagés via le `ServiceContainer` : **Base de données**, **Cache** et **Scheduler**. Tous sont accessibles depuis `self.get_service(name)` dans un plugin `TrustedBase`. -## 1. Accès aux Services +--- -Chaque plugin accède aux services via l'objet `ServiceContainer`. Le SDK offre deux méthodes principales : +## Service Container ```python -from xcore.sdk import TrustedBase - -class Plugin(TrustedBase): - async def on_load(self): - # 1. Récupération directe (typage automatique par IDE) - self.db = self.get_service("db") - self.cache = self.get_service("cache") - - # 2. Récupération typée (pour connexions nommées ou extensions) - # from xcore.services.database.adapters.async_sql import AsyncSQLAdapter - # self.analytics = self.get_service_as("analytics", AsyncSQLAdapter) +# Accès typé depuis un plugin +self.db = self.get_service("db") # → AsyncSQLAdapter +self.cache = self.get_service("cache") # → CacheService +self.scheduler = self.get_service("scheduler") # → SchedulerService + +# Connexion nommée avec type explicite +from xcore.services.database.adapters.async_sql import AsyncSQLAdapter +self.analytics = self.get_service_as("analytics", AsyncSQLAdapter) ``` --- -## 2. Service de Base de Données (`db`) +## Base de données + +### Configuration `xcore.yaml` + +```yaml +services: + databases: + # Connexion principale (accessible via self.get_service("db")) + db: + type: sqlasync # sqlasync | sql | redis | mongodb + url: postgresql+asyncpg://user:pass@localhost/mydb + pool_size: 5 + max_overflow: 10 + echo: false + + # Connexion secondaire (accessible via self.get_service("analytics")) + analytics: + type: sqlasync + url: postgresql+asyncpg://user:pass@analytics-host/metrics + + # Redis comme base de données (clé/valeur) + redis_db: + type: redis + url: redis://localhost:6379/0 + max_connections: 50 +``` -XCore utilise **SQLAlchemy** (ou des adaptateurs spécifiques) pour gérer les connexions SQL (PostgreSQL, MySQL, SQLite). +Types supportés : -### Repository SQL : `BaseSyncRepository` / `BaseAsyncRepository` +| `type` | Adaptateur | Usage | +|:-------|:-----------|:------| +| `sqlasync` | `AsyncSQLAdapter` | PostgreSQL, MySQL, SQLite (async) | +| `sql` | `SQLAdapter` | SQLAlchemy synchrone | +| `redis` | `RedisAdapter` | Redis key/value | +| `mongodb` | `MongoDBAdapter` | MongoDB | -Le SDK simplifie l'accès aux données avec le pattern Repository. +### Utilisation (SQLAlchemy async) ```python -from xcore.sdk import BaseAsyncRepository -from sqlalchemy import Column, String, Integer -from sqlalchemy.orm import declarative_base +async def on_load(self): + self.db = self.get_service("db") + +@action("get_users") +async def get_users(self, payload: dict) -> dict: + async with self.db.session() as session: + from sqlalchemy import select + result = await session.execute(select(User)) + users = result.scalars().all() + return ok(users=[u.to_dict() for u in users]) +``` -Base = declarative_base() +### Migrations Alembic -class User(Base): - __tablename__ = "users" - id = Column(Integer, primary_key=True) - name = Column(String) +XCore intègre Alembic pour les migrations. Configurer `alembic.ini` normalement, le `DATABASE_URL` peut être surchargé via `XCORE__SERVICES__DATABASES__DB__URL`. -class UserRepository(BaseAsyncRepository[User]): - """Repository personnalisé pour les utilisateurs.""" - pass +--- -# --- Dans votre plugin --- -class UserPlugin(TrustedBase): - async def on_load(self): - self.db = self.get_service("db") - self.users = UserRepository(User) - - async def create_user(self, name: str): - # Utilisation d'une session asynchrone - async with self.db.session() as session: - new_user = User(name=name) - await self.users.create(session, new_user) - await session.commit() - return new_user.id -``` +## Cache ---- +### Configuration -## 3. Service de Cache (`cache`) +```yaml +services: + cache: + backend: memory # memory | redis + ttl: 300 # TTL par défaut en secondes + max_size: 1000 # Taille max (memory uniquement) + url: redis://localhost:6379/0 # Requis si backend=redis +``` -Le service de cache supporte le stockage en **Mémoire** ou **Redis**. +### API ```python -# Accès au service de cache -cache = self.get_service("cache") - -# --- Opérations de base --- -# Stocker une valeur pendant 5 minutes (300s) -await cache.set("user_123:profile", {"name": "Alice"}, ttl=300) +async def on_load(self): + self.cache = self.get_service("cache") + +@action("get_profile") +async def get_profile(self, payload: dict) -> dict: + key = f"profile:{payload['user_id']}" + + # Lecture + cached = await self.cache.get(key) + if cached: + return ok(profile=cached, from_cache=True) + + # Écriture avec TTL custom + profile = await self._fetch_profile(payload["user_id"]) + await self.cache.set(key, profile, ttl=600) + return ok(profile=profile) + +@action("invalidate") +async def invalidate(self, payload: dict) -> dict: + await self.cache.delete(f"profile:{payload['user_id']}") + return ok() + +@action("batch_prime") +async def batch_prime(self, payload: dict) -> dict: + # Opérations batch — toujours préférer mset/mget (44-77x plus rapide sur Redis) + data = {"key:1": "val1", "key:2": "val2", "key:3": "val3"} + await self.cache.mset(data) + + values = await self.cache.mget(list(data.keys())) + return ok(values=values) +``` -# Récupérer une valeur -profile = await cache.get("user_123:profile") +API complète : -# Supprimer une valeur -await cache.delete("user_123:profile") +| Méthode | Description | +|:--------|:------------| +| `get(key)` | Lit une valeur | +| `set(key, value, ttl=None)` | Écrit une valeur | +| `delete(key)` | Supprime une clé | +| `clear()` | Vide le cache entier | +| `mget(keys)` | Lit plusieurs clés en une opération | +| `mset(mapping)` | Écrit plusieurs clés en une opération | -# --- Opérations groupées (Optimisées Redis) --- -await cache.mset({"k1": "v1", "k2": "v2"}) -values = await cache.mget(["k1", "k2"]) -``` +> **Performance** : avec Redis (réseau ~2 ms), `mset`/`mget` sur 100 clés est **44–77× plus rapide** que les appels séquentiels. Voir [Benchmarks](../reference/benchmarks.md). --- -## 4. Service de Planification (`scheduler`) +## Scheduler -XCore intègre **APScheduler** pour exécuter des tâches en arrière-plan ou de manière périodique. +### Configuration -```python -# Accès au planificateur -scheduler = self.get_service("scheduler") - -# --- Planifier une tâche --- -# Tâche immédiate (Background Job) -await scheduler.add_job( - self.process_data, - args=[data_id], - id=f"process_{data_id}" -) - -# Tâche planifiée (Cron) -await scheduler.add_cron_job( - self.daily_report, - hour=0, - minute=0, - id="daily_cleanup" -) - -# Tâche par intervalle -await scheduler.add_interval_job( - self.heartbeat, - seconds=60, - id="ping_external_api" -) -``` +```yaml +services: + scheduler: + enabled: true + backend: memory # memory | redis | database + timezone: Europe/Paris ---- + # Jobs statiques (optionnel — les plugins peuvent en ajouter dynamiquement) + jobs: + - id: cleanup + func: myapp.tasks:cleanup + trigger: cron + hour: 3 + minute: 0 +``` -## 5. Propagation des Services +Backends : -La propagation des services permet à un plugin d'exposer des fonctionnalités à d'autres plugins de manière structurée. +| Backend | Persistance | Usage | +|:--------|:------------|:------| +| `memory` | Non — perdu au redémarrage | Développement | +| `redis` | Oui — via Redis | Production | +| `database` | Oui — via SQLAlchemy | Production sans Redis | -### Cycle de Propagation -1. Un plugin fournisseur enregistre son service dans `on_load`. -2. Le `LifecycleManager` détecte l'enregistrement et met à jour le `ServiceContainer` global. -3. Les plugins dépendants (déclarés dans `requires`) peuvent alors accéder au nouveau service. +### Utilisation depuis un plugin ```python -# Plugin Fournisseur (ex: billing) -class BillingPlugin(TrustedBase): - async def on_load(self): - self.register_service("billing_engine", self.engine) - -# Plugin Consommateur -class CartPlugin(TrustedBase): - async def on_load(self): - # Disponible car 'billing' est une dépendance requise - self.billing = self.get_service("billing_engine") +async def on_load(self): + self.scheduler = self.get_service("scheduler") + + # Cron : tous les jours à 3h + @self.scheduler.cron("0 3 * * *") + async def nightly_cleanup(): + await self._cleanup() + + # Interval + self.scheduler.add_job( + self._sync_data, + trigger="interval", + minutes=5, + id="sync_data", + replace_existing=True, + ) + + # One-shot (date précise) + from datetime import datetime, timedelta + self.scheduler.add_job( + self._send_reminder, + trigger="date", + run_date=datetime.now() + timedelta(hours=1), + id="reminder_once", + ) + +async def _sync_data(self): + # Exécuté toutes les 5 minutes + pass ``` --- -## 6. Surveillance de la Santé (Health Checks) +## Extensions (services custom) -Chaque service intégré implémente une méthode de santé. Vous pouvez consulter l'état de tous les services via : +Pour enregistrer un service tiers dans le conteneur : -```bash -# Via la CLI -xcore services status - -# Via l'API HTTP -curl http://localhost:8082/plugin/ipc/health +```yaml +services: + extensions: + stripe: + api_key: "${STRIPE_KEY}" + webhook_secret: "${STRIPE_WEBHOOK_SECRET}" ``` ---- - -## Bonnes Pratiques - -1. **Lazy Initialization** : Si votre service consomme beaucoup de ressources, n'établissez la connexion réelle que lors du premier appel. -2. **Gestion des Timeouts** : Utilisez toujours des timeouts lors des appels aux services externes (DB, Redis) pour ne pas bloquer le framework. -3. **Namespacing des Clés** : Préfixez toujours vos clés de cache et IDs de jobs par le nom de votre plugin (ex: `auth:token:123`) pour éviter les collisions. +```python +# Dans un plugin "stripe_plugin" +async def on_load(self): + cfg = self.ctx.config.services.extensions.get("stripe", {}) + self._client = StripeClient(api_key=cfg["api_key"]) + # Enregistrer dans le container pour les autres plugins + self.ctx.services.register("stripe", self._client) +``` diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml index 073581c3..3a3bf1d0 100644 --- a/doc/mkdocs.yml +++ b/doc/mkdocs.yml @@ -1,8 +1,5 @@ -# mkdocs.yml -# Configuration for MkDocs documentation - site_name: XCore Framework -site_description: Production-grade plugin-first Python framework +site_description: Plugin-first orchestration framework built on FastAPI site_author: XCore Team site_url: https://xcore.readthedocs.io @@ -12,7 +9,6 @@ site_dir: ../docs theme: name: material palette: - # Light mode - media: "(prefers-color-scheme: light)" scheme: default primary: indigo @@ -20,7 +16,6 @@ theme: toggle: icon: material/brightness-7 name: Switch to dark mode - # Dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: indigo @@ -60,7 +55,11 @@ plugins: markdown_extensions: - admonition - pymdownx.details - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.highlight: anchor_linenums: true - pymdownx.inlinehilite @@ -76,36 +75,37 @@ markdown_extensions: permalink: true nav: - - Home: index.md + - Accueil: index.md - Getting Started: - - getting-started/installation.md - - getting-started/quickstart.md + - Installation: getting-started/installation.md + - Quick Start: getting-started/quickstart.md - Guides: - - guides/creating-plugins.md - - guides/creating-services.md - - guides/services.md - - guides/events.md - - guides/security.md - - guides/monitoring.md - - guides/scaling.md - - guides/troubleshooting.md + - Créer un Plugin: guides/creating-plugins.md + - Services (DB, Cache, Scheduler): guides/services.md + - Événements & Hooks: guides/events.md + - Sécurité: guides/security.md + - Monitoring: guides/monitoring.md + - Scalabilité: guides/scaling.md + - Troubleshooting: guides/troubleshooting.md - Reference: - - reference/sdk.md - - reference/configuration.md - - reference/api.md + - SDK: reference/sdk.md + - Configuration: reference/configuration.md + - CLI: reference/cli.md + - API: reference/api.md + - Benchmarks: reference/benchmarks.md - Architecture: - - architecture/overview.md + - Vue d'ensemble: architecture/overview.md + - Décisions: architecture/decisions.md - Deployment: - - deployment/guide.md + - Guide: deployment/guide.md - Development: - - development/testing.md - - development/contributing.md - - Examples: - - examples/basic-plugin.md - - examples/complete-plugin.md - - examples/trusted-plugin.md - - examples/sandboxed-plugin.md - - About: - - versions.md + - Tests: development/testing.md + - Contribuer: development/contributing.md + - Exemples: + - Plugin basique: examples/basic-plugin.md + - Plugin complet: examples/complete-plugin.md + - Plugin Trusted: examples/trusted-plugin.md + - Plugin Sandboxed: examples/sandboxed-plugin.md + - Changelog: versions.md -copyright: Copyright © 2023 XCore Team +copyright: Copyright © 2026 XCore Team diff --git a/doc/reference/benchmarks.md b/doc/reference/benchmarks.md new file mode 100644 index 00000000..45801647 --- /dev/null +++ b/doc/reference/benchmarks.md @@ -0,0 +1,151 @@ +# Performance & Benchmarks + +Résultats mesurés sur Python 3.14 · pytest-benchmark 5.2.3 · `time.perf_counter` +Machine : Linux x86_64, génération 2026-04-29 (XCore v2.1.2) + +Reproduire les mesures : + +```bash +# Benchmarks kernel (pytest-benchmark) +poetry run pytest tests/benchmarks/test_kernel_benchmarks.py \ + tests/benchmarks/test_permission_bench.py \ + --benchmark-only --benchmark-json=bench_results.json + +# Benchmarks cache (script autonome) +poetry run python tests/benchmarks/cache_batch_perf.py +``` + +--- + +## 1. Permission Engine + +### 1.1 Policy Matching (µs par opération) + +La méthode `Policy.matches()` est le cœur du moteur de permissions. Elle compare une ressource et une action à un pattern glob. + +| Test | Min (µs) | Moy (µs) | Ops/s | +|:-----|--------:|--------:|------:| +| `failure_action` — action non autorisée | **0.245** | 0.409 | **2 443 000** | +| `failure_resource` — ressource hors scope | 0.752 | 1.042 | 960 000 | +| `success` — match complet | 0.997 | 1.637 | 611 000 | + +``` +Policy.matches() — distribution des moyennes (µs) +───────────────────────────────────────────────── + failure_action ▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.41 µs +failure_resource ▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░░░ 1.04 µs + success ▓▓▓▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░ 1.64 µs + 0 0.5 1.0 1.5 2.0 +``` + +> **Lecture** : Le short-circuit sur l'action (`failure_action`) est **4× plus rapide** qu'un match complet car la comparaison des actions se fait avant le glob de ressource. + +### 1.2 Permission Engine — cache vs sans cache + +Le `PermissionEngine` évalue un jeu de 6 paires `(resource, action)` contre 5 policies. + +| Mode | Min (µs) | Moy (µs) | Ops/s | +|:-----|--------:|--------:|------:| +| Avec cache LRU | **85.5** | 113.1 | 8 843 | +| Sans cache (worst case) | 107.6 | 151.8 | 6 589 | + +``` +PermissionEngine — latence moyenne (µs) +──────────────────────────────────────── + avec cache ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░ 113 µs + sans cache ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░ 152 µs + 0 50 100 150 +``` + +**Gain du cache : ~25 % de latence en moins, +34 % de débit.** + +--- + +## 2. Cache Service — Batch vs Séquentiel + +### 2.1 MemoryBackend (100 clés, in-process) + +| Opération | Séquentiel | Batch (mset/mget) | Gain | +|:----------|----------:|------------------:|-----:| +| SET × 100 | 0.28 ms | **0.14 ms** | 2× | +| GET × 100 | 0.09 ms | **0.07 ms** | 1.3× | + +``` +MemoryBackend — SET 100 clés (ms) +─────────────────────────────────── + Séquentiel ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░ 0.28 ms + Batch mset ▓▓▓▓▓▓▒░░░░░░░░░ 0.14 ms + 0 0.1 0.2 0.3 +``` + +### 2.2 RedisCacheBackend (100 clés, 2 ms de latence réseau simulée) + +| Opération | Séquentiel | Batch optimisé | Gain | +|:----------|----------:|--------------:|-----:| +| SET × 100 | 232.6 ms | **5.3 ms** | **44×** | +| GET × 100 | 227.1 ms | **2.9 ms** | **77×** | + +``` +Redis SET 100 clés — impact du batch (ms, 2 ms latence réseau) +────────────────────────────────────────────────────────────── + Séquentiel ████████████████████████████████████████ 232 ms + Pipeline ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 5.3 ms + 0 100 200 232 +``` + +``` +Redis GET 100 clés +──────────────────── + Séquentiel ████████████████████████████████████████ 227 ms + MGET natif ▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.9 ms +``` + +> **En production**, avec un Redis distant, toujours utiliser `mset`/`mget` pour les opérations par lot. Le gain est de 44–77× par rapport aux appels séquentiels. + +--- + +## 3. Synthèse Comparative + +``` +Composant Débit (ops/s) Latence moy +───────────────────────────────────────────────────────────── +Policy.matches (match) 611 000 ops/s 1.6 µs +Policy.matches (miss) 2 443 000 ops/s 0.4 µs +PermissionEngine+cache 8 843 ops/s 113 µs ← 6 checks/eval +PermissionEngine raw 6 589 ops/s 152 µs +Cache Memory MSET ~700 000 ops/s 0.14 ms (100 keys) +Cache Redis MSET ~18 900 ops/s 5.3 ms (100 keys, 2ms net) +Cache Redis sequential ~430 ops/s 232 ms (100 keys, 2ms net) +``` + +--- + +## 4. Évolution des Performances (v2.x) + +| Version | Changement | Impact | +|:--------|:-----------|:-------| +| **v2.1.2** | Cache LRU sur `PermissionEngine` | +34 % débit engine | +| **v2.1.2** | `mset`/`mget` natif sur Redis backend | 44–77× batch throughput | +| **v2.0.0** | Moteur de policies (fnmatch + eval order) | Baseline | + +--- + +## 5. Interprétation et Recommandations + +### Permission Engine +- Le cache LRU est activé par défaut. Ne pas l'invalider entre chaque requête sans raison. +- Un plugin avec 5 policies évalue une paire `(resource, action)` en **~85–150 µs** cache compris. +- Le budget total par requête HTTP est largement inférieur à la milliseconde pour la couche permissions. + +### Cache Service +- Toujours préférer les APIs batch (`mset`, `mget`) dès que `n > 5` clés. +- Avec Redis en production (réseau ≥ 1 ms), le gain batch est **toujours supérieur à 20×**. +- Pour des lectures à très haute fréquence, envisager `MemoryBackend` comme L1 devant Redis (L2). + +--- + +## Liens Connexes + +- [Service Cache — Guide](../guides/services.md) +- [Security & Permissions](../guides/security.md) +- [Testing Guide](../development/testing.md) diff --git a/doc/reference/cli.md b/doc/reference/cli.md index 92bffa95..f5d06aab 100644 --- a/doc/reference/cli.md +++ b/doc/reference/cli.md @@ -1,83 +1,210 @@ -# Manuel de la CLI XCore +# CLI Reference -La CLI `xcore` est l'outil en ligne de commande pour gérer le framework, les plugins et les services. +Le CLI `xcore` expose des commandes pour gérer les plugins, le sandbox, le marketplace et l'état du système. + +```bash +poetry run xcore --help +poetry run xcore --version +``` --- -## 1. Commandes Globales +## `xcore plugin` + +### `list` + +Liste tous les plugins installés dans le répertoire configuré. ```bash -# Voir la version installée -xcore --version +poetry run xcore plugin list +``` -# Afficher l'aide générale -xcore --help +### `info ` -# Spécifier un fichier de configuration personnalisé -xcore --config /chemin/vers/xcore.yaml +Affiche les métadonnées détaillées d'un plugin (version, mode, auteur, permissions). + +```bash +poetry run xcore plugin info mon_plugin ``` ---- +### `load ` + +Charge un plugin sur le serveur en cours d'exécution via l'API interne. + +```bash +poetry run xcore plugin load mon_plugin [--host 127.0.0.1] [--port 8000] [--key ] +``` + +### `reload ` + +Recharge à chaud un plugin sans redémarrer le serveur. + +```bash +poetry run xcore plugin reload mon_plugin +``` + +### `install ` + +Installe un plugin depuis le marketplace, un fichier zip ou un dépôt git. + +```bash +# Depuis le marketplace (défaut) +poetry run xcore plugin install auth_plugin + +# Depuis une archive zip +poetry run xcore plugin install mon_plugin --source zip --url https://example.com/plugin.zip + +# Depuis git +poetry run xcore plugin install mon_plugin --source git --url https://github.com/user/plugin +``` + +### `remove ` + +Supprime un plugin du répertoire. + +```bash +poetry run xcore plugin remove mon_plugin +``` + +### `sign ` + +Génère une signature HMAC-SHA256 (`plugin.sig`) pour un plugin Trusted. + +```bash +poetry run xcore plugin sign plugins/mon_plugin --key "ma_cle_secrete" +``` + +### `verify ` -## 2. Gestion des Plugins (`xcore plugin`) +Vérifie la signature d'un plugin. -| Commande | Usage | Description | -|----------|-------|-------------| -| **`list`** | `xcore plugin list` | Affiche tous les plugins installés et leur statut. | -| **`health`** | `xcore plugin health` | Vérifie la santé de tous les plugins chargés. | -| **`load`** | `xcore plugin load ` | Charge un plugin dynamiquement. | -| **`reload`** | `xcore plugin reload ` | Recharge un plugin (Hot-reload). | -| **`remove`** | `xcore plugin remove ` | Désinstalle un plugin du répertoire `./plugins`. | -| **`info`** | `xcore plugin info ` | Affiche les métadonnées détaillées du plugin. | -| **`validate`** | `xcore plugin validate `| Vérifie la syntaxe du manifeste et du code source. | -| **`sign`** | `xcore plugin sign ` | Signe cryptographiquement un plugin Trusted. | -| **`verify`** | `xcore plugin verify ` | Vérifie la signature cryptographique du plugin. | +```bash +poetry run xcore plugin verify plugins/mon_plugin --key "ma_cle_secrete" +``` + +### `validate ` + +Valide le manifeste d'un plugin (structure, types, compatibilité framework). + +```bash +poetry run xcore plugin validate plugins/mon_plugin +``` + +### `health` + +Vérifie l'état de santé de tous les plugins. + +```bash +poetry run xcore plugin health +``` --- -## 3. Gestion du Bac à Sable (`xcore sandbox`) +## `xcore sandbox` -Le mode sandbox permet d'exécuter et de tester des plugins dans un environnement isolé sans lancer tout le framework XCore. +### `run ` -| Commande | Usage | Description | -|----------|-------|-------------| -| **`run`** | `xcore sandbox run ` | Lance le plugin en mode isolé pour test. | -| **`limits`** | `xcore sandbox limits ` | Affiche les limites (mémoire, CPU) du plugin. | -| **`fs`** | `xcore sandbox fs ` | Affiche la politique du système de fichiers (Allowed/Denied). | -| **`network`**| `xcore sandbox network `| Affiche la politique réseau du plugin. | +Lance un plugin dans un processus sandbox isolé. + +```bash +poetry run xcore sandbox run mon_plugin +``` + +### `limits ` + +Affiche les limites de ressources configurées (mémoire, timeout, rate limit). + +```bash +poetry run xcore sandbox limits mon_plugin +``` + +### `network ` + +Affiche la politique réseau du plugin sandboxed. + +```bash +poetry run xcore sandbox network mon_plugin +``` + +### `fs ` + +Affiche la politique filesystem (chemins autorisés/interdits). + +```bash +poetry run xcore sandbox fs mon_plugin +``` --- -## 4. Marketplace (`xcore marketplace`) +## `xcore marketplace` -Interagit avec le catalogue de plugins public XCore. +### `list` -| Commande | Usage | Description | -|----------|-------|-------------| -| **`list`** | `xcore marketplace list` | Affiche tous les plugins du catalogue. | -| **`search`** | `xcore marketplace search `| Recherche des plugins par mot-clé. | -| **`trending`**| `xcore marketplace trending` | Affiche les plugins populaires. | -| **`show`** | `xcore marketplace show ` | Détails techniques d'un plugin. | -| **`rate`** | `xcore marketplace rate ` | Donne une note au plugin (1 à 5). | +Liste tous les plugins disponibles sur le marketplace. + +```bash +poetry run xcore marketplace list +``` + +### `trending` + +Affiche les plugins populaires. + +```bash +poetry run xcore marketplace trending +``` + +### `search ` + +Recherche des plugins par mot-clé. + +```bash +poetry run xcore marketplace search "authentication" +``` + +### `show ` + +Affiche les détails d'un plugin du marketplace. + +```bash +poetry run xcore marketplace show auth_plugin +``` + +### `rate --score <1-5>` + +Note un plugin. + +```bash +poetry run xcore marketplace rate auth_plugin --score 5 +``` --- -## 5. État des Services (`xcore services`) +## `xcore services status` -Vérifie la santé de l'infrastructure commune. +Affiche l'état de tous les services (DB, Cache, Scheduler). -| Commande | Usage | Description | -|----------|-------|-------------| -| **`status`** | `xcore services status` | Affiche l'état des connexions DB, Cache, etc. | -| **`health`** | `xcore health` | Lance un diagnostic global du framework. | +```bash +poetry run xcore services status +poetry run xcore services status --json +``` --- -## Options d'affichage +## `xcore health` -La plupart des commandes supportent l'option `--json` pour faciliter l'intégration avec d'autres outils (ex: `jq`). +Health check global du système. ```bash -# Récupérer la liste des plugins en JSON -xcore plugin list --json | jq '.[].name' +poetry run xcore health +poetry run xcore health --json ``` + +--- + +## Options globales + +| Option | Description | +|:-------|:------------| +| `--config ` | Chemin vers `xcore.yaml` (défaut : auto-détection) | +| `--version` | Affiche la version (`xcore v2.1.2`) | diff --git a/doc/reference/configuration.md b/doc/reference/configuration.md index c4742cbe..bcde5246 100644 --- a/doc/reference/configuration.md +++ b/doc/reference/configuration.md @@ -1,110 +1,183 @@ -# Configuration XCore (`xcore.yaml`) +# Configuration (`xcore.yaml`) -Le fichier `xcore.yaml` est le cerveau du framework. Il centralise toutes les configurations de l'application, des services et des plugins. +Le fichier `xcore.yaml` centralise toute la configuration du framework. --- -## 1. Structure Globale +## Structure complète ```yaml +# ── Application ────────────────────────────────────────────── app: - name: "mon-app-xcore" - env: "development" # development | staging | production + name: "mon-app" + env: development # development | staging | production debug: false - secret_key: "change-me-in-production" - plugin_prefix: "/plugin" # Préfixe racine des routes HTTP - plugin_tags: ["XCore"] # Tags OpenAPI globaux + secret_key: "${APP_SECRET_KEY}" # clé JWT / sessions + plugin_prefix: "/plugin" # préfixe des routes HTTP plugins + plugin_tags: [] # tags OpenAPI globaux + server_key: "${SERVER_KEY}" # clé HMAC pour l'API interne + server_key_iterations: 100000 +# ── Plugins ────────────────────────────────────────────────── plugins: - directory: "./plugins" # Répertoire de stockage des plugins - secret_key: "change-me-in-production" - strict_trusted: true # Refuse les plugins non signés en production - interval: 2 # Intervalle du watcher en secondes - entry_point: "src/main.py" # Point d'entrée par défaut - + directory: ./plugins + secret_key: "${PLUGIN_SECRET_KEY}" + strict_trusted: true # true = refus des plugins non signés + interval: 2 # watcher hot-reload (secondes, 0 = désactivé) + entry_point: src/main.py + + # Fichiers ignorés par le watcher + snapshot: + extensions: [".log", ".pyc", ".html", ".map"] + filenames: ["__pycache__", "__init__.py", ".env", ".DS_Store"] + hidden: true + +# ── Services ───────────────────────────────────────────────── services: - # Configuration des bases de données SQL/NoSQL + # Bases de données (plusieurs connexions possibles) databases: - default: - type: "sqlite" # sqlite | postgresql | mysql - url: "sqlite:///./xcore.db" + db: # accessible via self.get_service("db") + type: sqlasync # sqlasync | sql | redis | mongodb + url: "${DATABASE_URL}" pool_size: 5 max_overflow: 10 echo: false - # Configuration du cache (Mémoire ou Redis) + analytics: # accessible via self.get_service("analytics") + type: sqlasync + url: "${ANALYTICS_DB_URL}" + + redis_db: # accessible via self.get_service("redis_db") + type: redis + url: "${REDIS_URL}" + max_connections: 50 + + # Cache cache: - backend: "memory" # memory | redis + backend: redis # memory | redis + url: "${REDIS_URL}" ttl: 300 - max_size: 1000 - url: "redis://localhost:6379/0" # Requis si backend=redis + max_size: 10000 # ignoré en mode redis - # Configuration du planificateur de tâches + # Scheduler scheduler: enabled: true - backend: "memory" # memory | redis | database - timezone: "UTC" - - # Extensions (Services personnalisés tiers) + backend: redis # memory | redis | database + timezone: Europe/Paris + + # Jobs statiques (optionnel) + jobs: + - id: cleanup + func: myapp.tasks:cleanup + trigger: cron + hour: 3 + minute: 0 + + # Extensions (services custom) extensions: - mon_service_custom: - api_key: "${CUSTOM_API_KEY}" - timeout: 30 + stripe: + api_key: "${STRIPE_KEY}" +# ── Observabilité ───────────────────────────────────────────── observability: logging: - level: "INFO" # DEBUG, INFO, WARNING, ERROR + level: INFO # DEBUG | INFO | WARNING | ERROR format: "%(asctime)s [%(levelname)s] %(name)s: %(message)s" - file: "logs/xcore.log" + file: log/app.log + max_bytes: 10485760 # 10 MB + backup_count: 5 metrics: - enabled: false - backend: "memory" # memory | prometheus | statsd - prefix: "xcore" + enabled: true + backend: memory # memory | prometheus | statsd + prefix: xcore tracing: enabled: false - backend: "noop" # noop | opentelemetry | jaeger - service_name: "xcore" + backend: noop # noop | opentelemetry | jaeger + service_name: xcore + endpoint: null +# ── Sécurité ───────────────────────────────────────────────── security: - allowed_imports: ["math", "json", "time"] # Imports autorisés en sandbox - forbidden_imports: ["os", "sys", "subprocess"] # Imports bloqués par l'AST - rate_limit_default: - calls: 100 - period_seconds: 60 + # (Réservé — extension future pour Vault, rotation de clés, etc.) +# ── Marketplace ─────────────────────────────────────────────── marketplace: - url: "https://marketplace.xcore.dev" - api_key: "" - timeout: 10 - cache_ttl: 300 + url: "https://marketplace.xcore.io" + api_key: "${MARKETPLACE_KEY}" ``` --- -## 2. Variables d'Environnement (Interpolation) +## Types de bases de données -XCore supporte l'injection de variables d'environnement directement dans le fichier YAML via la syntaxe `${VAR_NAME}`. +| `type` | Driver | Usage | +|:-------|:-------|:------| +| `sqlasync` | SQLAlchemy + asyncpg / aiosqlite | PostgreSQL, MySQL, SQLite async | +| `sql` | SQLAlchemy synchrone | SQLAlchemy sync | +| `redis` | redis-py asyncio | Redis key/value | +| `mongodb` | Motor (async) | MongoDB | + +--- + +## Résolution des valeurs + +### Substitution `${VAR}` + +Toute valeur au format `${NOM_VAR}` est substituée depuis l'environnement OS ou le `.env` configuré. ```yaml app: - secret_key: "${APP_SECRET_KEY}" + secret_key: "${APP_SECRET_KEY}" # résolu depuis os.environ ``` -### Surcharge par variables d'environnement +### Surcharge via variables d'environnement -Toutes les clés du fichier YAML peuvent être surchargées via des variables d'environnement préfixées par `XCORE__` (double soulignement pour les niveaux d'imbrication). +Format : `XCORE__
__=valeur` -- `XCORE__APP__ENV=production` surcharge `app.env`. -- `XCORE__SERVICES__CACHE__BACKEND=redis` surcharge `services.cache.backend`. +```bash +XCORE__APP__DEBUG=true +XCORE__APP__ENV=production +XCORE__SERVICES__CACHE__BACKEND=redis +XCORE__SERVICES__CACHE__URL=redis://prod-redis:6379/0 +XCORE__PLUGINS__STRICT_TRUSTED=true +``` + +Priorité : **variables d'environnement > xcore.yaml > valeurs par défaut** + +### Chargement d'un `.env` + +```yaml +app: + dotenv: "./.env" # chemin relatif au projet +``` + +--- + +## Valeurs par défaut + +Tous les champs ont des valeurs par défaut : un `xcore.yaml` vide (ou absent) ne provoque pas d'erreur, XCore démarre en mode minimal (SQLite en mémoire, cache memory, scheduler désactivé). --- -## 3. Valeurs par Défaut +## Fichiers alternatifs -XCore est conçu pour fonctionner sans configuration (Zéro-Config) en utilisant des valeurs par défaut sécurisées : -- **DB** : SQLite locale (`./xcore.db`). -- **Cache** : En mémoire (`memory`). -- **Logs** : Niveau `INFO` vers la sortie standard. -- **Plugins** : Dossier `./plugins`. +Le `ConfigLoader` cherche dans cet ordre si aucun chemin n'est spécifié : + +1. `integation.yaml` +2. `integation.yml` +3. `integation.json` +4. `config/integation.yaml` + +Pour spécifier un chemin custom : + +```python +xcore = Xcore(config_path="config/prod.yaml") +``` + +ou via CLI : + +```bash +poetry run xcore --config config/prod.yaml plugin list +``` diff --git a/doc/reference/sdk.md b/doc/reference/sdk.md index 7d478da3..77c32da0 100644 --- a/doc/reference/sdk.md +++ b/doc/reference/sdk.md @@ -1,736 +1,281 @@ -# XCore SDK Reference - -Le SDK XCore est une bibliothèque haut niveau conçue pour faciliter le développement de plugins sécurisés, scalables et maintenables. +# SDK Reference --- -## Vue d'Ensemble du SDK - -```mermaid -flowchart TB - subgraph Core["🎯 Classes Core"] - TB[TrustedBase
Base plugins trusted] - BP[BasePlugin
Interface low-level] - SB[SandboxedBase
Base plugins sandboxed] - end - - subgraph Mixins["🔧 Mixins"] - ADM[AutoDispatchMixin
Routage @action] - RP[RoutedPlugin
Routes HTTP] - VM[ValidateMixin
Validation Pydantic] - end - - subgraph Decorators["📝 Décorateurs"] - D1[@action] - D2[@route] - D3[@validate_payload] - D4[@on / @filter] - end - - subgraph Helpers["🛠️ Helpers"] - H1[ok / error] - H2[Event] - H3[Response types] - end - - Core --> Mixins - Mixins --> Decorators - Decorators --> Helpers - - style Core fill:#E3F2FD,stroke:#1976D2 - style Mixins fill:#FFF3E0,stroke:#F57C00 - style Decorators fill:#E8F5E9,stroke:#388E3C - style Helpers fill:#F3E5F5,stroke:#7B1FA2 -``` - ---- - -## 1. Classes Core +## Classes principales ### `TrustedBase` -Classe de base principale pour les plugins qui s'exécutent dans le **processus principal** XCore. - -```mermaid -classDiagram - class TrustedBase { - #ctx: PluginContext - #logger: Logger - +name: str - +version: str - +async on_load() None - +async on_unload() None - +async on_reload() None - +async handle(action, payload) dict - +get_service(name) Any - +call_plugin(name, action, payload) Future - } - - class AutoDispatchMixin { - +async handle(action, payload) dict - #_find_action_handler(action) Method - } - - class RoutedPlugin { - #_routes: List[Route] - #discover_routes() None - #get_routes() List[Route] - } - - TrustedBase <|-- AutoDispatchMixin - TrustedBase <|-- RoutedPlugin - - note for TrustedBase "Base class pour plugins trusted" - note for AutoDispatchMixin "Routage automatique via @action" - note for RoutedPlugin "Support des routes HTTP" -``` +Classe de base pour les plugins qui s'exécutent dans le processus principal. -#### Cycle de Vie +```python +from xcore.kernel.api.contract import TrustedBase -```mermaid -sequenceDiagram - participant S as Supervisor - participant P as Plugin Instance - participant L as on_load - participant U as on_unload +class Plugin(TrustedBase): + async def handle(self, action: str, payload: dict) -> dict: ... +``` - S->>P: Instancier - S->>L: on_load() - Note over L: Initialiser services
Subscrire événements - L-->>S: ✅ Ready +**Attributs injectés par le framework :** - Note over P: Plugin opérationnel... +| Attribut | Type | Description | +|:---------|:-----|:------------| +| `self.ctx` | `PluginContext` | Contexte complet (services, events, hooks, config) | - S->>U: on_unload() - Note over U: Cleanup ressources
Unsubscribe événements - U-->>S: ✅ Unloaded -``` +**Méthodes :** -#### Implémentation Complète +| Méthode | Signature | Description | +|:--------|:----------|:------------| +| `get_service` | `(name: str) → Any` | Retourne un service du container (typé) | +| `get_service_as` | `(name: str, type_: Type[T]) → T` | Variante avec type explicite | +| `call_plugin` | `(plugin: str, action: str, payload: dict) → dict` | Appel inter-plugins | +| `get_router` | `() → APIRouter \| None` | Surcharger pour exposer des routes HTTP | -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok, error -from pydantic import BaseModel, Field -from typing import Optional +**Clés typées de `get_service` :** -class GreetInput(BaseModel): - """Schema de validation pour l'action greet.""" - name: str = Field(min_length=1, max_length=100, description="Nom à saluer") - language: str = Field(default="fr", description="Langue") +| Clé | Type retourné | +|:----|:--------------| +| `"db"` | `AsyncSQLAdapter` | +| `"cache"` | `CacheService` | +| `"scheduler"` | `SchedulerService` | +| `"syncdb"` | `SQLAdapter` | +| `"mongodb"` | `MongoDBAdapter` | +| `"redisAdapter"` | `RedisAdapter` | -class Plugin(AutoDispatchMixin, TrustedBase): - """ - Plugin de démonstration montrant toutes les fonctionnalités de TrustedBase. - """ - - async def on_load(self) -> None: - """ - 🎯 Point d'entrée au chargement du plugin. - - Initialiser ici : - - Les services (db, cache, scheduler) - - Les subscriptions d'événements - - L'état interne - """ - self.logger.info(f"🚀 Chargement de {self.name} v{self.version}") - - # Initialiser les services - self.db = self.get_service("db") - self.cache = self.get_service("cache") - self.scheduler = self.get_service("scheduler") - - # S'abonner à un événement - self.ctx.events.on("user.created", self._on_user_created, priority=50) - - self.logger.info("✅ Plugin prêt") - - async def on_unload(self) -> None: - """ - 🧹 Point de cleanup au déchargement. - - Nettoyer ici : - - Connexions ouvertes - - Subscriptions événements - - Tâches scheduler - """ - self.logger.info(f"👋 Déchargement de {self.name}") - # Cleanup si nécessaire - - async def on_reload(self) -> None: - """ - 🔄 Appelé lors d'un hot-reload. - - Re-initialiser l'état sans perdre les connexions. - """ - self.logger.info(f"🔄 Rechargement de {self.name}") - await self.on_load() # Ré-initialiser +**Hooks de cycle de vie (optionnels) :** - @action("greet") - async def greet(self, payload: dict) -> dict: - """ - 🎯 Action: Saluer un utilisateur. - - Args: - payload: {"name": "Votre nom", "language": "fr"} - - Returns: - {"status": "ok", "message": "Bonjour, X !"} - """ - # Valider l'input - try: - input_data = GreetInput(**payload) - except ValidationError as e: - return error(msg=str(e), code="invalid_input", status_code=400) - - # Logique métier - greetings = { - "fr": "Bonjour", - "en": "Hello", - "es": "Hola" - } - greeting = greetings.get(input_data.language, "Hello") - message = f"{greeting}, {input_data.name} !" - - self.logger.info(f"Salutation: {message}") - return ok(message=message) - - @on("user.created") - async def _on_user_created(self, event: Event) -> None: - """Handler pour l'événement user.created.""" - user_email = event.data.get("email") - self.logger.info(f"Nouvel utilisateur: {user_email}") +```python +async def on_init(self) -> None: ... # avant injection du contexte +async def on_load(self) -> None: ... # après injection — services disponibles +async def on_start(self) -> None: ... # serveur prêt à recevoir +async def on_reload(self) -> None: ... # après hot-reload +async def on_stop(self) -> None: ... # avant arrêt +async def on_unload(self) -> None: ... # après déchargement ``` --- -### `BasePlugin` +### `BasePlugin` (Protocol) -Interface low-level pour tous les types de plugins. Utilisé pour la logique interne du framework ou pour construire des runners personnalisés. +Interface minimale duck-typing (pas d'héritage requis) : ```python -from xcore.sdk import BasePlugin - -class CustomPluginRunner(BasePlugin): - """ - Implémentation personnalisée d'un runner de plugin. - """ - - async def initialize(self, config: dict) -> None: - """Initialisation personnalisée.""" - pass - - async def execute(self, action: str, payload: dict) -> dict: - """Exécution personnalisée.""" - return {"status": "ok", "result": "custom"} - - async def cleanup(self) -> None: - """Cleanup personnalisé.""" - pass +class BasePlugin(Protocol): + async def handle(self, action: str, payload: dict) -> dict: ... + async def on_init(self) -> None: ... + async def on_start(self) -> None: ... + async def on_stop(self) -> None: ... ``` --- -## 2. Décorateurs - -### `@action(name: str)` - -Définit une méthode comme handler d'action IPC. Nécessite `AutoDispatchMixin`. - -```mermaid -flowchart LR - A[Appel IPC
action="greet"] --> B[AutoDispatchMixin.handle] - B --> C{Rechercher
@action("greet")} - C -->|Trouvé| D[Exécuter handler] - C -->|Non trouvé| E[❌ Error 404] - D --> F[Retourner résultat] +### `AutoDispatchMixin` - style B fill:#E3F2FD,stroke:#1976D2 - style D fill:#E8F5E9,stroke:#388E3C - style E fill:#FFEBEE,stroke:#C62828 -``` +Génère automatiquement `handle()` en routant vers les méthodes `@action`. ```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok +from xcore.sdk.decorators import AutoDispatchMixin, action class Plugin(AutoDispatchMixin, TrustedBase): - @action("ping") - async def ping(self, payload: dict) -> dict: - """Health check simple.""" - return ok(status="pong", timestamp=self._now()) - - @action("add") - async def add(self, payload: dict) -> dict: - """Additionne deux nombres.""" - a = payload.get("a", 0) - b = payload.get("b", 0) - return ok(result=a + b) - - @action("complex_operation") - async def complex_op(self, payload: dict) -> dict: - """Opération avec validation et services.""" - # Accéder au service - db = self.get_service("db") - - # Vérifier le cache - cache_key = f"op:{payload.get('id')}" - cached = await self.cache.get(cache_key) - if cached: - return ok(result=cached, source="cache") - - # Exécuter l'opération - result = await self._do_operation(payload) - - # Caster le résultat - await self.cache.set(cache_key, result, ttl=300) - - return ok(result=result, source="computed") + @action("greet") + async def greet(self, payload: dict) -> dict: + return ok(msg="hello") + + # handle("greet", {}) → appelle self.greet({}) + # handle("unknown", {}) → {"status":"error","code":"unknown_action"} ``` --- -### `@route(path: str, method: str, ...)` - -Définit une route FastAPI directement sur la classe du plugin. Nécessite le mixin `RoutedPlugin`. +### `RoutedPlugin` -```mermaid -flowchart TB - subgraph Plugin["🧩 Plugin"] - R1[@route("/users", GET)] - R2[@route("/users/{id}", GET)] - R3[@route("/users", POST)] - end +Génère automatiquement `get_router()` à partir des méthodes `@route`. - subgraph Mount["📡 FastAPI Mount"] - M1[/plugin/my_plugin/users] - M2[/plugin/my_plugin/users/{id}] - end +```python +from xcore.sdk.decorators import RoutedPlugin, route - R1 --> M1 - R2 --> M2 - R3 --> M1 +class Plugin(RoutedPlugin, AutoDispatchMixin, TrustedBase): - style Plugin fill:#FFF3E0,stroke:#F57C00 - style Mount fill:#E3F2FD,stroke:#1976D2 -``` + @route("/items", method="GET", tags=["items"]) + async def list_items(self): + return [] -#### Paramètres du Décorateur + @route("/items/{item_id}", method="DELETE", + permissions=["admin"]) # RBAC déclaratif + async def delete_item(self, item_id: int): + return {"deleted": item_id} -| Paramètre | Type | Défaut | Description | -| :--- | :--- | :--- | :--- | -| `path` | str | Requis | Chemin de la route | -| `method` | str | "GET" | Méthode HTTP | -| `tags` | list | None | Tags pour OpenAPI | -| `summary` | str | None | Résumé pour docs | -| `status_code` | int | 200 | Code de statut par défaut | -| `dependencies` | list | None | Dépendances FastAPI | -| `permissions` | list | None | Permissions requises | + async def handle(self, action: str, payload: dict) -> dict: + return error("unknown", "unknown_action") +``` -#### Exemple Complet +--- -```python -from xcore.sdk import TrustedBase, RoutedPlugin, route, ok, error -from fastapi import Depends, HTTPException -from pydantic import BaseModel, EmailStr +## Décorateurs -class UserCreate(BaseModel): - email: EmailStr - name: str - password: str +### `@action(name: str)` -class UserResponse(BaseModel): - id: int - email: str - name: str +Marque une méthode comme handler d'action. Utilisé par `AutoDispatchMixin`. -class Plugin(RoutedPlugin, TrustedBase): - """Plugin avec routes HTTP complètes.""" - - @route("/users", method="GET", tags=["users"], summary="Lister les utilisateurs") - async def list_users(self, limit: int = 100, offset: int = 0) -> dict: - """ - Liste tous les utilisateurs avec pagination. - - - **limit**: Nombre maximum d'utilisateurs - - **offset**: Décalage pour la pagination - """ - db = self.get_service("db") - async with db.session() as session: - query = f"SELECT * FROM users LIMIT {limit} OFFSET {offset}" - result = await session.execute(query) - users = result.fetchall() - - return ok(users=[dict(u) for u in users], total=len(users)) - - @route("/users/{user_id}", method="GET", tags=["users"]) - async def get_user(self, user_id: int) -> dict: - """Récupérer un utilisateur par ID.""" - db = self.get_service("db") - async with db.session() as session: - query = "SELECT * FROM users WHERE id = :id" - result = await session.execute(query, {"id": user_id}) - user = result.fetchone() - - if not user: - return error(msg="Utilisateur non trouvé", code="not_found", status_code=404) - - return ok(user=dict(user)) - - @route("/users", method="POST", tags=["users"], status_code=201) - async def create_user(self, data: UserCreate) -> dict: - """Créer un nouvel utilisateur.""" - db = self.get_service("db") - - async with db.session() as session: - query = "INSERT INTO users (email, name) VALUES (:email, :name) RETURNING *" - result = await session.execute(query, dict(data)) - user = result.fetchone() - await session.commit() - - # Émettre un événement - await self.ctx.events.emit("user.created", { - "id": user.id, - "email": user.email - }) - - return ok(user=dict(user)) - - @route("/health", method="GET", tags=["system"]) - async def health_check(self) -> dict: - """Endpoint de santé du plugin.""" - return ok( - plugin=self.name, - version=self.version, - status="healthy" - ) +```python +@action("create_user") +async def create_user(self, payload: dict) -> dict: ... ``` --- -### `@validate_payload(schema: Type[BaseModel], type_response="dict")` - -Valide automatiquement le payload IPC contre un modèle Pydantic. +### `@validate_payload(schema, type_response="dict", unset=True)` -```mermaid -flowchart TB - A[Payload Reçu] --> B[@validate_payload] - B --> C{Validation} - C -->|✅ Valide| D[Handler exécuté] - C -->|❌ Invalide| E[Erreur 400] +Valide le payload avec Pydantic avant d'appeler le handler. - D --> F{type_response ?} - F -->|dict| G[Dict validé] - F -->|pydantic| H[Instance Pydantic] - - style B fill:#E3F2FD,stroke:#1976D2 - style E fill:#FFEBEE,stroke:#C62828 -``` - -#### Modes de Retour +| Paramètre | Type | Description | +|:----------|:-----|:------------| +| `schema` | `Type[BaseModel]` ou `dict` | Schéma de validation | +| `type_response` | `"dict"` ou `"model"` | `"dict"` = `.model_dump()`, `"model"` = instance Pydantic | +| `unset` | `bool` | `True` = exclure les champs non fournis du dict | ```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok -from xcore.sdk.decorators import validate_payload from pydantic import BaseModel -class CreateUserInput(BaseModel): - email: str - name: str - age: int = Field(ge=0, le=150) +class OrderPayload(BaseModel): + product_id: int + quantity: int = 1 + note: str | None = None -class Plugin(AutoDispatchMixin, TrustedBase): +@action("create_order") +@validate_payload(OrderPayload, type_response="model") +async def create_order(self, payload: OrderPayload) -> dict: + # payload est un OrderPayload typé + return ok(product=payload.product_id, qty=payload.quantity) +``` - # Mode 1: Retourne un dict validé - @action("create_user_dict") - @validate_payload(CreateUserInput, type_response="dict") - async def create_user_dict(self, payload: dict) -> dict: - # payload est un dict validé - email = payload["email"] - name = payload["name"] - return ok(email=email, name=name) - - # Mode 2: Retourne l'instance Pydantic - @action("create_user_model") - @validate_payload(CreateUserInput, type_response="pydantic") - async def create_user_model(self, payload: CreateUserInput) -> dict: - # payload est une instance CreateUserInput - email = payload.email - name = payload.name - age = payload.age - return ok(email=email, name=name, age=age) +En cas d'erreur de validation, retourne automatiquement : +```json +{"status": "error", "msg": "Validation error", "code": "validation_error", "errors": [...]} ``` --- -### `@trusted` / `@sandboxed` +### `@require_service(*names: str)` -Markers pour s'assurer qu'une méthode s'exécute uniquement dans le mode spécifié. +Vérifie que les services sont disponibles avant l'exécution. ```python -from xcore.sdk import TrustedBase, trusted, sandboxed, action - -class Plugin(TrustedBase): - - @action("trusted_only") - @trusted - async def trusted_operation(self, payload: dict) -> dict: - """Ne s'exécute que en mode trusted.""" - return ok(result="trusted") - - @action("sandboxed_only") - @sandboxed - async def sandboxed_operation(self, payload: dict) -> dict: - """Ne s'exécute que en mode sandboxed.""" - return ok(result="sandboxed") +@action("save") +@require_service("db", "cache") +async def save(self, payload: dict) -> dict: ... ``` ---- - -### `@on(event_name: str, priority: int = 50)` - -Subscribe un handler à un événement. +Lève `KeyError` avec message clair si un service est absent. -```python -from xcore.sdk import TrustedBase, on, Event +--- -class Plugin(TrustedBase): +### `@route(path, method, *, tags, summary, status_code, response_model, dependencies, permissions, scopes)` - @on("user.created", priority=100) - async def handle_user_created(self, event: Event): - """Handler avec haute priorité.""" - pass +Déclare une route HTTP FastAPI sur le plugin. - @on("order.completed", priority=50) - async def handle_order_completed(self, event: Event): - """Handler avec priorité normale.""" - pass -``` +| Paramètre | Défaut | Description | +|:----------|:-------|:------------| +| `path` | — | Chemin relatif (ex : `"/items/{id}"`) | +| `method` | `"GET"` | Verbe HTTP | +| `tags` | `[]` | Tags OpenAPI | +| `summary` | nom de la méthode | Description OpenAPI | +| `status_code` | `200` | Code HTTP de succès | +| `response_model` | `None` | Modèle Pydantic de réponse | +| `dependencies` | `[]` | `Depends()` FastAPI par route | +| `permissions` | `[]` | Permissions RBAC requises | +| `scopes` | `[]` | OAuth2 scopes | --- -### `@filter(hook_name: str)` et `@action_hook(hook_name: str)` +### `@trusted` / `@sandboxed` -Enregistre un hook filter ou action. +Marqueurs informatifs — n'affectent pas l'exécution mais documentent l'intention. ```python -from xcore.sdk import TrustedBase, filter, action_hook - -class Plugin(TrustedBase): - - @filter("page_title") - def modify_title(self, title: str) -> str: - """Filter: modifie le titre de page.""" - return f"{title} | Mon App" - - @action_hook("before_render") - def before_render_log(self, template: str): - """Action hook: log avant rendu.""" - self.logger.info(f"Rendu: {template}") +@action("admin_action") +@trusted +async def admin_action(self, payload: dict) -> dict: ... ``` --- -## 3. Mixins - -### `AutoDispatchMixin` +## Fonctions helpers -Fournit une implémentation automatique de `handle()` en routant vers les méthodes décorées avec `@action`. - -```mermaid -flowchart TB - A[IPC: handle("greet", payload)] --> B[AutoDispatchMixin.handle] - B --> C[Rechercher @action("greet")] - C --> D{Trouvé ?} - D -->|Oui| E[Exécuter greet(payload)] - D -->|Non| F[Erreur: Action inconnue] - E --> G[Retourner résultat] - F --> H[Retourner error()] - - style B fill:#E3F2FD,stroke:#1976D2 - style E fill:#E8F5E9,stroke:#388E3C - style F fill:#FFEBEE,stroke:#C62828 -``` +### `ok(**kwargs) → dict` ```python -# Sans AutoDispatchMixin (verbeux) -class Plugin(TrustedBase): - async def handle(self, action: str, payload: dict) -> dict: - if action == "ping": - return await self.ping(payload) - elif action == "greet": - return await self.greet(payload) - else: - return error(f"Action inconnue: {action}", code="not_found") - -# Avec AutoDispatchMixin (concis) -class Plugin(AutoDispatchMixin, TrustedBase): +from xcore.kernel.api.contract import ok - @action("ping") - async def ping(self, payload: dict) -> dict: - return ok(status="pong") - - @action("greet") - async def greet(self, payload: dict) -> dict: - return ok(message=f"Hello !") +return ok(user={"id": 1}, created=True) +# → {"status": "ok", "user": {"id": 1}, "created": True} ``` ---- - -### `RoutedPlugin` - -Active la découverte automatique et le montage des routes FastAPI. +### `error(msg, code=None, **kwargs) → dict` ```python -from xcore.sdk import TrustedBase, RoutedPlugin, route +from xcore.kernel.api.contract import error -class Plugin(RoutedPlugin, TrustedBase): - """Les routes sont automatiquement montées sur /plugin/{name}/...""" - - @route("/hello", methods=["GET"]) - async def hello(self): - return {"message": "Hello"} - - @route("/hello/{name}", methods=["GET"]) - async def hello_name(self, name: str): - return {"message": f"Hello, {name}!"} +return error("Utilisateur introuvable", code="not_found", user_id=42) +# → {"status": "error", "msg": "Utilisateur introuvable", "code": "not_found", "user_id": 42} ``` -**Routes montées :** -- `GET /plugin/my_plugin/hello` -- `GET /plugin/my_plugin/hello/{name}` - --- -## 4. Helpers - -### `ok(**kwargs) -> dict` +## PluginManifest (dataclass) -Génère une réponse de succès standardisée. +Représentation typée du `plugin.yaml` parsé. ```python -from xcore.sdk import ok - -# Simple -ok(message="Succès") -# → {"status": "ok", "message": "Succès"} - -# Multiple -ok(user_id=123, email="test@example.com") -# → {"status": "ok", "user_id": 123, "email": "test@example.com"} - -# Avec données complexes -ok(users=[...], total=100, page=1) -# → {"status": "ok", "users": [...], "total": 100, "page": 1} +@dataclass +class PluginManifest: + name: str + version: str + plugin_dir: Path + author: str = "unknown" + description: str = "" + framework_version: str = ">=2.0" + entry_point: str = "src/main.py" + execution_mode: ExecutionMode = ExecutionMode.LEGACY + requires: list[PluginDependency] = field(default_factory=list) + allowed_imports: list[str] = field(default_factory=list) + permissions: list[dict] = field(default_factory=list) + env: dict[str, str] = field(default_factory=dict) + resources: ResourceConfig = field(default_factory=ResourceConfig) + runtime: RuntimeConfig = field(default_factory=RuntimeConfig) + filesystem: FilesystemConfig = field(default_factory=FilesystemConfig) + extra: dict[str, Any] = field(default_factory=dict) ``` -### `error(msg: str, code: str, status_code: int = 400) -> dict` - -Génère une réponse d'erreur standardisée. +### `ResourceConfig` ```python -from xcore.sdk import error - -# Erreur de validation -error(msg="Champ requis manquant", code="validation_error", status_code=400) -# → {"status": "error", "msg": "Champ requis manquant", "code": "validation_error"} - -# Erreur not found -error(msg="Ressource non trouvée", code="not_found", status_code=404) -# → {"status": "error", "msg": "Ressource non trouvée", "code": "not_found"} - -# Erreur serveur -error(msg="Erreur interne", code="internal_error", status_code=500) +@dataclass +class ResourceConfig: + timeout_seconds: int = 10 + max_memory_mb: int = 128 + max_disk_mb: int = 50 + rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig) + +@dataclass +class RateLimitConfig: + calls: int = 100 + period_seconds: int = 60 ``` ---- - -## 5. Repositories & Data Access - -### `BaseAsyncRepository` - -Classe de base pour les interactions asynchrones avec la base de données. +### `PluginDependency` ```python -from xcore.repositories import BaseAsyncRepository -from typing import TypeVar, Generic, List, Optional -from pydantic import BaseModel - -class User(BaseModel): - id: int - email: str +@dataclass +class PluginDependency: name: str - -class UserRepository(BaseAsyncRepository): - """Repository pour les opérations sur les utilisateurs.""" - - async def get_by_email(self, email: str) -> Optional[User]: - """Récupérer un utilisateur par email.""" - query = "SELECT * FROM users WHERE email = :email" - result = await self.session.execute(query, {"email": email}) - row = result.fetchone() - return User(**dict(row)) if row else None - - async def list_all(self, limit: int = 100) -> List[User]: - """Lister tous les utilisateurs.""" - query = "SELECT * FROM users LIMIT :limit" - result = await self.session.execute(query, {"limit": limit}) - return [User(**dict(row)) for row in result.fetchall()] - - async def create(self, data: dict) -> User: - """Créer un nouvel utilisateur.""" - query = "INSERT INTO users (email, name) VALUES (:email, :name) RETURNING *" - result = await self.session.execute(query, data) - row = result.fetchone() - await self.session.commit() - return User(**dict(row)) + version_constraint: str = "*" # "*" ou ">=1.0,<2.0" ``` ---- - -## 6. RBAC Checker - -Dépendance FastAPI pour appliquer le contrôle d'accès basé sur les rôles. - -```python -from fastapi import Depends -from xcore.security import RBACChecker, require_role - -checker = RBACChecker() - -@route("/admin/users", methods=["GET"], dependencies=[Depends(require_role("admin"))]) -async def list_users_admin(): - """Route protégée: nécessite le rôle admin.""" - pass - -@route("/profile", methods=["GET"], dependencies=[Depends(require_role("user"))]) -async def get_profile(): - """Route protégée: nécessite le rôle user.""" - pass +Formats YAML supportés : +```yaml +requires: + - other_plugin # version="*" + - name: other_plugin + version: ">=2.0,<3.0" ``` - ---- - -## 7. Tableau Récapitulatif - -| Composant | Usage | Exemple | -| :--- | :--- | :--- | -| `TrustedBase` | Base class plugins | `class Plugin(TrustedBase)` | -| `AutoDispatchMixin` | Routage @action | `class Plugin(AutoDispatchMixin, TrustedBase)` | -| `RoutedPlugin` | Routes HTTP | `@route("/path")` | -| `@action` | Handler IPC | `@action("greet")` | -| `@route` | Route HTTP | `@route("/users/{id}")` | -| `@validate_payload` | Validation Pydantic | `@validate_payload(UserSchema)` | -| `@on` | Subscription événement | `@on("user.created")` | -| `@filter` | Hook filter | `@filter("page_title")` | -| `ok()` | Réponse succès | `ok(message="OK")` | -| `error()` | Réponse erreur | `error(msg="Error", code="bad")` | - ---- - -## Prochaines Lectures - -| 📚 Guide | Objectif | -| :--- | :--- | -| [Creating Plugins](../guides/creating-plugins.md) | Tutoriel complet | -| [Security](../guides/security.md) | Bonnes pratiques sécurité | -| [Events](../guides/events.md) | Système d'événements | diff --git a/doc/versions.md b/doc/versions.md index 1b8dd840..11368680 100644 --- a/doc/versions.md +++ b/doc/versions.md @@ -1,24 +1,61 @@ -# Versions de XCore +# Versions -## v2.1.2 (Version Actuelle) -- **Stabilité** : Correction de 13 échecs de tests critiques. -- **Sécurité** : Scanner AST amélioré pour détecter les contournements via alias d'imports. -- **Performance** : Intégration de `pytest-benchmark` pour le suivi des performances noyau. -- **Qualité** : Hooks `pre-commit` synchronisés avec `make lint-fix` et tests automatiques. +## v2.1.2 — Actuelle (2026-04-29) + +### Corrections +- 13 échecs de tests critiques résolus (kernel, permissions, sandbox). +- Scanner AST : détection des contournements via alias d'imports (`import os as système`). + +### Performances +- Cache LRU sur `PermissionEngine` : **+34 % de débit**, latence moyenne 113 µs (vs 152 µs sans cache). +- `mset`/`mget` natif sur `RedisCacheBackend` : **44× plus rapide** sur les SET batch, **77×** sur GET. +- Compilation regex pré-calculée dans `Policy.matches()` : short-circuit action en **0.4 µs**. + +### Qualité +- `pytest-benchmark` intégré pour le suivi continu des performances. +- Hooks `pre-commit` synchronisés : `black` + `isort` + `flake8` + tests automatiques au commit. +- `pyproject.toml` migré vers PEP 621 (`[project]`) — compatible Poetry 2.x. + +### Chiffres mesurés (Python 3.14) + +| Composant | Métrique | Valeur | +|:----------|:---------|-------:| +| `Policy.matches` — match | Latence moy. | 1.6 µs | +| `Policy.matches` — short-circuit action | Latence moy. | 0.4 µs | +| `PermissionEngine` avec cache (6 checks) | Latence moy. | 113 µs | +| `PermissionEngine` sans cache (6 checks) | Latence moy. | 152 µs | +| `Redis mset` 100 clés (2 ms réseau) | Latence totale | 5.3 ms | +| `Redis sequential set` 100 clés | Latence totale | 232 ms | + +→ [Page Benchmarks complète](reference/benchmarks.md) + +--- ## v2.0.0 -- **Architecture Plugin-First** : Nouveau noyau modulaire. -- **Sandboxing Avancé** : Isolation par sous-processus et FilesystemGuard. -- **Service Container** : Injection de dépendances pour DB, Cache et Scheduler. -- **Middleware Pipeline** : Chaîne d'exécution pré-compilée pour haute performance. -- **SDK Enrichi** : Décorateurs `@action`, `@route`, `@validate_payload`. -## v1.x (Legacy) +- **Architecture Plugin-First** : noyau modulaire, séparation Kernel / Services / Plugins. +- **Sandboxing avancé** : isolation par sous-processus OS, communication JSON-RPC 2.0. +- **ServiceContainer** : injection de dépendances pour DB (SQLAlchemy 2.0), Cache (Redis/Memory), Scheduler (APScheduler). +- **MiddlewarePipeline** : chaîne Tracing → RateLimit → Permissions → Retry pré-compilée. +- **SDK enrichi** : décorateurs `@action`, `@route`, `@validate_payload`, `AutoDispatchMixin`, `RoutedPlugin`. +- **RBAC** : `AuthBackend` pluggable + `RBACChecker` déclaratif sur les routes. +- **StateMachine** : FSM par plugin avec transitions validées. +- **PluginRegistry** : métadonnées, dépendances, versioning semver. + +--- + +## v1.x — Legacy + - Première version stable basée sur FastAPI. - Système de plugins monolithique sans isolation. - Support limité pour les services asynchrones. -## Roadmap Future -- **v2.1.0** : Support natif de gRPC pour l'IPC haute performance. -- **v2.2.0** : Gestionnaire de secrets intégré (Vault). -- **v3.0.0** : Clusterisation multi-nœuds native. +--- + +## Roadmap + +| Version | Fonctionnalité prévue | +|:--------|:----------------------| +| **v2.2.0** | Gestionnaire de secrets intégré (Vault / SOPS) | +| **v2.3.0** | Support natif gRPC pour l'IPC haute performance | +| **v3.0.0** | Clusterisation multi-nœuds native | diff --git a/docgen/config.yaml b/docgen/config.yaml index 8901d027..ffda6524 100644 --- a/docgen/config.yaml +++ b/docgen/config.yaml @@ -1,6 +1,6 @@ project: name: XCore - repo_path: /home/eliezer/devs/xcore/xcore + repo_path: ./xcore docs_dir: IAdocs # remplace output_file — la doc est maintenant un dossier cache: diff --git a/docs-tech/LICENSE b/docs-tech/LICENSE deleted file mode 100644 index 7729fac9..00000000 --- a/docs-tech/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Xcore team's - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docs-tech/README.md b/docs-tech/README.md deleted file mode 100644 index 41fe3da7..00000000 --- a/docs-tech/README.md +++ /dev/null @@ -1,200 +0,0 @@ -# ⚡ XCore Framework - -[![Version](https://img.shields.io/badge/version-2.0.0-blue.svg)](https://github.com/traoreera/xcore) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) -[![FastAPI](https://img.shields.io/badge/FastAPI-0.118+-green.svg)](https://fastapi.tiangolo.com/) - -**XCore** is a high-performance, plugin-first orchestration framework built on top of **FastAPI**. It is designed to load, isolate, and manage modular extensions (plugins) in a secure, sandboxed environment. - ---- - -## ✨ Key Features - -- 🧩 **Dynamic Plugin System**: Hot-reload, load, and unload plugins at runtime without restarting the server. -- 🛡️ **Sandboxing & Security**: Isolated execution using sub-processes with strict resource limits (CPU, Memory, Timeouts) and AST validation. -- 🔌 **Native Service Integration**: Built-in support for SQL (PostgreSQL, MySQL, SQLite), NoSQL (Redis), and Task Scheduling (APScheduler). -- 🛰️ **Event-Driven Architecture**: Powerful Event Bus and Hook system for seamless inter-plugin communication. -- 🔐 **Signature Verification**: Ensure plugin integrity with HMAC-SHA256 signature validation. -- 🛠️ **Developer Friendly**: Built-in CLI, structured logging, and comprehensive documentation. - ---- - -## 🏗️ Architecture Overview - -XCore follows a "minimal core" philosophy where most features are provided via plugins or shared services. - -```mermaid -flowchart TB - subgraph Core["XCore Kernel"] - X[Orchestrator] --> PS[Plugin Supervisor] - X --> EB[Event Bus] - X --> SC[Service Container] - end - - PS --> Trusted[Trusted Plugins] - PS --> Sandbox[Sandboxed Plugins] - - SC --> DB[(Database)] - SC --> RD[(Redis)] - SC --> SCH[Scheduler] -``` - ---- - -## 🚀 Getting Started - -### Prerequisites - -- **Python 3.11+** -- **Poetry** (Package Manager) - -### Installation - -1. **Clone the repository**: - ```bash - git clone https://github.com/traoreera/xcore - cd xcore - ``` - -2. **Install dependencies**: - ```bash - poetry install - ``` - -3. **Run the development server**: - ```bash - make run-dev - ``` - -### or use pip to install and use it -```bash -uv add https://github.com/traoreera/xcore -``` - ---- - -## 💻 Usage - -### 1. Integration with FastAPI - -```python -from fastapi import FastAPI -from xcore import Xcore -from contextlib import asynccontextmanager - -xcore = Xcore(config_path="xcore.yaml") - -@asynccontextmanager -async def lifespan(app: FastAPI): - await xcore.boot(app) - yield - await xcore.shutdown() - -app = FastAPI(lifespan=lifespan) -``` - -### 2. Standalone Usage - -```python -from xcore import Xcore - -async def main(): - app = Xcore() - await app.boot() - - # Call a plugin action - result = await app.plugins.call("users_plugin", "get_user", {"id": 1}) - print(result) - - await app.shutdown() -``` - ---- - -## 🔌 Plugin Development - -Plugins reside in the `plugins/` directory. A standard plugin structure looks like this: - -```text -plugins/my_plugin/ -├── plugin.yaml # Manifest (metadata & entry point) -├── plugin.sig # Security signature (for trusted plugins) -└── src/ - └── main.py # Core logic -``` - -### Example `plugin.yaml` -```yaml -name: my_plugin -version: "2.0.0" -author: Your Name -description: "A sample plugin" -execution_mode: trusted # or "sandboxed" -framework_version: ">=2.0" -entry_point: src/main.py - -permissions: - - resource: "cache.*" - actions: ["read", "write"] - effect: allow - -resources: - timeout_seconds: 30 - rate_limit: - calls: 100 - period_seconds: 60 -``` - ---- - -## 📚 Documentation - -For complete documentation, visit the [XCore Documentation Site](https://xcore.readthedocs.io). - -- [Installation Guide](getting-started/installation.md) -- [Quick Start](getting-started/quickstart.md) -- [Creating Plugins](guides/creating-plugins.md) -- [Architecture](architecture/overview.md) -- [API Reference](reference/api.md) -- [Versions & Changelog](versions.md) - ---- - -## 🛠️ CLI Reference - -XCore comes with a powerful CLI for management and security. - -| Command | Description | -| :--- | :--- | -| `xcore plugin list` | List all loaded plugins | -| `xcore plugin load ` | Load a specific plugin | -| `xcore plugin reload ` | Hot-reload a plugin | -| `xcore plugin sign ` | Generate a security signature for a plugin | -| `xcore plugin validate `| Validate plugin manifest and structure | -| `xcore services status` | Check the health of DB, Cache, and Scheduler | -| `xcore health` | Perform a global system health check | - ---- - -## 📜 Makefile Commands - -| Command | Description | -| :--- | :--- | -| `make init` | Initialize project (install + run) | -| `make test` | Run the test suite | -| `make lint-fix` | Auto-format code (Black, Isort) | -| `make docker-dev` | Spin up development environment with Docker | -| `make logs-live` | View real-time structured logs | - ---- - -## 📄 License - -This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. - ---- - -

- Built with ❤️ by Eliezer Traore -

diff --git a/docs-tech/api/README.md b/docs-tech/api/README.md deleted file mode 100644 index 2930edab..00000000 --- a/docs-tech/api/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# API Documentation - -Auto-generated API documentation. - -This directory is reserved for API documentation generated from source code. - -To generate API docs: - -```bash -# Using mkdocstrings -poetry run mkdocs build - -# Or manually with pydoc -python -m pydoc xcore.Xcore -``` - -For now, see the [API Reference](../reference/api.md) for complete API documentation. diff --git a/docs-tech/architecture/decisions.md b/docs-tech/architecture/decisions.md deleted file mode 100644 index dd955041..00000000 --- a/docs-tech/architecture/decisions.md +++ /dev/null @@ -1,82 +0,0 @@ -# Technical Decisions & Patterns - -This document explains the "Why" behind the architectural choices in XCore. It is intended for developers who want to understand the framework's internal logic and design philosophy. - ---- - -## 1. Modular Monolith vs. Microservices - -**Decision**: XCore implements a Modular Monolith. - -- **Why?**: Microservices introduce significant overhead (network latency, distributed consistency, deployment complexity). A Modular Monolith allows for clean separation of concerns and independent plugin development while maintaining the performance and simplicity of a single binary. -- **Pattern**: Separation by Plugins with shared Kernel Orchestration. - ---- - -## 2. Strategy Pattern for Plugin Execution - -**Decision**: Use the Strategy Pattern for `Trusted` vs. `Sandboxed` execution. - -- **Why?**: We needed a way to run plugins with different levels of trust without changing the core management logic. -- **Implementation**: - - `BaseActivator`: The interface. - - `TrustedActivator`: In-process loading via `importlib`. - - `SandboxedActivator`: Out-of-process execution via `multiprocessing`. -- **Benefit**: Easy to add future execution modes (e.g., WASM, Docker) by implementing a new strategy. - ---- - -## 3. JSON-RPC 2.0 for IPC - -**Decision**: Choose JSON-RPC 2.0 over OS Pipes for Sandbox communication. - -- **Why?**: - - **Standardization**: JSON-RPC is a well-known, simple protocol. - - **Compatibility**: It only requires basic string serialization, avoiding the security risks of `pickle` or the complexity of `gRPC`/`Protobuf`. - - **Performance**: For local OS pipes, the overhead of JSON serialization is negligible compared to the isolation benefits. - ---- - -## 4. AST-Based Security Scanning - -**Decision**: Perform static analysis on plugin code before execution. - -- **Why?**: Relying solely on OS-level isolation isn't enough for Python, where "monkey-patching" and introspection are common. The `ASTScanner` acts as a "firewall" for the Python runtime. -- **Mechanism**: Blocks `Import` and `Attribute` nodes that match a forbidden list (e.g., `os`, `sys`, `__globals__`). -- **Benefit**: Prevents sandbox escapes before they can even be attempted. - ---- - -## 5. DAG-Based Dependency Resolution - -**Decision**: Use Kahn's Algorithm for topological sorting of plugins. - -- **Why?**: Plugins often depend on services or events provided by other plugins. We need a deterministic load order to prevent "Service Not Found" errors during boot. -- **Benefit**: Detects circular dependencies at boot time and ensures a stable, wave-based loading sequence. - ---- - -## 6. Event Bus vs. Direct Calls - -**Decision**: Use both. - -- **Direct Calls (`supervisor.call`)**: Used when a result is expected immediately (Request/Response). It passes through the security middleware. -- **Event Bus (`ctx.events.emit`)**: Used for decoupled side effects (Pub/Sub). It allows multiple plugins to react to a single action (e.g., "User Created") without the caller knowing about the subscribers. - ---- - -## 7. Configuration: YAML + Environment Variables - -**Decision**: Use YAML for structure and `${VAR}` syntax for secrets. - -- **Why?**: YAML is human-readable and supports complex hierarchies. Environment variable injection ensures that secrets (like `DB_PASSWORD`) never end up in version-controlled configuration files. -- **Pattern**: "Zero Config" defaults in Python dataclasses, overridden by YAML, overridden by Environment Variables. - ---- - -## 8. Service Scoping & Protection - -**Decision**: Prevent plugins from overwriting core kernel services. - -- **Why?**: To prevent malicious or buggy plugins from "hijacking" the database or cache service used by other plugins. -- **Implementation**: A `PROTECTED_SERVICES` list in the `LifecycleManager` blocks registration of services with reserved names. diff --git a/docs-tech/architecture/overview.md b/docs-tech/architecture/overview.md deleted file mode 100644 index 06eecf25..00000000 --- a/docs-tech/architecture/overview.md +++ /dev/null @@ -1,530 +0,0 @@ -# Architecture Overview - -Plongée approfondie dans l'architecture interne de XCore : comment le kernel, les services et les plugins interagissent. - ---- - -## 1. Vue d'Ensemble Haute Niveau - -XCore suit le pattern **Modular Monolith**. Tous les plugins s'exécutent dans un environnement orchestré unifié, mais sont strictement isolés via des frontières logiques (injection de contexte) et physiques (sandbox au niveau processus). - -```mermaid -flowchart TB - subgraph Core["🎯 XCore Kernel"] - direction TB - X[⚙️ Xcore Engine
Point d'entrée principal] - PS[🔄 Plugin Supervisor
Gestion cycle de vie] - SC[🔧 Service Container
Registry services] - EB[📡 Event Bus
Messagerie async] - PE[🛡️ Permission Engine
Contrôle d'accès] - end - - subgraph Infra["🏗️ Infrastructure Partagée"] - DB[(🗄️ Database
SQL/NoSQL)] - CACHE[(⚡ Cache
Redis/Memory)] - SCHED[⏰ Scheduler
APScheduler] - LOG[📝 Logger
Structuré] - end - - subgraph Plugins["🧩 Écosystème Plugins"] - direction LR - TP[🔐 Trusted Plugins
Processus principal] - SP[📦 Sandboxed Plugins
Processus isolé] - end - - subgraph App["🌐 Application"] - FA[FastAPI
Routes HTTP] - WS[WebSocket
Real-time] - end - - App --> X - X --> PS - X --> SC - X --> EB - X --> PE - - SC --> Infra - PS --> TP - PS --> SP - - EB -.->|Pub/Sub| TP - EB -.->|Pub/Sub| SP - PE -.->|Audit| PS - - style Core fill:#E3F2FD,stroke:#1976D2,stroke-width:2px - style Infra fill:#E8F5E9,stroke:#388E3C,stroke-width:2px - style Plugins fill:#FFF3E0,stroke:#F57C00,stroke-width:2px - style App fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px -``` - -### Composants Clés - -| Composant | Rôle | Analogie | -| :--- | :--- | :--- | -| **Xcore Engine** | Point d'entrée, coordonne le boot | Chef d'orchestre | -| **PluginSupervisor** | Lifecycle, hot-reload, IPC | Gestionnaire de processus | -| **ServiceContainer** | Registry DB, Cache, Scheduler | Boîte à outils partagée | -| **EventBus** | Dispatcher async Observer pattern | Système de notification | -| **PermissionEngine** | Policy-based access control | Garde de sécurité | - ---- - -## 2. Boot Sequence Détaillée - -Le framework suit une séquence d'initialisation stricte pour garantir que toutes les dépendances sont résolues avant le démarrage des plugins. - -```mermaid -sequenceDiagram - participant Dev as Développeur - participant Main as main.py - participant X as Xcore Engine - participant Cfg as Config Loader - participant S as Services - participant L as PluginLoader - participant DAG as Dependency Graph - participant P as Plugins - participant R as Router - - Dev->>Main: uvicorn app:app - Main->>X: boot(app) - - Note over X: 📋 Phase 1: Configuration - X->>Cfg: Charger xcore.yaml - X->>Cfg: Charger .env - Cfg-->>X: Config validée - - Note over X: 🔧 Phase 2: Services - X->>S: Initialiser DB - X->>S: Initialiser Cache - X->>S: Initialiser Scheduler - S-->>X: ✅ Services Ready - - Note over X: 🧩 Phase 3: Plugins - X->>L: Découvrir plugins/ - L->>L: Parser plugin.yaml - L->>DAG: Build dependency graph - DAG->>DAG: Topological sort - DAG-->>L: Ordre de chargement - - loop Chaque vague de plugins - L->>P: Load plugin (trusted/sandboxed) - P->>P: on_load() - P-->>L: ✅ Ready - end - - Note over X: 🌐 Phase 4: Routage - X->>R: Monter routes FastAPI - R->>R: Plugin routes - R->>R: API routes - R-->>X: ✅ Routes montées - - X-->>Main: Application prête - Main-->>Dev: 🚀 Running on :8000 -``` - -### Détails des Phases - -```mermaid -gantt - title Boot Sequence Timeline - dateFormat X - axisFormat %L - - section Configuration - Charger YAML :a1, 0, 10 - Charger ENV :a2, after a1, 5 - Validation :a3, after a2, 5 - - section Services - Database :b1, after a3, 20 - Cache :b2, after a3, 15 - Scheduler :b3, after a3, 10 - - section Plugins - Discovery :c1, after b1, 10 - DAG Build :c2, after c1, 15 - Load Wave 1 :c3, after c2, 30 - Load Wave 2 :c4, after c3, 30 - - section Routing - Mount Routes :d1, after c4, 10 -``` - ---- - -## 3. Machine d'État des Plugins - -Chaque plugin est géré par une **Finite State Machine (FSM)** pour assurer des transitions sûres pendant les hot-reloads ou en cas d'échec. - -```mermaid -stateDiagram-v2 - [*] --> UNLOADED: Découverte - - UNLOADED --> LOADING: supervisor.load() - - state LOADING { - [*] --> SCANNING: AST Scan - SCANNING --> VALIDATING: Scan OK - VALIDATING --> INSTANTIATING: Manifest OK - INSTANTIATING --> INITIALIZING: Entry point OK - INITIALIZING --> READY: on_load() OK - end - - LOADING --> FAILED: Erreur AST - LOADING --> FAILED: Erreur Manifest - LOADING --> FAILED: Erreur on_load() - - READY --> RELOADING: supervisor.reload() - - state RELOADING { - [*] --> UNLOADING: on_unload() - UNLOADING --> RELOADING_CODE: Recharger code - RELOADING_CODE --> RE_INITIALIZING: Nouveau on_load() - RE_INITIALIZING --> READY: Succès - end - - RELOADING --> FAILED: Échec reload - - READY --> UNLOADED: supervisor.unload() - FAILED --> UNLOADED: Cleanup - FAILED --> RELOADING: Retry (max 3) - - note right of READY - 🟢 État stable - - Routes actives - - Événements abonnés - - Services connectés - end note - - note right of FAILED - 🔴 État terminal - - Erreur enregistrée - - Cleanup requis - - Notification envoyée - end note - - note right of UNLOADED - ⚪ État initial/final - - Code non chargé - - Aucune ressource - end note -``` - -### Codes d'État - -| État | Signification | Actions Possibles | -| :--- | :--- | :--- | -| `UNLOADED` | Plugin découvert mais non chargé | `load()` | -| `LOADING` | Initialisation en cours | Attendre | -| `READY` | Plugin opérationnel | `reload()`, `unload()`, `call()` | -| `RELOADING` | Mise à jour à chaud | Attendre | -| `FAILED` | Erreur irrécupérable | `unload()`, `reload()` (retry) | - ---- - -## 4. Communication Inter-Plugins (IPC) - -Quand le **Plugin A** appelle le **Plugin B**, l'appel n'est **jamais direct**. Il transite par le **Supervisor Pipeline** du kernel. - -### Architecture du Pipeline - -```mermaid -flowchart LR - subgraph Caller["📤 Plugin A (Appelant)"] - A[Code métier] - end - - subgraph Kernel["🎯 XCore Supervisor Pipeline"] - direction TB - M1[📍 Tracing
Génère span] - M2[🛑 Rate Limiting
Vérifie quota] - M3[🔐 Permission Audit
Valide autorisation] - M4[🔄 Retry Logic
Gère échecs] - end - - subgraph Target["📥 Plugin B (Cible)"] - B[Handler @action] - end - - A -- "ctx.plugins.call()" --> M1 - M1 --> M2 - M2 --> M3 - M3 --> M4 - M4 -- "execute" --> B - B -- "result" --> A - - style Kernel fill:#FFEBEE,stroke:#C62828,stroke-width:2px - style M3 fill:#C62828,color:#fff -``` - -### Détail des Étapes du Pipeline - -```mermaid -sequenceDiagram - participant A as Plugin A - participant T as Tracing - participant R as RateLimit - participant P as PermissionEngine - participant L as RetryLogic - participant B as Plugin B - - A->>T: call("plugin_b", "action", payload) - - Note over T: 1. Tracing - T->>T: Créer span ID - T->>T: Injecter context - - Note over R: 2. Rate Limiting - R->>R: Vérifier quota A - R-->>T: ✅ Dans la limite - - Note over P: 3. Permission Audit - P->>P: Charger policies - P->>P: Vérifier A → B.action - P-->>T: ✅ Autorisé - - Note over L: 4. Retry Logic - L->>B: Exécuter action - alt Succès - B-->>L: Résultat - L-->>A: ✅ Résultat - else Échec transitoire - B-->>L: Erreur - L->>L: Retry (max 3) - L-->>A: ✅ Résultat (retry) - else Échec permanent - L-->>A: ❌ Erreur - end -``` - -### Middleware Stack Détaillé - -| Étape | Rôle | Détails | -| :--- | :--- | :--- | -| **Tracing** | Observabilité | Génère un span OpenTelemetry, propage le context | -| **Rate Limiting** | Protection | Vérifie le quota du caller (calls/période) | -| **Permission Audit** | Sécurité | Valide la policy `resource: action` | -| **Retry Logic** | Résilience | Retry exponentiel sur erreurs transitoires | - ---- - -## 5. Modèle de Sandboxing - -Les plugins sandboxed s'exécutent dans un **processus OS dédié**. C'est le niveau d'isolation le plus élevé. - -### Architecture du Sandbox - -```mermaid -flowchart TB - subgraph Kernel["🎯 Kernel Process"] - K[XCore Engine] - PS[PluginSupervisor] - WR[Worker Manager] - end - - subgraph IPC["📡 JSON-RPC 2.0 Channel"] - direction LR - PIPE1[Pipe stdout] - PIPE2[Pipe stdin] - end - - subgraph Sandbox["📦 Sandbox Process"] - SW[Sandbox Worker] - AST[🛡️ AST Scanner] - EX[Code Exécuté] - end - - K --> PS - PS --> WR - WR --> IPC - IPC --> Sandbox - - style Sandbox fill:#FFEBEE,stroke:#C62828 - style AST fill:#C62828,color:#fff -``` - -### Les 3 Couches de Protection - -```mermaid -flowchart LR - subgraph L1["Couche 1: AST Scan"] - A1[Parser AST] --> A2[Forbidden Names] - A2 --> A3[Import Whitelist] - end - - subgraph L2["Couche 2: Process Isolation"] - B1[Processus dédié] --> B2[JSON-RPC sur pipes] - B2 --> B3[Pas de pickle] - end - - subgraph L3["Couche 3: Resource Limits"] - C1[Memory Monitor] --> C2[Timeout Monitor] - C2 --> C3[Kill si dépassement] - end - - L1 --> L2 --> L3 - - style L1 fill:#FFE0B2,stroke:#F57C00 - style L2 fill:#C8E6C9,stroke:#388E3C - style L3 fill:#BBDEFB,stroke:#1976D2 -``` - -#### Couche 1: Analyse Statique (AST) - -Avant exécution, l'`ASTScanner` parse l'arbre syntaxique du plugin : - -```python -# Ce qui est BLOQUÉ : -import os # ❌ Forbidden module -import subprocess # ❌ Forbidden module -eval() # ❌ Forbidden name -exec() # ❌ Forbidden name -__class__ # ❌ Forbidden attribute -__globals__ # ❌ Forbidden attribute -__subclasses__() # ❌ Forbidden attribute (sandbox escape) -``` - -#### Couche 2: Isolation Processus - -```mermaid -sequenceDiagram - participant K as Kernel - participant P as Pipe (IPC) - participant S as Sandbox Worker - - K->>P: {"jsonrpc":"2.0","method":"call",...} - Note over P: JSON uniquement
Pas d'objets Python - P->>S: Désérialiser JSON - S->>S: Exécuter handler - S->>P: {"jsonrpc":"2.0","result":...} - P->>K: Résultat JSON -``` - -#### Couche 3: Limites de Ressources - -| Ressource | Limite | Action | -| :--- | :--- | :--- | -| **Mémoire (RSS)** | `max_memory_mb` | Kill process si dépassement | -| **Temps CPU** | `timeout_seconds` | Kill + timeout error | -| **Appels** | `rate_limit` | 429 Too Many Requests | - ---- - -## 6. Service Scoping - -Les services enregistrés dans le `ServiceContainer` ont différents niveaux de visibilité : - -```mermaid -flowchart TB - subgraph Container["🔧 Service Container"] - direction TB - PUB[🟢 Public Services
db, cache, scheduler] - PRI[🔴 Private Services
Kernel interne] - SCO[🟡 Scoped Services
Plugins autorisés] - end - - subgraph Access["Accès"] - K[🎯 Kernel] - T[🔐 Trusted Plugins] - S[📦 Sandboxed Plugins] - end - - PUB --> K - PUB --> T - PUB --> S - - PRI --> K - - SCO --> K - SCO --> T - SCO -.->|Si autorisé| S - - style PUB fill:#C8E6C9,stroke:#388E3C - style PRI fill:#FFCDD2,stroke:#C62828 - style SCO fill:#FFF9C4,stroke:#F57F17 -``` - -### Matrice d'Accès - -| Scope | Description | Accès | Exemple | -| :--- | :--- | :--- | :--- | -| **Public** | Infrastructure générale | Kernel + Tous plugins | `db`, `cache`, `scheduler` | -| **Private** | Services kernel internes | Kernel uniquement | `permission_engine`, `plugin_supervisor` | -| **Scoped** | Restreint à plugins spécifiques | Plugins autorisés uniquement | `stripe_api` (plugin paiement) | - -### Exemple de Configuration - -```yaml -# xcore.yaml -services: - # Service public - accessible par tous - db: - scope: public - backend: postgresql - url: ${DATABASE_URL} - - # Service scoped - accessible uniquement par authorized plugins - payment_db: - scope: scoped - authorized_plugins: - - stripe_plugin - - billing_plugin - backend: postgresql - url: ${PAYMENT_DATABASE_URL} - - # Service private - kernel uniquement - audit_log: - scope: private - backend: sqlite -``` - ---- - -## 7. Flux de Données Complet - -Voici le flux complet d'une requête HTTP à travers XCore : - -```mermaid -sequenceDiagram - participant Client as 🌐 Client HTTP - participant API as 📡 FastAPI Router - participant X as ⚙️ XCore Engine - participant P as 🛡️ Permission Engine - participant S as 🔄 Plugin Supervisor - participant PL as 🧩 Plugin Cible - participant DB as 🗄️ Database - - Client->>API: POST /plugins/call - API->>X: plugins.call(plugin, action, payload) - - Note over X,P: 1. Permission Check - X->>P: verify(plugin, action) - P-->>X: ✅ Autorisé - - Note over X,S: 2. Resolve Plugin - X->>S: get_handler(plugin, action) - S-->>X: Handler trouvé - - Note over S,PL: 3. Execute Action - S->>PL: @action(payload) - - alt Besoin DB - PL->>DB: Query - DB-->>PL: Résultats - end - - PL-->>S: Résultat - S-->>X: Response - X-->>API: JSON - API-->>Client: HTTP 200 OK -``` - ---- - -## Prochaines Lectures - -| 📚 Document | Objectif | -| :--- | :--- | -| [Technical Decisions](decisions.md) | Comprendre les choix architecturaux | -| [Security Deep Dive](../guides/security.md) | Détails du sandboxing | -| [Scaling Analysis](../guides/scaling.md) | Passage à l'échelle | diff --git a/docs-tech/architecture_audit_report.md b/docs-tech/architecture_audit_report.md deleted file mode 100644 index ac6608cb..00000000 --- a/docs-tech/architecture_audit_report.md +++ /dev/null @@ -1,41 +0,0 @@ -# XCore Architectural Audit Report - -## 1. Executive Summary -XCore is a modular framework for managing plugins with isolation and dependency injection. This audit identifies several areas for improvement to enhance scalability, maintainability, and extensibility. Key findings include hardcoded service providers, imperative plugin orchestration, and limited middleware introspection. - -## 2. Layer Analysis - -### Core Kernel (`xcore.kernel`) -- **PluginSupervisor:** Acts as a central orchestrator but suffers from "God Object" tendencies. It manually manages permissions, rate limits, and plugin loading in its `boot()` method, making it difficult to extend without modification. -- **Middleware System:** Robust but lacks introspection. It's difficult to know which middlewares are active at runtime. - -### Plugins & Registry (`xcore.registry`) -- **PluginRegistry:** Centralizes plugin metadata and exported services. While it supports basic scoping (public/private), it lacks a "protected" scope for core services, making them vulnerable to accidental overwrites by plugins. - -### Services Layer (`xcore.services`) -- **ServiceContainer:** Uses a hardcoded `DEFAULT_PROVIDERS` list. This violates the Open/Closed Principle as adding a new core service requires modifying the framework itself. - -### Sandbox (`xcore.kernel.sandbox`) -- Strong isolation via `FilesystemGuard` and AST scanning. The IPC mechanism for sandboxed services is a potential area for further unification. - -## 3. Coupling Points & Friction -- **Manual Orchestration:** The `PluginSupervisor.boot()` method's imperative nature couples plugin loading with security and rate-limiting configuration. -- **Hardcoded Providers:** The dependency on `DEFAULT_PROVIDERS` in `ServiceContainer` prevents external packages from contributing core-level services easily. - -## 4. Technical Debt Risks -- **Extensibility Ceiling:** As the number of cross-cutting concerns (logging, metrics, etc.) grows, `PluginSupervisor` will become increasingly complex. -- **Service Collision:** Without a protected scope, a malicious or buggy plugin could attempt to override critical core services like `db` or `cache`. - -## 5. Proposed Design Improvements - -### A. Dynamic Service Providers -Refactor `ServiceContainer` to support a registry of providers instead of a hardcoded list. - -### B. Protected Service Scope -Introduce a `protected` scope in `PluginRegistry` to safeguard core services. - -### C. Event-Driven Lifecycle -Transition `PluginSupervisor` to reactively configure plugins by listening to `plugin.loaded` events, decoupling loading from configuration. - -### D. Middleware Introspection -Add methods to inspect the active middleware pipeline for better observability. diff --git a/docs-tech/deployment/guide.md b/docs-tech/deployment/guide.md deleted file mode 100644 index fc87a8c2..00000000 --- a/docs-tech/deployment/guide.md +++ /dev/null @@ -1,108 +0,0 @@ -# Deployment Guide - -This guide provides instructions and best practices for deploying XCore Framework in production environments. - -## 1. Production Configuration - -In production, you should use a dedicated `xcore.yaml` and environment variables for sensitive data. - -### Security Checklist -- **Disable Debug**: Set `app.debug: false`. -- **Secure Keys**: Use strong, unique strings for `app.secret_key` and `app.server_key`. -- **Strict Trusted Mode**: Set `plugins.strict_trusted: true` to ensure only signed plugins can run in the main process. -- **Resource Limits**: Define strict limits for all sandboxed plugins to prevent DoS. - -### Minimal `xcore.yaml` for Production -```yaml -app: - env: production - debug: false - secret_key: ${XCORE_SECRET_KEY} - -plugins: - directory: "/etc/xcore/plugins" - strict_trusted: true - autoload: true - -services: - database: - enabled: true - databases: - db: - url: ${DATABASE_URL} - pool_size: 20 - cache: - backend: redis - url: ${REDIS_URL} -``` - -## 2. Docker Deployment - -We recommend using Docker for consistent and isolated deployments. - -### Example Dockerfile -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -# Install system dependencies for sandboxing -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq-dev gcc python3-dev \ - && rm -rf /var/lib/apt/lists/* - -# Install Poetry and dependencies -RUN pip install poetry -COPY pyproject.toml poetry.lock ./ -RUN poetry config virtualenvs.create false && poetry install --only main - -# Copy application and plugins -COPY . . - -# Run with uvicorn -CMD ["uvicorn", "app:main", "--host", "0.0.0.0", "--port", "8000"] -``` - -## 3. Reverse Proxy (Nginx) - -Always run XCore behind a reverse proxy like Nginx or Traefik to handle SSL termination and load balancing. - -```nginx -server { - listen 443 ssl; - server_name api.example.com; - - location / { - proxy_pass http://127.0.0.1:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -## 4. Monitoring & Logging - -### Structured Logs -XCore outputs logs in a structured format by default. Redirect these to a central log aggregator like ELK or Datadog. - -### Prometheus Metrics -Expose the `/metrics` endpoint to monitor CPU, memory, and plugin execution times. - -```yaml -observability: - metrics: - enabled: true - backend: prometheus -``` - -## 5. Continuous Deployment - -1. **Plugin Signing**: Integrate `xcore plugin sign` into your CI/CD pipeline. Only signed plugins should be promoted to production. -2. **Blue-Green Deployment**: Use Kubernetes or Docker Swarm to perform zero-downtime deployments by swapping containers. - -## 6. Scaling - -- **Horizontal Scaling**: Run multiple instances of XCore. Use a shared Redis backend for the `cache` and `scheduler` to maintain consistency across nodes. -- **Sandbox Workers**: If using many sandboxed plugins, ensure the host machine has enough CPU cores and RAM to handle the isolated processes. diff --git a/docs-tech/development/contributing.md b/docs-tech/development/contributing.md deleted file mode 100644 index 6af21358..00000000 --- a/docs-tech/development/contributing.md +++ /dev/null @@ -1,78 +0,0 @@ -# Contributing to XCore - -Thank you for your interest in contributing to XCore! This guide will help you get started with our development process. - -## 1. Getting Started - -### Fork and Clone -1. Fork the repository on GitHub. -2. Clone your fork locally: - ```bash - git clone https://github.com/your-username/xcore.git - cd xcore - ``` - -### Development Environment -We use **Poetry** for dependency management. -```bash -# Install dependencies -poetry install - -# Activate the virtual environment -poetry shell -``` - -## 2. Development Workflow - -### Branching Policy -- `main`: The stable branch. Do not commit directly here. -- `feature/your-feature`: For new features. -- `fix/bug-name`: For bug fixes. -- `docs/improvement`: For documentation updates. - -### Coding Standards -We follow PEP 8 and use **Black** for formatting. -- Run `make lint-fix` before committing. -- Ensure all public classes and methods have docstrings. -- Use type hints wherever possible. - -## 3. Testing - -Every new feature or bug fix must include tests. -- **Unit Tests**: Located in `tests/unit/`. -- **Integration Tests**: Located in `tests/integration/`. - -Run the test suite: -```bash -# Run all tests -make test - -# Run with coverage report -poetry run pytest --cov=xcore -``` - -## 4. Documentation - -Documentation is built with **MkDocs Material**. -- Source files are in the `doc/` directory. -- To preview documentation locally: - ```bash - poetry run mkdocs serve - ``` - -## 5. Pull Request Process - -1. Create a new branch from `main`. -2. Implement your changes and add tests. -3. Ensure the linting and tests pass. -4. Commit your changes using [Conventional Commits](https://www.conventionalcommits.org/). -5. Submit a Pull Request (PR) to the `main` branch. -6. Provide a clear description of the changes and link any related issues. - -## 6. Community - -- **Issues**: Use GitHub Issues to report bugs or suggest features. -- **Discussions**: Join our GitHub Discussions for questions and ideas. - ---- -By contributing, you agree that your contributions will be licensed under the project's [MIT License](LICENSE). diff --git a/docs-tech/development/testing.md b/docs-tech/development/testing.md deleted file mode 100644 index dd8d3591..00000000 --- a/docs-tech/development/testing.md +++ /dev/null @@ -1,92 +0,0 @@ -# Testing Guide - -This guide covers how to test XCore plugins and the framework itself. - -## 1. Testing Setup - -Ensure you have the test dependencies installed: -```bash -poetry install --with test -``` - -We use **pytest** as our primary testing framework. - -## 2. Testing Plugins - -Plugins should be tested for both their IPC actions and their HTTP routes. - -### IPC Action Testing -Use a standard `pytest` class and the `Xcore` instance in test mode. - -```python -import pytest -from xcore import Xcore - -@pytest.fixture -async def app(): - x = Xcore(config_path="tests/test_config.yaml") - await x.boot() - yield x - await x.shutdown() - -@pytest.mark.asyncio -async def test_my_plugin_action(app): - # Call a plugin action directly - result = await app.plugins.call("my_plugin", "ping", {}) - assert result["status"] == "ok" - assert result["message"] == "pong" -``` - -### HTTP Route Testing -Use `httpx.AsyncClient` to test plugin-exposed FastAPI routes. - -```python -import pytest -from httpx import AsyncClient - -@pytest.mark.asyncio -async def test_plugin_route(app): - async with AsyncClient(app=app.fastapi_app, base_url="http://test") as ac: - response = await ac.get("/plugins/my_plugin/hello") - assert response.status_code == 200 - assert response.json() == {"message": "Hello World"} -``` - -## 3. Mocking Services - -When testing plugins, you may want to mock core services like the database or cache. - -```python -from unittest.mock import AsyncMock - -@pytest.mark.asyncio -async def test_with_mock_cache(app): - # Mock the cache service - mock_cache = AsyncMock() - app.services.register_service("cache", mock_cache) - - await app.plugins.call("my_plugin", "save_data", {"key": "val"}) - mock_cache.set.assert_called_with("val", ...) -``` - -## 4. Testing Sandboxed Plugins - -To test sandboxed plugins, use the XCore CLI to run them in an isolated environment. - -```bash -# Run a plugin in a test sandbox -xcore sandbox run my_plugin - -# Verify resource limits are enforced -xcore sandbox limits my_plugin -``` - -## 5. Best Practices - -1. **Isolated State**: Each test should run with a clean state. Use fixtures to boot/shutdown the app. -2. **Test Edge Cases**: Include tests for invalid inputs, rate limit hits, and permission denials. -3. **Use Coverage**: Aim for at least 80% code coverage in your plugins. - ```bash - pytest --cov=plugins/my_plugin - ``` -4. **Mock Externals**: Always mock external API calls to keep tests fast and deterministic. diff --git a/docs-tech/examples/README.md b/docs-tech/examples/README.md deleted file mode 100644 index 8c4a3a32..00000000 --- a/docs-tech/examples/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# XCore Plugin Examples - -This directory contains complete examples of plugins for the XCore framework, illustrating different execution modes and advanced patterns. - -## Table of Contents - -| Example | Mode | Complexity | Description | -|---------|------|------------|-------------| -| [Basic Plugin](./basic-plugin.md) | Trusted | Beginner | Simple calculator plugin with HTTP and IPC endpoints. | -| [Trusted Plugin](./trusted-plugin.md) | **Trusted** | Advanced | Task manager with separate `router.py` and `.env` configuration. | -| [Sandboxed Plugin](./sandboxed-plugin.md) | **Sandboxed** | Advanced | Secure document converter with process isolation. | -| [Complete Plugin](./complete-plugin.md) | **Trusted** | Advanced | Email notification service with event integration. | - ---- - -## Choosing the Execution Mode - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Which mode to choose? │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ │ -│ │ Trusted │ │ Sandboxed │ │ -│ │ │ │ │ │ -│ │ • DB Access │ │ • Max Isolation │ │ -│ │ • Services │ │ • Resource │ │ -│ │ • Extended │ │ Limits │ │ -│ │ Filesystem │ │ • Import │ │ -│ │ │ │ Whitelist │ │ -│ └──────┬───────┘ └────────┬─────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌─────────────────┐ ┌────────────────────┐ │ -│ │ User Management │ │ File Processing │ │ -│ │ Notifications │ │ Doc Conversion │ │ -│ │ Complex Caching │ │ Code Execution │ │ -│ │ Analytics │ │ Compression │ │ -│ └─────────────────┘ └────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Typical Plugin Structure - -### Trusted Mode (with .env) - -``` -plugins/my_plugin/ -├── plugin.yaml # Manifest + structural config -├── .env # Sensitive variables (not committed) -├── src/ -│ ├── __init__.py -│ ├── main.py # Entry point, lifecycle -│ ├── router.py # FastAPI HTTP routes -│ ├── services.py # Business logic -│ └── models.py # Dataclasses/Pydantic models -└── data/ # Local persistence - └── ... -``` - -### Sandboxed Mode - -``` -plugins/my_plugin/ -├── plugin.yaml # Manifest with restrictions -├── src/ -│ ├── __init__.py -│ ├── main.py # Sandboxed Plugin -│ ├── router.py # HTTP Routes -│ └── logic.py # Isolated logic -└── data/temp/ # Temporary workspace -``` - ---- - -## Pattern: Separate Router.py - -Advanced examples utilize a pattern of separating HTTP routes from core logic. - -### Benefits - -1. **Separation of Concerns**: HTTP routing is isolated from business logic. -2. **Testability**: Routes can be tested independently. -3. **Readability**: `main.py` remains focused on lifecycle and IPC actions. -4. **Reusability**: Business logic can be used without the HTTP layer. - -### Implementation - -```python -# src/router.py -def create_router(plugin_instance) -> APIRouter: - router = APIRouter(prefix="/api", tags=["my-plugin"]) - - @router.get("/items") - async def list_items(): - return await plugin_instance.service.get_items() - - return router - -# src/main.py -class Plugin(TrustedBase): - def get_router(self) -> APIRouter | None: - from .router import create_router - return create_router(self) -``` - ---- - -## Pattern: Configuration via .env (Trusted only) - -### Structure - -```yaml -# plugin.yaml -env: - DB_POOL_SIZE: "10" - API_KEY: "" - -envconfiguration: - inject: true # Injects variables into ctx.env - required: true # Fails if .env is missing -``` - -```bash -# .env (not committed) -DB_POOL_SIZE=20 -API_KEY=sk_live_xxx -SECRET_KEY=super_secret -``` - -### Usage - -```python -async def on_load(self): - self.config = { - "pool_size": int(self.ctx.env.get("DB_POOL_SIZE", "10")), - "api_key": self.ctx.env.get("API_KEY"), - } -``` - ---- - -## Key Points by Mode - -### Trusted - -- ✅ Full access to services (DB, Cache, Email, Scheduler). -- ✅ Configurable filesystem access. -- ✅ Unlimited Python imports. -- ✅ Configuration via `.env`. -- ⚠️ Beware of SQL injection and XSS. -- ⚠️ Validate all user inputs. - -### Sandboxed - -- 🔒 Complete system isolation. -- 🔒 Limited resources (memory, CPU, disk). -- 🔒 Import whitelisting. -- 🔒 Operation timeouts. -- ✅ Ideal for file processing. -- ❌ No direct DB access. - ---- - -## Quick Commands - -### Create a New Plugin - -```bash -# Basic structure -mkdir -p plugins/my_plugin/src plugins/my_plugin/data - -# Necessary files -touch plugins/my_plugin/plugin.yaml -touch plugins/my_plugin/src/__init__.py -touch plugins/my_plugin/src/main.py - -# If Trusted with .env -touch plugins/my_plugin/.env -echo ".env" >> plugins/my_plugin/.gitignore -``` - -### Test a Plugin - -```bash -# Start XCore -make run-dev - -# Test HTTP -curl http://localhost:8082/plugins/my_plugin/health - -# Test IPC -curl -X POST http://localhost:8082/app/my_plugin/action \ - -H "Content-Type: application/json" \ - -d '{"key": "value"}' -``` - ---- - -## Additional Resources - -- [Main Documentation](../index.md) -- [SDK Reference](../reference/sdk.md) -- [Security Guide](../guides/security.md) -- [Troubleshooting](../guides/troubleshooting.md) diff --git a/docs-tech/examples/basic-plugin.md b/docs-tech/examples/basic-plugin.md deleted file mode 100644 index 1a841763..00000000 --- a/docs-tech/examples/basic-plugin.md +++ /dev/null @@ -1,43 +0,0 @@ -# Example: Calculator Plugin - -A simple plugin demonstrating basic IPC actions, arithmetic logic, and cache usage. - -## 1. Manifest (`plugin.yaml`) - -```yaml -name: calculator -version: 1.0.0 -execution_mode: trusted -entry_point: src/main.py - -permissions: - - resource: "cache.calc.*" - actions: ["read", "write"] - effect: allow -``` - -## 2. Implementation (`src/main.py`) - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok, error - -class Plugin(AutoDispatchMixin, TrustedBase): - async def on_load(self): - self.cache = self.get_service("cache") - - @action("add") - async def add(self, payload: dict): - a = payload.get("a", 0) - b = payload.get("b", 0) - res = a + b - - # Cache the last result - await self.cache.set("calc.last_result", res) - - return ok(result=res) - - @action("last") - async def get_last(self, payload: dict): - res = await self.cache.get("calc.last_result") - return ok(result=res) -``` diff --git a/docs-tech/examples/complete-plugin.md b/docs-tech/examples/complete-plugin.md deleted file mode 100644 index 0ecec67e..00000000 --- a/docs-tech/examples/complete-plugin.md +++ /dev/null @@ -1,119 +0,0 @@ -# Example: Production-Grade Notification Service - -This example showcases a complete, production-ready plugin that integrates with external APIs, uses multiple XCore services, and handles complex event orchestration. - ---- - -## 1. Directory Layout - -```text -plugins/notification_hub/ -├── plugin.yaml # Manifest & Permissions -├── src/ -│ ├── main.py # Entry point & IPC -│ ├── providers/ # External service providers -│ │ ├── email.py -│ │ └── slack.py -│ └── events.py # Event handlers -└── schemas/ # Pydantic validation schemas - └── notify.py -``` - ---- - -## 2. Manifest (`plugin.yaml`) - -```yaml -name: notification_hub -version: "3.2.0" -author: "XCore Team" -description: "Centralized notification hub with multi-channel support." -execution_mode: trusted - -# Requires the cache for rate-limiting per recipient -permissions: - - resource: "cache.notifications.*" - actions: ["read", "write"] - effect: allow - - resource: "db.history" - actions: ["write"] - effect: allow - -# Listen to critical system events -runtime: - health_check: - enabled: true - interval_seconds: 60 -``` - ---- - -## 3. Advanced Implementation (`src/main.py`) - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok, error, validate_payload -from .providers.email import EmailProvider -from .providers.slack import SlackProvider -from .schemas.notify import NotificationRequest - -class Plugin(AutoDispatchMixin, TrustedBase): - """ - Highly resilient notification hub. - Uses 'Circuit Breaker' pattern when calling external providers. - """ - - async def on_load(self) -> None: - self.email = EmailProvider(self.ctx.env.get("SMTP_HOST")) - self.slack = SlackProvider(self.ctx.env.get("SLACK_TOKEN")) - self.cache = self.get_service("cache") - self.db = self.get_service("db") - - # Subscribe to global user events - self.ctx.events.on("user.password_changed", self._on_security_event) - self.logger.info("Notification Hub Active") - - @action("broadcast") - @validate_payload(NotificationRequest) - async def broadcast(self, payload: dict) -> dict: - """Sends a message to all configured channels.""" - - # 1. Deduplication Check (Cache) - dedup_key = f"notify:hash:{hash(payload['message'])}" - if await self.cache.exists(dedup_key): - return error("Duplicate message detected", code="duplicate") - - # 2. Parallel Dispatch - results = await asyncio.gather( - self.email.send(payload), - self.slack.send(payload), - return_exceptions=True - ) - - # 3. Log to History - async with self.db.session() as session: - await session.execute( - "INSERT INTO notification_history (msg, status) VALUES (:m, :s)", - {"m": payload['message'], "s": "sent"} - ) - - await self.cache.set(dedup_key, True, ttl=60) - return ok(message="Broadcast complete") - - async def _on_security_event(self, event): - """React to kernel-level security events.""" - user_id = event.data.get("user_id") - await self.broadcast({ - "recipient": user_id, - "message": "Your password has been changed successfully.", - "channel": "email" - }) -``` - ---- - -## 4. Key Engineering Patterns Used - -1. **Parallel Execution**: Uses `asyncio.gather` for non-blocking I/O across multiple providers. -2. **Idempotency**: Implements a simple deduplication logic using the `cache` service. -3. **Kernel Integration**: Directly subscribes to `user.password_changed` events emitted by other core plugins. -4. **Resilience**: The structure allows adding more providers without changing the core IPC interface. diff --git a/docs-tech/examples/sandboxed-plugin.md b/docs-tech/examples/sandboxed-plugin.md deleted file mode 100644 index 06a34365..00000000 --- a/docs-tech/examples/sandboxed-plugin.md +++ /dev/null @@ -1,40 +0,0 @@ -# Example: Secure File Processor - -A sandboxed plugin designed for processing untrusted data with strict resource limits. - -## 1. Manifest (`plugin.yaml`) - -```yaml -name: secure_processor -version: 1.0.0 -execution_mode: sandboxed -entry_point: src/main.py - -# Only allow safe modules -allowed_imports: - - json - - hashlib - - base64 - -resources: - timeout_seconds: 5 - max_memory_mb: 128 - rate_limit: - calls: 10 - period_seconds: 60 -``` - -## 2. Implementation (`src/main.py`) - -```python -from xcore.sdk import TrustedBase, ok -import hashlib - -class Plugin(TrustedBase): - async def handle(self, action: str, payload: dict) -> dict: - if action == "hash": - data = payload.get("data", "").encode() - return ok(hash=hashlib.sha256(data).hexdigest()) - - return {"status": "error", "msg": "Unknown action"} -``` diff --git a/docs-tech/examples/trusted-plugin.md b/docs-tech/examples/trusted-plugin.md deleted file mode 100644 index 0fe308d6..00000000 --- a/docs-tech/examples/trusted-plugin.md +++ /dev/null @@ -1,193 +0,0 @@ -# Example: Trusted Task Manager (Advanced) - -This comprehensive example demonstrates a production-grade **Trusted** plugin with a modular architecture, database persistence, notifications, and complex business logic. - ---- - -## 1. Plugin Structure - -```text -plugins/task_manager/ -├── plugin.yaml # Plugin manifest -├── .env # Local configuration -└── src/ - ├── __init__.py - ├── main.py # Main entry point - ├── router.py # FastAPI routes - ├── models.py # Data models - ├── services.py # Business logic - └── repository.py # Database access -``` - ---- - -## 2. Manifest (`plugin.yaml`) - -```yaml -name: task_manager -version: 2.1.0 -author: XCore Team -description: | - Advanced task manager with: - - Full CRUD via HTTP and IPC - - Email notifications via external service - - Statistics and reports - - PostgreSQL/SQLite persistence - -execution_mode: trusted -framework_version: ">=2.0" -entry_point: src/main.py - -requires: - - user_service # Depends on the user service for assignment validation - -permissions: - - resource: "db.tasks.*" - actions: ["read", "write", "delete"] - effect: allow - - resource: "ext.email_service" - actions: ["send"] - effect: allow - - resource: "scheduler" - actions: ["schedule", "cancel"] - effect: allow - -resources: - timeout_seconds: 30 - rate_limit: - calls: 1000 - period_seconds: 60 -``` - ---- - -## 3. Data Models (`src/models.py`) - -```python -from pydantic import BaseModel, Field -from datetime import datetime -from enum import Enum -from typing import Optional, List - -class TaskStatus(str, Enum): - PENDING = "pending" - IN_PROGRESS = "in_progress" - COMPLETED = "completed" - CANCELLED = "cancelled" - -class TaskPriority(str, Enum): - LOW = "low" - MEDIUM = "medium" - HIGH = "high" - URGENT = "urgent" - -class TaskCreate(BaseModel): - title: str = Field(..., min_length=1, max_length=200) - description: Optional[str] = None - priority: TaskPriority = TaskPriority.MEDIUM - assigned_to: Optional[int] = None - due_date: Optional[datetime] = None - tags: List[str] = [] - -class Task(TaskCreate): - id: int - created_by: int - status: TaskStatus = TaskStatus.PENDING - created_at: datetime = Field(default_factory=datetime.utcnow) - updated_at: datetime = Field(default_factory=datetime.utcnow) -``` - ---- - -## 4. Business Services (`src/services.py`) - -```python -from xcore.sdk import ok, error -from .models import TaskCreate, TaskStatus - -class TaskService: - def __init__(self, ctx): - self.ctx = ctx - self.db = ctx.services.get("db") - self.email = ctx.services.get("ext.email_service") - self.scheduler = ctx.services.get("scheduler") - - async def create_task(self, data: TaskCreate, creator_id: int): - try: - # 1. Validate assignment via another plugin - user_check = await self.ctx.plugins.call( - "user_service", "exists", {"id": data.assigned_to} - ) - if data.assigned_to and user_check.get("status") != "ok": - return error("Assigned user does not exist", code="user_not_found") - - # 2. Persist to Database - async with self.db.session() as session: - task_id = await session.execute( - "INSERT INTO tasks (title, description, created_by, assigned_to) " - "VALUES (:t, :d, :c, :a) RETURNING id", - {"t": data.title, "d": data.description, "c": creator_id, "a": data.assigned_to} - ) - - # 3. Notify via Email (Side effect) - if data.assigned_to: - await self.email.send( - recipient_id=data.assigned_to, - subject=f"New Task: {data.title}", - body="You have been assigned a new task." - ) - - # 4. Schedule a reminder if due date exists - if data.due_date: - self.scheduler.add_job( - self._send_reminder, - trigger="date", - run_date=data.due_date, - args=[task_id] - ) - - return ok(id=task_id, message="Task created successfully") - - except Exception as e: - self.ctx.logger.exception("Failed to create task") - return error(str(e), code="internal_error") - - async def _send_reminder(self, task_id: int): - # Implementation for reminder notification - pass -``` - ---- - -## 5. Main Entry Point (`src/main.py`) - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, RoutedPlugin, action, validate_payload -from .services import TaskService -from .models import TaskCreate -from .router import create_router - -class Plugin(AutoDispatchMixin, RoutedPlugin, TrustedBase): - """Advanced Task Manager Plugin.""" - - async def on_load(self) -> None: - # Initialize internal service - self.service = TaskService(self.ctx) - self.logger.info("Task Manager Loaded") - - def get_router(self): - # Mount separate router file - return create_router(self) - - @action("create") - @validate_payload(TaskCreate) - async def handle_create(self, payload: dict): - # Dispatch to business service - creator_id = self.ctx.get("user_id", 0) - return await self.service.create_task(TaskCreate(**payload), creator_id) - - @action("stats") - async def get_stats(self, payload: dict): - # Return aggregated data - return ok(total=150, completed=120, pending=30) -``` diff --git a/docs-tech/getting-started/installation.md b/docs-tech/getting-started/installation.md deleted file mode 100644 index a8c47a17..00000000 --- a/docs-tech/getting-started/installation.md +++ /dev/null @@ -1,83 +0,0 @@ -# Installation - -This guide covers the installation of XCore Framework for development and production environments. - ---- - -## 1. Prerequisites - -XCore requires a modern Python environment: - -- **Python 3.11+**: Built on the latest `asyncio` features. -- **Package Manager**: [Poetry](https://python-poetry.org/) is strongly recommended, but `pip` is also supported. -- **OS**: Linux or macOS are recommended for the best sandboxing experience. Windows is supported for development. - ---- - -## 2. Standard Installation - -### Using Poetry (Recommended) -```bash -# Clone the repository -git clone https://github.com/traoreera/xcore.git -cd xcore - -# Install core dependencies -poetry install - -# Activate the environment -poetry shell -``` - -### Using Pip -```bash -pip install xcore-framework -``` - ---- - -## 3. Database & Service Drivers - -By default, XCore comes with SQLite support. For other backends, install the required drivers: - -```bash -# PostgreSQL -pip install asyncpg psycopg2-binary - -# MySQL -pip install mysql-connector-python - -# Redis (for Cache & Scheduler) -pip install redis hiredis -``` - ---- - -## 4. Environment Configuration - -XCore uses a dual configuration system: `xcore.yaml` for system structure and `.env` for secrets. - -1. **Initialize Configuration**: - ```bash - cp .env.example .env - ``` - -2. **Required Environment Variables**: - Ensure these are set in your `.env`: - ```env - XCORE_SECRET_KEY=generate-a-secure-random-string - XCORE_SERVER_KEY=another-secure-random-string - DATABASE_URL=sqlite+aiosqlite:///./xcore.db - ``` - ---- - -## 5. Verification - -Run the built-in health check to ensure everything is configured correctly: - -```bash -xcore health -``` - -You should see a "Healthy" status for the Kernel and all enabled services. diff --git a/docs-tech/getting-started/quickstart.md b/docs-tech/getting-started/quickstart.md deleted file mode 100644 index 57a27602..00000000 --- a/docs-tech/getting-started/quickstart.md +++ /dev/null @@ -1,334 +0,0 @@ -# Quick Start Guide - -Créez votre première application XCore et votre premier plugin en **moins de 5 minutes**. - ---- - -## Vue d'Ensemble du Flux - -```mermaid -flowchart LR - A[1. Initialiser
l'Application] --> B[2. Créer
le Plugin] - B --> C[3. Démarrer
le Serveur] - C --> D[4. Tester
l'Action] - - style A fill:#4CAF50,color:#fff - style B fill:#2196F3,color:#fff - style C fill:#FF9800,color:#fff - style D fill:#9C27B0,color:#fff -``` - ---- - -## Étape 1 : Initialiser l'Application - -Créez un fichier `app.py` qui servira de point d'entrée à votre serveur FastAPI : - -```python -# app.py -from fastapi import FastAPI -from xcore import Xcore - -# 📦 Initialisation -app = FastAPI(title="Mon App XCore") -core = Xcore(config_path="xcore.yaml") - -# 🚀 Démarrage du kernel -@app.on_event("startup") -async def startup(): - await core.boot(app) - # Le kernel initialise : - # 1. ✅ Les services (DB, Cache, Scheduler) - # 2. ✅ Les plugins découverts - # 3. ✅ Les routes HTTP - -# 🛑 Arrêt propre -@app.on_event("shutdown") -async def shutdown(): - await core.shutdown() -``` - -### Ce qui se passe au démarrage : - -```mermaid -sequenceDiagram - participant You as Vous - participant App as app.py - participant Kernel as XCore Kernel - participant Services as Services - participant Plugins as Plugins - - You->>App: uvicorn app:app - App->>Kernel: boot() - - Note over Kernel: Initialisation... - Kernel->>Services: DB, Cache, Scheduler - Kernel->>Plugins: Chargement - - Services-->>Kernel: ✅ Prêts - Plugins-->>Kernel: ✅ Chargés - - Kernel-->>App: ✅ Prêt - App-->>You: 🚀 Serveur sur :8000 -``` - ---- - -## Étape 2 : Créer Votre Premier Plugin - -### A. Structure du Dossier - -```bash -# Créez l'arborescence -mkdir -p plugins/hello_plugin/src - -# Créez les fichiers -touch plugins/hello_plugin/plugin.yaml -touch plugins/hello_plugin/src/main.py -``` - -``` -plugins/hello_plugin/ -├── plugin.yaml # 📋 Manifeste (métadonnées) -└── src/ - └── main.py # 💡 Logique métier -``` - -### B. Le Manifeste (`plugin.yaml`) - -```yaml -# 📋 Identification -name: hello_plugin -version: "1.0.0" -author: Votre Nom -description: "Mon premier plugin XCore" - -# ⚙️ Configuration -execution_mode: trusted # trusted | sandboxed -entry_point: src/main.py - -# 🔐 Permissions (optionnel) -permissions: - - resource: "cache.*" - actions: ["read", "write"] - effect: allow - -# 📊 Ressources (limites) -resources: - timeout_seconds: 30 - rate_limit: - calls: 100 - period_seconds: 60 -``` - -### C. La Logique (`src/main.py`) - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok, error - -class Plugin(AutoDispatchMixin, TrustedBase): - """ - Plugin de démonstration qui salue les utilisateurs. - """ - - async def on_load(self): - """🎯 Appelé quand le plugin est chargé.""" - self.logger.info("✨ Hello plugin est prêt !") - - async def on_unload(self): - """🧹 Appelé quand le plugin est déchargé.""" - self.logger.info("👋 Hello plugin est déchargé") - - @action("greet") - async def greet_user(self, payload: dict): - """ - 🎯 Action: greet - - Args: - payload: {"name": "Votre nom"} - - Returns: - {"status": "ok", "message": "Hello, X !"} - """ - name = payload.get("name", "World") - self.logger.info(f"Salutation de: {name}") - return ok(message=f"Hello, {name} !") - - @action("ping") - async def ping(self, payload: dict): - """Test de connectivité.""" - return ok(status="alive") -``` - ---- - -## Étape 3 : Démarrer et Tester - -### Démarrer le Serveur - -```bash -# Dans le terminal 1 -uvicorn app:app --reload --host 0.0.0.0 --port 8000 -``` - -``` -INFO: Started server process [12345] -INFO: Waiting for application startup. -INFO: XCore Kernel booting... -INFO: Services initialized: db, cache, scheduler -INFO: Plugins loaded: hello_plugin (v1.0.0) -INFO: Application startup complete. -INFO: Uvicorn running on http://0.0.0.0:8000 -``` - -### Tester via CLI - -```bash -# Dans le terminal 2 -xcore plugin call hello_plugin greet '{"name": "Développeur"}' -``` - -**Réponse :** -```json -{ - "status": "ok", - "message": "Hello, Développeur !" -} -``` - -### Tester via HTTP - -Si vous avez ajouté des routes HTTP : - -```bash -curl http://localhost:8000/plugin/hello_plugin/hello/Dev -``` - -### Flux Complet d'un Appel - -```mermaid -sequenceDiagram - participant CLI as xcore CLI - participant API as API HTTP - participant Kernel as XCore Kernel - participant Perm as Permission Engine - participant Plugin as hello_plugin - - CLI->>API: POST /plugins/call - API->>Kernel: plugins.call("hello_plugin", "greet") - - Kernel->>Perm: Vérifier permissions - Perm-->>Kernel: ✅ Autorisé - - Kernel->>Plugin: @action("greet") - Plugin->>Plugin: greet_user(payload) - Plugin-->>Kernel: {"status": "ok", ...} - - Kernel-->>API: Résultat - API-->>CLI: Response JSON -``` - ---- - -## Étape 4 : Aller Plus Loin - -### Ajouter une Route HTTP - -```python -from xcore.sdk import TrustedBase, RoutedPlugin, route - -class Plugin(RoutedPlugin, TrustedBase): - - @route("/hello/{name}", methods=["GET"]) - async def hello_api(self, name: str): - """Route HTTP accessible directement.""" - return {"message": f"Hello, {name} !"} - - @route("/status", methods=["GET"]) - async def status(self): - """Endpoint de santé du plugin.""" - return { - "plugin": self.name, - "version": self.version, - "status": "healthy" - } -``` - -**Routes disponibles :** -- `GET /plugin/hello_plugin/hello/{name}` -- `GET /plugin/hello_plugin/status` - -### Utiliser le Cache - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok - -class Plugin(AutoDispatchMixin, TrustedBase): - - async def on_load(self): - self.cache = self.get_service("cache") - - @action("greet_cached") - async def greet_cached(self, payload: dict): - name = payload.get("name", "World") - cache_key = f"greeting:{name}" - - # Vérifier le cache - cached = await self.cache.get(cache_key) - if cached: - return ok(message=cached, source="cache") - - # Générer et caster - message = f"Hello, {name} !" - await self.cache.set(cache_key, message, ttl=60) - - return ok(message=message, source="generated") -``` - -### Émettre un Événement - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok - -class Plugin(AutoDispatchMixin, TrustedBase): - - @action("create_user") - async def create_user(self, payload: dict): - # ... logique de création ... - - # 📡 Émettre un événement - await self.ctx.events.emit("user.created", { - "id": user_id, - "email": payload.get("email") - }) - - return ok(user_id=user_id) -``` - ---- - -## Résumé des Commandes - -| Commande | Description | -| :--- | :--- | -| `xcore plugin list` | Lister tous les plugins chargés | -| `xcore plugin load hello_plugin` | Charger un plugin | -| `xcore plugin reload hello_plugin` | Recharger à chaud | -| `xcore plugin unload hello_plugin` | Décharger un plugin | -| `xcore plugin call hello_plugin greet '{}'` | Appeler une action | -| `xcore health` | Vérifier la santé du système | - ---- - -## Prochaines Étapes - -| 📚 Guide | Objectif | -| :--- | :--- | -| [📖 SDK Reference](../reference/sdk.md) | Découvrir tous les décorateurs | -| [🛡️ Sécurité](../guides/security.md) | Comprendre le sandboxing | -| [📡 Événements](../guides/events.md) | Maîtriser le système d'événements | -| [💾 Services](../guides/services.md) | Utiliser DB et Cache | - ---- - -> 💡 **Astuce** : Pour le développement, utilisez `execution_mode: trusted` pour un débogage plus facile. En production, passez à `sandboxed` pour une isolation maximale. diff --git a/docs-tech/guides/creating-plugins.md b/docs-tech/guides/creating-plugins.md deleted file mode 100644 index 6aed63c2..00000000 --- a/docs-tech/guides/creating-plugins.md +++ /dev/null @@ -1,180 +0,0 @@ -# Creating Plugins - -This guide covers everything you need to know about creating XCore plugins, from simple IPC handlers to full REST API endpoints. - -## 1. Plugin Structure - -A standard XCore plugin requires a specific directory structure: - -```text -plugins/my_plugin/ -├── plugin.yaml # Plugin manifest (metadata & configuration) -├── plugin.sig # Security signature (for trusted mode in production) -└── src/ - └── main.py # Core logic and entry point -``` - -## 2. Inter-Process Communication (IPC) - -XCore uses a standardized IPC system for communication between the core and plugins: - -- **Core → Plugin**: Via HTTP API calls or internal process signals. -- **Plugin → Core**: Via the `self.ctx.plugins.call()` method. -- **Plugin → Plugin**: Also via `self.ctx.plugins.call()`, routed through the kernel for security checks. - -### Managing Plugins via CLI - -You can manage the plugin lifecycle directly from the command line: - -```bash -# List all discovered plugins -xcore plugin list - -# Load a specific plugin -xcore plugin load - -# Hot-reload a plugin (refreshes code and manifest) -xcore plugin reload - -# Unload a plugin -xcore plugin unload -``` - -## 3. The Manifest (`plugin.yaml`) - -The manifest is a YAML file that describes your plugin's metadata, requirements, and security policies. - -```yaml -name: my_plugin # Unique identifier -version: 1.0.0 # Semantic version -author: Your Name # Author name -description: Plugin description # Short description - -execution_mode: trusted # trusted | sandboxed -framework_version: ">=2.0" # Compatible XCore version -entry_point: src/main.py # Main file path - -# Dependencies on other plugins -requires: - - name: other_plugin - version: ">=1.5.0" - -# Granular service permissions -permissions: - - resource: "db.*" - actions: ["read", "write"] - effect: allow - - resource: "cache.global" - actions: ["read"] - effect: allow - -# Resource limits (especially important for sandboxed plugins) -resources: - timeout_seconds: 30 - max_memory_mb: 256 - rate_limit: - calls: 1000 - period_seconds: 60 -``` - -## 4. Implementation Styles - -### Basic Plugin (Action-Based) - -The simplest way to implement a plugin is to inherit from `TrustedBase` and implement the `handle` method. - -```python -from xcore.sdk import TrustedBase, ok, error - -class Plugin(TrustedBase): - """A simple calculator plugin.""" - - async def handle(self, action: str, payload: dict) -> dict: - if action == "add": - result = payload.get("a", 0) + payload.get("b", 0) - return ok(result=result) - - return error(f"Unknown action: {action}", code="unknown_action") -``` - -### Advanced Plugin with Auto-Dispatch - -Use the `AutoDispatchMixin` and `@action` decorator to automatically route IPC calls to specific methods. - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok - -class Plugin(AutoDispatchMixin, TrustedBase): - - @action("greet") - async def say_hello(self, payload: dict): - name = payload.get("name", "Guest") - return ok(message=f"Hello, {name}!") -``` - -### Plugin with HTTP Routes - -Expose REST API endpoints by using the `RoutedPlugin` mixin and `@route` decorator. These routes are automatically mounted by XCore. - -```python -from xcore.sdk import TrustedBase, RoutedPlugin, route - -class Plugin(RoutedPlugin, TrustedBase): - - @route("/hello/{name}", method="GET") - async def hello_api(self, name: str): - return {"message": f"Hello, {name}!"} -``` -*Note: These routes will be available at `/plugins/my_plugin/hello/{name}`.* - -## 5. Using Core Services - -XCore provides built-in services for common tasks. Access them using `self.get_service()`. - -### Database Access - -```python -async def on_load(self): - self.db = self.get_service("db") - -async def get_users(self): - async with self.db.session() as session: - result = await session.execute("SELECT * FROM users") - return result.fetchall() -``` - -### Cache & Events - -```python -async def on_load(self): - self.cache = self.get_service("cache") - # Subscribe to an event - self.ctx.events.on("user.created", self._on_user_created) - -async def _on_user_created(self, event): - # React to the event - user_email = event.data.get("email") - await self.cache.set(f"welcome_sent:{user_email}", True) -``` - -## 6. Plugin Lifecycle Hooks - -Override these methods to manage your plugin's lifecycle: - -- **`on_load(self)`**: Called when the plugin is first loaded. Use it for initialization. -- **`on_reload(self)`**: Called during a hot-reload. Use it to refresh state or connections. -- **`on_unload(self)`**: Called before the plugin is removed. Use it for cleanup. - -## 7. Best Practices - -1. **Strict Typing**: Use Python type hints for better maintainability. -2. **Input Validation**: Use Pydantic models with the `@validate_payload` decorator for all IPC and HTTP inputs. -3. **Error Handling**: Always return standardized responses using `ok()` and `error()` helpers. -4. **Least Privilege**: Only request the permissions your plugin absolutely needs in `plugin.yaml`. -5. **Documentation**: Include docstrings for your class and all exposed actions/routes. - -## Next Steps - -- [Working with Services](services.md) -- [Security Best Practices](security.md) -- [Testing Plugins](../development/testing.md) diff --git a/docs-tech/guides/creating-services.md b/docs-tech/guides/creating-services.md deleted file mode 100644 index 4f3d7a59..00000000 --- a/docs-tech/guides/creating-services.md +++ /dev/null @@ -1,130 +0,0 @@ -# Creating Services & Extensions - -This guide explains how to create your own services and extensions within the XCore framework. This allows you to share complex logic, connections, or state between multiple plugins. - -## 1. Services vs. Extensions - -- **Services**: Core-level components managed by the `ServiceContainer`. They are typically initialized before any plugins are loaded and are often used for shared infrastructure (e.g., a specific database connection or an external API client). -- **Extensions**: A specialized type of service that is loaded from a specific directory and can be dynamically discovered. Use extensions for high-level business logic that needs to be shared across multiple plugins. - -## 2. Creating a Custom Service Provider - -Custom services are typically implemented as `BaseService` classes and registered in the `ServiceContainer`. - -### Step 1: Implement the Service - -```python -from xcore.services.base import BaseService, ServiceStatus - -class MyCustomService(BaseService): - name = "my_custom_service" - - def __init__(self, config): - super().__init__() - self.config = config - self.client = None - - async def init(self) -> None: - self._status = ServiceStatus.INITIALIZING - # Initialize client, connection, etc. - self.client = MyExternalClient(self.config.get("api_key")) - self._status = ServiceStatus.READY - print(f"Service {self.name} initialized") - - async def shutdown(self) -> None: - if self.client: - await self.client.close() - self._status = ServiceStatus.STOPPED - - async def health_check(self) -> tuple[bool, str]: - if self.client and await self.client.ping(): - return True, "ok" - return False, "client unreachable" - - async def do_something(self, data): - """Custom method for the service.""" - return await self.client.process(data) -``` - -### Step 2: Register the Service - -In your `app.py`, you can manually register your service before calling `xcore.boot()`: - -```python -xcore = Xcore(config_path="xcore.yaml") -custom_service = MyCustomService(my_config) - -# Register before boot -xcore.services.register_service("my_service", custom_service) - -# Alternatively, use a ServiceProvider for lazy-loading -class MyServiceProvider: - def provide(self, name: str): - if name == "my_service": - return custom_service - return None - -xcore.services.register_provider("my_provider", MyServiceProvider()) -``` - -## 3. Creating an Extension - -Extensions are automatically loaded from the `extensions/` directory if configured in `xcore.yaml`. - -### Step 1: Create the Extension Structure - -```bash -extensions/ - my_extension/ - __init__.py - extension.py -``` - -### Step 2: Implement the Extension - -```python -# extensions/my_extension/extension.py - -class MyExtension: - """A custom extension shared between plugins.""" - - def __init__(self, config): - self.config = config - - async def init(self): - # Initialization logic - print("Extension initialized!") - - def get_info(self): - return {"version": "1.0", "status": "active"} -``` - -### Step 3: Configure in `xcore.yaml` - -```yaml -services: - extensions: - directory: "extensions/" - enabled: true - configs: - my_extension: - api_key: "secret_123" -``` - -### Step 4: Use the Extension in a Plugin - -```python -class MyPlugin(TrustedBase): - async def on_load(self): - # Extensions are prefixed with 'ext.' - self.my_ext = self.get_service("ext.my_extension") - print(self.my_ext.get_info()) -``` - -## 4. Best Practices - -1. **Lazy Loading**: For services that are not used by every plugin, consider using a `ServiceProvider` to initialize them only when first requested. -2. **Health Checks**: Always implement `health_check()` to allow the kernel to monitor the status of your service. -3. **Graceful Shutdown**: Ensure your `shutdown()` method properly releases all resources (database connections, file handles, etc.). -4. **Logging**: Use the standard XCore logging (available via `self.logger` or `logging.getLogger("xcore.services.custom")`). -5. **Type Safety**: Provide type hints for your service methods to improve the developer experience for plugin authors. diff --git a/docs-tech/guides/events.md b/docs-tech/guides/events.md deleted file mode 100644 index 494b0dca..00000000 --- a/docs-tech/guides/events.md +++ /dev/null @@ -1,567 +0,0 @@ -# Event & Hook System - -XCore propose deux mécanismes de communication découplée : les **Événements** (asynchrones, side-effects) et les **Hooks** (synchrones, transformation de données). - ---- - -## Vue d'Ensemble Comparative - -```mermaid -flowchart LR - subgraph Events["📡 Événements (Async)"] - E1[Fire-and-forget] - E2[Handlers parallèles] - E3[Pas de retour] - E4[Side effects] - end - - subgraph Hooks["🪝 Hooks (Sync)"] - H1[Transformation] - H2[Séquentiel] - H3[Retour valeur] - H4[Inline logic] - end - - Events -.->|Découplage fort| Hooks - Hooks -.->|Transformation| Events - - style Events fill:#E3F2FD,stroke:#1976D2 - style Hooks fill:#FFF3E0,stroke:#F57C00 -``` - ---- - -## 1. Système d'Événements Asynchrones - -L'Event Bus est utilisé pour la communication **"fire-and-forget"**. Idéal pour déclencher des side effects (logging, emails, cache) après une action. - -### Architecture de l'Event Bus - -```mermaid -flowchart TB - subgraph Publisher["📤 Publishers"] - P1[Plugin A] - P2[Plugin B] - K[Kernel] - end - - subgraph EventBus["📡 Event Bus"] - direction TB - Q[Queue Async] - D[Dispatcher] - R[Registry Handlers] - end - - subgraph Subscriber["📥 Subscribers"] - S1[Handler 1] - S2[Handler 2] - S3[Handler 3] - end - - P1 --> EventBus - P2 --> EventBus - K --> EventBus - - EventBus --> S1 - EventBus --> S2 - EventBus --> S3 - - style EventBus fill:#E8F5E9,stroke:#388E3C -``` - -### Concepts Clés - -| Concept | Description | Usage | -| :--- | :--- | :--- | -| **Priority** | Les handlers avec priorité haute (100) s'exécutent avant les bas (50) | Ordonnancer l'exécution | -| **Propagation** | Un handler peut appeler `event.stop_propagation()` | Stopper la chaîne | -| **Gather** | Par défaut, handlers exécutés en parallèle via `asyncio.gather` | Performance | - ---- - -### S'abonner à un Événement - -```python -from xcore.sdk import TrustedBase, on, Event - -class Plugin(TrustedBase): - - async def on_load(self): - # Méthode 1: Décorateur (recommandé) - # Le handler est automatiquement abonné - - # Méthode 2: Subscription manuelle - self.ctx.events.on("user.created", self._on_user_created, priority=100) - - @on("user.created", priority=100) - async def _on_user_created(self, event: Event): - """ - Handler pour l'événement user.created - - Args: - event: Contient data, timestamp, source - """ - user_id = event.data.get("id") - email = event.data.get("email") - - self.logger.info(f"Nouvel utilisateur créé: {user_id}") - - # Side effect: envoyer email de bienvenue - await self._send_welcome_email(email) - - # Optionnel: stopper la propagation - # event.stop_propagation() - - async def _send_welcome_email(self, email: str): - # ... logique d'envoi ... - pass -``` - -### Émettre un Événement - -```python -from xcore.sdk import TrustedBase, action, ok - -class Plugin(TrustedBase): - - @action("create_user") - async def create_user(self, payload: dict): - # 1. Créer l'utilisateur - user_id = await self._create_in_db(payload) - - # 2. Émettre l'événement - await self.ctx.events.emit("user.created", { - "id": user_id, - "email": payload.get("email"), - "created_at": self._now() - }) - - # 3. Retourner résultat (l'événement est fire-and-forget) - return ok(user_id=user_id) -``` - -### Flux d'un Événement - -```mermaid -sequenceDiagram - participant P as Plugin Publisher - participant EB as Event Bus - participant H1 as Handler 1 (priority=100) - participant H2 as Handler 2 (priority=50) - participant H3 as Handler 3 (priority=50) - - P->>EB: emit("user.created", data) - - Note over EB: Registry handlers par priority - EB->>H1: _on_user_created(event) - - par Exécution parallèle (asyncio.gather) - EB->>H2: handler_2(event) - EB->>H3: handler_3(event) - end - - H1-->>EB: ✅ Done - H2-->>EB: ✅ Done - H3-->>EB: ✅ Done - - EB-->>P: ✅ Tous handlers exécutés - - Note over P,H3: ⚠️ Le publisher n'attend pas
les résultats des handlers -``` - ---- - -## 2. Événements Système Built-in - -XCore émet plusieurs événements système que vous pouvez subscriber. - -### Tableau des Événements Système - -| Événement | Quand | Payload | Usage Typique | -| :--- | :--- | :--- | :--- | -| `xcore.plugins.booted` | Tous plugins chargés | `{"report": {...}}` | Initialisation globale | -| `plugin..loaded` | Plugin chargé | `{"name", "version"}` | Logging, metrics | -| `plugin..reloaded` | Plugin rechargé | `{"name"}` | Refresh cache | -| `plugin..unloaded` | Plugin déchargé | `{"name"}` | Cleanup | -| `permission.deny` | Permission refusée | `{"plugin", "resource", "action"}` | Audit sécurité | -| `security.violation` | Scan AST échoué | `{"plugin", "errors"}` | Alertes sécurité | -| `user.created` | Custom: utilisateur créé | `{"id", "email"}` | Welcome email | -| `user.deleted` | Custom: utilisateur supprimé | `{"id"}` | Cleanup données | - -### Exemple: Subscriber aux Événements Système - -```python -from xcore.sdk import TrustedBase, on, Event - -class Plugin(TrustedBase): - - @on("xcore.plugins.booted", priority=200) - async def _on_system_boot(self, event: Event): - """Exécuté quand TOUS les plugins sont chargés.""" - report = event.data.get("report", {}) - loaded_count = report.get("loaded", 0) - self.logger.info(f"🚀 Système prêt : {loaded_count} plugins chargés") - - @on("plugin.auth_plugin.loaded", priority=100) - async def _on_auth_ready(self, event: Event): - """Exécuté quand le plugin auth est prêt.""" - self.logger.info("✅ Plugin auth disponible") - # Initialiser la connexion avec le plugin auth - - @on("permission.deny", priority=50) - async def _on_permission_denied(self, event: Event): - """Logging de toutes les permissions refusées.""" - plugin = event.data.get("plugin") - resource = event.data.get("resource") - self.logger.warning(f"🚫 Permission refusée: {plugin} → {resource}") -``` - ---- - -## 3. Best Practices pour les Événements - -```mermaid -flowchart TB - subgraph Do["✅ Bonnes Pratiques"] - D1[Handlers légers] - D2[Priorités claires] - D3[Cleanup handlers] - D4[Événements nommés] - end - - subgraph Dont["❌ Erreurs à Éviter"] - A1[Tâches lourdes] - A2[Priorités magiques] - A3[Memory leaks] - A4[Noms obscurs] - end - - Do -.->|Au lieu de| Dont - - style Do fill:#E8F5E9,stroke:#388E3C - style Dont fill:#FFEBEE,stroke:#C62828 -``` - -### 1. Handlers Légers - -```python -# ❌ MAUVAIS: Handler lourd (bloque l'event loop) -@on("user.created") -async def bad_handler(self, event): - # Envoi email synchrone (lent !) - send_email_sync(event.data["email"]) - - # Traitement lourd - for i in range(1000000): - process(i) - -# ✅ BON: Déléguer au scheduler -@on("user.created") -async def good_handler(self, event): - # Quick: queue la tâche - await self.scheduler.add_job( - self._send_welcome_email, - args=[event.data["email"]] - ) -``` - -### 2. Priorités Explicites - -```python -# ❌ MAUVAIS: Priorités arbitraires -@on("user.created", priority=42) -@on("user.created", priority=99) - -# ✅ BON: Priorités sémantiques -PRIORITY_LOGGING = 100 # Logging en premier -PRIORITY_BUSINESS = 50 # Logique métier -PRIORITY_CLEANUP = 10 # Cleanup en dernier - -@on("user.created", priority=PRIORITY_LOGGING) -@on("user.created", priority=PRIORITY_BUSINESS) -``` - -### 3. Nommage des Événements - -```python -# ❌ MAUVAIS: Noms obscurs -await self.ctx.events.emit("stuff.happened", data) -await self.ctx.events.emit("do_thing", data) - -# ✅ BON: Noms sémantiques (domain.event) -await self.ctx.events.emit("user.created", data) -await self.ctx.events.emit("user.deleted", data) -await self.ctx.events.emit("order.completed", data) -``` - -### 4. Stop Propagation (Usage Avancé) - -```python -from xcore.sdk import TrustedBase, on, Event - -class Plugin(TrustedBase): - - @on("user.created", priority=100) - async def validate_user(self, event: Event): - """Validation en premier, peut bloquer les autres handlers.""" - if not self._is_valid(event.data): - self.logger.error("Utilisateur invalide") - event.stop_propagation() # ⚠️ Stop les handlers suivants - return - - @on("user.created", priority=50) - async def send_welcome(self, event: Event): - """Ne s'exécute que si validation passe.""" - if event.propagation_stopped: - self.logger.debug("Propagation stoppée, skip welcome") - return - - # Envoyer email... -``` - ---- - -## 4. Hooks Synchrones - -Les hooks permettent de modifier des données ou d'exécuter de la logique synchrone pendant un processus spécifique. - -### Architecture du HookManager - -```mermaid -flowchart LR - subgraph Filters["🔄 Filters (Transformation)"] - F1[Value] --> F2[Hook 1] - F2 --> F3[Hook 2] - F3 --> F4[Hook 3] - F4 --> F5[Modified Value] - end - - subgraph Actions["⚡ Actions (Side Effects)"] - A1[Trigger] --> A2[Hook 1] - A2 --> A3[Hook 2] - A3 --> A4[Hook 3] - A4 --> A5[Done] - end - - style Filters fill:#E3F2FD,stroke:#1976D2 - style Actions fill:#FFF3E0,stroke:#F57C00 -``` - -### A. Filters (Transformation de Données) - -Les filters permettent de "faire passer une valeur dans une chaîne" pour laisser d'autres plugins la modifier. - -```mermaid -flowchart LR - V[Valeur Originale] --> H1[Hook 1] - H1 -->|Modifie| H2[Hook 2] - H2 -->|Modifie| H3[Hook 3] - H3 -->|Modifie| R[Valeur Finale] - - style V fill:#E8F5E9 - style R fill:#C8E6C9 -``` - -```python -# Côté Kernel ou Plugin -class Plugin(TrustedBase): - - async def render_page(self): - # 1. Valeur de base - title = "Bienvenue sur XCore" - - # 2. Appliquer les filters - title = self.ctx.hooks.apply_filters("page_title", title) - - # title peut maintenant être modifié par d'autres plugins - # Ex: "Bienvenue sur XCore | Dashboard" - - return f"

{title}

" - - # 3. Enregistrer un filter - @filter("page_title") - def modify_title(self, title: str) -> str: - """Ajoute le suffixe au titre.""" - return f"{title} | Mon App" -``` - -### B. Actions (Side Effects Sync) - -Les actions sont des side effects synchrones qui ne retournent pas de valeur. - -```python -# Côté Kernel ou Plugin -class Plugin(TrustedBase): - - async def render_template(self, template_name: str): - # 1. Trigger action avant rendu - self.ctx.hooks.do_action("before_render", template=template_name) - - # 2. Rendu du template - result = self._render(template_name) - - # 3. Trigger action après rendu - self.ctx.hooks.do_action("after_render", template=template_name, result=result) - - return result - - # 4. Enregistrer un action hook - @action_hook("before_render") - def on_before_render(self, template: str): - """Log chaque rendu de template.""" - self.logger.debug(f"Rendu du template: {template}") -``` - ---- - -## 5. Différence Entre Events et Hooks - -| Caractéristique | Events | Hooks | -| :--- | :--- | :--- | -| **Exécution** | Asynchrone (`async def`) | Synchrone (`def`) | -| **Retourne Valeur** | ❌ Non | ✅ Oui (Filters) | -| **Parallèle** | ✅ Oui (`asyncio.gather`) | ❌ Non (Séquentiel) | -| **Propagation** | ✅ `stop_propagation()` | ❌ N/A | -| **Priorités** | ✅ Oui | ⚠️ Ordre d'enregistrement | -| **Use Case** | Side effects découplés | Transformation de données | - -### Quand Utiliser Quoi ? - -```mermaid -flowchart TB - Start{Besoin ?} - - Start -->|Modifier données| Hooks - Start -->|Side effect| Events - - Start -->|Synchrone requis| Hooks - Start -->|Async OK| Events - - Start -->|Plusieurs listeners| Events - Start -->|Chaîne transformation| Hooks - - Hooks --> H[🪝 Hooks] - Events --> E[📡 Events] - - style H fill:#FFF3E0,stroke:#F57C00 - style E fill:#E3F2FD,stroke:#1976D2 -``` - -### Exemple Comparatif - -```python -# 📡 EVENT: Pour side effect découplé -@on("user.created") -async def send_welcome_email(self, event): - # Envoi email async (ne modifie pas les données) - await self.email_service.send(event.data["email"]) - -# 🪝 HOOK FILTER: Pour modifier des données -@filter("user.profile_data") -def add_premium_badge(self, profile_data: dict) -> dict: - # Modifie les données avant retour - if self._is_premium(user_id): - profile_data["badge"] = "premium" - return profile_data - -# 🪝 HOOK ACTION: Pour side effect synchrone -@action_hook("before_user_save") -def log_user_change(self, user_data: dict): - # Logging synchrone - self.logger.debug(f"User update: {user_data}") -``` - ---- - -## 6. Exemple Complet: Système de Notification - -```python -from xcore.sdk import TrustedBase, on, action, ok, filter - -class NotificationPlugin(TrustedBase): - """ - Plugin de notification qui démontre Events + Hooks. - """ - - async def on_load(self): - # S'abonner aux événements système - self.ctx.events.on("user.created", self._on_user_created, priority=50) - self.ctx.events.on("order.completed", self._on_order_completed, priority=50) - - @on("user.created") - async def _on_user_created(self, event): - """Envoyer email de bienvenue.""" - email = event.data.get("email") - await self._send_email(email, "Bienvenue !") - - @on("order.completed") - async def _on_order_completed(self, event): - """Envoyer confirmation de commande.""" - email = event.data.get("email") - await self._send_email(email, "Commande confirmée !") - - @filter("notification.message") - def add_disclaimer(self, message: str) -> str: - """Ajouter un disclaimer à tous les messages.""" - return f"{message}\n\n-- Ceci est un message automatique" - - @action("send_notification") - async def send_notification(self, payload: dict): - """Action publique pour envoyer une notification.""" - # Appliquer les filters - message = self.ctx.hooks.apply_filters( - "notification.message", - payload.get("message", "") - ) - - # Émettre un événement pour logging - await self.ctx.events.emit("notification.sent", { - "to": payload.get("to"), - "message": message - }) - - return ok(status="sent") -``` - ---- - -## 7. Debugging et Monitoring - -### Voir les Handlers Enregistrés - -```bash -# Lister tous les handlers d'événements -xcore events list - -# Lister tous les hooks -xcore hooks list -``` - -### Tracing des Événements - -```python -# Activer le debug logging -# xcore.yaml -logging: - level: DEBUG - events: true # Log tous les événements émis -``` - -### Diagnostic des Problèmes - -| Problème | Cause Possible | Solution | -| :--- | :--- | :--- | -| Handler non appelé | Mauvais nom d'événement | Vérifier le nom exact | -| Handler appelé trop tard | Priorité trop basse | Augmenter priority | -| Memory leak | Handler non unsubscribed | Unsubscribe dans `on_unload` | -| Event loop bloquée | Handler trop lourd | Déléguer au scheduler | - ---- - -## Prochaines Lectures - -| 📚 Guide | Objectif | -| :--- | :--- | -| [Creating Plugins](creating-plugins.md) | Développer des plugins | -| [Services](services.md) | Utiliser les services partagés | -| [Monitoring](monitoring.md) | Observer les événements en prod | diff --git a/docs-tech/guides/marketplace.md b/docs-tech/guides/marketplace.md deleted file mode 100644 index 14ce7b42..00000000 --- a/docs-tech/guides/marketplace.md +++ /dev/null @@ -1,56 +0,0 @@ -# Marketplace Guide - -The XCore Marketplace is a central repository for sharing and discovering plugins. It allows you to extend your application with pre-built components like authentication providers, payment gateways, and logging tools. - ---- - -## 1. Using the Marketplace CLI - -The `xcore marketplace` command set allows you to interact with the official registry. - -### Searching for Plugins -Search by name or description: -```bash -xcore marketplace search "notification" -``` - -### Getting Plugin Details -View metadata, version history, and required permissions before installing: -```bash -xcore marketplace info notify-provider -``` - -### Installing a Plugin -Downloads the plugin and extracts it into your `./plugins` directory: -```bash -xcore marketplace install notify-provider -``` - ---- - -## 2. Marketplace Configuration - -In your `xcore.yaml`, you can configure custom registry URLs or provide API keys for private marketplaces. - -```yaml -marketplace: - url: "https://marketplace.xcore.dev" - api_key: "${MARKETPLACE_KEY}" - timeout: 10 -``` - ---- - -## 3. Security in the Marketplace - -The XCore team performs automated and manual security audits on marketplace plugins: - -- **Automatic Scanning**: All plugins are scanned by the XCore `ASTScanner` and `Bandit` during submission. -- **Verification**: "Verified" plugins undergo a manual code review. -- **Sandboxing**: We recommend running marketplace plugins in `sandboxed` mode unless you trust the author explicitly. - ---- - -## 4. Publishing Your Own Plugin - -Coming Soon: The publishing process is currently in private beta. To join, contact the XCore team at `marketplace@xcore.dev`. diff --git a/docs-tech/guides/monitoring.md b/docs-tech/guides/monitoring.md deleted file mode 100644 index 9790505c..00000000 --- a/docs-tech/guides/monitoring.md +++ /dev/null @@ -1,139 +0,0 @@ -# Monitoring & Observability - -XCore provides a comprehensive observability system to monitor your plugins and services in production. - -## Overview - -The XCore observability system is built on four pillars: - -1. **Metrics**: Counters, gauges, and histograms to measure performance. -2. **Health Checks**: Real-time status monitoring of services and plugins. -3. **Logging**: Structured logging with configurable levels and rotation. -4. **Tracing**: Distributed tracing for debugging complex request flows. - -## Metrics - -The metrics registry supports three primary types: - -- **Counter**: Values that only increase (e.g., total requests). -- **Gauge**: Values that can go up or down (e.g., active connections). -- **Histogram**: Distribution of values (e.g., request latency). - -### Basic Usage - -```python -from xcore.kernel.observability.metrics import MetricsRegistry -from xcore.sdk import TrustedBase - -class Plugin(TrustedBase): - async def on_load(self) -> None: - self.metrics = MetricsRegistry() - - # Define metrics - self.request_count = self.metrics.counter("http.requests.total") - self.active_connections = self.metrics.gauge("connections.active") - self.request_latency = self.metrics.histogram("http.request.duration") - - def get_router(self): - from fastapi import APIRouter - import time - router = APIRouter() - - @router.get("/data") - async def get_data(): - start = time.monotonic() - self.active_connections.inc() - try: - # Logic here... - self.request_count.inc() - return {"status": "ok"} - finally: - self.request_latency.observe(time.monotonic() - start) - self.active_connections.dec() - return router -``` - -### Metrics with Labels - -Labels allow you to add dimensions to your metrics for more granular analysis. - -```python -self.request_count = self.metrics.counter( - "http.requests.total", - labels={"plugin": "my_plugin", "method": "GET"} -) -``` - -## Health Checks - -Health checks allow you to monitor the status of your dependencies and internal state. - -### Registering Checks - -```python -from xcore.kernel.observability.health import HealthChecker - -class Plugin(TrustedBase): - async def on_load(self) -> None: - self.health = HealthChecker() - self.db = self.get_service("db") - - @self.health.register("database") - async def check_db(): - try: - async with self.db.session() as session: - await session.execute("SELECT 1") - return True, "Database OK" - except Exception as e: - return False, str(e) -``` - -## Logging - -XCore uses structured logging to make logs easier to parse and analyze. - -### Configuration (`xcore.yaml`) - -```yaml -observability: - logging: - level: INFO - format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - file: "logs/app.log" -``` - -### Usage in Plugins - -```python -import logging -logger = logging.getLogger("xcore.plugin.my_plugin") - -class Plugin(TrustedBase): - async def on_load(self): - logger.info("Plugin loaded successfully", extra={"version": "1.0.0"}) -``` - -## Tracing - -Tracing helps you visualize the flow of requests across multiple plugins and services. - -```python -from xcore.kernel.observability.tracing import Tracer - -class Plugin(TrustedBase): - async def on_load(self): - self.tracer = Tracer(service_name="my_plugin") - - async def handle(self, action, payload): - with self.tracer.span("process_action", action=action) as span: - # Operation logic... - span.set_attribute("result", "success") - return ok() -``` - -## Best Practices - -1. **Meaningful Names**: Use clear, dot-separated names for metrics (e.g., `api.user.login_success`). -2. **Avoid High Cardinality**: Don't use unique IDs (like user IDs) as labels in metrics. -3. **Use Appropriate Levels**: Use `DEBUG` for verbose info, `INFO` for general flow, and `ERROR` for actual failures. -4. **Monitor Dependencies**: Always include health checks for external APIs or databases your plugin relies on. diff --git a/docs-tech/guides/scaling.md b/docs-tech/guides/scaling.md deleted file mode 100644 index e8e5e6d3..00000000 --- a/docs-tech/guides/scaling.md +++ /dev/null @@ -1,147 +0,0 @@ -# Scaling & High Availability - -This guide covers scaling strategies for deploying XCore in production environments with high availability. - -## Overview - -XCore supports several deployment modes to meet varying scale requirements: - -- **Single Instance**: Development and small workloads. -- **Multi-Instance**: Horizontal scaling with a load balancer. -- **Cluster Mode**: Multiple nodes with shared coordination (via Redis). - -## Scaling Architecture - -```text - ┌─────────────────┐ - │ Load Balancer │ - │ (Nginx/HAProxy)│ - └────────┬────────┘ - │ - ┌────────────────────┼────────────────────┐ - │ │ │ - ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ - │ XCore 1 │ │ XCore 2 │ │ XCore 3 │ - │ │ │ │ │ │ - │ Plugins │ │ Plugins │ │ Plugins │ - └────┬────┘ └────┬────┘ └────┬────┘ - │ │ │ - └────────────────────┼────────────────────┘ - │ - ┌────────▼────────┐ - │ Redis Cluster │ - │ (Cache + Queue) │ - └─────────────────┘ - │ - ┌──────────────┼──────────────┐ - │ │ │ - ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ - │ PostgreSQL │ │ PostgreSQL │ │ PostgreSQL │ - │ Primary │ │ Replica │ │ Replica │ - └─────────────┘ └─────────────┘ └─────────────┘ -``` - -## Multi-Instance Configuration - -### 1. Load Balancer (Nginx) - -```nginx -upstream xcore_backend { - least_conn; # Load balance by connection count - server 192.168.1.10:8080 weight=5; - server 192.168.1.11:8080 weight=5; - server 192.168.1.12:8080 weight=5 backup; - keepalive 32; -} - -server { - listen 80; - server_name api.example.com; - - location / { - proxy_pass http://xcore_backend; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # Timeouts - proxy_connect_timeout 30s; - proxy_send_timeout 30s; - proxy_read_timeout 30s; - } -} -``` - -### 2. Redis Cluster Configuration - -```yaml -# xcore.yaml -services: - cache: - backend: redis - url: "redis://redis-cluster:6379" - cluster: - enabled: true - nodes: - - "redis://192.168.1.20:6379" - - "redis://192.168.1.21:6379" - - scheduler: - enabled: true - backend: redis - url: "redis://redis-cluster:6379" -``` - -### 3. Database with Replication - -```yaml -services: - databases: - primary: - type: postgresql - url: "${DATABASE_PRIMARY_URL}" - pool_size: 20 - - replica1: - type: postgresql - url: "${DATABASE_REPLICA1_URL}" - pool_size: 10 - read_only: true -``` - -## Session & State Distribution - -### External Sessions - -To ensure "statelessness", always store user sessions and shared state in an external cache (Redis) rather than in memory. - -```python -class SessionManager(TrustedBase): - async def on_load(self) -> None: - self.cache = self.get_service("cache") - - async def get_session(self, session_id: str) -> dict | None: - # Sessions are shared across all XCore instances via Redis - return await self.cache.get(f"session:{session_id}") -``` - -## Distributed Rate Limiting - -By using the Redis cache backend, XCore's built-in rate limiter becomes distributed automatically across all running instances. - -```yaml -# plugin.yaml -resources: - rate_limit: - calls: 100 - period_seconds: 60 -``` -*Note: This limit applies globally across all instances.* - -## Best Practices - -1. **Stateless Design**: Never store state locally in a plugin; use Redis or a Database. -2. **Graceful Shutdown**: Always handle `SIGTERM` to allow tasks to complete before an instance stops. -3. **Circuit Breaker**: Use circuit breakers when calling external APIs to prevent cascading failures. -4. **Auto-Scaling**: Use metrics (CPU/RAM/Request Rate) to scale instances automatically in Kubernetes or AWS. -5. **Database Connection Pooling**: Configure appropriate pool sizes for each instance to avoid overwhelming the database. diff --git a/docs-tech/guides/security.md b/docs-tech/guides/security.md deleted file mode 100644 index 3ba8354e..00000000 --- a/docs-tech/guides/security.md +++ /dev/null @@ -1,639 +0,0 @@ -# Security Deep Dive - -XCore suit un modèle **"Zero Trust"** pour les plugins. Même les plugins trusted sont restreints de modifier l'état du kernel, tandis que les plugins sandboxed sont isolés aux niveaux OS et Runtime. - ---- - -## Vue d'Ensemble de la Sécurité - -```mermaid -flowchart TB - subgraph Defense["Défense en Profondeur"] - direction TB - L1[🛡️ Couche 1: AST Scan
Analyse statique] - L2[🛡️ Couche 2: Process Isolation
Sandbox OS] - L3[🛡️ Couche 3: Resource Limits
Memory/CPU] - L4[🛡️ Couche 4: Permissions
Policy Engine] - L5[🛡️ Couche 5: Signature
HMAC Verification] - end - - subgraph Attaques["Attaques Bloquées"] - A1[Code Injection] - A2[Sandbox Escape] - A3[DoS Resource] - A4[Accès Non Autorisé] - A5[Tampering] - end - - L1 -.->|Bloque| A1 - L2 -.->|Bloque| A2 - L3 -.->|Bloque| A3 - L4 -.->|Bloque| A4 - L5 -.->|Bloque| A5 - - style Defense fill:#E8F5E9,stroke:#388E3C - style L1 fill:#C8E6C9 - style L2 fill:#C8E6C9 - style L3 fill:#C8E6C9 - style L4 fill:#C8E6C9 - style L5 fill:#C8E6C9 -``` - ---- - -## 1. Le Multi-Layer Sandbox - -Quand `execution_mode: sandboxed` est défini, le kernel applique **3 couches de protection** successives. - -### Vue d'Ensemble du Flux - -```mermaid -flowchart LR - subgraph PreExec["Pré-Exécution"] - A[Code Source] --> B[🔍 AST Scanner] - B --> C{Scan OK ?} - C -->|Non| D[❌ Rejeté] - C -->|Oui| E[📦 Spawn Worker] - end - - subgraph Exec["Exécution"] - E --> F[📡 JSON-RPC IPC] - F --> G[⏱️ Timeout Monitor] - G --> H[🧠 Memory Monitor] - end - - subgraph PostExec["Post-Exécution"] - H --> I{Limites OK ?} - I -->|Non| J[❌ Kill Worker] - I -->|Oui| K[✅ Résultat] - end - - style PreExec fill:#FFF3E0,stroke:#F57C00 - style Exec fill:#E3F2FD,stroke:#1976D2 - style PostExec fill:#E8F5E9,stroke:#388E3C -``` - ---- - -### Couche 1: Analyse Statique (AST) - -Avant l'exécution, l'`ASTScanner` parse l'arbre syntaxique du plugin avec un `ast.NodeVisitor`. - -```mermaid -flowchart TB - subgraph AST["🔍 AST Scanner"] - S[Code Source] --> P[Parser AST] - P --> N[NodeVisitor] - - N --> F1[Forbidden Names] - N --> F2[Forbidden Attributes] - N --> F3[Import Whitelist] - - F1 --> B1[❌ eval, exec] - F1 --> B2[❌ globals, locals] - F1 --> B3[❌ __import__] - - F2 --> B4[❌ __class__] - F2 --> B5[❌ __globals__] - F2 --> B6[❌ __subclasses__] - - F3 --> B7[❌ os, subprocess] - F3 --> B8[❌ socket, requests] - end - - style AST fill:#FFEBEE,stroke:#C62828 - style B1 fill:#FFCDD2 - style B2 fill:#FFCDD2 - style B3 fill:#FFCDD2 - style B4 fill:#FFCDD2 - style B5 fill:#FFCDD2 - style B6 fill:#FFCDD2 - style B7 fill:#FFCDD2 - style B8 fill:#FFCDD2 -``` - -#### Liste Complète des Restrictions - -```python -# ❌ Forbidden Names (bloqués par l'AST) -FORBIDDEN_NAMES = { - 'eval', 'exec', 'compile', - 'globals', 'locals', 'vars', - '__import__', 'open', 'input', - 'breakpoint', 'delattr', 'setattr' -} - -# ❌ Forbidden Attributes (sandboxes escapes courants) -FORBIDDEN_ATTRIBUTES = { - '__class__', '__bases__', '__subclasses__', - '__globals__', '__code__', '__closure__', - '__builtins__', '__import__', 'gi_frame', - 'cr_frame', 'f_globals', 'f_locals' -} - -# ❌ Forbidden Modules (nécessite allowed_imports) -FORBIDDEN_MODULES = { - 'os', 'sys', 'subprocess', 'multiprocessing', - 'socket', 'requests', 'urllib', 'http', - 'pickle', 'marshal', 'shelve' -} -``` - -#### Exemple de Configuration - -```yaml -# plugin.yaml -# Pour utiliser des modules normalement restreints -allowed_imports: - - datetime # ✅ Autorisé - - json # ✅ Autorisé - - hashlib # ✅ Autorisé (si besoin) - - re # ✅ Autorisé - -# Tentative d'import bloqué : -# import os # ❌ Erreur: Module 'os' non autorisé -``` - ---- - -### Couche 2: Isolation Processus - -Les plugins sandboxed s'exécutent dans un **processus OS dédié** via le module `multiprocessing`. - -```mermaid -sequenceDiagram - participant K as 🎯 Kernel Process - participant M as 📡 Multiprocessing Manager - participant W as 📦 Sandbox Worker - - Note over K: Boot du kernel - K->>M: spawn_worker(plugin_path) - - Note over M: Création processus - M->>W: fork()/spawn() - W->>W: Charger code plugin - - Note over K,W: Communication via pipes - K->>W: {"jsonrpc":"2.0","method":"call",...} - Note over K,W: JSON uniquement
⚠️ Pas de pickle ! - - W->>W: Exécuter handler - W->>K: {"jsonrpc":"2.0","result":...} - - Note over K: Désérialiser résultat -``` - -#### Transport: JSON-RPC 2.0 sur Pipes OS - -```python -# Format des messages Kernel → Worker -{ - "jsonrpc": "2.0", - "id": "uuid-1234-5678", - "method": "call", - "params": { - "action": "greet", - "payload": {"name": "World"} - } -} - -# Format des messages Worker → Kernel -{ - "jsonrpc": "2.0", - "id": "uuid-1234-5678", - "result": {"status": "ok", "message": "Hello !"} -} - -# Ou en cas d'erreur -{ - "jsonrpc": "2.0", - "id": "uuid-1234-5678", - "error": { - "code": -32000, - "message": "Action not found" - } -} -``` - -#### Pourquoi JSON-RPC ? - -| Avantage | Description | -| :--- | :--- | -| **Sérialisation sûre** | Seulement types JSON (pas de `pickle` → pas de RCE) | -| **Langage-agnostique** | Pourrait être implémenté dans tout langage | -| **Debug-friendly** | Messages lisibles en clair | -| **Standardisé** | Spécification JSON-RPC 2.0 | - ---- - -### Couche 3: Limites de Ressources OS - -Le kernel monitorle RSS (Resident Set Size) et le temps CPU du worker. - -```mermaid -flowchart LR - subgraph Monitors["📊 Resource Monitors"] - MM[🧠 Memory Monitor
RSS Tracker] - TM[⏱️ Timeout Monitor
Execution Timer] - CM[📈 CPU Monitor
CPU Time Tracker] - end - - subgraph Limits["⚠️ Limites"] - ML[Max: max_memory_mb] - TL[Max: timeout_seconds] - CL[Max: cpu_seconds] - end - - subgraph Actions["🔥 Actions"] - MA[⚠️ Warning] - KA[❌ Kill Process] - RA[📝 Log Violation] - end - - MM --> ML --> MA --> KA - TM --> TL --> KA - CM --> CL --> KA - - style Monitors fill:#E3F2FD,stroke:#1976D2 - style Limits fill:#FFF3E0,stroke:#F57C00 - style Actions fill:#FFEBEE,stroke:#C62828 -``` - -#### Configuration des Limites - -```yaml -# plugin.yaml -resources: - # Mémoire maximale (Mo) - max_memory_mb: 256 - - # Timeout d'exécution (secondes) - timeout_seconds: 30 - - # Rate limiting - rate_limit: - calls: 1000 - period_seconds: 60 -``` - -#### Comportement en Cas de Dépassement - -| Ressource | Limite | Comportement | -| :--- | :--- | :--- | -| **Mémoire** | `max_memory_mb` | Warning à 80%, Kill à 100% | -| **Timeout** | `timeout_seconds` | Kill immédiat + erreur timeout | -| **Rate Limit** | `calls/period` | Erreur 429 Too Many Requests | - ---- - -## 2. Permission Engine - -Chaque appel inter-plugin et accès service est audité par le **PermissionEngine**. - -### Modèle d'Évaluation - -```mermaid -flowchart TB - subgraph Evaluation["🔍 Évaluation Permission"] - Start[Début Évaluation] - CheckDeny[Vérifier DENY explicite ?] - CheckAllow[Vérifier ALLOW explicite ?] - DefaultDeny[❌ DENY par défaut] - Allow[✅ ALLOW] - Deny[❌ DENY] - end - - Start --> CheckDeny - CheckDeny -->|Oui| Deny - CheckDeny -->|Non| CheckAllow - CheckAllow -->|Oui| Allow - CheckAllow -->|Non| DefaultDeny - - style Deny fill:#FFCDD2,stroke:#C62828 - style Allow fill:#C8E6C9,stroke:#388E3C - style DefaultDeny fill:#FFCDD2,stroke:#C62828 -``` - -### Modèle "Fail-Closed" - -XCore utilise un modèle **"Fail-Closed"** : si aucune policy n'autorise explicitement une action, elle est **refusée**. - -``` -1. Vérifier DENY explicite → Si trouvé: ❌ REFUSÉ -2. Vérifier ALLOW explicite → Si trouvé: ✅ AUTORISÉ -3. Aucun match → ❌ REFUSÉ (défaut) -``` - -### Patterns de Permissions - -#### Wildcards sur les Ressources - -```yaml -permissions: - # ✅ Autorise lecture sur TOUTES les tables 'users_*' - - resource: "db.users.*" - actions: ["read"] - effect: allow - - # ✅ Autorise lecture/écriture sur cache global - - resource: "cache.global" - actions: ["read", "write"] - effect: allow - - # ✅ Autorise TOUS les actions sur une resource - - resource: "logs.*" - actions: ["*"] - effect: allow - - # ❌ DENY explicite (priorité haute) - - resource: "db.admin.*" - actions: ["*"] - effect: deny -``` - -#### Exemple Complet de Policy - -```yaml -# plugin.yaml -permissions: - # Lecture seule sur la DB principale - - resource: "db.main.users" - actions: ["read"] - effect: allow - - - resource: "db.main.posts" - actions: ["read"] - effect: allow - - # Écriture sur cache - - resource: "cache.global" - actions: ["read", "write"] - effect: allow - - # Accès scheduler - - resource: "scheduler.jobs" - actions: ["create", "delete"] - effect: allow - - # Interdiction explicite (sécurité défense) - - resource: "db.admin.*" - actions: ["*"] - effect: deny - - - resource: "kernel.*" - actions: ["*"] - effect: deny -``` - -### Flux d'Évaluation - -```mermaid -sequenceDiagram - participant A as Plugin A - participant S as Supervisor - participant P as PermissionEngine - participant L as Policy Loader - - A->>S: call("plugin_b", "get_user") - S->>P: evaluate("plugin_a", "plugin_b.get_user") - - P->>L: Charger policies de plugin_a - L-->>P: Retourner rules - - P->>P: Vérifier DENY explicite - Note over P: ❌ Aucun DENY trouvé - - P->>P: Vérifier ALLOW explicite - Note over P: ✅ Rule trouvée ! - - P-->>S: ✅ Autorisé - S->>A: Procéder à l'appel -``` - ---- - -## 3. Signature des Plugins Trusted - -En production, le mode `strict_trusted` garantit que les plugins trusted n'ont pas été altérés. - -### Architecture de Signature - -```mermaid -flowchart TB - subgraph Sign["📝 Signature (Développement)"] - D1[Fichiers Plugin] --> D2[Calcul HMAC-SHA256] - D2 --> D3[Générer plugin.sig] - end - - subgraph Verify["🔍 Vérification (Production)"] - V1[Charger plugin.sig] --> V2[Re-calculer hashes] - V2 --> V3{Match ?} - V3 -->|Oui| V4[✅ Charger] - V3 -->|Non| V5[❌ Rejeté] - end - - Sign --> Verify - - style Sign fill:#E8F5E9,stroke:#388E3C - style Verify fill:#FFEBEE,stroke:#C62828 -``` - -### Générer une Signature - -```bash -# Signer un plugin trusted -xcore plugin sign plugins/my_plugin/ - -# Le fichier plugin.sig est créé/mis à jour -``` - -### Structure de `plugin.sig` - -```json -{ - "plugin": "my_plugin", - "version": "1.0.0", - "timestamp": "2024-01-15T10:30:00Z", - "files": { - "plugin.yaml": "a1b2c3d4e5f6...", - "src/main.py": "f6e5d4c3b2a1...", - "src/utils.py": "1234567890ab..." - }, - "signature": "hmac-sha256-signature-here" -} -``` - -### Vérification au Boot - -```mermaid -sequenceDiagram - participant K as Kernel - participant S as SignatureVerifier - participant F as Fichiers Plugin - participant Sig as plugin.sig - - K->>S: verify_trusted("my_plugin") - - S->>Sig: Charger signature - S->>F: Lire fichiers actuels - S->>S: Calculer hashes actuels - - S->>S: Comparer avec plugin.sig - alt Hashes correspondent - S-->>K: ✅ Vérifié - K->>K: Charger plugin - else Hashes différents - S-->>K: ❌ Tampering détecté - K->>K: ❌ Refuser chargement - end -``` - ---- - -## 4. Bonnes Pratiques pour les Développeurs - -### Checklist de Sécurité - -```mermaid -flowchart LR - subgraph Do["✅ À Faire"] - D1[Sandboxed par défaut] - D2[Permissions minimales] - D3[Valider inputs] - D4[Pas d'état local] - D5[Signer en prod] - end - - subgraph Dont["❌ À Éviter"] - A1[Trusted sans besoin] - A2[Permissions larges] - A3[Inputs non validés] - A4[État dans le plugin] - A5[Code non signé] - end - - Do -.->|Au lieu de| Dont - - style Do fill:#E8F5E9,stroke:#388E3C - style Dont fill:#FFEBEE,stroke:#C62828 -``` - -### 1. Utiliser le Sandboxing par Défaut - -```yaml -# ✅ RECOMMANDÉ: Sandboxed pour isolation maximale -execution_mode: sandboxed - -# ⚠️ UNIQUEMENT SI NÉCESSAIRE: Trusted pour accès kernel -execution_mode: trusted -# Nécessite: accès FastAPI hooks, objets kernel complexes -``` - -### 2. Demander des Permissions Minimales - -```yaml -# ❌ TROP LARGE -permissions: - - resource: "db.*" - actions: ["*"] - effect: allow - -# ✅ MINIMAL -permissions: - - resource: "db.main.users" - actions: ["read"] - effect: allow - - resource: "db.main.posts" - actions: ["read", "write"] - effect: allow -``` - -### 3. Valider Tous les Inputs - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok -from pydantic import BaseModel, Field - -# ✅ Schema de validation -class GreetInput(BaseModel): - name: str = Field(min_length=1, max_length=100) - language: str = Field(default="fr") - -class Plugin(AutoDispatchMixin, TrustedBase): - - @action("greet") - async def greet(self, payload: dict): - # Valider l'input - try: - validated = GreetInput(**payload) - except ValidationError as e: - return error(msg=str(e), code="invalid_input") - - return ok(message=f"Bonjour, {validated.name} !") -``` - -### 4. Pas d'État Local - -```python -# ❌ MAUVAIS: État dans le plugin -class Plugin(TrustedBase): - def __init__(self): - self.cache_local = {} # ⚠️ Perdu au reload ! - -# ✅ BON: État dans les services -class Plugin(TrustedBase): - async def on_load(self): - self.cache = self.get_service("cache") # ✅ Persistant - - async def store(self, key, value): - await self.cache.set(key, value) # ✅ Sauvegardé -``` - -### 5. Signer en Production - -```bash -# Développement -# Pas de signature requise - -# Production (strict_trusted activé) -xcore plugin sign plugins/my_plugin/ - -# Vérifier -xcore plugin validate plugins/my_plugin/ -``` - ---- - -## 5. matrice de Sécurité - -### Comparaison Trusted vs Sandboxed - -| Caractéristique | Trusted | Sandboxed | -| :--- | :--- | :--- | -| **Processus** | Principal | Isolé (OS) | -| **Performance** | ⚡ Rapide | 🐌 IPC overhead | -| **Isolation** | 🔶 Logique | 🔒 Physique | -| **Accès Kernel** | ✅ Complet | ❌ Via IPC uniquement | -| **AST Scan** | ⚠️ Partiel | ✅ Complet | -| **Usage** | Dev / Confiance | Prod / Tiers | - -### Tableau des Permissions par Défaut - -| Action | Trusted | Sandboxed | -| :--- | :--- | :--- | -| Appeler autre plugin | ✅ (avec permission) | ✅ (avec permission) | -| Accéder DB publique | ✅ | ✅ | -| Accéder DB privée | ❌ | ❌ | -| Modifier kernel | ❌ | ❌ | -| Import restreint | ⚠️ (whitelist) | ❌ (bloqué AST) | -| Route HTTP | ✅ | ✅ (via IPC) | - ---- - -## Prochaines Lectures - -| 📚 Guide | Objectif | -| :--- | :--- | -| [Event System](events.md) | Messagerie sécurisée | -| [Plugin Creation](creating-plugins.md) | Développer des plugins | -| [Troubleshooting](troubleshooting.md) | Déboguer les erreurs | diff --git a/docs-tech/guides/services.md b/docs-tech/guides/services.md deleted file mode 100644 index 326e5b5b..00000000 --- a/docs-tech/guides/services.md +++ /dev/null @@ -1,95 +0,0 @@ -# Working with Services - -The `ServiceContainer` provides access to global infrastructure shared across all plugins. - ---- - -## 1. Database Service (`db`) - -The `db` service supports multiple engines and provides typed adapters for SQL and NoSQL. - -### A. SQL Patterns (PostgreSQL/SQLite) -We recommend using the `AsyncSQLAdapter` for non-blocking database operations. - -```python -async def get_user_tasks(self, user_id: int): - db = self.get_service("db") - async with db.session() as session: - result = await session.execute( - "SELECT * FROM tasks WHERE user_id = :uid", - {"uid": user_id} - ) - return [dict(row) for row in result.fetchall()] -``` - -### B. Redis Patterns -The Redis service is ideal for distributed locks, counters, and high-frequency data. - -```python -async def rate_limit_check(self, user_id: str): - redis = self.get_service("redis") - key = f"limit:{user_id}" - count = await redis.incr(key) - if count == 1: - await redis.expire(key, 60) - return count <= 100 -``` - ---- - -## 2. Cache Service (`cache`) - -Unified interface for `memory` and `redis` backends. - -### `get_or_set` Pattern -Simplifies the "check cache, then fetch" logic. - -```python -data = await self.cache.get_or_set( - f"profile:{id}", - factory=lambda: self._fetch_profile_from_db(id), - ttl=3600 -) -``` - ---- - -## 3. Scheduler Service (`scheduler`) - -Powered by APScheduler, allowing both one-time and recurring tasks. - -```python -def sync_data(self): - print("Syncing data...") - -async def on_load(self): - scheduler = self.get_service("scheduler") - # Add a CRON job - scheduler.add_job( - self.sync_data, - trigger="cron", - hour="0", - minute="0", - id="daily_sync" - ) -``` - ---- - -## 4. Custom Extensions - -Extensions are global services loaded from the `extensions/` directory. - -```python -# In extensions/my_ext/extension.py -class MyExtension: - def __init__(self, config): - self.config = config - def ping(self): - return "pong" - -# In your Plugin -async def on_load(self): - ext = self.get_service("ext.my_ext") - print(ext.ping()) -``` diff --git a/docs-tech/guides/troubleshooting.md b/docs-tech/guides/troubleshooting.md deleted file mode 100644 index 3e497d7e..00000000 --- a/docs-tech/guides/troubleshooting.md +++ /dev/null @@ -1,373 +0,0 @@ -# Troubleshooting Guide - -Common issues and solutions for XCore. - -## Installation Issues - -### Poetry Installation Fails - -**Problem**: Poetry installation fails or command not found - -**Solutions**: - -```bash -# Install Poetry -pip install --user poetry - -# Add to PATH -export PATH="$HOME/.local/bin:$PATH" - -# Or use pipx -pipx install poetry -``` - -### Dependency Conflicts - -**Problem**: Poetry dependency resolution fails - -**Solutions**: - -```bash -# Clear Poetry cache -poetry cache clear pypi --all - -# Update lock file -poetry lock --no-update - -# Install with specific Python -poetry env use python3.11 -poetry install -``` - -## Runtime Issues - -### Plugin Not Loading - -**Problem**: Plugin doesn't appear in loaded plugins - -**Check**: - -```bash -# Verify plugin structure -ls -la plugins/my_plugin/ -# Should show: plugin.yaml, src/main.py - -# Check plugin.yaml syntax -cat plugins/my_plugin/plugin.yaml | python -c "import yaml, sys; yaml.safe_load(sys.stdin)" - -# Check logs -poetry run xcore plugin list -``` - -**Solutions**: - -1. Check `plugin.yaml` is valid YAML -2. Verify `entry_point` path exists -3. Ensure `name` is unique -4. Check for syntax errors in `main.py` - -### Database Connection Errors - -**Problem**: Cannot connect to database - -**Error Messages**: -``` -ConnectionRefusedError: [Errno 111] Connection refused -psycopg2.OperationalError: could not connect to server -``` - -**Solutions**: - -```bash -# Check database is running -pg_isready -h localhost -p 5432 - -# Test connection -psql $DATABASE_URL -c "SELECT 1" - -# Verify environment variables -echo $DATABASE_URL - -# Check firewall/network -nc -zv localhost 5432 -``` - -### Redis Connection Errors - -**Problem**: Cannot connect to Redis - -**Solutions**: - -```bash -# Check Redis is running -redis-cli ping - -# Test with Python -python -c "import redis; r = redis.from_url('$REDIS_URL'); print(r.ping())" - -# Check URL format -# Correct: redis://:password@host:6379/0 -``` - -## Service Issues - -### Cache Not Working - -**Problem**: Cache returns None or doesn't persist - -**Solutions**: - -```python -# Check cache configuration -assert xcore.services.has("cache") - -cache = xcore.services.get("cache") - -# Test basic operations -await cache.set("test", "value", ttl=60) -value = await cache.get("test") -assert value == "value" -``` - -### Scheduler Not Running Jobs - -**Problem**: Scheduled jobs don't execute - -**Solutions**: - -```yaml -# Verify scheduler is enabled -services: - scheduler: - enabled: true - backend: redis # or memory -``` - -```python -# Check scheduler -scheduler = xcore.services.get("scheduler") -jobs = scheduler.get_jobs() -print(f"Scheduled jobs: {len(jobs)}") - -# Add test job -def test_job(): - print("Job executed!") - -scheduler.add_job(test_job, trigger="interval", seconds=5) -``` - -## Plugin Development Issues - -### Import Errors - -**Problem**: Cannot import from xcore.sdk - -**Solutions**: - -```python -# Correct imports -from xcore.sdk import TrustedBase, ok, error - -# If import fails, check: -# 1. Poetry shell is activated -# 2. Package is installed: poetry install -# 3. Python path includes xcore -``` - -### Service Access Errors - -**Problem**: `KeyError: Service 'db' not found` - -**Solutions**: - -```python -class Plugin(TrustedBase): - async def on_load(self) -> None: - # Wait for context to be injected - if self.ctx is None: - raise RuntimeError("Context not injected") - - # Check service exists before getting - if not self.ctx.services.has("db"): - raise RuntimeError("Database service not available") - - self.db = self.get_service("db") -``` - -### Async/Await Issues - -**Problem**: `RuntimeWarning: coroutine was never awaited` - -**Solutions**: - -```python -# Correct async/await usage -async def handle(self, action: str, payload: dict) -> dict: - # Must await async functions - result = await self.db.fetch("SELECT 1") - - # For sync operations in async context - import asyncio - loop = asyncio.get_event_loop() - result = await loop.run_in_executor(None, sync_function) - - return ok() -``` - -## Performance Issues - -### High Memory Usage - -**Problem**: Application uses too much memory - -**Solutions**: - -```yaml -# Reduce connection pool sizes -services: - databases: - default: - pool_size: 10 - max_overflow: 5 - - cache: - backend: memory - max_size: 5000 -``` - -```python -# Clear caches periodically -await self.cache.clear() - -# Use streaming for large datasets -async for row in self.db.iter_rows("SELECT * FROM large_table"): - yield row -``` - -### Slow Plugin Calls - -**Problem**: Plugin actions take too long - -**Solutions**: - -1. **Use caching**: -```python -async def get_data(self, key): - return await self.cache.get_or_set( - key, - factory=lambda: self._fetch_expensive_data(key), - ttl=300 - ) -``` - -2. **Database indexing**: -```sql -CREATE INDEX idx_users_email ON users(email); -``` - -3. **Async operations**: -```python -# Parallel calls -results = await asyncio.gather( - self.get_service("db").fetch("..."), - self.get_service("cache").get("..."), -) -``` - -## Debugging - -### Enable Debug Logging - -```yaml -# In config -observability: - logging: - level: DEBUG -``` - -```python -# In plugin -import logging - -logger = logging.getLogger("my_plugin") - -async def handle(self, action: str, payload: dict) -> dict: - logger.debug(f"Received action: {action}") - logger.debug(f"Payload: {payload}") - - try: - result = self._process(payload) - logger.info(f"Action {action} completed") - return ok(data=result) - except Exception as e: - logger.exception(f"Error in {action}") - return error(str(e)) -``` - -### Using PDB - -```python -async def handle(self, action: str, payload: dict) -> dict: - import pdb; pdb.set_trace() # Breakpoint - - # Debug with: - # (Pdb) p payload - # (Pdb) p self.ctx.services - # (Pdb) c # continue - - return ok() -``` - -### Health Check Endpoints - -```python -def get_router(self): - from fastapi import APIRouter - router = APIRouter() - - @router.get("/health") - async def health(): - checks = { - "database": await self._check_db(), - "cache": await self._check_cache(), - "memory": self._check_memory() - } - - healthy = all(checks.values()) - return { - "status": "healthy" if healthy else "unhealthy", - "checks": checks - } - - return router -``` - -## Common Error Messages - -| Error | Cause | Solution | -|-------|-------|----------| -| `Context not injected` | Accessing ctx before on_load | Wait for on_load to complete | -| `Service 'x' not found` | Service not configured | Check integration.yaml | -| `Plugin not found` | Plugin name incorrect | Check plugin.yaml name | -| `Rate limit exceeded` | Too many calls | Adjust rate_limit config | -| `Signature invalid` | Wrong secret key | Verify PLUGIN_SECRET_KEY | -| `Import error` | Forbidden import in sandbox | Use allowed imports only | - -## Getting Help - -If you can't resolve your issue: - -1. Check [GitHub Issues](https://github.com/traoreera/xcore/issues) -2. Search [Documentation](https://xcore.readthedocs.io) -3. Ask in [Discussions](https://github.com/traoreera/xcore/discussions) -4. Create a minimal reproduction case - -### Reporting Bugs - -Include: -- XCore version -- Python version -- Operating system -- Full error traceback -- Steps to reproduce -- Minimal code example diff --git a/docs-tech/index.md b/docs-tech/index.md deleted file mode 100644 index beb079f6..00000000 --- a/docs-tech/index.md +++ /dev/null @@ -1,333 +0,0 @@ -# XCore Framework - -**XCore** est un framework Python de niveau production, conçu pour construire des applications modulaires, extensibles et sécurisées. Basé sur **FastAPI** et **asyncio**, il suit une architecture **Modular Monolith** où chaque fonctionnalité est encapsulée dans des plugins isolés. - ---- - -
- -- :material-rocket-launch:{ .lg .middle } **Haute Performance** - - Construit sur FastAPI et asyncio pour un débit maximal avec un overhead minimal. - -- :material-shield-lock:{ .lg .middle } **Sécurité par Design** - - Sandbox multi-couche, validation AST, et moteur de permissions granulaire. - -- :material-puzzle-outline:{ .lg .middle } **Architecture Plugin-First** - - Tout est un plugin. Chargement, déchargement et rechargement à chaud sans downtime. - -- :material-database:{ .lg .middle } **Services Intégrés** - - Bases SQL/NoSQL, cache Redis, et scheduling de tâches inclus. - -- :material-lightning-bolt:{ .lg .middle } **Événements & Hooks** - - Système de messagerie asynchrone pour un couplage lâche entre composants. - -- :material-scale-balance:{ .lg .middle } **Isolation Garantie** - - Plugins trusted (processus principal) ou sandboxed (processus OS isolé). - -
- ---- - -## Pourquoi XCore ? - -Les applications modernes oscillent entre deux extrêmes : le **monolithe rigide** difficile à maintenir et les **microservices complexes** à orchestrer. XCore propose une troisième voie : le **Monolithe Modulaire**. - -```mermaid -flowchart LR - subgraph Traduction["Spectre des Architectures"] - direction LR - M[Monolithe
Rigide] <--> MM[**Monolithe Modulaire**
XCore] <--> MS[Microservices
Complexes] - end - - style MM fill:#4CAF50,color:#fff,stroke:#2E7D32 - style M fill:#FFC107,color:#000 - style MS fill:#FFC107,color:#000 -``` - -### Les Avantages du Modular Monolith - -| Aspect | Monolithe Classique | XCore (Modular Monolith) | Microservices | -| :--- | :--- | :--- | :--- | -| **Déploiement** | Unique, risqué | Unique, plugins isolés | Multiple, complexe | -| **Isolation** | Aucune | Sandbox par plugin | Processus/network | -| **Scalabilité** | Verticale | Plugins scalables | Horizontale | -| **Complexité** | Faible | Moyenne | Élevée | -| **Temps de dev** | Rapide | Rapide | Lent | - ---- - -## Architecture en un Coup d'Œil - -```mermaid -flowchart TB - subgraph App["Application FastAPI"] - FA[Routes HTTP] - end - - subgraph Kernel["XCore Kernel (Orchestrateur)"] - direction TB - X[Xcore Engine
Point d'entrée] - SC[Service Container
DB, Cache, Scheduler] - PS[Plugin Supervisor
Cycle de vie] - EB[Event Bus
Messagerie async] - PE[Permission Engine
Contrôle d'accès] - end - - subgraph Plugins["Écosystème Plugins"] - direction LR - TP[Plugins Trusted
Processus principal] - SP[Plugins Sandboxed
Processus isolé] - end - - subgraph Services["Services Partagés"] - DB[(Base de données
SQL/NoSQL)] - CACHE[(Cache
Redis/Memory)] - SCHED[Scheduler
APScheduler] - end - - FA --> X - X --> SC - X --> PS - X --> EB - X --> PE - - SC --> Services - PS --> TP - PS --> SP - - EB -.->|Événements| TP - EB -.->|Événements| SP - PE -.->|Permissions| PS - - style Kernel fill:#E3F2FD,stroke:#1976D2 - style Plugins fill:#FFF3E0,stroke:#F57C00 - style Services fill:#E8F5E9,stroke:#388E3C -``` - ---- - -## Comment XCore Fonctionne - -### 1. Initialisation (Boot Sequence) - -```mermaid -sequenceDiagram - participant Dev as Développeur - participant App as FastAPI - participant Kernel as XCore Engine - participant Services as Services - participant Loader as PluginLoader - participant P as Plugins - - Dev->>App: Lance l'application - App->>Kernel: boot(app) - - Note over Kernel: 1. Configuration - Kernel->>Kernel: Charge xcore.yaml + .env - - Note over Kernel: 2. Services - Kernel->>Services: Initialise DB, Cache, Scheduler - Services-->>Kernel: Services prêts - - Note over Kernel: 3. Plugins - Kernel->>Loader: Découvre les plugins - Loader->>Loader: Build DAG de dépendances - Loader->>Loader: Tri topologique - - loop Chargement par vagues - Loader->>P: Load (Trusted/Sandboxed) - P-->>Loader: Ready - end - - Kernel->>App: Monte les routes plugins - App-->>Dev: Serveur prêt ! -``` - -### 2. Cycle de Vie d'un Plugin - -```mermaid -stateDiagram-v2 - [*] --> UNLOADED: Plugin découvert - - UNLOADED --> LOADING: supervisor.load() - LOADING --> READY: on_load() ✅ - LOADING --> FAILED: Erreur ❌ - - READY --> RELOADING: supervisor.reload() - RELOADING --> READY: Mise à jour ✅ - RELOADING --> FAILED: Échec ❌ - - READY --> UNLOADED: supervisor.unload() - FAILED --> UNLOADED: Cleanup - FAILED --> RELOADING: Retry - - note right of READY - Plugin opérationnel - Routes montées - Événements abonnés - end note - - note right of FAILED - Erreur de syntaxe - Dépendance manquante - Permission refusée - end note -``` - -### 3. Communication Inter-Plugins - -Quand le **Plugin A** appelle le **Plugin B**, l'appel transite par le **Supervisor** qui applique un pipeline de sécurité : - -```mermaid -flowchart LR - subgraph PluginA["Plugin A (Appelant)"] - A[Code: ctx.plugins.call] - end - - subgraph Supervisor["Pipeline Supervisor"] - direction TB - M1[📍 Tracing
Span] - M2[🛑 Rate Limit
Quota] - M3[🔐 Permissions
Audit] - M4[🔄 Retry
Logic] - end - - subgraph PluginB["Plugin B (Cible)"] - B[Handler @action] - end - - A -- "call()" --> M1 - M1 --> M2 - M2 --> M3 - M3 --> M4 - M4 -- "exécute" --> B - B -- "résultat" --> A - - style Supervisor fill:#FFEBEE,stroke:#C62828 - style M3 fill:#C62828,color:#fff -``` - ---- - -## Démarrage Rapide - -### Étape 1 : Installation - -```bash -# Avec Poetry (recommandé) -poetry add xcore-framework - -# Ou avec pip -pip install xcore-framework -``` - -### Étape 2 : Configuration - -Créez un fichier `xcore.yaml` : - -```yaml -app: - name: "Mon Application" - debug: true - -services: - db: - backend: "sqlite" - url: "sqlite+aiosqlite:///./app.db" - - cache: - backend: "memory" - ttl: 300 -``` - -### Étape 3 : Point d'Entrée - -```python -from fastapi import FastAPI -from xcore import Xcore - -app = FastAPI(title="Mon App XCore") -core = Xcore(config_path="xcore.yaml") - -@app.on_event("startup") -async def startup(): - await core.boot(app) # 🚀 Boot du kernel - -@app.on_event("shutdown") -async def shutdown(): - await core.shutdown() # 🛑 Cleanup -``` - -### Étape 4 : Premier Plugin - -``` -plugins/hello/ -├── plugin.yaml -└── src/ - └── main.py -``` - -**plugin.yaml** : -```yaml -name: hello -version: "1.0.0" -execution_mode: trusted -entry_point: src/main.py -``` - -**src/main.py** : -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok - -class Plugin(AutoDispatchMixin, TrustedBase): - - @action("greet") - async def greet(self, payload: dict): - name = payload.get("name", "Monde") - return ok(message=f"Bonjour, {name} !") -``` - -### Étape 5 : Tester - -```bash -# Via CLI -xcore plugin call hello greet '{"name": "Développeur"}' - -# Résultat : {"status": "ok", "message": "Bonjour, Développeur !"} -``` - ---- - -## Parcours de Documentation - -| 📚 Section | Description | -| :--- | :--- | -| [🚀 Installation](getting-started/installation.md) | Configurer l'environnement de développement | -| [⚡ Quick Start](getting-started/quickstart.md) | Créer votre premier plugin en 5 minutes | -| [🧩 Créer un Plugin](guides/creating-plugins.md) | Guide complet du développement de plugins | -| [🏗️ Architecture](architecture/overview.md) | Comprendre les internals du framework | -| [🛡️ Sécurité](guides/security.md) | Sandbox, permissions et isolation | -| [📡 Événements](guides/events.md) | Système de messagerie et hooks | -| [🔧 Services](guides/services.md) | Utiliser DB, Cache et Scheduler | -| [📖 SDK Reference](reference/sdk.md) | API complète du SDK | - ---- - -## Ressources Supplémentaires - -- [Exemples de code](examples/README.md) - Plugins de base à avancés -- [CLI Reference](reference/cli.md) - Commandes de gestion -- [Troubleshooting](guides/troubleshooting.md) - Résolution de problèmes -- [Contributing](development/contributing.md) - Guide de contribution - ---- - -

- XCore Framework — Construit avec ❤️ par l'équipe XCore -

diff --git a/docs-tech/reference/api.md b/docs-tech/reference/api.md deleted file mode 100644 index 03fec0e3..00000000 --- a/docs-tech/reference/api.md +++ /dev/null @@ -1,52 +0,0 @@ -# Kernel API Reference - -This document covers the internal APIs of the XCore kernel, available to `Trusted` plugins via `self.ctx`. - -## 1. `PluginSupervisor` - -Manages the lifecycle and execution of plugins. - -- **`async call(plugin_name: str, action: str, payload: dict, *, resource: str = None) -> dict`**: Executes a cross-plugin call with full middleware support (Security, RateLimit, Tracing). -- **`async load(plugin_name: str) -> None`**: Dynamically loads a plugin from disk. -- **`async reload(plugin_name: str) -> None`**: Performs a hot-reload of a plugin. -- **`async unload(plugin_name: str) -> None`**: Gracefully stops and removes a plugin. -- **`status() -> dict`**: Returns the health and state of all managed plugins. - ---- - -## 2. `ServiceContainer` - -Registry for shared resources and infrastructure. - -- **`get(name: str) -> Any`**: Retrieves a registered service (e.g., `db`, `cache`). -- **`has(name: str) -> bool`**: Checks if a service is registered. -- **`register_service(name: str, service: Any) -> None`**: Manually registers a new service instance. -- **`async health() -> dict`**: Aggregates health status from all services. - ---- - -## 3. `EventBus` - -Asynchronous event distribution. - -- **`async emit(event: str, data: dict = None, source: str = None)`**: Dispatches an event to all subscribers. -- **`on(event: str, priority: int = 50)`**: Decorator to subscribe to an event. -- **`once(event: str)`**: Subscribes for the next occurrence only. - ---- - -## 4. `HookManager` - -Synchronous filters and actions for fine-grained extensibility. - -- **`apply_filters(hook_name: str, value: Any, **kwargs) -> Any`**: Passes a value through a chain of filter functions. -- **`do_action(hook_name: str, **kwargs)`**: Executes a chain of side-effect actions. - ---- - -## 5. `PermissionEngine` - -Evaluates security policies. - -- **`check(plugin: str, resource: str, action: str) -> None`**: Validates access and raises `PermissionDenied` if unauthorized. -- **`allows(plugin: str, resource: str, action: str) -> bool`**: Returns boolean status. diff --git a/docs-tech/reference/cli.md b/docs-tech/reference/cli.md deleted file mode 100644 index 6ee0dea7..00000000 --- a/docs-tech/reference/cli.md +++ /dev/null @@ -1,111 +0,0 @@ -# CLI Reference - -The `xcore` CLI is the primary tool for managing plugins, sandboxes, and the marketplace from the terminal. - -## General Usage - -```bash -PYTHONPATH=. python -m xcore.cli.main [COMMAND] [OPTIONS] -``` -*(If installed via pip/poetry, you can simply use `xcore [COMMAND]`)* - ---- - -## 1. Plugin Management (`plugin`) - -Commands for interacting with the framework's plugins. - -### `list` -Lists all discovered plugins and their current status. -```bash -xcore plugin list -``` - -### `load` / `unload` -Manually load or unload a plugin. -```bash -xcore plugin load my_plugin -xcore plugin unload my_plugin -``` - -### `reload` -Hot-reloads a plugin's code and manifest. -```bash -xcore plugin reload my_plugin -``` - -### `call` -Invokes an IPC action on a running plugin. -```bash -xcore plugin call my_plugin my_action '{"key": "value"}' -``` - -### `sign` -Generates a `plugin.sig` HMAC signature for a plugin directory. -```bash -xcore plugin sign ./plugins/my_plugin --key YOUR_SECRET_KEY -``` - ---- - -## 2. Sandbox Management (`sandbox`) - -Tools for inspecting and testing sandboxed plugins. - -### `limits` -Displays the resource limits and current consumption for a sandboxed plugin. -```bash -xcore sandbox limits my_plugin -``` - -### `network` -Audits and displays the network access policy for a sandbox. -```bash -xcore sandbox network my_plugin -``` - -### `fs` -Displays the allowed and denied filesystem paths for a sandbox. -```bash -xcore sandbox fs my_plugin -``` - ---- - -## 3. Marketplace (`marketplace`) - -Commands for interacting with the official XCore Marketplace. - -### `search` -Search for plugins in the marketplace. -```bash -xcore marketplace search "auth" -``` - -### `info` -Get detailed metadata for a specific marketplace plugin. -```bash -xcore marketplace info auth-provider -``` - -### `install` -Downloads and installs a plugin from the marketplace. -```bash -xcore marketplace install auth-provider -``` - ---- - -## 4. Validation & Utility - -### `validate` -Checks a plugin's `plugin.yaml` and `src/` for syntax errors and security violations. -```bash -xcore validate ./plugins/my_plugin -``` - -### `health` -Displays the health status of the kernel and all registered services. -```bash -xcore health -``` diff --git a/docs-tech/reference/configuration.md b/docs-tech/reference/configuration.md deleted file mode 100644 index de9149ed..00000000 --- a/docs-tech/reference/configuration.md +++ /dev/null @@ -1,109 +0,0 @@ -# Exhaustive Configuration Reference - -This reference covers every configuration key available in XCore's system and plugin manifests. - ---- - -## 1. System Configuration (`xcore.yaml`) - -The root configuration file controls the kernel and shared services. It supports environment variable injection using `${VARIABLE_NAME}`. - -### `app` Section: Global Identity -| Key | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `name` | `str` | `xcore-app` | Logical name for the application. | -| `env` | `str` | `development` | Environment mode: `development`, `staging`, `production`. | -| `debug` | `bool` | `false` | Enables verbose logging and detailed error responses. | -| `secret_key` | `str` | - | Primary key for cryptographic operations. | -| `server_key` | `str` | - | High-entropy key for core-to-plugin authentication. | -| `plugin_prefix` | `str` | `/plugin` | Base URL prefix for all plugin-exposed HTTP routes. | - -### `plugins` Section: Lifecycle & Discovery -| Key | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `directory` | `str` | `./plugins` | Filesystem path to search for plugins. | -| `autoload` | `bool` | `true` | Automatically load all valid plugins found in the directory. | -| `strict_trusted` | `bool` | `true` | In production, `trusted` plugins MUST have a valid `plugin.sig`. | -| `interval` | `int` | `2` | Polling interval (seconds) for the hot-reload watcher. | -| `entry_point` | `str` | `src/main.py` | Default entry point file for all plugins. | - -### `services` Section: Shared Infrastructure - -#### `database` -| Key | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `enabled` | `bool` | `true` | Enable/disable the global database manager. | -| `databases` | `dict` | `{}` | Map of named database configurations (see below). | - -**Individual Database Entry:** -- `type`: `sqlite`, `postgresql`, `mysql`, `mongodb`. -- `url`: Connection string (e.g., `postgresql+asyncpg://...`). -- `pool_size`: Number of persistent connections (SQL only). -- `max_overflow`: Extra connections allowed during peak load (SQL only). -- `echo`: Log all SQL queries to the console. - -#### `cache` -| Key | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `backend` | `str` | `memory` | `memory` or `redis`. | -| `url` | `str` | - | Redis URL (required if backend is `redis`). | -| `ttl` | `int` | `300` | Default expiration time in seconds. | -| `max_size` | `int` | `1000` | Maximum number of items for `memory` backend. | - -#### `scheduler` -| Key | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `enabled` | `bool` | `true` | Enable/disable the APScheduler integration. | -| `backend` | `str` | `memory` | `memory` or `redis` (for persistent jobs). | -| `timezone` | `str` | `UTC` | Timezone for cron triggers. | - -### `observability` Section: Monitoring -- **`logging`**: `level` (DEBUG/INFO/WARN/ERROR), `format`, `file`, `max_bytes`, `backup_count`. -- **`metrics`**: `enabled` (bool), `backend` (`prometheus`/`statsd`), `prefix`. -- **`tracing`**: `enabled` (bool), `backend` (`opentelemetry`/`jaeger`), `endpoint`. - ---- - -## 2. Plugin Manifest (`plugin.yaml`) - -Each plugin must have a `plugin.yaml` in its root directory. - -### Identity -- `name` (str): Unique plugin ID. -- `version` (str): Semantic version. -- `execution_mode` (str): `trusted` (in-process) or `sandboxed` (isolated process). -- `entry_point` (str): Relative path to the `.py` file containing the `Plugin` class. - -### Dependencies & Imports -- `requires` (list): List of plugins needed before this one can start. -- `allowed_imports` (list): Modules allowed in `sandboxed` mode (e.g., `math`, `json`, `pydantic`). - -### Security (Permissions) -A list of policy rules: -```yaml -permissions: - - resource: "db.users.*" # Pattern matching - actions: ["read", "write"] - effect: allow # allow | deny -``` - -### Resource Management (`resources`) -- `timeout_seconds` (int): Max time an IPC call can take. -- `max_memory_mb` (int): RAM limit for sandboxed processes. -- `max_disk_mb` (int): Disk limit for sandboxed processes. -- `rate_limit`: - - `calls` (int): Max calls allowed in the period. - - `period_seconds` (int): Rolling window duration. - -### Runtime Behavior (`runtime`) -- `health_check`: - - `enabled` (bool): Enable automatic health polling. - - `interval_seconds` (int): Frequency. -- `retry`: - - `max_attempts` (int): Retries on IPC failure. - - `backoff_seconds` (float): Delay between retries. - -### Filesystem & Env -- `filesystem.allowed_paths` (list): Paths relative to plugin root. -- `env` (dict): Environment variables passed to the plugin. -- `envconfiguration.inject` (bool): Automatically load a local `.env` file. diff --git a/docs-tech/reference/sdk.md b/docs-tech/reference/sdk.md deleted file mode 100644 index 7d478da3..00000000 --- a/docs-tech/reference/sdk.md +++ /dev/null @@ -1,736 +0,0 @@ -# XCore SDK Reference - -Le SDK XCore est une bibliothèque haut niveau conçue pour faciliter le développement de plugins sécurisés, scalables et maintenables. - ---- - -## Vue d'Ensemble du SDK - -```mermaid -flowchart TB - subgraph Core["🎯 Classes Core"] - TB[TrustedBase
Base plugins trusted] - BP[BasePlugin
Interface low-level] - SB[SandboxedBase
Base plugins sandboxed] - end - - subgraph Mixins["🔧 Mixins"] - ADM[AutoDispatchMixin
Routage @action] - RP[RoutedPlugin
Routes HTTP] - VM[ValidateMixin
Validation Pydantic] - end - - subgraph Decorators["📝 Décorateurs"] - D1[@action] - D2[@route] - D3[@validate_payload] - D4[@on / @filter] - end - - subgraph Helpers["🛠️ Helpers"] - H1[ok / error] - H2[Event] - H3[Response types] - end - - Core --> Mixins - Mixins --> Decorators - Decorators --> Helpers - - style Core fill:#E3F2FD,stroke:#1976D2 - style Mixins fill:#FFF3E0,stroke:#F57C00 - style Decorators fill:#E8F5E9,stroke:#388E3C - style Helpers fill:#F3E5F5,stroke:#7B1FA2 -``` - ---- - -## 1. Classes Core - -### `TrustedBase` - -Classe de base principale pour les plugins qui s'exécutent dans le **processus principal** XCore. - -```mermaid -classDiagram - class TrustedBase { - #ctx: PluginContext - #logger: Logger - +name: str - +version: str - +async on_load() None - +async on_unload() None - +async on_reload() None - +async handle(action, payload) dict - +get_service(name) Any - +call_plugin(name, action, payload) Future - } - - class AutoDispatchMixin { - +async handle(action, payload) dict - #_find_action_handler(action) Method - } - - class RoutedPlugin { - #_routes: List[Route] - #discover_routes() None - #get_routes() List[Route] - } - - TrustedBase <|-- AutoDispatchMixin - TrustedBase <|-- RoutedPlugin - - note for TrustedBase "Base class pour plugins trusted" - note for AutoDispatchMixin "Routage automatique via @action" - note for RoutedPlugin "Support des routes HTTP" -``` - -#### Cycle de Vie - -```mermaid -sequenceDiagram - participant S as Supervisor - participant P as Plugin Instance - participant L as on_load - participant U as on_unload - - S->>P: Instancier - S->>L: on_load() - Note over L: Initialiser services
Subscrire événements - L-->>S: ✅ Ready - - Note over P: Plugin opérationnel... - - S->>U: on_unload() - Note over U: Cleanup ressources
Unsubscribe événements - U-->>S: ✅ Unloaded -``` - -#### Implémentation Complète - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok, error -from pydantic import BaseModel, Field -from typing import Optional - -class GreetInput(BaseModel): - """Schema de validation pour l'action greet.""" - name: str = Field(min_length=1, max_length=100, description="Nom à saluer") - language: str = Field(default="fr", description="Langue") - -class Plugin(AutoDispatchMixin, TrustedBase): - """ - Plugin de démonstration montrant toutes les fonctionnalités de TrustedBase. - """ - - async def on_load(self) -> None: - """ - 🎯 Point d'entrée au chargement du plugin. - - Initialiser ici : - - Les services (db, cache, scheduler) - - Les subscriptions d'événements - - L'état interne - """ - self.logger.info(f"🚀 Chargement de {self.name} v{self.version}") - - # Initialiser les services - self.db = self.get_service("db") - self.cache = self.get_service("cache") - self.scheduler = self.get_service("scheduler") - - # S'abonner à un événement - self.ctx.events.on("user.created", self._on_user_created, priority=50) - - self.logger.info("✅ Plugin prêt") - - async def on_unload(self) -> None: - """ - 🧹 Point de cleanup au déchargement. - - Nettoyer ici : - - Connexions ouvertes - - Subscriptions événements - - Tâches scheduler - """ - self.logger.info(f"👋 Déchargement de {self.name}") - # Cleanup si nécessaire - - async def on_reload(self) -> None: - """ - 🔄 Appelé lors d'un hot-reload. - - Re-initialiser l'état sans perdre les connexions. - """ - self.logger.info(f"🔄 Rechargement de {self.name}") - await self.on_load() # Ré-initialiser - - @action("greet") - async def greet(self, payload: dict) -> dict: - """ - 🎯 Action: Saluer un utilisateur. - - Args: - payload: {"name": "Votre nom", "language": "fr"} - - Returns: - {"status": "ok", "message": "Bonjour, X !"} - """ - # Valider l'input - try: - input_data = GreetInput(**payload) - except ValidationError as e: - return error(msg=str(e), code="invalid_input", status_code=400) - - # Logique métier - greetings = { - "fr": "Bonjour", - "en": "Hello", - "es": "Hola" - } - greeting = greetings.get(input_data.language, "Hello") - message = f"{greeting}, {input_data.name} !" - - self.logger.info(f"Salutation: {message}") - return ok(message=message) - - @on("user.created") - async def _on_user_created(self, event: Event) -> None: - """Handler pour l'événement user.created.""" - user_email = event.data.get("email") - self.logger.info(f"Nouvel utilisateur: {user_email}") -``` - ---- - -### `BasePlugin` - -Interface low-level pour tous les types de plugins. Utilisé pour la logique interne du framework ou pour construire des runners personnalisés. - -```python -from xcore.sdk import BasePlugin - -class CustomPluginRunner(BasePlugin): - """ - Implémentation personnalisée d'un runner de plugin. - """ - - async def initialize(self, config: dict) -> None: - """Initialisation personnalisée.""" - pass - - async def execute(self, action: str, payload: dict) -> dict: - """Exécution personnalisée.""" - return {"status": "ok", "result": "custom"} - - async def cleanup(self) -> None: - """Cleanup personnalisé.""" - pass -``` - ---- - -## 2. Décorateurs - -### `@action(name: str)` - -Définit une méthode comme handler d'action IPC. Nécessite `AutoDispatchMixin`. - -```mermaid -flowchart LR - A[Appel IPC
action="greet"] --> B[AutoDispatchMixin.handle] - B --> C{Rechercher
@action("greet")} - C -->|Trouvé| D[Exécuter handler] - C -->|Non trouvé| E[❌ Error 404] - D --> F[Retourner résultat] - - style B fill:#E3F2FD,stroke:#1976D2 - style D fill:#E8F5E9,stroke:#388E3C - style E fill:#FFEBEE,stroke:#C62828 -``` - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok - -class Plugin(AutoDispatchMixin, TrustedBase): - - @action("ping") - async def ping(self, payload: dict) -> dict: - """Health check simple.""" - return ok(status="pong", timestamp=self._now()) - - @action("add") - async def add(self, payload: dict) -> dict: - """Additionne deux nombres.""" - a = payload.get("a", 0) - b = payload.get("b", 0) - return ok(result=a + b) - - @action("complex_operation") - async def complex_op(self, payload: dict) -> dict: - """Opération avec validation et services.""" - # Accéder au service - db = self.get_service("db") - - # Vérifier le cache - cache_key = f"op:{payload.get('id')}" - cached = await self.cache.get(cache_key) - if cached: - return ok(result=cached, source="cache") - - # Exécuter l'opération - result = await self._do_operation(payload) - - # Caster le résultat - await self.cache.set(cache_key, result, ttl=300) - - return ok(result=result, source="computed") -``` - ---- - -### `@route(path: str, method: str, ...)` - -Définit une route FastAPI directement sur la classe du plugin. Nécessite le mixin `RoutedPlugin`. - -```mermaid -flowchart TB - subgraph Plugin["🧩 Plugin"] - R1[@route("/users", GET)] - R2[@route("/users/{id}", GET)] - R3[@route("/users", POST)] - end - - subgraph Mount["📡 FastAPI Mount"] - M1[/plugin/my_plugin/users] - M2[/plugin/my_plugin/users/{id}] - end - - R1 --> M1 - R2 --> M2 - R3 --> M1 - - style Plugin fill:#FFF3E0,stroke:#F57C00 - style Mount fill:#E3F2FD,stroke:#1976D2 -``` - -#### Paramètres du Décorateur - -| Paramètre | Type | Défaut | Description | -| :--- | :--- | :--- | :--- | -| `path` | str | Requis | Chemin de la route | -| `method` | str | "GET" | Méthode HTTP | -| `tags` | list | None | Tags pour OpenAPI | -| `summary` | str | None | Résumé pour docs | -| `status_code` | int | 200 | Code de statut par défaut | -| `dependencies` | list | None | Dépendances FastAPI | -| `permissions` | list | None | Permissions requises | - -#### Exemple Complet - -```python -from xcore.sdk import TrustedBase, RoutedPlugin, route, ok, error -from fastapi import Depends, HTTPException -from pydantic import BaseModel, EmailStr - -class UserCreate(BaseModel): - email: EmailStr - name: str - password: str - -class UserResponse(BaseModel): - id: int - email: str - name: str - -class Plugin(RoutedPlugin, TrustedBase): - """Plugin avec routes HTTP complètes.""" - - @route("/users", method="GET", tags=["users"], summary="Lister les utilisateurs") - async def list_users(self, limit: int = 100, offset: int = 0) -> dict: - """ - Liste tous les utilisateurs avec pagination. - - - **limit**: Nombre maximum d'utilisateurs - - **offset**: Décalage pour la pagination - """ - db = self.get_service("db") - async with db.session() as session: - query = f"SELECT * FROM users LIMIT {limit} OFFSET {offset}" - result = await session.execute(query) - users = result.fetchall() - - return ok(users=[dict(u) for u in users], total=len(users)) - - @route("/users/{user_id}", method="GET", tags=["users"]) - async def get_user(self, user_id: int) -> dict: - """Récupérer un utilisateur par ID.""" - db = self.get_service("db") - async with db.session() as session: - query = "SELECT * FROM users WHERE id = :id" - result = await session.execute(query, {"id": user_id}) - user = result.fetchone() - - if not user: - return error(msg="Utilisateur non trouvé", code="not_found", status_code=404) - - return ok(user=dict(user)) - - @route("/users", method="POST", tags=["users"], status_code=201) - async def create_user(self, data: UserCreate) -> dict: - """Créer un nouvel utilisateur.""" - db = self.get_service("db") - - async with db.session() as session: - query = "INSERT INTO users (email, name) VALUES (:email, :name) RETURNING *" - result = await session.execute(query, dict(data)) - user = result.fetchone() - await session.commit() - - # Émettre un événement - await self.ctx.events.emit("user.created", { - "id": user.id, - "email": user.email - }) - - return ok(user=dict(user)) - - @route("/health", method="GET", tags=["system"]) - async def health_check(self) -> dict: - """Endpoint de santé du plugin.""" - return ok( - plugin=self.name, - version=self.version, - status="healthy" - ) -``` - ---- - -### `@validate_payload(schema: Type[BaseModel], type_response="dict")` - -Valide automatiquement le payload IPC contre un modèle Pydantic. - -```mermaid -flowchart TB - A[Payload Reçu] --> B[@validate_payload] - B --> C{Validation} - C -->|✅ Valide| D[Handler exécuté] - C -->|❌ Invalide| E[Erreur 400] - - D --> F{type_response ?} - F -->|dict| G[Dict validé] - F -->|pydantic| H[Instance Pydantic] - - style B fill:#E3F2FD,stroke:#1976D2 - style E fill:#FFEBEE,stroke:#C62828 -``` - -#### Modes de Retour - -```python -from xcore.sdk import TrustedBase, AutoDispatchMixin, action, ok -from xcore.sdk.decorators import validate_payload -from pydantic import BaseModel - -class CreateUserInput(BaseModel): - email: str - name: str - age: int = Field(ge=0, le=150) - -class Plugin(AutoDispatchMixin, TrustedBase): - - # Mode 1: Retourne un dict validé - @action("create_user_dict") - @validate_payload(CreateUserInput, type_response="dict") - async def create_user_dict(self, payload: dict) -> dict: - # payload est un dict validé - email = payload["email"] - name = payload["name"] - return ok(email=email, name=name) - - # Mode 2: Retourne l'instance Pydantic - @action("create_user_model") - @validate_payload(CreateUserInput, type_response="pydantic") - async def create_user_model(self, payload: CreateUserInput) -> dict: - # payload est une instance CreateUserInput - email = payload.email - name = payload.name - age = payload.age - return ok(email=email, name=name, age=age) -``` - ---- - -### `@trusted` / `@sandboxed` - -Markers pour s'assurer qu'une méthode s'exécute uniquement dans le mode spécifié. - -```python -from xcore.sdk import TrustedBase, trusted, sandboxed, action - -class Plugin(TrustedBase): - - @action("trusted_only") - @trusted - async def trusted_operation(self, payload: dict) -> dict: - """Ne s'exécute que en mode trusted.""" - return ok(result="trusted") - - @action("sandboxed_only") - @sandboxed - async def sandboxed_operation(self, payload: dict) -> dict: - """Ne s'exécute que en mode sandboxed.""" - return ok(result="sandboxed") -``` - ---- - -### `@on(event_name: str, priority: int = 50)` - -Subscribe un handler à un événement. - -```python -from xcore.sdk import TrustedBase, on, Event - -class Plugin(TrustedBase): - - @on("user.created", priority=100) - async def handle_user_created(self, event: Event): - """Handler avec haute priorité.""" - pass - - @on("order.completed", priority=50) - async def handle_order_completed(self, event: Event): - """Handler avec priorité normale.""" - pass -``` - ---- - -### `@filter(hook_name: str)` et `@action_hook(hook_name: str)` - -Enregistre un hook filter ou action. - -```python -from xcore.sdk import TrustedBase, filter, action_hook - -class Plugin(TrustedBase): - - @filter("page_title") - def modify_title(self, title: str) -> str: - """Filter: modifie le titre de page.""" - return f"{title} | Mon App" - - @action_hook("before_render") - def before_render_log(self, template: str): - """Action hook: log avant rendu.""" - self.logger.info(f"Rendu: {template}") -``` - ---- - -## 3. Mixins - -### `AutoDispatchMixin` - -Fournit une implémentation automatique de `handle()` en routant vers les méthodes décorées avec `@action`. - -```mermaid -flowchart TB - A[IPC: handle("greet", payload)] --> B[AutoDispatchMixin.handle] - B --> C[Rechercher @action("greet")] - C --> D{Trouvé ?} - D -->|Oui| E[Exécuter greet(payload)] - D -->|Non| F[Erreur: Action inconnue] - E --> G[Retourner résultat] - F --> H[Retourner error()] - - style B fill:#E3F2FD,stroke:#1976D2 - style E fill:#E8F5E9,stroke:#388E3C - style F fill:#FFEBEE,stroke:#C62828 -``` - -```python -# Sans AutoDispatchMixin (verbeux) -class Plugin(TrustedBase): - async def handle(self, action: str, payload: dict) -> dict: - if action == "ping": - return await self.ping(payload) - elif action == "greet": - return await self.greet(payload) - else: - return error(f"Action inconnue: {action}", code="not_found") - -# Avec AutoDispatchMixin (concis) -class Plugin(AutoDispatchMixin, TrustedBase): - - @action("ping") - async def ping(self, payload: dict) -> dict: - return ok(status="pong") - - @action("greet") - async def greet(self, payload: dict) -> dict: - return ok(message=f"Hello !") -``` - ---- - -### `RoutedPlugin` - -Active la découverte automatique et le montage des routes FastAPI. - -```python -from xcore.sdk import TrustedBase, RoutedPlugin, route - -class Plugin(RoutedPlugin, TrustedBase): - """Les routes sont automatiquement montées sur /plugin/{name}/...""" - - @route("/hello", methods=["GET"]) - async def hello(self): - return {"message": "Hello"} - - @route("/hello/{name}", methods=["GET"]) - async def hello_name(self, name: str): - return {"message": f"Hello, {name}!"} -``` - -**Routes montées :** -- `GET /plugin/my_plugin/hello` -- `GET /plugin/my_plugin/hello/{name}` - ---- - -## 4. Helpers - -### `ok(**kwargs) -> dict` - -Génère une réponse de succès standardisée. - -```python -from xcore.sdk import ok - -# Simple -ok(message="Succès") -# → {"status": "ok", "message": "Succès"} - -# Multiple -ok(user_id=123, email="test@example.com") -# → {"status": "ok", "user_id": 123, "email": "test@example.com"} - -# Avec données complexes -ok(users=[...], total=100, page=1) -# → {"status": "ok", "users": [...], "total": 100, "page": 1} -``` - -### `error(msg: str, code: str, status_code: int = 400) -> dict` - -Génère une réponse d'erreur standardisée. - -```python -from xcore.sdk import error - -# Erreur de validation -error(msg="Champ requis manquant", code="validation_error", status_code=400) -# → {"status": "error", "msg": "Champ requis manquant", "code": "validation_error"} - -# Erreur not found -error(msg="Ressource non trouvée", code="not_found", status_code=404) -# → {"status": "error", "msg": "Ressource non trouvée", "code": "not_found"} - -# Erreur serveur -error(msg="Erreur interne", code="internal_error", status_code=500) -``` - ---- - -## 5. Repositories & Data Access - -### `BaseAsyncRepository` - -Classe de base pour les interactions asynchrones avec la base de données. - -```python -from xcore.repositories import BaseAsyncRepository -from typing import TypeVar, Generic, List, Optional -from pydantic import BaseModel - -class User(BaseModel): - id: int - email: str - name: str - -class UserRepository(BaseAsyncRepository): - """Repository pour les opérations sur les utilisateurs.""" - - async def get_by_email(self, email: str) -> Optional[User]: - """Récupérer un utilisateur par email.""" - query = "SELECT * FROM users WHERE email = :email" - result = await self.session.execute(query, {"email": email}) - row = result.fetchone() - return User(**dict(row)) if row else None - - async def list_all(self, limit: int = 100) -> List[User]: - """Lister tous les utilisateurs.""" - query = "SELECT * FROM users LIMIT :limit" - result = await self.session.execute(query, {"limit": limit}) - return [User(**dict(row)) for row in result.fetchall()] - - async def create(self, data: dict) -> User: - """Créer un nouvel utilisateur.""" - query = "INSERT INTO users (email, name) VALUES (:email, :name) RETURNING *" - result = await self.session.execute(query, data) - row = result.fetchone() - await self.session.commit() - return User(**dict(row)) -``` - ---- - -## 6. RBAC Checker - -Dépendance FastAPI pour appliquer le contrôle d'accès basé sur les rôles. - -```python -from fastapi import Depends -from xcore.security import RBACChecker, require_role - -checker = RBACChecker() - -@route("/admin/users", methods=["GET"], dependencies=[Depends(require_role("admin"))]) -async def list_users_admin(): - """Route protégée: nécessite le rôle admin.""" - pass - -@route("/profile", methods=["GET"], dependencies=[Depends(require_role("user"))]) -async def get_profile(): - """Route protégée: nécessite le rôle user.""" - pass -``` - ---- - -## 7. Tableau Récapitulatif - -| Composant | Usage | Exemple | -| :--- | :--- | :--- | -| `TrustedBase` | Base class plugins | `class Plugin(TrustedBase)` | -| `AutoDispatchMixin` | Routage @action | `class Plugin(AutoDispatchMixin, TrustedBase)` | -| `RoutedPlugin` | Routes HTTP | `@route("/path")` | -| `@action` | Handler IPC | `@action("greet")` | -| `@route` | Route HTTP | `@route("/users/{id}")` | -| `@validate_payload` | Validation Pydantic | `@validate_payload(UserSchema)` | -| `@on` | Subscription événement | `@on("user.created")` | -| `@filter` | Hook filter | `@filter("page_title")` | -| `ok()` | Réponse succès | `ok(message="OK")` | -| `error()` | Réponse erreur | `error(msg="Error", code="bad")` | - ---- - -## Prochaines Lectures - -| 📚 Guide | Objectif | -| :--- | :--- | -| [Creating Plugins](../guides/creating-plugins.md) | Tutoriel complet | -| [Security](../guides/security.md) | Bonnes pratiques sécurité | -| [Events](../guides/events.md) | Système d'événements | diff --git a/docs-tech/requirements.txt b/docs-tech/requirements.txt deleted file mode 100644 index a28673ed..00000000 --- a/docs-tech/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -mkdocs ==1.6.1 -mkdocs-material ==9.7.3 -mkdocs-minify-plugin ==0.8.0 -mkdocstrings ==1.0.3 diff --git a/docs-tech/scaling_analysis.md b/docs-tech/scaling_analysis.md deleted file mode 100644 index ebaf6973..00000000 --- a/docs-tech/scaling_analysis.md +++ /dev/null @@ -1,108 +0,0 @@ -# XCore Architectural Scaling & Audit Report - -## 1. Executive Summary -The XCore architecture is a robust "Modular Monolith" designed to manage dynamic plugins with strong isolation and dependency injection. While the current version (v2.0.0) successfully addresses several initial design flaws (e.g., namespace isolation), our audit identifies critical coupling points and anti-patterns that will hinder scalability and maintenance as the ecosystem grows. - -This report outlines our findings and provides a concrete roadmap for future refactorings. - ---- - -## 2. Layer Analysis & Audit - -### Core Kernel (`xcore.kernel`) -- **Shared State Contention:** The framework relies on a shared `dict[str, Any]` for service discovery. This lacks encapsulation and is prone to race conditions during concurrent plugin operations. -- **Manual Orchestration:** `PluginSupervisor` manually configures permissions, rate limits, and routing. This approach is not extensible; adding new cross-cutting concerns (e.g., logging, circuit breakers) requires modifying the core. - -### Plugins & Sandbox -- **Divergent Activation:** Trusted and Sandboxed plugins follow different activation paths, leading to code duplication in `PluginLoader`. -- **Fragile Isolation:** Trusted plugins use `sys.modules` manipulation for isolation, which can lead to memory leaks or stale state if sub-modules are not perfectly purged. - -### Services Layer (`xcore.services`) -- **Open/Closed Violation:** Core service providers are hardcoded in the `ServiceContainer`. Adding a new core service requires a framework-level change. - ---- - -## 3. Detected Anti-Patterns & Risks - -| Anti-Pattern | Location | Impact | Risk Level | -| :--- | :--- | :--- | :--- | -| **Hardcoded Providers** | `container.py` | Violates Open/Closed Principle | Medium | -| **Manual Middleware Config**| `supervisor.py` | Hinders extensibility | High | -| **Cryptic Naming** | `lifecycle.py` | Increases cognitive load (`mems`) | Low (Fixed) | -| **Direct State Mutation** | `LifecycleManager` | Risk of inconsistent system state | High | - -> **Note:** The cryptic `mems` method has been refactored to `propagate_services` as part of this audit to improve maintainability. - ---- - -## 4. Proposed Refactorings & Improvements - -### A. Unified & Scoped Service Registry -Merge `PluginRegistry` and `ServiceContainer` into a single, thread-safe registry that supports private vs. public scoping. - -```python -from dataclasses import dataclass, field -from typing import Any, Dict -import threading - -@dataclass -class ServiceEntry: - obj: Any - plugin: str - scope: str = "public" # "public", "private", or "protected" - metadata: Dict[str, Any] = field(default_factory=dict) - -class UnifiedServiceRegistry: - def __init__(self): - self._services: Dict[str, ServiceEntry] = {} - self._lock = threading.Lock() - - def register(self, name: str, service: Any, plugin: str, scope: str = "public"): - with self._lock: - if name in self._services and self._services[name].scope == "protected": - raise PermissionError(f"Cannot overwrite protected service: {name}") - self._services[name] = ServiceEntry(obj=service, plugin=plugin, scope=scope) - - def get(self, name: str, requester: str = None) -> Any: - entry = self._services.get(name) - if not entry: - raise KeyError(f"Service not found: {name}") - - if entry.scope == "private" and entry.plugin != requester: - raise PermissionError(f"Access to private service '{name}' denied to '{requester}'") - - return entry.obj -``` - -### B. Event-Driven Lifecycle Hooks -Transition from manual orchestration to a reactive lifecycle using the `EventBus`. This decouples the core from individual plugin configurations. - -```python -# In LifecycleManager.load() -async def load(self): - await self._do_load() - # Replace manual calls with events - await self._events.emit("plugin.activated", { - "name": self.manifest.name, - "mode": self.manifest.execution_mode, - "services": self._instance.get_exposed_services() - }) - -# In PluginSupervisor (or a new PluginLifecycleObserver) -async def on_plugin_activated(self, event): - # Reactive configuration of cross-cutting concerns - name = event["name"] - self._registry.register_all(name, event["services"]) - self._permissions.auto_load(name, self._loader.get(name).manifest) - self._rate_limiter.auto_register(name, self._loader.get(name).manifest) -``` - -### C. Standardized IPC for Sandboxed Services -Expose services from sandboxed plugins via a standardized RPC-over-IPC protocol. This would allow the core to treat sandboxed services as "remote objects," unifying the API for developers regardless of the plugin's execution mode. - ---- - -## 5. Final Recommendations -1. **Unify Service Discovery:** Implement the `UnifiedServiceRegistry` to centralize access control. -2. **Move to Reactive Lifecycle:** Use events to handle plugin startup and shutdown to improve extensibility. -3. **Enhance Sandbox Transparency:** Build a proxy layer so sandboxed services can be injected just like trusted ones. diff --git a/docs-tech/versions.md b/docs-tech/versions.md deleted file mode 100644 index 81d76651..00000000 --- a/docs-tech/versions.md +++ /dev/null @@ -1,394 +0,0 @@ -# XCore Versions - -This page lists the version history of the XCore framework, major changes, and update notes. - -## Current Version - -### XCore v2.0.0 (Stable) - -**Release Date**: March 2025 - -**Status**: ✅ Stable and Recommended - -Version 2.0 is a complete rewrite of the framework with a plugin-first architecture, a reinforced sandboxing system, and native service integration. - -#### Main Features - -- **🏗️ Plugin-First Architecture** — The core is minimal; all features are implemented as plugins. -- **🔒 Sandboxing System** — Isolated plugin execution with AST and IPC restrictions. -- **⚡ Native FastAPI Integration** — Plugins can expose their own HTTP routes seamlessly. -- **🔄 Hot Reloading** — Dynamic plugin reloading without server restarts. -- **📊 Built-in Observability** — Structured logging, Prometheus metrics, and OpenTelemetry tracing. -- **🗄️ Multi-Database Support** — Support for PostgreSQL, MySQL, SQLite, MongoDB, and Redis. -- **⏰ Integrated Scheduler** — APScheduler with Redis persistence. -- **🔐 Reinforced Security** — HMAC signatures, granular permissions, and rate limiting. - -#### Changes from v1.x - -| Feature | v1.x | v2.0 | -|---------|------|------| -| Architecture | Monolithic | Plugin-first | -| Isolation | Basic Process | AST Sandboxing + IPC | -| HTTP Routes | Limited | Full via `get_router()` | -| Plugin Types | Trusted Only | Trusted + Sandboxed | -| Configuration | Python-only | YAML + Environment | -| Database | SQL Only | SQL + NoSQL + Cache | -| EventBus | Basic | Priorities + Hooks | -| Reloading | Manual | Automatic (Watchdog) | - -#### Migration from v1.x - -See the detailed migration guide in [Migrations](#migration-from-v1x). - ---- - -## Version History - -### v2.0.0 (2025-03-21) - -**Major New Features** - -- Complete architectural shift to plugin-first. -- Introduction of the sandboxing system with process-level isolation. -- Support for Sandboxed plugins via IPC. -- Full FastAPI integration with automatic router mounting. -- ServiceContainer with typed dependency injection. -- EventBus with priorities (HIGH, NORMAL, LOW). -- HookManager for filters and actions. -- PermissionEngine for granular access control. -- Per-plugin RateLimiter with YAML configuration. -- Integrated health checks for all services. -- Redis support as a cache and scheduler backend. -- Comprehensive CLI (`xcore plugin`, `xcore sandbox`, `xcore services`). -- Auto-discovery and automatic plugin loading. - -**Breaking Changes** - -- The `Plugin` class from v1 is replaced by `TrustedBase`. -- Imports have changed: `from xcore import Xcore, TrustedBase`. -- Configuration file moved from `config.py` to `xcore.yaml`. -- Plugins must now define a `plugin.yaml` manifest. - -**Fixes** - -- Complete plugin isolation (no module name conflicts). -- Clean lifecycle management (load/reload/unload). -- Memory leaks fixed during reloading. - ---- - -### v1.2.0 (2024-11-15) - -**New Features** - -- Support for Python 3.12. -- Performance improvements for PluginLoader. -- New CLI commands. - -**Fixes** - -- Memory leak in plugin reloading. -- Race condition in EventBus. - ---- - -### v1.1.0 (2024-08-20) - -**New Features** - -- Redis support for distributed cache. -- Logging system improvements. -- MkDocs documentation. - -**Fixes** - -- Async DB connection issue. -- Timeouts too short for some plugins. - ---- - -### v1.0.0 (2024-06-01) - -**First Stable Version** - -- Basic plugin system. -- Support for PostgreSQL and SQLite. -- Simple EventBus. -- Python-based configuration. -- Minimal CLI. - ---- - -### v0.9.0-beta (2024-04-10) - -**Initial Beta Version** - -- Core architecture. -- Dynamic plugin loading. -- Basic FastAPI support. - ---- - -## Roadmap - -### v2.1.0 (Planned - Q2 2025) - -**Planned Features** - -- [ ] Integrated Plugin Marketplace. -- [ ] GraphQL support for plugins. -- [ ] Native WebSocket for real-time communication. -- [ ] Automatic auto-scaling of sandbox workers. -- [ ] Web-based monitoring dashboard. - -### v2.2.0 (Planned - Q3 2025) - -**Planned Features** - -- [ ] gRPC support for inter-service communication. -- [ ] Integrated Circuit Breaker. -- [ ] End-to-end encryption for IPC. -- [ ] Plugin marketplace with automatic verification. - -### v3.0.0 (Planned - 2026) - -**Long-term Goals** - -- WebAssembly support for ultra-isolated plugins. -- Distributed XCore (multi-node cluster). -- Integrated Machine Learning for auto-scaling. - ---- - -## Support Policy - -| Version | Status | Supported Until | -|---------|--------|------------------| -| v2.0.x | ✅ Active | March 2026 | -| v1.2.x | 🛟 Maintenance | June 2025 | -| v1.1.x | ❌ End of Life | November 2024 | -| v1.0.x | ❌ End of Life | August 2024 | -| < v1.0 | ❌ End of Life | June 2024 | - -**Legend**: - -- ✅ **Active** — Receives all fixes and new features. -- 🛟 **Maintenance** — Security fixes only. -- ❌ **End of Life** — No longer supported; update recommended. - ---- - -## Detailed Release Notes - -### XCore v2.0.0 - -#### Kernel Architecture - -The XCore v2 kernel is designed around several key components: - -``` -XCore -├── PluginSupervisor # Plugin orchestration -├── ServiceContainer # Service management -├── EventBus # Event-driven communication -├── HookManager # Hooks and filters -└── PluginRegistry # Plugin registry -``` - -#### Plugin Types - -**Trusted Plugins** - -- Run in the main process. -- Full access to services. -- Can expose HTTP routes. -- HMAC signature required in strict mode. - -**Sandboxed Plugins** - -- Run in isolated processes. -- AST restrictions on imports. -- Communication via IPC. -- Resource limits (memory, CPU, time). - -#### Integrated Services - -| Service | Description | Configuration | -|---------|-------------|---------------| -| Database | SQL and NoSQL | `services.databases` | -| Cache | Memory or Redis | `services.cache` | -| Scheduler | Scheduled tasks | `services.scheduler` | -| Extensions | Custom services | `services.extensions` | - -#### Migration from v1.x - -**Step 1**: Update imports - -```python -# v1.x -from xcore import Plugin - -# v2.0 -from xcore import TrustedBase -``` - -**Step 2**: Create `plugin.yaml` - -```yaml -# plugin.yaml -name: my_plugin -version: "2.0.0" -execution_mode: trusted -entry_point: src/main.py -``` - -**Step 3**: Migrate configuration - -```yaml -# xcore.yaml (new) -app: - name: my-app - env: production - -plugins: - directory: ./plugins - -services: - databases: - default: - type: postgresql - url: "${DATABASE_URL}" -``` - -**Step 4**: Adapt lifecycle hooks - -```python -# v1.x -class Plugin: - def load(self): - pass - -# v2.0 -class Plugin(TrustedBase): - async def on_load(self): - pass - - async def on_unload(self): - pass -``` - ---- - -## Specific Version Installation - -### Via Poetry - -```bash -# Latest version -poetry add xcore - -# Specific version -poetry add xcore@2.0.0 - -# Version constraint -poetry add "xcore@^2.0" -``` - -### Via pip - -```bash -# Latest version -pip install xcore - -# Specific version -pip install xcore==2.0.0 - -# Minimum version -pip install "xcore>=2.0.0" -``` - -### Via git - -```bash -# Latest stable version -git clone https://github.com/traoreera/xcore.git -cd xcore -poetry install - -# Specific tag -git checkout v2.0.0 -poetry install -``` - ---- - -## Reporting Issues - -If you encounter issues with a specific version: - -1. Check [GitHub issues](https://github.com/traoreera/xcore/issues). -2. Verify if the issue exists in the latest version. -3. Create an issue including: - - XCore version. - - Python version. - - Operating system. - - Description of the issue. - - Reproduction code. - ---- - -## Verify Your Version - -```python -import xcore - -print(xcore.__version__) # "2.0.0" -print(xcore.__version_info__) # (2, 0, 0) -``` - -Via CLI: - -```bash -poetry run xcore --version -# xcore v2.0.0 -``` - ---- - -## Compatibility - -### Python - -| XCore Version | Python 3.11 | Python 3.12 | Python 3.13 | -|---------------|-------------|-------------|-------------| -| v2.0.x | ✅ | ✅ | ✅ | -| v1.2.x | ✅ | ✅ | ⚠️ | -| v1.1.x | ✅ | ⚠️ | ❌ | -| v1.0.x | ✅ | ❌ | ❌ | - -### Major Dependencies - -| XCore | FastAPI | SQLAlchemy | Pydantic | -|-------|---------|------------|----------| -| v2.0.x | 0.118+ | 2.0+ | 2.11+ | -| v1.2.x | 0.100+ | 1.4+ | 1.10+ | -| v1.1.x | 0.95+ | 1.4+ | 1.10+ | -| v1.0.x | 0.90+ | 1.3+ | 1.10+ | - ---- - -## Contributing - -To contribute to XCore development: - -1. Fork the repository. -2. Create a feature branch (`git checkout -b feature/amazing`). -3. Commit your changes (`git commit -m 'Add amazing feature'`). -4. Push to the branch (`git push origin feature/amazing`). -5. Open a Pull Request. - -See the [contribution guide](development/contributing.md) for more details. - ---- - -*Last updated: March 21, 2025* diff --git a/mkdocs.yml b/mkdocs.yml index b49360f6..3fa0c9de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,5 @@ -# mkdocs.yml -# Configuration for MkDocs documentation - site_name: XCore Framework -site_description: Production-grade plugin-first Python framework +site_description: Plugin-first orchestration framework built on FastAPI site_author: XCore Team site_url: https://xcore.readthedocs.io @@ -12,7 +9,6 @@ site_dir: docs theme: name: material palette: - # Light mode - media: "(prefers-color-scheme: light)" scheme: default primary: indigo @@ -20,7 +16,6 @@ theme: toggle: icon: material/brightness-7 name: Switch to dark mode - # Dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: indigo @@ -60,7 +55,11 @@ plugins: markdown_extensions: - admonition - pymdownx.details - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.highlight: anchor_linenums: true - pymdownx.inlinehilite @@ -76,36 +75,37 @@ markdown_extensions: permalink: true nav: - - Home: index.md + - Accueil: index.md - Getting Started: - - getting-started/installation.md - - getting-started/quickstart.md + - Installation: getting-started/installation.md + - Quick Start: getting-started/quickstart.md - Guides: - - guides/creating-plugins.md - - guides/creating-services.md - - guides/services.md - - guides/events.md - - guides/security.md - - guides/monitoring.md - - guides/scaling.md - - guides/troubleshooting.md + - Créer un Plugin: guides/creating-plugins.md + - Services (DB, Cache, Scheduler): guides/services.md + - Événements & Hooks: guides/events.md + - Sécurité: guides/security.md + - Monitoring: guides/monitoring.md + - Scalabilité: guides/scaling.md + - Troubleshooting: guides/troubleshooting.md - Reference: - - reference/sdk.md - - reference/configuration.md - - reference/api.md + - SDK: reference/sdk.md + - Configuration: reference/configuration.md + - CLI: reference/cli.md + - API: reference/api.md + - Benchmarks: reference/benchmarks.md - Architecture: - - architecture/overview.md + - Vue d'ensemble: architecture/overview.md + - Décisions: architecture/decisions.md - Deployment: - - deployment/guide.md + - Guide: deployment/guide.md - Development: - - development/testing.md - - development/contributing.md - - Examples: - - examples/basic-plugin.md - - examples/complete-plugin.md - - examples/trusted-plugin.md - - examples/sandboxed-plugin.md - - About: - - versions.md + - Tests: development/testing.md + - Contribuer: development/contributing.md + - Exemples: + - Plugin basique: examples/basic-plugin.md + - Plugin complet: examples/complete-plugin.md + - Plugin Trusted: examples/trusted-plugin.md + - Plugin Sandboxed: examples/sandboxed-plugin.md + - Changelog: versions.md -copyright: Copyright © 2023 XCore Team +copyright: Copyright © 2026 XCore Team diff --git a/poetry.lock b/poetry.lock index f0d20760..c5943697 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "aiocache" @@ -18,17 +18,175 @@ msgpack = ["msgpack (>=0.5.5)"] redis = ["redis (>=4.2.0)"] [[package]] -name = "aiofiles" -version = "25.1.0" -description = "File support for asyncio." +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695"}, - {file = "aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2"}, + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, ] +[[package]] +name = "aiohttp" +version = "3.13.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5"}, + {file = "aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95"}, + {file = "aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e"}, + {file = "aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7"}, + {file = "aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9"}, + {file = "aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d"}, + {file = "aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc"}, + {file = "aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac"}, + {file = "aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3"}, + {file = "aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06"}, + {file = "aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416"}, + {file = "aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1"}, + {file = "aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe"}, + {file = "aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14"}, + {file = "aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3"}, + {file = "aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832"}, + {file = "aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665"}, + {file = "aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6"}, + {file = "aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c"}, + {file = "aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc"}, + {file = "aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be"}, + {file = "aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b"}, + {file = "aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1"}, + {file = "aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b"}, + {file = "aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3"}, + {file = "aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254"}, + {file = "aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a"}, + {file = "aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500"}, + {file = "aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8"}, + {file = "aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1"}, + {file = "aiohttp-3.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258"}, + {file = "aiohttp-3.13.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f"}, + {file = "aiohttp-3.13.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b"}, + {file = "aiohttp-3.13.5-cp39-cp39-win32.whl", hash = "sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83"}, + {file = "aiohttp-3.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67"}, + {file = "aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + [[package]] name = "aiosqlite" version = "0.22.1" @@ -83,7 +241,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -149,6 +307,33 @@ files = [ {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "autoflake" +version = "2.3.3" +description = "Removes unused imports and unused variables" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "autoflake-2.3.3-py3-none-any.whl", hash = "sha256:a51a3412aff16135ee5b3ec25922459fef10c1f23ce6d6c4977188df859e8b53"}, + {file = "autoflake-2.3.3.tar.gz", hash = "sha256:c24809541e23999f7a7b0d2faadf15deb0bc04cdde49728a2fd943a0c8055504"}, +] + +[package.dependencies] +pyflakes = ">=3.0.0" + [[package]] name = "autopep8" version = "2.3.2" @@ -199,6 +384,31 @@ files = [ [package.extras] extras = ["regex"] +[[package]] +name = "bandit" +version = "1.9.4" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e"}, + {file = "bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] +yaml = ["PyYAML"] + [[package]] name = "black" version = "26.3.1" @@ -263,103 +473,17 @@ files = [ ] [[package]] -name = "cffi" -version = "2.0.0" -description = "Foreign Function Interface for Python calling C code." +name = "cfgv" +version = "3.5.0" +description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +files = [ + {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, + {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, ] -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - [[package]] name = "charset-normalizer" version = "3.4.7" @@ -533,7 +657,7 @@ version = "7.13.5" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, @@ -646,78 +770,6 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] -[[package]] -name = "cryptography" -version = "46.0.7" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" -groups = ["dev"] -files = [ - {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"}, - {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"}, - {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"}, - {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"}, - {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"}, - {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"}, - {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"}, - {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"}, - {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"}, - {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"}, -] - -[package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] - [[package]] name = "csscompressor" version = "0.9.5" @@ -729,6 +781,18 @@ files = [ {file = "csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05"}, ] +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -756,7 +820,7 @@ version = "0.19.2" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399"}, {file = "ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930"}, @@ -785,6 +849,21 @@ files = [ dnspython = ">=2.0.0" idna = ">=2.0.0" +[[package]] +name = "faker" +version = "37.12.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4"}, + {file = "faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa"}, +] + +[package.dependencies] +tzdata = "*" + [[package]] name = "fastapi" version = "0.135.4" @@ -1018,6 +1097,18 @@ files = [ {file = "fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce"}, ] +[[package]] +name = "filelock" +version = "3.29.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"}, + {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"}, +] + [[package]] name = "flake8" version = "7.3.0" @@ -1035,6 +1126,146 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.14.0,<2.15.0" pyflakes = ">=3.4.0,<3.5.0" +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -1380,6 +1611,21 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "identify" +version = "2.6.19" +description = "File identification library for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a"}, + {file = "identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.11" @@ -1401,7 +1647,7 @@ version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -1813,6 +2059,162 @@ griffelib = ">=2.0" mkdocs-autorefs = ">=1.4" mkdocstrings = ">=0.30" +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1869,38 +2271,24 @@ telemetry = ["opentelemetry-api (==1.33.1)", "opentelemetry-exporter-otlp-proto- webauthn = ["fido2 (==1.1.2)"] [[package]] -name = "opshero" -version = "0.1.6" -description = "OpsHero CLI — Analyze CI/CD pipeline errors from your terminal" +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" optional = false -python-versions = ">=3.11" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] files = [ - {file = "opshero-0.1.6-py3-none-any.whl", hash = "sha256:af06323e77bfd423fd9245fa3f9b904153747bf2efd5bbf04de024f7a5bd2d96"}, - {file = "opshero-0.1.6.tar.gz", hash = "sha256:e0892e048acb9971620ca56bd8c94fb85ad6bccb036d979c8b46ba6a24fcdbd1"}, + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, ] -[package.dependencies] -aiofiles = ">=23.2.0" -anyio = ">=4.4.0" -click = ">=8.1.0" -httpx = ">=0.27.0" -platformdirs = ">=4.2.0" -pydantic = ">=2.7.0" -pydantic-settings = ">=2.3.0" -python-jose = {version = ">=3.3.0", extras = ["cryptography"]} -rich = ">=13.7.0" - -[package.extras] -dev = ["pytest (>=8.0.0)", "pytest-asyncio (>=0.23.0)", "pytest-httpx (>=0.30.0)", "ruff (>=0.4.0)"] - [[package]] name = "packaging" version = "26.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["dev", "docs"] files = [ {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, @@ -2049,18 +2437,6 @@ test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] -[[package]] -name = "pip" -version = "26.0.1" -description = "The PyPA recommended tool for installing Python packages." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b"}, - {file = "pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8"}, -] - [[package]] name = "platformdirs" version = "4.9.6" @@ -2079,7 +2455,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -2089,6 +2465,157 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "4.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b"}, + {file = "pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "propcache" +version = "0.4.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, + {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, + {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, + {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, + {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, + {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, + {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, + {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, + {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, + {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, + {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, + {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, + {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, + {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, + {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, + {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, + {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, + {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, + {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, + {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, + {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, + {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, + {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, + {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, + {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, + {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, + {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, +] + [[package]] name = "psutil" version = "7.2.2" @@ -2159,27 +2686,12 @@ version = "0.6.3" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"}, {file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"}, ] -[[package]] -name = "pybind11" -version = "3.0.4" -description = "Seamless operability between C++11 and Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pybind11-3.0.4-py3-none-any.whl", hash = "sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63"}, - {file = "pybind11-3.0.4.tar.gz", hash = "sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c"}, -] - -[package.extras] -global = ["pybind11-global (==3.0.4)"] - [[package]] name = "pycodestyle" version = "2.14.0" @@ -2192,26 +2704,13 @@ files = [ {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, ] -[[package]] -name = "pycparser" -version = "3.0" -description = "C parser in Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, - {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, -] - [[package]] name = "pydantic" version = "2.13.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"}, {file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"}, @@ -2234,7 +2733,7 @@ version = "2.46.3" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1"}, {file = "pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f"}, @@ -2393,7 +2892,7 @@ version = "2.14.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e"}, {file = "pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d"}, @@ -2459,26 +2958,46 @@ extra = ["pygments (>=2.19.1)"] [[package]] name = "pytest" -version = "9.0.3" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" pygments = ">=2.7.2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, +] + +[package.dependencies] +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-benchmark" version = "5.2.3" @@ -2506,7 +3025,7 @@ version = "7.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, @@ -2535,13 +3054,33 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-discovery" +version = "1.2.2" +description = "Python interpreter discovery" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a"}, + {file = "python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + [[package]] name = "python-dotenv" version = "1.2.2" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, @@ -2556,14 +3095,13 @@ version = "3.5.0" description = "JOSE implementation in Python" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771"}, {file = "python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b"}, ] [package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} ecdsa = "!=0.15" pyasn1 = ">=0.5.0" rsa = ">=4.0,<4.1.1 || >4.1.1,<4.4 || >4.4,<5.0" @@ -2647,7 +3185,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -2991,7 +3529,7 @@ version = "4.9.1" description = "Pure-Python RSA implementation" optional = false python-versions = "<4,>=3.6" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -3064,27 +3602,6 @@ statsig = ["statsig (>=0.55.3)"] tornado = ["tornado (>=6)"] unleash = ["UnleashClient (>=6.0.1)"] -[[package]] -name = "setuptools" -version = "82.0.1" -description = "Most extensible Python build backend with support for C/C++ extension modules" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, - {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] - [[package]] name = "shellingham" version = "1.5.4" @@ -3103,7 +3620,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev", "docs"] +groups = ["main", "docs"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3231,20 +3748,17 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\"" full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] -name = "tabulate" -version = "0.10.0" -description = "Pretty-print tabular data" +name = "stevedore" +version = "5.7.0" +description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3"}, - {file = "tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d"}, + {file = "stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed"}, + {file = "stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3"}, ] -[package.extras] -widechars = ["wcwidth"] - [[package]] name = "toml" version = "0.10.2" @@ -3286,6 +3800,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +markers = {dev = "python_version < \"3.13\""} [[package]] name = "typing-inspection" @@ -3293,7 +3808,7 @@ version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, @@ -3308,12 +3823,12 @@ version = "2026.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] -markers = "platform_system == \"Windows\"" +groups = ["main", "dev"] files = [ {file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"}, {file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"}, ] +markers = {main = "platform_system == \"Windows\""} [[package]] name = "tzlocal" @@ -3488,6 +4003,24 @@ dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=25.3.0,<25.4.0)", "pycodestyle (>=2.11.0,<2.12.0)"] +[[package]] +name = "virtualenv" +version = "21.3.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7"}, + {file = "virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} +platformdirs = ">=3.9.1,<5" +python-discovery = ">=1.2.2" + [[package]] name = "watchdog" version = "6.0.0" @@ -3724,7 +4257,150 @@ files = [ {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, ] +[[package]] +name = "yarl" +version = "1.23.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6"}, + {file = "yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d"}, + {file = "yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb"}, + {file = "yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2"}, + {file = "yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5"}, + {file = "yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46"}, + {file = "yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34"}, + {file = "yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d"}, + {file = "yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e"}, + {file = "yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543"}, + {file = "yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957"}, + {file = "yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3"}, + {file = "yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5"}, + {file = "yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595"}, + {file = "yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090"}, + {file = "yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe"}, + {file = "yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169"}, + {file = "yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70"}, + {file = "yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4"}, + {file = "yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4"}, + {file = "yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2"}, + {file = "yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25"}, + {file = "yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f"}, + {file = "yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + [metadata] lock-version = "2.1" python-versions = ">=3.11,<4.0" -content-hash = "3fc39d181a1260ea8c089aa5795d75d637eabf367df2e52ed3d0c31d9abb48ee" +content-hash = "23a4d623eba940f87e97f0c92080279edb8a46ec2d9979d2dbc7e0e6e2279946" diff --git a/pyproject.toml b/pyproject.toml index 10b5a842..df80c6d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,111 +1,106 @@ -[tool.poetry] +[project] name = "xcore" version = "2.1.2" -description = "for base html core app" -authors = ["Eliezer Traore <68350805+traoreera@users.noreply.github.com>"] +description = "Plugin-first orchestration framework built on FastAPI" +authors = [ + { name = "Eliezer Traore", email = "68350805+traoreera@users.noreply.github.com" }, +] license = "MIT" readme = "README.md" +requires-python = ">=3.11,<4.0" + +dependencies = [ + # Web + "fastapi[standard]>=0.135.1,<1.0.0", + "uvicorn>=0.38.0,<1.0.0", + "user-agents>=2.2.0,<3.0.0", + + # Data validation + "pydantic[email]>=2.11.7,<3.0.0", + "pydantic-settings>=2.12.0,<3.0.0", + + # Database & ORM + "sqlalchemy>=2.0.41,<3.0.0", + "alembic>=1.16.1,<2.0.0", + "psycopg2>=2.9.11,<3.0.0", + "aiosqlite>=0.22.1,<1.0.0", + "mysql-connector-python>=9.4.0,<10.0.0", + + # Cache + "redis[hiredis]>=7.0.0,<8.0.0", + "aiocache>=0.12.3,<1.0.0", + + # Scheduling + "apscheduler>=3.11.0,<4.0.0", + + # Security + "python-jose>=3.4.0,<4.0.0", + + # Configuration & environment + "python-dotenv>=1.1.0,<2.0.0", + "pyyaml>=6.0.3,<7.0.0", + "toml>=0.10.2,<1.0.0", + + # Observability & utilities + "rich>=14.0.0,<15.0.0", + "psutil>=7.0.0,<8.0.0", + "watchdog>=6.0.0,<7.0.0", + "markdown>=3.8.2,<4.0.0", + "pillow>=11.2.1,<13.0.0", +] -[tool.poetry.dependencies] - -# environment -python-dotenv = ">=1.1.0,<2.0.0" - -# database migration -alembic = ">=1.16.1,<2.0.0" - -# tools -apscheduler = ">=3.11.0,<4.0.0" -rich = ">=14.0.0,<15.0.0" -watchdog = ">=6.0.0,<7.0.0" -markdown = ">=3.8.2,<4.0.0" -pillow = ">=11.2.1,<13.0.0" -psutil = ">=7.0.0,<8.0.0" -toml = "^0.10.2" -uvicorn = "^0.38.0" - -# security -python-jose = ">=3.4.0,<4.0.0" - - -# python -pip = "==26.0.1" -python = ">=3.11,<4.0" +[project.scripts] +xcore = "xcore.cli.main:main" -# http -user-agents = "^2.2.0" +# ── Build system ────────────────────────────────────────────────────────────── +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" -# data validation -pydantic = { extras = ["email"], version = ">=2.11.7,<3.0.0" } -pydantic-settings = "^2.12.0" +# ── Development dependencies ────────────────────────────────────────────────── -# database -psycopg2 = "^2.9.11" -aiosqlite = "^0.22.1" -mysql-connector-python = "^9.4.0" -sqlalchemy = ">=2.0.41,<3.0.0" -fastapi = {extras = ["standard"], version = "^0.135.1"} -aiocache = "^0.12.3" -redis = {extras = ["hiredis"], version = "^7.0.0"} -pyyaml = "^6.0.3" -pybind11 = "^3.0.3" -setuptools = "^82.0.1" +[tool.poetry.group.dev.dependencies] +# Testing +pytest = "^8.4.2" +pytest-asyncio = "^1.2.0" pytest-cov = "^7.1.0" +pytest-benchmark = "^5.2.3" +httpx = "^0.28.1" +aiohttp = "^3.13.3" +faker = "^37.8.0" - -[tool.poetry.group.dev.dependencies] -pre-commit = "^4.6.0" -isort = "^8.0.1" +# Linting & formatting black = "^26.3.1" +isort = "^8.0.1" flake8 = "^7.3.0" autoflake = "^2.3.1" autopep8 = "^2.3.2" -pytest = "^8.4.2" -pytest-asyncio = "^1.2.0" -httpx = "^0.28.1" -faker = "^37.8.0" -pytest-cov = "^7.0.0" +# Security audit bandit = "^1.8.6" -aiohttp = "^3.13.3" +# Hooks +pre-commit = "^4.6.0" + +# ── Docs dependencies ───────────────────────────────────────────────────────── [tool.poetry.group.docs.dependencies] mkdocs = "^1.6.1" mkdocs-material = "^9.7.3" mkdocs-minify-plugin = "^0.8.0" mkdocstrings = "^1.0.3" +mkdocstrings-python = ">=2.0.3,<3.0.0" -[build-system] -requires = ["poetry-core>=2.0.0,<3.0.0"] -build-backend = "poetry.core.masonry.api" +# ── Tool configuration ──────────────────────────────────────────────────────── -[dependency-groups] -dev = [ - "autopep8>=2.3.2", - "opshero (>=0.1.2,<0.2.0)", - "isort (>=8.0.1,<9.0.0)", - "flake8 (>=7.3.0,<8.0.0)", - "black (>=26.3.1,<27.0.0)", - "pytest-benchmark (>=5.2.3,<6.0.0)", - "tabulate (>=0.10.0,<0.11.0)", -] -docs = [ - "mkdocs (==1.6.1)", - "mkdocs-material (==9.7.3)", - "mkdocs-minify-plugin (==0.8.0)", - "mkdocstrings (==1.0.3)", - "mkdocstrings-python (>=2.0.3,<3.0.0)", -] [tool.flake8] max-line-length = 88 +[tool.black] +line-length = 88 +target-version = ["py311"] - -[tool.poetry.scripts] -xcore = "xcore.cli.main:main" - -[tool.setuptools.packages.find] -where = ["."] -include = ["xcore*"] +[tool.isort] +profile = "black" +line_length = 88 diff --git a/scripts/benchmarks.py b/scripts/benchmarks.py index 4312c464..ff192c87 100644 --- a/scripts/benchmarks.py +++ b/scripts/benchmarks.py @@ -274,25 +274,141 @@ def _make_xcore_config(plugins_dir: Path) -> str: cfg_file.write_text( textwrap.dedent( f""" - app: - name: xcore-bench - secret_key: bench-secret-key-minimum-32-chars!! - server_key: bench-server-key-minimum-32-chars!! - env: development - - plugins: - directory: {plugins_dir} - strict_trusted: false - interval: 0 - - services: - databases: {{}} - cache: - backend: memory - ttl: 300 - max_size: 100000 - scheduler: - enabled: false + app: + name: my-app + env: development + debug: true + secret_key: "vjherbvjhe" + #dotenv: "./extensions/.env" + plugin_prefix: "/app" + server_key: "rhverujierf" + server_key_iterations: 100000 + plugin_tags: + - "test" + + # ── Plugins ─────────────────────────────────────────────────── + plugins: + directory: {plugins_dir} + + # Clé HMAC-SHA256 utilisée pour vérifier plugin.sig + secret_key: "12345" + + # true = refuse tout plugin Trusted non signé + strict_trusted: false + + # Intervalle du watcher de rechargement à chaud (secondes) + # Mettre à 0 pour désactiver en prod si tu gères les reloads via API + interval: 10 + + entry_point: src/main.py + + # Fichiers/extensions exclus du snapshot de détection de changements + snapshot: + extensions: [".log", ".pyc", ".html", ".map", ".min.js"] + filenames: ["__pycache__", "__init__.py", ".env", ".DS_Store"] + hidden: true + + # ── Services ────────────────────────────────────────────────── + services: + # ── Bases de données ──────────────────────────────────────── + databases: + db: + type: sqlasync + url: sqlite+aiosqlite:///db.sqlite3 + echo: false + redis_db: + type: redis + url: redis://localhost:6379/0 + max_connections: 50 + + # ── Cache ──────────────────────────────────────────────────── + cache: + backend: redisrhverujierf + url: redis://localhost:6379/0 + ttl: 300 # TTL par défaut en secondes + max_size: 10000 # ignoré en mode redis, actif en mode memory + + # ── Scheduler ──────────────────────────────────────────────── + scheduler: + enabled: true + backend: redis # redis = tâches persistantes, survivent au redémarrage + timezone: Europe/Paris + + # Jobs déclarés statiquement (optionnel) + #jobs: + # # Nettoyage des sessions expirées chaque nuit à 2h + # - id: cleanup_sessions + # func: myapp.tasks.maintenance:cleanup_sessions + # trigger: cron + # hour: 2 + # minute: 0 + # + # # Snapshot des métriques toutes les 5 minutes + # - id: metrics_snapshot + # func: myapp.tasks.monitoring:snapshot_metrics + # trigger: interval + # minutes: 5 + + # ── Observabilité ───────────────────────────────────────────── + + observability: + logging: + enabled: true + level: DEBUG # DEBUG | INFO | WARNING | ERROR | CRITICAL + format: "%(asctime)s [%(levelname)s] [%(name)s] %(message)s" + file: log/app.log + max_bytes: 52428800 # 50 MB par fichier + backup_count: 10 # 10 fichiers de rotation = 500 MB max + + metrics: + enabled: true + backend: prometheus # memory | prometheus | statsd + prefix: myapp # préfixe des métriques exposées sur /metrics + + tracing: + enabled: false # true si tu utilises OpenTelemetry / Jaeger + backend: noop # noop | opentelemetry | jaeger + service_name: my-app + endpoint: null # ex: http://jaeger:4317 (OTLP gRPC) + + # ── Sécurité ────────────────────────────────────────────────── + security: + # Imports Python autorisés dans les plugins Sandboxed (whitelist AST) + # Les plugins Trusted ne sont pas limités par cette liste + allowed_imports: + - argparse + - fastapi + - json + - re + - math + - random + - datetime + - time + - pathlib + - typing + - dataclasses + - enum + - functools + - itertools + - collections + - string + - hashlib + - base64 + - asyncio + - logging + - uuid + - decimal + - copy + + # Imports explicitement interdits (surcharge les allowed_imports) + forbidden_imports: + - os + + # Rate limit appliqué par défaut à chaque plugin non configuré + rate_limit_default: + calls: 200 + period_seconds: 60 + """ ).strip() ) @@ -314,7 +430,6 @@ async def bench_single_load_unload(self, n_runs: int = 20) -> list[BenchResult]: results = [] tmp = Path(tempfile.mkdtemp(prefix="xcore_bench_")) try: - from xcore.kernel.context import KernelContext from xcore.kernel.runtime.lifecycle import LifecycleManager from xcore.kernel.security.validation import ManifestValidator @@ -431,7 +546,6 @@ async def bench_reload(self, n_runs: int = 15) -> list[BenchResult]: samples: list[float] = [] errors = 0 try: - from xcore.kernel.context import KernelContext from xcore.kernel.runtime.lifecycle import LifecycleManager from xcore.kernel.security.validation import ManifestValidator @@ -1103,9 +1217,8 @@ async def run_suite(args) -> BenchReport: bench = PluginCallBench() for r in await bench.bench_sequential_calls(n_calls=args.calls): report.add(r) - print( - f" ✓ {r.name}: mean={r.mean_ms:.3f}ms, {r.throughput_ops_sec:.0f} ops/s" - ) + ops = r.throughput_ops_sec + print(f" ✓ {r.name}: mean={r.mean_ms:.3f}ms, {ops:.0f} ops/s") for r in await bench.bench_concurrent_calls([10, 50, 100]): report.add(r) print(f" ✓ {r.name}: {r.throughput_ops_sec:.0f} ops/s") @@ -1119,9 +1232,8 @@ async def run_suite(args) -> BenchReport: bench = MiddlewareBench() for r in await bench.bench_pipeline_overhead(n=2000): report.add(r) - print( - f" ✓ {r.name}: mean={r.mean_ms:.4f}ms, {r.throughput_ops_sec:.0f} ops/s" - ) + ops = r.throughput_ops_sec + print(f" ✓ {r.name}: mean={r.mean_ms:.4f}ms, {ops:.0f} ops/s") # ── 4. Events ───────────────────────────────────────────── if "events" in suites_to_run: @@ -1129,9 +1241,8 @@ async def run_suite(args) -> BenchReport: bench = EventsBench() for r in await bench.bench_eventbus(n=2000): report.add(r) - print( - f" ✓ {r.name}: mean={r.mean_ms:.4f}ms, {r.throughput_ops_sec:.0f} ops/s" - ) + ops = r.throughput_ops_sec + print(f" ✓ {r.name}: mean={r.mean_ms:.4f}ms, {ops:.0f} ops/s") for r in await bench.bench_hookmanager(n=1000): report.add(r) print(f" ✓ {r.name}: mean={r.mean_ms:.4f}ms") @@ -1142,9 +1253,8 @@ async def run_suite(args) -> BenchReport: bench = PermissionsBench() for r in bench.bench_engine(n=30000): report.add(r) - print( - f" ✓ {r.name}: mean={r.mean_ms:.5f}ms, {r.throughput_ops_sec:.0f} ops/s" - ) + ops = r.throughput_ops_sec + print(f" ✓ {r.name}: mean={r.mean_ms:.5f}ms, {ops:.0f} ops/s") # ── 6. Cache ────────────────────────────────────────────── if "cache" in suites_to_run: @@ -1152,9 +1262,8 @@ async def run_suite(args) -> BenchReport: bench = CacheBench() for r in await bench.bench_memory_backend(n=3000): report.add(r) - print( - f" ✓ {r.name}: mean={r.mean_ms:.4f}ms, {r.throughput_ops_sec:.0f} ops/s" - ) + ops = r.throughput_ops_sec + print(f" ✓ {r.name}: mean={r.mean_ms:.4f}ms, {ops:.0f} ops/s") # ── 7. Capacity ─────────────────────────────────────────── if "capacity" in suites_to_run or args.capacity: @@ -1262,7 +1371,7 @@ def print_report(report: BenchReport) -> None: if _HAS_PSUTIL: avail_mb = psutil.virtual_memory().available / 1024**2 theoretical_max = int(avail_mb / avg_mb) - print(f"\n Extrapolation:") + print("\n Extrapolation:") print( f" Average {avg_mb:.3f}MB/plugin — " f"{avail_mb:.0f}MB available → " @@ -1282,7 +1391,7 @@ def print_report(report: BenchReport) -> None: print( f" • Plugin call latency (ping): {r.mean_ms:.3f}ms mean, " f"{r.p99_ms:.3f}ms P99 — " - f"{'✅ excellent' if r.mean_ms < 1 else '⚠ acceptable' if r.mean_ms < 5 else '❌ slow'}" + f"{'✅ excellent' if r.mean_ms < 1 else '⚠ ok' if r.mean_ms < 5 else '❌'}" ) if ( diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 06c3066e..00000000 --- a/uv.lock +++ /dev/null @@ -1,525 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[manifest] - -[manifest.dependency-groups] -dev = [ - { name = "autopep8", specifier = ">=2.3.2" }, - { name = "opshero", specifier = ">=0.1.2,<0.2.0" }, -] - -[[package]] -name = "aiofiles" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, -] - -[[package]] -name = "autopep8" -version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycodestyle" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, -] - -[[package]] -name = "certifi" -version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "click" -version = "8.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, -] - -[[package]] -name = "ecdsa" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "opshero" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiofiles" }, - { name = "anyio" }, - { name = "click" }, - { name = "httpx" }, - { name = "platformdirs" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-jose", extra = ["cryptography"] }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/51/84e3285212e26f7b1b4bde8ef9e578672c73bb3ba195e3d502ff14aabc7d/opshero-0.1.2.tar.gz", hash = "sha256:83d4a4d637986dd898d18cd414bde7f8bc169bb293de9bc64c9909506ed0ed2c", size = 33389, upload-time = "2026-03-06T16:38:16.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/a6/2192729736b31cb94b625dc8680b71dfe01188b4081d26c73e9464a03baf/opshero-0.1.2-py3-none-any.whl", hash = "sha256:1dbd6c1d44388e5121d7a20fc5277f0340fef9d965263c15c0fef8df59e0a757", size = 40217, upload-time = "2026-03-06T16:38:15.303Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, -] - -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, - { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, - { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, - { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, - { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, - { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, - { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, - { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, - { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, - { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, - { url = "https://files.pythonhosted.org/packages/df/05/ab3b0742bad1d51822f1af0c4232208408902bdcfc47601f3b812e09e6c2/pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788", size = 2116814, upload-time = "2026-04-13T09:04:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/98/08/30b43d9569d69094a0899a199711c43aa58fce6ce80f6a8f7693673eb995/pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22", size = 1951867, upload-time = "2026-04-13T09:04:02.364Z" }, - { url = "https://files.pythonhosted.org/packages/db/a0/bf9a1ba34537c2ed3872a48195291138fdec8fe26c4009776f00d63cf0c8/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47", size = 1977040, upload-time = "2026-04-13T09:06:16.088Z" }, - { url = "https://files.pythonhosted.org/packages/71/70/0ba03c20e1e118219fc18c5417b008b7e880f0e3fb38560ec4465984d471/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b", size = 2055284, upload-time = "2026-04-13T09:05:25.125Z" }, - { url = "https://files.pythonhosted.org/packages/58/cf/1e320acefbde7fb7158a9e5def55e0adf9a4634636098ce28dc6b978e0d3/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530", size = 2238896, upload-time = "2026-04-13T09:05:01.345Z" }, - { url = "https://files.pythonhosted.org/packages/df/f5/ea8ba209756abe9eba891bb0ef3772b4c59a894eb9ad86cd5bd0dd4e3e52/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59", size = 2314353, upload-time = "2026-04-13T09:06:07.942Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f8/5885350203b72e96438eee7f94de0d8f0442f4627237ca8ef75de34db1cd/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa", size = 2098522, upload-time = "2026-04-13T09:04:23.239Z" }, - { url = "https://files.pythonhosted.org/packages/bf/88/5930b0e828e371db5a556dd3189565417ddc3d8316bb001058168aadcf5f/pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7", size = 2168757, upload-time = "2026-04-13T09:07:12.46Z" }, - { url = "https://files.pythonhosted.org/packages/da/75/63d563d3035a0548e721c38b5b69fd5626fdd51da0f09ff4467503915b82/pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3", size = 2202518, upload-time = "2026-04-13T09:05:44.418Z" }, - { url = "https://files.pythonhosted.org/packages/a7/53/1958eacbfddc41aadf5ae86dd85041bf054b675f34a2fa76385935f96070/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef", size = 2190148, upload-time = "2026-04-13T09:06:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/c7/17/098cc6d3595e4623186f2bc6604a6195eb182e126702a90517236391e9ce/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a", size = 2342925, upload-time = "2026-04-13T09:04:17.286Z" }, - { url = "https://files.pythonhosted.org/packages/71/a7/abdb924620b1ac535c690b36ad5b8871f376104090f8842c08625cecf1d3/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b", size = 2383167, upload-time = "2026-04-13T09:04:52.643Z" }, - { url = "https://files.pythonhosted.org/packages/d7/c9/2ddd10f50e4b7350d2574629a0f53d8d4eb6573f9c19a6b43e6b1487a31d/pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef", size = 1965660, upload-time = "2026-04-13T09:06:05.877Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e7/1efc38ed6f2680c032bcefa0e3ebd496a8c77e92dfdb86b07d0f2fc632b1/pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25", size = 2069563, upload-time = "2026-04-13T09:07:14.738Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1e/a325b4989e742bf7e72ed35fa124bc611fd76539c9f8cd2a9a7854473533/pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4", size = 2034966, upload-time = "2026-04-13T09:04:21.629Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/914891d384cdbf9a6f464eb13713baa22ea1e453d4da80fb7da522079370/pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca", size = 2113349, upload-time = "2026-04-13T09:04:59.407Z" }, - { url = "https://files.pythonhosted.org/packages/35/95/3a0c6f65e231709fb3463e32943c69d10285cb50203a2130a4732053a06d/pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940", size = 1949170, upload-time = "2026-04-13T09:06:09.935Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/d845c36a608469fe7bee226edeff0984c33dbfe7aecd755b0e7ab5a275c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb", size = 1977914, upload-time = "2026-04-13T09:04:56.16Z" }, - { url = "https://files.pythonhosted.org/packages/08/6f/f2e7a7f85931fb31671f5378d1c7fc70606e4b36d59b1b48e1bd1ef5d916/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b", size = 2050538, upload-time = "2026-04-13T09:05:06.789Z" }, - { url = "https://files.pythonhosted.org/packages/8c/97/f4aa7181dd9a16dd9059a99fc48fdab0c2aab68307283a5c04cf56de68c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7", size = 2236294, upload-time = "2026-04-13T09:07:03.2Z" }, - { url = "https://files.pythonhosted.org/packages/24/c1/6a5042fc32765c87101b500f394702890af04239c318b6002cfd627b710d/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655", size = 2312954, upload-time = "2026-04-13T09:06:11.919Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e4/566101a561492ce8454f0844ca29c3b675a6b3a7b3ff577db85ed05c8c50/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7", size = 2102533, upload-time = "2026-04-13T09:06:58.664Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ac/adc11ee1646a5c4dd9abb09a00e7909e6dc25beddc0b1310ca734bb9b48e/pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644", size = 2169447, upload-time = "2026-04-13T09:04:11.143Z" }, - { url = "https://files.pythonhosted.org/packages/26/73/408e686b45b82d28ac19e8229e07282254dbee6a5d24c5c7cf3cf3716613/pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e", size = 2200672, upload-time = "2026-04-13T09:03:54.056Z" }, - { url = "https://files.pythonhosted.org/packages/0a/3b/807d5b035ec891b57b9079ce881f48263936c37bd0d154a056e7fd152afb/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5", size = 2188293, upload-time = "2026-04-13T09:07:07.614Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ed/719b307516285099d1196c52769fdbe676fd677da007b9c349ae70b7226d/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae", size = 2335023, upload-time = "2026-04-13T09:04:05.176Z" }, - { url = "https://files.pythonhosted.org/packages/8d/90/8718e4ae98c4e8a7325afdc079be82be1e131d7a47cb6c098844a9531ffe/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2", size = 2377155, upload-time = "2026-04-13T09:06:18.081Z" }, - { url = "https://files.pythonhosted.org/packages/dd/dc/7172789283b963f81da2fc92b186e22de55687019079f71c4d570822502b/pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5", size = 1963078, upload-time = "2026-04-13T09:05:30.615Z" }, - { url = "https://files.pythonhosted.org/packages/e0/69/03a7ea4b6264def3a44eabf577528bcec2f49468c5698b2044dea54dc07e/pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e", size = 2068439, upload-time = "2026-04-13T09:04:57.729Z" }, - { url = "https://files.pythonhosted.org/packages/f5/eb/1c3afcfdee2ab6634b802ab0a0f1966df4c8b630028ec56a1cb0a710dc58/pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f", size = 2026470, upload-time = "2026-04-13T09:05:08.654Z" }, - { url = "https://files.pythonhosted.org/packages/5c/30/1177dde61b200785c4739665e3aa03a9d4b2c25d2d0408b07d585e633965/pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928", size = 2107447, upload-time = "2026-04-13T09:05:46.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/60/4e0f61f99bdabbbc309d364a2791e1ba31e778a4935bc43391a7bdec0744/pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e", size = 1926927, upload-time = "2026-04-13T09:06:20.371Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d0/67f89a8269152c1d6eaa81f04e75a507372ebd8ca7382855a065222caa80/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655", size = 1966613, upload-time = "2026-04-13T09:07:05.389Z" }, - { url = "https://files.pythonhosted.org/packages/cd/07/8dfdc3edc78f29a80fb31f366c50203ec904cff6a4c923599bf50ac0d0ff/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa", size = 2032902, upload-time = "2026-04-13T09:06:42.47Z" }, - { url = "https://files.pythonhosted.org/packages/b0/2a/111c5e8fe24f99c46bcad7d3a82a8f6dbc738066e2c72c04c71f827d8c78/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce", size = 2244456, upload-time = "2026-04-13T09:05:36.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7c/cfc5d11c15a63ece26e148572c77cfbb2c7f08d315a7b63ef0fe0711d753/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d", size = 2294535, upload-time = "2026-04-13T09:06:01.689Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2c/f0d744e3dab7bd026a3f4670a97a295157cff923a2666d30a15a70a7e3d0/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72", size = 2104621, upload-time = "2026-04-13T09:04:34.388Z" }, - { url = "https://files.pythonhosted.org/packages/a7/64/e7cc4698dc024264d214b51d5a47a2404221b12060dd537d76f831b2120a/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827", size = 2130718, upload-time = "2026-04-13T09:04:26.23Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a8/224e655fec21f7d4441438ad2ecaccb33b5a3876ce7bb2098c74a49efc14/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d", size = 2180738, upload-time = "2026-04-13T09:05:50.253Z" }, - { url = "https://files.pythonhosted.org/packages/32/7b/b3025618ed4c4e4cbaa9882731c19625db6669896b621760ea95bc1125ef/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea", size = 2171222, upload-time = "2026-04-13T09:07:29.929Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e3/68170aa1d891920af09c1f2f34df61dc5ff3a746400027155523e3400e89/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2", size = 2320040, upload-time = "2026-04-13T09:06:35.732Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/5e65807001b84972476300c1f49aea2b4971b7e9fffb5c2654877dadd274/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e", size = 2377062, upload-time = "2026-04-13T09:07:39.945Z" }, - { url = "https://files.pythonhosted.org/packages/75/03/48caa9dd5f28f7662bd52bff454d9a451f6b7e5e4af95e289e5e170749c9/pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32", size = 1951028, upload-time = "2026-04-13T09:04:20.224Z" }, - { url = "https://files.pythonhosted.org/packages/87/ed/e97ff55fe28c0e6e3cba641d622b15e071370b70e5f07c496b07b65db7c9/pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59", size = 2048519, upload-time = "2026-04-13T09:05:10.464Z" }, - { url = "https://files.pythonhosted.org/packages/b6/51/e0db8267a287994546925f252e329eeae4121b1e77e76353418da5a3adf0/pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5", size = 2026791, upload-time = "2026-04-13T09:04:37.724Z" }, - { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, - { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - -[[package]] -name = "python-jose" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ecdsa" }, - { name = "pyasn1" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, -] - -[package.optional-dependencies] -cryptography = [ - { name = "cryptography" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] diff --git a/xcore_bench_report.json b/xcore_bench_report.json new file mode 100644 index 00000000..de74e37f --- /dev/null +++ b/xcore_bench_report.json @@ -0,0 +1,461 @@ +{ + "meta": { + "xcore_version": "2.1.2", + "python_version": "3.14.4", + "platform": "linux", + "timestamp": "2026-04-29T11:02:58.798418", + "cpu_count": 8, + "total_duration_s": 3.172987825999371, + "system_info": { + "cpu_count": 8, + "cpu_freq_mhz": 1379.54425, + "total_ram_gb": 15.3, + "available_ram_gb": 8.4 + } + }, + "results": [ + { + "name": "single_plugin_load", + "category": "plugin_lifecycle", + "iterations": 20, + "total_ms": 23.769449999235803, + "mean_ms": 1.1884724999617902, + "median_ms": 1.1223724995943485, + "p95_ms": 2.5641230004112003, + "p99_ms": 2.5641230004112003, + "min_ms": 0.9731579993967898, + "max_ms": 2.5641230004112003, + "std_ms": 0.3313133053575544, + "throughput_ops_sec": 841.4161876123767, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "20 runs" + }, + { + "name": "single_plugin_unload", + "category": "plugin_lifecycle", + "iterations": 20, + "total_ms": 5.08798499686236, + "mean_ms": 0.254399249843118, + "median_ms": 0.24472649874951458, + "p95_ms": 0.3115250001428649, + "p99_ms": 0.3115250001428649, + "min_ms": 0.2223270003014477, + "max_ms": 0.3115250001428649, + "std_ms": 0.024043957390132698, + "throughput_ops_sec": 3930.829201016419, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "20 runs" + }, + { + "name": "plugin_reload", + "category": "plugin_lifecycle", + "iterations": 20, + "total_ms": 12.287289991945727, + "mean_ms": 0.6143644995972863, + "median_ms": 0.6017319992679404, + "p95_ms": 0.736632999178255, + "p99_ms": 0.736632999178255, + "min_ms": 0.5194500008656178, + "max_ms": 0.736632999178255, + "std_ms": 0.055637530881803024, + "throughput_ops_sec": 1627.6982160517027, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "batch_load_5_plugins", + "category": "plugin_lifecycle", + "iterations": 5, + "total_ms": 334.2003189991374, + "mean_ms": 66.84006379982748, + "median_ms": 66.84006379982748, + "p95_ms": 100.26009569974121, + "p99_ms": 133.68012759965495, + "min_ms": 0, + "max_ms": 334.2003189991374, + "std_ms": 0, + "throughput_ops_sec": 14.961086856451821, + "memory_delta_mb": 8.98046875, + "errors": 4, + "notes": "1/5 loaded, 1.80MB/plugin" + }, + { + "name": "batch_load_20_plugins", + "category": "plugin_lifecycle", + "iterations": 20, + "total_ms": 98.60400199977448, + "mean_ms": 4.930200099988724, + "median_ms": 4.930200099988724, + "p95_ms": 7.395300149983086, + "p99_ms": 9.860400199977448, + "min_ms": 0, + "max_ms": 98.60400199977448, + "std_ms": 0, + "throughput_ops_sec": 202.83152401913406, + "memory_delta_mb": 0.0078125, + "errors": 19, + "notes": "1/20 loaded, 0.00MB/plugin" + }, + { + "name": "batch_load_50_plugins", + "category": "plugin_lifecycle", + "iterations": 50, + "total_ms": 217.12198100067326, + "mean_ms": 4.342439620013465, + "median_ms": 4.342439620013465, + "p95_ms": 6.513659430020198, + "p99_ms": 8.68487924002693, + "min_ms": 0, + "max_ms": 217.12198100067326, + "std_ms": 0, + "throughput_ops_sec": 230.28529755283026, + "memory_delta_mb": 0.0078125, + "errors": 49, + "notes": "1/50 loaded, 0.00MB/plugin" + }, + { + "name": "sequential_call_ping", + "category": "plugin_calls", + "iterations": 300, + "total_ms": 0.43731200457841624, + "mean_ms": 0.0014577066819280542, + "median_ms": 0.0013984999895910732, + "p95_ms": 0.0015459991118405014, + "p99_ms": 0.003385999661986716, + "min_ms": 0.0013400003808783367, + "max_ms": 0.00393899972550571, + "std_ms": 0.0002761582529130064, + "throughput_ops_sec": 686009.0664312092, + "memory_delta_mb": 0.0, + "errors": 300, + "notes": "single plugin, 300 calls" + }, + { + "name": "sequential_call_echo_payload", + "category": "plugin_calls", + "iterations": 300, + "total_ms": 0.4002499863418052, + "mean_ms": 0.0013341666211393506, + "median_ms": 0.0013040007615927607, + "p95_ms": 0.0014170000213198364, + "p99_ms": 0.002333999873371795, + "min_ms": 0.001186999725177884, + "max_ms": 0.0084509993030224, + "std_ms": 0.0004493578761466993, + "throughput_ops_sec": 749531.5683628937, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "concurrent_10_calls", + "category": "plugin_calls", + "iterations": 10, + "total_ms": 0.1259989985555876, + "mean_ms": 0.01259989985555876, + "median_ms": 0.01259989985555876, + "p95_ms": 0.01637986981222639, + "p99_ms": 0.01889984978333814, + "min_ms": 0, + "max_ms": 0.1259989985555876, + "std_ms": 0, + "throughput_ops_sec": 79365.71016148395, + "memory_delta_mb": 0.0, + "errors": 10, + "notes": "asyncio.gather(10)" + }, + { + "name": "concurrent_50_calls", + "category": "plugin_calls", + "iterations": 50, + "total_ms": 0.9103359989239834, + "mean_ms": 0.01820671997847967, + "median_ms": 0.01820671997847967, + "p95_ms": 0.02366873597202357, + "p99_ms": 0.027310079967719503, + "min_ms": 0, + "max_ms": 0.9103359989239834, + "std_ms": 0, + "throughput_ops_sec": 54924.77509304253, + "memory_delta_mb": 0.0, + "errors": 50, + "notes": "asyncio.gather(50)" + }, + { + "name": "concurrent_100_calls", + "category": "plugin_calls", + "iterations": 100, + "total_ms": 0.977535000856733, + "mean_ms": 0.00977535000856733, + "median_ms": 0.00977535000856733, + "p95_ms": 0.012707955011137528, + "p99_ms": 0.014663025012850994, + "min_ms": 0, + "max_ms": 0.977535000856733, + "std_ms": 0, + "throughput_ops_sec": 102298.12734312104, + "memory_delta_mb": 0.0, + "errors": 100, + "notes": "asyncio.gather(100)" + }, + { + "name": "routing_5_plugins", + "category": "plugin_calls", + "iterations": 200, + "total_ms": 36.704412994367885, + "mean_ms": 0.18352206497183943, + "median_ms": 0.053088499953446444, + "p95_ms": 0.09035499897436239, + "p99_ms": 1.3090060001559323, + "min_ms": 0.04803100091521628, + "max_ms": 23.589220998474048, + "std_ms": 1.6658224108825137, + "throughput_ops_sec": 5448.936072910062, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "1 loaded, 200 calls round-robin" + }, + { + "name": "routing_20_plugins", + "category": "plugin_calls", + "iterations": 200, + "total_ms": 27.830518991322606, + "mean_ms": 0.13915259495661303, + "median_ms": 0.04957249984727241, + "p95_ms": 0.05526799941435456, + "p99_ms": 0.15645399980712682, + "min_ms": 0.04550499943434261, + "max_ms": 17.677613999694586, + "std_ms": 1.2464368919166002, + "throughput_ops_sec": 7186.3553842585125, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "1 loaded, 200 calls round-robin" + }, + { + "name": "pipeline_0_middlewares", + "category": "middleware", + "iterations": 2000, + "total_ms": 6.2837549648975255, + "mean_ms": 0.0031418774824487627, + "median_ms": 0.0030020000849617645, + "p95_ms": 0.003402001311769709, + "p99_ms": 0.005285000952426344, + "min_ms": 0.0028329995984677225, + "max_ms": 0.027694000891642645, + "std_ms": 0.0007802001839578725, + "throughput_ops_sec": 318281.02960291284, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "pipeline_4_middlewares", + "category": "middleware", + "iterations": 2000, + "total_ms": 15.405727997858776, + "mean_ms": 0.007702863998929388, + "median_ms": 0.00762699892220553, + "p95_ms": 0.007886999810580164, + "p99_ms": 0.008286000593216158, + "min_ms": 0.007045999154797755, + "max_ms": 0.038139998650876805, + "std_ms": 0.0009654246622238234, + "throughput_ops_sec": 129821.84290661095, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_emit_no_handlers", + "category": "events", + "iterations": 2000, + "total_ms": 5.895346959732706, + "mean_ms": 0.002947673479866353, + "median_ms": 0.002868000592570752, + "p95_ms": 0.0030080009310040623, + "p99_ms": 0.0050319995352765545, + "min_ms": 0.002712999048526399, + "max_ms": 0.043355999878258444, + "std_ms": 0.001064872533143774, + "throughput_ops_sec": 339250.6011369142, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_emit_1_handler", + "category": "events", + "iterations": 2000, + "total_ms": 60.01824601298722, + "mean_ms": 0.03000912300649361, + "median_ms": 0.02952399881905876, + "p95_ms": 0.03320500036352314, + "p99_ms": 0.041909999708877876, + "min_ms": 0.02584400135674514, + "max_ms": 0.2899190003518015, + "std_ms": 0.006676347765717948, + "throughput_ops_sec": 33323.199741079145, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_emit_10_handlers", + "category": "events", + "iterations": 2000, + "total_ms": 188.12697701469006, + "mean_ms": 0.09406348850734503, + "median_ms": 0.09339999996882398, + "p95_ms": 0.10070899952552281, + "p99_ms": 0.14355000166688114, + "min_ms": 0.08609399992565159, + "max_ms": 0.22089699996286072, + "std_ms": 0.009094239149095826, + "throughput_ops_sec": 10631.11751295418, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "eventbus_wildcard_matching", + "category": "events", + "iterations": 2000, + "total_ms": 61.01844501426967, + "mean_ms": 0.030509222507134837, + "median_ms": 0.030687499929626938, + "p95_ms": 0.03206899964425247, + "p99_ms": 0.03860600008920301, + "min_ms": 0.02817599852278363, + "max_ms": 0.11208199975953903, + "std_ms": 0.002674307538655587, + "throughput_ops_sec": 32776.97423348437, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "hookmanager_5_priority_hooks", + "category": "events", + "iterations": 1000, + "total_ms": 583.6400079806481, + "mean_ms": 0.5836400079806481, + "median_ms": 0.5261955002424656, + "p95_ms": 0.8773120007390389, + "p99_ms": 1.2226449998706812, + "min_ms": 0.4424439994181739, + "max_ms": 1.6500950005138293, + "std_ms": 0.15156558065797768, + "throughput_ops_sec": 1713.384939904869, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "permission_check_cold", + "category": "permissions", + "iterations": 30000, + "total_ms": 95.54461987681862, + "mean_ms": 0.0031848206625606206, + "median_ms": 0.0024439996195724234, + "p95_ms": 0.004006000381195918, + "p99_ms": 0.005276000592857599, + "min_ms": 0.0021680007193936035, + "max_ms": 16.837430999657954, + "std_ms": 0.09720053431247311, + "throughput_ops_sec": 313989.4223105147, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "permission_check_cached", + "category": "permissions", + "iterations": 30000, + "total_ms": 47.549882936436916, + "mean_ms": 0.0015849960978812304, + "median_ms": 0.0014399993233382702, + "p95_ms": 0.002587999915704131, + "p99_ms": 0.0039810001908335835, + "min_ms": 0.0012749987945426255, + "max_ms": 0.18682699919736478, + "std_ms": 0.0013853130015427815, + "throughput_ops_sec": 630916.3797543517, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_set_single", + "category": "cache", + "iterations": 3000, + "total_ms": 9.143716904873145, + "mean_ms": 0.003047905634957715, + "median_ms": 0.0027790010790340602, + "p95_ms": 0.003654000465758145, + "p99_ms": 0.0054299998737405986, + "min_ms": 0.0024029995984165, + "max_ms": 0.1521260001027258, + "std_ms": 0.0034273297951717763, + "throughput_ops_sec": 328094.147184407, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_get_hot", + "category": "cache", + "iterations": 3000, + "total_ms": 4.200314024274121, + "mean_ms": 0.0014001046747580403, + "median_ms": 0.0013295002645463683, + "p95_ms": 0.0017040001694113016, + "p99_ms": 0.0024310011212946847, + "min_ms": 0.0011490010365378112, + "max_ms": 0.019130999135086313, + "std_ms": 0.000579013570229349, + "throughput_ops_sec": 714232.3127896244, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_mset_100_keys", + "category": "cache", + "iterations": 300, + "total_ms": 31.199731001834152, + "mean_ms": 0.10399910333944717, + "median_ms": 0.09965949993784307, + "p95_ms": 0.12172499918960966, + "p99_ms": 0.14522600031341426, + "min_ms": 0.09780799882719293, + "max_ms": 0.15260500003932975, + "std_ms": 0.008374501325396673, + "throughput_ops_sec": 9615.467517407884, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + }, + { + "name": "cache_mget_100_keys", + "category": "cache", + "iterations": 300, + "total_ms": 13.054071996521088, + "mean_ms": 0.04351357332173696, + "median_ms": 0.04144000013184268, + "p95_ms": 0.05062099990027491, + "p99_ms": 0.07507800000894349, + "min_ms": 0.04022000030090567, + "max_ms": 0.08977200013760012, + "std_ms": 0.0059871690475594725, + "throughput_ops_sec": 22981.33487236397, + "memory_delta_mb": 0.0, + "errors": 0, + "notes": "" + } + ], + "capacity": {} +}