Skip to content

Commit f033481

Browse files
authored
feat: 2 read adapters (wttr, openfda) + contract tests (#1355)
* feat: 2 read adapters across 2 new sites + contract tests (wttr, openfda) Trimmed from original Round 11 per WAWQAQ feedback (msg=3899a382): drop novelty/niche sites (timeapi / zippopotam / spacedevs / citybik) — keep only sites with clear real-world utility: - wttr (current, forecast) — wttr.in weather, no auth, simple text/json toggle - openfda (drug-label, food-recall) — FDA drug labels + food recall enforcement 13 contract tests across 2 sites cover Lucene operator query construction (openfda +AND+ literal handling), [string] 1-elem array unwrap, brand-OR- generic match, wttr [{value:"..."}] array-of-objects 1-elem unwrap. Manifest 757→759 (+2). Audits clean: typed-error-lint=196 baseline. * fix(openfda): use brand or generic label search
1 parent 39943c0 commit f033481

12 files changed

Lines changed: 905 additions & 0 deletions

File tree

cli-manifest.json

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16236,6 +16236,107 @@
1623616236
"modulePath": "openalex/work.js",
1623716237
"sourceFile": "openalex/work.js"
1623816238
},
16239+
{
16240+
"site": "openfda",
16241+
"name": "drug-label",
16242+
"description": "Search FDA-approved drug labels (brand or generic name)",
16243+
"access": "read",
16244+
"domain": "fda.gov",
16245+
"strategy": "public",
16246+
"browser": false,
16247+
"args": [
16248+
{
16249+
"name": "query",
16250+
"type": "str",
16251+
"required": true,
16252+
"positional": true,
16253+
"help": "Brand or generic drug name (e.g. \"aspirin\", \"lisinopril\")"
16254+
},
16255+
{
16256+
"name": "limit",
16257+
"type": "int",
16258+
"default": 5,
16259+
"required": false,
16260+
"help": "Max rows (1-25, default 5; openFDA caps anonymous tier at 25/page)"
16261+
}
16262+
],
16263+
"columns": [
16264+
"rank",
16265+
"id",
16266+
"brandName",
16267+
"genericName",
16268+
"manufacturer",
16269+
"productType",
16270+
"route",
16271+
"productNdc",
16272+
"pharmClass",
16273+
"purpose",
16274+
"indications",
16275+
"warnings",
16276+
"dosage",
16277+
"effectiveTime"
16278+
],
16279+
"type": "js",
16280+
"modulePath": "openfda/drug-label.js",
16281+
"sourceFile": "openfda/drug-label.js"
16282+
},
16283+
{
16284+
"site": "openfda",
16285+
"name": "food-recall",
16286+
"description": "FDA food recall and enforcement actions (most recent first)",
16287+
"access": "read",
16288+
"domain": "fda.gov",
16289+
"strategy": "public",
16290+
"browser": false,
16291+
"args": [
16292+
{
16293+
"name": "query",
16294+
"type": "str",
16295+
"required": false,
16296+
"help": "Free-text Lucene query (e.g. \"salmonella\", \"listeria\"); default: all recent recalls"
16297+
},
16298+
{
16299+
"name": "status",
16300+
"type": "str",
16301+
"required": false,
16302+
"help": "Filter by status: \"Ongoing\", \"Completed\", \"Terminated\""
16303+
},
16304+
{
16305+
"name": "classification",
16306+
"type": "str",
16307+
"required": false,
16308+
"help": "Filter by class: \"Class I\" (most serious), \"Class II\", \"Class III\""
16309+
},
16310+
{
16311+
"name": "limit",
16312+
"type": "int",
16313+
"default": 10,
16314+
"required": false,
16315+
"help": "Max rows (1-100, default 10; openFDA caps anonymous tier at 100/page)"
16316+
}
16317+
],
16318+
"columns": [
16319+
"rank",
16320+
"recallNumber",
16321+
"status",
16322+
"classification",
16323+
"voluntary",
16324+
"recallingFirm",
16325+
"city",
16326+
"state",
16327+
"country",
16328+
"productDescription",
16329+
"reasonForRecall",
16330+
"productQuantity",
16331+
"distributionPattern",
16332+
"reportDate",
16333+
"recallInitiationDate",
16334+
"terminationDate"
16335+
],
16336+
"type": "js",
16337+
"modulePath": "openfda/food-recall.js",
16338+
"sourceFile": "openfda/food-recall.js"
16339+
},
1623916340
{
1624016341
"site": "openreview",
1624116342
"name": "paper",
@@ -23521,6 +23622,93 @@
2352123622
"modulePath": "wikipedia/trending.js",
2352223623
"sourceFile": "wikipedia/trending.js"
2352323624
},
23625+
{
23626+
"site": "wttr",
23627+
"name": "current",
23628+
"description": "Current weather conditions for a location (city, lat,lon, or airport code)",
23629+
"access": "read",
23630+
"domain": "wttr.in",
23631+
"strategy": "public",
23632+
"browser": false,
23633+
"args": [
23634+
{
23635+
"name": "location",
23636+
"type": "str",
23637+
"required": true,
23638+
"positional": true,
23639+
"help": "City name, \"lat,lon\", airport ICAO code, or \"@domain\""
23640+
}
23641+
],
23642+
"columns": [
23643+
"location",
23644+
"region",
23645+
"country",
23646+
"latitude",
23647+
"longitude",
23648+
"observedAt",
23649+
"tempC",
23650+
"tempF",
23651+
"feelsLikeC",
23652+
"feelsLikeF",
23653+
"description",
23654+
"humidity",
23655+
"cloudCover",
23656+
"pressure",
23657+
"precipMm",
23658+
"visibilityKm",
23659+
"uvIndex",
23660+
"windKmph",
23661+
"windDirection",
23662+
"windDirectionDegree"
23663+
],
23664+
"type": "js",
23665+
"modulePath": "wttr/current.js",
23666+
"sourceFile": "wttr/current.js"
23667+
},
23668+
{
23669+
"site": "wttr",
23670+
"name": "forecast",
23671+
"description": "Multi-day weather forecast (up to 3 days, wttr.in free tier max)",
23672+
"access": "read",
23673+
"domain": "wttr.in",
23674+
"strategy": "public",
23675+
"browser": false,
23676+
"args": [
23677+
{
23678+
"name": "location",
23679+
"type": "str",
23680+
"required": true,
23681+
"positional": true,
23682+
"help": "City name, \"lat,lon\", airport ICAO code, or \"@domain\""
23683+
},
23684+
{
23685+
"name": "days",
23686+
"type": "int",
23687+
"default": 3,
23688+
"required": false,
23689+
"help": "Max forecast days (1-3, wttr.in caps the response at 3 days)"
23690+
}
23691+
],
23692+
"columns": [
23693+
"rank",
23694+
"date",
23695+
"minTempC",
23696+
"maxTempC",
23697+
"avgTempC",
23698+
"minTempF",
23699+
"maxTempF",
23700+
"avgTempF",
23701+
"sunHour",
23702+
"totalSnowCm",
23703+
"uvIndex",
23704+
"description",
23705+
"sunrise",
23706+
"sunset"
23707+
],
23708+
"type": "js",
23709+
"modulePath": "wttr/forecast.js",
23710+
"sourceFile": "wttr/forecast.js"
23711+
},
2352423712
{
2352523713
"site": "xianyu",
2352623714
"name": "chat",

clis/openfda/drug-label.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// openfda drug-label — FDA-approved drug label search.
2+
//
3+
// Endpoint: GET /drug/label.json?search=<lucene>&limit=<n>
4+
// Default search field is brand_name OR generic_name (Lucene syntax via openfda
5+
// query DSL). Returns label sections (purpose, warnings, dosage, etc.) and
6+
// metadata (manufacturer, product_ndc, route, etc.).
7+
import { cli, Strategy } from '@jackwener/opencli/registry';
8+
import { EmptyResultError } from '@jackwener/opencli/errors';
9+
import {
10+
OPENFDA_BASE,
11+
firstOrNull,
12+
joinOrNull,
13+
openfdaFetch,
14+
requireBoundedInt,
15+
requireString,
16+
} from './utils.js';
17+
18+
cli({
19+
site: 'openfda',
20+
name: 'drug-label',
21+
access: 'read',
22+
description: 'Search FDA-approved drug labels (brand or generic name)',
23+
domain: 'fda.gov',
24+
strategy: Strategy.PUBLIC,
25+
browser: false,
26+
args: [
27+
{ name: 'query', positional: true, required: true, help: 'Brand or generic drug name (e.g. "aspirin", "lisinopril")' },
28+
{ name: 'limit', type: 'int', default: 5, help: 'Max rows (1-25, default 5; openFDA caps anonymous tier at 25/page)' },
29+
],
30+
columns: [
31+
'rank', 'id', 'brandName', 'genericName', 'manufacturer',
32+
'productType', 'route', 'productNdc', 'pharmClass',
33+
'purpose', 'indications', 'warnings', 'dosage', 'effectiveTime',
34+
],
35+
func: async (args) => {
36+
const query = requireString(args.query, 'query');
37+
const limit = requireBoundedInt(args.limit, 5, 25);
38+
const brand = `openfda.brand_name:"${query}"`;
39+
const generic = `openfda.generic_name:"${query}"`;
40+
// URLSearchParams encodes spaces/operators in ways openFDA's Lucene
41+
// parser handles poorly. Keep the OR literal visible and encode only
42+
// each clause, matching food-recall's manual +AND+ handling.
43+
const search = `${encodeURIComponent(brand)}+OR+${encodeURIComponent(generic)}`;
44+
const url = `${OPENFDA_BASE}/drug/label.json?search=${search}&limit=${limit}`;
45+
const body = await openfdaFetch(url, 'openfda drug-label');
46+
const list = Array.isArray(body?.results) ? body.results : [];
47+
if (!list.length) {
48+
throw new EmptyResultError('openfda drug-label', `openFDA returned no labels matching "${query}".`);
49+
}
50+
return list.map((r, i) => {
51+
const o = r?.openfda ?? {};
52+
// pharm_class fields: epc (established pharmacologic class) is the
53+
// most user-meaningful — fall back through moa/cs/pe in that order.
54+
const pharmClass = firstOrNull(o.pharm_class_epc) ?? firstOrNull(o.pharm_class_moa)
55+
?? firstOrNull(o.pharm_class_cs) ?? firstOrNull(o.pharm_class_pe);
56+
return {
57+
rank: i + 1,
58+
id: r?.id ?? null,
59+
brandName: firstOrNull(o.brand_name),
60+
genericName: firstOrNull(o.generic_name),
61+
manufacturer: firstOrNull(o.manufacturer_name),
62+
productType: firstOrNull(o.product_type),
63+
route: joinOrNull(o.route),
64+
productNdc: firstOrNull(o.product_ndc),
65+
pharmClass,
66+
purpose: firstOrNull(r.purpose),
67+
indications: firstOrNull(r.indications_and_usage),
68+
warnings: firstOrNull(r.warnings),
69+
dosage: firstOrNull(r.dosage_and_administration),
70+
effectiveTime: r?.effective_time ?? null,
71+
};
72+
});
73+
},
74+
});

clis/openfda/food-recall.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// openfda food-recall — FDA food enforcement (recalls, market withdrawals, alerts).
2+
//
3+
// Endpoint: GET /food/enforcement.json?search=<lucene>&limit=<n>
4+
// Sorted by report_date descending (most recent first).
5+
import { cli, Strategy } from '@jackwener/opencli/registry';
6+
import { EmptyResultError } from '@jackwener/opencli/errors';
7+
import { OPENFDA_BASE, openfdaFetch, requireBoundedInt } from './utils.js';
8+
9+
cli({
10+
site: 'openfda',
11+
name: 'food-recall',
12+
access: 'read',
13+
description: 'FDA food recall and enforcement actions (most recent first)',
14+
domain: 'fda.gov',
15+
strategy: Strategy.PUBLIC,
16+
browser: false,
17+
args: [
18+
{ name: 'query', help: 'Free-text Lucene query (e.g. "salmonella", "listeria"); default: all recent recalls' },
19+
{ name: 'status', help: 'Filter by status: "Ongoing", "Completed", "Terminated"' },
20+
{ name: 'classification', help: 'Filter by class: "Class I" (most serious), "Class II", "Class III"' },
21+
{ name: 'limit', type: 'int', default: 10, help: 'Max rows (1-100, default 10; openFDA caps anonymous tier at 100/page)' },
22+
],
23+
columns: [
24+
'rank', 'recallNumber', 'status', 'classification', 'voluntary',
25+
'recallingFirm', 'city', 'state', 'country',
26+
'productDescription', 'reasonForRecall', 'productQuantity',
27+
'distributionPattern', 'reportDate', 'recallInitiationDate', 'terminationDate',
28+
],
29+
func: async (args) => {
30+
const limit = requireBoundedInt(args.limit, 10, 100);
31+
const filters = [];
32+
if (args.query) filters.push(String(args.query).trim());
33+
if (args.status) filters.push(`status:"${String(args.status).trim()}"`);
34+
if (args.classification) filters.push(`classification:"${String(args.classification).trim()}"`);
35+
// URLSearchParams percent-encodes the `+AND+` separator that openFDA's
36+
// Lucene parser treats specially, so build the query string by hand.
37+
const qs = filters.length
38+
? `search=${filters.map(f => encodeURIComponent(f)).join('+AND+')}&limit=${limit}`
39+
: `limit=${limit}`;
40+
const url = `${OPENFDA_BASE}/food/enforcement.json?${qs}`;
41+
const body = await openfdaFetch(url, 'openfda food-recall');
42+
const list = Array.isArray(body?.results) ? body.results : [];
43+
if (!list.length) {
44+
throw new EmptyResultError('openfda food-recall', 'openFDA returned no food recall records matching the filter.');
45+
}
46+
return list.map((r, i) => ({
47+
rank: i + 1,
48+
recallNumber: r?.recall_number ?? null,
49+
status: r?.status ?? null,
50+
classification: r?.classification ?? null,
51+
voluntary: r?.voluntary_mandated ?? null,
52+
recallingFirm: r?.recalling_firm ?? null,
53+
city: r?.city ?? null,
54+
state: r?.state ?? null,
55+
country: r?.country ?? null,
56+
productDescription: r?.product_description ?? null,
57+
reasonForRecall: r?.reason_for_recall ?? null,
58+
productQuantity: r?.product_quantity ?? null,
59+
distributionPattern: r?.distribution_pattern ?? null,
60+
reportDate: r?.report_date ?? null,
61+
recallInitiationDate: r?.recall_initiation_date ?? null,
62+
terminationDate: r?.termination_date ?? null,
63+
}));
64+
},
65+
});

0 commit comments

Comments
 (0)