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.
- agentdata/__init__.py +33 -0
- agentdata/__main__.py +4 -0
- agentdata/cache.py +115 -0
- agentdata/cli.py +173 -0
- agentdata/client.py +158 -0
- agentdata/company/__init__.py +3 -0
- agentdata/company/cf.py +145 -0
- agentdata/company/ch_zefix.py +100 -0
- agentdata/company/cli.py +243 -0
- agentdata/company/co_rues.py +72 -0
- agentdata/company/edgar_cik.py +84 -0
- agentdata/company/edgar_fts.py +101 -0
- agentdata/company/faces.py +746 -0
- agentdata/company/fdic_bankfind.py +107 -0
- agentdata/company/fi_ytj.py +89 -0
- agentdata/company/fr_recherche.py +124 -0
- agentdata/company/fts.py +121 -0
- agentdata/company/gleif.py +178 -0
- agentdata/company/kr_hometax.py +112 -0
- agentdata/company/no_brreg.py +124 -0
- agentdata/company/ror.py +105 -0
- agentdata/company/ted.py +117 -0
- agentdata/company/usaspending.py +124 -0
- agentdata/company/vies.py +66 -0
- agentdata/daterange.py +35 -0
- agentdata/economy/__init__.py +10 -0
- agentdata/economy/bis.py +132 -0
- agentdata/economy/cli.py +199 -0
- agentdata/economy/ecb.py +117 -0
- agentdata/economy/eurostat.py +163 -0
- agentdata/economy/faces.py +573 -0
- agentdata/economy/fred.py +92 -0
- agentdata/economy/ilo_stat.py +99 -0
- agentdata/economy/imf_sdmx.py +133 -0
- agentdata/economy/un_comtrade.py +199 -0
- agentdata/economy/unsd_sdg.py +154 -0
- agentdata/economy/wb.py +208 -0
- agentdata/economy/who_xmart.py +177 -0
- agentdata/entries.py +30 -0
- agentdata/errors.py +38 -0
- agentdata/esg/__init__.py +5 -0
- agentdata/esg/cli.py +180 -0
- agentdata/esg/climatetrace.py +260 -0
- agentdata/esg/eiti.py +187 -0
- agentdata/esg/epa_frs.py +138 -0
- agentdata/esg/euasl.py +75 -0
- agentdata/esg/eutl.py +66 -0
- agentdata/esg/faces.py +498 -0
- agentdata/esg/paris_mou.py +240 -0
- agentdata/esg/thetis_mrv.py +67 -0
- agentdata/healthcare/__init__.py +7 -0
- agentdata/healthcare/au_tga.py +142 -0
- agentdata/healthcare/ca_dpd.py +123 -0
- agentdata/healthcare/ca_mdall.py +155 -0
- agentdata/healthcare/cli.py +375 -0
- agentdata/healthcare/ctgov.py +101 -0
- agentdata/healthcare/dailymed.py +102 -0
- agentdata/healthcare/epmc.py +108 -0
- agentdata/healthcare/eudamed.py +124 -0
- agentdata/healthcare/faces.py +811 -0
- agentdata/healthcare/fda_crl.py +86 -0
- agentdata/healthcare/fda_devices.py +199 -0
- agentdata/healthcare/fda_drugs.py +156 -0
- agentdata/healthcare/fda_recalls.py +137 -0
- agentdata/healthcare/gb_mhra.py +137 -0
- agentdata/healthcare/gudid.py +84 -0
- agentdata/healthcare/mesh.py +89 -0
- agentdata/healthcare/rasff.py +164 -0
- agentdata/healthcare/rxnav.py +67 -0
- agentdata/healthcare/safety_gate.py +175 -0
- agentdata/healthcare/shortages.py +107 -0
- agentdata/legal/__init__.py +4 -0
- agentdata/legal/afdb.py +93 -0
- agentdata/legal/ca_sema.py +143 -0
- agentdata/legal/cli.py +312 -0
- agentdata/legal/co_sic.py +70 -0
- agentdata/legal/courtlistener.py +77 -0
- agentdata/legal/csl.py +139 -0
- agentdata/legal/dgcomp.py +125 -0
- agentdata/legal/ecfr.py +129 -0
- agentdata/legal/faces.py +700 -0
- agentdata/legal/federalregister.py +188 -0
- agentdata/legal/gb_cma.py +117 -0
- agentdata/legal/gb_tna_caselaw.py +245 -0
- agentdata/legal/idb.py +85 -0
- agentdata/legal/lda.py +147 -0
- agentdata/legal/sipri.py +178 -0
- agentdata/legal/ukec.py +148 -0
- agentdata/legal/wb.py +92 -0
- agentdata/procurement/__init__.py +5 -0
- agentdata/procurement/br_pncp.py +126 -0
- agentdata/procurement/cli.py +155 -0
- agentdata/procurement/co_secop.py +88 -0
- agentdata/procurement/faces.py +239 -0
- agentdata/procurement/ny_open_data.py +82 -0
- agentdata/procurement/ungm.py +137 -0
- agentdata/procurement/wb_proc.py +84 -0
- agentdata/research/__init__.py +4 -0
- agentdata/research/biorxiv.py +66 -0
- agentdata/research/cli.py +129 -0
- agentdata/research/crossref.py +121 -0
- agentdata/research/faces.py +349 -0
- agentdata/research/gb_gtr.py +58 -0
- agentdata/research/icite.py +52 -0
- agentdata/research/nih_reporter.py +143 -0
- agentdata/research/nsf.py +68 -0
- agentdata/research/openalex.py +145 -0
- agentdata/research/opencitations.py +50 -0
- agentdata/security/__init__.py +3 -0
- agentdata/security/cisa_kev.py +42 -0
- agentdata/security/cli.py +37 -0
- agentdata/security/faces.py +91 -0
- agentdata/security/osv.py +72 -0
- agentdata/transport.py +541 -0
- agentdata-0.3.26.dist-info/METADATA +266 -0
- agentdata-0.3.26.dist-info/RECORD +120 -0
- agentdata-0.3.26.dist-info/WHEEL +5 -0
- agentdata-0.3.26.dist-info/entry_points.txt +10 -0
- agentdata-0.3.26.dist-info/licenses/LICENSE +201 -0
- agentdata-0.3.26.dist-info/top_level.txt +1 -0
agentdata/esg/epa_frs.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""epa_frs — EPA FRS facility master registry Envirofacts efservice provider (keyless).
|
|
2
|
+
|
|
3
|
+
FRS (Facility Registry Service) = the authoritative key source of the shared facility master registry in the
|
|
4
|
+
esg domain (ECHO/TRI/GHGRP converge on the Registry ID); this face opens a facility point query:
|
|
5
|
+
the frs_facility_site table has 5,602,878 rows and 52 columns.
|
|
6
|
+
Measured shape (probed 2026-10-29):
|
|
7
|
+
- Real host **data.epa.gov/efservice** (enviro.epa.gov redirects 301 to the same path; the old
|
|
8
|
+
ofmpub.epa.gov is 404).
|
|
9
|
+
- Filter syntax = **column/operator/value as three separate path segments**: `/STATE_CODE/=/DE/` — a
|
|
10
|
+
single segment `/=DE/` silently returns []; the table name must be lowercase (FRS_FACILITY_SITE in
|
|
11
|
+
uppercase = silent empty result, not an error); multiple filters chain into AND
|
|
12
|
+
(STATE_CODE+City_NAME hit as measured); values are URL-encoded (space->%20, exact names verified).
|
|
13
|
+
- Paging `/ROWS/{start}:{end}/` is a **closed interval** (0:2 = 3 rows); **when end<start the upstream
|
|
14
|
+
silently degrades to offset=start with an unbounded dump** (ROWS/3:2 measured 18,791 rows = DE
|
|
15
|
+
18,794−3) — this face pins end=start+size−1, which is always valid, and never omits the ROWS segment
|
|
16
|
+
(omitting it dumps the whole 5.6M-row table).
|
|
17
|
+
- Operator lineage: the `=` family works; **`~`/`!~`/LIKE silently return 0** (do not use); **a value
|
|
18
|
+
containing % gives nginx 403** (WAF).
|
|
19
|
+
- Name search is therefore an **exact match** (PRIMARY_NAME/=/MILTON RIVERFRONT LLC/ verified) —
|
|
20
|
+
fuzzy facility-name search goes through the ECHO face (a separate line); this face adds no sugar.
|
|
21
|
+
- Total `/COUNT/JSON` -> [{TOTALQUERYRESULTS}] (same syntax in filtered state, DE=18,794 as measured).
|
|
22
|
+
52 columns of note: registry_id (authoritative key) /primary_name/std_name/address family
|
|
23
|
+
(location_address·city·county·state·postal·std_* standardized family)/site_type_name/
|
|
24
|
+
interest_status_code/operating_status/epa_region_code/parent_registry_id/fips_code. This table has
|
|
25
|
+
**no coordinate columns** (lat/long live in other FRS tables).
|
|
26
|
+
Politeness tier: size<=500, rate 1rps; every call = rows + count (two requests) (total_matches contract).
|
|
27
|
+
"""
|
|
28
|
+
from ..errors import SdkError
|
|
29
|
+
|
|
30
|
+
provider = {
|
|
31
|
+
"id": "epa_frs",
|
|
32
|
+
"baseUrl": "https://data.epa.gov/efservice",
|
|
33
|
+
"rate": {"rps": 1.0, "burst": 2},
|
|
34
|
+
# ops are dynamic: the paths of facilities/count are assembled at call time from the effective filter columns (_op_for) —
|
|
35
|
+
# the build_url {token} template eats the filter values (quoted, space->%20 is exactly the verified form).
|
|
36
|
+
# ttl at the top level must be a dict (transport's fallback lookup is eager) — the real values hang on the dynamic opdef.
|
|
37
|
+
"ops": {},
|
|
38
|
+
"ttl": {},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_FILTERS = ("registry_id", "primary_name", "state_code", "city_name", "country_name", "site_type_name",
|
|
42
|
+
"epa_region_code") # order = URL segment order (stable, replayable); the 0.3.20 audit added the
|
|
43
|
+
# three trailing columns (probed 2026-11-02: COUNTRY_NAME/=/GERMANY -> COUNT 110 — FRS covers non-US facilities,
|
|
44
|
+
# UNITED STATES -> 3,129,491; SITE_TYPE_NAME/=/STATIONARY -> 4,725,914; EPA_REGION_CODE/=/10 -> 214,197, the
|
|
45
|
+
# value form is the zero-padded two-digit string '03'). interest_status_code/operating_status are null on the
|
|
46
|
+
# master table (they live on the interest subtables) — not exposed.
|
|
47
|
+
TTL = 86400
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _op_for(cols, *, count=False):
|
|
51
|
+
"""Effective filter columns -> efservice path template (three-segment separated operator form; count=True takes /COUNT/JSON)."""
|
|
52
|
+
segs = "".join(f"/{c.upper()}/=/{{{c}}}" for c in cols)
|
|
53
|
+
tail = "/COUNT/JSON" if count else "/ROWS/{start}:{end}/JSON"
|
|
54
|
+
return {"path": f"/frs_facility_site{segs}{tail}", "ttl": TTL}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _polite_size(v, dflt):
|
|
58
|
+
"""Politeness tier 500 (rows average ~1.5KB across 52 fat columns; upstream has no hard limit, but an invalid ROWS segment = the unbounded-dump trap)."""
|
|
59
|
+
return min(max(1, int(v or dflt)), 500)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def facilities_from_upstream(data, size, start):
|
|
63
|
+
"""Upstream response -> facilities face result (pure function, offline-testable; projects 18 of the 52 columns)."""
|
|
64
|
+
rows = [{
|
|
65
|
+
"registry_id": f.get("registry_id"), # authoritative FRS facility key (ECHO/TRI/GHGRP convergence anchor)
|
|
66
|
+
"primary_name": f.get("primary_name"),
|
|
67
|
+
"std_name": f.get("std_name"),
|
|
68
|
+
"location_address": f.get("location_address"),
|
|
69
|
+
"city_name": f.get("city_name"),
|
|
70
|
+
"county_name": f.get("county_name"),
|
|
71
|
+
"state_code": f.get("state_code"),
|
|
72
|
+
"state_name": f.get("state_name"), # dirty upstream (measured: empty strings and the 'DE' abbreviation form)
|
|
73
|
+
"postal_code": f.get("postal_code"),
|
|
74
|
+
"country_name": f.get("country_name"),
|
|
75
|
+
"std_full_address": f.get("std_full_address"), # USPS standardized full address
|
|
76
|
+
"site_type_name": f.get("site_type_name"),
|
|
77
|
+
"interest_status_code": f.get("interest_status_code"),
|
|
78
|
+
"operating_status": f.get("operating_status"),
|
|
79
|
+
"epa_region_code": f.get("epa_region_code"),
|
|
80
|
+
"parent_registry_id": f.get("parent_registry_id"), # parent facility key (not the corporate parent)
|
|
81
|
+
"fips_code": f.get("fips_code"),
|
|
82
|
+
"create_date": f.get("create_date"),
|
|
83
|
+
} for f in (data or []) if isinstance(f, dict)]
|
|
84
|
+
return {"rows": rows, "count": len(rows), "limit_applied": size, "offset": start}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _norm_filters(registry_id, primary_name, state_code, city_name, country_name, site_type_name, epa_region_code):
|
|
88
|
+
out = {}
|
|
89
|
+
upper_cols = {"state_code", "country_name", "site_type_name", "epa_region_code"} # upstream stores uppercase forms
|
|
90
|
+
for k, v in (("registry_id", registry_id), ("primary_name", primary_name),
|
|
91
|
+
("state_code", state_code), ("city_name", city_name),
|
|
92
|
+
("country_name", country_name), ("site_type_name", site_type_name),
|
|
93
|
+
("epa_region_code", epa_region_code)):
|
|
94
|
+
v = str(v or "").strip()
|
|
95
|
+
if k in upper_cols and v:
|
|
96
|
+
v = v.upper() # upstream uses the uppercase form (DE / GERMANY / STATIONARY / '03'); tolerate lowercase input
|
|
97
|
+
if v:
|
|
98
|
+
out[k] = v
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def facilities_raw(registry_id=None, primary_name=None, state_code=None, city_name=None, *,
|
|
103
|
+
country_name=None, site_type_name=None, epa_region_code=None,
|
|
104
|
+
start=0, size=50, cache=None, no_cache=False):
|
|
105
|
+
"""0.3.2 flat face internals: seven filter columns (all `=` exact, combinable = AND) + start/size paging.
|
|
106
|
+
|
|
107
|
+
The four 0.3.2 columns (registry_id/primary_name/state_code/city_name) plus the three the 0.3.20 audit
|
|
108
|
+
added (country_name/site_type_name/epa_region_code — country_name unlocks the non-US facility slice,
|
|
109
|
+
GERMANY=110 measured). Every call makes two requests: rows (ROWS/{start}:{end} closed interval, end
|
|
110
|
+
always computed locally) + count (TOTALQUERYRESULTS).
|
|
111
|
+
-> {rows[], count, limit_applied, offset, total_matches, params, _meta, _count_meta}.
|
|
112
|
+
Local safety wall: at least one of the seven filters (a bare scan of all 5,602,878 rows is served by
|
|
113
|
+
the batch pipeline as national_single.zip)."""
|
|
114
|
+
from ..transport import call_provider
|
|
115
|
+
|
|
116
|
+
filters = _norm_filters(registry_id, primary_name, state_code, city_name,
|
|
117
|
+
country_name, site_type_name, epa_region_code)
|
|
118
|
+
if not filters:
|
|
119
|
+
raise SdkError("usage_error", provider="epa_frs",
|
|
120
|
+
detail="facilities_raw requires at least one of registry_id / primary_name / state_code / "
|
|
121
|
+
"city_name / country_name / site_type_name / epa_region_code "
|
|
122
|
+
"(local safety wall: a bare scan of all 5,602,878 rows is served by the "
|
|
123
|
+
"batch pipeline as national_single.zip)")
|
|
124
|
+
cols = [c for c in _FILTERS if c in filters]
|
|
125
|
+
sz = _polite_size(size, 50)
|
|
126
|
+
st = max(0, int(start or 0))
|
|
127
|
+
end = st + sz - 1 # right end of the closed interval; always >= start — an invalid ROWS segment = unbounded upstream dump (trap noted in the header)
|
|
128
|
+
params = {**filters, "start": st, "end": end}
|
|
129
|
+
dyn = {**provider, "ops": {"facilities": _op_for(cols)}}
|
|
130
|
+
r = call_provider(dyn, "facilities", params, cache=cache, no_cache=no_cache)
|
|
131
|
+
dync = {**provider, "ops": {"count": _op_for(cols, count=True)}}
|
|
132
|
+
rc = call_provider(dync, "count", dict(filters), cache=cache, no_cache=no_cache)
|
|
133
|
+
total = None
|
|
134
|
+
if isinstance(rc["data"], list) and rc["data"]:
|
|
135
|
+
total = rc["data"][0].get("TOTALQUERYRESULTS")
|
|
136
|
+
out = facilities_from_upstream(r["data"], sz, st)
|
|
137
|
+
return {**out, "total_matches": total, "filters": filters,
|
|
138
|
+
"params": params, "_meta": r, "_count_meta": rc}
|
agentdata/esg/euasl.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""euasl — EU Air Safety List (third-country carriers banned or restricted, European Commission) file-catalog provider (keyless).
|
|
2
|
+
|
|
3
|
+
esg transport-compliance axis (2026-11-02, 0.3.22 batch): the list lives on a Liferay page
|
|
4
|
+
(transport.ec.europa.eu/transport-themes/eu-air-safety-list_en — the pre-migration /aviation-policy path is
|
|
5
|
+
dead 404) whose only machine-readable form is a single XLSX (~28KB: Annex A = carriers subject to an
|
|
6
|
+
operating ban, 188 rows measured 2026-10-20; Annex B = carriers operating under restrictions and conditions,
|
|
7
|
+
2 rows — AIR KORYO / IRAN AIR) plus 24 official-language PDF renditions off the same page. The SDK does not
|
|
8
|
+
proxy binary bodies (the JSON-only transport would corrupt a zip — same posture as eutl/thetis_mrv): this
|
|
9
|
+
face returns the live download catalog. Measured shape (2026-11-02, fresh page 200/127KB):
|
|
10
|
+
- 25 download links, href="/document/download/<uuid>_<lang>?filename=air-safety-list[_<lang>][.ext]" —
|
|
11
|
+
**the document UUIDs rotate on every republication** (they are publish-time content ids, not stable keys),
|
|
12
|
+
so reconcile harvests against this listing and never guess file names. One XLSX master (uuid 67b75752-…,
|
|
13
|
+
lang en) + 24 language PDFs (uuid 3195fabb-…; the hr rendition carries an _2 suffix on the filename only —
|
|
14
|
+
the language token is still the path segment).
|
|
15
|
+
- Update cadence: "whenever the Commission considers it necessary" (historically several revisions a year).
|
|
16
|
+
- The XLSX parses offline but not naively: ~71 self-closing <c/> cells in the sheet mean position-guessing
|
|
17
|
+
fails — cell references (r="A<n>") are the parse key, sharedStrings holds the strings (batch-pipeline
|
|
18
|
+
concern; the catalog face is what keeps the link honest).
|
|
19
|
+
TTL 1 day (the page re-renders on each republication; links are stable between them). Politeness tier 0.5 rps.
|
|
20
|
+
"""
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
_DOWNLOAD_RE = re.compile(r'href="(/document/download/([^"]+?))"')
|
|
24
|
+
_FILENAME_RE = re.compile(r'filename=([^&"]+)')
|
|
25
|
+
|
|
26
|
+
provider = {
|
|
27
|
+
"id": "euasl",
|
|
28
|
+
"baseUrl": "https://transport.ec.europa.eu",
|
|
29
|
+
"rate": {"rps": 0.5, "burst": 2},
|
|
30
|
+
"ops": {
|
|
31
|
+
"page": {"path": "/transport-themes/eu-air-safety-list_en", "accept": "text"}, # Liferay HTML — links parsed at this layer
|
|
32
|
+
},
|
|
33
|
+
"ttl": {"page": 86400},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def datasets_from_upstream(html):
|
|
38
|
+
"""Page HTML -> download-catalog rows (pure function, offline-testable).
|
|
39
|
+
|
|
40
|
+
rows are sorted xlsx-master first then by language code (deterministic); download_path is a site path
|
|
41
|
+
(prefix the baseUrl — the batch pipeline assembles the URL from it); `master` carries the machine-readable
|
|
42
|
+
XLSX row (or None — the page without the xlsx link is an abnormal state the caller surfaces)."""
|
|
43
|
+
rows, seen = [], set()
|
|
44
|
+
for full, tail in _DOWNLOAD_RE.findall(html or ""):
|
|
45
|
+
if full in seen:
|
|
46
|
+
continue
|
|
47
|
+
seen.add(full)
|
|
48
|
+
path_part, _, query = tail.partition("?") # the query (?filename=…) rides the href too — split it off before the language split
|
|
49
|
+
uuid, _, lang = path_part.rpartition("_") # uuid carries hyphens only, the language token is the last path segment
|
|
50
|
+
fm = _FILENAME_RE.search(query)
|
|
51
|
+
file_name = fm.group(1) if fm else None
|
|
52
|
+
fmt = ("xlsx" if (file_name or "").lower().endswith(".xlsx")
|
|
53
|
+
else "pdf" if (file_name or "").lower().endswith(".pdf") else None)
|
|
54
|
+
rows.append({
|
|
55
|
+
"document_id": uuid or None,
|
|
56
|
+
"language": lang or None,
|
|
57
|
+
"file_name": file_name,
|
|
58
|
+
"format": fmt,
|
|
59
|
+
"download_path": full,
|
|
60
|
+
})
|
|
61
|
+
rows.sort(key=lambda r: (0 if r["format"] == "xlsx" else 1, r["language"] or "", r["file_name"] or ""))
|
|
62
|
+
master = next(({"file_name": r["file_name"], "download_path": r["download_path"]}
|
|
63
|
+
for r in rows if r["format"] == "xlsx"), None)
|
|
64
|
+
return {"rows": rows, "count": len(rows), "master": master}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def datasets_raw(*, cache=None, no_cache=False):
|
|
68
|
+
"""0.3.22 flat face internals: the live EU Air Safety List download catalog in one request (upstream has no filter params).
|
|
69
|
+
|
|
70
|
+
-> {rows[], count, master, params, _meta} — params={} (zero upstream params)."""
|
|
71
|
+
from ..transport import call_provider
|
|
72
|
+
|
|
73
|
+
params = {}
|
|
74
|
+
r = call_provider(provider, "page", params, cache=cache, no_cache=no_cache)
|
|
75
|
+
return {**datasets_from_upstream(r["data"]), "params": params, "_meta": r}
|
agentdata/esg/eutl.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""eutl — EU Emissions Trading System registry (Union Registry) official bulk-download catalog provider (keyless).
|
|
2
|
+
|
|
3
|
+
Sibling face of the esg batch pipeline: EUTL's 15 bulk files (the carbon-emissions surface) previously had
|
|
4
|
+
download coordinates but no API face; 2026-10-29 overturned that: the portal ships its own catalog API
|
|
5
|
+
`GET /api/data-download` that returns a keyless JSON array of 15 files
|
|
6
|
+
`{name, pane_index, group_index, sort_index, url, action}` — this face adopts it as the **download-catalog
|
|
7
|
+
query face** (the file bodies still go through the batch pipeline; this SDK does not proxy large-file downloads).
|
|
8
|
+
Measured shape (2026-10-29):
|
|
9
|
+
- Host **union-registry-data.ec.europa.eu** (the old euetsdata.europa.eu is ENOTFOUND — do not probe it again).
|
|
10
|
+
- The 15 files fall into three tiers (measured 4+10+1): 4 Azure daily extracts
|
|
11
|
+
(dlsclimabi.blob.core.windows.net…csv.gz: Accounts/Operators/Operators Yearly Activity/Registry Holdings Daily)
|
|
12
|
+
+ 1 Transactions file + 10 climate.ec.europa.eu yearly files (Compliance 2021-2025 x5 + Verified Emissions 2021-2025).
|
|
13
|
+
- Yearly file URLs are direct document/download/<uuid> links (no suffix); daily file URLs carry the cadence in the path.
|
|
14
|
+
- The action field is a portal frontend hook name (d_accounts_daily etc.), not a stable contract — pass through, do not interpret.
|
|
15
|
+
TTL 1 day (the catalog file set rolls slowly); politeness tier 0.5 rps.
|
|
16
|
+
"""
|
|
17
|
+
from ..errors import SdkError
|
|
18
|
+
|
|
19
|
+
provider = {
|
|
20
|
+
"id": "eutl",
|
|
21
|
+
"baseUrl": "https://union-registry-data.ec.europa.eu",
|
|
22
|
+
"rate": {"rps": 0.5, "burst": 2},
|
|
23
|
+
"ops": {
|
|
24
|
+
"datasets": {"path": "/api/data-download"}, # no params: one request returns the whole 15-file catalog
|
|
25
|
+
},
|
|
26
|
+
"ttl": {"datasets": 86400},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cadence(name, url):
|
|
31
|
+
"""Derive the cadence tier from the file name/URL: daily = Azure daily extract / yearly = portal yearly file /
|
|
32
|
+
static = the Transactions file (it hangs on the same climate.ec.europa.eu domain but is not a rolling yearly
|
|
33
|
+
file — split by name; measured 15 files = 4+10+1)."""
|
|
34
|
+
n, u = name or "", url or ""
|
|
35
|
+
if "blob.core.windows.net" in u:
|
|
36
|
+
return "daily"
|
|
37
|
+
if "transaction" in n.lower():
|
|
38
|
+
return "static"
|
|
39
|
+
if "climate.ec.europa.eu" in u:
|
|
40
|
+
return "yearly"
|
|
41
|
+
return "other"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def datasets_from_upstream(data):
|
|
45
|
+
"""Upstream response -> datasets face result (pure function, offline-testable)."""
|
|
46
|
+
rows = [{
|
|
47
|
+
"name": d.get("name"),
|
|
48
|
+
"url": d.get("url"), # direct download link (daily = .csv.gz on Azure; yearly = document uuid)
|
|
49
|
+
"cadence": _cadence(d.get("name"), d.get("url")), # daily/yearly/other
|
|
50
|
+
"group_index": d.get("group_index"),
|
|
51
|
+
"pane_index": d.get("pane_index"),
|
|
52
|
+
"sort_index": d.get("sort_index"),
|
|
53
|
+
"action": d.get("action"), # portal frontend hook name, not a stable contract
|
|
54
|
+
} for d in (data or []) if isinstance(d, dict)]
|
|
55
|
+
return {"rows": rows, "count": len(rows)}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def datasets_raw(*, cache=None, no_cache=False):
|
|
59
|
+
"""0.3.2 flat face internals: the whole catalog in one request (upstream has no filter params — the client filters rows locally).
|
|
60
|
+
|
|
61
|
+
-> {rows[], count, params, _meta} — params={} (zero upstream params, replayability proven by the empty param set)."""
|
|
62
|
+
from ..transport import call_provider
|
|
63
|
+
|
|
64
|
+
params = {}
|
|
65
|
+
r = call_provider(provider, "datasets", params, cache=cache, no_cache=no_cache)
|
|
66
|
+
return {**datasets_from_upstream(r["data"]), "params": params, "_meta": r}
|