diff --git a/france-market-scanner/.env.example b/france-market-scanner/.env.example
new file mode 100644
index 0000000..a6781cb
--- /dev/null
+++ b/france-market-scanner/.env.example
@@ -0,0 +1,13 @@
+# INPI Credentials (required for financial data)
+# Create free account at https://data.inpi.fr
+INPI_USERNAME=
+INPI_PASSWORD=
+
+# BODACC FTPS (optional, for bulk historical data)
+# Contact donnees-dila@dila.gouv.fr for access
+BODACC_FTPS_USERNAME=
+BODACC_FTPS_PASSWORD=
+
+# Optional: Override config defaults
+# DATABASE_PATH=data/france_companies.duckdb
+# LOG_LEVEL=INFO
diff --git a/france-market-scanner/.gitignore b/france-market-scanner/.gitignore
new file mode 100644
index 0000000..05b1e53
--- /dev/null
+++ b/france-market-scanner/.gitignore
@@ -0,0 +1,56 @@
+# Data files
+data/
+*.duckdb
+*.duckdb.wal
+*.parquet
+
+# Downloads
+downloads/
+
+# Logs
+logs/
+*.log
+
+# Environment
+.env
+.env.local
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual environments
+venv/
+ENV/
+env/
+.venv/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
diff --git a/france-market-scanner/README.md b/france-market-scanner/README.md
new file mode 100644
index 0000000..c114af3
--- /dev/null
+++ b/france-market-scanner/README.md
@@ -0,0 +1,532 @@
+# France Market Scanner
+
+Identify French company "gems" - small teams with high revenue/profits, ideal for AI automation or acquisition.
+
+## Data Available
+
+| Source | File | Records | Size |
+|--------|------|---------|------|
+| **SIRENE** (companies) | `sirene_unites_legales.parquet` | 29M | 716MB |
+| **SIRENE** (establishments) | `sirene_etablissements.parquet` | 42M | 1.9GB |
+| **INPI** (annual accounts) | `inpi_comptes_*.parquet` | 6.6M | 325MB |
+| **BODACC** (legal announcements) | `bodacc_annonces.parquet` | 50K | 17MB |
+| **Total** | | | **2.9GB** |
+
+## Quick Start
+
+```bash
+uv sync
+```
+
+### Query with chdb (ClickHouse SQL on Parquet)
+
+```python
+import chdb
+
+# Simple query
+chdb.query('''
+ SELECT siren, chiffre_affaires, resultat_net
+ FROM file("data/parquet/inpi_comptes_2022.parquet")
+ WHERE chiffre_affaires > 1000000
+ LIMIT 10
+''', 'PrettyCompact')
+```
+
+## Joining Data
+
+Join INPI financials with SIRENE company info:
+
+```python
+import chdb
+
+result = chdb.query('''
+ SELECT
+ i.siren,
+ s.denomination,
+ s.tranche_effectifs as employees,
+ s.activite_principale as naf_code,
+ i.chiffre_affaires as revenue,
+ i.resultat_net as profit
+ FROM file("data/parquet/inpi_comptes_2022.parquet") i
+ JOIN file("data/parquet/sirene_unites_legales.parquet") s
+ ON i.siren = s.siren
+ WHERE i.chiffre_affaires > 1000000
+ ORDER BY i.chiffre_affaires DESC
+ LIMIT 20
+''', 'DataFrame') # returns pandas DataFrame
+```
+
+### Employee brackets (`tranche_effectifs`)
+
+| Code | Employees |
+|------|-----------|
+| 00 | 0 |
+| 01 | 1-2 |
+| 02 | 3-5 |
+| 03 | 6-9 |
+| 11 | 10-19 |
+| 12 | 20-49 |
+| 21 | 50-99 |
+| NN | Unknown (~90%) |
+
+### Find small teams with high revenue
+
+```python
+import chdb
+
+# Companies with <10 employees but >1M revenue
+result = chdb.query('''
+ SELECT
+ i.siren,
+ s.denomination,
+ s.tranche_effectifs,
+ s.activite_principale,
+ i.chiffre_affaires,
+ i.resultat_net,
+ round(i.resultat_net / i.chiffre_affaires * 100, 1) as margin_pct
+ FROM file("data/parquet/inpi_comptes_2022.parquet") i
+ JOIN file("data/parquet/sirene_unites_legales.parquet") s
+ ON i.siren = s.siren
+ WHERE i.chiffre_affaires > 1000000
+ AND i.resultat_net > 0
+ AND s.tranche_effectifs IN ('01', '02', '03')
+ ORDER BY margin_pct DESC
+ LIMIT 50
+''', 'DataFrame')
+```
+
+## Finding Gems
+
+Query to find small teams with high revenue per employee:
+
+```python
+import chdb
+
+result = chdb.query("""
+ SELECT
+ i.siren,
+ s.denomination,
+ s.tranche_effectifs as emp,
+ s.activite_principale as naf,
+ round(i.chiffre_affaires, 0) as revenue,
+ round(i.resultat_net, 0) as profit,
+ round(i.chiffre_affaires /
+ multiIf(s.tranche_effectifs = '01', 1.5,
+ s.tranche_effectifs = '02', 4,
+ s.tranche_effectifs = '03', 7.5, 1), 0) as rev_per_emp,
+ round(i.resultat_net / i.chiffre_affaires * 100, 1) as margin
+ FROM file('data/parquet/inpi_comptes_2022.parquet') i
+ JOIN file('data/parquet/sirene_unites_legales.parquet') s
+ ON i.siren = s.siren
+ WHERE i.chiffre_affaires > 500000
+ AND i.resultat_net > 0
+ AND s.tranche_effectifs IN ('01', '02', '03') -- <10 employees
+ ORDER BY rev_per_emp DESC
+ LIMIT 30
+""", 'DataFrame')
+```
+
+### Caveats
+
+Many "gems" are actually:
+- **Holding companies** (NAF 64.xx, 70.10Z) - no real employees, just financial flows
+- **Subsidiaries** - employees counted in parent company
+- **Trading companies** - high revenue, low margin commodity trading
+
+Filter them out:
+```sql
+WHERE s.activite_principale NOT LIKE '64%' -- exclude holdings
+ AND s.activite_principale NOT LIKE '70.10%'
+ AND i.resultat_net / i.chiffre_affaires > 0.05 -- >5% margin
+```
+
+## Data Reliability
+
+### The SIRENE Employee Problem
+
+The `tranche_effectifs` field from SIRENE is **unreliable**:
+
+| Issue | Reality |
+|-------|---------|
+| 90% are "NN" (unknown) | Most companies never declare |
+| Self-declared | No verification, rarely updated |
+| Brackets are stale | Company grows from 5 to 300, still shows "02" |
+
+**Example**: CONGO MARITIME shows `tranche_effectifs = "02"` (3-5 employees) but actually has ~300 employees based on payroll.
+
+### The Solution: Use Payroll Data
+
+INPI provides `charges_personnel` (total staff costs) from **audited annual accounts**. This is much more reliable:
+
+| Source | Field | Reliability | Why |
+|--------|-------|-------------|-----|
+| **INPI** | `charges_personnel` | **HIGH** | Audited income statement, legal requirement |
+| **INPI** | `resultat_net` | **HIGH** | Official annual accounts filed with Greffe |
+| **SIRENE** | `tranche_effectifs` | **LOW** | Self-declared, rarely updated |
+
+### What is `charges_personnel`?
+
+Total employer cost for all employees, including:
+- Gross salaries
+- Social charges (~45% of gross in France)
+- Benefits and bonuses
+
+**Average cost per employee in France: ~70,000€/year**
+
+```
+charges_personnel = 2,100,000€
+estimated_employees = 2,100,000 / 70,000 = 30 employees
+```
+
+### Better Query: Payroll-Based Employee Estimates
+
+```python
+import chdb
+
+result = chdb.query("""
+ SELECT
+ i.siren,
+ s.denomination,
+ s.activite_principale as naf,
+ round(i.chiffre_affaires / 1e6, 2) as revenue_millions,
+ round(i.charges_personnel / 1e6, 2) as payroll_millions,
+ round(i.resultat_net / 1e6, 2) as profit_millions,
+
+ -- Payroll-based employee estimate (70K avg cost in France)
+ round(i.charges_personnel / 70000, 0) as estimated_employees,
+
+ -- Key ratios
+ round(i.resultat_net / (i.charges_personnel / 70000), 0) as profit_per_employee,
+ round(i.chiffre_affaires / (i.charges_personnel / 70000), 0) as revenue_per_employee,
+ round(i.resultat_net / i.chiffre_affaires * 100, 1) as margin_pct
+
+ FROM file('data/parquet/inpi_comptes_2022.parquet') i
+ JOIN file('data/parquet/sirene_unites_legales.parquet') s
+ ON i.siren = s.siren
+ WHERE i.charges_personnel > 700000 -- At least ~10 employees
+ AND i.resultat_net > 0 -- Profitable
+ AND i.resultat_net < i.chiffre_affaires -- Exclude holding companies (dividend income)
+ AND s.activite_principale NOT IN ('70.10Z', '64.20Z', '64.30Z') -- Exclude holdings
+ AND i.resultat_net / i.chiffre_affaires BETWEEN 0.05 AND 0.50 -- Realistic margins
+ ORDER BY profit_per_employee DESC
+ LIMIT 50
+""", 'DataFrame')
+```
+
+### Why Filter `resultat_net < chiffre_affaires`?
+
+Holding companies receive **dividends from subsidiaries** which appear in `resultat_net` but not in `chiffre_affaires`:
+
+| Company | Revenue | Profit | What's happening |
+|---------|---------|--------|------------------|
+| BOLLORE SE | 1.7M€ | 43M€ | Dividends from subsidiaries |
+| VINCI (holding) | 0.2M€ | 29M€ | Same - not operating profit |
+
+These aren't real "gems" - they're financial structures with a few HQ staff.
+
+## Limitations of Payroll-Based Estimates
+
+### The Fundamental Problem: We Can't Find "Small Teams"
+
+**The goal**: Find companies with few employees but high revenue/profit (productive small teams).
+
+**The problem**: We don't have real employee counts (SIRENE is 94% unknown).
+
+**The attempted workaround**: Estimate employees from payroll (`charges_personnel / 70K`).
+
+**Why it doesn't work**: The math cancels out:
+
+```
+profit_per_employee = profit / (payroll / 70K)
+ = profit × 70K / payroll
+ = 70K × (profit / payroll)
+```
+
+This is just `profit / payroll` multiplied by a constant. The 70K assumption adds no information - it just changes units.
+
+### The Bias Problem
+
+| Company Type | Real Salary | Our 70K Estimate | Result |
+|--------------|-------------|------------------|--------|
+| Low-wage (retail, agriculture) | 50K€ | Undercounts employees | **Looks like a "gem"** ✓ |
+| High-wage (tech, pharma) | 100K€ | Overcounts employees | **Looks mediocre** ✗ |
+
+**We're biased toward finding:**
+- Low-wage businesses (not gems, just cheap labor)
+- Capital-intensive businesses (profit from assets, not people)
+
+**We MISS actual gems** - tech companies where 5 highly-paid people generate huge profits.
+
+### What We Can Actually Measure
+
+Without real employee counts, the only unbiased metrics are:
+
+| Metric | Formula | What it tells you |
+|--------|---------|-------------------|
+| **Profit margin** | `profit / revenue` | Business efficiency |
+| **Profit/payroll** | `profit / charges_personnel` | Return on labor cost |
+| **Revenue** | `chiffre_affaires` | Business scale |
+
+These don't tell you "profit per employee" - they tell you "how profitable is this business relative to its costs".
+
+### When Payroll IS Useful
+
+Payroll-based filtering is still useful for:
+
+1. **Detecting holding companies**: If `profit > revenue`, the company is receiving dividends, not operating
+2. **Filtering out tiny companies**: `payroll > 100K` means at least 1-2 real employees
+3. **Rough size bucketing**: `payroll 500K-2M` roughly means 7-30 employees (±50% error)
+
+### Group vs Entity Mismatch
+
+SIRENE `tranche_effectifs` counts **group employees**, while INPI `charges_personnel` is **entity-only**:
+
+```
+SANOFI group:
+├── SANOFI SA (parent) → SIRENE: 10000+ (group) | payroll: 14M€ (~200 HQ staff)
+├── SANOFI CHIMIE → SIRENE: part of group | payroll: 2.8M€ (~40 employees)
+└── SANOFI PASTEUR EUROPE → SIRENE: "02" (wrong!) | payroll: 7.3M€ (~104 employees)
+```
+
+### Bottom Line
+
+**To find "small productive teams", you need actual employee counts.** This data doesn't reliably have them.
+
+What you CAN find:
+- High-margin businesses (`profit / revenue > 15%`)
+- High profit-per-payroll businesses (efficient labor use, but biased toward low-wage sectors)
+- Specific sectors where data is good (medical labs, software publishing)
+
+## Limitations
+
+| Issue | Impact |
+|-------|--------|
+| **75% missing revenue** | Most small companies use confidential filings |
+| **90% unknown employees** | `tranche_effectifs = 'NN'` for most companies |
+| **Payroll varies by sector** | 50K-100K per employee depending on wages |
+| **Group vs entity mismatch** | SIRENE counts group, INPI is entity-level |
+| **2023 incomplete** | Only 1.2M records vs 2.2M in 2022 |
+
+## How Data Was Produced
+
+| Source | Method |
+|--------|--------|
+| **SIRENE** | Downloaded from data.gouv.fr (Parquet), exported |
+| **INPI** | Downloaded 1,297 7z archives from data.cquest.org mirror, extracted XML with 12 workers |
+| **BODACC** | API calls to OpenDataSoft |
+
+```bash
+# Regenerate INPI (takes ~1 hour)
+uv run python extract_inpi.py
+```
+
+## File Structure
+
+```
+data/parquet/
+├── sirene_unites_legales.parquet (716MB, 29M companies)
+├── sirene_etablissements.parquet (1.9GB, 42M establishments)
+├── inpi_comptes_2020.parquet (72MB, 1.4M accounts)
+├── inpi_comptes_2021.parquet (93MB, 1.9M accounts)
+├── inpi_comptes_2022.parquet (106MB, 2.2M accounts)
+├── inpi_comptes_2023.parquet (55MB, 1.2M accounts)
+└── bodacc_annonces.parquet (17MB, 50K announcements)
+```
+
+## Data Dictionary
+
+### SIRENE - Unités Légales (29M companies, 716MB)
+
+Legal entities registered in France.
+
+| Column | Type | Coverage | Description |
+|--------|------|----------|-------------|
+| `siren` | String | 100% | 9-digit company identifier |
+| `statut_diffusion` | String | 100% | O=public, P=private |
+| `date_creation` | Date | 100% | Company creation date |
+| `sigle` | String | 5% | Company acronym |
+| `denomination` | String | **53%** | Company name (47% are individuals) |
+| `denomination_usuelle_1/2/3` | String | <1% | Alternative names |
+| `prenom` | String | 47% | First name (for individuals) |
+| `nom` | String | 47% | Last name (for individuals) |
+| `categorie_juridique` | String | 100% | Legal form code (5710=SAS, 5499=SARL...) |
+| `activite_principale` | String | **99.9%** | NAF code (industry classification) |
+| `nomenclature_activite` | String | 100% | NAF version (NAFRev2) |
+| `tranche_effectifs` | String | **6%** | Employee bracket - **UNRELIABLE** |
+| `annee_effectifs` | Int | 6% | Year of employee declaration |
+| `caractere_employeur` | String | 35% | O=employer, N=no employees |
+| `categorie_entreprise` | String | **36%** | PME/ETI/GE classification |
+| `annee_categorie_entreprise` | Int | 36% | Year of category |
+| `economie_sociale_solidaire` | String | 3% | O/N - social economy |
+| `societe_mission` | String | <1% | O/N - mission-driven company |
+| `etat_administratif` | String | 100% | A=active (58%), C=closed |
+| `date_cessation` | Date | 42% | Closure date |
+| `date_derniere_mise_a_jour` | DateTime | 100% | Last update |
+| `_loaded_at` | DateTime | 100% | ETL timestamp |
+| `_source_file` | String | 100% | Source file name |
+
+**Key limitations:**
+- 94% have unknown employee count (`tranche_effectifs = 'NN'`)
+- 47% are individuals (no company name, use `prenom`/`nom`)
+- 42% are closed businesses
+
+### SIRENE - Établissements (42M establishments, 1.9GB)
+
+Physical locations/branches of companies.
+
+| Column | Type | Coverage | Description |
+|--------|------|----------|-------------|
+| `siret` | String | 100% | 14-digit establishment ID (siren + nic) |
+| `siren` | String | 100% | Parent company |
+| `nic` | String | 100% | 5-digit establishment number |
+| `statut_diffusion` | String | 100% | O=public, P=private |
+| `date_creation` | Date | 100% | Establishment creation |
+| `denomination_usuelle` | String | 3% | Trade name |
+| `enseigne_1/2/3` | String | **19%** | Shop sign/brand |
+| `activite_principale` | String | **99.9%** | NAF code |
+| `nomenclature_activite` | String | 100% | NAF version |
+| `activite_principale_registre_metiers` | String | 2% | Craft register code |
+| `etablissement_siege` | Bool | 100% | true=headquarters |
+| `tranche_effectifs` | String | **5.5%** | Employee bracket |
+| `annee_effectifs` | Int | 5.5% | Year of employee data |
+| `caractere_employeur` | String | 30% | O=employer |
+| `complement_adresse` | String | 15% | Address complement |
+| `numero_voie` | String | 70% | Street number |
+| `indice_repetition` | String | 2% | B, TER, etc. |
+| `type_voie` | String | 75% | RUE, AVENUE, etc. |
+| `libelle_voie` | String | 80% | Street name |
+| `code_postal` | String | **99.3%** | Postal code |
+| `libelle_commune` | String | 99% | City name |
+| `libelle_commune_etranger` | String | <1% | Foreign city |
+| `code_commune` | String | 99% | INSEE commune code |
+| `code_cedex` | String | 5% | CEDEX code |
+| `libelle_cedex` | String | 5% | CEDEX label |
+| `code_pays_etranger` | String | <1% | Foreign country code |
+| `libelle_pays_etranger` | String | <1% | Foreign country name |
+| `departement` | String | 99% | Department code |
+| `region` | String | 99% | Region code |
+| `etat_administratif` | String | 100% | A=active (40%), F=closed |
+| `date_cessation` | Date | 60% | Closure date |
+| `date_derniere_mise_a_jour` | DateTime | 100% | Last update |
+| `_loaded_at` | DateTime | 100% | ETL timestamp |
+| `_source_file` | String | 100% | Source file |
+
+**Key limitations:**
+- 60% are closed establishments
+- Employee data even worse (5.5% known)
+- Use for address/location data, not employee counts
+
+### INPI - Comptes Annuels (6.6M accounts, 325MB)
+
+Annual financial accounts filed with commercial courts.
+
+| Column | Type | Coverage | Description |
+|--------|------|----------|-------------|
+| `siren` | String | 100% | Company identifier |
+| `date_cloture` | String | 100% | Fiscal year end (YYYY-MM-DD) |
+| `annee_cloture` | Int | 100% | Fiscal year |
+| `duree_exercice` | Int | 95% | Period length in months |
+| `type_comptes` | String | 100% | C=complet, S=simplifié, K=consolidé |
+| `date_depot` | String | 100% | Filing date |
+| `code_greffe` | String | 100% | Court code |
+| `confidentialite` | String | 100% | 1=confidential (**55%**), 0=public |
+| **Balance Sheet - Assets** |
+| `immobilisations_incorporelles` | Float | 20% | Intangible assets |
+| `immobilisations_corporelles` | Float | 20% | Tangible assets |
+| `immobilisations_financieres` | Float | 20% | Financial assets |
+| `actif_immobilise_net` | Float | 20% | Total fixed assets |
+| `stocks` | Float | 15% | Inventory |
+| `creances_clients` | Float | 20% | Accounts receivable |
+| `disponibilites` | Float | 20% | Cash & equivalents |
+| `actif_circulant` | Float | 20% | Current assets |
+| `total_actif` | Float | 22% | Total assets |
+| **Balance Sheet - Liabilities** |
+| `capital_social` | Float | 25% | Share capital |
+| `reserves` | Float | 20% | Retained earnings |
+| `resultat_exercice` | Float | 25% | Net income (same as resultat_net) |
+| `capitaux_propres` | Float | **44%** | Shareholders' equity |
+| `dettes` | Float | 20% | Total debt |
+| `total_passif` | Float | 22% | Total liabilities |
+| **Income Statement** |
+| `chiffre_affaires` | Float | **23%** | Revenue |
+| `charges_personnel` | Float | **23%** | Payroll (salaries + social charges) |
+| `resultat_exploitation` | Float | 20% | Operating income |
+| `resultat_financier` | Float | 15% | Financial result |
+| `resultat_exceptionnel` | Float | 10% | Exceptional items |
+| `resultat_net` | Float | **29%** | Net profit/loss |
+
+**Account types:**
+| Code | Name | Count | Has Revenue |
+|------|------|-------|-------------|
+| C | Complet (full) | 1.5M | 25% |
+| S | Simplifié (simplified) | 700K | 18% |
+| K | Consolidé (consolidated) | 4K | **92%** |
+
+**Key limitations:**
+- **55% file confidential accounts** (no financials visible)
+- Only 23% have visible revenue
+- Small companies hide numbers; large companies are visible
+- Best sector coverage: medical labs, software publishing
+
+### BODACC - Annonces (50K announcements, 17MB)
+
+Legal announcements (company events).
+
+| Column | Type | Coverage | Description |
+|--------|------|----------|-------------|
+| `id` | String | 100% | Announcement ID |
+| `siren` | String | **99.6%** | Company identifier |
+| `numero_annonce` | String | 100% | Announcement number |
+| `date_parution` | Date | 100% | Publication date |
+| `numero_parution` | String | 100% | Publication number |
+| `type_bulletin` | String | 100% | Bulletin type |
+| `famille` | String | 100% | Event type (see below) |
+| `nature` | String | 80% | Event nature |
+| `denomination` | String | **0%** | Company name - **EMPTY** |
+| `forme_juridique` | String | 50% | Legal form |
+| `administration` | String | 30% | Management info |
+| `adresse` | String | 70% | Address |
+| `code_postal` | String | 70% | Postal code |
+| `ville` | String | 70% | City |
+| `activite` | String | 40% | Activity description |
+| `details` | JSON | 80% | Structured details |
+| `type_procedure` | String | 7% | Procedure type (bankruptcy) |
+| `date_jugement` | Date | 7% | Judgment date |
+| `tribunal` | String | 50% | Court name |
+| `date_cloture_exercice` | Date | 40% | Fiscal year end |
+| `type_depot` | String | 40% | Filing type |
+| `contenu_annonce` | String | 80% | Full announcement text |
+| `_loaded_at` | DateTime | 100% | ETL timestamp |
+| `_source_file` | String | 100% | Source file |
+
+**Event types (`famille`):**
+| Type | Count | Description |
+|------|-------|-------------|
+| dpc | 19K | Dépôt des comptes (account filings) |
+| modification | 9K | Company changes |
+| creation | 9K | New companies |
+| radiation | 7K | Company closures |
+| collective | 3K | Bankruptcy proceedings |
+| vente | 681 | Business sales |
+| immatriculation | 714 | Registrations |
+
+**Key limitations:**
+- Only 50K records (small sample)
+- `denomination` is empty - must join with SIRENE
+- Limited to recent announcements
+
+## Data Quality Summary
+
+| Issue | Impact |
+|-------|--------|
+| **77% revenue hidden** | Most INPI filings are confidential |
+| **94% employees unknown** | SIRENE `tranche_effectifs` useless |
+| **55% INPI confidential** | Small companies hide numbers |
+| **BODACC names empty** | Must join with SIRENE for company names |
+| **60% establishments closed** | Filter by `etat_administratif = 'A'` |
+
+## Tech Stack
+
+- **chdb**: ClickHouse engine for instant SQL on Parquet
+- **pandas/pyarrow**: DataFrame operations
+- **uv**: Fast Python package manager
diff --git a/france-market-scanner/analyze_ratios.py b/france-market-scanner/analyze_ratios.py
new file mode 100644
index 0000000..13b283f
--- /dev/null
+++ b/france-market-scanner/analyze_ratios.py
@@ -0,0 +1,72 @@
+"""Find companies with best profit/employee ratios using payroll-based estimates."""
+import duckdb
+
+# Connect to parquet files
+con = duckdb.connect()
+
+query = """
+WITH latest_accounts AS (
+ SELECT
+ siren,
+ date_cloture,
+ chiffre_affaires,
+ charges_personnel,
+ resultat_net,
+ resultat_exploitation,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 100000 -- At least 100K payroll (filter tiny companies)
+ AND resultat_net IS NOT NULL
+ AND chiffre_affaires > 0
+),
+company_info AS (
+ SELECT
+ siren,
+ denomination as nom,
+ tranche_effectifs as sirene_effectif_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ a.siren,
+ c.nom,
+ a.date_cloture,
+
+ -- Raw financials
+ round(a.chiffre_affaires / 1e6, 2) as ca_millions,
+ round(a.charges_personnel / 1e6, 2) as payroll_millions,
+ round(a.resultat_net / 1e6, 2) as resultat_millions,
+
+ -- Estimated employees from payroll (avg 70K€ total cost per person in France)
+ round(a.charges_personnel / 70000, 0) as estimated_employees,
+
+ -- SIRENE bracket for comparison
+ c.sirene_effectif_code,
+
+ -- Key ratios
+ round(a.resultat_net / (a.charges_personnel / 70000), 0) as profit_per_employee,
+ round(a.chiffre_affaires / (a.charges_personnel / 70000), 0) as revenue_per_employee,
+
+ -- Profit margin
+ round(100 * a.resultat_net / a.chiffre_affaires, 1) as margin_pct
+
+FROM latest_accounts a
+LEFT JOIN company_info c ON a.siren = c.siren
+WHERE a.rn = 1
+ AND a.resultat_net > 0 -- Profitable only
+ AND (a.charges_personnel / 70000) >= 5 -- At least ~5 employees
+ORDER BY profit_per_employee DESC
+LIMIT 50
+"""
+
+print("Top 50 companies by profit per employee (using payroll-based estimates)")
+print("=" * 100)
+print()
+
+df = con.execute(query).df()
+print(df.to_string())
+
+print("\n\n")
+print("Legend:")
+print(" - estimated_employees = charges_personnel / 70,000€ (avg French employer cost)")
+print(" - profit_per_employee = resultat_net / estimated_employees")
+print(" - SIRENE codes: 00=0, 01=1-2, 02=3-5, 03=6-9, 11=10-19, 12=20-49, etc.")
diff --git a/france-market-scanner/analyze_ratios_v2.py b/france-market-scanner/analyze_ratios_v2.py
new file mode 100644
index 0000000..c51bf3c
--- /dev/null
+++ b/france-market-scanner/analyze_ratios_v2.py
@@ -0,0 +1,88 @@
+"""Find REAL operating companies with best profit/employee ratios.
+
+Filters out:
+- Holding companies (revenue << profit = dividend income)
+- Implausible SIRENE brackets
+"""
+import duckdb
+
+con = duckdb.connect()
+
+query = """
+WITH latest_accounts AS (
+ SELECT
+ siren,
+ date_cloture,
+ chiffre_affaires,
+ charges_personnel,
+ resultat_net,
+ resultat_exploitation,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 500000 -- At least 500K payroll (~7 employees)
+ AND resultat_net IS NOT NULL
+ AND resultat_net > 0
+ AND chiffre_affaires > 1000000 -- At least 1M revenue
+),
+company_info AS (
+ SELECT
+ siren,
+ denomination as nom,
+ tranche_effectifs as sirene_effectif_code,
+ activite_principale as naf_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ a.siren,
+ c.nom,
+ c.naf_code,
+ a.date_cloture,
+
+ -- Raw financials
+ round(a.chiffre_affaires / 1e6, 2) as ca_millions,
+ round(a.charges_personnel / 1e6, 2) as payroll_millions,
+ round(a.resultat_net / 1e6, 2) as resultat_millions,
+ round(a.resultat_exploitation / 1e6, 2) as rex_millions,
+
+ -- Estimated employees
+ round(a.charges_personnel / 70000, 0) as estimated_employees,
+
+ -- Key ratios
+ round(a.resultat_net / (a.charges_personnel / 70000), 0) as profit_per_employee,
+ round(a.chiffre_affaires / (a.charges_personnel / 70000), 0) as revenue_per_employee,
+ round(100 * a.resultat_net / a.chiffre_affaires, 1) as margin_pct,
+
+ -- SIRENE bracket
+ c.sirene_effectif_code
+
+FROM latest_accounts a
+LEFT JOIN company_info c ON a.siren = c.siren
+WHERE a.rn = 1
+ -- FILTER OUT HOLDING COMPANIES: profit should be < revenue for operating business
+ AND a.resultat_net < a.chiffre_affaires
+ -- Filter out obvious holding company NAF codes
+ AND c.naf_code NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z')
+ -- At least 10 employees estimated
+ AND (a.charges_personnel / 70000) >= 10
+ -- Profit margin between 5% and 50% (realistic for operating company)
+ AND (100.0 * a.resultat_net / a.chiffre_affaires) BETWEEN 5 AND 50
+ORDER BY profit_per_employee DESC
+LIMIT 50
+"""
+
+print("Top 50 OPERATING companies by profit per employee")
+print("(Excluding holding companies and unrealistic margins)")
+print("=" * 120)
+print()
+
+df = con.execute(query).df()
+print(df.to_string())
+
+print("\n")
+print("Filters applied:")
+print(" - Revenue > 1M€")
+print(" - Payroll > 500K€ (min ~7 employees)")
+print(" - Profit < Revenue (excludes holding companies living on dividends)")
+print(" - NAF code not in holding/investment codes (70.10Z, 64.20Z, 64.30Z)")
+print(" - Profit margin 5%-50% (realistic operating range)")
+print(" - At least 10 estimated employees")
diff --git a/france-market-scanner/check_sirene_accuracy.py b/france-market-scanner/check_sirene_accuracy.py
new file mode 100644
index 0000000..d16a997
--- /dev/null
+++ b/france-market-scanner/check_sirene_accuracy.py
@@ -0,0 +1,124 @@
+"""Check: Do companies with known SIRENE codes have matching payroll estimates?
+
+Hypothesis: Well-managed companies keep SIRENE updated, so when code != NN,
+it should roughly match payroll-based estimate.
+"""
+import duckdb
+
+con = duckdb.connect()
+
+# SIRENE bracket midpoints
+BRACKET_MIDPOINTS = {
+ '00': 0,
+ '01': 1.5, # 1-2
+ '02': 4, # 3-5
+ '03': 7.5, # 6-9
+ '11': 14.5, # 10-19
+ '12': 34.5, # 20-49
+ '21': 74.5, # 50-99
+ '22': 149.5, # 100-199
+ '31': 249.5, # 200-249
+ '32': 374.5, # 250-499
+ '41': 749.5, # 500-999
+ '42': 1499.5, # 1000-1999
+ '51': 3499.5, # 2000-4999
+ '52': 7499.5, # 5000-9999
+ '53': 10000, # 10000+
+}
+
+query = """
+WITH latest AS (
+ SELECT siren, charges_personnel,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0
+),
+combined AS (
+ SELECT
+ a.siren,
+ s.denomination,
+ s.tranche_effectifs as sirene_code,
+ a.charges_personnel,
+ round(a.charges_personnel / 70000, 0) as est_employees_70k,
+ round(a.charges_personnel / 50000, 0) as est_employees_50k,
+ round(a.charges_personnel / 100000, 0) as est_employees_100k
+ FROM latest a
+ JOIN (SELECT siren, denomination, tranche_effectifs
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')) s
+ ON a.siren = s.siren
+ WHERE a.rn = 1
+ AND s.tranche_effectifs NOT IN ('NN', '')
+ AND s.tranche_effectifs IS NOT NULL
+)
+SELECT * FROM combined
+WHERE est_employees_70k > 0
+"""
+
+df = con.execute(query).df()
+
+print("=" * 100)
+print("COMPARING SIRENE BRACKETS VS PAYROLL-BASED ESTIMATES")
+print("=" * 100)
+
+# Add SIRENE midpoint
+df['sirene_midpoint'] = df['sirene_code'].map(BRACKET_MIDPOINTS)
+df = df.dropna(subset=['sirene_midpoint'])
+
+# Calculate ratio: payroll estimate / SIRENE midpoint
+df['ratio_70k'] = df['est_employees_70k'] / df['sirene_midpoint']
+df['ratio_50k'] = df['est_employees_50k'] / df['sirene_midpoint']
+df['ratio_100k'] = df['est_employees_100k'] / df['sirene_midpoint']
+
+# Filter out zeros
+df = df[df['sirene_midpoint'] > 0]
+
+print(f"\nTotal companies with known SIRENE code: {len(df):,}")
+print()
+
+# Accuracy by bracket
+print("ACCURACY BY SIRENE BRACKET (ratio = payroll_estimate / sirene_midpoint)")
+print("-" * 80)
+print("If SIRENE is accurate: ratio should be ~1.0")
+print("If ratio >> 1: SIRENE is UNDERSTATING employees (stale data)")
+print("If ratio << 1: SIRENE is OVERSTATING employees (or high-wage sector)")
+print()
+
+for code in sorted(BRACKET_MIDPOINTS.keys()):
+ subset = df[df['sirene_code'] == code]
+ if len(subset) >= 10:
+ median_ratio = subset['ratio_70k'].median()
+ pct_close = len(subset[(subset['ratio_70k'] > 0.5) & (subset['ratio_70k'] < 2)]) / len(subset) * 100
+ print(f" {code} ({BRACKET_MIDPOINTS[code]:>6.0f} emp): "
+ f"n={len(subset):>6,} "
+ f"median_ratio={median_ratio:>5.1f} "
+ f"within_2x={pct_close:>5.1f}%")
+
+print()
+print()
+
+# Show extreme mismatches
+print("EXTREME MISMATCHES: SIRENE says small, payroll says big")
+print("-" * 80)
+mismatches = df[(df['sirene_code'].isin(['01', '02', '03'])) & (df['est_employees_70k'] > 50)]
+mismatches = mismatches.sort_values('est_employees_70k', ascending=False).head(20)
+print(mismatches[['siren', 'denomination', 'sirene_code', 'est_employees_70k', 'charges_personnel']].to_string())
+
+print()
+print()
+
+# Show well-matched examples
+print("WELL-MATCHED: SIRENE and payroll agree (ratio 0.8-1.2)")
+print("-" * 80)
+matched = df[(df['ratio_70k'] > 0.8) & (df['ratio_70k'] < 1.2) & (df['est_employees_70k'] > 10)]
+matched = matched.sample(min(20, len(matched)))
+print(matched[['siren', 'denomination', 'sirene_code', 'sirene_midpoint', 'est_employees_70k', 'ratio_70k']].to_string())
+
+print()
+print()
+
+# Summary stats
+print("SUMMARY: How often does SIRENE match payroll estimate?")
+print("-" * 80)
+for threshold in [0.5, 1.0, 2.0]:
+ within = len(df[(df['ratio_70k'] > 1/threshold) & (df['ratio_70k'] < threshold)]) / len(df) * 100
+ print(f" Within {threshold}x: {within:.1f}%")
diff --git a/france-market-scanner/cli.py b/france-market-scanner/cli.py
new file mode 100644
index 0000000..a159ef2
--- /dev/null
+++ b/france-market-scanner/cli.py
@@ -0,0 +1,526 @@
+#!/usr/bin/env python3
+"""France Market Scanner CLI - Bulk French company data collection."""
+import sys
+from pathlib import Path
+
+# Add src to path
+sys.path.insert(0, str(Path(__file__).parent))
+
+import click
+from rich.console import Console
+from rich.table import Table
+from loguru import logger
+
+from src.core.config import load_config, get_project_root
+from src.core.database import DatabaseManager
+
+console = Console()
+
+
+def setup_logging(verbose: bool = False):
+ """Configure logging."""
+ logger.remove()
+ level = "DEBUG" if verbose else "INFO"
+ logger.add(
+ sys.stderr,
+ level=level,
+ format="{level: <8} | {name}:{function} - {message}",
+ )
+
+
+@click.group()
+@click.option("--config", "-c", default=None, help="Path to config file")
+@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
+@click.pass_context
+def cli(ctx, config, verbose):
+ """France Market Scanner - Bulk French company data collection.
+
+ Collect and analyze French company data from public sources:
+ - SIRENE (INSEE): Company registry
+ - INPI: Annual accounts
+ - BODACC: Legal announcements
+ """
+ ctx.ensure_object(dict)
+ setup_logging(verbose)
+
+ try:
+ ctx.obj["config"] = load_config(config)
+ except FileNotFoundError as e:
+ console.print(f"[red]Error:[/red] {e}")
+ sys.exit(1)
+
+
+# =============================================================================
+# Database Commands
+# =============================================================================
+
+@cli.command("init-db")
+@click.option("--force", is_flag=True, help="Drop existing tables")
+@click.pass_context
+def init_db(ctx, force):
+ """Initialize the DuckDB database schema."""
+ config = ctx.obj["config"]
+ db_path = config["database"]["path"]
+
+ if force:
+ console.print("[yellow]Warning:[/yellow] Force mode - existing tables will be dropped")
+ if not click.confirm("Continue?"):
+ return
+
+ with DatabaseManager(db_path) as db:
+ db.init_schema(force=force)
+
+ console.print(f"[green]Database initialized:[/green] {db_path}")
+
+
+@cli.command("db-info")
+@click.pass_context
+def db_info(ctx):
+ """Show database information and statistics."""
+ config = ctx.obj["config"]
+ db_path = config["database"]["path"]
+
+ if not Path(db_path).exists():
+ console.print(f"[red]Database not found:[/red] {db_path}")
+ console.print("Run 'init-db' first to create the database.")
+ return
+
+ with DatabaseManager(db_path) as db:
+ stats = db.get_stats()
+
+ # Display stats
+ table = Table(title="Database Statistics")
+ table.add_column("Table", style="cyan")
+ table.add_column("Rows", justify="right", style="green")
+
+ for table_name, count in stats.items():
+ if table_name != "last_loads":
+ table.add_row(table_name, f"{count:,}")
+
+ console.print(table)
+
+ # Last loads
+ if stats.get("last_loads"):
+ console.print("\n[bold]Last successful loads:[/bold]")
+ for source, date in stats["last_loads"].items():
+ console.print(f" {source}: {date}")
+
+
+@cli.command("stats")
+@click.pass_context
+def stats(ctx):
+ """Show quick statistics about loaded data."""
+ config = ctx.obj["config"]
+ db_path = config["database"]["path"]
+
+ if not Path(db_path).exists():
+ console.print(f"[red]Database not found:[/red] {db_path}")
+ return
+
+ with DatabaseManager(db_path) as db:
+ # Companies by status
+ try:
+ result = db.fetchall("""
+ SELECT
+ etat_administratif,
+ COUNT(*) as count
+ FROM sirene_unites_legales
+ GROUP BY etat_administratif
+ """)
+ console.print("\n[bold]Companies by status:[/bold]")
+ for row in result:
+ status = "Active" if row[0] == "A" else "Closed"
+ console.print(f" {status}: {row[1]:,}")
+ except Exception:
+ console.print("[yellow]No SIRENE data loaded yet[/yellow]")
+
+ # Financial data
+ try:
+ result = db.fetchone("""
+ SELECT
+ COUNT(DISTINCT siren) as companies,
+ COUNT(*) as accounts,
+ MIN(annee_cloture) as min_year,
+ MAX(annee_cloture) as max_year
+ FROM inpi_compte_resultat
+ """)
+ if result and result[0] > 0:
+ console.print(f"\n[bold]Financial data:[/bold]")
+ console.print(f" Companies with accounts: {result[0]:,}")
+ console.print(f" Total accounts: {result[1]:,}")
+ console.print(f" Years: {result[2]} - {result[3]}")
+ except Exception:
+ pass
+
+
+# =============================================================================
+# SIRENE Commands
+# =============================================================================
+
+@cli.group()
+def sirene():
+ """SIRENE data commands (INSEE company registry)."""
+ pass
+
+
+@sirene.command("download")
+@click.option("--output", "-o", default=None, help="Output directory")
+@click.option(
+ "--type", "file_type",
+ type=click.Choice(["all", "unites", "etablissements"]),
+ default="all",
+ help="Which files to download"
+)
+@click.pass_context
+def sirene_download(ctx, output, file_type):
+ """Download SIRENE bulk files from data.gouv.fr."""
+ from src.extractors.sirene import SireneExtractor
+
+ config = ctx.obj["config"]
+ output_dir = Path(output or config["downloads"]["directory"]) / "sirene"
+
+ console.print(f"[cyan]Downloading SIRENE data to:[/cyan] {output_dir}")
+
+ extractor = SireneExtractor(config)
+ extractor.download(output_dir, file_type)
+
+ console.print("[green]Download complete![/green]")
+
+
+@sirene.command("load")
+@click.option("--source", "-s", default=None, help="Source directory with Parquet files")
+@click.option("--type", "file_type",
+ type=click.Choice(["all", "unites", "etablissements"]),
+ default="all",
+ help="Which data to load"
+)
+@click.pass_context
+def sirene_load(ctx, source, file_type):
+ """Load SIRENE data into DuckDB."""
+ from src.extractors.sirene import SireneExtractor
+
+ config = ctx.obj["config"]
+ source_dir = Path(source or config["downloads"]["directory"]) / "sirene"
+ db_path = config["database"]["path"]
+
+ if not source_dir.exists():
+ console.print(f"[red]Source directory not found:[/red] {source_dir}")
+ console.print("Run 'sirene download' first.")
+ return
+
+ console.print(f"[cyan]Loading SIRENE data from:[/cyan] {source_dir}")
+
+ extractor = SireneExtractor(config)
+ with DatabaseManager(db_path) as db:
+ extractor.load(db, source_dir, file_type)
+
+ console.print("[green]Load complete![/green]")
+
+
+@sirene.command("sync")
+@click.option("--output", "-o", default=None, help="Output directory")
+@click.pass_context
+def sirene_sync(ctx, output):
+ """Download and load SIRENE data (combined operation)."""
+ ctx.invoke(sirene_download, output=output, file_type="all")
+ ctx.invoke(sirene_load, source=output, file_type="all")
+
+
+# =============================================================================
+# INPI Commands
+# =============================================================================
+
+@cli.group()
+def inpi():
+ """INPI data commands (annual accounts)."""
+ pass
+
+
+@inpi.command("download")
+@click.option("--output", "-o", default=None, help="Output directory")
+@click.option("--year", "-y", type=int, help="Specific year to download")
+@click.option("--years", help="Year range (e.g., '2020-2024')")
+@click.option("--source", type=click.Choice(["mirror", "sftp"]), default="mirror",
+ help="Data source (mirror recommended, sftp often unavailable)")
+@click.option("--max-files", type=int, default=None, help="Max files per year (for testing)")
+@click.pass_context
+def inpi_download(ctx, output, year, years, source, max_files):
+ """Download INPI annual accounts data.
+
+ The mirror source (data.cquest.org) is recommended as the official SFTP
+ is often unavailable. Mirror has data from 2017-2023.
+ """
+ from src.extractors.inpi import INPIExtractor
+
+ config = ctx.obj["config"]
+ output_dir = Path(output or config["downloads"]["directory"]) / "inpi"
+
+ # Determine years to download
+ if year:
+ years_list = [year]
+ elif years:
+ start, end = map(int, years.split("-"))
+ years_list = list(range(start, end + 1))
+ else:
+ years_list = config.get("inpi", {}).get("years_to_sync", [2023])
+
+ console.print(f"[cyan]Downloading INPI data for years:[/cyan] {years_list}")
+ console.print(f"[cyan]Output directory:[/cyan] {output_dir}")
+ console.print(f"[cyan]Source:[/cyan] {source}")
+
+ extractor = INPIExtractor(config)
+
+ if source == "mirror":
+ extractor.download_mirror(output_dir, years_list, max_files_per_year=max_files)
+ else:
+ extractor.download(output_dir, years_list)
+
+ console.print("[green]Download complete![/green]")
+
+
+@inpi.command("load")
+@click.option("--source", "-s", default=None, help="Source directory")
+@click.option("--year", "-y", type=int, help="Specific year to load")
+@click.option("--format", "data_format", type=click.Choice(["xml", "json"]), default="xml",
+ help="Data format (xml for mirror, json for sftp)")
+@click.pass_context
+def inpi_load(ctx, source, year, data_format):
+ """Load INPI data into DuckDB.
+
+ Use --format xml for data downloaded from the mirror (7z archives with XML).
+ Use --format json for data downloaded from SFTP (zip archives with JSON).
+ """
+ from src.extractors.inpi import INPIExtractor
+
+ config = ctx.obj["config"]
+ source_dir = Path(source or config["downloads"]["directory"]) / "inpi"
+ db_path = config["database"]["path"]
+
+ if not source_dir.exists():
+ console.print(f"[red]Source directory not found:[/red] {source_dir}")
+ console.print("Run 'inpi download' first.")
+ return
+
+ console.print(f"[cyan]Loading INPI data from:[/cyan] {source_dir}")
+ console.print(f"[cyan]Format:[/cyan] {data_format}")
+
+ extractor = INPIExtractor(config)
+ with DatabaseManager(db_path) as db:
+ if data_format == "xml":
+ stats = extractor.load_xml(db, source_dir, year)
+ else:
+ stats = extractor.load(db, source_dir, year)
+
+ console.print(f"[green]Load complete![/green] Loaded: {stats}")
+
+
+@inpi.command("sync")
+@click.option("--output", "-o", default=None, help="Output directory")
+@click.option("--years", default="2020-2023", help="Year range (2017-2023 available)")
+@click.option("--max-files", type=int, default=None, help="Max files per year (for testing)")
+@click.pass_context
+def inpi_sync(ctx, output, years, max_files):
+ """Download and load INPI data (combined operation).
+
+ Uses the data.cquest.org mirror by default (recommended).
+ """
+ ctx.invoke(inpi_download, output=output, years=years, source="mirror", max_files=max_files)
+ ctx.invoke(inpi_load, source=output, data_format="xml")
+
+
+# =============================================================================
+# BODACC Commands
+# =============================================================================
+
+@cli.group()
+def bodacc():
+ """BODACC data commands (legal announcements)."""
+ pass
+
+
+@bodacc.command("download")
+@click.option("--output", "-o", default=None, help="Output directory")
+@click.option("--year", "-y", type=int, help="Specific year")
+@click.option("--days", "-d", type=int, default=30, help="Number of days to fetch (API mode)")
+@click.option("--source", type=click.Choice(["api", "ftps"]), default="api",
+ help="Data source (API for recent, FTPS for bulk)")
+@click.pass_context
+def bodacc_download(ctx, output, year, days, source):
+ """Download BODACC announcements."""
+ from src.extractors.bodacc import BODACCExtractor
+
+ config = ctx.obj["config"]
+ output_dir = Path(output or config["downloads"]["directory"]) / "bodacc"
+
+ console.print(f"[cyan]Downloading BODACC data to:[/cyan] {output_dir}")
+
+ extractor = BODACCExtractor(config)
+ extractor.download(output_dir, year=year, days=days, source=source)
+
+ console.print("[green]Download complete![/green]")
+
+
+@bodacc.command("load")
+@click.option("--source", "-s", default=None, help="Source directory")
+@click.pass_context
+def bodacc_load(ctx, source):
+ """Load BODACC data into DuckDB."""
+ from src.extractors.bodacc import BODACCExtractor
+
+ config = ctx.obj["config"]
+ source_dir = Path(source or config["downloads"]["directory"]) / "bodacc"
+ db_path = config["database"]["path"]
+
+ if not source_dir.exists():
+ console.print(f"[red]Source directory not found:[/red] {source_dir}")
+ console.print("Run 'bodacc download' first.")
+ return
+
+ console.print(f"[cyan]Loading BODACC data from:[/cyan] {source_dir}")
+
+ extractor = BODACCExtractor(config)
+ with DatabaseManager(db_path) as db:
+ extractor.load(db, source_dir)
+
+ console.print("[green]Load complete![/green]")
+
+
+@bodacc.command("sync")
+@click.option("--output", "-o", default=None, help="Output directory")
+@click.option("--days", "-d", type=int, default=30, help="Number of days to fetch")
+@click.pass_context
+def bodacc_sync(ctx, output, days):
+ """Download and load BODACC data (combined operation)."""
+ ctx.invoke(bodacc_download, output=output, days=days)
+ ctx.invoke(bodacc_load, source=output)
+
+
+# =============================================================================
+# Query Commands
+# =============================================================================
+
+@cli.command("search")
+@click.option("--name", "-n", help="Search by company name")
+@click.option("--siren", "-s", help="Search by SIREN")
+@click.option("--naf", help="Filter by NAF/APE code")
+@click.option("--departement", "-d", help="Filter by department")
+@click.option("--limit", "-l", type=int, default=20, help="Max results")
+@click.option("--format", "output_format", type=click.Choice(["table", "json", "csv"]),
+ default="table", help="Output format")
+@click.pass_context
+def search(ctx, name, siren, naf, departement, limit, output_format):
+ """Search for companies."""
+ import json
+
+ config = ctx.obj["config"]
+ db_path = config["database"]["path"]
+
+ if not Path(db_path).exists():
+ console.print(f"[red]Database not found:[/red] {db_path}")
+ return
+
+ # Build query
+ conditions = ["ul.etat_administratif = 'A'"]
+ params = []
+
+ if name:
+ conditions.append("ul.denomination ILIKE ?")
+ params.append(f"%{name}%")
+ if siren:
+ conditions.append("ul.siren = ?")
+ params.append(siren)
+ if naf:
+ conditions.append("ul.activite_principale = ?")
+ params.append(naf)
+ if departement:
+ conditions.append("e.departement = ?")
+ params.append(departement)
+
+ where = " AND ".join(conditions)
+
+ query = f"""
+ SELECT
+ ul.siren,
+ ul.denomination,
+ ul.activite_principale,
+ ul.tranche_effectifs,
+ e.code_postal,
+ e.libelle_commune
+ FROM sirene_unites_legales ul
+ LEFT JOIN sirene_etablissements e
+ ON ul.siren = e.siren AND e.etablissement_siege = 'true'
+ WHERE {where}
+ LIMIT ?
+ """
+ params.append(limit)
+
+ with DatabaseManager(db_path) as db:
+ results = db.fetchall(query, params)
+
+ if not results:
+ console.print("[yellow]No results found[/yellow]")
+ return
+
+ if output_format == "json":
+ data = [
+ {
+ "siren": r[0],
+ "denomination": r[1],
+ "naf": r[2],
+ "effectifs": r[3],
+ "code_postal": r[4],
+ "commune": r[5],
+ }
+ for r in results
+ ]
+ console.print(json.dumps(data, indent=2, ensure_ascii=False))
+ elif output_format == "csv":
+ console.print("siren,denomination,naf,effectifs,code_postal,commune")
+ for r in results:
+ console.print(",".join(str(v or "") for v in r))
+ else:
+ table = Table(title=f"Search Results ({len(results)} companies)")
+ table.add_column("SIREN", style="cyan")
+ table.add_column("Denomination")
+ table.add_column("NAF")
+ table.add_column("Effectifs")
+ table.add_column("CP")
+ table.add_column("Commune")
+
+ for r in results:
+ table.add_row(*(str(v or "-") for v in r))
+
+ console.print(table)
+
+
+@cli.command("export")
+@click.option("--query", "-q", required=True, help="SQL query to export")
+@click.option("--output", "-o", required=True, help="Output file path")
+@click.option("--format", "output_format", type=click.Choice(["parquet", "csv", "json"]),
+ default="parquet", help="Output format")
+@click.pass_context
+def export(ctx, query, output, output_format):
+ """Export query results to file."""
+ config = ctx.obj["config"]
+ db_path = config["database"]["path"]
+ output_path = Path(output)
+
+ if not Path(db_path).exists():
+ console.print(f"[red]Database not found:[/red] {db_path}")
+ return
+
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+
+ with DatabaseManager(db_path) as db:
+ if output_format == "parquet":
+ db.execute(f"COPY ({query}) TO '{output_path}' (FORMAT PARQUET)")
+ elif output_format == "csv":
+ db.execute(f"COPY ({query}) TO '{output_path}' (FORMAT CSV, HEADER)")
+ else:
+ db.execute(f"COPY ({query}) TO '{output_path}' (FORMAT JSON)")
+
+ console.print(f"[green]Exported to:[/green] {output_path}")
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/france-market-scanner/config/config.yaml b/france-market-scanner/config/config.yaml
new file mode 100644
index 0000000..933e1da
--- /dev/null
+++ b/france-market-scanner/config/config.yaml
@@ -0,0 +1,56 @@
+# France Market Scanner Configuration
+
+database:
+ path: data/france_companies.duckdb
+ memory_limit: 4GB
+ threads: 4
+
+logging:
+ level: INFO
+ file: logs/scanner.log
+ format: "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}"
+
+downloads:
+ directory: data/downloads
+ retry_attempts: 3
+ timeout_seconds: 300
+ chunk_size: 8192 # bytes
+
+# SIRENE (INSEE) - French company registry
+sirene:
+ base_url: https://files.data.gouv.fr/insee-sirene/
+ stock_url: https://www.data.gouv.fr/fr/datasets/r/0651fb76-bcf3-4f6a-a38d-bc04fa708576
+ files:
+ unites_legales: StockUniteLegale_utf8.zip
+ etablissements: StockEtablissement_utf8.zip
+ # Alternative: Parquet format (smaller, faster)
+ parquet_base_url: https://object.files.data.gouv.fr/data-pipeline-open/siren/stock/
+ parquet_files:
+ unites_legales: StockUniteLegale_utf8.parquet
+ etablissements: StockEtablissement_utf8.parquet
+ batch_size: 100000
+
+# INPI - Annual accounts (requires free account)
+inpi:
+ api_base: https://data.inpi.fr/api
+ # SFTP for bulk downloads
+ sftp_host: opendata-rncs.inpi.fr
+ sftp_port: 22
+ base_path: /public/IMR_Donnees_Saisies/tc/flux/
+ years_to_sync:
+ - 2020
+ - 2021
+ - 2022
+ - 2023
+ - 2024
+
+# BODACC - Legal announcements
+bodacc:
+ api_base: https://bodacc-datadila.opendatasoft.com/api/v2
+ dataset: annonces-commerciales
+ page_size: 100
+ # Bulletin types: A (sales/insolvency), B (modifications), C (account deposits)
+ bulletin_types:
+ - A
+ - B
+ - C
diff --git a/france-market-scanner/extract_inpi.py b/france-market-scanner/extract_inpi.py
new file mode 100644
index 0000000..ced8101
--- /dev/null
+++ b/france-market-scanner/extract_inpi.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+"""Simple script to extract INPI data to Parquet files."""
+import sys
+sys.path.insert(0, '.')
+
+from pathlib import Path
+from src.extractors.inpi import INPIExtractor
+
+def main():
+ extractor = INPIExtractor()
+
+ source_dir = Path("data/downloads/inpi")
+ output_dir = Path("data/parquet")
+
+ # Check if we have downloaded data
+ if not source_dir.exists():
+ print(f"Source directory not found: {source_dir}")
+ print("Run download first")
+ return
+
+ # Extract all years to Parquet
+ print(f"Extracting INPI data from {source_dir} to {output_dir}")
+ stats = extractor.extract_to_parquet(source_dir, output_dir)
+
+ print("\n=== DONE ===")
+ for year, count in sorted(stats.items()):
+ print(f" {year}: {count:,} records")
+
+ total = sum(stats.values())
+ print(f" TOTAL: {total:,} records")
+ print(f"\nParquet files in: {output_dir}")
+
+if __name__ == "__main__":
+ main()
diff --git a/france-market-scanner/find_dividend_machines.py b/france-market-scanner/find_dividend_machines.py
new file mode 100644
index 0000000..cef265c
--- /dev/null
+++ b/france-market-scanner/find_dividend_machines.py
@@ -0,0 +1,149 @@
+"""Find small companies that extract profits as dividends.
+
+Logic:
+- High profits over multiple years
+- But equity (capitaux_propres) doesn't grow proportionally
+- = money is being paid out as dividends
+
+Also look for:
+- Small payroll (few employees)
+- High revenue AND/OR high profit
+- High cash on hand (disponibilites)
+"""
+import duckdb
+
+con = duckdb.connect()
+
+# First: Find companies with multiple years of data to track equity changes
+query = """
+WITH yearly_data AS (
+ SELECT
+ siren,
+ annee_cloture as year,
+ chiffre_affaires as revenue,
+ resultat_net as profit,
+ capitaux_propres as equity,
+ charges_personnel as payroll,
+ disponibilites as cash,
+ reserves
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE resultat_net IS NOT NULL
+ AND capitaux_propres IS NOT NULL
+),
+multi_year AS (
+ SELECT
+ siren,
+ COUNT(DISTINCT year) as years_of_data,
+ SUM(profit) as total_profit,
+ MAX(equity) - MIN(equity) as equity_change,
+ AVG(revenue) as avg_revenue,
+ AVG(profit) as avg_profit,
+ AVG(payroll) as avg_payroll,
+ MAX(cash) as latest_cash,
+ MAX(year) as latest_year
+ FROM yearly_data
+ WHERE year >= 2019
+ GROUP BY siren
+ HAVING COUNT(DISTINCT year) >= 3 -- At least 3 years of data
+),
+company_info AS (
+ SELECT siren, denomination, activite_principale as naf
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ m.siren,
+ c.denomination,
+ c.naf,
+ m.years_of_data,
+ m.latest_year,
+
+ -- Financials (average per year)
+ round(m.avg_revenue / 1e6, 2) as avg_revenue_M,
+ round(m.avg_profit / 1e6, 2) as avg_profit_M,
+ round(m.avg_payroll / 1e6, 2) as avg_payroll_M,
+
+ -- Total profit over the period
+ round(m.total_profit / 1e6, 2) as total_profit_M,
+
+ -- Equity change over the period
+ round(m.equity_change / 1e6, 2) as equity_change_M,
+
+ -- INFERRED DIVIDENDS = profit that didn't stay in the company
+ round((m.total_profit - m.equity_change) / 1e6, 2) as inferred_dividends_M,
+
+ -- Dividend payout ratio (what % of profits were extracted)
+ CASE WHEN m.total_profit > 0
+ THEN round(100 * (m.total_profit - m.equity_change) / m.total_profit, 0)
+ ELSE 0
+ END as payout_ratio_pct,
+
+ -- Cash on hand
+ round(m.latest_cash / 1e6, 2) as cash_M
+
+FROM multi_year m
+LEFT JOIN company_info c ON m.siren = c.siren
+WHERE m.total_profit > 500000 -- At least 500K total profit over period
+ AND m.avg_payroll BETWEEN 100000 AND 5000000 -- Small company (roughly 1-70 employees)
+ AND m.total_profit > m.equity_change -- More profit than equity growth = dividends
+ AND c.naf NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z') -- Not holdings
+ORDER BY inferred_dividends_M DESC
+LIMIT 100
+"""
+
+print("=" * 140)
+print("DIVIDEND MACHINES: Small companies extracting profits")
+print("=" * 140)
+print()
+print("inferred_dividends = total_profit - equity_change")
+print("payout_ratio = % of profits extracted (not retained)")
+print()
+
+df = con.execute(query).df()
+print(df.to_string(max_rows=100))
+
+# Now find the REAL small gems: high revenue OR high profit with tiny payroll
+print("\n\n")
+print("=" * 140)
+print("CASH COWS: Small teams (< 1M payroll) with big numbers")
+print("=" * 140)
+
+gems_query = """
+WITH latest AS (
+ SELECT
+ siren,
+ chiffre_affaires as revenue,
+ resultat_net as profit,
+ charges_personnel as payroll,
+ disponibilites as cash,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0
+),
+company_info AS (
+ SELECT siren, denomination, activite_principale as naf
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ l.siren,
+ c.denomination,
+ c.naf,
+ round(l.revenue / 1e6, 2) as revenue_M,
+ round(l.profit / 1e6, 2) as profit_M,
+ round(l.payroll / 1e6, 2) as payroll_M,
+ round(l.cash / 1e6, 2) as cash_M,
+ round(l.profit / l.payroll, 2) as profit_per_payroll
+
+FROM latest l
+LEFT JOIN company_info c ON l.siren = c.siren
+WHERE l.rn = 1
+ AND l.payroll BETWEEN 70000 AND 1000000 -- 1-14 employees roughly
+ AND (l.revenue > 5000000 OR l.profit > 500000) -- High revenue OR high profit
+ AND l.profit > 0
+ AND l.profit < l.revenue -- Not a holding
+ AND c.naf NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z', '64.19Z')
+ORDER BY profit_M DESC
+LIMIT 50
+"""
+
+df2 = con.execute(gems_query).df()
+print(df2.to_string())
diff --git a/france-market-scanner/find_gems.py b/france-market-scanner/find_gems.py
new file mode 100644
index 0000000..3488b9a
--- /dev/null
+++ b/france-market-scanner/find_gems.py
@@ -0,0 +1,115 @@
+"""Find mid-sized hidden gems - high profit/employee, not household names.
+
+Target: Companies with 10-200 employees, good margins, solid revenue.
+"""
+import duckdb
+
+con = duckdb.connect()
+
+query = """
+WITH latest_accounts AS (
+ SELECT
+ siren,
+ date_cloture,
+ chiffre_affaires,
+ charges_personnel,
+ resultat_net,
+ resultat_exploitation,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel IS NOT NULL
+ AND resultat_net IS NOT NULL
+ AND chiffre_affaires IS NOT NULL
+),
+company_info AS (
+ SELECT
+ siren,
+ denomination as nom,
+ tranche_effectifs as sirene_effectif_code,
+ activite_principale as naf_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ a.siren,
+ c.nom,
+ c.naf_code,
+ a.date_cloture,
+
+ -- Financials
+ round(a.chiffre_affaires / 1e6, 2) as ca_millions,
+ round(a.charges_personnel / 1e6, 2) as payroll_millions,
+ round(a.resultat_net / 1e6, 2) as resultat_millions,
+
+ -- Estimated employees (using 70K avg cost)
+ round(a.charges_personnel / 70000, 0) as est_employees,
+
+ -- Key ratios
+ round(a.resultat_net / (a.charges_personnel / 70000), 0) as profit_per_emp,
+ round(a.chiffre_affaires / (a.charges_personnel / 70000), 0) as rev_per_emp,
+ round(100 * a.resultat_net / a.chiffre_affaires, 1) as margin_pct
+
+FROM latest_accounts a
+LEFT JOIN company_info c ON a.siren = c.siren
+WHERE a.rn = 1
+ -- PROFITABLE
+ AND a.resultat_net > 0
+ -- REAL OPERATING COMPANY (not holding)
+ AND a.resultat_net < a.chiffre_affaires
+ AND c.naf_code NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z', '64.19Z')
+ -- MID-SIZED (10-200 employees based on payroll)
+ AND a.charges_personnel BETWEEN 700000 AND 14000000 -- 10-200 employees @ 70K
+ -- SOLID REVENUE (1M-100M - not tiny, not giant)
+ AND a.chiffre_affaires BETWEEN 1000000 AND 100000000
+ -- HEALTHY MARGIN (10%-40%)
+ AND (100.0 * a.resultat_net / a.chiffre_affaires) BETWEEN 10 AND 40
+ -- GOOD PROFIT/EMPLOYEE (at least 30K profit per employee)
+ AND (a.resultat_net / (a.charges_personnel / 70000)) > 30000
+ORDER BY profit_per_emp DESC
+LIMIT 100
+"""
+
+print("=" * 130)
+print("HIDDEN GEMS: Mid-sized companies with exceptional profit/employee")
+print("Filters: 10-200 employees | 1M-100M revenue | 10-40% margin | >30K profit/emp")
+print("=" * 130)
+print()
+
+df = con.execute(query).df()
+
+# Group by sector
+print(df.to_string(max_rows=100))
+
+# Summary by NAF code
+print("\n\nTOP SECTORS (by avg profit/employee):")
+print("-" * 60)
+sector_summary = """
+SELECT
+ c.naf_code,
+ COUNT(*) as company_count,
+ round(AVG(a.resultat_net / (a.charges_personnel / 70000)), 0) as avg_profit_per_emp,
+ round(AVG(100.0 * a.resultat_net / a.chiffre_affaires), 1) as avg_margin
+FROM (
+ SELECT siren, date_cloture, chiffre_affaires, charges_personnel, resultat_net,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel IS NOT NULL AND resultat_net IS NOT NULL AND chiffre_affaires IS NOT NULL
+) a
+LEFT JOIN (
+ SELECT siren, activite_principale as naf_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+) c ON a.siren = c.siren
+WHERE a.rn = 1
+ AND a.resultat_net > 0
+ AND a.resultat_net < a.chiffre_affaires
+ AND c.naf_code NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z', '64.19Z')
+ AND a.charges_personnel BETWEEN 700000 AND 14000000
+ AND a.chiffre_affaires BETWEEN 1000000 AND 100000000
+ AND (100.0 * a.resultat_net / a.chiffre_affaires) BETWEEN 10 AND 40
+ AND (a.resultat_net / (a.charges_personnel / 70000)) > 30000
+GROUP BY c.naf_code
+HAVING COUNT(*) >= 3
+ORDER BY avg_profit_per_emp DESC
+LIMIT 20
+"""
+df2 = con.execute(sector_summary).df()
+print(df2.to_string())
diff --git a/france-market-scanner/find_gems_correct.py b/france-market-scanner/find_gems_correct.py
new file mode 100644
index 0000000..dba39cd
--- /dev/null
+++ b/france-market-scanner/find_gems_correct.py
@@ -0,0 +1,116 @@
+"""Find gems using ACTUAL ratios - no assumed employee cost needed.
+
+The real metrics that matter:
+- profit / payroll = profit per euro spent on staff
+- profit / revenue = profit margin
+- revenue / payroll = revenue efficiency
+
+These are ACTUAL data, no assumptions.
+"""
+import duckdb
+
+con = duckdb.connect()
+
+query = """
+WITH latest_accounts AS (
+ SELECT
+ siren,
+ date_cloture,
+ chiffre_affaires,
+ charges_personnel,
+ resultat_net,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0
+ AND resultat_net IS NOT NULL
+ AND chiffre_affaires > 0
+),
+company_info AS (
+ SELECT
+ siren,
+ denomination as nom,
+ activite_principale as naf_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ a.siren,
+ c.nom,
+ c.naf_code,
+ a.date_cloture,
+
+ -- Raw financials (millions)
+ round(a.chiffre_affaires / 1e6, 2) as revenue_M,
+ round(a.charges_personnel / 1e6, 2) as payroll_M,
+ round(a.resultat_net / 1e6, 2) as profit_M,
+
+ -- ACTUAL RATIOS - no assumptions needed
+ round(a.resultat_net / a.charges_personnel, 2) as profit_per_payroll_euro,
+ round(a.chiffre_affaires / a.charges_personnel, 1) as revenue_per_payroll_euro,
+ round(100 * a.resultat_net / a.chiffre_affaires, 1) as profit_margin_pct
+
+FROM latest_accounts a
+LEFT JOIN company_info c ON a.siren = c.siren
+WHERE a.rn = 1
+ AND a.resultat_net > 0
+ AND a.resultat_net < a.chiffre_affaires -- Not a holding
+ AND c.naf_code NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z') -- Not holdings
+ -- Mid-sized by payroll
+ AND a.charges_personnel BETWEEN 500000 AND 15000000
+ -- Reasonable revenue
+ AND a.chiffre_affaires BETWEEN 1000000 AND 100000000
+ -- Healthy margin
+ AND (100.0 * a.resultat_net / a.chiffre_affaires) BETWEEN 5 AND 45
+ORDER BY profit_per_payroll_euro DESC
+LIMIT 50
+"""
+
+print("=" * 130)
+print("GEMS BY PROFIT/PAYROLL RATIO (no employee estimate needed)")
+print("=" * 130)
+print()
+print("profit_per_payroll_euro = how much profit for each euro spent on staff")
+print(" > 0.50 = exceptional (50 cents profit per euro of payroll)")
+print(" > 0.30 = very good")
+print(" > 0.15 = good")
+print()
+
+df = con.execute(query).df()
+print(df.to_string())
+
+# Now let's see which SECTORS have the best ratios
+print("\n\n")
+print("=" * 100)
+print("BEST SECTORS BY PROFIT/PAYROLL RATIO")
+print("=" * 100)
+
+sector_query = """
+WITH latest AS (
+ SELECT siren, chiffre_affaires, charges_personnel, resultat_net,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0 AND resultat_net > 0 AND chiffre_affaires > 0
+)
+SELECT
+ c.naf_code,
+ COUNT(*) as n_companies,
+ round(AVG(a.resultat_net / a.charges_personnel), 2) as avg_profit_per_payroll,
+ round(AVG(100.0 * a.resultat_net / a.chiffre_affaires), 1) as avg_margin_pct,
+ round(AVG(a.chiffre_affaires / a.charges_personnel), 1) as avg_rev_per_payroll
+FROM latest a
+JOIN (SELECT siren, activite_principale as naf_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')) c
+ ON a.siren = c.siren
+WHERE a.rn = 1
+ AND a.resultat_net < a.chiffre_affaires
+ AND c.naf_code NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z', '64.19Z')
+ AND a.charges_personnel BETWEEN 500000 AND 15000000
+ AND a.chiffre_affaires BETWEEN 1000000 AND 100000000
+ AND (100.0 * a.resultat_net / a.chiffre_affaires) BETWEEN 5 AND 45
+GROUP BY c.naf_code
+HAVING COUNT(*) >= 5
+ORDER BY avg_profit_per_payroll DESC
+LIMIT 30
+"""
+
+df2 = con.execute(sector_query).df()
+print(df2.to_string())
diff --git a/france-market-scanner/find_highwage_gems.py b/france-market-scanner/find_highwage_gems.py
new file mode 100644
index 0000000..6252a5f
--- /dev/null
+++ b/france-market-scanner/find_highwage_gems.py
@@ -0,0 +1,166 @@
+"""Find gems in HIGH-WAGE sectors (tech, finance, pharma, consulting).
+
+In these sectors, real cost/employee is 90-120K€, not 70K€.
+So our 70K estimate OVERCOUNTS employees, making ratios look WORSE than reality.
+If they still look good at 70K estimate, they're probably REALLY good.
+"""
+import duckdb
+
+con = duckdb.connect()
+
+# High-wage NAF codes (90-120K€ per employee typically)
+HIGH_WAGE_NAF = """
+ '62.01Z', -- Programming
+ '62.02A', -- IT consulting
+ '62.02B', -- IT facilities management
+ '62.03Z', -- Computer facilities management
+ '62.09Z', -- Other IT services
+ '63.11Z', -- Data processing/hosting
+ '63.12Z', -- Web portals
+ '58.29A', -- Software publishing (games)
+ '58.29B', -- Software publishing (other)
+ '58.29C', -- Software publishing
+ '64.11Z', -- Central banking
+ '64.19Z', -- Other monetary intermediation
+ '64.91Z', -- Financial leasing
+ '64.92Z', -- Other credit granting
+ '64.99Z', -- Other financial services
+ '66.11Z', -- Financial market admin
+ '66.12Z', -- Securities brokerage
+ '66.19A', -- Financial asset management
+ '66.19B', -- Other financial support
+ '66.21Z', -- Risk evaluation
+ '66.22Z', -- Insurance agents
+ '66.29Z', -- Other insurance support
+ '70.21Z', -- PR and communications
+ '70.22Z', -- Business consulting
+ '71.11Z', -- Architecture
+ '71.12A', -- Engineering consulting
+ '71.12B', -- Technical studies
+ '71.20A', -- Testing laboratories
+ '71.20B', -- Technical analysis
+ '72.11Z', -- Biotech R&D
+ '72.19Z', -- Other R&D natural sciences
+ '72.20Z', -- R&D social sciences
+ '73.11Z', -- Advertising agencies
+ '73.12Z', -- Media buying
+ '21.10Z', -- Pharma manufacturing
+ '21.20Z', -- Pharma preparations
+ '26.11Z', -- Electronic components
+ '26.20Z', -- Computers
+ '26.30Z', -- Communications equipment
+ '26.51A', -- Instruments manufacturing
+ '26.51B', -- Instruments manufacturing
+ '26.60Z', -- Medical devices
+ '30.30Z' -- Aerospace
+"""
+
+query = f"""
+WITH latest_accounts AS (
+ SELECT
+ siren,
+ date_cloture,
+ chiffre_affaires,
+ charges_personnel,
+ resultat_net,
+ resultat_exploitation,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0
+ AND resultat_net IS NOT NULL
+ AND chiffre_affaires > 0
+),
+company_info AS (
+ SELECT
+ siren,
+ denomination as nom,
+ tranche_effectifs as sirene_code,
+ activite_principale as naf_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ a.siren,
+ c.nom,
+ c.naf_code,
+ a.date_cloture,
+
+ -- Financials
+ round(a.chiffre_affaires / 1e6, 2) as ca_M,
+ round(a.charges_personnel / 1e6, 2) as payroll_M,
+ round(a.resultat_net / 1e6, 2) as profit_M,
+
+ -- Conservative estimate (70K) - OVERCOUNTS for high-wage
+ round(a.charges_personnel / 70000, 0) as est_emp_70k,
+
+ -- Realistic estimate for high-wage (100K)
+ round(a.charges_personnel / 100000, 0) as est_emp_100k,
+
+ -- Ratios using CONSERVATIVE 70K (if good here, definitely good in reality)
+ round(a.resultat_net / (a.charges_personnel / 70000), 0) as profit_per_emp_70k,
+
+ -- Ratios using REALISTIC 100K (closer to reality for these sectors)
+ round(a.resultat_net / (a.charges_personnel / 100000), 0) as profit_per_emp_100k,
+
+ round(100 * a.resultat_net / a.chiffre_affaires, 1) as margin_pct,
+ c.sirene_code
+
+FROM latest_accounts a
+LEFT JOIN company_info c ON a.siren = c.siren
+WHERE a.rn = 1
+ AND a.resultat_net > 0
+ AND a.resultat_net < a.chiffre_affaires -- Not a holding
+ AND c.naf_code IN ({HIGH_WAGE_NAF})
+ -- Mid-sized: 5-100 employees (at 100K estimate)
+ AND a.charges_personnel BETWEEN 500000 AND 10000000
+ -- Decent revenue
+ AND a.chiffre_affaires BETWEEN 1000000 AND 50000000
+ -- Good margin
+ AND (100.0 * a.resultat_net / a.chiffre_affaires) BETWEEN 10 AND 45
+ORDER BY profit_per_emp_100k DESC
+LIMIT 100
+"""
+
+print("=" * 140)
+print("HIGH-WAGE SECTOR GEMS (Tech, Finance, Pharma, Consulting)")
+print("These use 100K€/employee estimate - more accurate for these sectors")
+print("=" * 140)
+print()
+
+df = con.execute(query).df()
+print(df.to_string(max_rows=100))
+
+print("\n")
+print("NOTE: profit_per_emp_100k is the realistic figure for these high-wage sectors")
+print(" profit_per_emp_70k is CONSERVATIVE (actual performance is even better)")
+
+# Summary by NAF
+print("\n\nBY SECTOR:")
+print("-" * 80)
+summary = f"""
+SELECT
+ c.naf_code,
+ COUNT(*) as n,
+ round(AVG(a.resultat_net / (a.charges_personnel / 100000)), 0) as avg_profit_per_emp,
+ round(AVG(100.0 * a.resultat_net / a.chiffre_affaires), 1) as avg_margin
+FROM (
+ SELECT siren, chiffre_affaires, charges_personnel, resultat_net,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM read_parquet('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0 AND resultat_net > 0 AND chiffre_affaires > 0
+) a
+JOIN (
+ SELECT siren, activite_principale as naf_code
+ FROM read_parquet('data/parquet/sirene_unites_legales.parquet')
+) c ON a.siren = c.siren
+WHERE a.rn = 1
+ AND a.resultat_net < a.chiffre_affaires
+ AND c.naf_code IN ({HIGH_WAGE_NAF})
+ AND a.charges_personnel BETWEEN 500000 AND 10000000
+ AND a.chiffre_affaires BETWEEN 1000000 AND 50000000
+ AND (100.0 * a.resultat_net / a.chiffre_affaires) BETWEEN 10 AND 45
+GROUP BY c.naf_code
+HAVING COUNT(*) >= 3
+ORDER BY avg_profit_per_emp DESC
+"""
+df2 = con.execute(summary).df()
+print(df2.to_string())
diff --git a/france-market-scanner/find_real_small_gems.py b/france-market-scanner/find_real_small_gems.py
new file mode 100644
index 0000000..b2ec0b3
--- /dev/null
+++ b/france-market-scanner/find_real_small_gems.py
@@ -0,0 +1,129 @@
+"""Find ACTUALLY small independent companies - not subsidiaries of giants.
+
+Filters:
+- Revenue between 1M and 20M (too small for CAC40 subsidiary)
+- Payroll between 200K and 2M (3-30 employees roughly)
+- Not obviously a subsidiary name (no "FRANCE", "EUROPE", "SAS" of known giants)
+- Has actual business activity (not holding NAF codes)
+"""
+import chdb
+
+query = """
+WITH latest AS (
+ SELECT
+ siren,
+ date_cloture,
+ chiffre_affaires as revenue,
+ resultat_net as profit,
+ charges_personnel as payroll,
+ disponibilites as cash,
+ capital_social,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM file('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0
+ AND resultat_net IS NOT NULL
+ AND chiffre_affaires > 0
+),
+company_info AS (
+ SELECT
+ siren,
+ denomination,
+ activite_principale as naf,
+ categorie_entreprise, -- PME, ETI, GE
+ date_creation
+ FROM file('data/parquet/sirene_unites_legales.parquet')
+)
+SELECT
+ l.siren,
+ c.denomination,
+ c.naf,
+ c.categorie_entreprise as cat,
+ l.date_cloture,
+
+ round(l.revenue / 1e6, 2) as revenue_M,
+ round(l.profit / 1e6, 2) as profit_M,
+ round(l.payroll / 1e6, 2) as payroll_M,
+ round(l.cash / 1e6, 2) as cash_M,
+ round(l.capital_social / 1e3, 0) as capital_K,
+
+ round(l.profit / l.payroll, 2) as profit_per_payroll,
+ round(100 * l.profit / l.revenue, 1) as margin_pct
+
+FROM latest l
+LEFT JOIN company_info c ON l.siren = c.siren
+WHERE l.rn = 1
+ -- SIZE FILTERS: Actually small
+ AND l.revenue BETWEEN 1000000 AND 20000000 -- 1M-20M revenue
+ AND l.payroll BETWEEN 200000 AND 2000000 -- 3-30 employees roughly
+ AND l.profit > 100000 -- At least 100K profit
+
+ -- NOT A HOLDING
+ AND l.profit < l.revenue
+ AND c.naf NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z', '64.19Z', '64.99Z')
+
+ -- CATEGORY FILTER: PME only (excludes ETI and GE = big groups)
+ AND (c.categorie_entreprise = 'PME' OR c.categorie_entreprise IS NULL)
+
+ -- Exclude obvious subsidiary names
+ AND c.denomination NOT LIKE '%FRANCE%'
+ AND c.denomination NOT LIKE '%EUROPE%'
+ AND c.denomination NOT LIKE '%INTERNATIONAL%'
+ AND c.denomination NOT LIKE '%HOLDING%'
+ AND c.denomination NOT LIKE '%GROUP%'
+
+ -- Good margin (real operating business)
+ AND (100.0 * l.profit / l.revenue) BETWEEN 5 AND 40
+
+ORDER BY profit_per_payroll DESC
+LIMIT 100
+"""
+
+print("=" * 130)
+print("REAL SMALL GEMS: Independent PMEs with high profit/payroll")
+print("Filters: 1-20M revenue | 200K-2M payroll | PME category | No giant subsidiaries")
+print("=" * 130)
+print()
+
+result = chdb.query(query, 'DataFrame')
+print(result.to_string(max_rows=100))
+
+# Show by sector
+print("\n\n")
+print("BY SECTOR:")
+print("-" * 80)
+sector_query = """
+WITH latest AS (
+ SELECT siren, chiffre_affaires as revenue, resultat_net as profit, charges_personnel as payroll,
+ ROW_NUMBER() OVER (PARTITION BY siren ORDER BY date_cloture DESC) as rn
+ FROM file('data/parquet/inpi_comptes_*.parquet')
+ WHERE charges_personnel > 0 AND resultat_net > 0 AND chiffre_affaires > 0
+)
+SELECT
+ c.naf,
+ COUNT(*) as n,
+ round(AVG(l.profit / l.payroll), 2) as avg_profit_per_payroll,
+ round(AVG(100.0 * l.profit / l.revenue), 1) as avg_margin,
+ round(AVG(l.revenue) / 1e6, 2) as avg_revenue_M
+FROM latest l
+JOIN (SELECT siren, activite_principale as naf, categorie_entreprise, denomination
+ FROM file('data/parquet/sirene_unites_legales.parquet')) c ON l.siren = c.siren
+WHERE l.rn = 1
+ AND l.revenue BETWEEN 1000000 AND 20000000
+ AND l.payroll BETWEEN 200000 AND 2000000
+ AND l.profit > 100000
+ AND l.profit < l.revenue
+ AND c.naf NOT IN ('70.10Z', '64.20Z', '64.30Z', '66.30Z', '64.19Z', '64.99Z')
+ AND (c.categorie_entreprise = 'PME' OR c.categorie_entreprise IS NULL)
+ AND c.denomination NOT LIKE '%FRANCE%'
+ AND c.denomination NOT LIKE '%EUROPE%'
+ AND c.denomination NOT LIKE '%INTERNATIONAL%'
+ AND c.denomination NOT LIKE '%HOLDING%'
+ AND c.denomination NOT LIKE '%GROUP%'
+ AND (100.0 * l.profit / l.revenue) BETWEEN 5 AND 40
+GROUP BY c.naf
+HAVING COUNT(*) >= 5
+ORDER BY avg_profit_per_payroll DESC
+LIMIT 20
+"""
+result2 = chdb.query(sector_query, 'DataFrame')
+print(result2.to_string())
diff --git a/france-market-scanner/pyproject.toml b/france-market-scanner/pyproject.toml
new file mode 100644
index 0000000..90ec3f8
--- /dev/null
+++ b/france-market-scanner/pyproject.toml
@@ -0,0 +1,36 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "france-market-scanner"
+version = "0.1.0"
+description = "Bulk French company data collection from public APIs (SIRENE, INPI, BODACC)"
+readme = "README.md"
+requires-python = ">=3.10"
+license = {text = "MIT"}
+authors = [
+ {name = "Jack"}
+]
+dependencies = [
+ "duckdb>=1.0.0",
+ "python-dotenv>=1.0.0",
+ "loguru>=0.7.0",
+ "pyyaml>=6.0",
+ "httpx>=0.27.0",
+ "aiofiles>=23.0.0",
+ "paramiko>=3.0.0",
+ "lxml>=5.0.0",
+ "click>=8.1.0",
+ "rich>=13.0.0",
+ "pyarrow>=15.0.0",
+ "pandas>=2.3.3",
+ "chdb>=3.7.2",
+]
+
+[project.scripts]
+fms = "cli:cli"
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["src*"]
diff --git a/france-market-scanner/requirements.txt b/france-market-scanner/requirements.txt
new file mode 100644
index 0000000..69c8c9a
--- /dev/null
+++ b/france-market-scanner/requirements.txt
@@ -0,0 +1,22 @@
+# Core
+duckdb>=1.0.0
+python-dotenv>=1.0.0
+loguru>=0.7.0
+pyyaml>=6.0
+
+# HTTP/Downloads
+httpx>=0.27.0
+aiofiles>=23.0.0
+
+# SFTP (for INPI)
+paramiko>=3.0.0
+
+# XML Parsing (for BODACC)
+lxml>=5.0.0
+
+# CLI
+click>=8.1.0
+rich>=13.0.0
+
+# Data Processing
+pyarrow>=15.0.0
diff --git a/france-market-scanner/src/__init__.py b/france-market-scanner/src/__init__.py
new file mode 100644
index 0000000..c0965da
--- /dev/null
+++ b/france-market-scanner/src/__init__.py
@@ -0,0 +1 @@
+"""France Market Scanner - Bulk French company data collection."""
diff --git a/france-market-scanner/src/core/__init__.py b/france-market-scanner/src/core/__init__.py
new file mode 100644
index 0000000..7e5b33b
--- /dev/null
+++ b/france-market-scanner/src/core/__init__.py
@@ -0,0 +1,6 @@
+"""Core modules for database and utilities."""
+from .database import DatabaseManager
+from .config import load_config, get_project_root
+from .downloader import Downloader
+
+__all__ = ["DatabaseManager", "load_config", "get_project_root", "Downloader"]
diff --git a/france-market-scanner/src/core/config.py b/france-market-scanner/src/core/config.py
new file mode 100644
index 0000000..e84f312
--- /dev/null
+++ b/france-market-scanner/src/core/config.py
@@ -0,0 +1,73 @@
+"""Configuration management."""
+from pathlib import Path
+from typing import Any
+import os
+import yaml
+from dotenv import load_dotenv
+
+
+def load_config(config_path: str | Path = None) -> dict[str, Any]:
+ """Load configuration from YAML file and environment variables.
+
+ Args:
+ config_path: Path to config file. Defaults to config/config.yaml.
+
+ Returns:
+ Configuration dictionary.
+ """
+ # Load .env file if present
+ load_dotenv()
+
+ # Determine config path
+ if config_path is None:
+ # Try to find config relative to project root
+ current = Path(__file__).parent.parent.parent
+ config_path = current / "config" / "config.yaml"
+
+ config_path = Path(config_path)
+
+ if not config_path.exists():
+ raise FileNotFoundError(f"Config file not found: {config_path}")
+
+ # Load YAML config
+ with open(config_path) as f:
+ config = yaml.safe_load(f)
+
+ # Override with environment variables
+ config = _apply_env_overrides(config)
+
+ return config
+
+
+def _apply_env_overrides(config: dict) -> dict:
+ """Apply environment variable overrides to config."""
+ # Database path
+ if db_path := os.getenv("DATABASE_PATH"):
+ config["database"]["path"] = db_path
+
+ # Log level
+ if log_level := os.getenv("LOG_LEVEL"):
+ config["logging"]["level"] = log_level
+
+ # INPI credentials
+ if not config.get("inpi"):
+ config["inpi"] = {}
+ if username := os.getenv("INPI_USERNAME"):
+ config["inpi"]["username"] = username
+ if password := os.getenv("INPI_PASSWORD"):
+ config["inpi"]["password"] = password
+
+ # BODACC FTPS credentials
+ if not config.get("bodacc"):
+ config["bodacc"] = {}
+ if username := os.getenv("BODACC_FTPS_USERNAME"):
+ config["bodacc"]["ftps_username"] = username
+ if password := os.getenv("BODACC_FTPS_PASSWORD"):
+ config["bodacc"]["ftps_password"] = password
+
+ return config
+
+
+def get_project_root() -> Path:
+ """Get the project root directory."""
+ return Path(__file__).parent.parent.parent
diff --git a/france-market-scanner/src/core/database.py b/france-market-scanner/src/core/database.py
new file mode 100644
index 0000000..8455e54
--- /dev/null
+++ b/france-market-scanner/src/core/database.py
@@ -0,0 +1,611 @@
+"""DuckDB database manager with schema initialization."""
+from pathlib import Path
+from datetime import datetime
+from typing import Optional, Any
+import duckdb
+from loguru import logger
+
+
+class DatabaseManager:
+ """Manage DuckDB connection and schema operations."""
+
+ def __init__(
+ self,
+ db_path: str | Path = "data/france_companies.duckdb",
+ memory_limit: str = "4GB",
+ threads: int = 4,
+ ):
+ self.db_path = Path(db_path)
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
+ self.memory_limit = memory_limit
+ self.threads = threads
+ self._conn: Optional[duckdb.DuckDBPyConnection] = None
+
+ @property
+ def conn(self) -> duckdb.DuckDBPyConnection:
+ """Get or create database connection."""
+ if self._conn is None:
+ self._conn = duckdb.connect(str(self.db_path))
+ self._conn.execute(f"SET memory_limit = '{self.memory_limit}'")
+ self._conn.execute(f"SET threads = {self.threads}")
+ # Optimize for large bulk inserts
+ self._conn.execute("SET preserve_insertion_order = false")
+ logger.info(f"Connected to DuckDB: {self.db_path}")
+ return self._conn
+
+ def close(self) -> None:
+ """Close database connection."""
+ if self._conn is not None:
+ self._conn.close()
+ self._conn = None
+ logger.info("Database connection closed")
+
+ def __enter__(self) -> "DatabaseManager":
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ self.close()
+
+ def init_schema(self, force: bool = False) -> None:
+ """Initialize database schema.
+
+ Args:
+ force: If True, drop existing tables before creating.
+ """
+ if force:
+ logger.warning("Force mode: dropping existing tables")
+ self._drop_tables()
+
+ self._create_sirene_tables()
+ self._create_inpi_tables()
+ self._create_bodacc_tables()
+ self._create_etl_tables()
+ self._create_views()
+
+ logger.info("Database schema initialized successfully")
+
+ def _drop_tables(self) -> None:
+ """Drop all existing tables."""
+ tables = [
+ "v_company_overview", # Views first
+ "bodacc_annonces",
+ "inpi_compte_resultat",
+ "inpi_bilan",
+ "inpi_comptes_annuels",
+ "sirene_etablissements",
+ "sirene_unites_legales",
+ "etl_loads",
+ "etl_downloads",
+ ]
+ for table in tables:
+ try:
+ self.conn.execute(f"DROP TABLE IF EXISTS {table} CASCADE")
+ self.conn.execute(f"DROP VIEW IF EXISTS {table} CASCADE")
+ except Exception:
+ pass
+
+ def _create_sirene_tables(self) -> None:
+ """Create SIRENE tables for legal units and establishments."""
+ # Legal units (entreprises) - ~12M rows
+ # Note: No PRIMARY KEY because file contains historical periods (duplicates)
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS sirene_unites_legales (
+ siren VARCHAR(9) NOT NULL,
+
+ -- Identification
+ statut_diffusion VARCHAR(1),
+ date_creation DATE,
+ sigle VARCHAR(20),
+
+ -- Denomination
+ denomination VARCHAR(200),
+ denomination_usuelle_1 VARCHAR(70),
+ denomination_usuelle_2 VARCHAR(70),
+ denomination_usuelle_3 VARCHAR(70),
+
+ -- Physical person (if applicable)
+ prenom VARCHAR(50),
+ nom VARCHAR(100),
+
+ -- Legal form
+ categorie_juridique VARCHAR(4),
+
+ -- Activity
+ activite_principale VARCHAR(6),
+ nomenclature_activite VARCHAR(8),
+
+ -- Size
+ tranche_effectifs VARCHAR(2),
+ annee_effectifs INTEGER,
+ caractere_employeur VARCHAR(1),
+
+ -- Category
+ categorie_entreprise VARCHAR(10),
+ annee_categorie_entreprise INTEGER,
+
+ -- Economy
+ economie_sociale_solidaire VARCHAR(1),
+ societe_mission VARCHAR(1),
+
+ -- Status
+ etat_administratif VARCHAR(1),
+ date_cessation DATE,
+
+ -- Timestamps
+ date_derniere_mise_a_jour TIMESTAMP,
+
+ -- ETL metadata
+ _loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ _source_file VARCHAR(200)
+ )
+ """)
+ logger.debug("Created table: sirene_unites_legales")
+
+ # Establishments (établissements) - ~30M rows
+ # Note: No PRIMARY KEY because file contains historical periods (duplicates)
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS sirene_etablissements (
+ siret VARCHAR(14) NOT NULL,
+ siren VARCHAR(9) NOT NULL,
+ nic VARCHAR(5) NOT NULL,
+
+ -- Identification
+ statut_diffusion VARCHAR(1),
+ date_creation DATE,
+
+ -- Denomination
+ denomination_usuelle VARCHAR(100),
+ enseigne_1 VARCHAR(50),
+ enseigne_2 VARCHAR(50),
+ enseigne_3 VARCHAR(50),
+
+ -- Activity
+ activite_principale VARCHAR(6),
+ nomenclature_activite VARCHAR(8),
+ activite_principale_registre_metiers VARCHAR(6),
+
+ -- Type
+ etablissement_siege VARCHAR(5),
+
+ -- Size
+ tranche_effectifs VARCHAR(2),
+ annee_effectifs INTEGER,
+ caractere_employeur VARCHAR(1),
+
+ -- Address
+ complement_adresse VARCHAR(200),
+ numero_voie VARCHAR(10),
+ indice_repetition VARCHAR(5),
+ type_voie VARCHAR(10),
+ libelle_voie VARCHAR(200),
+ code_postal VARCHAR(5),
+ libelle_commune VARCHAR(100),
+ libelle_commune_etranger VARCHAR(100),
+ code_commune VARCHAR(5),
+ code_cedex VARCHAR(10),
+ libelle_cedex VARCHAR(100),
+ code_pays_etranger VARCHAR(5),
+ libelle_pays_etranger VARCHAR(100),
+
+ -- Geo (derived)
+ departement VARCHAR(3),
+ region VARCHAR(3),
+
+ -- Status
+ etat_administratif VARCHAR(1),
+ date_cessation DATE,
+
+ -- Timestamps
+ date_derniere_mise_a_jour TIMESTAMP,
+
+ -- ETL metadata
+ _loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ _source_file VARCHAR(200)
+ )
+ """)
+ logger.debug("Created table: sirene_etablissements")
+
+ def _create_inpi_tables(self) -> None:
+ """Create INPI tables for annual accounts."""
+ # Annual accounts metadata
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS inpi_comptes_annuels (
+ id VARCHAR(100) PRIMARY KEY,
+ siren VARCHAR(9) NOT NULL,
+
+ -- Exercise period
+ date_cloture DATE,
+ duree_exercice INTEGER,
+ annee_cloture INTEGER,
+
+ -- Document info
+ code_greffe VARCHAR(10),
+ num_depot VARCHAR(50),
+ date_depot DATE,
+
+ -- Type and confidentiality
+ type_comptes VARCHAR(50),
+ confidentialite VARCHAR(1),
+
+ -- ETL metadata
+ _loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ _source_file VARCHAR(200)
+ )
+ """)
+ logger.debug("Created table: inpi_comptes_annuels")
+
+ # Balance sheet (Bilan)
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS inpi_bilan (
+ id BIGINT PRIMARY KEY,
+ compte_annuel_id VARCHAR(100) NOT NULL,
+ siren VARCHAR(9) NOT NULL,
+ annee_cloture INTEGER,
+
+ -- ACTIF (Assets)
+ immobilisations_incorporelles DECIMAL(15,2),
+ immobilisations_corporelles DECIMAL(15,2),
+ immobilisations_financieres DECIMAL(15,2),
+ actif_immobilise_brut DECIMAL(15,2),
+ actif_immobilise_net DECIMAL(15,2),
+
+ stocks DECIMAL(15,2),
+ creances_clients DECIMAL(15,2),
+ autres_creances DECIMAL(15,2),
+ valeurs_mobilieres_placement DECIMAL(15,2),
+ disponibilites DECIMAL(15,2),
+ actif_circulant DECIMAL(15,2),
+
+ charges_constatees_avance DECIMAL(15,2),
+ total_actif DECIMAL(15,2),
+
+ -- PASSIF (Liabilities)
+ capital_social DECIMAL(15,2),
+ primes_emission DECIMAL(15,2),
+ reserves DECIMAL(15,2),
+ report_a_nouveau DECIMAL(15,2),
+ resultat_exercice DECIMAL(15,2),
+ subventions_investissement DECIMAL(15,2),
+ provisions_reglementees DECIMAL(15,2),
+ capitaux_propres DECIMAL(15,2),
+
+ provisions_risques_charges DECIMAL(15,2),
+
+ emprunts_dettes_financieres DECIMAL(15,2),
+ avances_acomptes_recus DECIMAL(15,2),
+ dettes_fournisseurs DECIMAL(15,2),
+ dettes_fiscales_sociales DECIMAL(15,2),
+ autres_dettes DECIMAL(15,2),
+ dettes DECIMAL(15,2),
+
+ produits_constates_avance DECIMAL(15,2),
+ total_passif DECIMAL(15,2),
+
+ -- ETL metadata
+ _loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+ logger.debug("Created table: inpi_bilan")
+
+ # Income statement (Compte de résultat)
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS inpi_compte_resultat (
+ id BIGINT PRIMARY KEY,
+ compte_annuel_id VARCHAR(100) NOT NULL,
+ siren VARCHAR(9) NOT NULL,
+ annee_cloture INTEGER,
+
+ -- PRODUITS (Revenue)
+ ventes_marchandises DECIMAL(15,2),
+ production_vendue_biens DECIMAL(15,2),
+ production_vendue_services DECIMAL(15,2),
+ chiffre_affaires DECIMAL(15,2),
+
+ production_stockee DECIMAL(15,2),
+ production_immobilisee DECIMAL(15,2),
+ subventions_exploitation DECIMAL(15,2),
+ reprises_provisions DECIMAL(15,2),
+ autres_produits DECIMAL(15,2),
+ total_produits_exploitation DECIMAL(15,2),
+
+ -- CHARGES (Expenses)
+ achats_marchandises DECIMAL(15,2),
+ variation_stock_marchandises DECIMAL(15,2),
+ achats_matieres_premieres DECIMAL(15,2),
+ variation_stock_matieres DECIMAL(15,2),
+ autres_achats_charges_externes DECIMAL(15,2),
+ impots_taxes DECIMAL(15,2),
+ salaires_traitements DECIMAL(15,2),
+ charges_sociales DECIMAL(15,2),
+ charges_personnel DECIMAL(15,2),
+ dotations_amortissements DECIMAL(15,2),
+ dotations_provisions DECIMAL(15,2),
+ autres_charges DECIMAL(15,2),
+ total_charges_exploitation DECIMAL(15,2),
+
+ -- RESULTS
+ resultat_exploitation DECIMAL(15,2),
+
+ -- Financial
+ produits_financiers DECIMAL(15,2),
+ charges_financieres DECIMAL(15,2),
+ resultat_financier DECIMAL(15,2),
+
+ resultat_courant_avant_impot DECIMAL(15,2),
+
+ -- Exceptional
+ produits_exceptionnels DECIMAL(15,2),
+ charges_exceptionnelles DECIMAL(15,2),
+ resultat_exceptionnel DECIMAL(15,2),
+
+ -- Taxes and final result
+ participation_salaries DECIMAL(15,2),
+ impot_benefice DECIMAL(15,2),
+ resultat_net DECIMAL(15,2),
+
+ -- ETL metadata
+ _loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+ logger.debug("Created table: inpi_compte_resultat")
+
+ def _create_bodacc_tables(self) -> None:
+ """Create BODACC tables for legal announcements."""
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS bodacc_annonces (
+ id VARCHAR(100) PRIMARY KEY,
+
+ -- Identification
+ siren VARCHAR(9),
+ numero_annonce VARCHAR(50),
+
+ -- Publication
+ date_parution DATE,
+ numero_parution VARCHAR(50),
+ type_bulletin VARCHAR(1),
+
+ -- Classification
+ famille VARCHAR(100),
+ nature VARCHAR(200),
+
+ -- Company info
+ denomination VARCHAR(300),
+ forme_juridique VARCHAR(100),
+ administration VARCHAR(500),
+
+ -- Address
+ adresse VARCHAR(500),
+ code_postal VARCHAR(10),
+ ville VARCHAR(100),
+
+ -- Activity
+ activite VARCHAR(500),
+
+ -- Event-specific data (JSON for flexibility)
+ details JSON,
+
+ -- For collective procedures
+ type_procedure VARCHAR(100),
+ date_jugement DATE,
+ tribunal VARCHAR(200),
+
+ -- For account deposits (BODACC C)
+ date_cloture_exercice DATE,
+ type_depot VARCHAR(100),
+
+ -- Raw content
+ contenu_annonce TEXT,
+
+ -- ETL metadata
+ _loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ _source_file VARCHAR(200)
+ )
+ """)
+ logger.debug("Created table: bodacc_annonces")
+
+ def _create_etl_tables(self) -> None:
+ """Create ETL tracking tables."""
+ # Track data loads
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS etl_loads (
+ id INTEGER PRIMARY KEY,
+ source VARCHAR(50) NOT NULL,
+ load_type VARCHAR(20) NOT NULL,
+ started_at TIMESTAMP NOT NULL,
+ completed_at TIMESTAMP,
+ status VARCHAR(20) DEFAULT 'running',
+ rows_processed INTEGER DEFAULT 0,
+ rows_inserted INTEGER DEFAULT 0,
+ rows_updated INTEGER DEFAULT 0,
+ error_message VARCHAR(2000),
+ source_file VARCHAR(500)
+ )
+ """)
+ # Create sequence for etl_loads.id
+ self.conn.execute("""
+ CREATE SEQUENCE IF NOT EXISTS seq_etl_loads START 1
+ """)
+ logger.debug("Created table: etl_loads")
+
+ # Track source file downloads
+ self.conn.execute("""
+ CREATE TABLE IF NOT EXISTS etl_downloads (
+ id INTEGER PRIMARY KEY,
+ source VARCHAR(50) NOT NULL,
+ url VARCHAR(1000) NOT NULL,
+ filename VARCHAR(300) NOT NULL,
+ downloaded_at TIMESTAMP NOT NULL,
+ file_size_bytes BIGINT,
+ checksum VARCHAR(64),
+ status VARCHAR(20) DEFAULT 'pending'
+ )
+ """)
+ # Create sequence for etl_downloads.id
+ self.conn.execute("""
+ CREATE SEQUENCE IF NOT EXISTS seq_etl_downloads START 1
+ """)
+ logger.debug("Created table: etl_downloads")
+
+ def _create_views(self) -> None:
+ """Create analytical views."""
+ # Company overview combining SIRENE + latest financials
+ self.conn.execute("""
+ CREATE OR REPLACE VIEW v_company_overview AS
+ SELECT
+ ul.siren,
+ ul.denomination,
+ ul.sigle,
+ ul.activite_principale AS naf_code,
+ ul.categorie_juridique,
+ ul.tranche_effectifs,
+ ul.caractere_employeur,
+ ul.etat_administratif,
+ ul.date_creation,
+ ul.date_cessation,
+
+ -- Siege info
+ e.siret AS siret_siege,
+ e.code_postal,
+ e.libelle_commune AS commune,
+ e.departement,
+
+ -- Latest financials
+ cr.chiffre_affaires,
+ cr.resultat_net,
+ cr.resultat_exploitation,
+ cr.charges_personnel,
+ cr.annee_cloture AS annee_financiere,
+
+ b.total_actif,
+ b.capitaux_propres,
+ b.dettes,
+ b.disponibilites
+
+ FROM sirene_unites_legales ul
+ LEFT JOIN sirene_etablissements e
+ ON ul.siren = e.siren AND e.etablissement_siege = 'true'
+ LEFT JOIN (
+ SELECT DISTINCT ON (siren) *
+ FROM inpi_compte_resultat
+ ORDER BY siren, annee_cloture DESC
+ ) cr ON ul.siren = cr.siren
+ LEFT JOIN (
+ SELECT DISTINCT ON (siren) *
+ FROM inpi_bilan
+ ORDER BY siren, annee_cloture DESC
+ ) b ON ul.siren = b.siren
+ """)
+ logger.debug("Created view: v_company_overview")
+
+ def _create_indexes(self) -> None:
+ """Create indexes for query performance."""
+ indexes = [
+ # SIRENE indexes
+ ("idx_ul_activite", "sirene_unites_legales", "activite_principale"),
+ ("idx_ul_categorie", "sirene_unites_legales", "categorie_juridique"),
+ ("idx_ul_effectifs", "sirene_unites_legales", "tranche_effectifs"),
+ ("idx_ul_etat", "sirene_unites_legales", "etat_administratif"),
+ ("idx_ul_date_creation", "sirene_unites_legales", "date_creation"),
+
+ ("idx_etab_siren", "sirene_etablissements", "siren"),
+ ("idx_etab_departement", "sirene_etablissements", "departement"),
+ ("idx_etab_code_postal", "sirene_etablissements", "code_postal"),
+ ("idx_etab_commune", "sirene_etablissements", "code_commune"),
+ ("idx_etab_activite", "sirene_etablissements", "activite_principale"),
+
+ # INPI indexes
+ ("idx_comptes_siren", "inpi_comptes_annuels", "siren"),
+ ("idx_comptes_annee", "inpi_comptes_annuels", "annee_cloture"),
+ ("idx_bilan_siren", "inpi_bilan", "siren"),
+ ("idx_bilan_annee", "inpi_bilan", "annee_cloture"),
+ ("idx_cr_siren", "inpi_compte_resultat", "siren"),
+ ("idx_cr_annee", "inpi_compte_resultat", "annee_cloture"),
+
+ # BODACC indexes
+ ("idx_bodacc_siren", "bodacc_annonces", "siren"),
+ ("idx_bodacc_date", "bodacc_annonces", "date_parution"),
+ ("idx_bodacc_type", "bodacc_annonces", "type_bulletin"),
+ ("idx_bodacc_famille", "bodacc_annonces", "famille"),
+ ]
+
+ for idx_name, table, column in indexes:
+ try:
+ self.conn.execute(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}({column})")
+ except Exception as e:
+ logger.warning(f"Could not create index {idx_name}: {e}")
+
+ def get_stats(self) -> dict[str, Any]:
+ """Get database statistics."""
+ stats = {}
+
+ tables = [
+ "sirene_unites_legales",
+ "sirene_etablissements",
+ "inpi_comptes_annuels",
+ "inpi_bilan",
+ "inpi_compte_resultat",
+ "bodacc_annonces",
+ ]
+
+ for table in tables:
+ try:
+ result = self.conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()
+ stats[table] = result[0] if result else 0
+ except Exception:
+ stats[table] = 0
+
+ # Get last ETL load dates
+ try:
+ result = self.conn.execute("""
+ SELECT source, MAX(completed_at) as last_load
+ FROM etl_loads
+ WHERE status = 'success'
+ GROUP BY source
+ """).fetchall()
+ stats["last_loads"] = {row[0]: row[1] for row in result}
+ except Exception:
+ stats["last_loads"] = {}
+
+ return stats
+
+ def log_etl_start(self, source: str, load_type: str, source_file: str = None) -> int:
+ """Log start of ETL process and return load ID."""
+ result = self.conn.execute("""
+ INSERT INTO etl_loads (id, source, load_type, started_at, source_file)
+ VALUES (nextval('seq_etl_loads'), ?, ?, ?, ?)
+ RETURNING id
+ """, [source, load_type, datetime.now(), source_file]).fetchone()
+ return result[0]
+
+ def log_etl_complete(
+ self,
+ load_id: int,
+ status: str = "success",
+ rows_processed: int = 0,
+ rows_inserted: int = 0,
+ error_message: str = None
+ ) -> None:
+ """Log completion of ETL process."""
+ self.conn.execute("""
+ UPDATE etl_loads
+ SET completed_at = ?,
+ status = ?,
+ rows_processed = ?,
+ rows_inserted = ?,
+ error_message = ?
+ WHERE id = ?
+ """, [datetime.now(), status, rows_processed, rows_inserted, error_message, load_id])
+
+ def execute(self, query: str, params: list = None) -> duckdb.DuckDBPyConnection:
+ """Execute a query."""
+ if params:
+ return self.conn.execute(query, params)
+ return self.conn.execute(query)
+
+ def fetchall(self, query: str, params: list = None) -> list:
+ """Execute query and fetch all results."""
+ return self.execute(query, params).fetchall()
+
+ def fetchone(self, query: str, params: list = None) -> tuple:
+ """Execute query and fetch one result."""
+ return self.execute(query, params).fetchone()
diff --git a/france-market-scanner/src/core/downloader.py b/france-market-scanner/src/core/downloader.py
new file mode 100644
index 0000000..244cb4b
--- /dev/null
+++ b/france-market-scanner/src/core/downloader.py
@@ -0,0 +1,136 @@
+"""HTTP download utilities with progress tracking."""
+import asyncio
+from pathlib import Path
+from typing import Optional, Callable
+import httpx
+from loguru import logger
+
+
+class Downloader:
+ """Async HTTP downloader with progress tracking and resume support."""
+
+ def __init__(
+ self,
+ timeout: int = 300,
+ chunk_size: int = 8192,
+ retry_attempts: int = 3,
+ ):
+ self.timeout = timeout
+ self.chunk_size = chunk_size
+ self.retry_attempts = retry_attempts
+
+ async def download(
+ self,
+ url: str,
+ destination: Path,
+ progress_callback: Optional[Callable[[int, int], None]] = None,
+ ) -> Path:
+ """Download a file from URL to destination.
+
+ Args:
+ url: URL to download from.
+ destination: Path to save the file.
+ progress_callback: Optional callback(downloaded_bytes, total_bytes).
+
+ Returns:
+ Path to downloaded file.
+ """
+ destination = Path(destination)
+ destination.parent.mkdir(parents=True, exist_ok=True)
+
+ for attempt in range(1, self.retry_attempts + 1):
+ try:
+ return await self._download_with_resume(
+ url, destination, progress_callback
+ )
+ except Exception as e:
+ logger.warning(f"Download attempt {attempt} failed: {e}")
+ if attempt == self.retry_attempts:
+ raise
+ await asyncio.sleep(2 ** attempt) # Exponential backoff
+
+ raise RuntimeError("Download failed after all retries")
+
+ async def _download_with_resume(
+ self,
+ url: str,
+ destination: Path,
+ progress_callback: Optional[Callable[[int, int], None]] = None,
+ ) -> Path:
+ """Download with resume support."""
+ headers = {}
+ mode = "wb"
+ downloaded = 0
+
+ # Check for partial download
+ if destination.exists():
+ downloaded = destination.stat().st_size
+ headers["Range"] = f"bytes={downloaded}-"
+ mode = "ab"
+ logger.info(f"Resuming download from byte {downloaded}")
+
+ async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) as client:
+ async with client.stream("GET", url, headers=headers) as response:
+ # Handle resume response
+ if response.status_code == 416:
+ # Range not satisfiable - file already complete
+ logger.info("File already fully downloaded")
+ return destination
+
+ if response.status_code == 206:
+ # Partial content - resuming
+ content_range = response.headers.get("content-range", "")
+ if "/" in content_range:
+ total = int(content_range.split("/")[1])
+ else:
+ total = downloaded + int(response.headers.get("content-length", 0))
+ elif response.status_code == 200:
+ # Fresh download
+ total = int(response.headers.get("content-length", 0))
+ downloaded = 0
+ mode = "wb"
+ else:
+ response.raise_for_status()
+
+ logger.info(f"Downloading {url} ({total:,} bytes)")
+
+ with open(destination, mode) as f:
+ async for chunk in response.aiter_bytes(chunk_size=self.chunk_size):
+ f.write(chunk)
+ downloaded += len(chunk)
+ if progress_callback:
+ progress_callback(downloaded, total)
+
+ logger.info(f"Downloaded: {destination} ({downloaded:,} bytes)")
+ return destination
+
+ def download_sync(
+ self,
+ url: str,
+ destination: Path,
+ progress_callback: Optional[Callable[[int, int], None]] = None,
+ ) -> Path:
+ """Synchronous wrapper for download."""
+ return asyncio.run(self.download(url, destination, progress_callback))
+
+
+def create_progress_bar(description: str = "Downloading"):
+ """Create a Rich progress bar callback.
+
+ Returns:
+ Tuple of (progress, task_id, callback_function).
+ """
+ from rich.progress import Progress, BarColumn, DownloadColumn, TransferSpeedColumn
+
+ progress = Progress(
+ "[progress.description]{task.description}",
+ BarColumn(),
+ DownloadColumn(),
+ TransferSpeedColumn(),
+ )
+ task_id = progress.add_task(description, total=None)
+
+ def callback(downloaded: int, total: int):
+ progress.update(task_id, completed=downloaded, total=total)
+
+ return progress, task_id, callback
diff --git a/france-market-scanner/src/extractors/__init__.py b/france-market-scanner/src/extractors/__init__.py
new file mode 100644
index 0000000..be48db6
--- /dev/null
+++ b/france-market-scanner/src/extractors/__init__.py
@@ -0,0 +1,6 @@
+"""Data extractors for French public APIs."""
+from .sirene import SireneExtractor
+from .inpi import INPIExtractor
+from .bodacc import BODACCExtractor
+
+__all__ = ["SireneExtractor", "INPIExtractor", "BODACCExtractor"]
diff --git a/france-market-scanner/src/extractors/bodacc.py b/france-market-scanner/src/extractors/bodacc.py
new file mode 100644
index 0000000..9a8c25e
--- /dev/null
+++ b/france-market-scanner/src/extractors/bodacc.py
@@ -0,0 +1,658 @@
+"""BODACC data extractor - Legal announcements from French official gazette."""
+import asyncio
+import json
+import re
+from pathlib import Path
+from datetime import datetime, timedelta
+from typing import Optional
+import httpx
+import duckdb
+from loguru import logger
+from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
+
+from src.core.database import DatabaseManager
+
+
+class BODACCExtractor:
+ """Extract legal announcements from BODACC.
+
+ BODACC (Bulletin Officiel des Annonces Civiles et Commerciales) contains:
+ - Type A: Sales, creations, insolvency procedures
+ - Type B: Modifications, deregistrations
+ - Type C: Annual account deposits
+
+ Data is available via OpenDataSoft API (free, no auth required).
+ """
+
+ # OpenDataSoft API
+ API_BASE = "https://bodacc-datadila.opendatasoft.com/api/v2"
+ DATASET = "annonces-commerciales"
+
+ def __init__(self, config: dict):
+ self.config = config
+ self.page_size = config.get("bodacc", {}).get("page_size", 100)
+
+ def download(
+ self,
+ output_dir: Path,
+ year: Optional[int] = None,
+ days: int = 30,
+ source: str = "api",
+ ) -> list[Path]:
+ """Download BODACC announcements.
+
+ Args:
+ output_dir: Directory to save files.
+ year: Specific year (for bulk historical).
+ days: Number of recent days to fetch (API mode).
+ source: "api" for OpenDataSoft API, "ftps" for bulk historical.
+
+ Returns:
+ List of downloaded file paths.
+ """
+ output_dir = Path(output_dir)
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ if source == "ftps":
+ logger.warning("FTPS mode not yet implemented - contact DILA for access")
+ return []
+
+ # API mode - fetch recent announcements
+ return asyncio.run(self._download_api(output_dir, year, days))
+
+ async def _download_api(
+ self,
+ output_dir: Path,
+ year: Optional[int],
+ days: int,
+ ) -> list[Path]:
+ """Download via OpenDataSoft API with date windowing.
+
+ Splits large date ranges into 30-day windows to avoid API limit of 10K records.
+ """
+ downloaded = []
+
+ # Build date windows
+ windows = self._build_date_windows(year, days)
+ total_windows = len(windows)
+
+ if total_windows > 1:
+ logger.info(f"Splitting request into {total_windows} date windows")
+ else:
+ start, end = windows[0]
+ logger.info(f"Fetching BODACC announcements: {start} to {end}")
+
+ all_records = []
+
+ async with httpx.AsyncClient(timeout=60) as client:
+ with Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ TextColumn("{task.completed} records"),
+ ) as progress:
+ task = progress.add_task("Fetching announcements...", total=None)
+
+ for i, (start_date, end_date) in enumerate(windows):
+ if total_windows > 1:
+ logger.info(f"Window {i+1}/{total_windows}: {start_date} to {end_date}")
+
+ records = await self._fetch_window(
+ client, start_date, end_date, progress, task
+ )
+ all_records.extend(records)
+
+ logger.info(f"Fetched {len(all_records)} announcements total")
+
+ # Save to JSON file
+ if all_records:
+ date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
+ output_file = output_dir / f"bodacc_{date_str}.json"
+
+ with open(output_file, "w", encoding="utf-8") as f:
+ json.dump(all_records, f, ensure_ascii=False, indent=2)
+
+ downloaded.append(output_file)
+ logger.info(f"Saved JSON to: {output_file}")
+
+ # Also save flattened parquet
+ parquet_file = self._save_parquet(output_file, year)
+ if parquet_file:
+ downloaded.append(parquet_file)
+
+ return downloaded
+
+ def _save_parquet(self, json_file: Path, year: Optional[int] = None) -> Optional[Path]:
+ """Convert JSON to flattened Parquet file.
+
+ Automatically flattens nested JSON structures based on the schema.
+
+ Args:
+ json_file: Path to JSON file.
+ year: Year for naming (if available).
+
+ Returns:
+ Path to created Parquet file, or None on error.
+ """
+ try:
+ # Determine output filename
+ if year:
+ parquet_file = json_file.parent / f"bodacc_{year}.parquet"
+ else:
+ parquet_file = json_file.with_suffix(".parquet")
+
+ con = duckdb.connect()
+
+ # Load JSON and get schema
+ con.execute(f"CREATE TABLE raw AS SELECT * FROM read_json_auto('{json_file}')")
+
+ # Build flattening query dynamically
+ select_cols = self._build_flatten_query(con, "raw")
+
+ con.execute(f"CREATE TABLE flattened AS SELECT {select_cols} FROM raw")
+
+ # Export to parquet with compression
+ con.execute(f"COPY flattened TO '{parquet_file}' (FORMAT PARQUET, COMPRESSION ZSTD)")
+
+ row_count = con.execute("SELECT COUNT(*) FROM flattened").fetchone()[0]
+ file_size_mb = parquet_file.stat().st_size / (1024 * 1024)
+
+ logger.info(f"Saved Parquet to: {parquet_file} ({row_count:,} rows, {file_size_mb:.1f} MB)")
+
+ con.close()
+ return parquet_file
+
+ except Exception as e:
+ logger.error(f"Error creating Parquet file: {e}")
+ return None
+
+ def _build_flatten_query(
+ self,
+ con: duckdb.DuckDBPyConnection,
+ table: str,
+ max_depth: int = 4,
+ strip_prefixes: list[str] = None
+ ) -> str:
+ """Build SELECT clause that flattens nested STRUCT columns.
+
+ Args:
+ con: DuckDB connection.
+ table: Table name to inspect.
+ max_depth: Maximum nesting depth to flatten.
+ strip_prefixes: Prefixes to remove from column aliases.
+
+ Returns:
+ SELECT clause string with flattened columns.
+ """
+ if strip_prefixes is None:
+ strip_prefixes = ["record_fields_", "record_"]
+
+ schema = con.execute(f"DESCRIBE {table}").fetchall()
+ columns = []
+
+ for col_name, col_type, *_ in schema:
+ flat_cols = self._flatten_column(col_name, col_type, col_name, max_depth)
+ columns.extend(flat_cols)
+
+ # Strip prefixes from aliases
+ cleaned_columns = []
+ seen_aliases = {}
+
+ for col in columns:
+ if " AS " in col:
+ path, alias = col.rsplit(" AS ", 1)
+ original_alias = alias
+
+ # Strip prefixes
+ for prefix in strip_prefixes:
+ if alias.startswith(prefix):
+ alias = alias[len(prefix):]
+
+ # Handle collisions by keeping distinguishing prefix
+ if alias in seen_aliases:
+ # Use a more specific name for the collision
+ # Find the shortest unique suffix from original
+ alias = original_alias.replace("record_fields_", "fields_")
+ if alias in seen_aliases:
+ alias = original_alias
+
+ seen_aliases[alias] = True
+ col = f"{path} AS {alias}"
+
+ cleaned_columns.append(col)
+
+ return ", ".join(cleaned_columns)
+
+ def _flatten_column(
+ self,
+ path: str,
+ col_type: str,
+ alias_prefix: str,
+ max_depth: int,
+ current_depth: int = 0
+ ) -> list[str]:
+ """Recursively flatten a column based on its type.
+
+ Preserves parent field names in the alias to prevent collisions.
+
+ Args:
+ path: SQL path to the column (e.g., "record.fields.name").
+ col_type: DuckDB type string.
+ alias_prefix: Prefix for the flattened column alias.
+ max_depth: Maximum depth to recurse.
+ current_depth: Current recursion depth.
+
+ Returns:
+ List of "path AS alias" expressions.
+ """
+ # Skip certain columns that shouldn't be flattened
+ if alias_prefix == "links":
+ return []
+
+ # Handle STRUCT types - flatten their fields
+ if col_type.startswith("STRUCT(") and current_depth < max_depth:
+ # Parse struct fields from type string
+ inner = col_type[7:-1] # Remove "STRUCT(" and ")"
+ fields = self._parse_struct_fields(inner)
+
+ result = []
+ for field_name, field_type in fields:
+ new_path = f"{path}.{field_name}"
+ # Keep parent prefix to prevent collisions
+ new_alias = f"{alias_prefix}_{field_name}"
+
+ result.extend(self._flatten_column(
+ new_path, field_type, new_alias, max_depth, current_depth + 1
+ ))
+ return result
+
+ # Handle JSON strings that contain structured data - extract as JSON
+ if "jugement" in alias_prefix.lower() or "acte" in alias_prefix.lower() or "depot" in alias_prefix.lower():
+ if col_type == "VARCHAR" and current_depth < max_depth:
+ # These are JSON strings, try to extract common fields
+ return self._extract_json_fields(path, alias_prefix)
+
+ # Base case: return the column as-is
+ return [f"{path} AS {alias_prefix}"]
+
+ def _extract_json_fields(self, path: str, alias_prefix: str) -> list[str]:
+ """Extract fields from JSON string columns.
+
+ Args:
+ path: SQL path to the JSON column.
+ alias_prefix: Full prefix for extracted field aliases.
+
+ Returns:
+ List of json_extract expressions.
+ """
+ # Determine which JSON field type this is
+ alias_lower = alias_prefix.lower()
+ if "jugement" in alias_lower:
+ field_key = "jugement"
+ elif "acte" in alias_lower:
+ field_key = "acte"
+ elif "depot" in alias_lower:
+ field_key = "depot"
+ else:
+ return [f"{path} AS {alias_prefix}"]
+
+ # Common fields in BODACC JSON columns
+ json_fields = {
+ "jugement": [
+ ("type", "VARCHAR"),
+ ("famille", "VARCHAR"),
+ ("nature", "VARCHAR"),
+ ("date", "DATE"),
+ ("complementJugement", "VARCHAR"),
+ ],
+ "acte": [
+ ("dateImmatriculation", "DATE"),
+ ("dateCommencementActivite", "DATE"),
+ ("creation.categorieCreation", "VARCHAR"),
+ ],
+ "depot": [
+ ("dateCloture", "DATE"),
+ ("typeDepot", "VARCHAR"),
+ ("descriptif", "VARCHAR"),
+ ],
+ }
+
+ result = []
+ fields = json_fields.get(field_key, [])
+
+ for field_path, field_type in fields:
+ json_path = f"$.{field_path}"
+ # Use full prefix to prevent collisions
+ alias = f"{alias_prefix}_{field_path.replace('.', '_')}"
+
+ if field_type == "DATE":
+ result.append(f"TRY_CAST(json_extract_string({path}, '{json_path}') AS DATE) AS {alias}")
+ else:
+ result.append(f"json_extract_string({path}, '{json_path}') AS {alias}")
+
+ return result if result else [f"{path} AS {alias_prefix}"]
+
+ def _parse_struct_fields(self, struct_inner: str) -> list[tuple[str, str]]:
+ """Parse field definitions from a STRUCT type string.
+
+ Args:
+ struct_inner: Inner part of STRUCT(...) type definition.
+
+ Returns:
+ List of (field_name, field_type) tuples.
+ """
+ fields = []
+ depth = 0
+ current_field = ""
+
+ for char in struct_inner:
+ if char == "(" or char == "[":
+ depth += 1
+ current_field += char
+ elif char == ")" or char == "]":
+ depth -= 1
+ current_field += char
+ elif char == "," and depth == 0:
+ field = current_field.strip()
+ if field:
+ fields.append(self._parse_field_def(field))
+ current_field = ""
+ else:
+ current_field += char
+
+ # Don't forget the last field
+ field = current_field.strip()
+ if field:
+ fields.append(self._parse_field_def(field))
+
+ return fields
+
+ def _parse_field_def(self, field_def: str) -> tuple[str, str]:
+ """Parse a single field definition like 'name VARCHAR' or '"timestamp" TIMESTAMP'.
+
+ Args:
+ field_def: Field definition string.
+
+ Returns:
+ Tuple of (field_name, field_type).
+ """
+ field_def = field_def.strip()
+
+ # Handle quoted field names
+ if field_def.startswith('"'):
+ end_quote = field_def.find('"', 1)
+ name = field_def[1:end_quote]
+ type_str = field_def[end_quote + 1:].strip()
+ else:
+ parts = field_def.split(None, 1)
+ name = parts[0]
+ type_str = parts[1] if len(parts) > 1 else "VARCHAR"
+
+ return (name, type_str)
+
+ def load(
+ self,
+ db: DatabaseManager,
+ source_dir: Path,
+ ) -> dict[str, int]:
+ """Load BODACC data into DuckDB.
+
+ Args:
+ db: Database manager.
+ source_dir: Directory containing JSON files.
+
+ Returns:
+ Dict with row counts.
+ """
+ source_dir = Path(source_dir)
+ stats = {"bodacc_annonces": 0}
+
+ json_files = list(source_dir.glob("*.json"))
+ logger.info(f"Found {len(json_files)} JSON files to load")
+
+ for json_file in json_files:
+ try:
+ with open(json_file, encoding="utf-8") as f:
+ records = json.load(f)
+
+ count = self._load_records(db, records, json_file.name)
+ stats["bodacc_annonces"] += count
+ logger.info(f"Loaded {count} records from {json_file.name}")
+
+ except Exception as e:
+ logger.error(f"Error loading {json_file}: {e}")
+
+ return stats
+
+ def _load_records(
+ self,
+ db: DatabaseManager,
+ records: list,
+ source_file: str,
+ ) -> int:
+ """Load announcement records into database."""
+ count = 0
+
+ for record in records:
+ try:
+ # Extract fields from OpenDataSoft record format
+ fields = record.get("record", {}).get("fields", {})
+ if not fields:
+ fields = record.get("fields", {})
+ if not fields:
+ fields = record
+
+ annonce_id = fields.get("id") or record.get("record", {}).get("id")
+ if not annonce_id:
+ continue
+
+ # Extract SIREN from various possible fields
+ siren = self._extract_siren(fields)
+
+ # Parse date
+ date_parution = fields.get("dateparution")
+
+ # Determine bulletin type from id or content
+ type_bulletin = self._get_bulletin_type(annonce_id, fields)
+
+ # Build details JSON for type-specific data
+ details = {
+ k: v for k, v in fields.items()
+ if k not in [
+ "id", "dateparution", "numerodepartement",
+ "nomgreffeorigine", "tribunal"
+ ]
+ }
+
+ db.execute("""
+ INSERT OR REPLACE INTO bodacc_annonces (
+ id, siren, numero_annonce, date_parution, numero_parution,
+ type_bulletin, famille, nature, denomination, forme_juridique,
+ adresse, code_postal, ville, activite, details,
+ type_procedure, date_jugement, tribunal,
+ date_cloture_exercice, contenu_annonce,
+ _source_file
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, [
+ annonce_id,
+ siren,
+ fields.get("numeroannonce"),
+ date_parution,
+ fields.get("numeroparution"),
+ type_bulletin,
+ fields.get("familleavis"),
+ fields.get("typeavis"),
+ fields.get("denomination") or fields.get("raisonSociale"),
+ fields.get("formejuridique"),
+ fields.get("adresse"),
+ fields.get("codepostal"),
+ fields.get("ville") or fields.get("nomCommune"),
+ fields.get("activite") or fields.get("descriptif"),
+ json.dumps(details, ensure_ascii=False) if details else None,
+ fields.get("typeprocedure"),
+ fields.get("datejugement"),
+ fields.get("tribunal") or fields.get("nomgreffeorigine"),
+ fields.get("datecloture"),
+ fields.get("contenu") or fields.get("texte"),
+ source_file,
+ ])
+ count += 1
+
+ except Exception as e:
+ logger.debug(f"Error inserting record: {e}")
+
+ return count
+
+ def _extract_siren(self, fields: dict) -> Optional[str]:
+ """Extract SIREN from various possible field names."""
+ # Try direct SIREN field
+ siren = fields.get("siren") or fields.get("numeroImmatriculation")
+
+ if not siren:
+ # Try to extract from RCS number
+ rcs = fields.get("registre") or fields.get("numeroRcs")
+ if rcs:
+ # RCS format: "123 456 789 RCS Paris"
+ match = re.search(r"(\d{3})\s*(\d{3})\s*(\d{3})", str(rcs))
+ if match:
+ siren = "".join(match.groups())
+
+ # Validate SIREN format
+ if siren:
+ siren = re.sub(r"\D", "", str(siren))
+ if len(siren) == 9 and siren.isdigit():
+ return siren
+
+ return None
+
+ def _get_bulletin_type(self, annonce_id: str, fields: dict) -> str:
+ """Determine bulletin type (A, B, or C)."""
+ # Try to extract from ID
+ if annonce_id:
+ id_upper = str(annonce_id).upper()
+ if "BODA" in id_upper:
+ return "A"
+ elif "BODB" in id_upper:
+ return "B"
+ elif "BODC" in id_upper:
+ return "C"
+
+ # Try to infer from content
+ famille = (fields.get("familleavis") or "").lower()
+ if "vente" in famille or "creation" in famille or "collectif" in famille:
+ return "A"
+ elif "modification" in famille or "radiation" in famille:
+ return "B"
+ elif "depot" in famille or "compte" in famille:
+ return "C"
+
+ return "A" # Default
+
+ def _build_date_windows(self, year: Optional[int], days: int) -> list[tuple[str, str]]:
+ """Split date range into windows to avoid API limit of 10K records.
+
+ Uses 7-day windows for recent data (BODACC has ~2-3K announcements/day).
+ Uses 14-day windows for yearly data.
+
+ Args:
+ year: Specific year to fetch (uses 14-day windows).
+ days: Number of recent days to fetch (uses 7-day windows).
+
+ Returns:
+ List of (start_date, end_date) tuples as ISO strings.
+ """
+ windows = []
+ window_size = 7 # 7 days should stay well under 10K limit
+
+ if year:
+ # Full year: ~26 two-week windows
+ window_size = 14
+ start = datetime(year, 1, 1)
+ end = datetime(year, 12, 31)
+ current = start
+
+ while current < end:
+ window_end = min(current + timedelta(days=window_size - 1), end)
+ windows.append((
+ current.strftime("%Y-%m-%d"),
+ window_end.strftime("%Y-%m-%d")
+ ))
+ current = window_end + timedelta(days=1)
+ else:
+ # Recent days: 7-day windows working backwards
+ current_end = datetime.now()
+ remaining = days
+
+ while remaining > 0:
+ window_days = min(window_size, remaining)
+ window_start = current_end - timedelta(days=window_days)
+
+ windows.append((
+ window_start.strftime("%Y-%m-%d"),
+ current_end.strftime("%Y-%m-%d")
+ ))
+
+ current_end = window_start
+ remaining -= window_days
+
+ return windows
+
+ async def _fetch_window(
+ self,
+ client: httpx.AsyncClient,
+ start_date: str,
+ end_date: str,
+ progress,
+ task,
+ ) -> list:
+ """Fetch all records for a single date window.
+
+ Args:
+ client: HTTP client.
+ start_date: Start date (ISO format).
+ end_date: End date (ISO format).
+ progress: Rich progress bar.
+ task: Progress task ID.
+
+ Returns:
+ List of announcement records.
+ """
+ where = f"dateparution >= '{start_date}' AND dateparution <= '{end_date}'"
+ records = []
+ offset = 0
+ max_offset = 9900 # OpenDataSoft API limit
+
+ while offset < max_offset:
+ url = f"{self.API_BASE}/catalog/datasets/{self.DATASET}/records"
+ params = {
+ "where": where,
+ "limit": self.page_size,
+ "offset": offset,
+ "order_by": "dateparution DESC",
+ }
+
+ try:
+ response = await client.get(url, params=params)
+ response.raise_for_status()
+ except httpx.HTTPStatusError as e:
+ if e.response.status_code == 400 and offset > 0:
+ logger.warning(f"API limit reached at offset {offset}")
+ break
+ raise
+
+ data = response.json()
+ batch = data.get("records", [])
+
+ if not batch:
+ break
+
+ records.extend(batch)
+ offset += len(batch)
+ progress.update(task, completed=progress.tasks[task].completed + len(batch))
+
+ # Check if we've fetched all records for this window
+ total = data.get("total_count", 0)
+ if offset >= total:
+ break
+
+ return records
diff --git a/france-market-scanner/src/extractors/inpi.py b/france-market-scanner/src/extractors/inpi.py
new file mode 100644
index 0000000..f6a854e
--- /dev/null
+++ b/france-market-scanner/src/extractors/inpi.py
@@ -0,0 +1,334 @@
+"""INPI data extractor - Annual accounts from French IP office.
+
+Outputs Parquet files directly - no database needed.
+"""
+import subprocess
+import xml.etree.ElementTree as ET
+from pathlib import Path
+from typing import Optional
+from concurrent.futures import ProcessPoolExecutor, as_completed
+import multiprocessing
+import tempfile
+import httpx
+import csv
+import re
+from loguru import logger
+from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
+
+
+def _process_archive_worker(archive_path: str) -> list[dict]:
+ """Worker function to process a single 7z archive (runs in separate process)."""
+ archive_path = Path(archive_path)
+ records = []
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ result = subprocess.run(
+ ["7z", "x", str(archive_path), f"-o{tmpdir}", "-y"],
+ capture_output=True,
+ text=True,
+ )
+
+ if result.returncode != 0:
+ return records
+
+ tmppath = Path(tmpdir)
+ for xml_file in tmppath.rglob("*.xml"):
+ try:
+ record = _parse_xml_file(xml_file)
+ if record:
+ records.append(record)
+ except Exception:
+ pass
+
+ return records
+
+
+def _parse_xml_file(xml_path: Path) -> Optional[dict]:
+ """Parse a single XML bilan file and return structured data."""
+ tree = ET.parse(xml_path)
+ root = tree.getroot()
+
+ ns = {"inpi": "fr:inpi:odrncs:bilansSaisisXML"}
+
+ for bilan in root.findall(".//inpi:bilan", ns):
+ identite = bilan.find("inpi:identite", ns)
+ if identite is None:
+ continue
+
+ siren_el = identite.find("inpi:siren", ns)
+ siren = siren_el.text.strip() if siren_el is not None and siren_el.text else None
+ if not siren or len(siren) != 9:
+ continue
+
+ date_el = identite.find("inpi:date_cloture_exercice", ns)
+ date_cloture_raw = date_el.text.strip() if date_el is not None and date_el.text else None
+ if not date_cloture_raw or date_cloture_raw == "00000000":
+ continue
+
+ try:
+ date_cloture = f"{date_cloture_raw[:4]}-{date_cloture_raw[4:6]}-{date_cloture_raw[6:8]}"
+ annee = int(date_cloture_raw[:4])
+ except (ValueError, IndexError):
+ continue
+
+ # Get bilan type
+ type_el = identite.find("inpi:code_type_bilan", ns)
+ bilan_type = type_el.text.strip() if type_el is not None and type_el.text else "C"
+
+ # Format date_depot
+ depot_el = identite.find("inpi:date_depot", ns)
+ date_depot_raw = depot_el.text.strip() if depot_el is not None and depot_el.text else None
+ date_depot = None
+ if date_depot_raw and len(date_depot_raw) == 8:
+ date_depot = f"{date_depot_raw[:4]}-{date_depot_raw[4:6]}-{date_depot_raw[6:8]}"
+
+ # Parse duree
+ duree_el = identite.find("inpi:duree_exercice_n", ns)
+ duree_raw = duree_el.text.strip() if duree_el is not None and duree_el.text else None
+ duree = int(duree_raw) if duree_raw and duree_raw.isdigit() else None
+
+ # Helper to get element text
+ def get_text(parent, path):
+ el = parent.find(path, ns)
+ return el.text.strip() if el is not None and el.text else None
+
+ # Extract liasse codes
+ detail = bilan.find("inpi:detail", ns)
+ liasse_data = {}
+ if detail is not None:
+ for page in detail.findall("inpi:page", ns):
+ for liasse in page.findall("inpi:liasse", ns):
+ code = liasse.get("code", "")
+ m1 = liasse.get("m1") or liasse.get("m3")
+ if code and m1:
+ try:
+ m1_str = str(m1)
+ if m1_str.startswith("-"):
+ value = -int(m1_str[1:]) / 100
+ else:
+ value = int(m1_str) / 100
+ liasse_data[code] = value
+ except (ValueError, TypeError):
+ pass
+
+ # Helper to get value from multiple possible codes
+ def get_val(*codes):
+ for c in codes:
+ if c in liasse_data:
+ return liasse_data[c]
+ return None
+
+ # Return flat record with all data
+ return {
+ "siren": siren,
+ "date_cloture": date_cloture,
+ "annee_cloture": annee,
+ "duree_exercice": duree,
+ "type_comptes": bilan_type,
+ "date_depot": date_depot,
+ "code_greffe": get_text(identite, "inpi:code_greffe"),
+ "confidentialite": get_text(identite, "inpi:code_confidentialite"),
+ # Bilan - Actif
+ "immobilisations_incorporelles": get_val("028", "AB"),
+ "immobilisations_corporelles": get_val("040", "AN"),
+ "immobilisations_financieres": get_val("044", "CU"),
+ "actif_immobilise_net": get_val("050", "BJ"),
+ "stocks": get_val("060", "BT"),
+ "creances_clients": get_val("068", "BX"),
+ "disponibilites": get_val("072", "CF"),
+ "actif_circulant": get_val("080", "CJ"),
+ "total_actif": get_val("110", "CO"),
+ # Bilan - Passif
+ "capital_social": get_val("120", "DA"),
+ "reserves": get_val("134", "DG"),
+ "resultat_exercice": get_val("136", "DI"),
+ "capitaux_propres": get_val("142", "DL"),
+ "dettes": get_val("156", "EC"),
+ "total_passif": get_val("180", "EE"),
+ # Compte de résultat
+ "chiffre_affaires": get_val("218", "FJ", "FL"),
+ "charges_personnel": get_val("264", "FY"),
+ "resultat_exploitation": get_val("270", "GG"),
+ "resultat_financier": get_val("290", "GV"),
+ "resultat_exceptionnel": get_val("HI"),
+ "resultat_net": get_val("310", "HN"),
+ }
+
+ return None
+
+
+class INPIExtractor:
+ """Extract annual accounts from INPI and output to Parquet files."""
+
+ MIRROR_BASE = "http://data.cquest.org/inpi_rncs/comptes"
+
+ def __init__(self, config: dict = None):
+ self.config = config or {}
+
+ def download_mirror(
+ self,
+ output_dir: Path,
+ years: list[int],
+ max_files_per_year: Optional[int] = None,
+ ) -> list[Path]:
+ """Download INPI data from data.cquest.org mirror."""
+ output_dir = Path(output_dir)
+ output_dir.mkdir(parents=True, exist_ok=True)
+ downloaded = []
+
+ logger.info(f"Downloading from mirror: {self.MIRROR_BASE}")
+
+ for year in years:
+ if year < 2017 or year > 2023:
+ logger.warning(f"Year {year} not available on mirror (2017-2023 only)")
+ continue
+
+ year_dir = output_dir / str(year)
+ year_dir.mkdir(exist_ok=True)
+
+ year_url = f"{self.MIRROR_BASE}/{year}/"
+ try:
+ response = httpx.get(year_url, timeout=30)
+ response.raise_for_status()
+ except Exception as e:
+ logger.error(f"Failed to list files for {year}: {e}")
+ continue
+
+ files = re.findall(r'href="(bilans_saisis_\d+\.7z)"', response.text)
+
+ if max_files_per_year:
+ files = files[:max_files_per_year]
+
+ logger.info(f"Year {year}: {len(files)} files to download")
+
+ with Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ TextColumn("{task.completed}/{task.total}"),
+ ) as progress:
+ task = progress.add_task(f"Downloading {year}...", total=len(files))
+
+ for filename in files:
+ local_path = year_dir / filename
+
+ if local_path.exists():
+ progress.update(task, advance=1)
+ continue
+
+ file_url = f"{year_url}{filename}"
+ try:
+ with httpx.stream("GET", file_url, timeout=60) as r:
+ r.raise_for_status()
+ with open(local_path, "wb") as f:
+ for chunk in r.iter_bytes(chunk_size=8192):
+ f.write(chunk)
+ downloaded.append(local_path)
+ except Exception as e:
+ logger.error(f"Failed to download {filename}: {e}")
+
+ progress.update(task, advance=1)
+
+ logger.info(f"Downloaded {len(downloaded)} new files")
+ return downloaded
+
+ def extract_to_parquet(
+ self,
+ source_dir: Path,
+ output_dir: Path,
+ year: Optional[int] = None,
+ workers: Optional[int] = None,
+ ) -> dict[str, int]:
+ """Extract INPI XML data to Parquet files.
+
+ Args:
+ source_dir: Directory containing downloaded 7z files.
+ output_dir: Directory for output Parquet files.
+ year: Specific year to process, or None for all.
+ workers: Number of parallel workers (default: CPU count).
+
+ Returns:
+ Dict with record counts per year.
+ """
+ import time
+
+ source_dir = Path(source_dir)
+ output_dir = Path(output_dir)
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ if workers is None:
+ workers = multiprocessing.cpu_count()
+
+ stats = {}
+
+ # Find year directories
+ if year:
+ year_dirs = [source_dir / str(year)]
+ else:
+ year_dirs = [d for d in source_dir.iterdir() if d.is_dir() and d.name.isdigit()]
+
+ for year_dir in sorted(year_dirs):
+ if not year_dir.exists():
+ continue
+
+ year_name = year_dir.name
+ logger.info(f"Processing year: {year_name}")
+
+ # Find all 7z files
+ archives = list(year_dir.glob("*.7z"))
+ logger.info(f"Found {len(archives)} archives, using {workers} workers")
+
+ # Process archives in parallel
+ archive_paths = [str(a) for a in archives]
+ all_records = []
+
+ start_time = time.time()
+ with Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ TextColumn("{task.completed}/{task.total} archives"),
+ ) as progress:
+ task = progress.add_task(f"Extracting {year_name}...", total=len(archives))
+
+ with ProcessPoolExecutor(max_workers=workers) as executor:
+ futures = {executor.submit(_process_archive_worker, path): path for path in archive_paths}
+
+ for future in as_completed(futures):
+ try:
+ records = future.result()
+ all_records.extend(records)
+ except Exception as e:
+ logger.debug(f"Worker error: {e}")
+ progress.update(task, advance=1)
+
+ extract_time = time.time() - start_time
+ logger.info(f"Extracted {len(all_records):,} records in {extract_time:.1f}s")
+
+ # Write to Parquet
+ if all_records:
+ parquet_path = output_dir / f"inpi_comptes_{year_name}.parquet"
+ logger.info(f"Writing to {parquet_path}...")
+
+ write_start = time.time()
+ self._write_parquet(all_records, parquet_path)
+ write_time = time.time() - write_start
+
+ logger.info(f"Written {len(all_records):,} records in {write_time:.1f}s")
+ stats[year_name] = len(all_records)
+
+ return stats
+
+ def _write_parquet(self, records: list[dict], output_path: Path):
+ """Write records to Parquet file using pyarrow."""
+ try:
+ import pyarrow as pa
+ import pyarrow.parquet as pq
+
+ table = pa.Table.from_pylist(records)
+ pq.write_table(table, output_path, compression='snappy')
+ except ImportError:
+ # Fallback to pandas if pyarrow not available
+ import pandas as pd
+ df = pd.DataFrame(records)
+ df.to_parquet(output_path, compression='snappy', index=False)
diff --git a/france-market-scanner/src/extractors/sirene.py b/france-market-scanner/src/extractors/sirene.py
new file mode 100644
index 0000000..408c6b5
--- /dev/null
+++ b/france-market-scanner/src/extractors/sirene.py
@@ -0,0 +1,352 @@
+"""SIRENE data extractor - French company registry from INSEE."""
+import asyncio
+from pathlib import Path
+from typing import Optional
+from loguru import logger
+from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, DownloadColumn, TransferSpeedColumn
+
+from src.core.database import DatabaseManager
+from src.core.downloader import Downloader
+
+
+class SireneExtractor:
+ """Extract SIRENE data from INSEE/data.gouv.fr.
+
+ SIRENE contains the official French company registry with:
+ - Legal units (unités légales): ~12M companies
+ - Establishments (établissements): ~30M locations
+
+ Data is available as Parquet files, updated monthly.
+ """
+
+ # Direct Parquet file URLs (preferred - smaller and faster)
+ PARQUET_BASE_URL = "https://object.files.data.gouv.fr/data-pipeline-open/siren/stock/"
+
+ PARQUET_FILES = {
+ "unites": "StockUniteLegale_utf8.parquet",
+ "etablissements": "StockEtablissement_utf8.parquet",
+ }
+
+ # Column mappings from SIRENE to our schema
+ UNITE_LEGALE_COLUMNS = {
+ "siren": "siren",
+ "statutDiffusionUniteLegale": "statut_diffusion",
+ "dateCreationUniteLegale": "date_creation",
+ "sigleUniteLegale": "sigle",
+ "denominationUniteLegale": "denomination",
+ "denominationUsuelle1UniteLegale": "denomination_usuelle_1",
+ "denominationUsuelle2UniteLegale": "denomination_usuelle_2",
+ "denominationUsuelle3UniteLegale": "denomination_usuelle_3",
+ "prenom1UniteLegale": "prenom",
+ "nomUniteLegale": "nom",
+ "categorieJuridiqueUniteLegale": "categorie_juridique",
+ "activitePrincipaleUniteLegale": "activite_principale",
+ "nomenclatureActivitePrincipaleUniteLegale": "nomenclature_activite",
+ "trancheEffectifsUniteLegale": "tranche_effectifs",
+ "anneeEffectifsUniteLegale": "annee_effectifs",
+ "caractereEmployeurUniteLegale": "caractere_employeur",
+ "categorieEntreprise": "categorie_entreprise",
+ "anneeCategorieEntreprise": "annee_categorie_entreprise",
+ "economieSocialeSolidaireUniteLegale": "economie_sociale_solidaire",
+ "societeMissionUniteLegale": "societe_mission",
+ "etatAdministratifUniteLegale": "etat_administratif",
+ "dateDernierTraitementUniteLegale": "date_derniere_mise_a_jour",
+ }
+
+ ETABLISSEMENT_COLUMNS = {
+ "siret": "siret",
+ "siren": "siren",
+ "nic": "nic",
+ "statutDiffusionEtablissement": "statut_diffusion",
+ "dateCreationEtablissement": "date_creation",
+ "denominationUsuelleEtablissement": "denomination_usuelle",
+ "enseigne1Etablissement": "enseigne_1",
+ "enseigne2Etablissement": "enseigne_2",
+ "enseigne3Etablissement": "enseigne_3",
+ "activitePrincipaleEtablissement": "activite_principale",
+ "nomenclatureActivitePrincipaleEtablissement": "nomenclature_activite",
+ "activitePrincipaleRegistreMetiersEtablissement": "activite_principale_registre_metiers",
+ "etablissementSiege": "etablissement_siege",
+ "trancheEffectifsEtablissement": "tranche_effectifs",
+ "anneeEffectifsEtablissement": "annee_effectifs",
+ "caractereEmployeurEtablissement": "caractere_employeur",
+ "complementAdresseEtablissement": "complement_adresse",
+ "numeroVoieEtablissement": "numero_voie",
+ "indiceRepetitionEtablissement": "indice_repetition",
+ "typeVoieEtablissement": "type_voie",
+ "libelleVoieEtablissement": "libelle_voie",
+ "codePostalEtablissement": "code_postal",
+ "libelleCommuneEtablissement": "libelle_commune",
+ "libelleCommuneEtrangerEtablissement": "libelle_commune_etranger",
+ "codeCommuneEtablissement": "code_commune",
+ "codeCedexEtablissement": "code_cedex",
+ "libelleCedexEtablissement": "libelle_cedex",
+ "codePaysEtrangerEtablissement": "code_pays_etranger",
+ "libellePaysEtrangerEtablissement": "libelle_pays_etranger",
+ "etatAdministratifEtablissement": "etat_administratif",
+ "dateDernierTraitementEtablissement": "date_derniere_mise_a_jour",
+ }
+
+ def __init__(self, config: dict):
+ self.config = config
+ self.downloader = Downloader(
+ timeout=config.get("downloads", {}).get("timeout_seconds", 600),
+ retry_attempts=config.get("downloads", {}).get("retry_attempts", 3),
+ )
+
+ def download(
+ self,
+ output_dir: Path,
+ file_type: str = "all",
+ ) -> list[Path]:
+ """Download SIRENE Parquet files.
+
+ Args:
+ output_dir: Directory to save files.
+ file_type: "all", "unites", or "etablissements".
+
+ Returns:
+ List of downloaded file paths.
+ """
+ output_dir = Path(output_dir)
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ files_to_download = []
+ if file_type in ("all", "unites"):
+ files_to_download.append(("unites", self.PARQUET_FILES["unites"]))
+ if file_type in ("all", "etablissements"):
+ files_to_download.append(("etablissements", self.PARQUET_FILES["etablissements"]))
+
+ downloaded = []
+
+ with Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ DownloadColumn(),
+ TransferSpeedColumn(),
+ ) as progress:
+ for name, filename in files_to_download:
+ url = f"{self.PARQUET_BASE_URL}{filename}"
+ dest = output_dir / filename
+
+ task = progress.add_task(f"Downloading {name}...", total=None)
+
+ def update_progress(downloaded_bytes: int, total_bytes: int):
+ progress.update(task, completed=downloaded_bytes, total=total_bytes)
+
+ asyncio.run(self.downloader.download(url, dest, update_progress))
+ downloaded.append(dest)
+ logger.info(f"Downloaded: {dest}")
+
+ return downloaded
+
+ def load(
+ self,
+ db: DatabaseManager,
+ source_dir: Path,
+ file_type: str = "all",
+ ) -> dict[str, int]:
+ """Load SIRENE data into DuckDB.
+
+ Args:
+ db: Database manager.
+ source_dir: Directory containing Parquet files.
+ file_type: "all", "unites", or "etablissements".
+
+ Returns:
+ Dict with row counts per table.
+ """
+ source_dir = Path(source_dir)
+ stats = {}
+
+ if file_type in ("all", "unites"):
+ parquet_file = source_dir / self.PARQUET_FILES["unites"]
+ if parquet_file.exists():
+ count = self._load_unites_legales(db, parquet_file)
+ stats["sirene_unites_legales"] = count
+ else:
+ logger.warning(f"File not found: {parquet_file}")
+
+ if file_type in ("all", "etablissements"):
+ parquet_file = source_dir / self.PARQUET_FILES["etablissements"]
+ if parquet_file.exists():
+ count = self._load_etablissements(db, parquet_file)
+ stats["sirene_etablissements"] = count
+ else:
+ logger.warning(f"File not found: {parquet_file}")
+
+ return stats
+
+ def _load_unites_legales(self, db: DatabaseManager, parquet_file: Path) -> int:
+ """Load legal units from Parquet file."""
+ logger.info(f"Loading legal units from: {parquet_file}")
+
+ load_id = db.log_etl_start("sirene_unites_legales", "full", str(parquet_file))
+
+ try:
+ # Clear existing data
+ db.execute("DELETE FROM sirene_unites_legales")
+
+ # Load directly from Parquet with explicit column mapping
+ # Note: File contains historical periods, we load all and filter at query time
+ query = f"""
+ INSERT INTO sirene_unites_legales (
+ siren, statut_diffusion, date_creation, sigle,
+ denomination, denomination_usuelle_1, denomination_usuelle_2, denomination_usuelle_3,
+ prenom, nom, categorie_juridique, activite_principale, nomenclature_activite,
+ tranche_effectifs, annee_effectifs, caractere_employeur,
+ categorie_entreprise, annee_categorie_entreprise,
+ economie_sociale_solidaire, societe_mission,
+ etat_administratif, date_derniere_mise_a_jour,
+ _loaded_at, _source_file
+ )
+ SELECT
+ siren,
+ statutDiffusionUniteLegale,
+ TRY_CAST(dateCreationUniteLegale AS DATE),
+ sigleUniteLegale,
+ denominationUniteLegale,
+ denominationUsuelle1UniteLegale,
+ denominationUsuelle2UniteLegale,
+ denominationUsuelle3UniteLegale,
+ prenom1UniteLegale,
+ nomUniteLegale,
+ categorieJuridiqueUniteLegale,
+ activitePrincipaleUniteLegale,
+ nomenclatureActivitePrincipaleUniteLegale,
+ trancheEffectifsUniteLegale,
+ TRY_CAST(anneeEffectifsUniteLegale AS INTEGER),
+ caractereEmployeurUniteLegale,
+ categorieEntreprise,
+ TRY_CAST(anneeCategorieEntreprise AS INTEGER),
+ economieSocialeSolidaireUniteLegale,
+ societeMissionUniteLegale,
+ etatAdministratifUniteLegale,
+ TRY_CAST(dateDernierTraitementUniteLegale AS TIMESTAMP),
+ CURRENT_TIMESTAMP,
+ '{parquet_file.name}'
+ FROM read_parquet('{parquet_file}')
+ """
+
+ db.execute(query)
+
+ # Get count
+ count = db.fetchone("SELECT COUNT(*) FROM sirene_unites_legales")[0]
+ logger.info(f"Loaded {count:,} legal units")
+
+ db.log_etl_complete(load_id, "success", count, count)
+ return count
+
+ except Exception as e:
+ logger.error(f"Error loading legal units: {e}")
+ db.log_etl_complete(load_id, "failed", 0, 0, str(e))
+ raise
+
+ def _load_etablissements(self, db: DatabaseManager, parquet_file: Path) -> int:
+ """Load establishments from Parquet file."""
+ logger.info(f"Loading establishments from: {parquet_file}")
+
+ load_id = db.log_etl_start("sirene_etablissements", "full", str(parquet_file))
+
+ try:
+ # Clear existing data
+ db.execute("DELETE FROM sirene_etablissements")
+
+ # Load with explicit column mapping
+ # Note: File contains historical periods, we load all and filter at query time
+ query = f"""
+ INSERT INTO sirene_etablissements (
+ siret, siren, nic, statut_diffusion, date_creation,
+ denomination_usuelle, enseigne_1, enseigne_2, enseigne_3,
+ activite_principale, nomenclature_activite, activite_principale_registre_metiers,
+ etablissement_siege, tranche_effectifs, annee_effectifs, caractere_employeur,
+ complement_adresse, numero_voie, indice_repetition, type_voie, libelle_voie,
+ code_postal, libelle_commune, libelle_commune_etranger, code_commune,
+ code_cedex, libelle_cedex, code_pays_etranger, libelle_pays_etranger,
+ departement, region, etat_administratif, date_derniere_mise_a_jour,
+ _loaded_at, _source_file
+ )
+ SELECT
+ siret,
+ siren,
+ nic,
+ statutDiffusionEtablissement,
+ TRY_CAST(dateCreationEtablissement AS DATE),
+ denominationUsuelleEtablissement,
+ enseigne1Etablissement,
+ enseigne2Etablissement,
+ enseigne3Etablissement,
+ activitePrincipaleEtablissement,
+ nomenclatureActivitePrincipaleEtablissement,
+ activitePrincipaleRegistreMetiersEtablissement,
+ etablissementSiege,
+ trancheEffectifsEtablissement,
+ TRY_CAST(anneeEffectifsEtablissement AS INTEGER),
+ caractereEmployeurEtablissement,
+ complementAdresseEtablissement,
+ numeroVoieEtablissement,
+ indiceRepetitionEtablissement,
+ typeVoieEtablissement,
+ libelleVoieEtablissement,
+ codePostalEtablissement,
+ libelleCommuneEtablissement,
+ libelleCommuneEtrangerEtablissement,
+ codeCommuneEtablissement,
+ codeCedexEtablissement,
+ libelleCedexEtablissement,
+ codePaysEtrangerEtablissement,
+ libellePaysEtrangerEtablissement,
+ -- Derive department from postal code
+ CASE
+ WHEN codePostalEtablissement IS NOT NULL AND LENGTH(codePostalEtablissement) >= 2
+ THEN LEFT(codePostalEtablissement, 2)
+ ELSE NULL
+ END,
+ NULL, -- region
+ etatAdministratifEtablissement,
+ TRY_CAST(dateDernierTraitementEtablissement AS TIMESTAMP),
+ CURRENT_TIMESTAMP,
+ '{parquet_file.name}'
+ FROM read_parquet('{parquet_file}')
+ """
+
+ db.execute(query)
+
+ # Get count
+ count = db.fetchone("SELECT COUNT(*) FROM sirene_etablissements")[0]
+ logger.info(f"Loaded {count:,} establishments")
+
+ db.log_etl_complete(load_id, "success", count, count)
+ return count
+
+ except Exception as e:
+ logger.error(f"Error loading establishments: {e}")
+ db.log_etl_complete(load_id, "failed", 0, 0, str(e))
+ raise
+
+
+# Effectif (employee) code mapping
+EFFECTIF_CODES = {
+ "00": "0 salarié",
+ "01": "1 ou 2 salariés",
+ "02": "3 à 5 salariés",
+ "03": "6 à 9 salariés",
+ "11": "10 à 19 salariés",
+ "12": "20 à 49 salariés",
+ "21": "50 à 99 salariés",
+ "22": "100 à 199 salariés",
+ "31": "200 à 249 salariés",
+ "32": "250 à 499 salariés",
+ "41": "500 à 999 salariés",
+ "42": "1000 à 1999 salariés",
+ "51": "2000 à 4999 salariés",
+ "52": "5000 à 9999 salariés",
+ "53": "10000 salariés et plus",
+ "NN": "Non renseigné",
+}
+
+
+def get_effectif_label(code: str) -> str:
+ """Get human-readable label for effectif code."""
+ return EFFECTIF_CODES.get(code, "Inconnu")
diff --git a/france-market-scanner/src/loaders/__init__.py b/france-market-scanner/src/loaders/__init__.py
new file mode 100644
index 0000000..d0d30a3
--- /dev/null
+++ b/france-market-scanner/src/loaders/__init__.py
@@ -0,0 +1 @@
+"""Data loaders for DuckDB bulk inserts."""
diff --git a/france-market-scanner/src/transformers/__init__.py b/france-market-scanner/src/transformers/__init__.py
new file mode 100644
index 0000000..6606785
--- /dev/null
+++ b/france-market-scanner/src/transformers/__init__.py
@@ -0,0 +1 @@
+"""Data transformers for parsing and normalizing data."""
diff --git a/france-market-scanner/uv.lock b/france-market-scanner/uv.lock
new file mode 100644
index 0000000..1799e60
--- /dev/null
+++ b/france-market-scanner/uv.lock
@@ -0,0 +1,1079 @@
+version = 1
+revision = 1
+requires-python = ">=3.10"
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version < '3.11'",
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668 },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362 },
+]
+
+[[package]]
+name = "bcrypt"
+version = "5.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806 },
+ { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626 },
+ { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853 },
+ { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793 },
+ { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930 },
+ { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194 },
+ { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381 },
+ { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750 },
+ { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757 },
+ { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740 },
+ { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197 },
+ { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974 },
+ { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498 },
+ { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853 },
+ { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626 },
+ { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862 },
+ { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544 },
+ { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787 },
+ { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753 },
+ { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587 },
+ { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178 },
+ { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295 },
+ { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700 },
+ { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034 },
+ { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766 },
+ { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449 },
+ { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310 },
+ { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761 },
+ { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553 },
+ { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009 },
+ { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029 },
+ { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907 },
+ { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500 },
+ { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412 },
+ { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486 },
+ { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940 },
+ { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776 },
+ { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922 },
+ { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367 },
+ { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187 },
+ { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752 },
+ { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881 },
+ { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931 },
+ { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313 },
+ { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290 },
+ { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253 },
+ { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084 },
+ { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185 },
+ { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656 },
+ { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662 },
+ { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240 },
+ { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152 },
+ { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284 },
+ { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643 },
+ { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698 },
+ { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725 },
+ { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912 },
+ { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953 },
+ { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180 },
+ { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791 },
+ { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746 },
+ { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.11.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283 },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504 },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811 },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402 },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217 },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079 },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475 },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829 },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211 },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036 },
+ { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184 },
+ { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790 },
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597 },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574 },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { 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 },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 },
+]
+
+[[package]]
+name = "chdb"
+version = "3.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pandas" },
+ { name = "pyarrow" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/a5/d83bfd989e5819944cf2f238e4d5150669a8aad1c013a1f34af38af17fb2/chdb-3.7.2-cp38-abi3-macosx_10_15_x86_64.whl", hash = "sha256:2cc22753ae4b042d091d810e2de60cb8badfd6ae9829109406b24bab75bbcb6b", size = 97945158 },
+ { url = "https://files.pythonhosted.org/packages/aa/97/460dea74d213d8e5bd831e0b7ab4cf1872b977373f674b8aa3135b1e1093/chdb-3.7.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:baaf88db434196b1db900c1747f6e47c0cde0c175cd3a3aaba7a8770bb56ddb2", size = 87499449 },
+ { url = "https://files.pythonhosted.org/packages/fc/86/c5b8cf8319c9fce0b54e339becf2bd6886cd19784797f29b601d5e8817c1/chdb-3.7.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:605f121162b55bb3944af07c56a6f2290110b8f298c888047457c70105a0667b", size = 122954587 },
+ { url = "https://files.pythonhosted.org/packages/a7/05/8ee5fcbebd3ea6fba18265b688f028e6c758115af0be276b22144e34f9bd/chdb-3.7.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c119f0830c12438a4e4b60b8370859ba50992e8b1b01a8c681e1c57eeb2dc82d", size = 178830227 },
+ { url = "https://files.pythonhosted.org/packages/cd/ff/f7485d5ceec2223032a588abde6815f486d7f164da1f3f716bb342eda2d5/chdb-3.7.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9eb9ad98e0449a63c52d2296785fbb22d2ae8bd94846244179316a8f15581711", size = 145043926 },
+ { url = "https://files.pythonhosted.org/packages/f4/56/f5561b8f10ec5c6638fa288efe0ae48021af3ff7dff8d915517aa5238247/chdb-3.7.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2b8e2f45f0d856130de90d0908dec06581d49c794faf95528733f47883f7f050", size = 187017848 },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004 },
+ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667 },
+ { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807 },
+ { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615 },
+ { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800 },
+ { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707 },
+ { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541 },
+ { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464 },
+ { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838 },
+ { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596 },
+ { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782 },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381 },
+ { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988 },
+ { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451 },
+ { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007 },
+ { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012 },
+ { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728 },
+ { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078 },
+ { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460 },
+ { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237 },
+ { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344 },
+ { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564 },
+ { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415 },
+ { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457 },
+ { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074 },
+ { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569 },
+ { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941 },
+ { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339 },
+ { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315 },
+ { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331 },
+ { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248 },
+ { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089 },
+ { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029 },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222 },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280 },
+ { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958 },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714 },
+ { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970 },
+ { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236 },
+ { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642 },
+ { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126 },
+ { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573 },
+ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 },
+ { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 },
+ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 },
+ { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163 },
+ { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474 },
+ { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132 },
+ { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992 },
+ { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944 },
+ { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957 },
+ { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447 },
+ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528 },
+]
+
+[[package]]
+name = "duckdb"
+version = "1.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7f/da/17c3eb5458af69d54dedc8d18e4a32ceaa8ce4d4c699d45d6d8287e790c3/duckdb-1.4.3.tar.gz", hash = "sha256:fea43e03604c713e25a25211ada87d30cd2a044d8f27afab5deba26ac49e5268", size = 18478418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/3a/ea8e237e1ba40203dea4ed6a8798ea51e66a4c4f34605697025e5fa06fdd/duckdb-1.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa7f1191c59e34b688fcd4e588c1b903a4e4e1f4804945902cf0b20e08a9001", size = 29016021 },
+ { url = "https://files.pythonhosted.org/packages/48/88/07615298a2871362b454237b6a2d7724e6ba0afba2bddedddde5bbf129d5/duckdb-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fef6a053a1c485292000bf0c338bba60f89d334f6a06fc76ba4085a5a322b76", size = 15405906 },
+ { url = "https://files.pythonhosted.org/packages/fa/66/b407ab3cd4822191aa5defb27522213b6ba670437c7da09a062d8b75b0a4/duckdb-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:702dabbc22b27dc5b73e7599c60deef3d8c59968527c36b391773efddd8f4cf1", size = 13732991 },
+ { url = "https://files.pythonhosted.org/packages/33/f0/e8edab80446d87b4e0faf3aaa440f9cfd9d0609c21a4be56174c8ba7d23c/duckdb-1.4.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854b79375fa618f6ffa8d84fb45cbc9db887f6c4834076ea10d20bc106f1fd90", size = 18471503 },
+ { url = "https://files.pythonhosted.org/packages/8c/7a/8d257bc847f0ac6a6639ae0a6e7f35f0b5bfbae472ee4846ee32404670a6/duckdb-1.4.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bb8bd5a3dd205983726185b280a211eacc9f5bc0c4d4505bec8c87ac33a8ccb", size = 20466012 },
+ { url = "https://files.pythonhosted.org/packages/cf/d1/8f6bdaf2da6a076dd63c84ed87fb82d0741c9f4acb3dd476d73ca0a08ffe/duckdb-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:d0ff08388ef8b1d1a4c95c321d6c5fa11201b241036b1ee740f9d841df3d6ba2", size = 12328392 },
+ { url = "https://files.pythonhosted.org/packages/ec/bc/7c5e50e440c8629495678bc57bdfc1bb8e62f61090f2d5441e2bd0a0ed96/duckdb-1.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:366bf607088053dce845c9d24c202c04d78022436cc5d8e4c9f0492de04afbe7", size = 29019361 },
+ { url = "https://files.pythonhosted.org/packages/26/15/c04a4faf0dfddad2259cab72bf0bd4b3d010f2347642541bd254d516bf93/duckdb-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d080e8d1bf2d226423ec781f539c8f6b6ef3fd42a9a58a7160de0a00877a21f", size = 15407465 },
+ { url = "https://files.pythonhosted.org/packages/cb/54/a049490187c9529932fc153f7e1b92a9e145586281fe4e03ce0535a0497c/duckdb-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dc049ba7e906cb49ca2b6d4fbf7b6615ec3883193e8abb93f0bef2652e42dda", size = 13735781 },
+ { url = "https://files.pythonhosted.org/packages/14/b7/ee594dcecbc9469ec3cd1fb1f81cb5fa289ab444b80cfb5640c8f467f75f/duckdb-1.4.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b30245375ea94ab528c87c61fc3ab3e36331180b16af92ee3a37b810a745d24", size = 18470729 },
+ { url = "https://files.pythonhosted.org/packages/df/5f/a6c1862ed8a96d8d930feb6af5e55aadd983310aab75142468c2cb32a2a3/duckdb-1.4.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7c864df027da1ee95f0c32def67e15d02cd4a906c9c1cbae82c09c5112f526b", size = 20471399 },
+ { url = "https://files.pythonhosted.org/packages/5b/80/c05c0b6a6107b618927b7dcabe3bba6a7eecd951f25c9dbcd9c1f9577cc8/duckdb-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:813f189039b46877b5517f1909c7b94a8fe01b4bde2640ab217537ea0fe9b59b", size = 12329359 },
+ { url = "https://files.pythonhosted.org/packages/b0/83/9d8fc3413f854effa680dcad1781f68f3ada8679863c0c94ba3b36bae6ff/duckdb-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc63ffdd03835f660155b37a1b6db2005bcd46e5ad398b8cac141eb305d2a3d", size = 13070898 },
+ { url = "https://files.pythonhosted.org/packages/5a/d7/fdc2139b94297fc5659110a38adde293d025e320673ae5e472b95d323c50/duckdb-1.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6302452e57aef29aae3977063810ed7b2927967b97912947b9cca45c1c21955f", size = 29033112 },
+ { url = "https://files.pythonhosted.org/packages/eb/d9/ca93df1ce19aef8f799e3aaacf754a4dde7e9169c0b333557752d21d076a/duckdb-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:deab351ac43b6282a3270e3d40e3d57b3b50f472d9fd8c30975d88a31be41231", size = 15414646 },
+ { url = "https://files.pythonhosted.org/packages/16/90/9f2748e740f5fc05b739e7c5c25aab6ab4363e5da4c3c70419c7121dc806/duckdb-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5634e40e1e2d972e4f75bced1fbdd9e9e90faa26445c1052b27de97ee546944a", size = 13740477 },
+ { url = "https://files.pythonhosted.org/packages/5f/ec/279723615b4fb454efd823b7efe97cf2504569e2e74d15defbbd6b027901/duckdb-1.4.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:274d4a31aba63115f23e7e7b401e3e3a937f3626dc9dea820a9c7d3073f450d2", size = 18483715 },
+ { url = "https://files.pythonhosted.org/packages/10/63/af20cd20fd7fd6565ea5a1578c16157b6a6e07923e459a6f9b0dc9ada308/duckdb-1.4.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f868a7e6d9b37274a1aa34849ea92aa964e9bd59a5237d6c17e8540533a1e4f", size = 20495188 },
+ { url = "https://files.pythonhosted.org/packages/8c/ab/0acb4b64afb2cc6c1d458a391c64e36be40137460f176c04686c965ce0e0/duckdb-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef7ef15347ce97201b1b5182a5697682679b04c3374d5a01ac10ba31cf791b95", size = 12335622 },
+ { url = "https://files.pythonhosted.org/packages/50/d5/2a795745f6597a5e65770141da6efdc4fd754e5ee6d652f74bcb7f9c7759/duckdb-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:1b9b445970fd18274d5ac07a0b24c032e228f967332fb5ebab3d7db27738c0e4", size = 13075834 },
+ { url = "https://files.pythonhosted.org/packages/fd/76/288cca43a10ddd082788e1a71f1dc68d9130b5d078c3ffd0edf2f3a8719f/duckdb-1.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16952ac05bd7e7b39946695452bf450db1ebbe387e1e7178e10f593f2ea7b9a8", size = 29033392 },
+ { url = "https://files.pythonhosted.org/packages/64/07/cbad3d3da24af4d1add9bccb5fb390fac726ffa0c0cebd29bf5591cef334/duckdb-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de984cd24a6cbefdd6d4a349f7b9a46e583ca3e58ce10d8def0b20a6e5fcbe78", size = 15414567 },
+ { url = "https://files.pythonhosted.org/packages/c4/19/57af0cc66ba2ffb8900f567c9aec188c6ab2a7b3f2260e9c6c3c5f9b57b1/duckdb-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e5457dda91b67258aae30fb1a0df84183a9f6cd27abac1d5536c0d876c6dfa1", size = 13740960 },
+ { url = "https://files.pythonhosted.org/packages/73/dd/23152458cf5fd51e813fadda60b9b5f011517634aa4bb9301f5f3aa951d8/duckdb-1.4.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:006aca6a6d6736c441b02ff5c7600b099bb8b7f4de094b8b062137efddce42df", size = 18484312 },
+ { url = "https://files.pythonhosted.org/packages/1a/7b/adf3f611f11997fc429d4b00a730604b65d952417f36a10c4be6e38e064d/duckdb-1.4.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2813f4635f4d6681cc3304020374c46aca82758c6740d7edbc237fe3aae2744", size = 20495571 },
+ { url = "https://files.pythonhosted.org/packages/40/d5/6b7ddda7713a788ab2d622c7267ec317718f2bdc746ce1fca49b7ff0e50f/duckdb-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:6db124f53a3edcb32b0a896ad3519e37477f7e67bf4811cb41ab60c1ef74e4c8", size = 12335680 },
+ { url = "https://files.pythonhosted.org/packages/e8/28/0670135cf54525081fded9bac1254f78984e3b96a6059cd15aca262e3430/duckdb-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:a8b0a8764e1b5dd043d168c8f749314f7a1252b5a260fa415adaa26fa3b958fd", size = 13075161 },
+ { url = "https://files.pythonhosted.org/packages/b6/f4/a38651e478fa41eeb8e43a0a9c0d4cd8633adea856e3ac5ac95124b0fdbf/duckdb-1.4.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:316711a9e852bcfe1ed6241a5f654983f67e909e290495f3562cccdf43be8180", size = 29042272 },
+ { url = "https://files.pythonhosted.org/packages/16/de/2cf171a66098ce5aeeb7371511bd2b3d7b73a2090603b0b9df39f8aaf814/duckdb-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9e625b2b4d52bafa1fd0ebdb0990c3961dac8bb00e30d327185de95b68202131", size = 15419343 },
+ { url = "https://files.pythonhosted.org/packages/35/28/6b0a7830828d4e9a37420d87e80fe6171d2869a9d3d960bf5d7c3b8c7ee4/duckdb-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:130c6760f6c573f9c9fe9aba56adba0fab48811a4871b7b8fd667318b4a3e8da", size = 13748905 },
+ { url = "https://files.pythonhosted.org/packages/15/4d/778628e194d63967870873b9581c8a6b4626974aa4fbe09f32708a2d3d3a/duckdb-1.4.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c88effaa557a11267706b01419c542fe42f893dee66e5a6daa5974ea2d4a46", size = 18487261 },
+ { url = "https://files.pythonhosted.org/packages/c6/5f/87e43af2e4a0135f9675449563e7c2f9b6f1fe6a2d1691c96b091f3904dd/duckdb-1.4.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b35491db98ccd11d151165497c084a9d29d3dc42fc80abea2715a6c861ca43d", size = 20497138 },
+ { url = "https://files.pythonhosted.org/packages/94/41/abec537cc7c519121a2a83b9a6f180af8915fabb433777dc147744513e74/duckdb-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:23b12854032c1a58d0452e2b212afa908d4ce64171862f3792ba9a596ba7c765", size = 12836056 },
+ { url = "https://files.pythonhosted.org/packages/b1/5a/8af5b96ce5622b6168854f479ce846cf7fb589813dcc7d8724233c37ded3/duckdb-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:90f241f25cffe7241bf9f376754a5845c74775e00e1c5731119dc88cd71e0cb2", size = 13527759 },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 },
+]
+
+[[package]]
+name = "france-market-scanner"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "aiofiles" },
+ { name = "chdb" },
+ { name = "click" },
+ { name = "duckdb" },
+ { name = "httpx" },
+ { name = "loguru" },
+ { name = "lxml" },
+ { name = "pandas" },
+ { name = "paramiko" },
+ { name = "pyarrow" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "rich" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiofiles", specifier = ">=23.0.0" },
+ { name = "chdb", specifier = ">=3.7.2" },
+ { name = "click", specifier = ">=8.1.0" },
+ { name = "duckdb", specifier = ">=1.0.0" },
+ { name = "httpx", specifier = ">=0.27.0" },
+ { name = "loguru", specifier = ">=0.7.0" },
+ { name = "lxml", specifier = ">=5.0.0" },
+ { name = "pandas", specifier = ">=2.3.3" },
+ { name = "paramiko", specifier = ">=3.0.0" },
+ { name = "pyarrow", specifier = ">=15.0.0" },
+ { name = "python-dotenv", specifier = ">=1.0.0" },
+ { name = "pyyaml", specifier = ">=6.0" },
+ { name = "rich", specifier = ">=13.0.0" },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 },
+]
+
+[[package]]
+name = "invoke"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287 },
+]
+
+[[package]]
+name = "loguru"
+version = "0.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "win32-setctime", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
+]
+
+[[package]]
+name = "lxml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589 },
+ { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671 },
+ { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961 },
+ { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087 },
+ { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620 },
+ { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664 },
+ { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397 },
+ { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178 },
+ { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148 },
+ { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035 },
+ { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111 },
+ { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662 },
+ { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973 },
+ { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953 },
+ { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695 },
+ { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051 },
+ { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365 },
+ { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793 },
+ { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362 },
+ { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152 },
+ { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539 },
+ { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853 },
+ { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133 },
+ { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944 },
+ { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535 },
+ { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343 },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419 },
+ { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008 },
+ { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906 },
+ { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357 },
+ { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583 },
+ { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591 },
+ { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887 },
+ { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818 },
+ { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807 },
+ { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179 },
+ { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044 },
+ { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685 },
+ { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127 },
+ { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958 },
+ { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541 },
+ { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426 },
+ { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917 },
+ { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795 },
+ { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759 },
+ { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666 },
+ { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989 },
+ { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456 },
+ { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793 },
+ { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836 },
+ { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494 },
+ { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146 },
+ { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932 },
+ { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060 },
+ { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000 },
+ { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496 },
+ { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779 },
+ { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072 },
+ { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675 },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171 },
+ { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175 },
+ { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688 },
+ { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655 },
+ { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695 },
+ { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841 },
+ { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700 },
+ { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347 },
+ { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248 },
+ { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801 },
+ { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403 },
+ { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974 },
+ { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953 },
+ { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054 },
+ { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421 },
+ { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684 },
+ { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463 },
+ { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437 },
+ { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890 },
+ { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185 },
+ { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895 },
+ { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246 },
+ { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797 },
+ { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404 },
+ { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072 },
+ { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617 },
+ { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930 },
+ { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380 },
+ { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632 },
+ { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171 },
+ { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109 },
+ { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061 },
+ { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233 },
+ { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739 },
+ { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119 },
+ { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665 },
+ { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997 },
+ { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957 },
+ { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372 },
+ { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653 },
+ { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795 },
+ { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023 },
+ { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420 },
+ { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837 },
+ { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205 },
+ { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264 },
+ { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435 },
+ { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913 },
+ { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357 },
+ { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295 },
+ { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913 },
+ { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829 },
+ { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277 },
+ { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433 },
+ { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119 },
+ { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314 },
+ { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768 },
+]
+
+[[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 }
+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 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 },
+ { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 },
+ { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 },
+ { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 },
+ { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 },
+ { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 },
+ { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 },
+ { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 },
+ { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 },
+ { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 },
+ { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 },
+ { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 },
+ { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 },
+ { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 },
+ { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 },
+ { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 },
+ { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 },
+ { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 },
+ { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 },
+ { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 },
+ { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 },
+ { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 },
+ { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 },
+ { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 },
+ { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 },
+ { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 },
+ { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 },
+ { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 },
+ { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 },
+ { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 },
+ { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 },
+ { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 },
+ { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 },
+ { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 },
+ { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 },
+ { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 },
+ { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 },
+ { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 },
+ { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 },
+ { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 },
+ { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 },
+ { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 },
+ { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 },
+ { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 },
+ { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 },
+ { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 },
+ { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 },
+ { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 },
+ { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 },
+ { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 },
+ { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 },
+ { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 },
+ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 },
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/7e/7bae7cbcc2f8132271967aa03e03954fc1e48aa1f3bf32b29ca95fbef352/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166 },
+ { url = "https://files.pythonhosted.org/packages/0f/27/6c13f5b46776d6246ec884ac5817452672156a506d08a1f2abb39961930a/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781 },
+ { url = "https://files.pythonhosted.org/packages/14/1c/83b4998d4860d15283241d9e5215f28b40ac31f497c04b12fa7f428ff370/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247 },
+ { url = "https://files.pythonhosted.org/packages/54/08/cbce72c835d937795571b0464b52069f869c9e78b0c076d416c5269d2718/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807 },
+ { url = "https://files.pythonhosted.org/packages/ff/be/2e647961cd8c980591d75cdcd9e8f647d69fbe05e2a25613dc0a2ea5fb1a/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992 },
+ { url = "https://files.pythonhosted.org/packages/a2/fb/e1652fb8b6fd91ce6ed429143fe2e01ce714711e03e5b762615e7b36172c/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871 },
+ { url = "https://files.pythonhosted.org/packages/62/23/d841207e63c4322842f7cd042ae981cffe715c73376dcad8235fb31debf1/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190 },
+ { url = "https://files.pythonhosted.org/packages/bc/a0/6a842c8421ebfdec0a230e65f61e0dabda6edbef443d999d79b87c273965/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762 },
+ { url = "https://files.pythonhosted.org/packages/0a/d1/c79e0046641186f2134dde05e6181825b911f8bdcef31b19ddd16e232847/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359 },
+ { url = "https://files.pythonhosted.org/packages/fc/f0/74965001d231f28184d6305b8cdc1b6fcd4bf23033f6cb039cfe76c9fca7/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132 },
+ { url = "https://files.pythonhosted.org/packages/65/32/55408d0f46dfebce38017f5bd931affa7256ad6beac1a92a012e1fbc67a7/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977 },
+ { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117 },
+ { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711 },
+ { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355 },
+ { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298 },
+ { url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387 },
+ { url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091 },
+ { url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394 },
+ { url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378 },
+ { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432 },
+ { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201 },
+ { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234 },
+ { url = "https://files.pythonhosted.org/packages/a7/0d/853fd96372eda07c824d24adf02e8bc92bb3731b43a9b2a39161c3667cc4/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088 },
+ { url = "https://files.pythonhosted.org/packages/e3/37/cc636f1f2a9f585434e20a3e6e63422f70bfe4f7f6698e941db52ea1ac9a/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065 },
+ { url = "https://files.pythonhosted.org/packages/ed/69/0b78f37ca3690969beee54103ce5f6021709134e8020767e93ba691a72f1/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640 },
+ { url = "https://files.pythonhosted.org/packages/1d/2a/08569f8252abf590294dbb09a430543ec8f8cc710383abfb3e75cc73aeda/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556 },
+ { url = "https://files.pythonhosted.org/packages/93/e9/a949885a4e177493d61519377952186b6cbfdf1d6002764c664ba28349b5/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562 },
+ { url = "https://files.pythonhosted.org/packages/99/98/9d4ad53b0e9ef901c2ef1d550d2136f5ac42d3fd2988390a6def32e23e48/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719 },
+ { url = "https://files.pythonhosted.org/packages/28/de/5f3711a38341d6e8dd619f6353251a0cdd07f3d6d101a8fd46f4ef87f895/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053 },
+ { url = "https://files.pythonhosted.org/packages/2a/5b/2a3753dc43916501b4183532e7ace862e13211042bceafa253afb5c71272/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859 },
+ { url = "https://files.pythonhosted.org/packages/2c/c5/a18bcdd07a941db3076ef489d036ab16d2bfc2eae0cf27e5a26e29189434/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849 },
+ { url = "https://files.pythonhosted.org/packages/4f/f1/719010ff8061da6e8a26e1980cf090412d4f5f8060b31f0c45d77dd67a01/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840 },
+ { url = "https://files.pythonhosted.org/packages/f5/5a/b3d259083ed8b4d335270c76966cb6cf14a5d1b69e1a608994ac57a659e6/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509 },
+ { url = "https://files.pythonhosted.org/packages/31/01/95edcffd1bb6c0633df4e808130545c4f07383ab629ac7e316fb44fff677/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815 },
+ { url = "https://files.pythonhosted.org/packages/59/ea/5644b8baa92cc1c7163b4b4458c8679852733fa74ca49c942cfa82ded4e0/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321 },
+ { url = "https://files.pythonhosted.org/packages/26/4e/e10938106d70bc21319bd6a86ae726da37edc802ce35a3a71ecdf1fdfe7f/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635 },
+ { url = "https://files.pythonhosted.org/packages/b3/8d/a8828e3eaf5c0b4ab116924df82f24ce3416fa38d0674d8f708ddc6c8aac/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053 },
+ { url = "https://files.pythonhosted.org/packages/68/a1/17d97609d87d4520aa5ae2dcfb32305654550ac6a35effb946d303e594ce/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702 },
+ { url = "https://files.pythonhosted.org/packages/18/32/0f13c1b2d22bea1118356b8b963195446f3af124ed7a5adfa8fdecb1b6ca/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493 },
+ { url = "https://files.pythonhosted.org/packages/ae/23/48f21e3d309fbc137c068a1475358cbd3a901b3987dcfc97a029ab3068e2/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222 },
+ { url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216 },
+ { url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263 },
+ { url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265 },
+ { url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476 },
+ { url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563 },
+ { url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107 },
+ { url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067 },
+ { url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926 },
+ { url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295 },
+ { url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242 },
+ { url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875 },
+ { url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530 },
+ { url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890 },
+ { url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892 },
+ { url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312 },
+ { url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862 },
+ { url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986 },
+ { url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958 },
+ { url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394 },
+ { url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044 },
+ { url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772 },
+ { url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320 },
+ { url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460 },
+ { url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799 },
+ { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003 },
+ { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105 },
+ { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590 },
+ { url = "https://files.pythonhosted.org/packages/1d/4e/f1a084106df8c2df8132fc437e56987308e0524836aa7733721c8429d4fe/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947 },
+ { url = "https://files.pythonhosted.org/packages/63/09/3d8aeb809c0332c3f642da812ac2e3d74fc9252b3021f8c30c82e99e3f3d/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119 },
+ { url = "https://files.pythonhosted.org/packages/fd/7f/68f0fc43a2cbdc6bb239160c754d87c922f60fbaa0fa3cd3d312b8a7f5ee/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815 },
+ { url = "https://files.pythonhosted.org/packages/11/73/edeacba3167b1ca66d51b1a5a14697c2c40098b5ffa01811c67b1785a5ab/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376 },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763 },
+ { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217 },
+ { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791 },
+ { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373 },
+ { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444 },
+ { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459 },
+ { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086 },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790 },
+ { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831 },
+ { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267 },
+ { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281 },
+ { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453 },
+ { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361 },
+ { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702 },
+ { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 },
+ { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 },
+ { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 },
+ { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 },
+ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 },
+ { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 },
+ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 },
+ { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 },
+ { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 },
+ { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 },
+ { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 },
+ { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 },
+ { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 },
+ { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 },
+ { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 },
+ { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 },
+ { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 },
+ { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 },
+ { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 },
+ { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 },
+ { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 },
+ { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 },
+ { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 },
+ { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 },
+ { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 },
+ { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 },
+ { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 },
+ { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 },
+ { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 },
+ { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 },
+ { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 },
+ { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 },
+ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 },
+]
+
+[[package]]
+name = "paramiko"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "bcrypt" },
+ { name = "cryptography" },
+ { name = "invoke" },
+ { name = "pynacl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932 },
+]
+
+[[package]]
+name = "pyarrow"
+version = "22.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968 },
+ { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085 },
+ { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613 },
+ { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059 },
+ { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043 },
+ { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505 },
+ { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641 },
+ { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022 },
+ { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834 },
+ { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348 },
+ { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480 },
+ { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148 },
+ { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964 },
+ { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517 },
+ { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578 },
+ { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906 },
+ { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677 },
+ { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315 },
+ { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906 },
+ { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783 },
+ { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883 },
+ { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629 },
+ { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783 },
+ { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999 },
+ { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601 },
+ { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050 },
+ { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877 },
+ { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099 },
+ { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685 },
+ { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158 },
+ { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060 },
+ { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395 },
+ { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216 },
+ { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552 },
+ { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504 },
+ { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062 },
+ { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057 },
+ { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002 },
+ { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765 },
+ { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139 },
+ { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244 },
+ { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501 },
+ { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506 },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312 },
+ { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609 },
+ { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663 },
+ { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543 },
+ { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838 },
+ { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594 },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.23"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
+]
+
+[[package]]
+name = "pynacl"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b2/46/aeca065d227e2265125aea590c9c47fbf5786128c9400ee0eb7c88931f06/pynacl-1.6.1.tar.gz", hash = "sha256:8d361dac0309f2b6ad33b349a56cd163c98430d409fa503b10b70b3ad66eaa1d", size = 3506616 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/d6/4b2dca33ed512de8f54e5c6074aa06eaeb225bfbcd9b16f33a414389d6bd/pynacl-1.6.1-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:7d7c09749450c385301a3c20dca967a525152ae4608c0a096fe8464bfc3df93d", size = 389109 },
+ { url = "https://files.pythonhosted.org/packages/3c/30/e8dbb8ff4fa2559bbbb2187ba0d0d7faf728d17cb8396ecf4a898b22d3da/pynacl-1.6.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc734c1696ffd49b40f7c1779c89ba908157c57345cf626be2e0719488a076d3", size = 808254 },
+ { url = "https://files.pythonhosted.org/packages/44/f9/f5449c652f31da00249638dbab065ad4969c635119094b79b17c3a4da2ab/pynacl-1.6.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3cd787ec1f5c155dc8ecf39b1333cfef41415dc96d392f1ce288b4fe970df489", size = 1407365 },
+ { url = "https://files.pythonhosted.org/packages/eb/2f/9aa5605f473b712065c0a193ebf4ad4725d7a245533f0cd7e5dcdbc78f35/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b35d93ab2df03ecb3aa506be0d3c73609a51449ae0855c2e89c7ed44abde40b", size = 843842 },
+ { url = "https://files.pythonhosted.org/packages/32/8d/748f0f6956e207453da8f5f21a70885fbbb2e060d5c9d78e0a4a06781451/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dece79aecbb8f4640a1adbb81e4aa3bfb0e98e99834884a80eb3f33c7c30e708", size = 1445559 },
+ { url = "https://files.pythonhosted.org/packages/78/d0/2387f0dcb0e9816f38373999e48db4728ed724d31accdd4e737473319d35/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c2228054f04bf32d558fb89bb99f163a8197d5a9bf4efa13069a7fa8d4b93fc3", size = 825791 },
+ { url = "https://files.pythonhosted.org/packages/18/3d/ef6fb7eb072aaf15f280bc66f26ab97e7fc9efa50fb1927683013ef47473/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:2b12f1b97346f177affcdfdc78875ff42637cb40dcf79484a97dae3448083a78", size = 1410843 },
+ { url = "https://files.pythonhosted.org/packages/e3/fb/23824a017526850ee7d8a1cc4cd1e3e5082800522c10832edbbca8619537/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e735c3a1bdfde3834503baf1a6d74d4a143920281cb724ba29fb84c9f49b9c48", size = 801140 },
+ { url = "https://files.pythonhosted.org/packages/5d/d1/ebc6b182cb98603a35635b727d62f094bc201bf610f97a3bb6357fe688d2/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3384a454adf5d716a9fadcb5eb2e3e72cd49302d1374a60edc531c9957a9b014", size = 1371966 },
+ { url = "https://files.pythonhosted.org/packages/64/f4/c9d7b6f02924b1f31db546c7bd2a83a2421c6b4a8e6a2e53425c9f2802e0/pynacl-1.6.1-cp314-cp314t-win32.whl", hash = "sha256:d8615ee34d01c8e0ab3f302dcdd7b32e2bcf698ba5f4809e7cc407c8cdea7717", size = 230482 },
+ { url = "https://files.pythonhosted.org/packages/c4/2c/942477957fba22da7bf99131850e5ebdff66623418ab48964e78a7a8293e/pynacl-1.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5f5b35c1a266f8a9ad22525049280a600b19edd1f785bccd01ae838437dcf935", size = 243232 },
+ { url = "https://files.pythonhosted.org/packages/7a/0c/bdbc0d04a53b96a765ab03aa2cf9a76ad8653d70bf1665459b9a0dedaa1c/pynacl-1.6.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d984c91fe3494793b2a1fb1e91429539c6c28e9ec8209d26d25041ec599ccf63", size = 187907 },
+ { url = "https://files.pythonhosted.org/packages/49/41/3cfb3b4f3519f6ff62bf71bf1722547644bcfb1b05b8fdbdc300249ba113/pynacl-1.6.1-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:a6f9fd6d6639b1e81115c7f8ff16b8dedba1e8098d2756275d63d208b0e32021", size = 387591 },
+ { url = "https://files.pythonhosted.org/packages/18/21/b8a6563637799f617a3960f659513eccb3fcc655d5fc2be6e9dc6416826f/pynacl-1.6.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e49a3f3d0da9f79c1bec2aa013261ab9fa651c7da045d376bd306cf7c1792993", size = 798866 },
+ { url = "https://files.pythonhosted.org/packages/e8/6c/dc38033bc3ea461e05ae8f15a81e0e67ab9a01861d352ae971c99de23e7c/pynacl-1.6.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7713f8977b5d25f54a811ec9efa2738ac592e846dd6e8a4d3f7578346a841078", size = 1398001 },
+ { url = "https://files.pythonhosted.org/packages/9f/05/3ec0796a9917100a62c5073b20c4bce7bf0fea49e99b7906d1699cc7b61b/pynacl-1.6.1-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3becafc1ee2e5ea7f9abc642f56b82dcf5be69b961e782a96ea52b55d8a9fc", size = 834024 },
+ { url = "https://files.pythonhosted.org/packages/f0/b7/ae9982be0f344f58d9c64a1c25d1f0125c79201634efe3c87305ac7cb3e3/pynacl-1.6.1-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ce50d19f1566c391fedc8dc2f2f5be265ae214112ebe55315e41d1f36a7f0a9", size = 1436766 },
+ { url = "https://files.pythonhosted.org/packages/b4/51/b2ccbf89cf3025a02e044dd68a365cad593ebf70f532299f2c047d2b7714/pynacl-1.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:543f869140f67d42b9b8d47f922552d7a967e6c116aad028c9bfc5f3f3b3a7b7", size = 817275 },
+ { url = "https://files.pythonhosted.org/packages/a8/6c/dd9ee8214edf63ac563b08a9b30f98d116942b621d39a751ac3256694536/pynacl-1.6.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a2bb472458c7ca959aeeff8401b8efef329b0fc44a89d3775cffe8fad3398ad8", size = 1401891 },
+ { url = "https://files.pythonhosted.org/packages/0f/c1/97d3e1c83772d78ee1db3053fd674bc6c524afbace2bfe8d419fd55d7ed1/pynacl-1.6.1-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3206fa98737fdc66d59b8782cecc3d37d30aeec4593d1c8c145825a345bba0f0", size = 772291 },
+ { url = "https://files.pythonhosted.org/packages/4d/ca/691ff2fe12f3bb3e43e8e8df4b806f6384593d427f635104d337b8e00291/pynacl-1.6.1-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:53543b4f3d8acb344f75fd4d49f75e6572fce139f4bfb4815a9282296ff9f4c0", size = 1370839 },
+ { url = "https://files.pythonhosted.org/packages/30/27/06fe5389d30391fce006442246062cc35773c84fbcad0209fbbf5e173734/pynacl-1.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:319de653ef84c4f04e045eb250e6101d23132372b0a61a7acf91bac0fda8e58c", size = 791371 },
+ { url = "https://files.pythonhosted.org/packages/2c/7a/e2bde8c9d39074a5aa046c7d7953401608d1f16f71e237f4bef3fb9d7e49/pynacl-1.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:262a8de6bba4aee8a66f5edf62c214b06647461c9b6b641f8cd0cb1e3b3196fe", size = 1363031 },
+ { url = "https://files.pythonhosted.org/packages/dd/b6/63fd77264dae1087770a1bb414bc604470f58fbc21d83822fc9c76248076/pynacl-1.6.1-cp38-abi3-win32.whl", hash = "sha256:9fd1a4eb03caf8a2fe27b515a998d26923adb9ddb68db78e35ca2875a3830dde", size = 226585 },
+ { url = "https://files.pythonhosted.org/packages/12/c8/b419180f3fdb72ab4d45e1d88580761c267c7ca6eda9a20dcbcba254efe6/pynacl-1.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a569a4069a7855f963940040f35e87d8bc084cb2d6347428d5ad20550a0a1a21", size = 238923 },
+ { url = "https://files.pythonhosted.org/packages/35/76/c34426d532e4dce7ff36e4d92cb20f4cbbd94b619964b93d24e8f5b5510f/pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf", size = 183970 },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
+]
+
+[[package]]
+name = "rich"
+version = "14.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 },
+]
+
+[[package]]
+name = "win32-setctime"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
+]