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 }, +]