Commit ·
b36b826
1
Parent(s): 70b1f2f
NEw metadata
Browse files- data/processed/lonstatistik/curated_growth_forecast.jsonl +2 -0
- data/processed/lonstatistik/ida_records.jsonl +0 -0
- data/processed/lonstatistik/ida_salary_record.schema.json +181 -0
- pyproject.toml +5 -1
- scripts/build_ida_records.py +140 -0
- scripts/eval/results/benchmark_20260615_163150.json +540 -0
- src/backend/fusion.py +243 -0
- src/backend/indexing/ida_transform.py +405 -0
- src/backend/retrieval.py +132 -0
- src/backend/schemas.py +208 -0
- uv.lock +330 -1
data/processed/lonstatistik/curated_growth_forecast.jsonl
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"id":"IDA-curated-forecast-private","union":"IDA","source_doc":"IDA Lønstatistik 2025","source_page":2,"table_id":2,"table_title":"LØNUDVIKLING/PROGNOSE","record_type":"salary_forecast","sector":"private","data_period_month":"2025-09","currency":"DKK","pay_concept":"gross_monthly","education_type":"all","region":"all","position_level":"all","gender":"all","is_leader":"all","manages_people":"all","management_span_band":"all","stem_count_band":"all","specialization":"all","work_area":"all","branche":"all","jobloen":"all","cohort_growth_pct":3.35,"dimension_keys":[],"specificity":0,"reliability_tier":"full","measure":{"mean":3.35},"rag_text":"IDA Lønstatistik 2025 — IDA members, private sector. 2026 salary forecast 3.35% expected pay increase (private sector). Full reliability (n≥50). Source: IDA Lønstatistik 2025, tabel 2, p.2."}
|
| 2 |
+
{"id":"IDA-curated-growth-private","union":"IDA","source_doc":"IDA Lønstatistik 2025","source_page":1,"table_id":1,"table_title":"LØNUDVIKLING","record_type":"salary_growth_rate","sector":"private","data_period_month":"2025-09","currency":"DKK","pay_concept":"gross_monthly","education_type":"all","region":"all","position_level":"all","gender":"all","is_leader":"all","manages_people":"all","management_span_band":"all","stem_count_band":"all","specialization":"all","work_area":"all","branche":"all","jobloen":"all","cohort_growth_pct":3.35,"dimension_keys":[],"specificity":0,"reliability_tier":"full","measure":{"mean":3.35},"rag_text":"IDA Lønstatistik 2025 — IDA members, private sector. 3-year gliding annual salary growth 3.35% (private sector). Full reliability (n≥50). Source: IDA Lønstatistik 2025, tabel 1, p.1."}
|
data/processed/lonstatistik/ida_records.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/processed/lonstatistik/ida_salary_record.schema.json
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
| 3 |
+
"$id": "https://pay-equity-eu/ida-salary-record.schema.json",
|
| 4 |
+
"title": "IDA Lønstatistik 2025 salary record (extended for RAG filtering)",
|
| 5 |
+
"description": "One normalized salary-statistics observation extracted from the IDA 2025 Danish union salary booklet, extended with structured, hard-filterable dimensions so a user profile can be matched to the correct cell(s) and fused across tables. SCOPE DECISION (v1, schedule-driven): this schema covers ONLY the IDA salary corpus. The EU Pay Transparency Directive corpus is intentionally NOT structured for v1 and is retrieved via plain embedding/semantic search; revisit once the salary path is shipped. WILDCARD CONTRACT: every filterable dimension below is a required string enum that includes the sentinel \"all\". \"all\" means the table aggregates over that dimension and is the ONLY wildcard. A chunk matches a profile iff, for every dimension, chunk.value == profile.value OR chunk.value == \"all\". Nulls must never appear on a dimension field; backfill \"all\" explicitly at ingest (validation will fail otherwise).",
|
| 6 |
+
"type": "object",
|
| 7 |
+
"required": [
|
| 8 |
+
"id",
|
| 9 |
+
"union",
|
| 10 |
+
"source_doc",
|
| 11 |
+
"source_page",
|
| 12 |
+
"sector",
|
| 13 |
+
"currency",
|
| 14 |
+
"pay_concept",
|
| 15 |
+
"record_type",
|
| 16 |
+
"education_type",
|
| 17 |
+
"region",
|
| 18 |
+
"position_level",
|
| 19 |
+
"gender",
|
| 20 |
+
"is_leader",
|
| 21 |
+
"manages_people",
|
| 22 |
+
"management_span_band",
|
| 23 |
+
"stem_count_band",
|
| 24 |
+
"specialization",
|
| 25 |
+
"work_area",
|
| 26 |
+
"branche",
|
| 27 |
+
"jobloen",
|
| 28 |
+
"reliability_tier",
|
| 29 |
+
"specificity",
|
| 30 |
+
"dimension_keys",
|
| 31 |
+
"measure",
|
| 32 |
+
"rag_text"
|
| 33 |
+
],
|
| 34 |
+
"additionalProperties": false,
|
| 35 |
+
"properties": {
|
| 36 |
+
"id": {"type": "string", "description": "Stable record id: <union>-p<page>-<seq>."},
|
| 37 |
+
"union": {"type": "string", "enum": ["IDA"], "description": "Source union. Locked to IDA for v1 (engineers/scientists). Re-add Djoef when that corpus is onboarded."},
|
| 38 |
+
"source_doc": {"type": "string", "description": "Human-readable source document title."},
|
| 39 |
+
"source_page": {"type": "integer", "minimum": 1, "description": "1-based page number in the source PDF."},
|
| 40 |
+
"table_id": {"type": ["integer", "null"], "description": "TABEL number printed in the booklet (e.g. 4 for 'TABEL 4'). Null for non-table figures/text."},
|
| 41 |
+
"table_title": {"type": ["string", "null"], "description": "Table caption / title."},
|
| 42 |
+
"table_group": {"type": ["string", "null"], "description": "Raw table-level subgroup label as printed (provenance only; the typed dimension fields below are what you filter on)."},
|
| 43 |
+
"record_type": {
|
| 44 |
+
"type": "string",
|
| 45 |
+
"enum": [
|
| 46 |
+
"salary_observation",
|
| 47 |
+
"salary_growth_rate",
|
| 48 |
+
"salary_forecast",
|
| 49 |
+
"minimum_wage_recommendation",
|
| 50 |
+
"benefit_prevalence",
|
| 51 |
+
"terms_stat",
|
| 52 |
+
"self_employed_income"
|
| 53 |
+
],
|
| 54 |
+
"description": "Primary router. Filter on this BEFORE any other field. salary_observation = a pay cell (tabel 3-17, 27-41). salary_growth_rate = tabel 1. salary_forecast = tabel 2. minimum_wage_recommendation = p6 dimittend floors. benefit_prevalence = tabel 18. terms_stat = tabel 19-25 (vacation, clauses, jobchange, etc.). self_employed_income = tabel 42-51. NOTE: directive_provision is intentionally absent (directive uses plain embedding in v1)."
|
| 55 |
+
},
|
| 56 |
+
"sector": {
|
| 57 |
+
"type": "string",
|
| 58 |
+
"enum": ["private", "public_state", "public_municipal", "public_regional", "self_employed", "all"],
|
| 59 |
+
"description": "Employment sector. Switches between the private (tabel 3-25) and public (tabel 27-41) corpora; also gates directive direct-effect framing downstream."
|
| 60 |
+
},
|
| 61 |
+
"period": {"type": ["string", "null"], "description": "Measurement period as printed, e.g. 'september 2025' or 'maj 2025'."},
|
| 62 |
+
"data_period_month": {"type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}$", "description": "ISO YYYY-MM of the snapshot ('2025-09' private, '2025-05' public). Use for deterministic fremskrivning to 2026."},
|
| 63 |
+
"currency": {"type": "string", "enum": ["DKK"], "description": "Currency of all monetary values."},
|
| 64 |
+
"pay_concept": {
|
| 65 |
+
"type": "string",
|
| 66 |
+
"enum": ["gross_monthly", "gross_monthly_with_components", "net_monthly", "annual_income"],
|
| 67 |
+
"description": "What the monetary values represent. Bruttoloen=gross_monthly; public tables with base/supplement/pension breakdown=gross_monthly_with_components; self-employed=annual_income."
|
| 68 |
+
},
|
| 69 |
+
"includes_executives": {"type": ["boolean", "null"], "description": "True = row is inkl. topchefer, False = ekskl. topchefer. Means are NOT comparable across this flag; the dashboard must read it before differencing a user's salary against the cell. Null only if the source is genuinely ambiguous."},
|
| 70 |
+
"education_type": {
|
| 71 |
+
"type": "string",
|
| 72 |
+
"enum": ["civilingenior", "akademiingenior", "diplom_eksportingenior", "teknikumingenior", "cand_scient", "all"],
|
| 73 |
+
"description": "Filterable dimension. Derive from table_id: 4->civilingenior, 3->akademiingenior, 5->teknikumingenior, 6->diplom_eksportingenior, 7->cand_scient. 'all' for tabel 8-25 and public tables that mix educations."
|
| 74 |
+
},
|
| 75 |
+
"region": {
|
| 76 |
+
"type": "string",
|
| 77 |
+
"enum": ["hovedstaden", "sjaelland", "syddanmark", "midtjylland", "nordjylland", "all"],
|
| 78 |
+
"description": "Filterable dimension. From tabel 11 columns; 'all' elsewhere."
|
| 79 |
+
},
|
| 80 |
+
"position_level": {
|
| 81 |
+
"type": "string",
|
| 82 |
+
"enum": ["topchef", "funktionsdirektor", "afdelingschef", "projektleder", "chef_seniorkonsulent", "specialist", "ingenior_akademiker", "anden", "trainee", "all"],
|
| 83 |
+
"description": "Filterable dimension. Same enum whether the position appears as a row (tabel 12/13) or a column/segment (tabel 14/15)."
|
| 84 |
+
},
|
| 85 |
+
"position_level_rank": {"type": ["integer", "null"], "minimum": 1, "maximum": 9, "description": "Ordinal helper (trainee=1 ... topchef=9), null when position_level='all'. NOT a wildcard filter; use for 'next level up' framing and range queries."},
|
| 86 |
+
"gender": {
|
| 87 |
+
"type": "string",
|
| 88 |
+
"enum": ["male", "female", "all"],
|
| 89 |
+
"description": "Filterable dimension. From tabel 17 (and 24/25). Basis of the equal-pay comparison."
|
| 90 |
+
},
|
| 91 |
+
"is_leader": {
|
| 92 |
+
"type": "string",
|
| 93 |
+
"enum": ["leader", "non_leader", "all"],
|
| 94 |
+
"description": "Filterable dimension. Leader/non-leader grouping in tabel 16/17/23. (Boolean concept encoded as enum so 'all' remains the single wildcard.)"
|
| 95 |
+
},
|
| 96 |
+
"manages_people": {
|
| 97 |
+
"type": "string",
|
| 98 |
+
"enum": ["yes", "no", "all"],
|
| 99 |
+
"description": "Filterable dimension. Tabel 14."
|
| 100 |
+
},
|
| 101 |
+
"management_span_band": {
|
| 102 |
+
"type": "string",
|
| 103 |
+
"enum": ["none", "1-4", "5-10", "11-19", "20-29", "30-50", "51-100", "101-200", "201-500", "501-1000", "1001+", "all"],
|
| 104 |
+
"description": "Filterable dimension. Tabel 14. For the onboarding bucket '20+', match against span_min/span_max range rather than exact band."
|
| 105 |
+
},
|
| 106 |
+
"span_min": {"type": ["integer", "null"], "description": "Lower bound of management_span_band as an int (e.g. 5 for '5-10'), for range-contains matching. Null when band is 'all' or 'none'."},
|
| 107 |
+
"span_max": {"type": ["integer", "null"], "description": "Upper bound of management_span_band (e.g. 10 for '5-10'; null for open-ended '1001+')."},
|
| 108 |
+
"stem_count_band": {
|
| 109 |
+
"type": "string",
|
| 110 |
+
"enum": ["1-9", "10-24", "25-99", "100-249", "250+", "all"],
|
| 111 |
+
"description": "Filterable dimension. IDA company-size axis (number of engineers/STEM in the company, tabel 15). KEEP SEPARATE from the EU total-headcount band, which is not stored here in v1 (directive = embedding search)."
|
| 112 |
+
},
|
| 113 |
+
"specialization": {
|
| 114 |
+
"type": "string",
|
| 115 |
+
"enum": ["byggeri_anlaeg", "datalogi_it_software", "elektronik", "kemi_biologi_biotek", "maskin_industri_robot", "energi_miljo", "eksport_ledelse_forretning", "levnedsmiddel", "matematik_fysik_geologi", "all"],
|
| 116 |
+
"description": "Filterable dimension from tabel 8 (uddannelsesretning). Not in v1 onboarding; populate anyway ('all' off-table) so the wildcard logic holds and so it is a free upgrade later."
|
| 117 |
+
},
|
| 118 |
+
"work_area": {
|
| 119 |
+
"type": "string",
|
| 120 |
+
"enum": ["forskning_innovation_undervisning", "byggeri_anlaeg", "industri_produktion_kemi", "elektroteknik_tele", "it_programmering", "energi_forsyning_miljo", "transport_infrastruktur", "naturvidenskab_medicin", "administration_ledelse_service", "all"],
|
| 121 |
+
"description": "Filterable dimension from tabel 9 (arbejdsomraade). Not in v1 onboarding; populate anyway."
|
| 122 |
+
},
|
| 123 |
+
"branche": {
|
| 124 |
+
"type": "string",
|
| 125 |
+
"enum": ["raastofindvinding", "forsyningsvirksomhed", "foedevare_kemi_plast_trae", "medicinalindustri", "maskin_jern_metal", "elektronisk_industri", "raadgivende_ingenior", "entreprenor_bygge_anlaeg", "it_telekommunikation", "forskning_udvikling_finans", "handel_transport", "organisationer", "all"],
|
| 126 |
+
"description": "Filterable dimension from tabel 10 (branche). Not in v1 onboarding; populate anyway."
|
| 127 |
+
},
|
| 128 |
+
"jobloen": {
|
| 129 |
+
"type": "string",
|
| 130 |
+
"enum": ["yes", "no", "all"],
|
| 131 |
+
"description": "Filterable dimension. Tabel 16 (fixed salary, no right to overtime pay/afspadsering)."
|
| 132 |
+
},
|
| 133 |
+
"row_dimension": {"type": ["string", "null"], "description": "Raw dimension the table rows break down by (provenance), e.g. 'kandidataargang', 'stilling', 'branche', 'alder', 'region'."},
|
| 134 |
+
"row_label": {"type": ["string", "null"], "description": "Raw row label as printed, e.g. '1986-1987', 'Projektleder', '25-29 aar', 'Alle'. Display/provenance only."},
|
| 135 |
+
"segment_dimension": {"type": ["string", "null"], "description": "Raw dimension of the segment_label (provenance only)."},
|
| 136 |
+
"segment_label": {"type": ["string", "null"], "description": "Raw specific segment label (provenance only); the typed dimension fields are what you filter on."},
|
| 137 |
+
"graduation_year_start": {"type": ["integer", "null"], "description": "Earliest graduation year of the cohort; null = unbounded below ('Foer 1986' -> start null, end 1985)."},
|
| 138 |
+
"graduation_year_end": {"type": ["integer", "null"], "description": "Latest graduation year of the cohort; null = unbounded above (open-ended recent). Match rule: (start is null OR profile_year >= start) AND (end is null OR profile_year <= end)."},
|
| 139 |
+
"experience_years_min": {"type": ["integer", "null"], "description": "Derived min experience = 2025 - graduation_year_end (proxy)."},
|
| 140 |
+
"experience_years_max": {"type": ["integer", "null"], "description": "Derived max experience = 2025 - graduation_year_start; null when open-ended."},
|
| 141 |
+
"age_min": {"type": ["integer", "null"], "description": "Minimum age (age-breakdown rows)."},
|
| 142 |
+
"age_max": {"type": ["integer", "null"], "description": "Maximum age; null when open-ended."},
|
| 143 |
+
"cohort_growth_pct": {"type": ["number", "null"], "description": "3-yr gliding annual growth for this cohort+sector (tabel 1), if denormalized onto the observation. Recommended alternative: keep tabel 1 as separate salary_growth_rate records keyed by (graduation cohort, sector) and join at query time."},
|
| 144 |
+
"dimension_keys": {
|
| 145 |
+
"type": "array",
|
| 146 |
+
"items": {
|
| 147 |
+
"type": "string",
|
| 148 |
+
"enum": ["sector", "education_type", "region", "position_level", "gender", "is_leader", "manages_people", "management_span_band", "stem_count_band", "specialization", "work_area", "branche", "jobloen", "graduation_year"]
|
| 149 |
+
},
|
| 150 |
+
"uniqueItems": true,
|
| 151 |
+
"description": "The dimensions that are SPECIFIC (non-'all', or a bounded cohort for graduation_year) in this chunk = the table's breakdown axes. Drives specificity and the 'most relevant comparison' label."
|
| 152 |
+
},
|
| 153 |
+
"specificity": {"type": "integer", "minimum": 0, "description": "len(dimension_keys). Sort key: prefer the most specific cell consistent with the profile, falling back to broader cells when the specific one is suppressed."},
|
| 154 |
+
"sample_size": {"type": ["number", "null"], "description": "Antal / number of respondents (mirror of measure.count). Drives reliability_tier."},
|
| 155 |
+
"reliability_tier": {
|
| 156 |
+
"type": "string",
|
| 157 |
+
"enum": ["suppressed", "mean_only", "median_ok", "quartiles_ok", "full"],
|
| 158 |
+
"description": "Derived from sample_size per KRL suppression rules: n<5 -> suppressed (no mean); 5-9 -> mean_only; 10-19 -> median_ok (no quartiles); 20-49 -> quartiles_ok (no p90); n>=50 -> full. The dashboard reads this to decide which measure.* fields are safe to render."
|
| 159 |
+
},
|
| 160 |
+
"measure": {
|
| 161 |
+
"type": "object",
|
| 162 |
+
"description": "Statistics reported for this row/segment. All monetary values are DKK; bonus_pct is a percentage.",
|
| 163 |
+
"additionalProperties": false,
|
| 164 |
+
"minProperties": 1,
|
| 165 |
+
"properties": {
|
| 166 |
+
"count": {"type": "number", "description": "Antal - number of respondents (sample size)."},
|
| 167 |
+
"mean": {"type": "number", "description": "Gennemsnit / Bruttoloen - average."},
|
| 168 |
+
"p25": {"type": "number", "description": "Nedre kvartil / 25% fraktil."},
|
| 169 |
+
"median": {"type": "number", "description": "Median / 50% fraktil."},
|
| 170 |
+
"p75": {"type": "number", "description": "OEvre kvartil / 75% fraktil."},
|
| 171 |
+
"p90": {"type": "number", "description": "90% fraktil."},
|
| 172 |
+
"base": {"type": "number", "description": "Basisloen / Grundloen (public sector component)."},
|
| 173 |
+
"supplement": {"type": "number", "description": "Tillaeg (public sector component)."},
|
| 174 |
+
"pension": {"type": "number", "description": "Pensionsbidrag (public sector component)."},
|
| 175 |
+
"bonus_avg": {"type": "number", "description": "Average annual bonus (for those who receive bonus)."},
|
| 176 |
+
"bonus_pct": {"type": "number", "description": "Percent of the group receiving a bonus."}
|
| 177 |
+
}
|
| 178 |
+
},
|
| 179 |
+
"rag_text": {"type": "string", "description": "Natural-language summary for embedding/retrieval. MUST textually contain the human-readable values of every dimension_keys entry plus the reliability caveat, so vector recall backstops a too-narrow metadata filter. Include a source citation (union, table, page)."}
|
| 180 |
+
}
|
| 181 |
+
}
|
pyproject.toml
CHANGED
|
@@ -22,7 +22,11 @@ dependencies = [
|
|
| 22 |
# Dev-only. Never installed on the Space (its runtime only *loads* the committed
|
| 23 |
# index). `modal` powers scripts/build_index_modal.py — borrow a GPU to embed the
|
| 24 |
# corpus, then assemble the FAISS index locally. Install with `uv sync --group dev`.
|
| 25 |
-
dev = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
[tool.uv]
|
| 28 |
extra-index-url = ["https://download.pytorch.org/whl/cu118"]
|
|
|
|
| 22 |
# Dev-only. Never installed on the Space (its runtime only *loads* the committed
|
| 23 |
# index). `modal` powers scripts/build_index_modal.py — borrow a GPU to embed the
|
| 24 |
# corpus, then assemble the FAISS index locally. Install with `uv sync --group dev`.
|
| 25 |
+
dev = [
|
| 26 |
+
"modal>=0.70",
|
| 27 |
+
"pdfplumber", # deterministic PDF table re-extraction (tabel 11/17/27)
|
| 28 |
+
"jsonschema", # validate records against ida_salary_record.schema.json
|
| 29 |
+
]
|
| 30 |
|
| 31 |
[tool.uv]
|
| 32 |
extra-index-url = ["https://download.pytorch.org/whl/cu118"]
|
scripts/build_ida_records.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Assemble the structured IDA corpus → ``ida_records.jsonl`` (CPU-only).
|
| 2 |
+
|
| 3 |
+
Sources, in order:
|
| 4 |
+
1. Transform of the verified flat ``salary_records.jsonl`` (recoverable tables).
|
| 5 |
+
2. Re-extracted multi-axis tables (region/gender/leadership/public-position),
|
| 6 |
+
if ``ida_reextracted.jsonl`` exists (produced by scripts/extract_engine.py).
|
| 7 |
+
3. Curated ``salary_growth_rate`` / ``salary_forecast`` constants (tabel 1/2).
|
| 8 |
+
|
| 9 |
+
Every record gets a contract-compliant ``rag_text`` and is validated against
|
| 10 |
+
``ida_salary_record.schema.json``; the build FAILS on any violation. Output is
|
| 11 |
+
plain JSON (no embedding here) — embedding is a separate Modal step.
|
| 12 |
+
|
| 13 |
+
Usage:: uv run python scripts/build_ida_records.py
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
from jsonschema import Draft202012Validator
|
| 23 |
+
|
| 24 |
+
REPO = Path(__file__).resolve().parents[1]
|
| 25 |
+
sys.path.insert(0, str(REPO))
|
| 26 |
+
|
| 27 |
+
from src.backend.indexing.ida_transform import ( # noqa: E402
|
| 28 |
+
make_rag_text,
|
| 29 |
+
transform_corpus,
|
| 30 |
+
)
|
| 31 |
+
from src.backend.schemas import IdaSalaryRecord, Measure # noqa: E402
|
| 32 |
+
|
| 33 |
+
LON = REPO / "data" / "processed" / "lonstatistik"
|
| 34 |
+
SOURCE_JSONL = LON / "salary_records.jsonl"
|
| 35 |
+
REEXTRACT_JSONL = LON / "ida_reextracted.jsonl" # optional (task: re-extraction)
|
| 36 |
+
CURATED_JSONL = LON / "curated_growth_forecast.jsonl"
|
| 37 |
+
SCHEMA = LON / "ida_salary_record.schema.json"
|
| 38 |
+
OUT = LON / "ida_records.jsonl"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def build_curated() -> list[IdaSalaryRecord]:
|
| 42 |
+
"""Curated growth/forecast records (Q4 decision: documented constants).
|
| 43 |
+
|
| 44 |
+
Convention for non-observation record_types: the annual percentage lives in
|
| 45 |
+
``cohort_growth_pct`` (fusion reads it); the required salary-shaped
|
| 46 |
+
``measure`` carries the same number under ``mean`` purely to satisfy the
|
| 47 |
+
contract (it is a percentage, not DKK, and never rendered as a salary).
|
| 48 |
+
Real per-cohort tabel-1 growth is a v1.1 upgrade.
|
| 49 |
+
"""
|
| 50 |
+
forecast_mid = 3.35 # private 2026 band 3.2–3.5%
|
| 51 |
+
recs = [
|
| 52 |
+
IdaSalaryRecord(
|
| 53 |
+
id="IDA-curated-forecast-private",
|
| 54 |
+
source_doc="IDA Lønstatistik 2025",
|
| 55 |
+
source_page=2, table_id=2, table_title="LØNUDVIKLING/PROGNOSE",
|
| 56 |
+
record_type="salary_forecast", sector="private",
|
| 57 |
+
data_period_month="2025-09", pay_concept="gross_monthly",
|
| 58 |
+
cohort_growth_pct=forecast_mid, reliability_tier="full",
|
| 59 |
+
measure=Measure(mean=forecast_mid), rag_text="",
|
| 60 |
+
),
|
| 61 |
+
IdaSalaryRecord(
|
| 62 |
+
id="IDA-curated-growth-private",
|
| 63 |
+
source_doc="IDA Lønstatistik 2025",
|
| 64 |
+
source_page=1, table_id=1, table_title="LØNUDVIKLING",
|
| 65 |
+
record_type="salary_growth_rate", sector="private",
|
| 66 |
+
data_period_month="2025-09", pay_concept="gross_monthly",
|
| 67 |
+
cohort_growth_pct=forecast_mid, reliability_tier="full",
|
| 68 |
+
measure=Measure(mean=forecast_mid), rag_text="",
|
| 69 |
+
),
|
| 70 |
+
]
|
| 71 |
+
return recs
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def load_reextracted() -> list[IdaSalaryRecord]:
|
| 75 |
+
if not REEXTRACT_JSONL.exists():
|
| 76 |
+
return []
|
| 77 |
+
out = []
|
| 78 |
+
for line in REEXTRACT_JSONL.read_text(encoding="utf-8").splitlines():
|
| 79 |
+
if line.strip():
|
| 80 |
+
out.append(IdaSalaryRecord(**json.loads(line)))
|
| 81 |
+
return out
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def main() -> None:
|
| 85 |
+
old = [json.loads(l) for l in SOURCE_JSONL.read_text(encoding="utf-8").splitlines() if l.strip()]
|
| 86 |
+
records = transform_corpus(old)
|
| 87 |
+
n_transform = len(records)
|
| 88 |
+
|
| 89 |
+
reextracted = load_reextracted()
|
| 90 |
+
records += reextracted
|
| 91 |
+
|
| 92 |
+
curated = build_curated()
|
| 93 |
+
records += curated
|
| 94 |
+
|
| 95 |
+
# rag_text for every record (contract: names each dimension_keys value + caveat + cite).
|
| 96 |
+
for r in records:
|
| 97 |
+
r.rag_text = make_rag_text(r)
|
| 98 |
+
|
| 99 |
+
# Persist curated separately for transparency (Q4 artifact).
|
| 100 |
+
CURATED_JSONL.write_text(
|
| 101 |
+
"\n".join(r.model_dump_json(exclude_none=True) for r in curated) + "\n",
|
| 102 |
+
encoding="utf-8",
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Validate EVERY record against the authoritative schema; fail on any error.
|
| 106 |
+
validator = Draft202012Validator(json.loads(SCHEMA.read_text(encoding="utf-8")))
|
| 107 |
+
errors = []
|
| 108 |
+
seen_ids = set()
|
| 109 |
+
for r in records:
|
| 110 |
+
d = r.model_dump(exclude_none=True)
|
| 111 |
+
if r.id in seen_ids:
|
| 112 |
+
errors.append((r.id, "duplicate id"))
|
| 113 |
+
seen_ids.add(r.id)
|
| 114 |
+
for e in validator.iter_errors(d):
|
| 115 |
+
errors.append((r.id, f"{list(e.path)}: {e.message}"))
|
| 116 |
+
break
|
| 117 |
+
if errors:
|
| 118 |
+
print(f"VALIDATION FAILED: {len(errors)} error(s)")
|
| 119 |
+
for eid, msg in errors[:20]:
|
| 120 |
+
print(" ", eid, msg)
|
| 121 |
+
raise SystemExit(1)
|
| 122 |
+
|
| 123 |
+
OUT.write_text(
|
| 124 |
+
"\n".join(r.model_dump_json(exclude_none=True) for r in records) + "\n",
|
| 125 |
+
encoding="utf-8",
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
import collections
|
| 129 |
+
by_type = collections.Counter(r.record_type for r in records)
|
| 130 |
+
by_sector = collections.Counter(r.sector for r in records)
|
| 131 |
+
print(f"OK — {len(records)} records → {OUT}")
|
| 132 |
+
print(f" transform={n_transform} reextracted={len(reextracted)} curated={len(curated)}")
|
| 133 |
+
print(f" by record_type: {dict(by_type)}")
|
| 134 |
+
print(f" by sector: {dict(by_sector)}")
|
| 135 |
+
if not reextracted:
|
| 136 |
+
print(" NOTE: no ida_reextracted.jsonl — region/gender cells absent until re-extraction runs.")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
if __name__ == "__main__":
|
| 140 |
+
main()
|
scripts/eval/results/benchmark_20260615_163150.json
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"meta": {
|
| 3 |
+
"benchmark": "Pay Equity RAG Benchmark v1",
|
| 4 |
+
"timestamp": "20260615_163150",
|
| 5 |
+
"num_questions": 15,
|
| 6 |
+
"k": 5,
|
| 7 |
+
"run_generation": false
|
| 8 |
+
},
|
| 9 |
+
"aggregate": {
|
| 10 |
+
"retrieval": {
|
| 11 |
+
"mrr": 0.7022222222222222,
|
| 12 |
+
"ndcg@1": 0.6,
|
| 13 |
+
"ndcg@3": 0.7174573004761944,
|
| 14 |
+
"ndcg@5": 0.7432474876251638,
|
| 15 |
+
"recall@1": 0.6,
|
| 16 |
+
"recall@3": 0.8,
|
| 17 |
+
"recall@5": 0.8666666666666667
|
| 18 |
+
},
|
| 19 |
+
"generation": {
|
| 20 |
+
"answer_length_chars": 0.0,
|
| 21 |
+
"answer_length_words": 0.0,
|
| 22 |
+
"citations_count": 0.0,
|
| 23 |
+
"fact_coverage": 0.0,
|
| 24 |
+
"ground_truth_citations_present": 0.0
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"per_question": [
|
| 28 |
+
{
|
| 29 |
+
"id": "Q01",
|
| 30 |
+
"type": "directive",
|
| 31 |
+
"question": "Can a Danish employer ask me about my previous salary during a job interview?",
|
| 32 |
+
"ground_truth_chunk_ids": [
|
| 33 |
+
"dir-art-5"
|
| 34 |
+
],
|
| 35 |
+
"retrieved_chunk_ids": [
|
| 36 |
+
"dir-art-5",
|
| 37 |
+
"ida-p023-1594",
|
| 38 |
+
"ida-p023-1592",
|
| 39 |
+
"ida-p023-1611",
|
| 40 |
+
"ida-p023-1603"
|
| 41 |
+
],
|
| 42 |
+
"retrieved_scores": [
|
| 43 |
+
0.46843454241752625,
|
| 44 |
+
0.39175179600715637,
|
| 45 |
+
0.39157167077064514,
|
| 46 |
+
0.38680705428123474,
|
| 47 |
+
0.3867814242839813
|
| 48 |
+
],
|
| 49 |
+
"retrieval_time_s": 0.849,
|
| 50 |
+
"generation_time_s": 0.0,
|
| 51 |
+
"retrieval_metrics": {
|
| 52 |
+
"recall@1": 1.0,
|
| 53 |
+
"recall@3": 1.0,
|
| 54 |
+
"recall@5": 1.0,
|
| 55 |
+
"mrr": 1.0,
|
| 56 |
+
"ndcg@1": 1.0,
|
| 57 |
+
"ndcg@3": 1.0,
|
| 58 |
+
"ndcg@5": 1.0
|
| 59 |
+
},
|
| 60 |
+
"generation_metrics": null
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "Q02",
|
| 64 |
+
"type": "directive",
|
| 65 |
+
"question": "What are my rights as a worker to see what my colleagues earn?",
|
| 66 |
+
"ground_truth_chunk_ids": [
|
| 67 |
+
"dir-art-7"
|
| 68 |
+
],
|
| 69 |
+
"retrieved_chunk_ids": [
|
| 70 |
+
"dir-art-7",
|
| 71 |
+
"dir-art-12",
|
| 72 |
+
"dir-art-6",
|
| 73 |
+
"dir-art-5",
|
| 74 |
+
"dir-art-9"
|
| 75 |
+
],
|
| 76 |
+
"retrieved_scores": [
|
| 77 |
+
0.4882346987724304,
|
| 78 |
+
0.38052114844322205,
|
| 79 |
+
0.37416872382164,
|
| 80 |
+
0.37270212173461914,
|
| 81 |
+
0.3478201925754547
|
| 82 |
+
],
|
| 83 |
+
"retrieval_time_s": 0.528,
|
| 84 |
+
"generation_time_s": 0.0,
|
| 85 |
+
"retrieval_metrics": {
|
| 86 |
+
"recall@1": 1.0,
|
| 87 |
+
"recall@3": 1.0,
|
| 88 |
+
"recall@5": 1.0,
|
| 89 |
+
"mrr": 1.0,
|
| 90 |
+
"ndcg@1": 1.0,
|
| 91 |
+
"ndcg@3": 1.0,
|
| 92 |
+
"ndcg@5": 1.0
|
| 93 |
+
},
|
| 94 |
+
"generation_metrics": null
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"id": "Q03",
|
| 98 |
+
"type": "directive",
|
| 99 |
+
"question": "At what company size does mandatory pay gap reporting kick in under the new EU directive?",
|
| 100 |
+
"ground_truth_chunk_ids": [
|
| 101 |
+
"dir-art-9"
|
| 102 |
+
],
|
| 103 |
+
"retrieved_chunk_ids": [
|
| 104 |
+
"dir-art-9",
|
| 105 |
+
"dir-art-31",
|
| 106 |
+
"dir-art-29",
|
| 107 |
+
"dir-art-35",
|
| 108 |
+
"dir-art-10"
|
| 109 |
+
],
|
| 110 |
+
"retrieved_scores": [
|
| 111 |
+
0.5659891963005066,
|
| 112 |
+
0.4674082398414612,
|
| 113 |
+
0.4606870710849762,
|
| 114 |
+
0.4587571620941162,
|
| 115 |
+
0.4516479969024658
|
| 116 |
+
],
|
| 117 |
+
"retrieval_time_s": 0.654,
|
| 118 |
+
"generation_time_s": 0.0,
|
| 119 |
+
"retrieval_metrics": {
|
| 120 |
+
"recall@1": 1.0,
|
| 121 |
+
"recall@3": 1.0,
|
| 122 |
+
"recall@5": 1.0,
|
| 123 |
+
"mrr": 1.0,
|
| 124 |
+
"ndcg@1": 1.0,
|
| 125 |
+
"ndcg@3": 1.0,
|
| 126 |
+
"ndcg@5": 1.0
|
| 127 |
+
},
|
| 128 |
+
"generation_metrics": null
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"id": "Q04",
|
| 132 |
+
"type": "directive",
|
| 133 |
+
"question": "What happens if a company's pay report shows women earn less than men — does the directive require specific action?",
|
| 134 |
+
"ground_truth_chunk_ids": [
|
| 135 |
+
"dir-art-10"
|
| 136 |
+
],
|
| 137 |
+
"retrieved_chunk_ids": [
|
| 138 |
+
"dir-art-9",
|
| 139 |
+
"dir-art-10",
|
| 140 |
+
"dir-art-29",
|
| 141 |
+
"dir-art-3",
|
| 142 |
+
"dir-art-24"
|
| 143 |
+
],
|
| 144 |
+
"retrieved_scores": [
|
| 145 |
+
0.48109012842178345,
|
| 146 |
+
0.4684622585773468,
|
| 147 |
+
0.4062488377094269,
|
| 148 |
+
0.4044463336467743,
|
| 149 |
+
0.3909009099006653
|
| 150 |
+
],
|
| 151 |
+
"retrieval_time_s": 0.754,
|
| 152 |
+
"generation_time_s": 0.0,
|
| 153 |
+
"retrieval_metrics": {
|
| 154 |
+
"recall@1": 0.0,
|
| 155 |
+
"recall@3": 1.0,
|
| 156 |
+
"recall@5": 1.0,
|
| 157 |
+
"mrr": 0.5,
|
| 158 |
+
"ndcg@1": 0.0,
|
| 159 |
+
"ndcg@3": 0.6309297535714575,
|
| 160 |
+
"ndcg@5": 0.6309297535714575
|
| 161 |
+
},
|
| 162 |
+
"generation_metrics": null
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"id": "Q05",
|
| 166 |
+
"type": "directive",
|
| 167 |
+
"question": "If I sue my employer for pay discrimination and they didn't follow the transparency rules, who has to prove what?",
|
| 168 |
+
"ground_truth_chunk_ids": [
|
| 169 |
+
"dir-art-18"
|
| 170 |
+
],
|
| 171 |
+
"retrieved_chunk_ids": [
|
| 172 |
+
"dir-art-18",
|
| 173 |
+
"dir-art-6",
|
| 174 |
+
"dir-art-5",
|
| 175 |
+
"dir-art-9",
|
| 176 |
+
"dir-art-19"
|
| 177 |
+
],
|
| 178 |
+
"retrieved_scores": [
|
| 179 |
+
0.45788896083831787,
|
| 180 |
+
0.37829428911209106,
|
| 181 |
+
0.34863778948783875,
|
| 182 |
+
0.34772422909736633,
|
| 183 |
+
0.3451765179634094
|
| 184 |
+
],
|
| 185 |
+
"retrieval_time_s": 0.793,
|
| 186 |
+
"generation_time_s": 0.0,
|
| 187 |
+
"retrieval_metrics": {
|
| 188 |
+
"recall@1": 1.0,
|
| 189 |
+
"recall@3": 1.0,
|
| 190 |
+
"recall@5": 1.0,
|
| 191 |
+
"mrr": 1.0,
|
| 192 |
+
"ndcg@1": 1.0,
|
| 193 |
+
"ndcg@3": 1.0,
|
| 194 |
+
"ndcg@5": 1.0
|
| 195 |
+
},
|
| 196 |
+
"generation_metrics": null
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"id": "Q06",
|
| 200 |
+
"type": "directive",
|
| 201 |
+
"question": "How long do I have to bring an equal pay claim under the EU Pay Transparency Directive?",
|
| 202 |
+
"ground_truth_chunk_ids": [
|
| 203 |
+
"dir-art-21"
|
| 204 |
+
],
|
| 205 |
+
"retrieved_chunk_ids": [
|
| 206 |
+
"dir-art-21",
|
| 207 |
+
"dir-art-29",
|
| 208 |
+
"dir-art-9",
|
| 209 |
+
"dir-art-7",
|
| 210 |
+
"dir-art-18"
|
| 211 |
+
],
|
| 212 |
+
"retrieved_scores": [
|
| 213 |
+
0.5816149711608887,
|
| 214 |
+
0.5352213382720947,
|
| 215 |
+
0.511902928352356,
|
| 216 |
+
0.48460856080055237,
|
| 217 |
+
0.4799960255622864
|
| 218 |
+
],
|
| 219 |
+
"retrieval_time_s": 0.646,
|
| 220 |
+
"generation_time_s": 0.0,
|
| 221 |
+
"retrieval_metrics": {
|
| 222 |
+
"recall@1": 1.0,
|
| 223 |
+
"recall@3": 1.0,
|
| 224 |
+
"recall@5": 1.0,
|
| 225 |
+
"mrr": 1.0,
|
| 226 |
+
"ndcg@1": 1.0,
|
| 227 |
+
"ndcg@3": 1.0,
|
| 228 |
+
"ndcg@5": 1.0
|
| 229 |
+
},
|
| 230 |
+
"generation_metrics": null
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"id": "Q07",
|
| 234 |
+
"type": "directive",
|
| 235 |
+
"question": "What counts as 'pay' under the directive — just salary or also bonuses and other benefits?",
|
| 236 |
+
"ground_truth_chunk_ids": [
|
| 237 |
+
"dir-art-3"
|
| 238 |
+
],
|
| 239 |
+
"retrieved_chunk_ids": [
|
| 240 |
+
"dir-art-3",
|
| 241 |
+
"dir-art-4",
|
| 242 |
+
"dir-art-6",
|
| 243 |
+
"dir-art-1",
|
| 244 |
+
"dir-art-10"
|
| 245 |
+
],
|
| 246 |
+
"retrieved_scores": [
|
| 247 |
+
0.44907885789871216,
|
| 248 |
+
0.37218886613845825,
|
| 249 |
+
0.3629388213157654,
|
| 250 |
+
0.3602444529533386,
|
| 251 |
+
0.3525366187095642
|
| 252 |
+
],
|
| 253 |
+
"retrieval_time_s": 0.665,
|
| 254 |
+
"generation_time_s": 0.0,
|
| 255 |
+
"retrieval_metrics": {
|
| 256 |
+
"recall@1": 1.0,
|
| 257 |
+
"recall@3": 1.0,
|
| 258 |
+
"recall@5": 1.0,
|
| 259 |
+
"mrr": 1.0,
|
| 260 |
+
"ndcg@1": 1.0,
|
| 261 |
+
"ndcg@3": 1.0,
|
| 262 |
+
"ndcg@5": 1.0
|
| 263 |
+
},
|
| 264 |
+
"generation_metrics": null
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"id": "Q08",
|
| 268 |
+
"type": "directive",
|
| 269 |
+
"question": "What is the deadline for EU member states to implement the Pay Transparency Directive into national law?",
|
| 270 |
+
"ground_truth_chunk_ids": [
|
| 271 |
+
"dir-art-34"
|
| 272 |
+
],
|
| 273 |
+
"retrieved_chunk_ids": [
|
| 274 |
+
"dir-art-29",
|
| 275 |
+
"dir-art-9",
|
| 276 |
+
"dir-art-31",
|
| 277 |
+
"dir-art-35",
|
| 278 |
+
"dir-art-34"
|
| 279 |
+
],
|
| 280 |
+
"retrieved_scores": [
|
| 281 |
+
0.5351299047470093,
|
| 282 |
+
0.5249780416488647,
|
| 283 |
+
0.5037232637405396,
|
| 284 |
+
0.5034652948379517,
|
| 285 |
+
0.5011686682701111
|
| 286 |
+
],
|
| 287 |
+
"retrieval_time_s": 0.675,
|
| 288 |
+
"generation_time_s": 0.0,
|
| 289 |
+
"retrieval_metrics": {
|
| 290 |
+
"recall@1": 0.0,
|
| 291 |
+
"recall@3": 0.0,
|
| 292 |
+
"recall@5": 1.0,
|
| 293 |
+
"mrr": 0.2,
|
| 294 |
+
"ndcg@1": 0.0,
|
| 295 |
+
"ndcg@3": 0.0,
|
| 296 |
+
"ndcg@5": 0.38685280723454163
|
| 297 |
+
},
|
| 298 |
+
"generation_metrics": null
|
| 299 |
+
},
|
| 300 |
+
{
|
| 301 |
+
"id": "Q09",
|
| 302 |
+
"type": "directive",
|
| 303 |
+
"question": "What penalties can be imposed on employers who violate the equal pay principle under the directive?",
|
| 304 |
+
"ground_truth_chunk_ids": [
|
| 305 |
+
"dir-art-23"
|
| 306 |
+
],
|
| 307 |
+
"retrieved_chunk_ids": [
|
| 308 |
+
"dir-art-23",
|
| 309 |
+
"dir-art-24",
|
| 310 |
+
"dir-art-17",
|
| 311 |
+
"dir-art-1",
|
| 312 |
+
"dir-art-18"
|
| 313 |
+
],
|
| 314 |
+
"retrieved_scores": [
|
| 315 |
+
0.6221146583557129,
|
| 316 |
+
0.5374859571456909,
|
| 317 |
+
0.5216008424758911,
|
| 318 |
+
0.5104919672012329,
|
| 319 |
+
0.48372018337249756
|
| 320 |
+
],
|
| 321 |
+
"retrieval_time_s": 0.615,
|
| 322 |
+
"generation_time_s": 0.0,
|
| 323 |
+
"retrieval_metrics": {
|
| 324 |
+
"recall@1": 1.0,
|
| 325 |
+
"recall@3": 1.0,
|
| 326 |
+
"recall@5": 1.0,
|
| 327 |
+
"mrr": 1.0,
|
| 328 |
+
"ndcg@1": 1.0,
|
| 329 |
+
"ndcg@3": 1.0,
|
| 330 |
+
"ndcg@5": 1.0
|
| 331 |
+
},
|
| 332 |
+
"generation_metrics": null
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"id": "Q10",
|
| 336 |
+
"type": "salary",
|
| 337 |
+
"question": "Hvad er medianlønnen for en civilingeniør i det private med 10 års erfaring ifølge IDAs lønstatistik for 2025?",
|
| 338 |
+
"ground_truth_chunk_ids": [
|
| 339 |
+
"ida-p011-0035"
|
| 340 |
+
],
|
| 341 |
+
"retrieved_chunk_ids": [
|
| 342 |
+
"ida-p011-0035",
|
| 343 |
+
"ida-p011-0030",
|
| 344 |
+
"ida-p011-0045",
|
| 345 |
+
"ida-p021-1496",
|
| 346 |
+
"ida-p011-0044"
|
| 347 |
+
],
|
| 348 |
+
"retrieved_scores": [
|
| 349 |
+
0.5657927989959717,
|
| 350 |
+
0.561237096786499,
|
| 351 |
+
0.5611233115196228,
|
| 352 |
+
0.5604856610298157,
|
| 353 |
+
0.5585825443267822
|
| 354 |
+
],
|
| 355 |
+
"retrieval_time_s": 1.204,
|
| 356 |
+
"generation_time_s": 0.0,
|
| 357 |
+
"retrieval_metrics": {
|
| 358 |
+
"recall@1": 1.0,
|
| 359 |
+
"recall@3": 1.0,
|
| 360 |
+
"recall@5": 1.0,
|
| 361 |
+
"mrr": 1.0,
|
| 362 |
+
"ndcg@1": 1.0,
|
| 363 |
+
"ndcg@3": 1.0,
|
| 364 |
+
"ndcg@5": 1.0
|
| 365 |
+
},
|
| 366 |
+
"generation_metrics": null
|
| 367 |
+
},
|
| 368 |
+
{
|
| 369 |
+
"id": "Q11",
|
| 370 |
+
"type": "salary",
|
| 371 |
+
"question": "What is the median salary for a civilingeniør (MSc engineer) with 5 years of experience in the Danish private sector? How many respondents and what percentage receive a bonus?",
|
| 372 |
+
"ground_truth_chunk_ids": [
|
| 373 |
+
"ida-p011-0040"
|
| 374 |
+
],
|
| 375 |
+
"retrieved_chunk_ids": [
|
| 376 |
+
"ida-p011-0025",
|
| 377 |
+
"ida-p011-0029",
|
| 378 |
+
"ida-p011-0028",
|
| 379 |
+
"ida-p011-0030",
|
| 380 |
+
"ida-p011-0027"
|
| 381 |
+
],
|
| 382 |
+
"retrieved_scores": [
|
| 383 |
+
0.6918340921401978,
|
| 384 |
+
0.6845650672912598,
|
| 385 |
+
0.6832934617996216,
|
| 386 |
+
0.6826284527778625,
|
| 387 |
+
0.6825269460678101
|
| 388 |
+
],
|
| 389 |
+
"retrieval_time_s": 1.181,
|
| 390 |
+
"generation_time_s": 0.0,
|
| 391 |
+
"retrieval_metrics": {
|
| 392 |
+
"recall@1": 0.0,
|
| 393 |
+
"recall@3": 0.0,
|
| 394 |
+
"recall@5": 0.0,
|
| 395 |
+
"mrr": 0.0,
|
| 396 |
+
"ndcg@1": 0.0,
|
| 397 |
+
"ndcg@3": 0.0,
|
| 398 |
+
"ndcg@5": 0.0
|
| 399 |
+
},
|
| 400 |
+
"generation_metrics": null
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"id": "Q12",
|
| 404 |
+
"type": "salary",
|
| 405 |
+
"question": "What is the average starting salary for a newly graduated civilingeniør (2025 graduate with 0 years experience) in the Danish private sector?",
|
| 406 |
+
"ground_truth_chunk_ids": [
|
| 407 |
+
"ida-p011-0045"
|
| 408 |
+
],
|
| 409 |
+
"retrieved_chunk_ids": [
|
| 410 |
+
"ida-p011-0044",
|
| 411 |
+
"ida-p011-0045",
|
| 412 |
+
"ida-p011-0040",
|
| 413 |
+
"ida-p011-0038",
|
| 414 |
+
"ida-p011-0039"
|
| 415 |
+
],
|
| 416 |
+
"retrieved_scores": [
|
| 417 |
+
0.6287952661514282,
|
| 418 |
+
0.6287572979927063,
|
| 419 |
+
0.6224535703659058,
|
| 420 |
+
0.6201796531677246,
|
| 421 |
+
0.6197202205657959
|
| 422 |
+
],
|
| 423 |
+
"retrieval_time_s": 0.969,
|
| 424 |
+
"generation_time_s": 0.0,
|
| 425 |
+
"retrieval_metrics": {
|
| 426 |
+
"recall@1": 0.0,
|
| 427 |
+
"recall@3": 1.0,
|
| 428 |
+
"recall@5": 1.0,
|
| 429 |
+
"mrr": 0.5,
|
| 430 |
+
"ndcg@1": 0.0,
|
| 431 |
+
"ndcg@3": 0.6309297535714575,
|
| 432 |
+
"ndcg@5": 0.6309297535714575
|
| 433 |
+
},
|
| 434 |
+
"generation_metrics": null
|
| 435 |
+
},
|
| 436 |
+
{
|
| 437 |
+
"id": "Q13",
|
| 438 |
+
"type": "salary",
|
| 439 |
+
"question": "What is the average salary for a topchef/funktionsdirektør (C-level executive) with 25 years of experience according to IDA 2025 salary statistics?",
|
| 440 |
+
"ground_truth_chunk_ids": [
|
| 441 |
+
"ida-p021-1371"
|
| 442 |
+
],
|
| 443 |
+
"retrieved_chunk_ids": [
|
| 444 |
+
"ida-p021-1371",
|
| 445 |
+
"ida-p021-1491",
|
| 446 |
+
"ida-p021-1523",
|
| 447 |
+
"ida-p021-1515",
|
| 448 |
+
"ida-p021-1531"
|
| 449 |
+
],
|
| 450 |
+
"retrieved_scores": [
|
| 451 |
+
0.6942191123962402,
|
| 452 |
+
0.6869297027587891,
|
| 453 |
+
0.6857338547706604,
|
| 454 |
+
0.6853389739990234,
|
| 455 |
+
0.684471607208252
|
| 456 |
+
],
|
| 457 |
+
"retrieval_time_s": 1.151,
|
| 458 |
+
"generation_time_s": 0.0,
|
| 459 |
+
"retrieval_metrics": {
|
| 460 |
+
"recall@1": 1.0,
|
| 461 |
+
"recall@3": 1.0,
|
| 462 |
+
"recall@5": 1.0,
|
| 463 |
+
"mrr": 1.0,
|
| 464 |
+
"ndcg@1": 1.0,
|
| 465 |
+
"ndcg@3": 1.0,
|
| 466 |
+
"ndcg@5": 1.0
|
| 467 |
+
},
|
| 468 |
+
"generation_metrics": null
|
| 469 |
+
},
|
| 470 |
+
{
|
| 471 |
+
"id": "Q14",
|
| 472 |
+
"type": "salary",
|
| 473 |
+
"question": "What is the average salary in the pharmaceutical industry (medicinalindustri) for IDA engineers with 5 years of experience?",
|
| 474 |
+
"ground_truth_chunk_ids": [
|
| 475 |
+
"ida-p017-1176"
|
| 476 |
+
],
|
| 477 |
+
"retrieved_chunk_ids": [
|
| 478 |
+
"ida-p017-1011",
|
| 479 |
+
"ida-p017-1033",
|
| 480 |
+
"ida-p017-1176",
|
| 481 |
+
"ida-p017-1121",
|
| 482 |
+
"ida-p017-1044"
|
| 483 |
+
],
|
| 484 |
+
"retrieved_scores": [
|
| 485 |
+
0.6024791598320007,
|
| 486 |
+
0.5985130071640015,
|
| 487 |
+
0.5924195051193237,
|
| 488 |
+
0.5912789106369019,
|
| 489 |
+
0.5893527865409851
|
| 490 |
+
],
|
| 491 |
+
"retrieval_time_s": 0.906,
|
| 492 |
+
"generation_time_s": 0.0,
|
| 493 |
+
"retrieval_metrics": {
|
| 494 |
+
"recall@1": 0.0,
|
| 495 |
+
"recall@3": 1.0,
|
| 496 |
+
"recall@5": 1.0,
|
| 497 |
+
"mrr": 0.3333333333333333,
|
| 498 |
+
"ndcg@1": 0.0,
|
| 499 |
+
"ndcg@3": 0.5,
|
| 500 |
+
"ndcg@5": 0.5
|
| 501 |
+
},
|
| 502 |
+
"generation_metrics": null
|
| 503 |
+
},
|
| 504 |
+
{
|
| 505 |
+
"id": "Q15",
|
| 506 |
+
"type": "salary",
|
| 507 |
+
"question": "What is the average salary for an IDA engineer working in the state sector (staten) with around 20 years of experience?",
|
| 508 |
+
"ground_truth_chunk_ids": [
|
| 509 |
+
"ida-p030-1698"
|
| 510 |
+
],
|
| 511 |
+
"retrieved_chunk_ids": [
|
| 512 |
+
"ida-p030-1713",
|
| 513 |
+
"ida-p030-1701",
|
| 514 |
+
"ida-p030-1699",
|
| 515 |
+
"ida-p030-1693",
|
| 516 |
+
"ida-p030-1703"
|
| 517 |
+
],
|
| 518 |
+
"retrieved_scores": [
|
| 519 |
+
0.5219017267227173,
|
| 520 |
+
0.5170749425888062,
|
| 521 |
+
0.5162566900253296,
|
| 522 |
+
0.5155266523361206,
|
| 523 |
+
0.5143060684204102
|
| 524 |
+
],
|
| 525 |
+
"retrieval_time_s": 0.915,
|
| 526 |
+
"generation_time_s": 0.0,
|
| 527 |
+
"retrieval_metrics": {
|
| 528 |
+
"recall@1": 0.0,
|
| 529 |
+
"recall@3": 0.0,
|
| 530 |
+
"recall@5": 0.0,
|
| 531 |
+
"mrr": 0.0,
|
| 532 |
+
"ndcg@1": 0.0,
|
| 533 |
+
"ndcg@3": 0.0,
|
| 534 |
+
"ndcg@5": 0.0
|
| 535 |
+
},
|
| 536 |
+
"generation_metrics": null
|
| 537 |
+
}
|
| 538 |
+
],
|
| 539 |
+
"summary_text": "\n======================================================================\n Benchmark Results Summary\n======================================================================\n\n Retrieval:\n Recall@1: 0.600 (9/15 questions with ≥1 hit)\n Recall@3: 0.800\n Recall@5: 0.867\n MRR: 0.7022\n NDCG@5: 0.7432\n\n Directive questions (9):\n Recall@1: 0.778 MRR: 0.8556\n Salary questions (6):\n Recall@1: 0.333 MRR: 0.4722"
|
| 540 |
+
}
|
src/backend/fusion.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Profile-driven dashboard fusion (v1, salary path).
|
| 2 |
+
|
| 3 |
+
A single profile matches one cell from several IDA tables (education×cohort,
|
| 4 |
+
position×cohort, region×cohort, gender×leadership, …). This module fuses them:
|
| 5 |
+
for each breakdown axis it picks the most-specific *non-suppressed* cell
|
| 6 |
+
consistent with the profile (falling back to a broader cell when the specific
|
| 7 |
+
one is suppressed), joins the curated growth/forecast records, projects the
|
| 8 |
+
headline figure to 2026, and computes the user's percentile position + gap.
|
| 9 |
+
|
| 10 |
+
Reliability gating and matching live in ``retrieval``; this layer composes them
|
| 11 |
+
into the ``Dashboard`` the synthesis/UI layer renders.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from .retrieval import _profile_constraints, gated_measure, load_records, retrieve
|
| 17 |
+
from .schemas import Dashboard, IdaSalaryRecord, Profile, RecommendationCell
|
| 18 |
+
|
| 19 |
+
# Readable values for the enum dimensions that can appear in a comparison label.
|
| 20 |
+
_DIM_DISPLAY = {
|
| 21 |
+
"position_level": {
|
| 22 |
+
"topchef": "top executive", "funktionsdirektor": "functional director",
|
| 23 |
+
"afdelingschef": "department head", "projektleder": "project manager",
|
| 24 |
+
"chef_seniorkonsulent": "chief/senior consultant", "specialist": "specialist",
|
| 25 |
+
"ingenior_akademiker": "engineer/academic", "anden": "other role", "trainee": "trainee",
|
| 26 |
+
},
|
| 27 |
+
"region": {
|
| 28 |
+
"hovedstaden": "Capital Region", "sjaelland": "Region Zealand",
|
| 29 |
+
"syddanmark": "Southern Denmark", "midtjylland": "Central Denmark",
|
| 30 |
+
"nordjylland": "North Denmark",
|
| 31 |
+
},
|
| 32 |
+
"gender": {"male": "men", "female": "women"},
|
| 33 |
+
"is_leader": {"leader": "with staff responsibility", "non_leader": "without staff responsibility"},
|
| 34 |
+
"manages_people": {"yes": "managing people", "no": "not managing people"},
|
| 35 |
+
"jobloen": {"yes": "on fixed salary (jobløn)", "no": "not on jobløn"},
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Human label for each breakdown axis (the non-cohort dimension(s) of a cell).
|
| 39 |
+
_AXIS_LABEL = {
|
| 40 |
+
frozenset(): "Overall (your cohort)",
|
| 41 |
+
frozenset({"education_type"}): "By education",
|
| 42 |
+
frozenset({"position_level"}): "By position level",
|
| 43 |
+
frozenset({"region"}): "By region",
|
| 44 |
+
frozenset({"gender"}): "By gender",
|
| 45 |
+
frozenset({"gender", "is_leader"}): "By gender & leadership",
|
| 46 |
+
frozenset({"is_leader"}): "By leadership",
|
| 47 |
+
frozenset({"specialization"}): "By specialization",
|
| 48 |
+
frozenset({"work_area"}): "By work area",
|
| 49 |
+
frozenset({"branche"}): "By industry",
|
| 50 |
+
frozenset({"jobloen"}): "Fixed-salary (jobløn)",
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
_EDU_LABEL = {
|
| 54 |
+
"civilingenior": "Civil engineers", "akademiingenior": "Academy engineers",
|
| 55 |
+
"teknikumingenior": "Technical engineers", "diplom_eksportingenior": "Diploma/export engineers",
|
| 56 |
+
"cand_scient": "Science graduates",
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _axis_key(rec: IdaSalaryRecord) -> frozenset[str]:
|
| 61 |
+
return frozenset(d for d in rec.dimension_keys if d != "graduation_year")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _cohort_label(rec: IdaSalaryRecord) -> str:
|
| 65 |
+
s, e = rec.graduation_year_start, rec.graduation_year_end
|
| 66 |
+
if s is None and e is None:
|
| 67 |
+
return "all cohorts"
|
| 68 |
+
if s is None:
|
| 69 |
+
return f"graduated ≤{e}"
|
| 70 |
+
if e is None:
|
| 71 |
+
return f"graduated ≥{s}"
|
| 72 |
+
return f"cohort {s}–{e}" if s != e else f"graduated {s}"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _comparison_label(rec: IdaSalaryRecord) -> str:
|
| 76 |
+
bits: list[str] = []
|
| 77 |
+
if rec.education_type != "all":
|
| 78 |
+
bits.append(_EDU_LABEL.get(rec.education_type, rec.education_type))
|
| 79 |
+
for dim in ("position_level", "region", "gender", "is_leader",
|
| 80 |
+
"specialization", "work_area", "branche", "jobloen"):
|
| 81 |
+
val = getattr(rec, dim)
|
| 82 |
+
if val != "all":
|
| 83 |
+
if dim in ("specialization", "work_area", "branche"):
|
| 84 |
+
bits.append(rec.segment_label or str(val))
|
| 85 |
+
else:
|
| 86 |
+
bits.append(_DIM_DISPLAY.get(dim, {}).get(val, str(val)))
|
| 87 |
+
bits.append(_cohort_label(rec))
|
| 88 |
+
bits.append(rec.sector.replace("_", " "))
|
| 89 |
+
return ", ".join(bits)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _to_cell(rec: IdaSalaryRecord, growth_pct: float | None) -> RecommendationCell:
|
| 93 |
+
m = gated_measure(rec)
|
| 94 |
+
central = m.median if m.median is not None else m.mean
|
| 95 |
+
projected = (
|
| 96 |
+
round(central * (1 + growth_pct / 100)) if central is not None and growth_pct else None
|
| 97 |
+
)
|
| 98 |
+
return RecommendationCell(
|
| 99 |
+
comparison=_comparison_label(rec),
|
| 100 |
+
citation=f"{rec.source_doc}, tabel {rec.table_id}, p.{rec.source_page}"
|
| 101 |
+
if rec.table_id else f"{rec.source_doc}, p.{rec.source_page}",
|
| 102 |
+
record_id=rec.id,
|
| 103 |
+
dimension_keys=rec.dimension_keys,
|
| 104 |
+
specificity=rec.specificity,
|
| 105 |
+
sample_size=rec.sample_size,
|
| 106 |
+
reliability_tier=rec.reliability_tier,
|
| 107 |
+
includes_executives=rec.includes_executives,
|
| 108 |
+
measure=m,
|
| 109 |
+
projected_median_2026=projected,
|
| 110 |
+
growth_pct_applied=growth_pct if projected is not None else None,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _best_per_axis(cands: list[IdaSalaryRecord]) -> dict[frozenset, IdaSalaryRecord]:
|
| 115 |
+
"""Per axis, the most-specific non-suppressed cell (fallback: broadest non-suppressed)."""
|
| 116 |
+
by_axis: dict[frozenset, list[IdaSalaryRecord]] = {}
|
| 117 |
+
for r in cands:
|
| 118 |
+
by_axis.setdefault(_axis_key(r), []).append(r)
|
| 119 |
+
chosen: dict[frozenset, IdaSalaryRecord] = {}
|
| 120 |
+
for axis, recs in by_axis.items():
|
| 121 |
+
ok = [r for r in recs if r.reliability_tier != "suppressed"]
|
| 122 |
+
if not ok:
|
| 123 |
+
continue # whole axis suppressed for this profile → omit
|
| 124 |
+
# recs already globally sorted most-specific first; ok preserves that.
|
| 125 |
+
chosen[axis] = ok[0]
|
| 126 |
+
return chosen
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _forecast_pct(profile: Profile) -> float | None:
|
| 130 |
+
for r in retrieve(profile, record_type="salary_forecast"):
|
| 131 |
+
if r.cohort_growth_pct is not None:
|
| 132 |
+
return r.cohort_growth_pct
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _estimate_percentile(value: float, m) -> float | None:
|
| 137 |
+
"""Approximate percentile of ``value`` within a cell's quantiles."""
|
| 138 |
+
pts = sorted(
|
| 139 |
+
(p, getattr(m, k))
|
| 140 |
+
for p, k in ((25, "p25"), (50, "median"), (75, "p75"), (90, "p90"))
|
| 141 |
+
if getattr(m, k) is not None
|
| 142 |
+
)
|
| 143 |
+
if not pts:
|
| 144 |
+
return None
|
| 145 |
+
if value <= pts[0][1]:
|
| 146 |
+
return round(pts[0][0] * value / pts[0][1], 1) if pts[0][1] else 0.0
|
| 147 |
+
if value >= pts[-1][1]:
|
| 148 |
+
lo_p, lo_v = pts[-1]
|
| 149 |
+
return round(min(100.0, lo_p + (100 - lo_p) * (value - lo_v) / max(lo_v, 1)), 1)
|
| 150 |
+
for (p0, v0), (p1, v1) in zip(pts, pts[1:]):
|
| 151 |
+
if v0 <= value <= v1:
|
| 152 |
+
frac = (value - v0) / (v1 - v0) if v1 != v0 else 0
|
| 153 |
+
return round(p0 + frac * (p1 - p0), 1)
|
| 154 |
+
return None
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _known_dims(profile: Profile) -> set[str]:
|
| 158 |
+
"""Dimensions the profile actually pins — so a cell genuinely describes the user.
|
| 159 |
+
|
| 160 |
+
Cells that split on a dimension the profile didn't provide (e.g. an arbitrary
|
| 161 |
+
specialization) must not surface as "your" comparison. graduation_year and
|
| 162 |
+
education_type are always known; leadership is known when management is asked.
|
| 163 |
+
"""
|
| 164 |
+
known = {"graduation_year"} | set(_profile_constraints(profile))
|
| 165 |
+
if profile.manages_people is not None:
|
| 166 |
+
known |= {"is_leader", "manages_people"}
|
| 167 |
+
return known
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def build_dashboard(profile: Profile) -> Dashboard:
|
| 171 |
+
"""Fuse the matched IDA cells into a recommendation dashboard."""
|
| 172 |
+
load_records()
|
| 173 |
+
growth_pct = _forecast_pct(profile)
|
| 174 |
+
known = _known_dims(profile)
|
| 175 |
+
# Only cells whose breakdown axes are all known to the profile describe the
|
| 176 |
+
# user (the brief: unasked dimensions are free upgrades when onboarding grows).
|
| 177 |
+
matched = [
|
| 178 |
+
r for r in retrieve(profile, record_type="salary_observation")
|
| 179 |
+
if set(r.dimension_keys) <= known
|
| 180 |
+
]
|
| 181 |
+
|
| 182 |
+
chosen = _best_per_axis(matched)
|
| 183 |
+
# Stable, useful axis order for display.
|
| 184 |
+
order = [
|
| 185 |
+
frozenset({"education_type"}), frozenset({"position_level"}),
|
| 186 |
+
frozenset({"gender", "is_leader"}), frozenset({"gender"}),
|
| 187 |
+
frozenset({"region"}), frozenset({"specialization"}),
|
| 188 |
+
frozenset({"work_area"}), frozenset({"branche"}),
|
| 189 |
+
frozenset({"jobloen"}), frozenset(),
|
| 190 |
+
]
|
| 191 |
+
axes = [a for a in order if a in chosen] + [a for a in chosen if a not in order]
|
| 192 |
+
cells = [_to_cell(chosen[a], growth_pct) for a in axes]
|
| 193 |
+
|
| 194 |
+
notes: list[str] = []
|
| 195 |
+
# Headline = most specific non-suppressed cell that has a usable central figure.
|
| 196 |
+
headline = next(
|
| 197 |
+
(r for r in matched
|
| 198 |
+
if r.reliability_tier != "suppressed"
|
| 199 |
+
and (r.measure.median is not None or r.measure.mean is not None)),
|
| 200 |
+
None,
|
| 201 |
+
)
|
| 202 |
+
user_pct = gap = projected_target = None
|
| 203 |
+
forecast_range = (3.2, 3.5) if growth_pct else None
|
| 204 |
+
|
| 205 |
+
if headline is not None:
|
| 206 |
+
hm = gated_measure(headline)
|
| 207 |
+
central = hm.median if hm.median is not None else hm.mean
|
| 208 |
+
user_gross = profile.current_monthly_gross
|
| 209 |
+
if user_gross is not None and profile.current_monthly_gross_max:
|
| 210 |
+
user_gross = (user_gross + profile.current_monthly_gross_max) // 2
|
| 211 |
+
if user_gross is not None and central is not None:
|
| 212 |
+
user_pct = _estimate_percentile(user_gross, hm)
|
| 213 |
+
gap = user_gross - central
|
| 214 |
+
ask_base = hm.p75 if hm.p75 is not None else central
|
| 215 |
+
if ask_base is not None and growth_pct:
|
| 216 |
+
projected_target = round(ask_base * (1 + growth_pct / 100))
|
| 217 |
+
if headline.includes_executives is False:
|
| 218 |
+
notes.append("Benchmark excludes top executives (ekskl. topchefer); "
|
| 219 |
+
"means are not comparable across this flag.")
|
| 220 |
+
notes.append(
|
| 221 |
+
f"Most relevant comparison: {_comparison_label(headline)} "
|
| 222 |
+
f"(n={int(headline.sample_size) if headline.sample_size else 'n/a'}, "
|
| 223 |
+
f"{headline.reliability_tier})."
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
if growth_pct:
|
| 227 |
+
notes.append(
|
| 228 |
+
f"Figures projected to 2026 using the {forecast_range[0]}–{forecast_range[1]}% "
|
| 229 |
+
f"private forecast band (~{growth_pct}% applied)."
|
| 230 |
+
)
|
| 231 |
+
if not cells:
|
| 232 |
+
notes.append("No reliable salary cells matched this profile "
|
| 233 |
+
"(samples may be suppressed; broaden the profile).")
|
| 234 |
+
|
| 235 |
+
return Dashboard(
|
| 236 |
+
profile=profile,
|
| 237 |
+
cells=cells,
|
| 238 |
+
forecast_pct_range=forecast_range,
|
| 239 |
+
user_percentile=user_pct,
|
| 240 |
+
gap_to_median=gap,
|
| 241 |
+
projected_negotiation_target=projected_target,
|
| 242 |
+
notes=notes,
|
| 243 |
+
)
|
src/backend/indexing/ida_transform.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Transform the verified flat ``salary_records.jsonl`` into the structured
|
| 2 |
+
``IdaSalaryRecord`` schema (the recoverable tables).
|
| 3 |
+
|
| 4 |
+
The original extraction (deterministic ``pdfplumber``, 0/7,000 digit mismatches)
|
| 5 |
+
captured each cell's numbers, cohort, sector and the raw ``segment_label``. For
|
| 6 |
+
the tables where the second breakdown axis survived in ``segment_label``
|
| 7 |
+
(education in the title, specialization t8, work_area t9, branche t10, position
|
| 8 |
+
t13, jobløn t16), we can map straight into the typed wildcard schema here — no
|
| 9 |
+
re-parsing. The tables whose second axis was flattened away (region t11, gender×
|
| 10 |
+
leadership t17, public position t27, management t14) are handled separately by
|
| 11 |
+
re-extraction and are skipped here.
|
| 12 |
+
|
| 13 |
+
WILDCARD CONTRACT: every filterable dimension is set to a specific enum value or
|
| 14 |
+
the sentinel ``"all"`` (aggregate) — never null.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import re
|
| 20 |
+
|
| 21 |
+
from ..schemas import IdaSalaryRecord, Measure
|
| 22 |
+
|
| 23 |
+
# Tables re-extracted from the PDF (their discriminating axis was lost in the
|
| 24 |
+
# flat extraction) — skip them here to avoid emitting wrong/aggregate-only cells.
|
| 25 |
+
REEXTRACT_TABLES = {11, 14, 17, 27}
|
| 26 |
+
|
| 27 |
+
# table_id → education_type (tables 3–7 break down a single education by cohort).
|
| 28 |
+
EDUCATION_BY_TABLE = {
|
| 29 |
+
3: "akademiingenior",
|
| 30 |
+
4: "civilingenior",
|
| 31 |
+
5: "teknikumingenior",
|
| 32 |
+
6: "diplom_eksportingenior",
|
| 33 |
+
7: "cand_scient",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
# Ordinal rank for "next level up" framing; null when position_level == "all".
|
| 37 |
+
POSITION_RANK = {
|
| 38 |
+
"trainee": 1, "anden": 2, "ingenior_akademiker": 3, "specialist": 4,
|
| 39 |
+
"chef_seniorkonsulent": 5, "projektleder": 6, "afdelingschef": 7,
|
| 40 |
+
"funktionsdirektor": 8, "topchef": 9,
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# management_span_band → (span_min, span_max). Used only by re-extraction (t14);
|
| 44 |
+
# included for completeness so the transform stays the single source of truth.
|
| 45 |
+
SPAN_BOUNDS = {
|
| 46 |
+
"none": (None, None), "1-4": (1, 4), "5-10": (5, 10), "11-19": (11, 19),
|
| 47 |
+
"20-29": (20, 29), "30-50": (30, 50), "51-100": (51, 100),
|
| 48 |
+
"101-200": (101, 200), "201-500": (201, 500), "501-1000": (501, 1000),
|
| 49 |
+
"1001+": (1001, None), "all": (None, None),
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
# data_period_month by sector (private snapshot Sept 2025, public May 2025).
|
| 53 |
+
PERIOD_MONTH = {
|
| 54 |
+
"private": "2025-09", "public_state": "2025-05",
|
| 55 |
+
"public_municipal": "2025-05", "public_regional": "2025-05",
|
| 56 |
+
"self_employed": None,
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _fold(s: str) -> str:
|
| 61 |
+
"""Normalize a Danish label for matching: strip count prefix, fold æøå, keep alnum."""
|
| 62 |
+
s = re.sub(r"^\d+\s+", "", s.strip()) # '21 Alle' → 'Alle', '17 Org…' → 'Org…'
|
| 63 |
+
s = s.lower().replace("æ", "ae").replace("ø", "oe").replace("å", "aa")
|
| 64 |
+
return re.sub(r"[^a-z0-9]", "", s)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _is_all(label: str | None) -> bool:
|
| 68 |
+
"""True when the label is the aggregate ('Alle' / 'All' / empty)."""
|
| 69 |
+
return label is None or _fold(label) in {"alle", "all", ""}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# Normalized-label → enum maps for the recoverable segment dimensions.
|
| 73 |
+
_SPECIALIZATION = {
|
| 74 |
+
"byggerianlaeg": "byggeri_anlaeg",
|
| 75 |
+
"datalogiitsoftware": "datalogi_it_software",
|
| 76 |
+
"elektroniksvagstaerkstroem": "elektronik",
|
| 77 |
+
"kemibiologibiotek": "kemi_biologi_biotek",
|
| 78 |
+
"maskinindustriproduktionrobotmv": "maskin_industri_robot",
|
| 79 |
+
"energimiljoe": "energi_miljo",
|
| 80 |
+
"eksportledelseforretningsudvikling": "eksport_ledelse_forretning",
|
| 81 |
+
"levnedsmiddel": "levnedsmiddel",
|
| 82 |
+
"matematikfysikgeologi": "matematik_fysik_geologi",
|
| 83 |
+
}
|
| 84 |
+
_WORK_AREA = {
|
| 85 |
+
"forskninginnovationundervisning": "forskning_innovation_undervisning",
|
| 86 |
+
"byggerianlaeg": "byggeri_anlaeg",
|
| 87 |
+
"industriproduktionkemi": "industri_produktion_kemi",
|
| 88 |
+
"elektroteknikogtele": "elektroteknik_tele",
|
| 89 |
+
"itogprogrammering": "it_programmering",
|
| 90 |
+
"energiforsyningmiljoe": "energi_forsyning_miljo",
|
| 91 |
+
"transportinfrastruktur": "transport_infrastruktur",
|
| 92 |
+
"naturvidenskabmedicin": "naturvidenskab_medicin",
|
| 93 |
+
"administrationledelseservice": "administration_ledelse_service",
|
| 94 |
+
}
|
| 95 |
+
_BRANCHE = {
|
| 96 |
+
"raastofindvindingmv": "raastofindvinding",
|
| 97 |
+
"forsyningsvirksomhed": "forsyningsvirksomhed",
|
| 98 |
+
"foedevarekemiplasttraeindustri": "foedevare_kemi_plast_trae",
|
| 99 |
+
"medicinalindustri": "medicinalindustri",
|
| 100 |
+
"maskinjernogmetalindustri": "maskin_jern_metal",
|
| 101 |
+
"denelektroniskeindustri": "elektronisk_industri",
|
| 102 |
+
"raadgivendeingenioervirksomhed": "raadgivende_ingenior",
|
| 103 |
+
"entreprenoervirksomhedbyggeoganlaeg": "entreprenor_bygge_anlaeg",
|
| 104 |
+
"itogtelekommunikationsvirksomhed": "it_telekommunikation",
|
| 105 |
+
"forskningogudviklingfinansmv": "forskning_udvikling_finans",
|
| 106 |
+
"handeltransport": "handel_transport",
|
| 107 |
+
"organisationermv": "organisationer",
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _lookup(label: str, table: dict[str, str], dim: str) -> str:
|
| 112 |
+
"""Map a raw segment label to its enum value; fail loud on vocabulary drift."""
|
| 113 |
+
if _is_all(label):
|
| 114 |
+
return "all"
|
| 115 |
+
key = _fold(label)
|
| 116 |
+
if key not in table:
|
| 117 |
+
raise ValueError(f"Unmapped {dim} label {label!r} (folded {key!r})")
|
| 118 |
+
return table[key]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _position_to_enum(label: str) -> str:
|
| 122 |
+
"""Map a tabel-13 position label to the position_level enum (keyword-ordered)."""
|
| 123 |
+
if _is_all(label):
|
| 124 |
+
return "all"
|
| 125 |
+
low = label.lower()
|
| 126 |
+
if "topchef" in low or "funktionsdirekt" in low:
|
| 127 |
+
return "topchef"
|
| 128 |
+
if "afdelingschef" in low:
|
| 129 |
+
return "afdelingschef"
|
| 130 |
+
if "projektleder" in low:
|
| 131 |
+
return "projektleder"
|
| 132 |
+
if "seniorkonsulent" in low or low.startswith("chef"):
|
| 133 |
+
return "chef_seniorkonsulent"
|
| 134 |
+
if "specialist" in low:
|
| 135 |
+
return "specialist"
|
| 136 |
+
if "trainee" in low or "erhvervsph" in low:
|
| 137 |
+
return "trainee"
|
| 138 |
+
if "anden" in low:
|
| 139 |
+
return "anden"
|
| 140 |
+
if "ingeniør" in low or "akademisk" in low or "konsulent" in low:
|
| 141 |
+
return "ingenior_akademiker"
|
| 142 |
+
raise ValueError(f"Unmapped position label {label!r}")
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def reliability_tier(n: float | None) -> str:
|
| 146 |
+
"""KRL suppression rule on sample size."""
|
| 147 |
+
if n is None or n < 5:
|
| 148 |
+
return "suppressed"
|
| 149 |
+
if n < 10:
|
| 150 |
+
return "mean_only"
|
| 151 |
+
if n < 20:
|
| 152 |
+
return "median_ok"
|
| 153 |
+
if n < 50:
|
| 154 |
+
return "quartiles_ok"
|
| 155 |
+
return "full"
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _includes_executives(title: str | None) -> bool | None:
|
| 159 |
+
"""Read inkl./ekskl. topchefer from the caption; None when not stated."""
|
| 160 |
+
if not title:
|
| 161 |
+
return None
|
| 162 |
+
low = title.lower()
|
| 163 |
+
if "topchef" not in low:
|
| 164 |
+
return None
|
| 165 |
+
if "ekskl" in low or "uden topchef" in low:
|
| 166 |
+
return False
|
| 167 |
+
if "inkl" in low or "med topchef" in low:
|
| 168 |
+
return True
|
| 169 |
+
return None
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _corrected_cohort(rec: dict) -> tuple[int | None, int | None]:
|
| 173 |
+
"""Return (graduation_year_start, end) with the 'Før YYYY' convention fixed.
|
| 174 |
+
|
| 175 |
+
The source extraction stored open-ended-below cohorts backwards (e.g.
|
| 176 |
+
'Før 1985' → start=1985, end=None). The contract is half-open the other way:
|
| 177 |
+
'Før YYYY' = graduated in or before YYYY-1, i.e. start=None, end=YYYY-1.
|
| 178 |
+
Bounded cohorts ('1986-1987') and 'Alle' (None/None) are already correct.
|
| 179 |
+
"""
|
| 180 |
+
label = (rec.get("row_label") or "").lower()
|
| 181 |
+
m = re.search(r"(?:før|foer)\s*(\d{4})", label)
|
| 182 |
+
if m:
|
| 183 |
+
return None, int(m.group(1)) - 1
|
| 184 |
+
return rec.get("graduation_year_start"), rec.get("graduation_year_end")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def transform_record(rec: dict) -> IdaSalaryRecord | None:
|
| 188 |
+
"""Map one flat record into an ``IdaSalaryRecord``; None if it should be skipped."""
|
| 189 |
+
tid = rec.get("table_id")
|
| 190 |
+
if tid in REEXTRACT_TABLES or tid is None:
|
| 191 |
+
return None # handled by re-extraction (or an orphan fragment)
|
| 192 |
+
|
| 193 |
+
sector = rec["sector"]
|
| 194 |
+
seg = rec.get("segment_label")
|
| 195 |
+
dims: list[str] = []
|
| 196 |
+
|
| 197 |
+
education_type = "all"
|
| 198 |
+
position_level = "all"
|
| 199 |
+
specialization = "all"
|
| 200 |
+
work_area = "all"
|
| 201 |
+
branche = "all"
|
| 202 |
+
jobloen = "all"
|
| 203 |
+
|
| 204 |
+
if tid in EDUCATION_BY_TABLE:
|
| 205 |
+
education_type = EDUCATION_BY_TABLE[tid]
|
| 206 |
+
dims.append("education_type")
|
| 207 |
+
elif tid == 8:
|
| 208 |
+
specialization = _lookup(seg, _SPECIALIZATION, "specialization")
|
| 209 |
+
if specialization != "all":
|
| 210 |
+
dims.append("specialization")
|
| 211 |
+
elif tid == 9:
|
| 212 |
+
work_area = _lookup(seg, _WORK_AREA, "work_area")
|
| 213 |
+
if work_area != "all":
|
| 214 |
+
dims.append("work_area")
|
| 215 |
+
elif tid == 10:
|
| 216 |
+
branche = _lookup(seg, _BRANCHE, "branche")
|
| 217 |
+
if branche != "all":
|
| 218 |
+
dims.append("branche")
|
| 219 |
+
elif tid == 13:
|
| 220 |
+
position_level = _position_to_enum(seg)
|
| 221 |
+
if position_level != "all":
|
| 222 |
+
dims.append("position_level")
|
| 223 |
+
elif tid == 16:
|
| 224 |
+
jobloen = "yes" # the whole table is jobløn-paid engineers
|
| 225 |
+
dims.append("jobloen")
|
| 226 |
+
# else: public cohort/løntrin tables (26,28,29,32,33,34,35,38,39) and
|
| 227 |
+
# self-employed (42) — sector is their only typed axis (+ cohort below).
|
| 228 |
+
|
| 229 |
+
gy_start, gy_end = _corrected_cohort(rec)
|
| 230 |
+
if gy_start is not None or gy_end is not None:
|
| 231 |
+
dims.append("graduation_year")
|
| 232 |
+
# Recompute experience from corrected bounds (2025 snapshot proxy).
|
| 233 |
+
exp_min = 2025 - gy_end if gy_end is not None else None
|
| 234 |
+
exp_max = 2025 - gy_start if gy_start is not None else None
|
| 235 |
+
|
| 236 |
+
n = rec.get("measure", {}).get("count")
|
| 237 |
+
record_type = "self_employed_income" if sector == "self_employed" else "salary_observation"
|
| 238 |
+
|
| 239 |
+
out = IdaSalaryRecord(
|
| 240 |
+
id=rec["id"],
|
| 241 |
+
source_doc=rec["source_doc"],
|
| 242 |
+
source_page=rec["source_page"],
|
| 243 |
+
table_id=tid,
|
| 244 |
+
table_title=rec.get("table_title"),
|
| 245 |
+
table_group=rec.get("table_group"),
|
| 246 |
+
record_type=record_type,
|
| 247 |
+
sector=sector,
|
| 248 |
+
period=rec.get("period"),
|
| 249 |
+
data_period_month=PERIOD_MONTH.get(sector),
|
| 250 |
+
pay_concept=rec["pay_concept"],
|
| 251 |
+
includes_executives=_includes_executives(rec.get("table_title")),
|
| 252 |
+
education_type=education_type,
|
| 253 |
+
position_level=position_level,
|
| 254 |
+
position_level_rank=POSITION_RANK.get(position_level),
|
| 255 |
+
specialization=specialization,
|
| 256 |
+
work_area=work_area,
|
| 257 |
+
branche=branche,
|
| 258 |
+
jobloen=jobloen,
|
| 259 |
+
row_dimension=rec.get("row_dimension"),
|
| 260 |
+
row_label=rec.get("row_label"),
|
| 261 |
+
segment_dimension=rec.get("segment_dimension"),
|
| 262 |
+
segment_label=seg,
|
| 263 |
+
graduation_year_start=gy_start,
|
| 264 |
+
graduation_year_end=gy_end,
|
| 265 |
+
experience_years_min=exp_min,
|
| 266 |
+
experience_years_max=exp_max,
|
| 267 |
+
age_min=rec.get("age_min"),
|
| 268 |
+
age_max=rec.get("age_max"),
|
| 269 |
+
dimension_keys=dims,
|
| 270 |
+
specificity=len(dims),
|
| 271 |
+
sample_size=n,
|
| 272 |
+
reliability_tier=reliability_tier(n),
|
| 273 |
+
measure=Measure(**rec.get("measure", {})),
|
| 274 |
+
rag_text="", # filled by the orchestrator's make_rag_text()
|
| 275 |
+
)
|
| 276 |
+
return out
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def transform_corpus(records: list[dict]) -> list[IdaSalaryRecord]:
|
| 280 |
+
"""Transform all recoverable IDA records (skips re-extracted tables)."""
|
| 281 |
+
out: list[IdaSalaryRecord] = []
|
| 282 |
+
for rec in records:
|
| 283 |
+
if rec.get("union") != "IDA":
|
| 284 |
+
continue
|
| 285 |
+
t = transform_record(rec)
|
| 286 |
+
if t is not None:
|
| 287 |
+
out.append(t)
|
| 288 |
+
return out
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
# ---------------------------------------------------------------------------
|
| 292 |
+
# rag_text generation (English; embedded for vector recall). Per the contract,
|
| 293 |
+
# rag_text MUST name the human-readable value of every dimension_keys entry,
|
| 294 |
+
# carry the reliability caveat, and cite the source.
|
| 295 |
+
# ---------------------------------------------------------------------------
|
| 296 |
+
|
| 297 |
+
_DISPLAY: dict[str, dict[str, str]] = {
|
| 298 |
+
"education_type": {
|
| 299 |
+
"civilingenior": "Civil engineers (civilingeniører)",
|
| 300 |
+
"akademiingenior": "Academy engineers (akademiingeniører)",
|
| 301 |
+
"teknikumingenior": "Technical engineers (teknikumingeniører)",
|
| 302 |
+
"diplom_eksportingenior": "Diploma/export engineers",
|
| 303 |
+
"cand_scient": "Science graduates (cand.scient.)",
|
| 304 |
+
},
|
| 305 |
+
"sector": {
|
| 306 |
+
"private": "private sector", "public_state": "state sector",
|
| 307 |
+
"public_municipal": "municipal sector", "public_regional": "regional sector",
|
| 308 |
+
"self_employed": "self-employed",
|
| 309 |
+
},
|
| 310 |
+
"region": {
|
| 311 |
+
"hovedstaden": "Capital Region", "sjaelland": "Region Zealand",
|
| 312 |
+
"syddanmark": "Region of Southern Denmark", "midtjylland": "Central Denmark Region",
|
| 313 |
+
"nordjylland": "North Denmark Region",
|
| 314 |
+
},
|
| 315 |
+
"position_level": {
|
| 316 |
+
"topchef": "top executives", "funktionsdirektor": "functional directors",
|
| 317 |
+
"afdelingschef": "department heads/team leaders", "projektleder": "project managers",
|
| 318 |
+
"chef_seniorkonsulent": "chief/senior consultants", "specialist": "specialists",
|
| 319 |
+
"ingenior_akademiker": "engineers/academic staff", "anden": "other roles",
|
| 320 |
+
"trainee": "trainees",
|
| 321 |
+
},
|
| 322 |
+
"gender": {"male": "men", "female": "women"},
|
| 323 |
+
"is_leader": {"leader": "with staff responsibility", "non_leader": "without staff responsibility"},
|
| 324 |
+
"manages_people": {"yes": "managing people", "no": "not managing people"},
|
| 325 |
+
"jobloen": {"yes": "on fixed salary (jobløn)", "no": "not on jobløn"},
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
_TIER_CAVEAT = {
|
| 329 |
+
"suppressed": "Sample too small to report figures (n<5).",
|
| 330 |
+
"mean_only": "Small sample (n=5–9): average only.",
|
| 331 |
+
"median_ok": "Limited sample (n=10–19): median shown, no quartiles.",
|
| 332 |
+
"quartiles_ok": "Sample n=20–49: quartiles shown, no 90th percentile.",
|
| 333 |
+
"full": "Full reliability (n≥50).",
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _cohort_phrase(rec: IdaSalaryRecord) -> str:
|
| 338 |
+
s, e = rec.graduation_year_start, rec.graduation_year_end
|
| 339 |
+
if s is None and e is None:
|
| 340 |
+
return ""
|
| 341 |
+
if s is None:
|
| 342 |
+
return f"graduated in or before {e}"
|
| 343 |
+
if e is None:
|
| 344 |
+
return f"graduated {s} or later"
|
| 345 |
+
return f"graduation cohort {s}–{e}" if s != e else f"graduated {s}"
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def _dim_phrases(rec: IdaSalaryRecord) -> list[str]:
|
| 349 |
+
"""Human-readable phrase for each specific dimension key."""
|
| 350 |
+
out: list[str] = []
|
| 351 |
+
for key in rec.dimension_keys:
|
| 352 |
+
if key == "graduation_year":
|
| 353 |
+
p = _cohort_phrase(rec)
|
| 354 |
+
if p:
|
| 355 |
+
out.append(p)
|
| 356 |
+
continue
|
| 357 |
+
val = getattr(rec, key, "all")
|
| 358 |
+
disp = _DISPLAY.get(key, {}).get(val)
|
| 359 |
+
# specialization/work_area/branche keep their raw Danish label (readable).
|
| 360 |
+
if disp is None:
|
| 361 |
+
disp = (rec.segment_label or val) if key in (
|
| 362 |
+
"specialization", "work_area", "branche") else val
|
| 363 |
+
out.append(str(disp))
|
| 364 |
+
return out
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def _stats_phrase(rec: IdaSalaryRecord) -> str:
|
| 368 |
+
"""Figures permitted by the reliability tier, in DKK/month."""
|
| 369 |
+
m, tier = rec.measure, rec.reliability_tier
|
| 370 |
+
if tier == "suppressed":
|
| 371 |
+
return ""
|
| 372 |
+
parts: list[str] = []
|
| 373 |
+
if m.mean is not None:
|
| 374 |
+
parts.append(f"average {int(m.mean):,} DKK/month")
|
| 375 |
+
if tier in ("median_ok", "quartiles_ok", "full") and m.median is not None:
|
| 376 |
+
parts.append(f"median {int(m.median):,}")
|
| 377 |
+
if tier in ("quartiles_ok", "full") and m.p25 is not None and m.p75 is not None:
|
| 378 |
+
parts.append(f"quartiles {int(m.p25):,}–{int(m.p75):,}")
|
| 379 |
+
if tier == "full" and m.p90 is not None:
|
| 380 |
+
parts.append(f"90th pct {int(m.p90):,}")
|
| 381 |
+
return ", ".join(parts).replace(",", ".") # Danish thousands separator
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def make_rag_text(rec: IdaSalaryRecord) -> str:
|
| 385 |
+
"""Build the embedding text for a record (English, dimension-complete)."""
|
| 386 |
+
edu = _DISPLAY["education_type"].get(rec.education_type, "IDA engineers")
|
| 387 |
+
head = edu if rec.education_type != "all" else "IDA members"
|
| 388 |
+
sector = _DISPLAY["sector"].get(rec.sector, rec.sector)
|
| 389 |
+
dims = [d for d in _dim_phrases(rec) if d and d != head]
|
| 390 |
+
seg = f" — {', '.join(dims)}" if dims else ""
|
| 391 |
+
|
| 392 |
+
if rec.record_type == "salary_growth_rate":
|
| 393 |
+
body = f"3-year gliding annual salary growth {rec.cohort_growth_pct}% ({sector})."
|
| 394 |
+
elif rec.record_type == "salary_forecast":
|
| 395 |
+
body = f"2026 salary forecast {rec.cohort_growth_pct}% expected pay increase ({sector})."
|
| 396 |
+
else:
|
| 397 |
+
stats = _stats_phrase(rec)
|
| 398 |
+
n = int(rec.sample_size) if rec.sample_size is not None else "n/a"
|
| 399 |
+
body = (f"{stats}. Based on {n} respondents."
|
| 400 |
+
if stats else f"Based on {n} respondents.")
|
| 401 |
+
|
| 402 |
+
caveat = _TIER_CAVEAT.get(rec.reliability_tier, "")
|
| 403 |
+
tbl = f", tabel {rec.table_id}" if rec.table_id else ""
|
| 404 |
+
cite = f"Source: {rec.source_doc}{tbl}, p.{rec.source_page}."
|
| 405 |
+
return f"IDA Lønstatistik 2025 — {head}, {sector}{seg}. {body} {caveat} {cite}".strip()
|
src/backend/retrieval.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Profile → IDA cell matching, ranking, and reliability gating (v1).
|
| 2 |
+
|
| 3 |
+
The brief's pipeline, in order:
|
| 4 |
+
1. wildcard pre-filter (record_type → sector → remaining dimensions),
|
| 5 |
+
2. (optional) vector search within the candidate set,
|
| 6 |
+
3. specificity ranking (prefer the most specific cell consistent with the
|
| 7 |
+
profile; fall back to broader cells when the specific one is suppressed),
|
| 8 |
+
4. reliability gating (only render the measure.* a cell's tier permits).
|
| 9 |
+
|
| 10 |
+
This module is deterministic and CPU-only — it operates on the structured
|
| 11 |
+
records loaded from ``ida_records.jsonl``. Vector search is an optional recall
|
| 12 |
+
backstop for free-text queries; the profile-driven dashboard does not need it.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
from .config import REPO_ROOT
|
| 21 |
+
from .schemas import IdaSalaryRecord, Measure, Profile
|
| 22 |
+
|
| 23 |
+
_RECORDS_PATH = REPO_ROOT / "data" / "processed" / "lonstatistik" / "ida_records.jsonl"
|
| 24 |
+
_records: list[IdaSalaryRecord] | None = None
|
| 25 |
+
|
| 26 |
+
# Dimensions the onboarding profile actually constrains. specialization /
|
| 27 |
+
# work_area / branche are not asked (so never constrained → match any cell);
|
| 28 |
+
# stem_count_band and management_span are captured but DO NOT filter in v1.
|
| 29 |
+
_PROFILE_FILTER_DIMS = ("education_type", "region", "position_level", "gender",
|
| 30 |
+
"manages_people")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def load_records(path: Path | None = None) -> list[IdaSalaryRecord]:
|
| 34 |
+
"""Load the structured IDA corpus once (lazy singleton)."""
|
| 35 |
+
global _records
|
| 36 |
+
if _records is None or path is not None:
|
| 37 |
+
p = path or _RECORDS_PATH
|
| 38 |
+
recs = [
|
| 39 |
+
IdaSalaryRecord(**json.loads(line))
|
| 40 |
+
for line in p.read_text(encoding="utf-8").splitlines()
|
| 41 |
+
if line.strip()
|
| 42 |
+
]
|
| 43 |
+
if path is not None:
|
| 44 |
+
return recs
|
| 45 |
+
_records = recs
|
| 46 |
+
return _records
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _profile_constraints(profile: Profile) -> dict[str, str]:
|
| 50 |
+
"""The dimensions the profile pins to a specific value (drops 'all'/None)."""
|
| 51 |
+
raw = {
|
| 52 |
+
"education_type": profile.education_type,
|
| 53 |
+
"region": profile.region,
|
| 54 |
+
"position_level": profile.position_level,
|
| 55 |
+
"gender": profile.gender,
|
| 56 |
+
"manages_people": profile.manages_people,
|
| 57 |
+
}
|
| 58 |
+
return {
|
| 59 |
+
d: v for d, v in raw.items()
|
| 60 |
+
if d in _PROFILE_FILTER_DIMS and v is not None and v != "all"
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def matches(rec: IdaSalaryRecord, profile: Profile) -> bool:
|
| 65 |
+
"""Wildcard match on the remaining dimensions + graduation-year interval.
|
| 66 |
+
|
| 67 |
+
(record_type and sector are pre-filtered by the caller.) A dimension matches
|
| 68 |
+
iff the record's value equals the profile's OR is the ``"all"`` aggregate.
|
| 69 |
+
Dimensions the profile leaves open are not constrained.
|
| 70 |
+
"""
|
| 71 |
+
for dim, pval in _profile_constraints(profile).items():
|
| 72 |
+
rval = getattr(rec, dim)
|
| 73 |
+
if rval != pval and rval != "all":
|
| 74 |
+
return False
|
| 75 |
+
# graduation_year by interval (half-open; null = unbounded on that side).
|
| 76 |
+
y = profile.graduation_year
|
| 77 |
+
s, e = rec.graduation_year_start, rec.graduation_year_end
|
| 78 |
+
if (s is not None and y < s) or (e is not None and y > e):
|
| 79 |
+
return False
|
| 80 |
+
return True
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _renderable_fields(tier: str) -> set[str]:
|
| 84 |
+
"""Which measure.* fields a reliability tier permits rendering."""
|
| 85 |
+
if tier == "suppressed":
|
| 86 |
+
return set()
|
| 87 |
+
fields = {"mean", "base", "supplement", "pension", "bonus_avg", "bonus_pct", "count"}
|
| 88 |
+
if tier in ("median_ok", "quartiles_ok", "full"):
|
| 89 |
+
fields.add("median")
|
| 90 |
+
if tier in ("quartiles_ok", "full"):
|
| 91 |
+
fields |= {"p25", "p75"}
|
| 92 |
+
if tier == "full":
|
| 93 |
+
fields.add("p90")
|
| 94 |
+
return fields
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def gated_measure(rec: IdaSalaryRecord) -> Measure:
|
| 98 |
+
"""Return a Measure with only the fields the record's tier permits."""
|
| 99 |
+
allowed = _renderable_fields(rec.reliability_tier)
|
| 100 |
+
kept = {
|
| 101 |
+
f: getattr(rec.measure, f)
|
| 102 |
+
for f in allowed
|
| 103 |
+
if getattr(rec.measure, f) is not None
|
| 104 |
+
}
|
| 105 |
+
return Measure(**kept)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _rank_key(rec: IdaSalaryRecord) -> tuple:
|
| 109 |
+
"""Most specific first; among equals prefer larger, more reliable samples."""
|
| 110 |
+
return (rec.specificity, rec.sample_size or 0)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def retrieve(
|
| 114 |
+
profile: Profile,
|
| 115 |
+
record_type: str = "salary_observation",
|
| 116 |
+
sectors: tuple[str, ...] | None = None,
|
| 117 |
+
) -> list[IdaSalaryRecord]:
|
| 118 |
+
"""Hard pre-filter (record_type → sector → dims) then specificity ranking.
|
| 119 |
+
|
| 120 |
+
Returns all matching records, most-specific first. Suppressed cells are
|
| 121 |
+
retained so the fusion layer can fall back to broader cells when a specific
|
| 122 |
+
one is suppressed.
|
| 123 |
+
"""
|
| 124 |
+
allow_sectors = sectors or (profile.sector, "all")
|
| 125 |
+
out = [
|
| 126 |
+
r for r in load_records()
|
| 127 |
+
if r.record_type == record_type
|
| 128 |
+
and r.sector in allow_sectors
|
| 129 |
+
and matches(r, profile)
|
| 130 |
+
]
|
| 131 |
+
out.sort(key=_rank_key, reverse=True)
|
| 132 |
+
return out
|
src/backend/schemas.py
CHANGED
|
@@ -7,6 +7,8 @@ implementations have a stable contract to build against.
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
|
|
|
|
|
|
| 10 |
from pydantic import BaseModel, Field
|
| 11 |
|
| 12 |
|
|
@@ -74,3 +76,209 @@ class ChatResponse(BaseModel):
|
|
| 74 |
"This is general information, not legal advice. "
|
| 75 |
"Consult a qualified professional for your situation."
|
| 76 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
+
from typing import Literal, Optional
|
| 11 |
+
|
| 12 |
from pydantic import BaseModel, Field
|
| 13 |
|
| 14 |
|
|
|
|
| 76 |
"This is general information, not legal advice. "
|
| 77 |
"Consult a qualified professional for your situation."
|
| 78 |
)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ---------------------------------------------------------------------------
|
| 82 |
+
# IDA salary corpus (v1) — structured, profile-matchable records.
|
| 83 |
+
#
|
| 84 |
+
# These types mirror data/processed/lonstatistik/ida_salary_record.schema.json,
|
| 85 |
+
# the authoritative contract. That JSON Schema is the source of truth and every
|
| 86 |
+
# built record is validated against it; these Literals give the transform/match
|
| 87 |
+
# code type safety for the dimensions we actually filter on. WILDCARD CONTRACT:
|
| 88 |
+
# every filterable dimension is a required enum that includes "all" (the only
|
| 89 |
+
# wildcard). Profiles never carry "all" — a skipped answer simply drops the
|
| 90 |
+
# constraint (which then matches "all" cells and any specific cell).
|
| 91 |
+
# ---------------------------------------------------------------------------
|
| 92 |
+
|
| 93 |
+
RecordType = Literal[
|
| 94 |
+
"salary_observation", "salary_growth_rate", "salary_forecast",
|
| 95 |
+
"minimum_wage_recommendation", "benefit_prevalence", "terms_stat",
|
| 96 |
+
"self_employed_income",
|
| 97 |
+
]
|
| 98 |
+
Sector = Literal[
|
| 99 |
+
"private", "public_state", "public_municipal", "public_regional",
|
| 100 |
+
"self_employed", "all",
|
| 101 |
+
]
|
| 102 |
+
EducationType = Literal[
|
| 103 |
+
"civilingenior", "akademiingenior", "diplom_eksportingenior",
|
| 104 |
+
"teknikumingenior", "cand_scient", "all",
|
| 105 |
+
]
|
| 106 |
+
Region = Literal[
|
| 107 |
+
"hovedstaden", "sjaelland", "syddanmark", "midtjylland", "nordjylland", "all",
|
| 108 |
+
]
|
| 109 |
+
PositionLevel = Literal[
|
| 110 |
+
"topchef", "funktionsdirektor", "afdelingschef", "projektleder",
|
| 111 |
+
"chef_seniorkonsulent", "specialist", "ingenior_akademiker", "anden",
|
| 112 |
+
"trainee", "all",
|
| 113 |
+
]
|
| 114 |
+
Gender = Literal["male", "female", "all"]
|
| 115 |
+
IsLeader = Literal["leader", "non_leader", "all"]
|
| 116 |
+
YesNoAll = Literal["yes", "no", "all"]
|
| 117 |
+
ManagementSpanBand = Literal[
|
| 118 |
+
"none", "1-4", "5-10", "11-19", "20-29", "30-50", "51-100", "101-200",
|
| 119 |
+
"201-500", "501-1000", "1001+", "all",
|
| 120 |
+
]
|
| 121 |
+
StemCountBand = Literal["1-9", "10-24", "25-99", "100-249", "250+", "all"]
|
| 122 |
+
ReliabilityTier = Literal[
|
| 123 |
+
"suppressed", "mean_only", "median_ok", "quartiles_ok", "full",
|
| 124 |
+
]
|
| 125 |
+
DimensionKey = Literal[
|
| 126 |
+
"sector", "education_type", "region", "position_level", "gender",
|
| 127 |
+
"is_leader", "manages_people", "management_span_band", "stem_count_band",
|
| 128 |
+
"specialization", "work_area", "branche", "jobloen", "graduation_year",
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
# The filterable dimension fields (in the brief's pre-filter order: record_type
|
| 132 |
+
# and sector are applied first, separately). Used by the matcher/transform.
|
| 133 |
+
FILTER_DIMENSIONS: tuple[str, ...] = (
|
| 134 |
+
"education_type", "region", "position_level", "gender", "is_leader",
|
| 135 |
+
"manages_people", "stem_count_band", "specialization", "work_area",
|
| 136 |
+
"branche", "jobloen",
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class Measure(BaseModel):
|
| 141 |
+
"""Reported statistics. All monetary values DKK; bonus_pct a percentage."""
|
| 142 |
+
|
| 143 |
+
count: float | None = None
|
| 144 |
+
mean: float | None = None
|
| 145 |
+
p25: float | None = None
|
| 146 |
+
median: float | None = None
|
| 147 |
+
p75: float | None = None
|
| 148 |
+
p90: float | None = None
|
| 149 |
+
base: float | None = None
|
| 150 |
+
supplement: float | None = None
|
| 151 |
+
pension: float | None = None
|
| 152 |
+
bonus_avg: float | None = None
|
| 153 |
+
bonus_pct: float | None = None
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class IdaSalaryRecord(BaseModel):
|
| 157 |
+
"""One structured IDA observation (mirrors the JSON Schema contract).
|
| 158 |
+
|
| 159 |
+
Built by the transform/re-extraction, validated against the JSON Schema,
|
| 160 |
+
then embedded. Every filterable dimension defaults to ``"all"`` so a missed
|
| 161 |
+
backfill is a visible aggregation, never a hidden null.
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
id: str
|
| 165 |
+
union: Literal["IDA"] = "IDA"
|
| 166 |
+
source_doc: str
|
| 167 |
+
source_page: int
|
| 168 |
+
table_id: int | None = None
|
| 169 |
+
table_title: str | None = None
|
| 170 |
+
table_group: str | None = None
|
| 171 |
+
record_type: RecordType = "salary_observation"
|
| 172 |
+
sector: Sector
|
| 173 |
+
period: str | None = None
|
| 174 |
+
data_period_month: str | None = None
|
| 175 |
+
currency: Literal["DKK"] = "DKK"
|
| 176 |
+
pay_concept: Literal[
|
| 177 |
+
"gross_monthly", "gross_monthly_with_components", "net_monthly",
|
| 178 |
+
"annual_income",
|
| 179 |
+
]
|
| 180 |
+
includes_executives: bool | None = None
|
| 181 |
+
# Filterable dimensions (required, "all" = aggregate / wildcard).
|
| 182 |
+
education_type: EducationType = "all"
|
| 183 |
+
region: Region = "all"
|
| 184 |
+
position_level: PositionLevel = "all"
|
| 185 |
+
position_level_rank: int | None = None
|
| 186 |
+
gender: Gender = "all"
|
| 187 |
+
is_leader: IsLeader = "all"
|
| 188 |
+
manages_people: YesNoAll = "all"
|
| 189 |
+
management_span_band: ManagementSpanBand = "all"
|
| 190 |
+
span_min: int | None = None
|
| 191 |
+
span_max: int | None = None
|
| 192 |
+
stem_count_band: StemCountBand = "all"
|
| 193 |
+
specialization: str = "all"
|
| 194 |
+
work_area: str = "all"
|
| 195 |
+
branche: str = "all"
|
| 196 |
+
jobloen: YesNoAll = "all"
|
| 197 |
+
# Provenance (display only).
|
| 198 |
+
row_dimension: str | None = None
|
| 199 |
+
row_label: str | None = None
|
| 200 |
+
segment_dimension: str | None = None
|
| 201 |
+
segment_label: str | None = None
|
| 202 |
+
graduation_year_start: int | None = None
|
| 203 |
+
graduation_year_end: int | None = None
|
| 204 |
+
experience_years_min: int | None = None
|
| 205 |
+
experience_years_max: int | None = None
|
| 206 |
+
age_min: int | None = None
|
| 207 |
+
age_max: int | None = None
|
| 208 |
+
cohort_growth_pct: float | None = None
|
| 209 |
+
# Retrieval helpers.
|
| 210 |
+
dimension_keys: list[DimensionKey] = Field(default_factory=list)
|
| 211 |
+
specificity: int = 0
|
| 212 |
+
sample_size: float | None = None
|
| 213 |
+
reliability_tier: ReliabilityTier
|
| 214 |
+
measure: Measure
|
| 215 |
+
rag_text: str
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
class Profile(BaseModel):
|
| 219 |
+
"""User profile built from the 9 onboarding answers.
|
| 220 |
+
|
| 221 |
+
Maps to hard filters except where noted. ``graduation_year`` matches cohort
|
| 222 |
+
intervals (not equality). ``gender`` is skippable: ``None`` drops the
|
| 223 |
+
constraint. ``current_monthly_gross`` is *input*, not a filter — normalised
|
| 224 |
+
to IDA bruttoløn (base + permanent/temporary tillæg + fritvalg + taxable
|
| 225 |
+
perks + own and employer pension incl. ATP). ``stem_count_band`` /
|
| 226 |
+
``management_span_band`` are captured but do not filter in v1 (no IDA data);
|
| 227 |
+
``total_headcount_band`` is captured and unused.
|
| 228 |
+
"""
|
| 229 |
+
|
| 230 |
+
education_type: EducationType # Q1
|
| 231 |
+
graduation_year: int # Q2 (→ cohort interval; experience = 2025 − year)
|
| 232 |
+
sector: Sector # Q3
|
| 233 |
+
gender: Optional[Literal["male", "female"]] = None # Q4 (skippable)
|
| 234 |
+
current_monthly_gross: int | None = None # Q5 (input, not a filter)
|
| 235 |
+
current_monthly_gross_max: int | None = None # Q5 upper bound if a range
|
| 236 |
+
position_level: PositionLevel = "all" # Q6
|
| 237 |
+
manages_people: Literal["yes", "no"] | None = None # Q7
|
| 238 |
+
management_span_band: ManagementSpanBand | None = None # Q7 (captured, unused v1)
|
| 239 |
+
stem_count_band: StemCountBand | None = None # Q8a (captured, unused v1)
|
| 240 |
+
region: Region = "all" # Q8 (region)
|
| 241 |
+
total_headcount_band: str | None = None # Q8b (captured, unused v1)
|
| 242 |
+
|
| 243 |
+
@property
|
| 244 |
+
def experience_years(self) -> int:
|
| 245 |
+
"""Seniority proxy used for display (snapshot year is 2025)."""
|
| 246 |
+
return 2025 - self.graduation_year
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class RecommendationCell(BaseModel):
|
| 250 |
+
"""One fused comparison cell rendered to the dashboard.
|
| 251 |
+
|
| 252 |
+
Only the ``measure`` fields permitted by ``reliability_tier`` are populated;
|
| 253 |
+
forbidden statistics are left ``None`` (never rendered).
|
| 254 |
+
"""
|
| 255 |
+
|
| 256 |
+
comparison: str # human label, e.g. "Civilingeniører, your cohort, private"
|
| 257 |
+
citation: str # e.g. "IDA Lønstatistik 2025, tabel 4, p.11"
|
| 258 |
+
record_id: str
|
| 259 |
+
dimension_keys: list[DimensionKey] = Field(default_factory=list)
|
| 260 |
+
specificity: int = 0
|
| 261 |
+
sample_size: float | None = None
|
| 262 |
+
reliability_tier: ReliabilityTier = "suppressed"
|
| 263 |
+
includes_executives: bool | None = None
|
| 264 |
+
measure: Measure = Field(default_factory=Measure)
|
| 265 |
+
# 2026 projection of the cell's central figure (median or mean), if computed.
|
| 266 |
+
projected_median_2026: float | None = None
|
| 267 |
+
growth_pct_applied: float | None = None
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class Dashboard(BaseModel):
|
| 271 |
+
"""Profile-driven recommendation dashboard (v1, salary path)."""
|
| 272 |
+
|
| 273 |
+
profile: Profile
|
| 274 |
+
cells: list[RecommendationCell] = Field(default_factory=list)
|
| 275 |
+
forecast_pct_range: tuple[float, float] | None = None # e.g. (3.2, 3.5)
|
| 276 |
+
user_percentile: float | None = None # position within the best cell
|
| 277 |
+
gap_to_median: float | None = None # user gross − cell median (DKK)
|
| 278 |
+
projected_negotiation_target: float | None = None # 2026-projected ask
|
| 279 |
+
notes: list[str] = Field(default_factory=list)
|
| 280 |
+
disclaimer: str = (
|
| 281 |
+
"Salary figures are statistical benchmarks, not guarantees. "
|
| 282 |
+
"Means are not comparable across inkl./ekskl. topchefer. "
|
| 283 |
+
"This is general information, not legal or financial advice."
|
| 284 |
+
)
|
uv.lock
CHANGED
|
@@ -335,6 +335,63 @@ wheels = [
|
|
| 335 |
{ url = "https://download.pytorch.org/whl/certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18", upload-time = "2023-10-03T17:33:19Z" },
|
| 336 |
]
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
[[package]]
|
| 339 |
name = "charset-normalizer"
|
| 340 |
version = "2.1.1"
|
|
@@ -363,6 +420,56 @@ wheels = [
|
|
| 363 |
{ url = "https://download.pytorch.org/whl/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", upload-time = "2023-10-05T23:50:34Z" },
|
| 364 |
]
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
[[package]]
|
| 367 |
name = "faiss-cpu"
|
| 368 |
version = "1.14.2"
|
|
@@ -743,6 +850,33 @@ wheels = [
|
|
| 743 |
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
|
| 744 |
]
|
| 745 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
[[package]]
|
| 747 |
name = "markdown-it-py"
|
| 748 |
version = "4.2.0"
|
|
@@ -1229,7 +1363,9 @@ dependencies = [
|
|
| 1229 |
|
| 1230 |
[package.dev-dependencies]
|
| 1231 |
dev = [
|
|
|
|
| 1232 |
{ name = "modal" },
|
|
|
|
| 1233 |
]
|
| 1234 |
|
| 1235 |
[package.metadata]
|
|
@@ -1248,7 +1384,38 @@ requires-dist = [
|
|
| 1248 |
]
|
| 1249 |
|
| 1250 |
[package.metadata.requires-dev]
|
| 1251 |
-
dev = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1252 |
|
| 1253 |
[[package]]
|
| 1254 |
name = "pillow"
|
|
@@ -1456,6 +1623,15 @@ wheels = [
|
|
| 1456 |
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
| 1457 |
]
|
| 1458 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1459 |
[[package]]
|
| 1460 |
name = "pydantic"
|
| 1461 |
version = "2.13.4"
|
|
@@ -1564,6 +1740,35 @@ wheels = [
|
|
| 1564 |
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
| 1565 |
]
|
| 1566 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1567 |
[[package]]
|
| 1568 |
name = "python-dateutil"
|
| 1569 |
version = "2.9.0.post0"
|
|
@@ -1640,6 +1845,20 @@ wheels = [
|
|
| 1640 |
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
| 1641 |
]
|
| 1642 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1643 |
[[package]]
|
| 1644 |
name = "regex"
|
| 1645 |
version = "2026.5.9"
|
|
@@ -1755,6 +1974,116 @@ wheels = [
|
|
| 1755 |
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
| 1756 |
]
|
| 1757 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1758 |
[[package]]
|
| 1759 |
name = "safehttpx"
|
| 1760 |
version = "0.1.7"
|
|
|
|
| 335 |
{ url = "https://download.pytorch.org/whl/certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18", upload-time = "2023-10-03T17:33:19Z" },
|
| 336 |
]
|
| 337 |
|
| 338 |
+
[[package]]
|
| 339 |
+
name = "cffi"
|
| 340 |
+
version = "2.0.0"
|
| 341 |
+
source = { registry = "https://pypi.org/simple" }
|
| 342 |
+
dependencies = [
|
| 343 |
+
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
| 344 |
+
]
|
| 345 |
+
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
| 346 |
+
wheels = [
|
| 347 |
+
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
| 348 |
+
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
| 349 |
+
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
| 350 |
+
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
| 351 |
+
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
| 352 |
+
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
| 353 |
+
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
| 354 |
+
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
| 355 |
+
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
| 356 |
+
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
| 357 |
+
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
| 358 |
+
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
| 359 |
+
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
| 360 |
+
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
| 361 |
+
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
| 362 |
+
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
| 363 |
+
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
| 364 |
+
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
| 365 |
+
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
| 366 |
+
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
| 367 |
+
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
| 368 |
+
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
| 369 |
+
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
| 370 |
+
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
| 371 |
+
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
| 372 |
+
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
| 373 |
+
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
| 374 |
+
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
| 375 |
+
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
| 376 |
+
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
| 377 |
+
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
| 378 |
+
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
| 379 |
+
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
| 380 |
+
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
| 381 |
+
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
| 382 |
+
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
| 383 |
+
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
| 384 |
+
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
| 385 |
+
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
| 386 |
+
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
| 387 |
+
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
| 388 |
+
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
| 389 |
+
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
| 390 |
+
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
| 391 |
+
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
| 392 |
+
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
| 393 |
+
]
|
| 394 |
+
|
| 395 |
[[package]]
|
| 396 |
name = "charset-normalizer"
|
| 397 |
version = "2.1.1"
|
|
|
|
| 420 |
{ url = "https://download.pytorch.org/whl/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", upload-time = "2023-10-05T23:50:34Z" },
|
| 421 |
]
|
| 422 |
|
| 423 |
+
[[package]]
|
| 424 |
+
name = "cryptography"
|
| 425 |
+
version = "49.0.0"
|
| 426 |
+
source = { registry = "https://pypi.org/simple" }
|
| 427 |
+
dependencies = [
|
| 428 |
+
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
| 429 |
+
]
|
| 430 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
|
| 431 |
+
wheels = [
|
| 432 |
+
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
|
| 433 |
+
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
|
| 434 |
+
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
|
| 435 |
+
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
|
| 436 |
+
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
|
| 437 |
+
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
|
| 438 |
+
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
|
| 439 |
+
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
|
| 440 |
+
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
|
| 441 |
+
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
|
| 442 |
+
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
|
| 443 |
+
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
|
| 444 |
+
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
|
| 445 |
+
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
|
| 446 |
+
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
|
| 447 |
+
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
|
| 448 |
+
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
|
| 449 |
+
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
|
| 450 |
+
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
|
| 451 |
+
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
|
| 452 |
+
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
|
| 453 |
+
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
|
| 454 |
+
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
|
| 455 |
+
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
|
| 456 |
+
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
|
| 457 |
+
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
|
| 458 |
+
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
|
| 459 |
+
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
|
| 460 |
+
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
|
| 461 |
+
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
|
| 462 |
+
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
|
| 463 |
+
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
|
| 464 |
+
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
|
| 465 |
+
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
|
| 466 |
+
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
|
| 467 |
+
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
|
| 468 |
+
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
|
| 469 |
+
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
|
| 470 |
+
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
|
| 471 |
+
]
|
| 472 |
+
|
| 473 |
[[package]]
|
| 474 |
name = "faiss-cpu"
|
| 475 |
version = "1.14.2"
|
|
|
|
| 850 |
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
|
| 851 |
]
|
| 852 |
|
| 853 |
+
[[package]]
|
| 854 |
+
name = "jsonschema"
|
| 855 |
+
version = "4.26.0"
|
| 856 |
+
source = { registry = "https://pypi.org/simple" }
|
| 857 |
+
dependencies = [
|
| 858 |
+
{ name = "attrs" },
|
| 859 |
+
{ name = "jsonschema-specifications" },
|
| 860 |
+
{ name = "referencing" },
|
| 861 |
+
{ name = "rpds-py" },
|
| 862 |
+
]
|
| 863 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
|
| 864 |
+
wheels = [
|
| 865 |
+
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
|
| 866 |
+
]
|
| 867 |
+
|
| 868 |
+
[[package]]
|
| 869 |
+
name = "jsonschema-specifications"
|
| 870 |
+
version = "2025.9.1"
|
| 871 |
+
source = { registry = "https://pypi.org/simple" }
|
| 872 |
+
dependencies = [
|
| 873 |
+
{ name = "referencing" },
|
| 874 |
+
]
|
| 875 |
+
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
|
| 876 |
+
wheels = [
|
| 877 |
+
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
| 878 |
+
]
|
| 879 |
+
|
| 880 |
[[package]]
|
| 881 |
name = "markdown-it-py"
|
| 882 |
version = "4.2.0"
|
|
|
|
| 1363 |
|
| 1364 |
[package.dev-dependencies]
|
| 1365 |
dev = [
|
| 1366 |
+
{ name = "jsonschema" },
|
| 1367 |
{ name = "modal" },
|
| 1368 |
+
{ name = "pdfplumber" },
|
| 1369 |
]
|
| 1370 |
|
| 1371 |
[package.metadata]
|
|
|
|
| 1384 |
]
|
| 1385 |
|
| 1386 |
[package.metadata.requires-dev]
|
| 1387 |
+
dev = [
|
| 1388 |
+
{ name = "jsonschema" },
|
| 1389 |
+
{ name = "modal", specifier = ">=0.70" },
|
| 1390 |
+
{ name = "pdfplumber" },
|
| 1391 |
+
]
|
| 1392 |
+
|
| 1393 |
+
[[package]]
|
| 1394 |
+
name = "pdfminer-six"
|
| 1395 |
+
version = "20260107"
|
| 1396 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1397 |
+
dependencies = [
|
| 1398 |
+
{ name = "charset-normalizer" },
|
| 1399 |
+
{ name = "cryptography" },
|
| 1400 |
+
]
|
| 1401 |
+
sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" }
|
| 1402 |
+
wheels = [
|
| 1403 |
+
{ url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" },
|
| 1404 |
+
]
|
| 1405 |
+
|
| 1406 |
+
[[package]]
|
| 1407 |
+
name = "pdfplumber"
|
| 1408 |
+
version = "0.11.10"
|
| 1409 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1410 |
+
dependencies = [
|
| 1411 |
+
{ name = "pdfminer-six" },
|
| 1412 |
+
{ name = "pillow" },
|
| 1413 |
+
{ name = "pypdfium2" },
|
| 1414 |
+
]
|
| 1415 |
+
sdist = { url = "https://files.pythonhosted.org/packages/05/56/6f450312ba05a27d7713b73857c1a25100dbda04fbc1331b13fb227a607d/pdfplumber-0.11.10.tar.gz", hash = "sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57", size = 102892, upload-time = "2026-06-15T03:31:31.035Z" }
|
| 1416 |
+
wheels = [
|
| 1417 |
+
{ url = "https://files.pythonhosted.org/packages/a2/9a/07d658e1e7fad860f1c541ab941348125dbdab773be3a0afaf32361866c7/pdfplumber-0.11.10-py3-none-any.whl", hash = "sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580", size = 60047, upload-time = "2026-06-15T03:31:29.702Z" },
|
| 1418 |
+
]
|
| 1419 |
|
| 1420 |
[[package]]
|
| 1421 |
name = "pillow"
|
|
|
|
| 1623 |
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
| 1624 |
]
|
| 1625 |
|
| 1626 |
+
[[package]]
|
| 1627 |
+
name = "pycparser"
|
| 1628 |
+
version = "3.0"
|
| 1629 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1630 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
| 1631 |
+
wheels = [
|
| 1632 |
+
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
| 1633 |
+
]
|
| 1634 |
+
|
| 1635 |
[[package]]
|
| 1636 |
name = "pydantic"
|
| 1637 |
version = "2.13.4"
|
|
|
|
| 1740 |
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
| 1741 |
]
|
| 1742 |
|
| 1743 |
+
[[package]]
|
| 1744 |
+
name = "pypdfium2"
|
| 1745 |
+
version = "5.10.1"
|
| 1746 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1747 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1d/78/d9b45abb97a3686643f7c6472a5f7688f2013a373226121dc76b9debbacf/pypdfium2-5.10.1.tar.gz", hash = "sha256:f257d2011eb43c846b7e9f5a802e28646b29732763e4a35dd6ca76f9be580538", size = 272963, upload-time = "2026-06-15T10:09:16.179Z" }
|
| 1748 |
+
wheels = [
|
| 1749 |
+
{ url = "https://files.pythonhosted.org/packages/98/49/0b85fcc0d236582143a25cf275b0b4f5d786f51eb07a89ec9c79d43efe18/pypdfium2-5.10.1-py3-none-android_23_arm64_v8a.whl", hash = "sha256:13abf7a9f5e0ddebc8bbcccea5f13ae5abe8a298ea219e125b0fc24c1d2171b4", size = 3409176, upload-time = "2026-06-15T10:08:36.017Z" },
|
| 1750 |
+
{ url = "https://files.pythonhosted.org/packages/20/d2/2f522c5b2ad5166edf256bb4dbab97de5d07b2573f8a5630ddfcd1f8d4ee/pypdfium2-5.10.1-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:a0dc52b56631e2f7edcdb22bae3b155aa840bec32c5bd05781e90baaecee88e7", size = 2866175, upload-time = "2026-06-15T10:08:37.955Z" },
|
| 1751 |
+
{ url = "https://files.pythonhosted.org/packages/a7/be/477548c026c2badfdbf4afc3358b7135121fb5bec2e2effcb67e3a674d0b/pypdfium2-5.10.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:ebb9e63f92d15fc41b359fe7a187233dfae37548800e1fa09cb2fc466ac89951", size = 3621427, upload-time = "2026-06-15T10:08:39.969Z" },
|
| 1752 |
+
{ url = "https://files.pythonhosted.org/packages/8c/9b/0131c7f711b62c6edd6b200e9eb6340be6de4f6dc5baae625e3394d8d5fb/pypdfium2-5.10.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:d04f2050b6b32bb18624688b600543342a4ab3aacf64bf66521a5af72bbc7de1", size = 3682825, upload-time = "2026-06-15T10:08:42.181Z" },
|
| 1753 |
+
{ url = "https://files.pythonhosted.org/packages/9d/3d/bddfceb6e67e54d6dd1ab6c0f1feff796a89596e40f6345a3b4cc6a3d408/pypdfium2-5.10.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de53d2710ca9509fc2812340acc57c3f043697609068592d87de654f8cabf44", size = 3682206, upload-time = "2026-06-15T10:08:44.087Z" },
|
| 1754 |
+
{ url = "https://files.pythonhosted.org/packages/58/08/dedeb25c6645fc8a5eda24f54e0b1d083b7334ababf5f518bb939d729cc0/pypdfium2-5.10.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10a26ce04795f8ec079e81c707fcb5737061e8a78025babc3d6e36642e9c903a", size = 3413720, upload-time = "2026-06-15T10:08:45.739Z" },
|
| 1755 |
+
{ url = "https://files.pythonhosted.org/packages/cc/48/69e1fc8b1216005243c6415183bbf6de1cda3f5a06758b3fa4a26a7385c6/pypdfium2-5.10.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be6d2a8d1bcfd777188e7aa55a25c83f34d54e8350ad8810fb32017787d9b0d9", size = 3812913, upload-time = "2026-06-15T10:08:47.45Z" },
|
| 1756 |
+
{ url = "https://files.pythonhosted.org/packages/72/7f/132455a58ad736d76815c6cd1307532c3f433299d945b9d2f8cc2387c309/pypdfium2-5.10.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf4d2527f79f31c550490cc74c9f32e19385a944630a3ef4cd4d9b6f961fbf77", size = 4223220, upload-time = "2026-06-15T10:08:49.099Z" },
|
| 1757 |
+
{ url = "https://files.pythonhosted.org/packages/bf/8c/4d5804eca598bbe894e0a9a510807e221c1623e7276ce6b68fa2660dc933/pypdfium2-5.10.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ba127750bc3f4161461538d532d74491cd976f584f1753a6cee9cb821338ec1", size = 3738950, upload-time = "2026-06-15T10:08:50.821Z" },
|
| 1758 |
+
{ url = "https://files.pythonhosted.org/packages/d1/7f/baac59bf14ff914d97789ee0368c22ae233aa857aa9c0726bcb515dbc4d7/pypdfium2-5.10.1-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80f30517ee089dfbbc6e9de6da365b6e8c0ce8f80c40b7d4025a3b1d3bbd8a70", size = 4029869, upload-time = "2026-06-15T10:08:52.494Z" },
|
| 1759 |
+
{ url = "https://files.pythonhosted.org/packages/d0/d7/a5d58a0bcba31a0e37ed636a76ef3d2d215733f28af61637287d061b0c54/pypdfium2-5.10.1-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ad5f5de15febb788c6eb3853e58aceda9cd8c5187c92472abaeecf9558deb0cf", size = 3990927, upload-time = "2026-06-15T10:08:54.555Z" },
|
| 1760 |
+
{ url = "https://files.pythonhosted.org/packages/da/1a/98eebd14b36812176297cf765d504ebeebc982f895c8a3a9fbd2717797de/pypdfium2-5.10.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8947fa3cd808da33960bdf8ff9e5247aa94ccd94f0b09bb3402e99498d83bcc9", size = 4989624, upload-time = "2026-06-15T10:08:56.284Z" },
|
| 1761 |
+
{ url = "https://files.pythonhosted.org/packages/1c/20/f2e124d607b8bb90a9f1ce976afff38c70e215cd7ea86af784cb2e8a19dd/pypdfium2-5.10.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:139a6387a3a2652f288e53164268bb03af4fa221d78484ee18407053a60082a3", size = 4535124, upload-time = "2026-06-15T10:08:58.214Z" },
|
| 1762 |
+
{ url = "https://files.pythonhosted.org/packages/59/ef/469ea87f668a32ff3280ea15e522e3a7858d2c80f1ecc320ece1244624c9/pypdfium2-5.10.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:172ff3e10358d66456e27fb0b8b5098e28ec24e51072eb4b5b86077d550e21bb", size = 5229373, upload-time = "2026-06-15T10:09:00.702Z" },
|
| 1763 |
+
{ url = "https://files.pythonhosted.org/packages/c3/48/c7ed3001f0c5e28114c98bf918c8121e422a65ba7323e1e12f3f28e2d278/pypdfium2-5.10.1-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:09fda0609dc4749c9865a0315c447d6f2583a580693bcabd69980d3fbb22ad51", size = 5140010, upload-time = "2026-06-15T10:09:02.734Z" },
|
| 1764 |
+
{ url = "https://files.pythonhosted.org/packages/b2/bc/00b731bfc1fdc0f3c7d91108fceb54978ff4f5a92336820e5ed6c12535ff/pypdfium2-5.10.1-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:6b01adcfe9aaf7a635a59bed5687fd4fd7b0da292664f050d4ebd2bfa5c70584", size = 4643310, upload-time = "2026-06-15T10:09:04.794Z" },
|
| 1765 |
+
{ url = "https://files.pythonhosted.org/packages/9a/e8/3ad242233f657c19092a8c83684c8154b8d02a826535a6a2591d91aa5dde/pypdfium2-5.10.1-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:610e14c37d2090b826dccc0604fc7e7612c0ce591190780ae228abdb9abb971e", size = 5087879, upload-time = "2026-06-15T10:09:06.861Z" },
|
| 1766 |
+
{ url = "https://files.pythonhosted.org/packages/80/3f/9d0747a02ac3021ed3db7ac27c5187d97e78b0253a4bdfbf386666968474/pypdfium2-5.10.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f2803020952afa57e1e148adc19369b835154e1fa251a1ca85008ab6466a710f", size = 5047369, upload-time = "2026-06-15T10:09:08.883Z" },
|
| 1767 |
+
{ url = "https://files.pythonhosted.org/packages/9e/41/7d5187e9527eae81890a3b13193442112ef788d61ddaa68a6d59ac447bec/pypdfium2-5.10.1-py3-none-win32.whl", hash = "sha256:8702bb4f01ddfc8e7757b41b4c2c8392ac17c9f0234476e1e69672ea7c6d6aa0", size = 3680056, upload-time = "2026-06-15T10:09:10.95Z" },
|
| 1768 |
+
{ url = "https://files.pythonhosted.org/packages/16/1d/c62bd59dd8345cc4b640f942f465633f6b07b859d01ddb648610a7bf5c7c/pypdfium2-5.10.1-py3-none-win_amd64.whl", hash = "sha256:58da5b51fb7884c7d21a05062ab13edb011d1a08dfd9694f3d5d685df62796b9", size = 3812105, upload-time = "2026-06-15T10:09:12.684Z" },
|
| 1769 |
+
{ url = "https://files.pythonhosted.org/packages/10/d5/21bac39125df8a93e99c04583486b58a62b5997d6b3541e3ad0f69053392/pypdfium2-5.10.1-py3-none-win_arm64.whl", hash = "sha256:e3301c2f7a66fb8cb57dba857d0c9e90215e178f6602a87c5a306cd98513dab8", size = 3600043, upload-time = "2026-06-15T10:09:14.606Z" },
|
| 1770 |
+
]
|
| 1771 |
+
|
| 1772 |
[[package]]
|
| 1773 |
name = "python-dateutil"
|
| 1774 |
version = "2.9.0.post0"
|
|
|
|
| 1845 |
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
| 1846 |
]
|
| 1847 |
|
| 1848 |
+
[[package]]
|
| 1849 |
+
name = "referencing"
|
| 1850 |
+
version = "0.37.0"
|
| 1851 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1852 |
+
dependencies = [
|
| 1853 |
+
{ name = "attrs" },
|
| 1854 |
+
{ name = "rpds-py" },
|
| 1855 |
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
| 1856 |
+
]
|
| 1857 |
+
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
|
| 1858 |
+
wheels = [
|
| 1859 |
+
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
| 1860 |
+
]
|
| 1861 |
+
|
| 1862 |
[[package]]
|
| 1863 |
name = "regex"
|
| 1864 |
version = "2026.5.9"
|
|
|
|
| 1974 |
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
| 1975 |
]
|
| 1976 |
|
| 1977 |
+
[[package]]
|
| 1978 |
+
name = "rpds-py"
|
| 1979 |
+
version = "2026.5.1"
|
| 1980 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1981 |
+
sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" }
|
| 1982 |
+
wheels = [
|
| 1983 |
+
{ url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" },
|
| 1984 |
+
{ url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" },
|
| 1985 |
+
{ url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" },
|
| 1986 |
+
{ url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" },
|
| 1987 |
+
{ url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" },
|
| 1988 |
+
{ url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" },
|
| 1989 |
+
{ url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" },
|
| 1990 |
+
{ url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" },
|
| 1991 |
+
{ url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" },
|
| 1992 |
+
{ url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" },
|
| 1993 |
+
{ url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" },
|
| 1994 |
+
{ url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" },
|
| 1995 |
+
{ url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" },
|
| 1996 |
+
{ url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" },
|
| 1997 |
+
{ url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" },
|
| 1998 |
+
{ url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" },
|
| 1999 |
+
{ url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" },
|
| 2000 |
+
{ url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" },
|
| 2001 |
+
{ url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" },
|
| 2002 |
+
{ url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" },
|
| 2003 |
+
{ url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" },
|
| 2004 |
+
{ url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" },
|
| 2005 |
+
{ url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" },
|
| 2006 |
+
{ url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" },
|
| 2007 |
+
{ url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" },
|
| 2008 |
+
{ url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" },
|
| 2009 |
+
{ url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" },
|
| 2010 |
+
{ url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" },
|
| 2011 |
+
{ url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" },
|
| 2012 |
+
{ url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" },
|
| 2013 |
+
{ url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" },
|
| 2014 |
+
{ url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" },
|
| 2015 |
+
{ url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" },
|
| 2016 |
+
{ url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" },
|
| 2017 |
+
{ url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" },
|
| 2018 |
+
{ url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" },
|
| 2019 |
+
{ url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" },
|
| 2020 |
+
{ url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" },
|
| 2021 |
+
{ url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" },
|
| 2022 |
+
{ url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" },
|
| 2023 |
+
{ url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" },
|
| 2024 |
+
{ url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" },
|
| 2025 |
+
{ url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" },
|
| 2026 |
+
{ url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" },
|
| 2027 |
+
{ url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" },
|
| 2028 |
+
{ url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" },
|
| 2029 |
+
{ url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" },
|
| 2030 |
+
{ url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" },
|
| 2031 |
+
{ url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" },
|
| 2032 |
+
{ url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" },
|
| 2033 |
+
{ url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" },
|
| 2034 |
+
{ url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" },
|
| 2035 |
+
{ url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" },
|
| 2036 |
+
{ url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" },
|
| 2037 |
+
{ url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" },
|
| 2038 |
+
{ url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" },
|
| 2039 |
+
{ url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" },
|
| 2040 |
+
{ url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" },
|
| 2041 |
+
{ url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" },
|
| 2042 |
+
{ url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" },
|
| 2043 |
+
{ url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" },
|
| 2044 |
+
{ url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" },
|
| 2045 |
+
{ url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" },
|
| 2046 |
+
{ url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" },
|
| 2047 |
+
{ url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" },
|
| 2048 |
+
{ url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" },
|
| 2049 |
+
{ url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" },
|
| 2050 |
+
{ url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" },
|
| 2051 |
+
{ url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" },
|
| 2052 |
+
{ url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" },
|
| 2053 |
+
{ url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" },
|
| 2054 |
+
{ url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" },
|
| 2055 |
+
{ url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" },
|
| 2056 |
+
{ url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" },
|
| 2057 |
+
{ url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" },
|
| 2058 |
+
{ url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" },
|
| 2059 |
+
{ url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" },
|
| 2060 |
+
{ url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" },
|
| 2061 |
+
{ url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" },
|
| 2062 |
+
{ url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" },
|
| 2063 |
+
{ url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" },
|
| 2064 |
+
{ url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" },
|
| 2065 |
+
{ url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" },
|
| 2066 |
+
{ url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" },
|
| 2067 |
+
{ url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" },
|
| 2068 |
+
{ url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" },
|
| 2069 |
+
{ url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" },
|
| 2070 |
+
{ url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" },
|
| 2071 |
+
{ url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" },
|
| 2072 |
+
{ url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" },
|
| 2073 |
+
{ url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" },
|
| 2074 |
+
{ url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" },
|
| 2075 |
+
{ url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" },
|
| 2076 |
+
{ url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" },
|
| 2077 |
+
{ url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" },
|
| 2078 |
+
{ url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" },
|
| 2079 |
+
{ url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" },
|
| 2080 |
+
{ url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" },
|
| 2081 |
+
{ url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" },
|
| 2082 |
+
{ url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" },
|
| 2083 |
+
{ url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" },
|
| 2084 |
+
{ url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" },
|
| 2085 |
+
]
|
| 2086 |
+
|
| 2087 |
[[package]]
|
| 2088 |
name = "safehttpx"
|
| 2089 |
version = "0.1.7"
|