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
@@ -0,0 +1,349 @@
1
+ """research faces — flat face-method mixin (0.2.6 domain split, migrated from client.py _Domain).
2
+
3
+ Mixes into the root _Domain (assembled via MRO, see client.py); discipline: no __init__,
4
+ depend only on root internals (_flat_env/_c/_request…), method names globally unique across
5
+ domains (a collision would shadow another domain's face).
6
+ """
7
+ from ..errors import SdkError
8
+
9
+
10
+ from .. import research # call through the domain module (historical shape, kept for minimal churn)
11
+
12
+ class ResearchFaces:
13
+ def _research_only(self, tool: str):
14
+ if self.name != "research":
15
+ raise SdkError("usage_error", detail=f"{tool} is a research-domain capability (not available on the {self.name} domain)")
16
+
17
+ def nsf_awards(self, *, keyword=None, id=None, rpp=25, offset=0,
18
+ execution=None, no_cache=None):
19
+ """research nsf_awards: NSF funding search (awards.json verbatim four params; keyword
20
+ full-text × id point lookup, XOR enforced locally; rpp 1-200 upstream doc wall; upstream
21
+ sends no total — a full page signals more via has_more)."""
22
+ self._research_only("nsf_awards")
23
+ r = research.nsf.awards_raw(keyword, id=id, rpp=rpp, offset=offset,
24
+ cache=self._c._cache,
25
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
26
+ result = {k: x for k, x in r.items() if k != "_meta"}
27
+ top = (result["rows"] or [None])[0]
28
+ q = keyword or id
29
+ return self._flat_env(
30
+ tool="nsf_awards", input={"keyword": keyword, "id": id,
31
+ "rpp": r["params"]["rpp"], "offset": r["params"]["offset"]},
32
+ reason=(f"NSF: '{q}' — {result['count']} rows this page"
33
+ + (" (full page, likely more — continue with --offset)" if result.get("has_more") else "")
34
+ + (f", top hit {top['id']} {top['title'] or '—'}"
35
+ f" ({top['institution'] or '—'}, ${top['amount'] or '—'})" if top else ", zero hits")),
36
+ result=result,
37
+ evidence=[{"quote": f"{a['id'] or '—'} = {a['title'] or '—'}"
38
+ f" ({a['institution'] or '—'}, PI {a['principal_investigator'] or '—'})",
39
+ "url": a["award_notice_url"] or "https://www.nsf.gov/awardsearch/"}
40
+ for a in result["rows"][:5]],
41
+ provider_id="nsf", metas=[r["_meta"]],
42
+ query={"source": "raw", "upstream": r["params"]},
43
+ )
44
+
45
+ def nih_reporter_projects(self, *, org_name=None, fiscal_year=None, include_fields=None,
46
+ limit=25, offset=0, execution=None, no_cache=None):
47
+ """research nih_reporter_projects: NIH funded-project search (/v2/projects/search POST;
48
+ at least one of org_name/fiscal_year (local safety wall); criteria=snake_case and
49
+ include_fields=camelCase are the two axes each hard-bound upstream (a wrong shape is
50
+ silently ignored — bare params are not passed through); offset+limit ≤15,000 upstream
51
+ deep-paging wall)."""
52
+ self._research_only("nih_reporter_projects")
53
+ r = research.nih_reporter.projects_raw(org_name, fiscal_year=fiscal_year,
54
+ include_fields=include_fields, limit=limit, offset=offset,
55
+ cache=self._c._cache,
56
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
57
+ result = {k: x for k, x in r.items() if k != "_meta"}
58
+ top = (result["rows"] or [None])[0]
59
+ crit = r["params"]["criteria"]
60
+ who = ""
61
+ if crit.get("org_names"):
62
+ who += f"org '{crit['org_names'][0]}' "
63
+ if crit.get("fiscal_years"):
64
+ who += f"FY{crit['fiscal_years'][0]} "
65
+ if result["total"] is None:
66
+ reason = f"NIH RePORTER: {result['count']} items returned"
67
+ else:
68
+ reason = (f"NIH RePORTER: {who}— {result['total']} items in total, {result['count']} on this page"
69
+ + (f", top hit {top['appl_id']} {top['project_title'] or '—'}"
70
+ f" ({top['org_name'] or '—'})" if top else "")
71
+ + (f" — human-readable search page {result['search_url']}" if result.get("search_url") else ""))
72
+ return self._flat_env(
73
+ tool="nih_reporter_projects",
74
+ input={"org_name": org_name, "fiscal_year": fiscal_year,
75
+ "include_fields": include_fields, "limit": r["params"]["limit"],
76
+ "offset": r["params"]["offset"]},
77
+ reason=reason,
78
+ result=result,
79
+ evidence=[{"quote": f"{p['appl_id'] or '—'} = {p['project_title'] or '—'}"
80
+ f" ({p['org_name'] or '—'}, "
81
+ f"{'/'.join(x['full_name'] or '—' for x in p['pis'][:2]) or '—'})",
82
+ "url": f"https://reporter.nih.gov/project-details/{p['appl_id']}"}
83
+ for p in result["rows"][:5]],
84
+ provider_id="nih-reporter", metas=[r["_meta"]],
85
+ query={"source": "raw", "upstream": r["params"]},
86
+ )
87
+
88
+ # —— 0.2.4 research new faces (OpenAlex/Crossref/GtR/NIH bridge/iCite/bioRxiv/OpenCitations) ——
89
+
90
+ def openalex_works(self, *, search=None, ror=None, doi=None, title_search=None,
91
+ per_page=25, cursor=None, select=None, execution=None, no_cache=None):
92
+ """research openalex_works: global scholarly output search (/works verbatim name;
93
+ search full-text × ror institution edge × doi × title_search — four constraints combined
94
+ with AND; per_page 1-200 explicit upstream 400 wall; the cursor encodes paging only —
95
+ when continuing, the constraint parameters must be re-sent unchanged; select is
96
+ force-merged with the core fields (adversarial-review constraints 2/3))."""
97
+ self._research_only("openalex_works")
98
+ r = research.openalex.works_raw(search, ror=ror, doi=doi, title_search=title_search,
99
+ per_page=per_page, cursor=cursor, select=select,
100
+ cache=self._c._cache,
101
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
102
+ result = {k: x for k, x in r.items() if k != "_meta"}
103
+ top = (result["rows"] or [None])[0]
104
+ cons = []
105
+ if search:
106
+ cons.append(f"full-text '{search}'")
107
+ if r["params"].get("filter"):
108
+ cons.append("filter " + r["params"]["filter"])
109
+ return self._flat_env(
110
+ tool="openalex_works",
111
+ input={"search": search, "ror": ror, "doi": doi, "title_search": title_search,
112
+ "per_page": r["params"]["per-page"], "cursor": cursor, "select": select},
113
+ reason=(f"OpenAlex: {' × '.join(cons)} — {result['total']:,} works hit, {result['count']} rows this page"
114
+ + (f", top hit {top['display_name'] or '—'}"
115
+ f" ({top['publication_year']}, cited {top['cited_by_count']})" if top else "")
116
+ + (" — continue with next_cursor (re-send the constraint parameters unchanged)" if result.get("next_cursor") else "")),
117
+ result=result,
118
+ evidence=[{"quote": f"{w['display_name'] or '—'} ({w['publication_year']}, "
119
+ + "; ".join((a["institutions"] or [{}])[0].get("display_name")
120
+ or (a["raw_author_name"] or a["author"] or "—")
121
+ for a in w["authorships"][:2])
122
+ + f", cited {w['cited_by_count']})",
123
+ "url": w["doi"] or w["id"]}
124
+ for w in result["rows"][:5]],
125
+ provider_id="openalex", metas=[r["_meta"]],
126
+ query={"source": "raw", "upstream": r["params"]},
127
+ )
128
+
129
+ def openalex_institutions(self, *, search=None, per_page=25, execution=None, no_cache=None):
130
+ """research openalex_institutions: institution candidate search (/institutions?search=
131
+ verbatim name; same-name clones in different countries coexist (a pfizer probe returned
132
+ 24 candidates incl. US/UK/FR) — **the candidate list is never auto-picked**: choose the
133
+ ROR explicitly, then query output via openalex_works (adversarial-review constraint 1))."""
134
+ self._research_only("openalex_institutions")
135
+ r = research.openalex.institutions_raw(search, per_page=per_page,
136
+ cache=self._c._cache,
137
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
138
+ result = {k: x for k, x in r.items() if k != "_meta"}
139
+ top = (result["rows"] or [None])[0]
140
+ return self._flat_env(
141
+ tool="openalex_institutions",
142
+ input={"search": search, "per_page": r["params"]["per-page"]},
143
+ reason=(f"OpenAlex institution candidates: '{search}' hit {result['total']}, {result['count']} rows this page"
144
+ + (f", top relevance {top['display_name']} ({top['ror']}, {top['type']}, "
145
+ f"{top['works_count']:,} works) — same-name clones in different countries: "
146
+ "pick the ROR manually, then query output via openalex_works --ror" if top else ", zero hits")),
147
+ result=result,
148
+ evidence=[{"quote": f"{i['display_name']} ({i['ror']}, {i['country_code']} {i['type']}, "
149
+ f"{i['works_count']:,} works, cited {i['cited_by_count']:,})",
150
+ "url": i["id"]}
151
+ for i in result["rows"][:5]],
152
+ provider_id="openalex", metas=[r["_meta"]],
153
+ query={"source": "raw", "upstream": r["params"]},
154
+ )
155
+
156
+ def crossref_works(self, *, query=None, funder=None, award_number=None, rows=25,
157
+ offset=0, cursor=None, from_pub_date=None, to_pub_date=None,
158
+ execution=None, no_cache=None):
159
+ """research crossref_works: DOI registry paper search (/works verbatim name; query
160
+ full-text × funder (funder DOI→filter=funder:, the funder-output bridge) × award_number
161
+ (filter=award.number: combined with funder narrows exactly to the award number) ×
162
+ from/to_pub_date (publication window adopted in 0.2.9) combine with AND;
163
+ cursor=deep-pager is the proper deep-paging channel (adopted 0.2.9, "*" for the first
164
+ page/next_cursor to continue, mutually exclusive with offset — the offset channel is
165
+ the way around the 10,000 wall); awards live only inside rows and cannot be selected —
166
+ probe-confirmed)."""
167
+ self._research_only("crossref_works")
168
+ r = research.crossref.works_raw(query, funder=funder, award_number=award_number,
169
+ rows=rows, offset=offset, cursor=cursor,
170
+ from_pub_date=from_pub_date, to_pub_date=to_pub_date,
171
+ cache=self._c._cache,
172
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
173
+ result = {k: x for k, x in r.items() if k != "_meta"}
174
+ top = (result["rows"] or [None])[0]
175
+ cons = []
176
+ if query:
177
+ cons.append(f"full-text '{query}'")
178
+ if funder:
179
+ cons.append(f"funder {funder}")
180
+ if award_number:
181
+ cons.append(f"award {award_number}")
182
+ if from_pub_date or to_pub_date:
183
+ cons.append(f"pub window {from_pub_date or '…'}~{to_pub_date or '…'}")
184
+ return self._flat_env(
185
+ tool="crossref_works",
186
+ input={"query": query, "funder": funder, "award_number": award_number,
187
+ "rows": r["params"]["rows"],
188
+ "offset": r["params"].get("offset"), "cursor": cursor,
189
+ "from_pub_date": from_pub_date, "to_pub_date": to_pub_date},
190
+ reason=(f"Crossref: {' × '.join(cons) or 'hits'} — {result['total']:,} works in total, {result['count']} rows this page"
191
+ + (f", top hit {top['title'] or '—'} ({top['container_title'] or top['type']}, {top['year']})"
192
+ if top else ", zero hits")
193
+ + (", cursor continuation available" if result.get("next_cursor") else
194
+ (" (cursor last page)" if cursor else ""))),
195
+ result=result,
196
+ evidence=[{"quote": f"{w['title'] or '—'} ({w['container_title'] or w['type'] or '—'}, {w['year']}, "
197
+ + ("; ".join(f"{f['name']} award {'/'.join(f['award'][:2])}" for f in w["funders"])
198
+ if w["funders"] else "no funder block in row") + ")",
199
+ "url": w["url"] or f"https://doi.org/{w['doi']}"}
200
+ for w in result["rows"][:5]],
201
+ provider_id="crossref", metas=[r["_meta"]],
202
+ query={"source": "raw", "upstream": r["params"]},
203
+ )
204
+
205
+ def gb_gtr_projects(self, *, q=None, s=20, p=1, execution=None, no_cache=None):
206
+ """research gb_gtr_projects: UK UKRI funded-project search (/projects?q= verbatim three
207
+ params; s≥10 upstream 400 wall (probe showed a friendly error); upstream sends no
208
+ total — a full page signals more via has_more; the organisation edge is a second hop
209
+ in detail (links.link[] rel=LEAD_ORG), this face is the list only)."""
210
+ self._research_only("gb_gtr_projects")
211
+ r = research.gb_gtr.projects_raw(q, s=s, p=p,
212
+ cache=self._c._cache,
213
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
214
+ result = {k: x for k, x in r.items() if k != "_meta"}
215
+ top = (result["rows"] or [None])[0]
216
+ return self._flat_env(
217
+ tool="gb_gtr_projects",
218
+ input={"q": q, "s": r["params"]["s"], "p": r["params"]["p"]},
219
+ reason=(f"UKRI GtR: '{q}' — {result['count']} rows this page"
220
+ + (" (full page, likely more — continue with --p)" if result.get("has_more") else "")
221
+ + (f", top hit {top['title'] or '—'} ({top['status'] or '—'}, "
222
+ f"{top['start'] or '?'}→{top['end'] or '?'}, {top['lead_funder'] or '—'})"
223
+ if top else ", zero hits")),
224
+ result=result,
225
+ evidence=[{"quote": f"{x['title'] or '—'} ({x['status'] or '—'}, "
226
+ f"{x['start'] or '?'}→{x['end'] or '?'})",
227
+ "url": x["href"]}
228
+ for x in result["rows"][:5]],
229
+ provider_id="gb_gtr", metas=[r["_meta"]],
230
+ query={"source": "raw", "upstream": r["params"]},
231
+ )
232
+
233
+ def nih_reporter_publications(self, *, pmids=None, appl_ids=None, limit=25, offset=0,
234
+ execution=None, no_cache=None):
235
+ """research nih_reporter_publications: NIH funding↔paper bridge table
236
+ (/v2/publications/search POST; criteria accepts exactly two keys, pmids/appl_ids —
237
+ every other key is **silently ignored** upstream and the whole 8.3M table gets dumped
238
+ (probe-confirmed), hence the local wall = at least one of the two, and no other
239
+ criteria is passed through; rows = the 3-key bridge {coreproject,pmid,applid};
240
+ 0 rows = no linkage on record for that project (sparseness is a real answer; Yale
241
+ FY2018 probed with zero linked papers))."""
242
+ self._research_only("nih_reporter_publications")
243
+ r = research.nih_reporter.publications_raw(pmids, appl_ids=appl_ids,
244
+ limit=limit, offset=offset,
245
+ cache=self._c._cache,
246
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
247
+ result = {k: x for k, x in r.items() if k != "_meta"}
248
+ top = (result["rows"] or [None])[0]
249
+ crit = r["params"]["criteria"]
250
+ who = f"pmid {'/'.join(str(x) for x in crit['pmids'])}" if "pmids" in crit \
251
+ else f"appl {crit['appl_ids'][0]}" + (f" and {len(crit['appl_ids'])} items" if len(crit["appl_ids"]) > 1 else "")
252
+ return self._flat_env(
253
+ tool="nih_reporter_publications",
254
+ input={"pmids": r["params"]["criteria"].get("pmids"),
255
+ "appl_ids": r["params"]["criteria"].get("appl_ids"),
256
+ "limit": r["params"]["limit"], "offset": r["params"]["offset"]},
257
+ reason=(f"NIH RePORTER bridge: {who} — {result['total']} linked records in total, {result['count']} rows this page"
258
+ + (f", top hit applid {top['applid']} → pmid {top['pmid']} ({top['coreproject']})"
259
+ if top else " — 0 rows = no linkage on record for that project (sparse linkage is a real answer, not a failed query)")),
260
+ result=result,
261
+ evidence=[{"quote": f"applid {x['applid']} ↔ pmid {x['pmid']} ({x['coreproject']})",
262
+ "url": f"https://reporter.nih.gov/project-details/{x['applid']}"}
263
+ for x in result["rows"][:5]],
264
+ provider_id="nih-reporter", metas=[r["_meta"]],
265
+ query={"source": "raw", "upstream": r["params"]},
266
+ )
267
+
268
+ def icite_pubs(self, *, pmid=None, execution=None, no_cache=None):
269
+ """research icite_pubs: PubMed paper citation-impact card (/pubs?pmids= query-parameter
270
+ single record; RCR/FCR/clinical citations (is_clinical/cited_by_clin_count; the upstream
271
+ cited_by is a pmid array already reduced to a count) — the final impact hop after
272
+ nih_reporter_publications yields a pmid)."""
273
+ self._research_only("icite_pubs")
274
+ r = research.icite.pub_raw(pmid, cache=self._c._cache,
275
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
276
+ result = {k: x for k, x in r.items() if k != "_meta"}
277
+ cb = result["cited_by_count"]
278
+ cb_txt = f"{cb:,}" if isinstance(cb, int) else "—" # empty card (pmid not in iCite): None must not crash
279
+ rcr = result["relative_citation_ratio"]
280
+ return self._flat_env(
281
+ tool="icite_pubs",
282
+ input={"pmid": result["pmid"]},
283
+ reason=(f"iCite: pmid {result['pmid']} ({result['journal'] or '—'} {result['year'] or '—'}) "
284
+ f"cited {cb_txt}, RCR {rcr if rcr is not None else '—'}"
285
+ + (" (incl. clinical citations — cited_by_clin on record)" if result.get("cited_by_clin_count") else "")),
286
+ result=result,
287
+ evidence=[{"quote": f"{(result['title'] or '—')[:80]} (cited {cb_txt}, "
288
+ f"RCR {rcr if rcr is not None else '—'})",
289
+ "url": f"https://pubmed.ncbi.nlm.nih.gov/{result['pmid']}/"}],
290
+ provider_id="icite", metas=[r["_meta"]],
291
+ query={"source": "raw", "upstream": r["params"]},
292
+ )
293
+
294
+ def biorxiv_details(self, *, from_date=None, to_date=None, cursor=0, execution=None, no_cache=None):
295
+ """research biorxiv_details: bioRxiv preprint interval metadata stream
296
+ (/details/biorxiv/{from}/{to}/{cursor} verbatim name; authors is a semicolon-joined
297
+ string, jatsxml = a full-text XML link, not inline — the full-text bulk path goes
298
+ through jatsxml_url, this face is metadata only; the messages count strings are already
299
+ normalized)."""
300
+ self._research_only("biorxiv_details")
301
+ r = research.biorxiv.details_raw(from_date, to_date, cursor=cursor,
302
+ cache=self._c._cache,
303
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
304
+ result = {k: x for k, x in r.items() if k != "_meta"}
305
+ top = (result["rows"] or [None])[0]
306
+ return self._flat_env(
307
+ tool="biorxiv_details",
308
+ input={"from_date": from_date, "to_date": to_date, "cursor": r["params"]["cursor"]},
309
+ reason=(f"bioRxiv: {from_date}→{to_date} interval holds {result['total']:,} papers, "
310
+ f"cursor {r['params']['cursor']} page has {result['count']} rows"
311
+ + (" (a non-empty page may not be the end — continue with --cursor)" if result.get("has_more") else " (empty page = interval exhausted)")
312
+ + (f", top hit {top['title'] or '—'} ({top['category'] or '—'}, "
313
+ f"{top['author_corresponding_institution'] or '—'})" if top else ", zero hits")),
314
+ result=result,
315
+ evidence=[{"quote": f"{x['title'] or '—'} ({x['category'] or '—'}, {x['date']}, "
316
+ f"{x['author_corresponding_institution'] or '—'})",
317
+ "url": f"https://doi.org/{x['doi']}"}
318
+ for x in result["rows"][:5]],
319
+ provider_id="biorxiv", metas=[r["_meta"]],
320
+ query={"source": "raw", "upstream": {"from_date": from_date, "to_date": to_date,
321
+ "cursor": r["params"]["cursor"]}},
322
+ )
323
+
324
+ def opencitations_citations(self, *, doi=None, execution=None, no_cache=None):
325
+ """research opencitations_citations: open citation index COCI (/index/v1/citations/{doi}
326
+ — the full list for one DOI, verbatim name; the doi: prefix is auto-stripped (the
327
+ upstream 400 wall reports the format itself); the upstream limit parameter is silently
328
+ ignored (probe: ?limit=3 still returned all 217 rows) — no fake pagination, the full
329
+ list comes back)."""
330
+ self._research_only("opencitations_citations")
331
+ r = research.opencitations.citations_raw(doi, cache=self._c._cache,
332
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
333
+ result = {k: x for k, x in r.items() if k != "_meta"}
334
+ top = (result["rows"] or [None])[0]
335
+ return self._flat_env(
336
+ tool="opencitations_citations",
337
+ input={"doi": result["doi"]},
338
+ reason=(f"OpenCitations COCI: {result['doi']} cited {result['count']} times (full list; "
339
+ "the upstream limit is silently ignored, no fake pagination)"
340
+ + (f", latest {top['citing']} ({top['creation']})" if top else " — zero citations is a legitimate answer")),
341
+ result=result,
342
+ evidence=[{"quote": f"{x['citing']} → {x['cited']} ({x['creation']}, OCI {x['oci']})",
343
+ "url": f"https://doi.org/{x['citing']}"}
344
+ for x in result["rows"][:5]],
345
+ provider_id="opencitations", metas=[r["_meta"]],
346
+ query={"source": "raw", "upstream": r["params"]},
347
+ )
348
+
349
+ # ---------- 0.2.2 flat panel commands (security: OSV / CISA KEV) ----------
@@ -0,0 +1,58 @@
1
+ """gb_gtr — UKRI Gateway to Research API (UK public funding projects; keyless).
2
+
3
+ No key needed (gtr.ukri.org/gb_gtr/api; no stated quota — self-imposed polite 1 rps tier).
4
+ projects face (probed 2026-10-26): GET /projects?q=&s=&p=; s<10 gets an upstream 400
5
+ "Page size cannot be less than 10" (a friendly error, not a block — enforced locally as
6
+ s≥10); response shell {"project": [...]}.
7
+ The organisation edge is a second hop into detail: list rows carry only href; following it,
8
+ links.link[] entries with rel=LEAD_ORG/PM_PER hold the participating-institution/PI edges
9
+ (same two-hop shape as climatetrace) — this face is the list only; the second hop is left
10
+ to a future face.
11
+ """
12
+ provider = {
13
+ "id": "gb_gtr",
14
+ "baseUrl": "https://gtr.ukri.org/gb_gtr/api",
15
+ "rate": {"rps": 1, "burst": 5},
16
+ "ops": {
17
+ "projects": {"path": "/projects", "method": "GET", "idempotent": True},
18
+ },
19
+ "ttl": {"projects": 24 * 3600},
20
+ }
21
+
22
+ _SIZE_FLOOR = 10 # s<10 gets an upstream 400 "Page size cannot be less than 10" (probe 2026-10-26)
23
+
24
+
25
+ def _project_row(p):
26
+ return {
27
+ "id": p.get("id"), "title": p.get("title"), "status": p.get("status"),
28
+ "start": p.get("start"), "end": p.get("end"),
29
+ "grant_category": p.get("grantCategory"),
30
+ "lead_funder": p.get("leadFunder"),
31
+ "href": p.get("href"),
32
+ }
33
+
34
+
35
+ def projects_raw(q, *, s=20, p=1, cache=None, no_cache=False):
36
+ """→ {total, count, rows, has_more, params, _meta}; /projects?q= direct, verbatim name.
37
+ q required (local wall: browsing the whole corpus is not this face's use case); s≥10
38
+ (upstream 400 wall); p≥1; upstream sends no total — a full page signals more via
39
+ has_more (len(rows)==s, same pattern as nsf)."""
40
+ from ..transport import call_provider
41
+ from ..errors import SdkError
42
+
43
+ query = str(q or "").strip()
44
+ if not query:
45
+ raise SdkError("usage_error", provider="gb_gtr", detail="gb_gtr_projects requires q (search term)")
46
+ size = int(s)
47
+ if size < _SIZE_FLOOR:
48
+ raise SdkError("usage_error", provider="gb_gtr",
49
+ detail=f"s must be ≥{_SIZE_FLOOR} (upstream 400 wall: \"Page size cannot be less than 10\", probed 2026-10-26)")
50
+ page = int(p)
51
+ if page < 1:
52
+ raise SdkError("usage_error", provider="gb_gtr", detail="p must be ≥1")
53
+ params = {"q": query, "s": size, "p": page}
54
+ r = call_provider(provider, "projects", params, cache=cache, no_cache=no_cache)
55
+ data = r["data"] or {}
56
+ rows = [_project_row(x) for x in data.get("project") or []]
57
+ return {"total": None, "count": len(rows), "rows": rows,
58
+ "has_more": len(rows) == size, "params": params, "_meta": r}
@@ -0,0 +1,52 @@
1
+ """icite — NIH iCite API (PubMed paper citation-impact card; keyless).
2
+
3
+ No key needed (icite.od.nih.gov/api/pubs?pmids=; query-parameter style, re-probed 2026-10-26 —
4
+ api.ncbi.nlm.nih.gov/ihdb/pubs/v3/* is 404, do not trust the documented path). Probe (pmid
5
+ 26017442, "Deep learning", LeCun): RCR 737.5, citation_count 18,319, cited_by_clin clinical
6
+ citations present.
7
+ Two traps: `cited_by`/`cited_by_clin` are **pmid arrays** (the source of ten-thousand-element
8
+ envelope explosions) — reshaped down into counts (citation_count taken directly + len of
9
+ cited_by_clin); the count key is citation_count, not cited_by.
10
+ Single-record face (the impact hop after nih_reporter_publications hands you a pmid).
11
+ """
12
+ provider = {
13
+ "id": "icite",
14
+ "baseUrl": "https://icite.od.nih.gov/api",
15
+ "rate": {"rps": 2, "burst": 4}, # NCBI family tier (epmc/rxnav/ctgov/dailymed also 2/4)
16
+ "ops": {
17
+ "pubs": {"path": "/pubs", "method": "GET", "idempotent": True},
18
+ },
19
+ "ttl": {"pubs": 24 * 3600},
20
+ }
21
+
22
+
23
+ def pub_raw(pmid, *, cache=None, no_cache=False):
24
+ """→ {pmid, …impact card, params, _meta}; /pubs?pmids= direct, verbatim name (pmid required,
25
+ digits only, single-record card).
26
+ The pmid arrays of cited_by/cited_by_clin are reduced to counts (cited_by_count/
27
+ cited_by_clin_count) — ten-thousand-element arrays never enter the envelope."""
28
+ from ..transport import call_provider
29
+ from ..errors import SdkError
30
+
31
+ v = str(pmid or "").strip()
32
+ if not v:
33
+ raise SdkError("usage_error", provider="icite", detail="icite_pubs requires pmid (PubMed id)")
34
+ if not v.isdigit():
35
+ raise SdkError("usage_error", provider="icite", detail="pmid must be digits only (PubMed id like 26017442)")
36
+ r = call_provider(provider, "pubs", {"pmids": v}, cache=cache, no_cache=no_cache)
37
+ data = (r["data"] or {}).get("data") or []
38
+ row = data[0] if data else {}
39
+ return {"pmid": row.get("pmid") or int(v),
40
+ "doi": row.get("doi"), "title": row.get("title"),
41
+ "journal": row.get("journal"), "year": row.get("year"),
42
+ "authors": [a.get("fullName") for a in row.get("authors") or []][:20],
43
+ "relative_citation_ratio": row.get("relative_citation_ratio"),
44
+ "field_citation_rate": row.get("field_citation_rate"),
45
+ "expected_citations_per_year": row.get("expected_citations_per_year"),
46
+ "cited_by_count": row.get("citation_count"),
47
+ "cited_by_clin_count": len(row.get("cited_by_clin") or []),
48
+ "citations_per_year": row.get("citations_per_year"),
49
+ "is_clinical": row.get("is_clinical"),
50
+ "is_research_article": row.get("is_research_article"),
51
+ "nih_percentile": row.get("nih_percentile"),
52
+ "params": {"pmids": v}, "_meta": r}
@@ -0,0 +1,143 @@
1
+ """nih_reporter — NIH RePORTER API v2 (US NIH funded projects and the funding↔paper bridge; keyless).
2
+
3
+ No key needed, POST search faces (probed 2026-10-26). The projects face has two shape axes:
4
+ criteria only accepts snake_case (org_names/fiscal_years), include_fields only accepts
5
+ camelCase (ApplId/ProjectTitle/…) — a wrong shape is **silently ignored** (no error), so
6
+ both faces hard-code their fields instead of passing bare params through. The org_names array
7
+ semantics = exact match (not fuzzy — re-measured 0.2.9: the abbreviation "NHLBI"→0 rows, the
8
+ full name "NATIONAL HEART, LUNG, AND BLOOD INSTITUTE"→173 rows for FY2023); the fiscal_years
9
+ array is a set of years. Deep-paging wall: upstream rejects offset+limit > 15,000 (probed);
10
+ limit 1-500 is a real documented cap (0.2.9 re-counted limit=500 → all 500 rows returned,
11
+ total 85,133 — no silent clamping). The 0.2.9 audit did not adopt two keys (probed 2026-10-28):
12
+ core_project_nums is the key of the **publications face** (per the official swagger
13
+ classification); sending it on the projects face is silently ignored (total=2,983,713 = the
14
+ whole table dumped); advanced_text_search is documented but **behaviourally dead** — all three
15
+ inner shapes (search_text/searchtext/all three keys operator+searchfield+searchtext) are
16
+ silently ignored both alone and combined (the combined send gives total=173, identical to a
17
+ bare org+fy send, with no keyword filtering on the title) — a documented key is not a live key,
18
+ do not adopt it.
19
+ publications = the funding↔paper bridge table (added to the research domain in 0.2.4):
20
+ criteria accepts exactly two keys, pmids/appl_ids.
21
+ """
22
+ provider = {
23
+ "id": "nih-reporter",
24
+ "baseUrl": "https://api.reporter.nih.gov",
25
+ # The official "no more than one URL request per second" = spacing semantics (interval ≥1s
26
+ # between requests): burst=1+rps=1 is exactly compliant; the old burst=5 bucket fired 5
27
+ # requests in the first instant and broke the rule (compliance fix; rate-limits.md §7
28
+ # exception log). Declared at provider level: both faces (projects/publications) obey it.
29
+ "rate": {"rps": 1, "burst": 1},
30
+ "ops": {
31
+ "projectsSearch": {"path": "/v2/projects/search", "method": "POST", "idempotent": True},
32
+ "publicationsSearch": {"path": "/v2/publications/search", "method": "POST", "idempotent": True},
33
+ },
34
+ "ttl": {"projectsSearch": 24 * 3600, "publicationsSearch": 24 * 3600},
35
+ }
36
+
37
+ _OFFSET_WALL = 15000 # upstream rejects offset+limit above this (probed 2026-10)
38
+
39
+
40
+ def _project_row(p):
41
+ org = p.get("organization") or {}
42
+ return {
43
+ "appl_id": p.get("appl_id"), "fiscal_year": p.get("fiscal_year"),
44
+ "project_title": p.get("project_title"),
45
+ "org_name": org.get("org_name"), "org_city": org.get("org_city"),
46
+ "org_state": org.get("org_state"), "org_country": org.get("org_country"),
47
+ "org_uei": org.get("primary_uei"), "org_duns": org.get("primary_duns"),
48
+ "pis": [{"full_name": x.get("full_name"), "is_contact_pi": x.get("is_contact_pi"),
49
+ "profile_id": x.get("profile_id")}
50
+ for x in p.get("principal_investigators") or []],
51
+ "award_amount": p.get("award_amount"),
52
+ "abstract_text": p.get("abstract_text"),
53
+ }
54
+
55
+
56
+ def projects_raw(org_name=None, *, fiscal_year=None, include_fields=None, limit=25, offset=0,
57
+ cache=None, no_cache=False):
58
+ """→ {total, count, rows, search_url, params, _meta}; /v2/projects/search POST direct, verbatim name.
59
+ At least one of org_name/fiscal_year (local safety wall: a bare scan of the ~4M-project
60
+ corpus is served by the bulk pipeline); a scalar org_name is auto-wrapped into the array
61
+ (org_names array semantics upstream); fiscal_year→fiscal_years; include_fields is a
62
+ camelCase whitelist passed through (a wrong shape is silently ignored upstream — the face
63
+ does not validate spelling, a wrong name just returns more fields); limit 1-500 (upstream API
64
+ doc); offset+limit ≤15,000 (upstream deep-paging wall)."""
65
+ from ..transport import call_provider
66
+ from ..errors import SdkError
67
+
68
+ nm = str(org_name or "").strip() or None
69
+ fy = int(fiscal_year) if fiscal_year not in (None, "") else None
70
+ if not nm and fy is None:
71
+ raise SdkError("usage_error", provider="nih-reporter",
72
+ detail="nih_reporter_projects requires at least one of org_name or fiscal_year"
73
+ " (local safety wall: a bare whole-corpus scan is served by the bulk pipeline)")
74
+ lim = int(limit)
75
+ if not 1 <= lim <= 500:
76
+ raise SdkError("usage_error", provider="nih-reporter",
77
+ detail="limit must be 1-500 (upstream API doc page-size cap)")
78
+ off = int(offset)
79
+ if off < 0:
80
+ raise SdkError("usage_error", provider="nih-reporter", detail="offset must be ≥0")
81
+ if off + lim > _OFFSET_WALL:
82
+ raise SdkError("usage_error", provider="nih-reporter",
83
+ detail=f"offset+limit must be ≤{_OFFSET_WALL:,} (upstream deep-paging wall, confirmed by probing 2026-10)")
84
+ criteria = {}
85
+ if nm:
86
+ criteria["org_names"] = [nm] # upstream array semantics = exact match (not a fuzzy search)
87
+ if fy is not None:
88
+ criteria["fiscal_years"] = [fy]
89
+ body = {"criteria": criteria, "offset": off, "limit": lim}
90
+ if include_fields:
91
+ body["include_fields"] = [str(x).strip() for x in include_fields if str(x).strip()]
92
+ r = call_provider(provider, "projectsSearch", body, cache=cache, no_cache=no_cache)
93
+ data = r["data"] or {}
94
+ meta = data.get("meta") or {}
95
+ rows = [_project_row(p) for p in data.get("results") or []]
96
+ return {"total": meta.get("total"), "count": len(rows), "rows": rows,
97
+ "search_url": (meta.get("properties") or {}).get("URL"),
98
+ "params": body, "_meta": r}
99
+
100
+
101
+ _PUB_CRITERIA = ("pmids", "appl_ids") # the only two recognised filter keys (probed 2026-10-26: every other key is silently ignored → 8.3M-row dump)
102
+
103
+
104
+ def publications_raw(pmids=None, *, appl_ids=None, limit=25, offset=0,
105
+ cache=None, no_cache=False):
106
+ """→ {total, count, rows, params, _meta}; /v2/publications/search POST direct, verbatim name.
107
+ At least one of pmids/appl_ids (local safety wall — with both keys empty upstream dumps the
108
+ whole 8.3M table); the criteria key set is closed and only these two keys are sent:
109
+ fiscal_years/coreprojectnums/org_names etc. are all **silently ignored** upstream (no error,
110
+ no filtering, probed 2026-10-26), so this face passes no other criteria through;
111
+ rows = the 3-key bridge {coreproject, pmid, applid} (funding↔paper bridge table); sparse
112
+ linkage is a real answer (a Yale FY2018 project measured zero linked papers) — 0 rows means
113
+ no linkage on record for that project, not a failed query.
114
+ limit 1-500, offset same convention as the projects face."""
115
+ from ..transport import call_provider
116
+ from ..errors import SdkError
117
+
118
+ pm = [int(x) for x in (pmids or []) if str(x).strip()] if pmids else []
119
+ ap = [int(x) for x in (appl_ids or []) if str(x).strip()] if appl_ids else []
120
+ if not pm and not ap:
121
+ raise SdkError("usage_error", provider="nih-reporter",
122
+ detail="nih_reporter_publications requires at least one of pmids or appl_ids"
123
+ " (local safety wall: both keys empty = upstream dumps the whole 8.3M table)")
124
+ lim = int(limit)
125
+ if not 1 <= lim <= 500:
126
+ raise SdkError("usage_error", provider="nih-reporter",
127
+ detail="limit must be 1-500 (upstream API doc page-size cap)")
128
+ off = int(offset)
129
+ if off < 0:
130
+ raise SdkError("usage_error", provider="nih-reporter", detail="offset must be ≥0")
131
+ criteria = {}
132
+ if pm:
133
+ criteria["pmids"] = pm
134
+ if ap:
135
+ criteria["appl_ids"] = ap
136
+ body = {"criteria": criteria, "offset": off, "limit": lim}
137
+ r = call_provider(provider, "publicationsSearch", body, cache=cache, no_cache=no_cache)
138
+ data = r["data"] or {}
139
+ meta = data.get("meta") or {}
140
+ rows = [{"coreproject": x.get("coreproject"), "pmid": x.get("pmid"), "applid": x.get("applid")}
141
+ for x in data.get("results") or []]
142
+ return {"total": meta.get("total"), "count": len(rows), "rows": rows,
143
+ "params": body, "_meta": r}