agentdata 0.3.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. agentdata/__init__.py +33 -0
  2. agentdata/__main__.py +4 -0
  3. agentdata/cache.py +115 -0
  4. agentdata/cli.py +173 -0
  5. agentdata/client.py +158 -0
  6. agentdata/company/__init__.py +3 -0
  7. agentdata/company/cf.py +145 -0
  8. agentdata/company/ch_zefix.py +100 -0
  9. agentdata/company/cli.py +243 -0
  10. agentdata/company/co_rues.py +72 -0
  11. agentdata/company/edgar_cik.py +84 -0
  12. agentdata/company/edgar_fts.py +101 -0
  13. agentdata/company/faces.py +746 -0
  14. agentdata/company/fdic_bankfind.py +107 -0
  15. agentdata/company/fi_ytj.py +89 -0
  16. agentdata/company/fr_recherche.py +124 -0
  17. agentdata/company/fts.py +121 -0
  18. agentdata/company/gleif.py +178 -0
  19. agentdata/company/kr_hometax.py +112 -0
  20. agentdata/company/no_brreg.py +124 -0
  21. agentdata/company/ror.py +105 -0
  22. agentdata/company/ted.py +117 -0
  23. agentdata/company/usaspending.py +124 -0
  24. agentdata/company/vies.py +66 -0
  25. agentdata/daterange.py +35 -0
  26. agentdata/economy/__init__.py +10 -0
  27. agentdata/economy/bis.py +132 -0
  28. agentdata/economy/cli.py +199 -0
  29. agentdata/economy/ecb.py +117 -0
  30. agentdata/economy/eurostat.py +163 -0
  31. agentdata/economy/faces.py +573 -0
  32. agentdata/economy/fred.py +92 -0
  33. agentdata/economy/ilo_stat.py +99 -0
  34. agentdata/economy/imf_sdmx.py +133 -0
  35. agentdata/economy/un_comtrade.py +199 -0
  36. agentdata/economy/unsd_sdg.py +154 -0
  37. agentdata/economy/wb.py +208 -0
  38. agentdata/economy/who_xmart.py +177 -0
  39. agentdata/entries.py +30 -0
  40. agentdata/errors.py +38 -0
  41. agentdata/esg/__init__.py +5 -0
  42. agentdata/esg/cli.py +180 -0
  43. agentdata/esg/climatetrace.py +260 -0
  44. agentdata/esg/eiti.py +187 -0
  45. agentdata/esg/epa_frs.py +138 -0
  46. agentdata/esg/euasl.py +75 -0
  47. agentdata/esg/eutl.py +66 -0
  48. agentdata/esg/faces.py +498 -0
  49. agentdata/esg/paris_mou.py +240 -0
  50. agentdata/esg/thetis_mrv.py +67 -0
  51. agentdata/healthcare/__init__.py +7 -0
  52. agentdata/healthcare/au_tga.py +142 -0
  53. agentdata/healthcare/ca_dpd.py +123 -0
  54. agentdata/healthcare/ca_mdall.py +155 -0
  55. agentdata/healthcare/cli.py +375 -0
  56. agentdata/healthcare/ctgov.py +101 -0
  57. agentdata/healthcare/dailymed.py +102 -0
  58. agentdata/healthcare/epmc.py +108 -0
  59. agentdata/healthcare/eudamed.py +124 -0
  60. agentdata/healthcare/faces.py +811 -0
  61. agentdata/healthcare/fda_crl.py +86 -0
  62. agentdata/healthcare/fda_devices.py +199 -0
  63. agentdata/healthcare/fda_drugs.py +156 -0
  64. agentdata/healthcare/fda_recalls.py +137 -0
  65. agentdata/healthcare/gb_mhra.py +137 -0
  66. agentdata/healthcare/gudid.py +84 -0
  67. agentdata/healthcare/mesh.py +89 -0
  68. agentdata/healthcare/rasff.py +164 -0
  69. agentdata/healthcare/rxnav.py +67 -0
  70. agentdata/healthcare/safety_gate.py +175 -0
  71. agentdata/healthcare/shortages.py +107 -0
  72. agentdata/legal/__init__.py +4 -0
  73. agentdata/legal/afdb.py +93 -0
  74. agentdata/legal/ca_sema.py +143 -0
  75. agentdata/legal/cli.py +312 -0
  76. agentdata/legal/co_sic.py +70 -0
  77. agentdata/legal/courtlistener.py +77 -0
  78. agentdata/legal/csl.py +139 -0
  79. agentdata/legal/dgcomp.py +125 -0
  80. agentdata/legal/ecfr.py +129 -0
  81. agentdata/legal/faces.py +700 -0
  82. agentdata/legal/federalregister.py +188 -0
  83. agentdata/legal/gb_cma.py +117 -0
  84. agentdata/legal/gb_tna_caselaw.py +245 -0
  85. agentdata/legal/idb.py +85 -0
  86. agentdata/legal/lda.py +147 -0
  87. agentdata/legal/sipri.py +178 -0
  88. agentdata/legal/ukec.py +148 -0
  89. agentdata/legal/wb.py +92 -0
  90. agentdata/procurement/__init__.py +5 -0
  91. agentdata/procurement/br_pncp.py +126 -0
  92. agentdata/procurement/cli.py +155 -0
  93. agentdata/procurement/co_secop.py +88 -0
  94. agentdata/procurement/faces.py +239 -0
  95. agentdata/procurement/ny_open_data.py +82 -0
  96. agentdata/procurement/ungm.py +137 -0
  97. agentdata/procurement/wb_proc.py +84 -0
  98. agentdata/research/__init__.py +4 -0
  99. agentdata/research/biorxiv.py +66 -0
  100. agentdata/research/cli.py +129 -0
  101. agentdata/research/crossref.py +121 -0
  102. agentdata/research/faces.py +349 -0
  103. agentdata/research/gb_gtr.py +58 -0
  104. agentdata/research/icite.py +52 -0
  105. agentdata/research/nih_reporter.py +143 -0
  106. agentdata/research/nsf.py +68 -0
  107. agentdata/research/openalex.py +145 -0
  108. agentdata/research/opencitations.py +50 -0
  109. agentdata/security/__init__.py +3 -0
  110. agentdata/security/cisa_kev.py +42 -0
  111. agentdata/security/cli.py +37 -0
  112. agentdata/security/faces.py +91 -0
  113. agentdata/security/osv.py +72 -0
  114. agentdata/transport.py +541 -0
  115. agentdata-0.3.26.dist-info/METADATA +266 -0
  116. agentdata-0.3.26.dist-info/RECORD +120 -0
  117. agentdata-0.3.26.dist-info/WHEEL +5 -0
  118. agentdata-0.3.26.dist-info/entry_points.txt +10 -0
  119. agentdata-0.3.26.dist-info/licenses/LICENSE +201 -0
  120. agentdata-0.3.26.dist-info/top_level.txt +1 -0
agentdata/esg/cli.py ADDED
@@ -0,0 +1,180 @@
1
+ """esg cli — domain command face registration (split per domain in 0.2.6, moved in from the root cli.py; purely flat since the 0.3.0 verb-layer removal).
2
+
3
+ register(v, g): registers flat commands on the domain subparser (g = the root _leaf_globals factory,
4
+ passed in as a parameter to avoid a cli ↔ domain-cli cycle); FLAT = the flat dispatch table, merged by the root cli for main to use.
5
+ """
6
+ import argparse
7
+
8
+ def register(v, g):
9
+ f1 = v.add_parser("climatetrace_owners", parents=[g()],
10
+ help="Emissions-owner candidates (Climate TRACE v7 owners)",
11
+ description="Climate TRACE emissions-owner candidates: name/limit/offset upstream-verbatim; zero hit = legal empty (upstream 404 JSON mapped); ordering is NOT stable across limits — fetch enough in one go")
12
+ f1.add_argument("--name", required=True, help="Owner name to search (upstream param: name)")
13
+ f1.add_argument("--limit", type=int, default=10, help="Rows (upstream param: limit; politeness cap 50 — upstream tested to 1000)")
14
+ f1.add_argument("--offset", type=int, default=None, help="Skip rows (upstream param: offset; ordering unstable — prefer one large fetch)")
15
+
16
+ f2 = v.add_parser("climatetrace_sources", parents=[g()],
17
+ help="Facility-level GHG sources by owner/filter (Climate TRACE v7)",
18
+ description="Climate TRACE owner/filter→facility/aggregate emission sources: owner_ids→ownerIds (comma-joined = union & dedup, tested; repeated param = upstream silently first-wins, do not use) XOR --owner-name sugar (resolves first candidate); year no local wall (out-of-window year → upstream null, 2021+ has data); gas verbatim — non-co2e gas switches row key to emissions_quantity (native unit); 0.2.9 audit additions: offset paging, sectors closed-set validated locally (invalid values silently return the WHOLE corpus upstream), subsectors passthrough (upstream 400 self-describes), location family gadm_id/city_id/country_group/continent passthrough (upstream 400 enforces one-at-a-time); owner_ids or at least one filter required (bare corpus scan walled politely)")
19
+ f2.add_argument("--owner-ids", dest="owner_ids", default=None,
20
+ help="Owner id(s), upstream param ownerIds — E100000001492 or comma-joined for union")
21
+ f2.add_argument("--owner-name", dest="owner_name", default=None,
22
+ help="Sugar — resolves first owners candidate (XOR with --owner-ids)")
23
+ f2.add_argument("--year", type=int, default=None, help="Emissions year (upstream param: year; default=current year; data window 2021+)")
24
+ f2.add_argument("--gas", default=None, help="Gas (upstream param: gas; default co2e_100yr = tCO2e)")
25
+ f2.add_argument("--limit", type=int, default=50, help="Rows (upstream param: limit; politeness cap 500)")
26
+ f2.add_argument("--offset", type=int, default=None,
27
+ help="Skip rows (upstream param: offset; real paging tested 2026-10-28 — ordering unstable across limits, prefer one big fetch)")
28
+ f2.add_argument("--sectors", default=None,
29
+ help="Sector slug, closed set validated locally: agriculture/buildings/fluorinated-gases/forestry-and-land-use/fossil-fuel-operations/manufacturing/mineral-extraction/power/transportation/waste + special all_no_forest (invalid values silently return the whole corpus upstream)")
30
+ f2.add_argument("--subsectors", default=None,
31
+ help="Subsector slug(s) passthrough (68 values, GET /v7/definitions/subsectors; invalid value → upstream 400 'invalid subsector provided', self-describing)")
32
+ f2.add_argument("--gadm-id", dest="gadm_id", default=None, help="GADM region id (upstream param: gadmId)")
33
+ f2.add_argument("--city-id", dest="city_id", default=None, help="Metro area id (upstream param: cityId)")
34
+ f2.add_argument("--country-group", dest="country_group", default=None,
35
+ help="Country-group slug, upstream param countryGroup — e.g. eu/aosis/g20 (values from GET /v7/definitions/countrygroups)")
36
+ f2.add_argument("--continent", default=None,
37
+ help="Continent name (upstream param: continent; tested Europe/Asia 2026-10-28 — RUS counts as Europe)")
38
+
39
+ f3 = v.add_parser("eiti_companies", parents=[g()],
40
+ help="Extractives filing companies (EITI Datasette; name candidates)",
41
+ description="EITI extractives filing-company candidates: company_name__like is the upstream Datasette magic param verbatim (%/_ metachars yours to manage); --name sugar wraps %contains% (same slot, XOR); zero hit = legal empty")
42
+ f3.add_argument("--company-name__like", dest="company_name__like", default=None,
43
+ help="Upstream param verbatim (SQL LIKE; %%/_ metachars passthrough)")
44
+ f3.add_argument("--name", default=None, help="Company name — sugar for company_name__like=%%name%% (same slot, XOR with --company-name__like)")
45
+ f3.add_argument("--size", type=int, default=10, help="Rows (upstream param: _size; politeness cap 1000 — upstream tested to 11000+)")
46
+
47
+ f4 = v.add_parser("eiti_payments", parents=[g()],
48
+ help="Company→government payment lines (EITI; GFS-classified)",
49
+ description="EITI company→government payment lines: id channel (eiti_id_company__exact verbatim XOR --eiti-id sugar adding the eiti_id_company: prefix) + name channel (company_name__like verbatim XOR --name sugar); channels may combine; at least one required (bare scan walled locally)")
50
+ f4.add_argument("--eiti-id-company__exact", dest="eiti_id_company__exact", default=None,
51
+ help="Upstream param verbatim — full value incl. eiti_id_company: prefix")
52
+ f4.add_argument("--eiti-id", dest="eiti_id", default=None,
53
+ help="Sugar — bare UUID, prefix auto-added (same slot, XOR with --eiti-id-company__exact)")
54
+ f4.add_argument("--company-name__like", dest="company_name__like", default=None,
55
+ help="Upstream param verbatim (SQL LIKE)")
56
+ f4.add_argument("--name", default=None, help="Sugar — %%contains%% (same slot, XOR with --company-name__like)")
57
+ f4.add_argument("--size", type=int, default=50, help="Rows (upstream param: _size; politeness cap 1000)")
58
+
59
+ f5 = v.add_parser("epa_frs_facilities", parents=[g()],
60
+ help="EPA facility master lookup (FRS frs_facility_site)",
61
+ description="EPA FRS facility registry point query: seven filter columns upstream-verbatim (registry_id / primary_name / state_code / city_name + the 0.3.20 audit trio country_name / site_type_name / epa_region_code — ALL exact '=' match, combinable = AND; the upstream contains/~ operators silently return 0, fuzzy facility names go through ECHO; country_name unlocks the non-US facility slice, GERMANY=110 measured) + start/size paging (ROWS closed interval start:end, computed locally — an omitted/inverted ROWS segment makes the upstream dump the whole table); at least one filter required (bare 5.6M-row scan walled politely, batch pipeline serves that); each call = rows + count (total_matches)")
62
+ f5.add_argument("--registry-id", dest="registry_id", default=None,
63
+ help="FRS Registry ID — authoritative facility key (ECHO/TRI/GHGRP join anchor)")
64
+ f5.add_argument("--primary-name", dest="primary_name", default=None,
65
+ help="Facility primary name — EXACT match incl. spaces (contains/~ operators return 0 upstream)")
66
+ f5.add_argument("--state-code", dest="state_code", default=None,
67
+ help="State code, 2-letter (lowercase input uppercased locally, e.g. DE)")
68
+ f5.add_argument("--city-name", dest="city_name", default=None, help="City name (exact)")
69
+ f5.add_argument("--country-name", dest="country_name", default=None,
70
+ help="Country name (exact uppercase form, lowercase input uppercased locally — GERMANY=110 / "
71
+ "UNITED STATES=3,129,491 measured; the non-US facility slice)")
72
+ f5.add_argument("--site-type-name", dest="site_type_name", default=None,
73
+ help="Site type (exact uppercase, e.g. STATIONARY — 4,725,914 of 5,602,878 measured)")
74
+ f5.add_argument("--epa-region-code", dest="epa_region_code", default=None,
75
+ help="EPA region (zero-padded two-digit string form '03'; region 10 = 214,197 measured)")
76
+ f5.add_argument("--start", type=int, default=0, help="Paging offset (upstream ROWS start, closed interval)")
77
+ f5.add_argument("--size", type=int, default=50, help="Rows (politeness cap 500)")
78
+
79
+ f6 = v.add_parser("euasl_datasets", parents=[g()],
80
+ help="EU Air Safety List download catalog (banned/restricted air carriers)",
81
+ description="EU Air Safety List official download catalog (keyless): one call returns all files — the machine-readable XLSX master (Annex A = carriers under an operating ban, Annex B = restrictions/conditions) + 24 official-language PDF renditions; the document UUIDs rotate on every republication (reconcile against this listing, never guess file names); zero params upstream; the binary bodies go through the batch pipeline, this face gives coordinates")
82
+ # Upstream takes zero params: no flags (the catalog ships in one shot; clients pick the xlsx master row)
83
+
84
+ f7 = v.add_parser("eutl_datasets", parents=[g()],
85
+ help="EU carbon registry bulk-download catalog (EUTL)",
86
+ description="EUTL official bulk-download catalog (keyless): one call returns all datasets — Azure daily extracts (accounts/operators/holdings csv.gz) + climate.ec.europa.eu yearly files (compliance/verified emissions 2021-2025); zero params upstream; file downloads themselves go through the batch pipeline, this face gives coordinates")
87
+ # Upstream takes zero params: no flags (the catalog ships in one shot; clients filter rows themselves)
88
+
89
+ # esg 0.3.5 port-state emissions/supervision axis (thetis_mrv + paris_mou; upstream param names mirrored) — folded in and seated lexicographically in 0.3.10
90
+ f8 = v.add_parser("paris_mou_bans", parents=[g()],
91
+ help="Paris MoU banned ships register",
92
+ description="Paris MoU banned-ships register (keyless): ban date/occurrence/order status/reason/minimum duration/banning authority, with the embedded ISM company flattened to row keys — the official ship x management-company edge (~2/3 of rows carry a company IMO number); zero params upstream")
93
+ # Upstream takes zero params: no flags (the bans list ships in one shot)
94
+
95
+ f9 = v.add_parser("paris_mou_detentions", parents=[g()],
96
+ help="Paris MoU current detentions snapshot",
97
+ description="Paris MoU (Atlantic/European port state control) currently-detained ships snapshot (keyless): daily-rolling live state — ship IMO number (asset key), detention port+country, reporting authority; historical detentions go through paris_mou_inspections window search; zero params upstream")
98
+ # Upstream takes zero params: no flags (the same-day snapshot ships in one shot)
99
+
100
+ f10 = v.add_parser("paris_mou_inspections", parents=[g()],
101
+ help="Paris MoU historical PSC inspection window search",
102
+ description="Paris MoU historical port state control inspections (THETIS form-encoded POST, keyless): period_min/period_max are dd/mm/yyyy STRICT (upstream native order day/month/year — wrong order silently returns zero) and the window must be <= 3 years (enforced locally, upstream silently empties beyond); page is the paging switch — the face always sends it (without page upstream ignores limit and dumps the whole window); page+start+limit = start-offset paging (page 1/2 verified zero overlap); ism_company_imo_number / ism_company_name filter straight to the management company (resume window stats ships/inspections/detentions/ISM deficiencies ride along); psc_inspection_regime upstream-verbatim (1 = PSC default regime, only verified value; omitted = upstream 200 empty); zero rows in window = legal empty")
103
+ f10.add_argument("--period-min", dest="period_min", required=True,
104
+ help="Window start dd/mm/yyyy (upstream param periodMin; STRICT day-first)")
105
+ f10.add_argument("--period-max", dest="period_max", required=True,
106
+ help="Window end dd/mm/yyyy (upstream param periodMax; window <= 3 years)")
107
+ f10.add_argument("--psc-inspection-regime", dest="psc_inspection_regime", type=int, default=1,
108
+ help="Inspection regime (upstream param pscInspectionRegime; 1 = PSC default, only verified value)")
109
+ f10.add_argument("--page", type=int, default=1,
110
+ help="Page number (upstream param page; the paging switch — always sent by this face)")
111
+ f10.add_argument("--start", type=int, default=0,
112
+ help="Row offset within page (upstream param start; page+start+limit paging)")
113
+ f10.add_argument("--limit", type=int, default=50, help="Rows (politeness cap 500)")
114
+ f10.add_argument("--ism-company-name", dest="ism_company_name", default=None,
115
+ help="ISM management company name filter (upstream param ismCompanyName)")
116
+ f10.add_argument("--ism-company-imo-number", dest="ism_company_imo_number", default=None,
117
+ help="ISM company IMO number filter, 7-digit zero-padded (upstream param ismCompanyImoNumber, e.g. 0052193)")
118
+
119
+ f11 = v.add_parser("paris_mou_low_performance_companies", parents=[g()],
120
+ help="Paris MoU low-performance ISM companies",
121
+ description="Paris MoU low-performance ISM company list (rolling 6-month window, keyless): each row carries an IMO company number (7-digit zero-padded official string), with address/country/performance level (LOW dominant, some MEDIUM)/listing dates — company-risk attribute list; zero params upstream")
122
+ # Upstream takes zero params: no flags (the rolling-window list ships in one shot)
123
+
124
+ f12 = v.add_parser("thetis_mrv_datasets", parents=[g()],
125
+ help="EU shipping MRV emission-report file catalog (THETIS-MRV)",
126
+ description="THETIS-MRV versioned bulk-file catalog (keyless): reporting periods (2018+) x versioned regenerated XLSX publications (versions roll — reconcile by this list, never guess filenames; generation timestamps are early-morning batch runs); download_path template is served for the batch pipeline, this face gives coordinates; zero params upstream")
127
+ # Upstream takes zero params: no flags (reporting-period × version ledger composed; clients filter rows themselves)
128
+
129
+ FLAT = {
130
+ # esg 0.2.0 flat panel (upstream params under original names + our defaults; the lambda only ferries names across namespaces)
131
+ "climatetrace_owners": lambda a: {"name": a.name, "limit": a.limit, "offset": a.offset},
132
+ "climatetrace_sources": lambda a: {"owner_ids": a.owner_ids, "owner_name": a.owner_name,
133
+ "year": a.year, "gas": a.gas, "limit": a.limit,
134
+ "offset": a.offset, "sectors": a.sectors,
135
+ "subsectors": a.subsectors, "gadm_id": a.gadm_id,
136
+ "city_id": a.city_id, "country_group": a.country_group,
137
+ "continent": a.continent},
138
+ "eiti_companies": lambda a: {"company_name__like": a.company_name__like,
139
+ "name": a.name, "size": a.size},
140
+ "eiti_payments": lambda a: {"eiti_id_company__exact": a.eiti_id_company__exact,
141
+ "eiti_id": a.eiti_id, "company_name__like": a.company_name__like,
142
+ "name": a.name, "size": a.size},
143
+ "epa_frs_facilities": lambda a: {"registry_id": a.registry_id,
144
+ "primary_name": a.primary_name,
145
+ "state_code": a.state_code, "city_name": a.city_name,
146
+ "country_name": a.country_name, "site_type_name": a.site_type_name,
147
+ "epa_region_code": a.epa_region_code,
148
+ "start": a.start, "size": a.size},
149
+ "euasl_datasets": lambda a: {}, # upstream zero params (download catalog in one shot; 0.3.22)
150
+ "eutl_datasets": lambda a: {}, # upstream zero params (catalog in one shot)
151
+ # esg 0.3.5 port-state emissions/supervision axis (thetis_mrv + paris_mou; upstream param names mirrored)
152
+ "paris_mou_bans": lambda a: {}, # upstream zero params (bans list in one shot)
153
+ "paris_mou_detentions": lambda a: {}, # upstream zero params (same-day snapshot in one shot)
154
+ "paris_mou_inspections": lambda a: {"period_min": a.period_min,
155
+ "period_max": a.period_max,
156
+ "psc_inspection_regime": a.psc_inspection_regime,
157
+ "page": a.page, "start": a.start, "limit": a.limit,
158
+ "ism_company_name": a.ism_company_name,
159
+ "ism_company_imo_number": a.ism_company_imo_number},
160
+ "paris_mou_low_performance_companies": lambda a: {}, # upstream zero params (rolling-window list in one shot)
161
+ "thetis_mrv_datasets": lambda a: {}, # upstream zero params (reporting-period × version ledger composed)
162
+ }
163
+
164
+
165
+ # 0.3.10 jurisdiction chapter: face → jurisdiction (default = the issuing authority's jurisdiction; bodies
166
+ # covering global phenomena = GLOBAL; vocabulary = ISO-3166 alpha-2 + EU + GLOBAL) — merged into _JURISDICTION by the root cli.py; success envelopes carry the jurisdiction chapter
167
+ COUNTRY = {
168
+ "climatetrace_owners": "GLOBAL",
169
+ "climatetrace_sources": "GLOBAL",
170
+ "eiti_companies": "GLOBAL",
171
+ "eiti_payments": "GLOBAL",
172
+ "epa_frs_facilities": "US",
173
+ "euasl_datasets": "EU",
174
+ "eutl_datasets": "EU",
175
+ "paris_mou_bans": "GLOBAL",
176
+ "paris_mou_detentions": "GLOBAL",
177
+ "paris_mou_inspections": "GLOBAL",
178
+ "paris_mou_low_performance_companies": "GLOBAL",
179
+ "thetis_mrv_datasets": "EU",
180
+ }
@@ -0,0 +1,260 @@
1
+ """climatetrace — Climate TRACE v7 provider (api.climatetrace.org; keyless).
2
+
3
+ Counterpart to the Node implementation (esg domain Wave 3: trace_owner + trace_sources — the
4
+ facility/asset-level GHG emissions two-hop chain). 2026-10-23 measured shape
5
+ (v7 = the geo-aggregation rework; the openapi.json endpoint serves YAML not JSON — do not json.load it):
6
+ GET /v7/owners?name=<q>&limit=N → [{id:"E100000001492", name:"Gazprom PJSC"}] — **bare array, only id+name**,
7
+ a LIKE-style candidate face (the E-prefixed id is the stable company key; same pattern as EITI eiti_name:
8
+ candidates are not authoritative normalization);
9
+ GET /v7/sources?ownerIds=<E…>&year=YYYY → bare array: {id,name,sector,subsector,sourceType(point-source|aggregate),
10
+ country,emissionsQuantity(tCO2e),gas,year,capacity,capacityUnits,activity,activityUnits,assetType,
11
+ centroid:{latitude,longitude}} — 19 columns all verified by probing.
12
+ Parameter window: year data window 2021–current year (default current year), gas defaults to co2e_100yr.
13
+ 2026-10-23 wall-probing batch:
14
+ **owners zero hits = 404 JSON** ({"detail":"No owners matched…"}) not a 200 empty array — after the op declares
15
+ notFound it maps to a legal empty candidate list (previously mis-raised as upstream_unavailable); limit has no cap
16
+ (1000 really returned 991 rows), **ordering is unstable across limits** (limit=5 first 5 ≠ limit=1000 first 5 —
17
+ offset paging may skip or duplicate; on the candidate face fetch enough in one go);
18
+ **sources does not error on out-of-window years**: year=2020 → 200 with a null body (data-window guidance, not a block);
19
+ **gas goes straight through by name**: gas=ch4 returns the same shape, the gas field echoes ch4, and
20
+ emissionsQuantity is that gas's native unit, not tCO2e;
21
+ **multiple ownerIds comma-joined = union with dedup** (12∪2→13, a source shared across owners appears once);
22
+ a repeated param = silent first-wins (only the first ownerIds counts, the rest are dropped — a trap shape, do not use).
23
+ v7 dropped v6's assets endpoint (/v6/assets still 200 for backwards compatibility); EUTL is the EU statutory registry,
24
+ TRACE covers global point sources — complementary, not substitutes.
25
+ TTL: owners 30 days (stable-key face) / sources 30 days (annual data rolls slowly); politeness tier 1 rps.
26
+ """
27
+ import re
28
+ from datetime import datetime, timezone
29
+
30
+ from ..errors import SdkError
31
+
32
+ provider = {
33
+ "id": "climatetrace",
34
+ "baseUrl": "https://api.climatetrace.org",
35
+ "rate": {"rps": 1, "burst": 3},
36
+ "ops": {
37
+ # zero hits = 404 JSON "No owners matched" (measured 2026-10-23) → declared notFound, then mapped to a legal empty set
38
+ "owners": {"path": "/v7/owners", "notFound": [404]}, # params: name, limit, offset
39
+ "sources": {"path": "/v7/sources"}, # params: ownerIds (comma = union), year, gas, limit
40
+ },
41
+ "ttl": {"owners": 30 * 86400, "sources": 30 * 86400},
42
+ }
43
+
44
+ _E_ID = re.compile(r"^E\d+$")
45
+
46
+ # sectors closed set (probed 2026-10-28 at /v7/definitions/sectors, 10 values + the special value all_no_forest).
47
+ # The closed set comes from probing upstream — do not generalize from first-seen samples; invalid values make the
48
+ # upstream **silently return the WHOLE corpus** (sectors=electricity returns a row set bit-for-bit identical to the
49
+ # unfiltered one — a false-negative trap), so it is intercepted locally (same discipline as osv ecosystem).
50
+ SECTORS = {
51
+ "agriculture", "buildings", "fluorinated-gases", "forestry-and-land-use",
52
+ "fossil-fuel-operations", "manufacturing", "mineral-extraction",
53
+ "power", "transportation", "waste",
54
+ }
55
+ SECTORS_SPECIAL = {"all_no_forest"} # excludes forestry-and-land-use (openapi description verbatim; verified live 2026-10-28)
56
+
57
+
58
+ def owners_from_upstream(data, size):
59
+ """Upstream response -> company candidates (pure function, offline-testable; owners echoes no total — a truncated list)."""
60
+ items = data if isinstance(data, list) else []
61
+ return {
62
+ "candidates": [{"trace_owner_id": o.get("id"), # E-prefixed stable company key (anchor for the subsequent sources query)
63
+ "owner_name": o.get("name")} for o in items],
64
+ "limit_applied": size,
65
+ }
66
+
67
+
68
+ def sources_from_upstream(data, *, tco2e=True):
69
+ """Upstream response -> emission-source rows (pure function, offline-testable).
70
+
71
+ tco2e=True (default, gas=co2e_100yr basis): row key emissions_tco2e / total emissions_tco2e_sum —
72
+ the frozen face name stays; tco2e=False (explicit non-co2e gas): row key switches to
73
+ emissions_quantity / emissions_quantity_sum — that unit is the gas's native unit, not tCO2e
74
+ (measured 2026-10-23, gas=ch4, same shape); a key name that lies is worse than a switched key."""
75
+ qkey = "emissions_tco2e" if tco2e else "emissions_quantity"
76
+ rows = [{
77
+ "source_id": s.get("id"), # stable key of the facility/aggregate source
78
+ "name": s.get("name"),
79
+ "sector": s.get("sector"), "subsector": s.get("subsector"),
80
+ "source_type": s.get("sourceType"), # point-source = single facility / non-point = aggregate
81
+ "country": s.get("country"),
82
+ "year": s.get("year"), "gas": s.get("gas"), # gas defaults to co2e_100yr (100-year GWP basis)
83
+ qkey: s.get("emissionsQuantity"), # emissions (basis follows gas; key name follows basis)
84
+ "capacity": s.get("capacity"), "capacity_units": s.get("capacityUnits"),
85
+ "capacity_factor": s.get("capacityFactor"), # 0.2.9 audit addition (SourceSummary 19-column alignment)
86
+ "emissions_factor": s.get("emissionsFactor"), # 0.2.9 audit addition
87
+ "emissions_factor_units": s.get("emissionsFactorUnits"), # 0.2.9 audit addition
88
+ "activity": s.get("activity"), "activity_units": s.get("activityUnits"),
89
+ "asset_type": s.get("assetType"),
90
+ "location": ({"lat": (s.get("centroid") or {}).get("latitude"),
91
+ "lon": (s.get("centroid") or {}).get("longitude"), # SRID 4326
92
+ "srid": (s.get("centroid") or {}).get("srid")}
93
+ if s.get("centroid") else None),
94
+ } for s in (data if isinstance(data, list) else [])]
95
+ total = sum(r[qkey] for r in rows if isinstance(r[qkey], (int, float)))
96
+ return {"rows": rows, "count": len(rows),
97
+ f"{qkey}_sum": round(total, 2) if rows else None}
98
+
99
+
100
+ def search_owners(name, *, limit=10, cache=None, no_cache=False):
101
+ """-> {candidates[], limit_applied, _meta}; company candidates (LIKE search).
102
+
103
+ Zero hits = upstream 404 JSON (measured 2026-10-23) — after the notFound declaration this becomes
104
+ a legal empty candidate list (not an error)."""
105
+ from ..transport import call_provider
106
+
107
+ q = str(name or "").strip()
108
+ if not q:
109
+ raise SdkError("usage_error", provider="climatetrace", detail="search_owners requires name (company name)")
110
+ size = min(max(1, int(limit or 10)), 50)
111
+ try:
112
+ r = call_provider(provider, "owners", {"name": q, "limit": size}, cache=cache, no_cache=no_cache)
113
+ except SdkError as e:
114
+ if e.code == "upstream_not_found":
115
+ # 404="No owners matched": legal empty (notFound is not cached, meta minted on the spot — same convention as the gleif parent-company face)
116
+ return {"candidates": [], "limit_applied": size, "_meta": {
117
+ "from_cache": False,
118
+ "fetched_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")}}
119
+ raise
120
+ return {**owners_from_upstream(r["data"], size), "_meta": r}
121
+
122
+
123
+ def owners_raw(name, *, limit=10, offset=None, cache=None, no_cache=False):
124
+ """0.2.0 flat face internals: name/limit/offset straight to upstream under their own names (zero hits -> empty candidates).
125
+
126
+ -> {candidates[], limit_applied, params, _meta} — params are exactly the params sent upstream (echoed, replayable).
127
+ Ordering is unstable across limits with offset (measured 2026-10-23), paging may skip or
128
+ duplicate — on the candidate face fetch enough in one go."""
129
+ from ..transport import call_provider
130
+
131
+ q = str(name or "").strip()
132
+ if not q:
133
+ raise SdkError("usage_error", provider="climatetrace", detail="owners_raw requires name (company name, upstream param name)")
134
+ size = min(max(1, int(limit or 10)), 50) # politeness tier 50 (upstream has no cap; measured 2026-10-23, limit=1000 really returned 991 rows)
135
+ params = {"name": q, "limit": size}
136
+ if offset is not None:
137
+ params["offset"] = int(offset)
138
+ try:
139
+ r = call_provider(provider, "owners", params, cache=cache, no_cache=no_cache)
140
+ except SdkError as e:
141
+ if e.code == "upstream_not_found":
142
+ return {"candidates": [], "limit_applied": size, "params": params, "_meta": {
143
+ "from_cache": False,
144
+ "fetched_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")}}
145
+ raise
146
+ return {**owners_from_upstream(r["data"], size), "params": params, "_meta": r}
147
+
148
+
149
+ def sources_raw(owner_ids=None, *, year=None, gas=None, limit=50, offset=None,
150
+ sectors=None, subsectors=None, gadm_id=None, city_id=None,
151
+ country_group=None, continent=None, cache=None, no_cache=False):
152
+ """0.2.0 flat face internals; 0.2.9 audit parameter expansion: offset/sectors/subsectors/four location
153
+ params folded in (2026-10-28 openapi + live probing).
154
+
155
+ -> {rows[], count, <emissions_*_sum>, resolved_owner, year, gas, params, _meta}.
156
+ owner_ids: a single E key or comma-joined multiples (comma = union with dedup, measured 2026-10-23
157
+ 12∪2→13; a repeated param = upstream silent first-wins trap, do not use); year has no local wall —
158
+ an out-of-window year makes upstream return null, not an error (measured 2026-10-23 year=2020→200 null,
159
+ data window starts 2021); gas defaults to co2e_100yr (row key emissions_tco2e), an explicit non-co2e
160
+ gas switches the row key to emissions_quantity (native unit, measured 2026-10-23 gas=ch4, same shape).
161
+ 0.2.9 new params (all upstream names):
162
+ - offset: real paging (probed 2026-10-28, offset=0/5 row sets have zero overlap; also effective within
163
+ the ownerIds domain) — note ordering is unstable across limits (0.2.3 wall), offset paging may skip
164
+ or duplicate; on the audit face fetch enough in one go.
165
+ - sectors: 10-value closed set validated locally (SECTORS; + the special value all_no_forest =
166
+ excludes forestry) — invalid values make the upstream **silently return the WHOLE corpus**
167
+ (sectors=electricity returns a row set bit-for-bit identical to the unfiltered one, a
168
+ false-negative trap, probed 2026-10-28); valid values really filter (forestry-and-land-use 5/5
169
+ hits, all_no_forest top 10 has no forestry).
170
+ - subsectors: pass-through, no local closed set (68-value enum at /v7/definitions/subsectors;
171
+ invalid values get an upstream 400 self-describing "invalid subsector provided", not silent —
172
+ probed 2026-10-28, upstream self-validation suffices).
173
+ - four location params gadm_id/city_id/country_group/continent: pass-through (continent=Europe/Asia
174
+ probed, filtering effective — RUS counts as Europe, not Asia; countryGroup takes a group slug from
175
+ /v7/definitions/countrygroups, e.g. eu; eu probed, 5/5 rows in the EU list); upstream answers two
176
+ concurrent location params with a 400 self-describing the four-param enum ("choose no more than
177
+ one of: gadmId, countryGroup, continent, cityId") — mutual exclusion is upstream-proven, no local
178
+ wall is set.
179
+ - soft wall: at least one of owner_ids x sectors x subsectors x a location param — upstream allows a
180
+ bare whole-corpus query (year alone already returns a 200 with the whole corpus), bulk corpus scans
181
+ go through the batch pipeline service; this face's politeness tier intercepts.
182
+ Row shape completed in 0.2.9: capacity_factor/emissions_factor/emissions_factor_units +
183
+ location.srid (SourceSummary 19-column alignment, probed 2026-10-28, 18 keys all present)."""
184
+ from ..transport import call_provider
185
+
186
+ oid = str(owner_ids or "").strip()
187
+ parts = []
188
+ if oid:
189
+ parts = [p.strip() for p in oid.split(",") if p.strip()]
190
+ for p in parts:
191
+ if not _E_ID.match(p):
192
+ raise SdkError("usage_error", provider="climatetrace",
193
+ detail=f"owner_ids looks like E100000001492 (comma-separated for multiple owners; got {p!r})")
194
+ sec = str(sectors).strip() if sectors is not None else None
195
+ if sec is not None and sec not in SECTORS | SECTORS_SPECIAL:
196
+ raise SdkError("usage_error", provider="climatetrace",
197
+ detail=f"sectors {sec!r} not in the closed set ("
198
+ + ", ".join(sorted(SECTORS | SECTORS_SPECIAL)) +
199
+ "; invalid values make the upstream silently return the whole corpus — the false-negative trap is intercepted here; enum = /v7/definitions/sectors, probed 2026-10-28)")
200
+ try:
201
+ want_year = int(year) if year is not None else datetime.now(timezone.utc).year
202
+ except (TypeError, ValueError):
203
+ raise SdkError("usage_error", provider="climatetrace", detail=f"year must be an integer year (got {year!r})")
204
+ g = str(gas).strip() if gas is not None else None
205
+ size = min(max(1, int(limit or 50)), 500) # politeness tier 500 (upstream documents no paging param)
206
+ loc = {"gadmId": gadm_id, "cityId": city_id, "countryGroup": country_group, "continent": continent}
207
+ loc_params = {k: str(v).strip() for k, v in loc.items() if v not in (None, "")}
208
+ if not parts and not sec and subsectors in (None, "") and not loc_params:
209
+ raise SdkError("usage_error", provider="climatetrace",
210
+ detail="sources_raw requires at least one of owner_ids (E key), sectors/subsectors, "
211
+ "or a location param (gadm_id/city_id/country_group/continent) — a bare "
212
+ "whole-corpus scan goes through the batch pipeline service "
213
+ "(upstream allows it, but this face's politeness tier intercepts bulk corpus scans)")
214
+ params = {"year": want_year, "limit": size}
215
+ if parts:
216
+ params["ownerIds"] = ",".join(parts)
217
+ if offset is not None:
218
+ params["offset"] = int(offset)
219
+ if sec is not None:
220
+ params["sectors"] = sec
221
+ if subsectors not in (None, ""):
222
+ params["subsectors"] = str(subsectors).strip()
223
+ params.update(loc_params)
224
+ if g is not None:
225
+ params["gas"] = g
226
+ r = call_provider(provider, "sources", params, cache=cache, no_cache=no_cache)
227
+ tco2e = g in (None, "co2e_100yr")
228
+ out = {**sources_from_upstream(r["data"], tco2e=tco2e),
229
+ "year": want_year, "gas": g or "co2e_100yr", "params": params, "_meta": r}
230
+ if parts:
231
+ out["resolved_owner"] = {"trace_owner_ids": parts}
232
+ return out
233
+
234
+
235
+ def owner_sources(owner_id=None, owner_name=None, *, year=None, limit=50, cache=None, no_cache=False):
236
+ """-> {rows[], count, emissions_tco2e_sum, resolved_owner, year, _meta}; company emission-source rows.
237
+ owner_id (E key) is the authoritative anchor; owner_name is fallback only, resolving to an id first
238
+ (first candidate — same fallback semantics as eiti name: not authoritative normalization)."""
239
+ from ..transport import call_provider
240
+
241
+ oid = str(owner_id or "").strip()
242
+ want_year = int(year) if year else datetime.now(timezone.utc).year
243
+ if want_year < 2021:
244
+ raise SdkError("usage_error", provider="climatetrace", detail="year must be a year >= 2021")
245
+ if not oid:
246
+ name = str(owner_name or "").strip()
247
+ if not name:
248
+ raise SdkError("usage_error", provider="climatetrace",
249
+ detail="owner_sources requires trace_owner_id (E key) or name (fallback: resolve to an id first)")
250
+ s = search_owners(name, limit=5, cache=cache, no_cache=no_cache)
251
+ if not s["candidates"]:
252
+ return {**sources_from_upstream([]), "resolved_owner": None, "_meta": s["_meta"]}
253
+ oid = s["candidates"][0]["trace_owner_id"]
254
+ if not _E_ID.match(oid):
255
+ raise SdkError("usage_error", provider="climatetrace", detail=f"trace_owner_id looks like E100000001492 (got {oid!r})")
256
+ size = min(max(1, int(limit or 50)), 500) # upstream exposes no explicit paging param — politeness tier truncates at 500
257
+ r = call_provider(provider, "sources", {"ownerIds": oid, "year": want_year, "limit": size},
258
+ cache=cache, no_cache=no_cache)
259
+ return {**sources_from_upstream(r["data"]), "resolved_owner": {"trace_owner_id": oid},
260
+ "year": want_year, "_meta": r}
agentdata/esg/eiti.py ADDED
@@ -0,0 +1,187 @@
1
+ """eiti — EITI extractives transparency database provider (Datasette instance; keyless).
2
+
3
+ Counterpart to the Node implementation (esg domain Wave 3, P0). eiti-database.eiti.org.
4
+ 2026-10-20 measured shape: view path /eiti_database/{view}.json?_size=N&_shape=objects&<col>__like=<pattern>
5
+ (**the SQL face ?sql= errors out on views — unusable** — a view is not a table; filtering goes through
6
+ the Datasette magic params __like/__exact);
7
+ real column names: eiti_id_company values look like "eiti_id_company:<UUID>" (with the prefix!),
8
+ hq_country_name, first_year_reported/last_year_reported; with _shape=objects the rows are already
9
+ dicts (do not zip columns again).
10
+ view_payments_detailed has 104,484 rows, view_companies has 10,200 companies; companies carry no local
11
+ registration number (the UUID is the stable key).
12
+ 2026-10-21 audit batch B probe (gates 1-2):
13
+ **_size has no 100 cap** (the earlier note "upstream really caps at 100" was a 0.1.8 mis-recording) —
14
+ measured 11,000 rows per call sent as-is, not clamped;
15
+ our politeness cap is 1,000 (the realtime single-query face does not chase bulk; bulk goes through the
16
+ corpus batch pipeline). `_extra=count` is officially supported:
17
+ it returns count = total matching rows ignoring paging, but **count itself is capped at 10,001**
18
+ (count_truncated=true means ">=10,001").
19
+ __like= is SQL LIKE (%/_ metacharacters apply to user input verbatim; ASCII case-insensitive as measured),
20
+ __exact = equality.
21
+ Column fill rates (Equinor slice, 358 rows measured): gov_entity_name 100% / gfs_code 92.5% /
22
+ payment_value_usd 88.3% (in-kind payments etc. have no USD) / project_name 50.6% (across the whole
23
+ surface, not 23%).
24
+ companies face: hq_country_name fill rate is only 62/10,201 (hq_country is mostly null); estma_id has
25
+ 90 companies = ESTMA (Extractive Industries Transparency Standard) filers, **including foreign-owned
26
+ companies operating in Canada** (AngloGold Ghana etc.), not "Canadian companies only".
27
+ TTL 7 days (last_updated rolls slowly); politeness tier 0.5 rps.
28
+ """
29
+ from ..errors import SdkError
30
+
31
+ provider = {
32
+ "id": "eiti",
33
+ "baseUrl": "https://eiti-database.eiti.org",
34
+ "rate": {"rps": 0.5, "burst": 2},
35
+ "ops": {
36
+ "companies": {"path": "/eiti_database/view_companies.json"}, # params: company_name__like + _size/_shape/_extra
37
+ "payments": {"path": "/eiti_database/view_payments_detailed.json"}, # params: eiti_id_company__exact | company_name__like + _size/_shape/_extra
38
+ },
39
+ "ttl": {"companies": 7 * 86400, "payments": 7 * 86400},
40
+ }
41
+
42
+
43
+ def _rows_of(data):
44
+ return (data or {}).get("rows") if isinstance(data, dict) else (data if isinstance(data, list) else [])
45
+
46
+
47
+ def _norm_id(v):
48
+ """eiti_id_company official values carry the "eiti_id_company:" prefix — add it when the caller passes a bare UUID."""
49
+ s = str(v or "").strip()
50
+ if not s:
51
+ return ""
52
+ return s if s.startswith("eiti_id_company:") else f"eiti_id_company:{s}"
53
+
54
+
55
+ def _polite_size(v, dflt):
56
+ """Our politeness cap is 1,000 (the realtime single-query face does not chase bulk) — upstream has no hard 100 limit as measured (probe 2026-10-21, >=11,000 per call)."""
57
+ return min(max(1, int(v or dflt)), 1000)
58
+
59
+
60
+ def total_display(n, capped):
61
+ """Display form of the upstream count: count_truncated=true means count itself is capped at 10,001 -> display ">10000"."""
62
+ if n is None:
63
+ return "?"
64
+ return f">{n - 1}" if capped else str(n)
65
+
66
+
67
+ def companies_from_upstream(data, size):
68
+ """Upstream response -> companies face result (pure function, offline-testable)."""
69
+ candidates = [{
70
+ "eiti_id_company": _norm_id(c.get("eiti_id_company")), # own stable UUID (no local registration number)
71
+ "company_name": c.get("company_name"),
72
+ "hq_country": c.get("hq_country_name"), # fill rate only 62/10,201 (measured 2026-10-21) — mostly null
73
+ "sectors": c.get("sectors"),
74
+ "countries_of_operation": c.get("countries_of_operation"),
75
+ "estma_id": c.get("estma_id"), # ESTMA filer id; filers include foreign-owned companies operating in Canada
76
+ "first_year_reported": c.get("first_year_reported"),
77
+ "last_year_reported": c.get("last_year_reported"),
78
+ } for c in _rows_of(data)]
79
+ return {
80
+ "candidates": candidates,
81
+ "limit_applied": size, # truncation visible (limit contract)
82
+ "total_matches": data.get("count") if isinstance(data, dict) and isinstance(data.get("count"), int) else None,
83
+ "total_matches_capped": bool(isinstance(data, dict) and data.get("count_truncated")),
84
+ }
85
+
86
+
87
+ def payments_from_upstream(data, size):
88
+ """Upstream response -> payments face result (pure function, offline-testable)."""
89
+ rows = [{
90
+ "eiti_id_company": _norm_id(p.get("eiti_id_company")),
91
+ "company_name": p.get("company_name"),
92
+ "year": p.get("year"),
93
+ "country_iso3": p.get("country_iso3"), # country where the payment happened (resource country)
94
+ "project_name": p.get("project_name"), # fill rate about half (Equinor slice 50.6%, measured 2026-10-21)
95
+ "gov_entity": p.get("gov_entity_name"), # receiving government body (named down to bureau level; 100% non-null in the slice)
96
+ "revenue_stream": p.get("revenue_stream_name"), # tax type, upstream verbatim name
97
+ "gfs_code": p.get("gfs_code"), "gfs_name": p.get("gfs_name"), # IMF GFS standard classification (92.5% non-null in the slice)
98
+ "payment_value": p.get("payment_value"), # local currency
99
+ "payment_value_usd": p.get("payment_value_usd"), # official conversion (basis varies across filing batches; 88.3% non-null in the slice)
100
+ "payment_in_kind": p.get("payment_in_kind"), # in-kind payment flag
101
+ } for p in _rows_of(data)]
102
+ return {
103
+ "rows": rows, "count": len(rows),
104
+ "limit_applied": size, # truncation visible (limit contract)
105
+ "total_rows": data.get("count") if isinstance(data, dict) and isinstance(data.get("count"), int) else None,
106
+ "total_rows_capped": bool(isinstance(data, dict) and data.get("count_truncated")),
107
+ }
108
+
109
+
110
+ def search_companies(name, *, limit=10, cache=None, no_cache=False):
111
+ """-> {candidates[], limit_applied, total_matches, _meta}; extractives company candidates
112
+ (name LIKE search, ASCII case-insensitive; %/_ metacharacters apply verbatim; candidates are not authoritative normalization)."""
113
+ from ..transport import call_provider
114
+
115
+ q = str(name or "").strip()
116
+ if not q:
117
+ raise SdkError("usage_error", provider="eiti", detail="search_companies requires name")
118
+ size = _polite_size(limit, 10)
119
+ r = call_provider(provider, "companies", {
120
+ "company_name__like": f"%{q}%",
121
+ "_size": size,
122
+ "_shape": "objects",
123
+ "_extra": "count", # total matches ignoring paging (capped at 10,001, flagged by total_matches_capped)
124
+ }, cache=cache, no_cache=no_cache)
125
+ return {**companies_from_upstream(r["data"], size), "_meta": r}
126
+
127
+
128
+ def company_payments(eiti_id=None, name=None, *, limit=50, cache=None, no_cache=False):
129
+ """-> {rows[], count, limit_applied, total_rows, _meta}; company->government payment rows (eiti_id is the authoritative anchor; name is fallback only).
130
+ Rich row keys: gov_entity_name named down to bureau level + gfs_code (IMF GFS standard revenue classification — the key for cross-country comparability).
131
+ total_rows = upstream _extra=count (capped at 10,001, flagged by total_rows_capped) — the in-page sum is only a subtotal, not the company's total (lesson P1-3)."""
132
+ from ..transport import call_provider
133
+
134
+ eid = _norm_id(eiti_id)
135
+ q = str(name or "").strip()
136
+ if not eid and not q:
137
+ raise SdkError("usage_error", provider="eiti", detail="company_payments requires eiti_id or name")
138
+ params = {"eiti_id_company__exact": eid} if eid else {"company_name__like": f"%{q}%"}
139
+ size = _polite_size(limit, 50)
140
+ r = call_provider(provider, "payments", {
141
+ **params,
142
+ "_size": size,
143
+ "_shape": "objects",
144
+ "_extra": "count", # total rows ignoring paging (capped at 10,001, flagged by total_rows_capped)
145
+ }, cache=cache, no_cache=no_cache)
146
+ return {**payments_from_upstream(r["data"], size), "_meta": r}
147
+
148
+
149
+ def companies_raw(company_name__like, *, size=10, cache=None, no_cache=False):
150
+ """0.2.0 flat face internals: company_name__like straight to upstream under its own name (%/_ metacharacters yours to manage).
151
+
152
+ -> {candidates[], limit_applied, total_matches, total_matches_capped, params, _meta}
153
+ — params are exactly the params sent upstream (including the _size/_shape/_extra magic params)."""
154
+ from ..transport import call_provider
155
+
156
+ like = str(company_name__like or "").strip()
157
+ if not like:
158
+ raise SdkError("usage_error", provider="eiti",
159
+ detail="companies_raw requires company_name__like (Datasette magic param, upstream name; %/_ yours to manage)")
160
+ sz = _polite_size(size, 10)
161
+ params = {"company_name__like": like, "_size": sz, "_shape": "objects", "_extra": "count"}
162
+ r = call_provider(provider, "companies", params, cache=cache, no_cache=no_cache)
163
+ return {**companies_from_upstream(r["data"], sz), "params": params, "_meta": r}
164
+
165
+
166
+ def payments_raw(eiti_id_company__exact=None, company_name__like=None, *, size=50,
167
+ cache=None, no_cache=False):
168
+ """0.2.0 flat face internals: eiti_id_company__exact / company_name__like straight to upstream under their own names.
169
+
170
+ Both filters may be combined (AND across different upstream columns); at least one is required by a client-layer wall
171
+ (a bare scan of all 104,484 rows belongs to the batch pipeline).
172
+ -> {rows[], count, limit_applied, total_rows, total_rows_capped, params, _meta}."""
173
+ from ..transport import call_provider
174
+
175
+ eid = str(eiti_id_company__exact or "").strip() or None
176
+ like = str(company_name__like or "").strip() or None
177
+ if not eid and not like:
178
+ raise SdkError("usage_error", provider="eiti",
179
+ detail="payments_raw requires at least one of eiti_id_company__exact or company_name__like")
180
+ sz = _polite_size(size, 50)
181
+ params = {"_size": sz, "_shape": "objects", "_extra": "count"}
182
+ if eid:
183
+ params["eiti_id_company__exact"] = eid
184
+ if like:
185
+ params["company_name__like"] = like
186
+ r = call_provider(provider, "payments", params, cache=cache, no_cache=no_cache)
187
+ return {**payments_from_upstream(r["data"], sz), "params": params, "_meta": r}