gangtise-openapi-cli 0.50.0 → 0.52.0
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.
- package/README.md +94 -49
- package/dist/src/cli.js +29 -8
- package/dist/src/commands/ai.js +27 -12
- package/dist/src/commands/alternative.js +2 -2
- package/dist/src/commands/bond.js +4 -4
- package/dist/src/commands/fund.js +1 -1
- package/dist/src/commands/fundamental.js +11 -11
- package/dist/src/commands/indicator.js +5 -5
- package/dist/src/commands/insight.js +38 -17
- package/dist/src/commands/quote.js +6 -6
- package/dist/src/commands/raw.js +3 -3
- package/dist/src/commands/reference.js +35 -3
- package/dist/src/commands/shared.js +20 -8
- package/dist/src/commands/tool.js +9 -4
- package/dist/src/commands/update.js +11 -0
- package/dist/src/commands/vault.js +17 -16
- package/dist/src/core/client.js +21 -4
- package/dist/src/core/download.js +12 -3
- package/dist/src/core/driveUpload.js +6 -2
- package/dist/src/core/dryRun.js +68 -0
- package/dist/src/core/endpoints.js +87 -6
- package/dist/src/core/errors.js +2 -2
- package/dist/src/core/etfComponents.js +65 -8
- package/dist/src/core/exitStatus.js +4 -0
- package/dist/src/core/fileParse.js +10 -3
- package/dist/src/core/printer.js +39 -7
- package/dist/src/core/statePaths.js +41 -0
- package/dist/src/core/transport.js +7 -0
- package/gangtise-openapi/SKILL.md +43 -156
- package/gangtise-openapi/references/billing.md +142 -0
- package/gangtise-openapi/references/commands/ai.md +26 -0
- package/gangtise-openapi/references/commands/fund.md +3 -3
- package/gangtise-openapi/references/commands/fundamental.md +2 -1
- package/gangtise-openapi/references/commands/indicator.md +106 -15
- package/gangtise-openapi/references/commands/insight.md +31 -6
- package/gangtise-openapi/references/commands/reference-and-lookup.md +42 -4
- package/gangtise-openapi/references/commands/tool.md +2 -2
- package/gangtise-openapi/references/commands/vault.md +26 -4
- package/gangtise-openapi/references/errors.md +3 -3
- package/gangtise-openapi/references/examples.md +3 -3
- package/gangtise-openapi/references/lookup-ids.md +13 -0
- package/gangtise-openapi/references/response-schema.md +8 -2
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { Command, Option } from "commander";
|
|
|
2
2
|
import { collectList, collectNames, dateArg, maybeArray, parseNumberOption } from "../core/args.js";
|
|
3
3
|
import { maxUnitsOf } from "../core/endpoints.js";
|
|
4
4
|
import { ValidationError } from "../core/errors.js";
|
|
5
|
-
import { emit, noteLateStart } from "./shared.js";
|
|
5
|
+
import { emit, noteLateStart, OUTPUT_HELP } from "./shared.js";
|
|
6
6
|
const BOND_LATE_CAUSE = "The bond may have listed later, or this data starts later than the requested range — the endpoint returns from its first day of data without an error.";
|
|
7
7
|
export const bond = new Command("bond").description("Bond APIs: profiles, issuers, quotes, valuations, cash flows, ratings, announcements");
|
|
8
8
|
function requireBondCodes(codes, max) {
|
|
@@ -16,7 +16,7 @@ function requireBondCodes(codes, max) {
|
|
|
16
16
|
const bondSecurityOption = (command) => command
|
|
17
17
|
.option("--security <code>", "Bond code, e.g. 019742.SH / 123456.SZ / 220205.IB (repeatable). Short names and pinyin are rejected — resolve them with 'reference securities-search --category bond' first", collectList, [])
|
|
18
18
|
.option("--field <field>", "Field to return (repeatable); omit for all. An unsupported name rejects the whole call with 100003", collectList, [])
|
|
19
|
-
.option("--format <format>", "Output format", "table").option("--output <path>");
|
|
19
|
+
.option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP);
|
|
20
20
|
const addBondRange = (name, endpointKey, describe, rangeHelp, max) => bondSecurityOption(bond.command(name).description(describe))
|
|
21
21
|
.option("--start-date <date>", `${rangeHelp} start (yyyy-MM-dd); omit for all history`, dateArg("--start-date"))
|
|
22
22
|
.option("--end-date <date>", `${rangeHelp} end (yyyy-MM-dd); omit for all history`, dateArg("--end-date"))
|
|
@@ -64,7 +64,7 @@ bond.command("issuance-plan").description("Rate-bond issuance calendar over a da
|
|
|
64
64
|
.requiredOption("--start-date <date>", "Issue date range start (yyyy-MM-dd)", dateArg("--start-date"))
|
|
65
65
|
.requiredOption("--end-date <date>", "Issue date range end (yyyy-MM-dd)", dateArg("--end-date"))
|
|
66
66
|
.option("--field <field>", "Field to return (repeatable); omit for all", collectList, [])
|
|
67
|
-
.option("--format <format>", "Output format", "table").option("--output <path>")
|
|
67
|
+
.option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP)
|
|
68
68
|
.action((options) => emit(options, (client) => client.call("bond.issuance-plan", { startDate: options.startDate, endDate: options.endDate, fieldList: maybeArray(options.field) })));
|
|
69
69
|
bondSecurityOption(bond.command("announcement").description("Bond announcements, paged. Filter by bond codes OR by announcement date range, never both"))
|
|
70
70
|
.option("--start-date <date>", "Announcement date range start (yyyy-MM-dd); mutually exclusive with --security", dateArg("--start-date"))
|
|
@@ -90,7 +90,7 @@ bond.command("default-list").description("Defaulted bonds: by default-date range
|
|
|
90
90
|
.option("--security <code>", "Bond code, e.g. 169687.SH / 220205.IB (repeatable, no per-call limit)", collectList, [])
|
|
91
91
|
.option("--issuer <name>", "One issuer, by full name. A short name is matched to a single closest issuer, which may not be the one meant — check issuerName in the result", collectNames, [])
|
|
92
92
|
.option("--field <field>", "Field to return (repeatable); omit for all. An unsupported name rejects the whole call with 100005", collectList, [])
|
|
93
|
-
.option("--format <format>", "Output format", "table").option("--output <path>")
|
|
93
|
+
.option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP)
|
|
94
94
|
.action((options) => {
|
|
95
95
|
const byDate = Boolean(options.startDate || options.endDate);
|
|
96
96
|
const modes = [byDate, options.security.length > 0, options.issuer.length > 0].filter(Boolean).length;
|
|
@@ -3,7 +3,7 @@ import { collectNames } from "../core/args.js";
|
|
|
3
3
|
import { addComponentCodes, clientLookup } from "../core/etfComponents.js";
|
|
4
4
|
import { date, flag, format, list, output, query, requiredList, value } from "./shared.js";
|
|
5
5
|
export const fund = new Command("fund").description("Fund APIs: profiles, NAV, fees, managers, scale, holders, holdings and allocation, ETF creation/redemption");
|
|
6
|
-
const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted). From a fund name: 'reference securities-search --category fund',
|
|
6
|
+
const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted). From a fund name: 'reference securities-search --category fund', which returns funds only", "fundCodeList");
|
|
7
7
|
const range = (what) => [
|
|
8
8
|
date("--start-date <date>", `${what} range start (yyyy-MM-dd); omit both ends for everything in your account's history window`, "startDate"),
|
|
9
9
|
date("--end-date <date>", `${what} range end (yyyy-MM-dd)`, "endDate"),
|
|
@@ -4,16 +4,16 @@ import { COSTLY_FETCH_CREDITS } from "../core/endpoints.js";
|
|
|
4
4
|
import { normalizeRows } from "../core/normalize.js";
|
|
5
5
|
import { parseOutputFormat } from "../core/output.js";
|
|
6
6
|
import { printData } from "../core/printer.js";
|
|
7
|
-
import { emit, withClient, flagIfLimitTruncated, field,
|
|
7
|
+
import { emit, withClient, flagIfLimitTruncated, field, date, list, format, output, query, OUTPUT_HELP, SECURITY_CODE_FLAGS, SECURITY_CODE_HELP, oneSecurityCode, securityCode } from "./shared.js";
|
|
8
8
|
export const fundamental = new Command("fundamental").description("Fundamental APIs");
|
|
9
|
-
const addFinancialReport = (name, endpointKey, periodHelp = "Period") => query(fundamental, name, {
|
|
9
|
+
const addFinancialReport = (name, endpointKey, periodHelp = "Period: q1/interim/q3/annual/latest") => query(fundamental, name, {
|
|
10
10
|
endpoint: endpointKey,
|
|
11
11
|
fields: [
|
|
12
|
-
|
|
12
|
+
securityCode(),
|
|
13
13
|
date("--start-date <date>", "Start date (yyyy-MM-dd)", "startDate"),
|
|
14
14
|
date("--end-date <date>", "End date (yyyy-MM-dd)", "endDate"),
|
|
15
|
-
list("--fiscal-year <year>", "Fiscal year", "fiscalYear"),
|
|
16
|
-
list("--period <period>", periodHelp
|
|
15
|
+
list("--fiscal-year <year>", "Fiscal year; pass --period with it, or the latest report comes back whatever the year", "fiscalYear"),
|
|
16
|
+
list("--period <period>", `${periodHelp} (server default: latest)`, "period"),
|
|
17
17
|
list("--report-type <type>", "Report type", "reportType"),
|
|
18
18
|
list("--field <field>", "Field", "fieldList"),
|
|
19
19
|
format(), output(),
|
|
@@ -33,7 +33,7 @@ addFinancialReport("cash-flow-us", "fundamental.cash-flow-us", "Period: q1/h1/q3
|
|
|
33
33
|
query(fundamental, "main-business", {
|
|
34
34
|
endpoint: "fundamental.main-business",
|
|
35
35
|
fields: [
|
|
36
|
-
|
|
36
|
+
securityCode(),
|
|
37
37
|
date("--start-date <date>", "Start date (yyyy-MM-dd)", "startDate"),
|
|
38
38
|
date("--end-date <date>", "End date (yyyy-MM-dd)", "endDate"),
|
|
39
39
|
field(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product"), (v) => ({ breakdown: v })),
|
|
@@ -62,7 +62,7 @@ function firstRowTradeDate(data) {
|
|
|
62
62
|
return typeof value === "string" ? value : undefined;
|
|
63
63
|
}
|
|
64
64
|
fundamental.command("valuation-analysis")
|
|
65
|
-
.requiredOption(
|
|
65
|
+
.requiredOption(SECURITY_CODE_FLAGS, SECURITY_CODE_HELP, oneSecurityCode)
|
|
66
66
|
.addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory())
|
|
67
67
|
.option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date"))
|
|
68
68
|
.option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date"))
|
|
@@ -70,7 +70,7 @@ fundamental.command("valuation-analysis")
|
|
|
70
70
|
.option("--field <field>", "Field", collectList, [])
|
|
71
71
|
.option("--skip-null", "Drop rows where value or percentileRank is null")
|
|
72
72
|
.option("--format <format>", "Output format", "table")
|
|
73
|
-
.option("--output <path>")
|
|
73
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
74
74
|
.action((options) => withClient(async (client) => {
|
|
75
75
|
const format = parseOutputFormat(options.format);
|
|
76
76
|
const requested = maybeArray(options.field);
|
|
@@ -115,7 +115,7 @@ fundamental.command("valuation-analysis")
|
|
|
115
115
|
query(fundamental, "top-holders", {
|
|
116
116
|
endpoint: "fundamental.top-holders",
|
|
117
117
|
fields: [
|
|
118
|
-
|
|
118
|
+
securityCode(),
|
|
119
119
|
field(new Option("--holder-type <type>", "Holder type: top10/top10Float").choices(["top10", "top10Float"]).makeOptionMandatory(), (v) => ({ holderType: v })),
|
|
120
120
|
date("--start-date <date>", "Start date (yyyy-MM-dd)", "startDate"),
|
|
121
121
|
date("--end-date <date>", "End date (yyyy-MM-dd)", "endDate"),
|
|
@@ -125,13 +125,13 @@ query(fundamental, "top-holders", {
|
|
|
125
125
|
],
|
|
126
126
|
});
|
|
127
127
|
fundamental.command("earning-forecast")
|
|
128
|
-
.requiredOption(
|
|
128
|
+
.requiredOption(SECURITY_CODE_FLAGS, SECURITY_CODE_HELP, oneSecurityCode)
|
|
129
129
|
.option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date"))
|
|
130
130
|
.option("--end-date <date>", "End date (default: today)", dateArg("--end-date"))
|
|
131
131
|
.option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, [])
|
|
132
132
|
.option("--yes", `Fetch a range estimated above ${COSTLY_FETCH_CREDITS} credits (billed per row; see the refusal for the estimate)`)
|
|
133
133
|
.option("--format <format>", "Output format", "table")
|
|
134
|
-
.option("--output <path>")
|
|
134
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
135
135
|
.action((options) => emit(options, (client) => {
|
|
136
136
|
const endDate = options.endDate ?? beijingDateString(new Date());
|
|
137
137
|
const startDate = options.startDate ?? new Date(new Date(`${endDate}T00:00:00Z`).getTime() - 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
@@ -5,7 +5,7 @@ import { checkScreenerBindings, droppedFromMatrix, flattenCrossSection, flattenT
|
|
|
5
5
|
import { ValidationError } from "../core/errors.js";
|
|
6
6
|
import { parseOutputFormat } from "../core/output.js";
|
|
7
7
|
import { printData } from "../core/printer.js";
|
|
8
|
-
import { withClient } from "./shared.js";
|
|
8
|
+
import { withClient, OUTPUT_HELP } from "./shared.js";
|
|
9
9
|
function flagDropped(rows, data, requestedSecurities, requestedIndicators, timeSeries = false) {
|
|
10
10
|
if (isEmptyMatrix(data)) {
|
|
11
11
|
if (timeSeries) {
|
|
@@ -44,7 +44,7 @@ indicator.command("search")
|
|
|
44
44
|
.requiredOption("--keyword <text>", "Search keyword, e.g. '收盘价' '成交量' '营业收入' (not free-form questions)")
|
|
45
45
|
.option("--limit <number>", "Max results (default: 50, max: 100)", "50")
|
|
46
46
|
.option("--format <format>", "Output format", "table")
|
|
47
|
-
.option("--output <path>")
|
|
47
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
48
48
|
.action((options) => withClient(async (client) => {
|
|
49
49
|
const format = parseOutputFormat(options.format);
|
|
50
50
|
const raw = await client.call("indicator.search", {
|
|
@@ -62,7 +62,7 @@ indicator.command("cross-section")
|
|
|
62
62
|
.option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'. Bare 'code:' (nothing after the colon) declares the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_mkt, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'code:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'code:' + 'code:scale=8' keeps the scale", collectNames, [])
|
|
63
63
|
.addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode, unique & order-stable for batch code→value mapping").choices(["name", "code"]).default("name"))
|
|
64
64
|
.option("--format <format>", "Output format", "table")
|
|
65
|
-
.option("--output <path>")
|
|
65
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
66
66
|
.action((options) => withClient(async (client) => {
|
|
67
67
|
const format = parseOutputFormat(options.format);
|
|
68
68
|
requireIndicatorScope(options.indicator, options.security);
|
|
@@ -83,7 +83,7 @@ indicator.command("time-series")
|
|
|
83
83
|
.option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'", collectNames, [])
|
|
84
84
|
.addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode/securityCode, unique & order-stable for batch mapping").choices(["name", "code"]).default("name"))
|
|
85
85
|
.option("--format <format>", "Output format", "table")
|
|
86
|
-
.option("--output <path>")
|
|
86
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
87
87
|
.action((options) => withClient(async (client) => {
|
|
88
88
|
const format = parseOutputFormat(options.format);
|
|
89
89
|
requireIndicatorScope(options.indicator, options.security);
|
|
@@ -102,7 +102,7 @@ indicator.command("screener")
|
|
|
102
102
|
.option("--indicator-param <spec>", "Per-variable param 'F1:key=value', e.g. F1:scale=8 (repeat); read exact keys from 'indicator search'. Bare 'F1:' (nothing after the colon) declares that the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_sctr, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'F1:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'F1:' + 'F1:scale=8' keeps the scale", collectNames, [])
|
|
103
103
|
.addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode").choices(["name", "code"]).default("name"))
|
|
104
104
|
.option("--format <format>", "Output format", "table")
|
|
105
|
-
.option("--output <path>")
|
|
105
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
106
106
|
.action((options) => withClient(async (client) => {
|
|
107
107
|
const format = parseOutputFormat(options.format);
|
|
108
108
|
requireIndicatorScope(options.indicator, options.security);
|
|
@@ -3,7 +3,7 @@ import { collectList, dateArg, datetimeArg, maybeArray, parseChoiceList, parseEn
|
|
|
3
3
|
import { ValidationError } from "../core/errors.js";
|
|
4
4
|
import { fetchOpinionDetails } from "../core/opinionDetail.js";
|
|
5
5
|
import { rowCount } from "../core/rowSink.js";
|
|
6
|
-
import { emit, addDownloadCommand, confirmCostly, multiChoice, field, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
6
|
+
import { emit, addDownloadCommand, confirmCostly, multiChoice, field, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query, RESEARCH_AREA_CITIC_OR_DIRECTION, OUTPUT_HELP, securityCode } from "./shared.js";
|
|
7
7
|
export const insight = new Command("insight").description("Insight APIs");
|
|
8
8
|
const SW_INDUSTRY_ONLY = "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them";
|
|
9
9
|
const opinion = new Command("opinion");
|
|
@@ -21,6 +21,7 @@ const announcementHk = new Command("announcement-hk");
|
|
|
21
21
|
const announcementUs = new Command("announcement-us");
|
|
22
22
|
const foreignOpinion = new Command("foreign-opinion");
|
|
23
23
|
const independentOpinion = new Command("independent-opinion");
|
|
24
|
+
const overseasOpinion = new Command("overseas-opinion");
|
|
24
25
|
const officialAccount = new Command("official-account");
|
|
25
26
|
const qa = new Command("qa");
|
|
26
27
|
const reportImage = new Command("report-image");
|
|
@@ -46,7 +47,7 @@ query(opinion, "list", {
|
|
|
46
47
|
list("--broker-tag <tag>", "Broker tag: topBroker/newFortune", "brokerTagList"),
|
|
47
48
|
list("--source <source>", "Source", "sourceList"),
|
|
48
49
|
flag("--with-content", "Return the full body inline (v1 list, 30 credits/row) instead of the 200-char brief (1 credit/row). v1 shape: title and body sit under contentList.title / contentList.content, and there is no brief. Takes no --category / --broker-tag, and --llm-tag only strongRcmd/earningsReview/topBroker/newFortune"),
|
|
49
|
-
format(), output(
|
|
50
|
+
format(), output(),
|
|
50
51
|
...timeFilters(),
|
|
51
52
|
],
|
|
52
53
|
});
|
|
@@ -62,7 +63,7 @@ query(summary, "list", {
|
|
|
62
63
|
list("--category <name>", "Category", "categoryList"),
|
|
63
64
|
list("--market <name>", "Market", "marketList"),
|
|
64
65
|
list("--participant-role <name>", "Participant role", "participantRoleList"),
|
|
65
|
-
format(), output(
|
|
66
|
+
format(), output(),
|
|
66
67
|
...timeFilters(),
|
|
67
68
|
],
|
|
68
69
|
});
|
|
@@ -79,7 +80,7 @@ query(pamirsSummary, "list", {
|
|
|
79
80
|
list("--security <code>", "Security code, e.g. 000001.SZ", "securityList"),
|
|
80
81
|
choiceList("--category <name>", `Category: ${PAMIRS_CATEGORIES.join(" / ")}`, "categoryList", PAMIRS_CATEGORIES),
|
|
81
82
|
choiceList("--market <name>", `Market: ${PAMIRS_MARKETS.join(" / ")}`, "marketList", PAMIRS_MARKETS),
|
|
82
|
-
format(), output(
|
|
83
|
+
format(), output(),
|
|
83
84
|
...timeFilters(),
|
|
84
85
|
],
|
|
85
86
|
});
|
|
@@ -102,7 +103,7 @@ const addScheduleList = (command, endpointKey, fields) => query(command, "list",
|
|
|
102
103
|
fields.brokerType && list("--broker-type <name>", "Lead broker type: cnBroker/otherBroker", "brokerTypeList"),
|
|
103
104
|
fields.permission && numberList("--permission <number>", "Permission: 1=public 2=private", "permission"),
|
|
104
105
|
fields.location && list("--location <id>", "Location ID (domesticCity constant, via 'reference constant-list')", "locationList"),
|
|
105
|
-
format(), output(
|
|
106
|
+
format(), output(),
|
|
106
107
|
...timeFilters(),
|
|
107
108
|
publishTime("--publish-start-time <datetime>", "Collected from (publishTime, when the schedule first came in — not when it is held); combines with --start-time/--end-time", "publishStartTime"),
|
|
108
109
|
publishTime("--publish-end-time <datetime>", "Collected until (publishTime); a bare date covers that whole day", "publishEndTime"),
|
|
@@ -146,7 +147,7 @@ performanceCalendar.command("list").description("Earnings calendar (业绩预告
|
|
|
146
147
|
.option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, [])
|
|
147
148
|
.addOption(multiChoice("--market <name>", `Market: ${PERFORMANCE_MARKETS.join("/")}`, PERFORMANCE_MARKETS))
|
|
148
149
|
.addOption(multiChoice("--category <name>", `Event type: ${PERFORMANCE_CATEGORIES.join("/")}`, PERFORMANCE_CATEGORIES))
|
|
149
|
-
.option("--format <format>", "Output format", "table").option("--output <path>",
|
|
150
|
+
.option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP)
|
|
150
151
|
.addOption(confirmCostly().option)
|
|
151
152
|
.action((options) => {
|
|
152
153
|
const marketList = parseChoiceList(options.market, "--market", PERFORMANCE_MARKETS);
|
|
@@ -186,7 +187,7 @@ query(research, "list", {
|
|
|
186
187
|
count("--min-pages <number>", "Min report pages", "minReportPages"),
|
|
187
188
|
count("--max-pages <number>", "Max report pages", "maxReportPages"),
|
|
188
189
|
list("--source <type>", "Source type", "sourceList"),
|
|
189
|
-
format(), output(
|
|
190
|
+
format(), output(),
|
|
190
191
|
...timeFilters(),
|
|
191
192
|
],
|
|
192
193
|
});
|
|
@@ -206,7 +207,7 @@ query(foreignReport, "list", {
|
|
|
206
207
|
list("--rating-change <name>", "Rating change", "ratingChangeList"),
|
|
207
208
|
count("--min-pages <number>", "Min report pages", "minReportPages"),
|
|
208
209
|
count("--max-pages <number>", "Max report pages", "maxReportPages"),
|
|
209
|
-
format(), output(
|
|
210
|
+
format(), output(),
|
|
210
211
|
...timeFilters(),
|
|
211
212
|
],
|
|
212
213
|
});
|
|
@@ -218,7 +219,7 @@ query(announcement, "list", {
|
|
|
218
219
|
searchType(), rankType(),
|
|
219
220
|
list("--security <code>", "Security code", "securityList"),
|
|
220
221
|
list("--category <id>", "Category ID", "categoryList"),
|
|
221
|
-
format(), output(
|
|
222
|
+
format(), output(),
|
|
222
223
|
...timeFilters(parseTimestamp13, parseEndTimestamp13),
|
|
223
224
|
],
|
|
224
225
|
});
|
|
@@ -230,7 +231,7 @@ query(announcementHk, "list", {
|
|
|
230
231
|
searchType(), rankType(),
|
|
231
232
|
list("--security <code>", "Security code (e.g. 01913.HK)", "securityList"),
|
|
232
233
|
list("--category <id>", "Category ID", "categoryList"),
|
|
233
|
-
format(), output(
|
|
234
|
+
format(), output(),
|
|
234
235
|
...timeFilters(),
|
|
235
236
|
],
|
|
236
237
|
});
|
|
@@ -242,7 +243,7 @@ query(announcementUs, "list", {
|
|
|
242
243
|
searchType(), rankType(),
|
|
243
244
|
list("--security <code>", "Security code (e.g. TSLA.O)", "securityList"),
|
|
244
245
|
list("--category <id>", "Category ID (constant-list usShareAnnouncementCategory)", "categoryList"),
|
|
245
|
-
format(), output(
|
|
246
|
+
format(), output(),
|
|
246
247
|
...timeFilters(),
|
|
247
248
|
],
|
|
248
249
|
});
|
|
@@ -258,17 +259,17 @@ query(foreignOpinion, "list", {
|
|
|
258
259
|
list("--rating <name>", "Rating", "ratingList"),
|
|
259
260
|
list("--rating-change <name>", "Rating change", "ratingChangeList"),
|
|
260
261
|
flag("--with-content", "Return the full body inline (v1 list, 30 credits/row) as content / contentTranslate, instead of the 200-char brief (1 credit/row)"),
|
|
261
|
-
format(), output(
|
|
262
|
+
format(), output(),
|
|
262
263
|
...timeFilters(),
|
|
263
264
|
],
|
|
264
265
|
});
|
|
265
266
|
opinion.command("detail").description("Full bodies of domestic chief opinions by ID (30 credits per returned opinion; batched 20 per call)")
|
|
266
267
|
.requiredOption("--chief-opinion-id <id>", "chiefOpinionId from 'insight opinion list' (repeat or comma-separate)", collectList)
|
|
267
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
268
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
268
269
|
.action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.opinion.detail", "chiefOpinionIdList", "chiefOpinionId", options.chiefOpinionId)));
|
|
269
270
|
foreignOpinion.command("detail").description("Full bodies (content + contentTranslate) of foreign opinions by ID (30 credits per returned opinion; batched 20 per call)")
|
|
270
271
|
.requiredOption("--foreign-opinion-id <id>", "foreignOpinionId from 'insight foreign-opinion list' (repeat or comma-separate)", collectList)
|
|
271
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
272
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
272
273
|
.action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.foreign-opinion.detail", "foreignOpinionIdList", "foreignOpinionId", options.foreignOpinionId)));
|
|
273
274
|
query(independentOpinion, "list", {
|
|
274
275
|
endpoint: "insight.independent-opinion.list",
|
|
@@ -278,11 +279,30 @@ query(independentOpinion, "list", {
|
|
|
278
279
|
list("--industry <id>", SW_INDUSTRY_ONLY, "industryList"),
|
|
279
280
|
list("--rating <name>", "Rating", "ratingList"),
|
|
280
281
|
list("--rating-change <name>", "Rating change", "ratingChangeList"),
|
|
281
|
-
format(), output(
|
|
282
|
+
format(), output(),
|
|
282
283
|
...timeFilters(),
|
|
283
284
|
],
|
|
284
285
|
});
|
|
285
286
|
addDownloadCommand(independentOpinion, { endpointKey: "insight.independent-opinion.download", idOption: "--independent-opinion-id", idField: "independentOpinionId", fallbackPrefix: "independent-opinion", fileType: { description: "File type: 1=original HTML 2=CN-translated HTML", choices: ["1", "2"], required: true } });
|
|
287
|
+
query(overseasOpinion, "list", {
|
|
288
|
+
description: "List overseas opinions: brief + briefTranslate, body via 'overseas-opinion detail' (0.1 credits/row)",
|
|
289
|
+
endpoint: "insight.overseas-opinion.list",
|
|
290
|
+
fields: [
|
|
291
|
+
rankType(),
|
|
292
|
+
list("--account <id>", "Account ID from 'reference overseas-account-search' (repeat or comma-separate)", "accountIdList"),
|
|
293
|
+
list("--security <code>", "Security code with market suffix, e.g. NVDA.O. A missing suffix is rejected (120001); a real suffix of the wrong market (NVDA.N) matches nothing", "securityList"),
|
|
294
|
+
list("--category <name>", "Category: opinion/news/dataAnalysis/promotion (repeat). A row in several categories is returned under each; rows without a category only when this is omitted", "categoryList"),
|
|
295
|
+
list("--industry <id>", "GICS sector ID: 107700001 energy ... 107700011 real estate (list: 'reference constant-list --category gicsIndustry'). Other industry code sets are rejected (100005)", "industryList"),
|
|
296
|
+
list("--region <code>", "Region code: cn/cnHk/cnTw/us/jp/kr/uk/gl; other regionCategory values are rejected (100005)", "regionList"),
|
|
297
|
+
count("--original-flag <number>", "1=original posts only 0=reposts only; omit for both", "originalFlag"),
|
|
298
|
+
format(), output(),
|
|
299
|
+
...timeFilters(),
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
overseasOpinion.command("detail").description("Full bodies (content + contentTranslate) of overseas opinions by ID (10 credits per returned opinion; batched 20 per call)")
|
|
303
|
+
.requiredOption("--overseas-opinion-id <id>", "overseasOpinionId from 'insight overseas-opinion list' (repeat or comma-separate)", collectList)
|
|
304
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
305
|
+
.action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.overseas-opinion.detail", "overseasOpinionIdList", "overseasOpinionId", options.overseasOpinionId)));
|
|
286
306
|
query(officialAccount, "list", {
|
|
287
307
|
endpoint: "insight.official-account.list",
|
|
288
308
|
cache: { endpointKey: "insight.official-account.list", idField: "articleId" },
|
|
@@ -292,7 +312,7 @@ query(officialAccount, "list", {
|
|
|
292
312
|
list("--security <code>", "Security code (e.g. 000001.SZ)", "securityList"),
|
|
293
313
|
list("--category <type>", "Article type: news/law/report/view/data/event/meeting/notice/recruit/investEdu/brand/notes/other", "categoryList"),
|
|
294
314
|
list("--industry <id>", "Industry ID (constant-list citicIndustry/swIndustry)", "industryList"),
|
|
295
|
-
format(), output(
|
|
315
|
+
format(), output(),
|
|
296
316
|
...timeFilters(),
|
|
297
317
|
],
|
|
298
318
|
});
|
|
@@ -300,7 +320,7 @@ addDownloadCommand(officialAccount, { endpointKey: "insight.official-account.dow
|
|
|
300
320
|
query(qa, "list", {
|
|
301
321
|
endpoint: "insight.qa.list",
|
|
302
322
|
fields: [
|
|
303
|
-
|
|
323
|
+
securityCode("Security code, e.g. 601012.SH"),
|
|
304
324
|
from(), size("Total rows to return; omit to fetch all (max page 500)"),
|
|
305
325
|
startTime("Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"), endTime("End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"),
|
|
306
326
|
list("--source <type>", "Source: conference/interactive/survey (repeat)", "source"),
|
|
@@ -335,6 +355,7 @@ insight.addCommand(announcementHk);
|
|
|
335
355
|
insight.addCommand(announcementUs);
|
|
336
356
|
insight.addCommand(foreignOpinion);
|
|
337
357
|
insight.addCommand(independentOpinion);
|
|
358
|
+
insight.addCommand(overseasOpinion);
|
|
338
359
|
insight.addCommand(officialAccount);
|
|
339
360
|
insight.addCommand(qa);
|
|
340
361
|
insight.addCommand(reportImage);
|
|
@@ -8,7 +8,7 @@ import { flagMissingFields } from "../core/normalize.js";
|
|
|
8
8
|
import { parseOutputFormat } from "../core/output.js";
|
|
9
9
|
import { printData } from "../core/printer.js";
|
|
10
10
|
import { rowCount } from "../core/rowSink.js";
|
|
11
|
-
import { emit, withClient, DEFAULT_QUOTE_LIMIT, MAX_QUOTE_LIMIT, flagIfLimitTruncated, noteLateStart } from "./shared.js";
|
|
11
|
+
import { emit, withClient, DEFAULT_QUOTE_LIMIT, MAX_QUOTE_LIMIT, flagIfLimitTruncated, noteLateStart, OUTPUT_HELP } from "./shared.js";
|
|
12
12
|
export const quote = new Command("quote").description("Quote APIs");
|
|
13
13
|
const KLINE_MARKETS = { aShares: 1, hkStocks: 2, usStocks: 1 };
|
|
14
14
|
const LEGACY_ALL_MARKET = (shardDays) => ({ all: shardDays });
|
|
@@ -48,7 +48,7 @@ const addKlineCommand = (name, endpointKey, securityHelp, markets, noKeywordReas
|
|
|
48
48
|
.option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}). With several securities it also sizes the groups they are fetched in, so a small value fetches one security per request`)
|
|
49
49
|
.option("--field <field>", "Field", collectList, [])
|
|
50
50
|
.option("--format <format>", "Output format", "table")
|
|
51
|
-
.option("--output <path>")
|
|
51
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
52
52
|
.action((options) => {
|
|
53
53
|
checkMarketKeywords(options.security, Object.keys(markets), `quote ${name}`, noKeywordReason);
|
|
54
54
|
options.security = canonicalizeMarketKeywords(options.security, Object.keys(markets));
|
|
@@ -100,7 +100,7 @@ quote.command("minute-kline")
|
|
|
100
100
|
.option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT})`)
|
|
101
101
|
.option("--field <field>", "Field", collectList, [])
|
|
102
102
|
.option("--format <format>", "Output format", "table")
|
|
103
|
-
.option("--output <path>")
|
|
103
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
104
104
|
.action((options) => withClient(options, async (client) => {
|
|
105
105
|
const format = parseOutputFormat(options.format);
|
|
106
106
|
client.rowSink?.watchEarliest("tradeTime");
|
|
@@ -127,7 +127,7 @@ quote.command("realtime")
|
|
|
127
127
|
.option("--security <code>", "Security code — stock .SH/.SZ/.BJ/.HK/.O/.N/.A, ETF .SH/.SZ (e.g. 512800.SH), exchange index .SH/.SZ/.BJ, concept index .GT, industry index .CI/.SWI, global index (e.g. SPX.SPI / N225.NKI / HSI.HI); or one market keyword: aShares / hkStocks / usStocks (must be passed alone; keywords cover stocks only — ETFs and indices have no whole-market keyword)", collectList, [])
|
|
128
128
|
.option("--field <field>", "Field", collectList, [])
|
|
129
129
|
.option("--format <format>", "Output format", "table")
|
|
130
|
-
.option("--output <path>")
|
|
130
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
131
131
|
.action((options) => {
|
|
132
132
|
checkMarketKeywords(options.security, REALTIME_MARKETS, "quote realtime");
|
|
133
133
|
return emit(options, async (client) => {
|
|
@@ -147,7 +147,7 @@ quote.command("fund-flow")
|
|
|
147
147
|
.option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}; single-security cap — aShares auto-shards by day)`)
|
|
148
148
|
.option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, [])
|
|
149
149
|
.option("--format <format>", "Output format", "table")
|
|
150
|
-
.option("--output <path>")
|
|
150
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
151
151
|
.action((options) => {
|
|
152
152
|
checkMarketKeywords(options.security, FUND_FLOW_MARKETS, "quote fund-flow");
|
|
153
153
|
options.security = canonicalizeMarketKeywords(options.security, FUND_FLOW_MARKETS);
|
|
@@ -187,7 +187,7 @@ quote.command("sector-fund-flow")
|
|
|
187
187
|
.option("--limit <number>", `Max rows returned (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}). It cuts the answer only: a query too heavy to compute fails with 100006 whatever the limit — shorten the range, or pass fewer sectors or fields`)
|
|
188
188
|
.option("--field <field>", "Field, e.g. sectorMainNetInflow / sectorLargeInflow (repeat); omit for all. sectorId and tradeDate always come first", collectList, [])
|
|
189
189
|
.option("--format <format>", "Output format", "table")
|
|
190
|
-
.option("--output <path>")
|
|
190
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
191
191
|
.action((options) => emit(options, async (client) => {
|
|
192
192
|
const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: MAX_QUOTE_LIMIT }) ?? DEFAULT_QUOTE_LIMIT;
|
|
193
193
|
const fieldList = maybeArray(options.field);
|
package/dist/src/commands/raw.js
CHANGED
|
@@ -5,8 +5,8 @@ import { ConfigError, ValidationError } from "../core/errors.js";
|
|
|
5
5
|
import { flagFailedItems } from "../core/normalize.js";
|
|
6
6
|
import { parseOutputFormat } from "../core/output.js";
|
|
7
7
|
import { printData } from "../core/printer.js";
|
|
8
|
-
import { createClient, runDownload, assertConfirmed } from "./shared.js";
|
|
9
|
-
export const raw = new Command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive), or fetch every row of a per-row billed list past the credit guard").option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
|
|
8
|
+
import { createClient, runDownload, assertConfirmed, OUTPUT_HELP } from "./shared.js";
|
|
9
|
+
export const raw = new Command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>", "Request body as a JSON string").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive), or fetch every row of a per-row billed list past the credit guard").option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP).action(async (endpointKey, options) => {
|
|
10
10
|
const endpoint = ENDPOINTS[endpointKey];
|
|
11
11
|
if (!endpoint) {
|
|
12
12
|
throw new ConfigError(`Unknown endpoint key: ${endpointKey}`);
|
|
@@ -47,4 +47,4 @@ export const raw = new Command("raw").description("Raw API calls").addCommand(ne
|
|
|
47
47
|
finally {
|
|
48
48
|
await client.rowSink?.abort();
|
|
49
49
|
}
|
|
50
|
-
})).addCommand(new Command("list").description("List all registered endpoint keys (for use with 'raw call')").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => printData(listEndpoints(), parseOutputFormat(options.format), options.output)));
|
|
50
|
+
})).addCommand(new Command("list").description("List all registered endpoint keys (for use with 'raw call')").option("--format <format>", "Output format", "table").option("--output <path>", OUTPUT_HELP).action((options) => printData(listEndpoints(), parseOutputFormat(options.format), options.output)));
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { value, required, choiceList, top, format, output, query } from "./shared.js";
|
|
2
|
+
import { value, required, requiredList, date, choiceList, top, format, output, from, size, query } from "./shared.js";
|
|
3
3
|
export const reference = new Command("reference").description("Reference data APIs");
|
|
4
4
|
query(reference, "securities-search", {
|
|
5
5
|
endpoint: "reference.securities-search",
|
|
6
6
|
fields: [
|
|
7
7
|
required("--keyword <text>", "Search keyword (name/code/pinyin/English)", "keyword"),
|
|
8
|
-
choiceList("--category <type>", "Category: stock/dr/index/fund/bond.
|
|
8
|
+
choiceList("--category <type>", "Category: stock/dr/index/fund/bond. Without it, a convertible bond can rank above its stock on a short name", "category", ["stock", "dr", "index", "fund", "bond"]),
|
|
9
9
|
top(10),
|
|
10
10
|
format(), output(),
|
|
11
11
|
],
|
|
@@ -34,6 +34,29 @@ query(reference, "sector-constituents", {
|
|
|
34
34
|
endpoint: "reference.sector-constituents",
|
|
35
35
|
fields: [required("--sector-id <id>", "Sector ID from 'reference sector-search'", "sectorId"), format(), output()],
|
|
36
36
|
});
|
|
37
|
+
function noteMissingIndices(data, requested, tradeDate) {
|
|
38
|
+
const rows = data?.list;
|
|
39
|
+
if (!Array.isArray(rows))
|
|
40
|
+
return;
|
|
41
|
+
const found = new Set(rows.map((row) => String(row?.indexCode).toUpperCase()));
|
|
42
|
+
const missing = requested.filter((code) => !found.has(code.toUpperCase()));
|
|
43
|
+
if (!missing.length)
|
|
44
|
+
return;
|
|
45
|
+
process.stderr.write(`[gangtise] note: no constituent weights for ${missing.join(", ")} on ${tradeDate}. Weights come for the last trading day of each month — another date, like an unknown index code, comes back empty without an error. Check the date, and the code with 'reference securities-search --category index'.\n`);
|
|
46
|
+
}
|
|
47
|
+
query(reference, "index-weight", {
|
|
48
|
+
description: "Index constituents and their weights (%) on the last trading day of a month (free)",
|
|
49
|
+
endpoint: "reference.index-weight",
|
|
50
|
+
fields: [
|
|
51
|
+
requiredList("--index <code>", "Index code, e.g. 000300.SH / 000905.SH / 399006.SZ (repeat or comma-separate)", "indexCodeList"),
|
|
52
|
+
date("--date <date>", "Trading date (yyyy-MM-dd): the last trading day of a month, e.g. 2026-10-30; any other date returns no rows", "tradeDate", { required: true }),
|
|
53
|
+
format(), output(),
|
|
54
|
+
],
|
|
55
|
+
shape: async (data, _client, options) => {
|
|
56
|
+
noteMissingIndices(data, options.index, options.date);
|
|
57
|
+
return data;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
37
60
|
query(reference, "chiefs-search", {
|
|
38
61
|
endpoint: "reference.chiefs-search",
|
|
39
62
|
fields: [required("--keyword <text>", "Search keyword (chief name / institution / team)", "keyword"), top(10), format(), output()],
|
|
@@ -50,9 +73,18 @@ query(reference, "institution-search", {
|
|
|
50
73
|
query(reference, "official-account-search", {
|
|
51
74
|
endpoint: "reference.official-account-search",
|
|
52
75
|
fields: [
|
|
53
|
-
required("--keyword <text>", "
|
|
76
|
+
required("--keyword <text>", "Account name fragment, e.g. 中信证券 (finds 中信证券研究). Matches the account name: an institution's full name misses accounts named by short form (东吴证券 finds none, 东吴 finds 东吴策略陈刚团队)", "keyword"),
|
|
54
77
|
choiceList("--category <name>", "Category: listedCompany/broker/government/media (repeat); omit for all incl. uncategorized", "category", ["listedCompany", "broker", "government", "media"]),
|
|
55
78
|
top(10),
|
|
56
79
|
format(), output(),
|
|
57
80
|
],
|
|
58
81
|
});
|
|
82
|
+
query(reference, "overseas-account-search", {
|
|
83
|
+
description: "Search overseas opinion account IDs by name fragment, for 'insight overseas-opinion list --account' (free)",
|
|
84
|
+
endpoint: "reference.overseas-account-search",
|
|
85
|
+
fields: [
|
|
86
|
+
required("--keyword <text>", "Account name fragment: substring match anywhere in the name, case-insensitive (e.g. elo matches Elon Musk and Financelot)", "accountName"),
|
|
87
|
+
from(), size(),
|
|
88
|
+
format(), output(),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { Option } from "commander";
|
|
3
|
+
import { InvalidArgumentError, Option } from "commander";
|
|
4
4
|
import { QUOTE_MAX_LIMIT } from "../core/quoteSharding.js";
|
|
5
5
|
import { collectList, dateArg, datetimeArg, maybeArray, numberListArg, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize } from "../core/args.js";
|
|
6
6
|
import { loadConfig } from "../core/config.js";
|
|
7
|
-
import { placeStreamedDownload, releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
|
|
7
|
+
import { assertOutputFilePath, placeStreamedDownload, releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
|
|
8
|
+
import { isDryRun } from "../core/dryRun.js";
|
|
8
9
|
import { ENDPOINTS } from "../core/endpoints.js";
|
|
9
10
|
import { ValidationError } from "../core/errors.js";
|
|
10
11
|
import { parseOutputFormat, stagingPath } from "../core/output.js";
|
|
11
|
-
import { printData } from "../core/printer.js";
|
|
12
|
+
import { columnsSelected, printData } from "../core/printer.js";
|
|
12
13
|
import { ExportSink, getRowSink } from "../core/rowSink.js";
|
|
13
14
|
export async function createClient(stream) {
|
|
14
15
|
const { GangtiseClient } = await import("../core/client.js");
|
|
15
|
-
const sink = stream?.output && (stream.format === "jsonl" || stream.format === "csv") ? new ExportSink(stream.output, stream.format, stream.cache) : undefined;
|
|
16
|
+
const sink = stream?.output && (stream.format === "jsonl" || stream.format === "csv") && !columnsSelected() ? new ExportSink(stream.output, stream.format, stream.cache) : undefined;
|
|
16
17
|
const client = new GangtiseClient(loadConfig(), sink);
|
|
17
18
|
client.allowCostlyFetch = Boolean(stream?.yes);
|
|
18
19
|
return client;
|
|
@@ -89,6 +90,7 @@ export function noteLateStart(data, requestedStart, field, label, cause = "The s
|
|
|
89
90
|
}
|
|
90
91
|
export async function runDownload(client, endpointKey, query, options) {
|
|
91
92
|
if (options.output) {
|
|
93
|
+
await assertOutputFilePath(options.output);
|
|
92
94
|
const result = await client.call(endpointKey, options.body, query, { streamTo: options.output });
|
|
93
95
|
await saveDownloadResult(result, options.fallbackName, options.output);
|
|
94
96
|
return;
|
|
@@ -120,7 +122,7 @@ export async function runDownload(client, endpointKey, query, options) {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
export function addDownloadCommand(parent, spec) {
|
|
123
|
-
const cmd = parent.command(spec.name ?? "download").requiredOption(`${spec.idOption} <id
|
|
125
|
+
const cmd = parent.command(spec.name ?? "download").requiredOption(`${spec.idOption} <id>`, `The ${spec.idField} of the record, from the matching list command`);
|
|
124
126
|
if (spec.fileType) {
|
|
125
127
|
const option = new Option("--file-type <number>", spec.fileType.description).choices(spec.fileType.choices);
|
|
126
128
|
cmd.addOption(spec.fileType.required ? option.makeOptionMandatory() : option.default(spec.fileType.default));
|
|
@@ -132,7 +134,7 @@ export function addDownloadCommand(parent, spec) {
|
|
|
132
134
|
if (spec.titleListEndpoint) {
|
|
133
135
|
cmd.option("--resolve-title", "On a title-cache miss, query the list endpoint for a friendly filename (4 extra requests; most of these list endpoints bill per row). Without it the server filename or <prefix>-<id> is used. Ignored when --output is given");
|
|
134
136
|
}
|
|
135
|
-
cmd.option("--output <path>").action((options) => withClient(async (client) => {
|
|
137
|
+
cmd.option("--output <path>", DOWNLOAD_OUTPUT_HELP).action((options) => withClient(async (client) => {
|
|
136
138
|
const id = options[spec.idField];
|
|
137
139
|
const qp = { [spec.idField]: id };
|
|
138
140
|
if (spec.fileType && options.fileType)
|
|
@@ -151,7 +153,7 @@ export function addDownloadCommand(parent, spec) {
|
|
|
151
153
|
}
|
|
152
154
|
export function assertConfirmed(endpointKey, confirmed, target) {
|
|
153
155
|
const destructive = ENDPOINTS[endpointKey]?.destructive;
|
|
154
|
-
if (!destructive || confirmed)
|
|
156
|
+
if (!destructive || confirmed || isDryRun())
|
|
155
157
|
return;
|
|
156
158
|
throw new ValidationError(`${destructive.warning} 确认要对 ${target} 执行就加上 --yes。`);
|
|
157
159
|
}
|
|
@@ -201,8 +203,18 @@ export const rankType = () => numberEnum("--rank-type <number>", "Rank type: 1=c
|
|
|
201
203
|
export const searchType = () => numberEnum("--search-type <number>", "Search type: 1=title 2=fulltext", "searchType", ["1", "2"], "1");
|
|
202
204
|
export const top = (max) => field(new Option("--top <number>", `Max results (default: 10, max: ${max})`).default("10"), (v) => ({ top: parseNumberOption(v, "--top", { integer: true, min: 1, max }) }));
|
|
203
205
|
export const flag = (flags, description) => field(new Option(flags, description));
|
|
206
|
+
export const OUTPUT_HELP = "Write the result to this file instead of stdout (a file path, not a directory)";
|
|
207
|
+
export const DOWNLOAD_OUTPUT_HELP = "Save the file to this path (a file path, not a directory); omit for an automatic name in the current directory";
|
|
208
|
+
export const SECURITY_CODE_FLAGS = "--security, --security-code <code>";
|
|
209
|
+
export const SECURITY_CODE_HELP = "Security code, e.g. 600519.SH";
|
|
210
|
+
export const oneSecurityCode = (value) => {
|
|
211
|
+
if (value.includes(","))
|
|
212
|
+
throw new InvalidArgumentError("takes one security code; run the command once per code");
|
|
213
|
+
return value;
|
|
214
|
+
};
|
|
204
215
|
export const format = (fallback = "table") => field(new Option("--format <format>", "Output format").default(fallback));
|
|
205
|
-
export const output = (description) => field(new Option("--output <path>", description));
|
|
216
|
+
export const output = (description = OUTPUT_HELP) => field(new Option("--output <path>", description));
|
|
217
|
+
export const securityCode = (description = SECURITY_CODE_HELP) => field(new Option(SECURITY_CODE_FLAGS, description).makeOptionMandatory().argParser(oneSecurityCode), (v) => ({ securityCode: v }));
|
|
206
218
|
export const from = () => field(new Option("--from <number>", "Starting offset").default("0"), (v) => ({ from: parseFrom(v) }));
|
|
207
219
|
export const size = (description = "Total rows to return; omit to fetch all") => field(new Option("--size <number>", description), (v) => ({ size: parseSize(v) }));
|
|
208
220
|
const asTyped = (v) => v;
|