@zhuoyuezs/ml-platform 0.1.1 → 0.1.4
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/DEVELOPMENT.md +96 -9
- package/README.md +89 -38
- package/checksums.json +37 -32
- package/package.json +5 -1
- package/release-policy.json +10 -0
- package/release.json +13 -9
- package/runtime/business-client/README.md +13 -0
- package/runtime/business-client/package-lock.json +2 -2
- package/runtime/business-client/package.json +1 -1
- package/runtime/business-client/src/catalog.js +40 -17
- package/runtime/business-client/src/cli.js +150 -26
- package/runtime/business-client/src/config.js +6 -2
- package/runtime/business-client/src/http.js +88 -17
- package/scripts/lib.js +98 -4
- package/scripts/main.js +96 -11
- package/skills/feature-management/SKILL.md +187 -18
- package/skills/feature-management/assets/catalog-template/datasets/example_temperature_training.v1.json +16 -0
- package/skills/feature-management/assets/catalog-template/feature_sets/example_temperature_core.v1.json +1 -0
- package/skills/feature-management/assets/catalog-template/features/example_temperature_mean_5m.v1.json +9 -2
- package/skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py +51 -24
- package/skills/feature-management/assets/catalog-template/operators/example_temperature_features.v1.json +16 -1
- package/skills/feature-management/assets/catalog-template/parameters/example_temperature.v1.json +1 -0
- package/skills/feature-management/references/commands.md +66 -4
- package/skills/feature-management/references/contracts.md +43 -6
- package/skills/feature-management/references/operator-authoring.md +15 -7
- package/skills/feature-management/references/platform-capability-guide.md +44 -0
|
@@ -34,7 +34,7 @@ function objectList(value, label) { if (!Array.isArray(value) || value.some((ite
|
|
|
34
34
|
function stringMap(value, label) { object(value, label); if (Object.entries(value).some(([name, item]) => typeof name !== "string" || typeof item !== "string")) throw new Error(`${label} must contain only string keys and values`); return value; }
|
|
35
35
|
function unique(values, label) { if (new Set(values).size !== values.length) throw new Error(`${label} must be unique`); }
|
|
36
36
|
function ref(value, label) { exactKeys(value, ["name", "version", "project"], ["name", "version"], label); return { name: identifier(value.name, `${label} name`), version: identifier(value.version, `${label} version`) }; }
|
|
37
|
-
function parameterRef(value) { exactKeys(value, ["parameter", "version", "project"], ["parameter", "version"], "feature input"); return { parameter: identifier(value.parameter, "feature input parameter"), version: identifier(value.version, "feature input version") }; }
|
|
37
|
+
function parameterRef(value, parentProject) { exactKeys(value, ["parameter", "version", "project"], ["parameter", "version"], "feature input"); const project = identifier(defaultIfAbsent(value, "project", "default"), "feature input project"); if (parentProject && project !== parentProject) throw new Error("feature input cross-project reference is not allowed"); return { parameter: identifier(value.parameter, "feature input parameter"), version: identifier(value.version, "feature input version"), project }; }
|
|
38
38
|
function key(value) { return `${value.name}:${value.version}`; }
|
|
39
39
|
function datasetKey(value) { return `${value.dataset_id}:${value.dataset_version}`; }
|
|
40
40
|
function referenceKey(value) { return `${value.name}:${value.version}`; }
|
|
@@ -60,13 +60,16 @@ function normalizeCodeArtifact(value) { object(value, "operator code_artifact");
|
|
|
60
60
|
function normalizeParameter(payload) {
|
|
61
61
|
exactKeys(payload, ["schema_version", "project", "name", "display_name", "version", "data_type", "unit", "expected_frequency", "source", "time_semantics", "availability_sla", "value_field", "quality_rules", "rounding", "owner"], ["name", "display_name", "version", "source"], "parameter");
|
|
62
62
|
const source = object(payload.source, "parameter source");
|
|
63
|
+
if (Object.prototype.hasOwnProperty.call(source, "parameters")) throw new Error("parameter source uses unsupported field 'parameters'; use 'params'");
|
|
64
|
+
if (source.mode === "sql" && typeof source.sql === "string" && source.sql.trimEnd().endsWith(";")) throw new Error("SQL Parameter source.sql must not end with a semicolon");
|
|
63
65
|
const time = object(defaultIfAbsent(payload, "time_semantics", {}), "parameter time_semantics");
|
|
64
|
-
return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.parameter/v1")), name: identifier(payload.name, "parameter name"), display_name: requiredString(payload.display_name, "parameter display_name"), version: identifier(payload.version, "parameter version"), data_type: pythonString(defaultIfAbsent(payload, "data_type", "time_series")), unit: pythonString(defaultIfAbsent(payload, "unit", "")), expected_frequency: pythonString(defaultIfAbsent(payload, "expected_frequency", "10min")), source: { adapter: pythonString(defaultIfAbsent(source, "adapter", "synthetic")), mode: choice(defaultIfAbsent(source, "mode", "direct_column"), ["direct_column", "sql", "derived"], "parameter source mode"), source_metric: optionalString(source.source_metric, "source_metric"), source_group: optionalString(source.source_group, "source_group"), measurement: optionalString(source.measurement, "measurement"), field: optionalString(source.field, "field"), tags: stringMap(defaultIfAbsent(source, "tags", {}), "source tags"), schema: optionalString(aliasIfAbsent(source, "schema", "schema_name"), "schema"), table: optionalString(source.table, "table"), time_column: optionalString(source.time_column, "time_column"), value_column: optionalString(source.value_column, "value_column"), metric_name_column: optionalString(source.metric_name_column, "metric_name_column"), metric_name: optionalString(source.metric_name, "metric_name"), unit_column: optionalString(source.unit_column, "unit_column"), unit: optionalString(aliasIfAbsent(source, "unit", "source_unit"), "source unit"), filters: stringMap(defaultIfAbsent(source, "filters", {}), "source filters"), sql: optionalString(source.sql, "source sql"), params: object(defaultIfAbsent(source, "params", {}), "source params") }, time_semantics: { event_time_field: pythonString(defaultIfAbsent(time, "event_time_field", "event_time")), ingested_at_field: pythonString(defaultIfAbsent(time, "ingested_at_field", "ingested_at")), timezone: pythonString(defaultIfAbsent(time, "timezone", "Asia/Shanghai")), availability: normalizeAvailability(time.availability) }, availability_sla: normalizeAvailabilitySla(payload.availability_sla), value_field: pythonString(defaultIfAbsent(payload, "value_field", "value")), quality_rules: normalizeQualityRules(object(defaultIfAbsent(payload, "quality_rules", {}), "parameter quality_rules")), rounding: normalizeRounding(payload.rounding), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) };
|
|
66
|
+
return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.parameter/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "parameter project"), name: identifier(payload.name, "parameter name"), display_name: requiredString(payload.display_name, "parameter display_name"), version: identifier(payload.version, "parameter version"), data_type: pythonString(defaultIfAbsent(payload, "data_type", "time_series")), unit: pythonString(defaultIfAbsent(payload, "unit", "")), expected_frequency: pythonString(defaultIfAbsent(payload, "expected_frequency", "10min")), source: { adapter: pythonString(defaultIfAbsent(source, "adapter", "synthetic")), mode: choice(defaultIfAbsent(source, "mode", "direct_column"), ["direct_column", "sql", "derived"], "parameter source mode"), source_metric: optionalString(source.source_metric, "source_metric"), source_group: optionalString(source.source_group, "source_group"), measurement: optionalString(source.measurement, "measurement"), field: optionalString(source.field, "field"), tags: stringMap(defaultIfAbsent(source, "tags", {}), "source tags"), schema: optionalString(aliasIfAbsent(source, "schema", "schema_name"), "schema"), table: optionalString(source.table, "table"), time_column: optionalString(source.time_column, "time_column"), value_column: optionalString(source.value_column, "value_column"), metric_name_column: optionalString(source.metric_name_column, "metric_name_column"), metric_name: optionalString(source.metric_name, "metric_name"), unit_column: optionalString(source.unit_column, "unit_column"), unit: optionalString(aliasIfAbsent(source, "unit", "source_unit"), "source unit"), filters: stringMap(defaultIfAbsent(source, "filters", {}), "source filters"), sql: optionalString(source.sql, "source sql"), params: object(defaultIfAbsent(source, "params", {}), "source params") }, time_semantics: { event_time_field: pythonString(defaultIfAbsent(time, "event_time_field", "event_time")), ingested_at_field: pythonString(defaultIfAbsent(time, "ingested_at_field", "ingested_at")), timezone: pythonString(defaultIfAbsent(time, "timezone", "Asia/Shanghai")), availability: normalizeAvailability(time.availability) }, availability_sla: normalizeAvailabilitySla(payload.availability_sla), value_field: pythonString(defaultIfAbsent(payload, "value_field", "value")), quality_rules: normalizeQualityRules(object(defaultIfAbsent(payload, "quality_rules", {}), "parameter quality_rules")), rounding: normalizeRounding(payload.rounding), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) };
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
function normalizeOperator(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "type", "function_hash", "entrypoint", "code_hash", "package_uri", "code_artifact", "input_schema", "output_schema", "config_schema", "runtime", "resources", "deterministic", "supports_batch", "supports_online", "owner"], ["name", "version"], "operator"); const artifact = payload.code_artifact == null ? null : normalizeCodeArtifact(payload.code_artifact); const normalized = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.operator/v1")), name: identifier(payload.name, "operator name"), version: identifier(payload.version, "operator version"), type: choice(defaultIfAbsent(payload, "type", "standard"), ["standard", "custom_python", "spark_udf", "feature"], "operator type"), function_hash: pythonString(defaultIfAbsent(payload, "function_hash", "demo")), entrypoint: optionalString(payload.entrypoint, "operator entrypoint"), code_hash: optionalString(payload.code_hash, "operator code_hash"), package_uri: optionalString(payload.package_uri, "operator package_uri"), code_artifact: artifact, input_schema: object(defaultIfAbsent(payload, "input_schema", {}), "operator input_schema"), output_schema: object(defaultIfAbsent(payload, "output_schema", {}), "operator output_schema"), config_schema: object(defaultIfAbsent(payload, "config_schema", {}), "operator config_schema"), runtime: object(defaultIfAbsent(payload, "runtime", {}), "operator runtime"), resources: object(defaultIfAbsent(payload, "resources", {}), "operator resources"), deterministic: bool(defaultIfAbsent(payload, "deterministic", true), "operator deterministic"), supports_batch: bool(defaultIfAbsent(payload, "supports_batch", true), "operator supports_batch"), supports_online: bool(defaultIfAbsent(payload, "supports_online", false), "operator supports_online"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) }; if (artifact && normalized.code_hash !== artifact.sha256) throw new Error("operator code_hash must equal code_artifact.sha256"); if (artifact && normalized.package_uri !== artifact.uri) throw new Error("operator package_uri must equal code_artifact.uri"); return normalized; }
|
|
68
|
-
function
|
|
69
|
-
function
|
|
69
|
+
function normalizeOperator(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "type", "function_hash", "entrypoint", "code_hash", "package_uri", "code_artifact", "input_schema", "output_schema", "config_schema", "runtime", "resources", "deterministic", "supports_batch", "supports_online", "owner"], ["name", "version"], "operator"); const artifact = payload.code_artifact == null ? null : normalizeCodeArtifact(payload.code_artifact); const normalized = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.operator/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "operator project"), name: identifier(payload.name, "operator name"), version: identifier(payload.version, "operator version"), type: choice(defaultIfAbsent(payload, "type", "standard"), ["standard", "custom_python", "spark_udf", "feature"], "operator type"), function_hash: pythonString(defaultIfAbsent(payload, "function_hash", "demo")), entrypoint: optionalString(payload.entrypoint, "operator entrypoint"), code_hash: optionalString(payload.code_hash, "operator code_hash"), package_uri: optionalString(payload.package_uri, "operator package_uri"), code_artifact: artifact, input_schema: object(defaultIfAbsent(payload, "input_schema", {}), "operator input_schema"), output_schema: object(defaultIfAbsent(payload, "output_schema", {}), "operator output_schema"), config_schema: object(defaultIfAbsent(payload, "config_schema", {}), "operator config_schema"), runtime: object(defaultIfAbsent(payload, "runtime", {}), "operator runtime"), resources: object(defaultIfAbsent(payload, "resources", {}), "operator resources"), deterministic: bool(defaultIfAbsent(payload, "deterministic", true), "operator deterministic"), supports_batch: bool(defaultIfAbsent(payload, "supports_batch", true), "operator supports_batch"), supports_online: bool(defaultIfAbsent(payload, "supports_online", false), "operator supports_online"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) }; if (artifact && normalized.code_hash !== artifact.sha256) throw new Error("operator code_hash must equal code_artifact.sha256"); if (artifact && normalized.package_uri !== artifact.uri) throw new Error("operator package_uri must equal code_artifact.uri"); return normalized; }
|
|
70
|
+
function normalizeRegistryOperator(payload) { const value = object(payload, "operator"); if (!hasOwn(value, "deleted_at")) return normalizeOperator(value); const { deleted_at, ...contract } = value; void deleted_at; return normalizeOperator(contract); }
|
|
71
|
+
function normalizeFeature(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "inputs", "operator", "operator_version", "config", "output_column", "output_dtype", "offline_online_supported", "owner", "description"], ["name", "version", "inputs", "operator", "operator_version", "output_column"], "feature"); const project = identifier(defaultIfAbsent(payload, "project", "default"), "feature project"); const inputs = objectList(payload.inputs, "feature inputs").map((item) => parameterRef(item, project)); if (!inputs.length) throw new Error("feature inputs must not be empty"); unique(inputs.map(parameterReferenceKey), "feature inputs"); if (typeof payload.output_column !== "string" || !outputColumnPattern.test(payload.output_column)) throw new Error("feature output_column is invalid"); return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.feature/v1")), project, name: identifier(payload.name, "feature name"), version: identifier(payload.version, "feature version"), inputs, operator: identifier(payload.operator, "feature operator"), operator_version: identifier(payload.operator_version, "feature operator_version"), config: object(defaultIfAbsent(payload, "config", {}), "feature config"), output_column: payload.output_column, output_dtype: pythonString(defaultIfAbsent(payload, "output_dtype", "float64")), offline_online_supported: bool(defaultIfAbsent(payload, "offline_online_supported", true), "offline_online_supported"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")), description: pythonString(defaultIfAbsent(payload, "description", "")) }; }
|
|
72
|
+
function normalizeFeatureSet(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "features", "owner", "description"], ["name", "version", "features"], "feature set"); const features = objectList(payload.features, "feature set features").map((item) => ref(item, "feature reference")); if (!features.length) throw new Error("feature_set features must not be empty"); unique(features.map(referenceKey), "feature_set features"); return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.feature_set/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "feature set project"), name: identifier(payload.name, "feature set name"), version: identifier(payload.version, "feature set version"), features, owner: pythonString(defaultIfAbsent(payload, "owner", "demo")), description: pythonString(defaultIfAbsent(payload, "description", "")) }; }
|
|
70
73
|
function timestamp(value, label) {
|
|
71
74
|
requiredString(value, label);
|
|
72
75
|
const match = timestampPattern.exec(value);
|
|
@@ -90,15 +93,15 @@ function timestamp(value, label) {
|
|
|
90
93
|
return value;
|
|
91
94
|
}
|
|
92
95
|
function validateReferenceProject(value, parentProject, label) { if (value.project == null) return; const project = identifier(value.project, `${label} project`); if (project !== parentProject) throw new Error(`${label} cross-project reference is not allowed`); }
|
|
93
|
-
function normalizeParameterRequest(value, parentProject) { object(value, "dataset parameter request"); validateReferenceProject(value, parentProject, "dataset parameter request"); let alignment = null; if (value.alignment != null) { object(value.alignment, "parameter alignment"); alignment = { grid: pythonString(defaultIfAbsent(value.alignment, "grid", "10min")), method: choice(defaultIfAbsent(value.alignment, "method", "last_before_or_at"), ["last_before_or_at", "nearest", "none"], "parameter alignment method") }; } return { name: identifier(value.name, "dataset parameter name"), version: identifier(value.version, "dataset parameter version"), alias: optionalString(value.alias, "dataset parameter alias"), alignment, required: bool(defaultIfAbsent(value, "required", true), "dataset parameter required"), missing_policy: choice(defaultIfAbsent(value, "missing_policy", "report_only"), ["report_only", "fail_on_requested_range_gap", "drop_target_and_lookback"], "dataset parameter missing_policy") }; }
|
|
94
|
-
function normalizeMissingPolicy(value, parentProject) { exactKeys(value, ["parameter", "version", "project", "policy"], ["parameter", "version"], "dataset parameter missing policy"); validateReferenceProject(value, parentProject, "dataset parameter missing policy"); return { parameter: identifier(value.parameter, "dataset parameter missing policy parameter"), version: identifier(value.version, "dataset parameter missing policy version"), policy: choice(defaultIfAbsent(value, "policy", "report_only"), ["report_only", "fail_on_requested_range_gap", "drop_target_and_lookback"], "dataset parameter missing policy policy") }; }
|
|
96
|
+
function normalizeParameterRequest(value, parentProject) { object(value, "dataset parameter request"); validateReferenceProject(value, parentProject, "dataset parameter request"); let alignment = null; if (value.alignment != null) { object(value.alignment, "parameter alignment"); alignment = { grid: pythonString(defaultIfAbsent(value.alignment, "grid", "10min")), method: choice(defaultIfAbsent(value.alignment, "method", "last_before_or_at"), ["last_before_or_at", "nearest", "none"], "parameter alignment method") }; } return { name: identifier(value.name, "dataset parameter name"), version: identifier(value.version, "dataset parameter version"), project: parentProject, alias: optionalString(value.alias, "dataset parameter alias"), alignment, required: bool(defaultIfAbsent(value, "required", true), "dataset parameter required"), missing_policy: choice(defaultIfAbsent(value, "missing_policy", "report_only"), ["report_only", "fail_on_requested_range_gap", "drop_target_and_lookback"], "dataset parameter missing_policy") }; }
|
|
97
|
+
function normalizeMissingPolicy(value, parentProject) { exactKeys(value, ["parameter", "version", "project", "policy"], ["parameter", "version"], "dataset parameter missing policy"); validateReferenceProject(value, parentProject, "dataset parameter missing policy"); return { parameter: identifier(value.parameter, "dataset parameter missing policy parameter"), version: identifier(value.version, "dataset parameter missing policy version"), project: parentProject, policy: choice(defaultIfAbsent(value, "policy", "report_only"), ["report_only", "fail_on_requested_range_gap", "drop_target_and_lookback"], "dataset parameter missing policy policy") }; }
|
|
95
98
|
function normalizePreprocess(value) { object(value, "dataset preprocess"); return { name: identifier(value.name, "preprocess name"), version: identifier(value.version, "preprocess version"), function_hash: pythonString(defaultIfAbsent(value, "function_hash", "demo")), config: object(defaultIfAbsent(value, "config", {}), "preprocess config") }; }
|
|
96
|
-
function normalizeTarget(value) { object(value, "dataset target"); const result = structuredClone(value); if (result.parameter && typeof result.parameter === "object" && !Array.isArray(result.parameter))
|
|
99
|
+
function normalizeTarget(value, parentProject) { object(value, "dataset target"); const result = structuredClone(value); if (result.parameter && typeof result.parameter === "object" && !Array.isArray(result.parameter)) result.parameter = parameterRef(result.parameter, parentProject); return result; }
|
|
97
100
|
function distinct(left, right, label) { if (left === right) throw new Error(`${label} inputs must be different`); }
|
|
98
101
|
function normalizeRelationshipCheck(value, label) { object(value, label); const type = choice(value.type, ["compare", "absolute_difference", "ratio_range", "sum_equals", "co_presence"], `${label} type`); if (type === "co_presence") { exactKeys(value, ["type"], ["type"], label); return { type }; } if (type === "compare") { exactKeys(value, ["type", "left", "operator", "right", "tolerance"], ["type", "left", "operator", "right"], label); const left = identifier(value.left, `${label} left`); const right = identifier(value.right, `${label} right`); distinct(left, right, label); return { type, left, operator: choice(value.operator, ["lt", "lte", "gt", "gte", "eq", "ne"], `${label} operator`), right, tolerance: nonNegativeNumber(defaultIfAbsent(value, "tolerance", 0), `${label} tolerance`) }; } if (type === "absolute_difference") { exactKeys(value, ["type", "left", "right", "max_difference"], ["type", "left", "right", "max_difference"], label); const left = identifier(value.left, `${label} left`); const right = identifier(value.right, `${label} right`); distinct(left, right, label); return { type, left, right, max_difference: nonNegativeNumber(value.max_difference, `${label} max_difference`) }; } if (type === "ratio_range") { exactKeys(value, ["type", "numerator", "denominator", "min", "max", "inclusive_min", "inclusive_max"], ["type", "numerator", "denominator"], label); const numerator = identifier(value.numerator, `${label} numerator`); const denominator = identifier(value.denominator, `${label} denominator`); distinct(numerator, denominator, label); const range = validRange([defaultIfAbsent(value, "min", null), defaultIfAbsent(value, "max", null)]); return { type, numerator, denominator, min: range[0], max: range[1], inclusive_min: bool(defaultIfAbsent(value, "inclusive_min", true), `${label} inclusive_min`), inclusive_max: bool(defaultIfAbsent(value, "inclusive_max", true), `${label} inclusive_max`) }; } exactKeys(value, ["type", "terms", "target", "tolerance"], ["type", "terms", "target"], label); if (!Array.isArray(value.terms) || value.terms.some((item) => typeof item !== "string" || !item)) throw new Error(`${label} terms must be a list of non-empty strings`); const terms = value.terms.map((item) => identifier(item, `${label} term`)); if (!terms.length) throw new Error(`${label} terms must not be empty`); unique(terms, `${label} terms`); const target = identifier(value.target, `${label} target`); if (terms.includes(target)) throw new Error(`${label} target must not also be a term`); return { type, terms, target, tolerance: nonNegativeNumber(defaultIfAbsent(value, "tolerance", 0), `${label} tolerance`) }; }
|
|
99
102
|
function relationshipAliases(check) { if (["compare", "absolute_difference"].includes(check.type)) return [check.left, check.right]; if (check.type === "ratio_range") return [check.numerator, check.denominator]; if (check.type === "sum_equals") return [...check.terms, check.target]; return []; }
|
|
100
103
|
function normalizeRelationshipRule(value, relationshipId) { const label = `parameter relationship ${relationshipId} rule`; exactKeys(value, ["id", "stage", "check", "acceptance", "enforcement", "description"], ["id", "check"], label); return { id: identifier(value.id, `${label} id`), stage: choice(defaultIfAbsent(value, "stage", "normalized_source"), ["normalized_source", "post_preprocess", "aligned_grid"], `${label} stage`), check: normalizeRelationshipCheck(value.check, `${label} check`), acceptance: normalizeAcceptance(object(defaultIfAbsent(value, "acceptance", {}), `${label} acceptance`), label), enforcement: choice(defaultIfAbsent(value, "enforcement", "warn"), ["observe", "warn", "fail"], `${label} enforcement`), description: pythonString(defaultIfAbsent(value, "description", "")) }; }
|
|
101
|
-
function normalizeParameterRelationship(value) { exactKeys(value, ["id", "inputs", "alignment", "rules", "description"], ["id", "inputs", "rules"], "parameter relationship"); const id = identifier(value.id, "parameter relationship id"); const inputs = objectList(value.inputs, "parameter relationship inputs").map((item) => { exactKeys(item, ["alias", "parameter", "version"], ["alias", "parameter", "version"], "parameter relationship input"); return { alias: identifier(item.alias, "parameter relationship input alias"), parameter: identifier(item.parameter, "parameter relationship input parameter"), version: identifier(item.version, "parameter relationship input version") }; }); if (inputs.length < 2) throw new Error("parameter relationship requires at least two inputs"); unique(inputs.map((item) => item.alias), "parameter relationship input aliases"); unique(inputs.map(parameterReferenceKey), "parameter relationship inputs"); const alignmentValue = object(defaultIfAbsent(value, "alignment", {}), "parameter relationship alignment"); exactKeys(alignmentValue, ["method", "tolerance"], [], "parameter relationship alignment"); const tolerance = optionalDuration(alignmentValue.tolerance, "parameter relationship alignment tolerance"); if (tolerance != null && durationSeconds(tolerance) === 0) throw new Error("parameter relationship alignment tolerance must be positive"); const alignment = { method: choice(defaultIfAbsent(alignmentValue, "method", "last_before_or_at"), ["exact", "last_before_or_at"], "parameter relationship alignment method"), tolerance }; const rules = objectList(value.rules, "parameter relationship rules").map((item) => normalizeRelationshipRule(item, id)); if (!rules.length) throw new Error("parameter relationship rules must not be empty"); unique(rules.map((item) => item.id), `parameter relationship ${id} rule ids`); const aliases = new Set(inputs.map((item) => item.alias)); for (const rule of rules) { if (rule.stage !== "aligned_grid") throw new Error(`parameter relationship ${id} rule ${rule.id} must use stage=aligned_grid`); const missing = relationshipAliases(rule.check).filter((item) => !aliases.has(item)); if (missing.length) throw new Error(`parameter relationship ${id} rule ${rule.id} references undeclared aliases: ${missing.join(", ")}`); } return { id, inputs, alignment, rules, description: pythonString(defaultIfAbsent(value, "description", "")) }; }
|
|
104
|
+
function normalizeParameterRelationship(value, parentProject) { exactKeys(value, ["id", "inputs", "alignment", "rules", "description"], ["id", "inputs", "rules"], "parameter relationship"); const id = identifier(value.id, "parameter relationship id"); const inputs = objectList(value.inputs, "parameter relationship inputs").map((item) => { exactKeys(item, ["alias", "parameter", "version", "project"], ["alias", "parameter", "version"], "parameter relationship input"); validateReferenceProject(item, parentProject, "parameter relationship input"); return { alias: identifier(item.alias, "parameter relationship input alias"), parameter: identifier(item.parameter, "parameter relationship input parameter"), version: identifier(item.version, "parameter relationship input version"), project: parentProject }; }); if (inputs.length < 2) throw new Error("parameter relationship requires at least two inputs"); unique(inputs.map((item) => item.alias), "parameter relationship input aliases"); unique(inputs.map(parameterReferenceKey), "parameter relationship inputs"); const alignmentValue = object(defaultIfAbsent(value, "alignment", {}), "parameter relationship alignment"); exactKeys(alignmentValue, ["method", "tolerance"], [], "parameter relationship alignment"); const tolerance = optionalDuration(alignmentValue.tolerance, "parameter relationship alignment tolerance"); if (tolerance != null && durationSeconds(tolerance) === 0) throw new Error("parameter relationship alignment tolerance must be positive"); const alignment = { method: choice(defaultIfAbsent(alignmentValue, "method", "last_before_or_at"), ["exact", "last_before_or_at"], "parameter relationship alignment method"), tolerance }; const rules = objectList(value.rules, "parameter relationship rules").map((item) => normalizeRelationshipRule(item, id)); if (!rules.length) throw new Error("parameter relationship rules must not be empty"); unique(rules.map((item) => item.id), `parameter relationship ${id} rule ids`); const aliases = new Set(inputs.map((item) => item.alias)); for (const rule of rules) { if (rule.stage !== "aligned_grid") throw new Error(`parameter relationship ${id} rule ${rule.id} must use stage=aligned_grid`); const missing = relationshipAliases(rule.check).filter((item) => !aliases.has(item)); if (missing.length) throw new Error(`parameter relationship ${id} rule ${rule.id} references undeclared aliases: ${missing.join(", ")}`); } return { id, inputs, alignment, rules, description: pythonString(defaultIfAbsent(value, "description", "")) }; }
|
|
102
105
|
function normalizeDataset(payload) {
|
|
103
106
|
object(payload, "dataset"); payload = Object.fromEntries(Object.entries(payload).filter(([name]) => name !== "deleted_at"));
|
|
104
107
|
exactKeys(payload, ["schema_version", "project", "dataset_id", "dataset_version", "mode", "read_policy", "snapshot_id", "as_of", "time_range", "parameters", "feature_set", "preprocess", "output", "source_read", "realtime_fetch", "abnormal_windows", "prediction", "target", "parameter_relationships", "parameter_missing_policies", "rowset", "rowset_splits", "endpoint_policy"], ["dataset_id", "time_range", "feature_set"], "dataset");
|
|
@@ -116,14 +119,27 @@ function normalizeDataset(payload) {
|
|
|
116
119
|
unique(missingPolicies.map(parameterReferenceKey), "dataset parameter_missing_policies Parameter references");
|
|
117
120
|
const overlap = parameters.map(referenceKey).filter((item) => new Set(missingPolicies.map(parameterReferenceKey)).has(item)); if (overlap.length) throw new Error("dataset parameter_missing_policies is only for Feature-only Parameters");
|
|
118
121
|
if (payload.endpoint_policy != null) { const drops = [...parameters.filter((item) => item.missing_policy === "drop_target_and_lookback"), ...missingPolicies.filter((item) => item.policy === "drop_target_and_lookback")]; if (drops.length) throw new Error("dataset endpoint_policy is incompatible with drop_target_and_lookback"); }
|
|
119
|
-
const output = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.dataset_manifest/v1")), dataset_id: identifier(payload.dataset_id, "dataset id"), dataset_version: identifier(defaultIfAbsent(payload, "dataset_version", "v1"), "dataset version"), mode: choice(defaultIfAbsent(payload, "mode", "training"), ["training", "inference"], "dataset mode"), read_policy: readPolicy, snapshot_id: snapshotId, as_of: asOf, time_range: { start: timestamp(time.start, "dataset time_range start"), end: timestamp(time.end, "dataset time_range end"), grid: pythonString(defaultIfAbsent(time, "grid", "10min")) }, parameters, feature_set: ref(object(payload.feature_set, "dataset feature set"), "dataset feature set"), preprocess: objectList(defaultIfAbsent(payload, "preprocess", []), "dataset preprocess").map(normalizePreprocess), output: object(defaultIfAbsent(payload, "output", {}), "dataset output") };
|
|
122
|
+
const output = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.dataset_manifest/v1")), project: parentProject, dataset_id: identifier(payload.dataset_id, "dataset id"), dataset_version: identifier(defaultIfAbsent(payload, "dataset_version", "v1"), "dataset version"), mode: choice(defaultIfAbsent(payload, "mode", "training"), ["training", "inference"], "dataset mode"), read_policy: readPolicy, snapshot_id: snapshotId, as_of: asOf, time_range: { start: timestamp(time.start, "dataset time_range start"), end: timestamp(time.end, "dataset time_range end"), grid: pythonString(defaultIfAbsent(time, "grid", "10min")) }, parameters, feature_set: ref(object(payload.feature_set, "dataset feature set"), "dataset feature set"), preprocess: objectList(defaultIfAbsent(payload, "preprocess", []), "dataset preprocess").map(normalizePreprocess), output: object(defaultIfAbsent(payload, "output", {}), "dataset output") };
|
|
120
123
|
if (missingPolicies.length) output.parameter_missing_policies = missingPolicies;
|
|
121
124
|
for (const name of ["source_read", "realtime_fetch", "abnormal_windows", "prediction", "rowset", "rowset_splits", "endpoint_policy"]) if (payload[name] != null) output[name] = payload[name];
|
|
122
|
-
if (payload.target != null) output.target = normalizeTarget(payload.target);
|
|
123
|
-
const relationships = objectList(defaultIfAbsent(payload, "parameter_relationships", []), "dataset parameter_relationships").map(normalizeParameterRelationship); unique(relationships.map((item) => item.id), "dataset parameter_relationship ids"); if (relationships.length) output.parameter_relationships = relationships;
|
|
125
|
+
if (payload.target != null) output.target = normalizeTarget(payload.target, parentProject);
|
|
126
|
+
const relationships = objectList(defaultIfAbsent(payload, "parameter_relationships", []), "dataset parameter_relationships").map((item) => normalizeParameterRelationship(item, parentProject)); unique(relationships.map((item) => item.id), "dataset parameter_relationship ids"); if (relationships.length) output.parameter_relationships = relationships;
|
|
124
127
|
return output;
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
function normalizeRegistryDataset(payload) {
|
|
131
|
+
const value = object(payload, "registered dataset");
|
|
132
|
+
const responseOnly = ["entity_keys", "label_materializations"];
|
|
133
|
+
const contract = Object.fromEntries(Object.entries(value).filter(([name]) => !responseOnly.includes(name)));
|
|
134
|
+
const normalized = normalizeDataset(contract);
|
|
135
|
+
for (const name of responseOnly) {
|
|
136
|
+
if (!hasOwn(value, name)) continue;
|
|
137
|
+
if (!Array.isArray(value[name])) throw new Error(`registered dataset ${name} must be a list`);
|
|
138
|
+
normalized[name] = structuredClone(value[name]);
|
|
139
|
+
}
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
function assetPath(root, relative, suffix) { if (typeof relative !== "string" || !relative) throw new Error("catalog asset path must be a non-empty string"); if (path.isAbsolute(relative)) throw new Error(`catalog asset path must be relative: ${relative}`); const target = path.resolve(root, relative); if (path.extname(target).toLowerCase() !== suffix) throw new Error(`catalog asset must use ${suffix}: ${relative}`); if (!target.startsWith(`${root}${path.sep}`)) throw new Error(`catalog asset path escapes catalog directory: ${relative}`); if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error(`catalog asset not found: ${target}`); return target; }
|
|
128
144
|
function readJson(file) { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { throw new Error(`cannot read JSON asset ${file}: ${error.message}`); } }
|
|
129
145
|
function readObject(file) { return object(readJson(file), `JSON file ${file}`); }
|
|
@@ -145,13 +161,13 @@ function loadCatalog(directory) {
|
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
async function fetchAll(client, endpoint, params = {}) { const rows = []; for (let offset = 0; ; offset += 500) { const response = await client.get(endpoint, { ...params, limit: 500, offset }); if (Array.isArray(response)) return response; const page = response.items; if (!Array.isArray(page)) throw new Error(`${endpoint} returned an invalid list response`); rows.push(...page); if (response.total == null || rows.length >= response.total || page.length < 500) return rows; } }
|
|
148
|
-
async function snapshot(client, targetProject) { const scope =
|
|
164
|
+
async function snapshot(client, targetProject) { const scope = { project: targetProject ?? "default" }; const [parameters, operators, features, featureSets, datasets] = await Promise.all([fetchAll(client, "/parameters", scope), fetchAll(client, "/operators", scope), fetchAll(client, "/features", scope), fetchAll(client, "/feature-sets", scope), fetchAll(client, "/datasets", { ...scope, include_deleted: true })]); return { parameters: new Map(parameters.map((x) => [key(normalizeParameter(x)), normalizeParameter(x)])), operators: new Map(operators.map((x) => [key(normalizeRegistryOperator(x)), normalizeRegistryOperator(x)])), features: new Map(features.map((x) => [key(normalizeFeature(x)), normalizeFeature(x)])), feature_sets: new Map(featureSets.map((x) => [key(normalizeFeatureSet(x)), normalizeFeatureSet(x)])), datasets: new Map(datasets.filter((x) => !x.deleted_at).map((x) => [datasetKey(normalizeRegistryDataset(x)), normalizeRegistryDataset(x)])) }; }
|
|
149
165
|
|
|
150
166
|
function requiresPackage(spec) { return ["custom_python", "feature", "spark_udf"].includes(spec.type) && spec.code_artifact == null && !String(spec.package_uri ?? "").startsWith("pythonpath://"); }
|
|
151
167
|
function validateCatalog(catalog, current) {
|
|
152
168
|
const parameters = new Set([...current.parameters.keys(), ...catalog.parameters.map(key)]); const operators = new Map([...current.operators, ...catalog.operators.map((x) => [key(x.spec), x.spec])]); const features = new Map([...current.features, ...catalog.features.map((x) => [key(x), x])]); const featureSets = new Map([...current.feature_sets, ...catalog.feature_sets.map((x) => [key(x), x])]);
|
|
153
169
|
for (const item of catalog.operators) if (!item.package_path && requiresPackage(item.spec)) throw new Error(`catalog operator ${key(item.spec)} requires a wheel package path`);
|
|
154
|
-
for (const feature of catalog.features) { for (const input of feature.inputs) if (!parameters.has(parameterReferenceKey(input))) throw new Error(`catalog feature ${key(feature)} references missing parameter ${parameterReferenceKey(input)}`); const operatorKey = `${feature.operator}:${feature.operator_version}`; const operator = operators.get(operatorKey); if (!operator) throw new Error(`catalog feature ${key(feature)} references missing operator ${operatorKey}`); if (operator.type !== "feature") throw new Error(`catalog feature ${key(feature)} requires OperatorSpec.type='feature'`); const declaredInputs = new Set(operator.input_schema.parameters ?? []); if (declaredInputs.size) for (const input of feature.inputs) if (!declaredInputs.has(
|
|
170
|
+
for (const feature of catalog.features) { for (const input of feature.inputs) if (!parameters.has(parameterReferenceKey(input))) throw new Error(`catalog feature ${key(feature)} references missing parameter ${parameterReferenceKey(input)}`); const operatorKey = `${feature.operator}:${feature.operator_version}`; const operator = operators.get(operatorKey); if (!operator) throw new Error(`catalog feature ${key(feature)} references missing operator ${operatorKey}`); if (operator.type !== "feature") throw new Error(`catalog feature ${key(feature)} requires OperatorSpec.type='feature'`); const declaredInputs = new Set(operator.input_schema.parameters ?? []); if (declaredInputs.size) for (const input of feature.inputs) { const inputKey = parameterReferenceKey(input); const qualifiedInputKey = `${operator.project ?? feature.project ?? "default"}/${inputKey}`; if (!declaredInputs.has(inputKey) && !declaredInputs.has(qualifiedInputKey)) throw new Error(`catalog feature ${key(feature)} input is not declared by ${operatorKey}`); } const columns = operator.output_schema.columns ?? []; if (columns.length && !columns.includes(feature.output_column)) throw new Error(`catalog feature ${key(feature)} output_column is not declared by ${operatorKey}`); }
|
|
155
171
|
for (const set of catalog.feature_sets) { const names = []; for (const item of set.features) { const feature = features.get(referenceKey(item)); if (!feature) throw new Error(`catalog feature set ${key(set)} references missing feature ${referenceKey(item)}`); names.push(feature.name); } unique(names, `catalog feature set ${key(set)} final output names`); }
|
|
156
172
|
for (const dataset of catalog.datasets) {
|
|
157
173
|
const datasetId = datasetKey(dataset); const declared = new Set();
|
|
@@ -163,22 +179,29 @@ function validateCatalog(catalog, current) {
|
|
|
163
179
|
for (const item of dataset.preprocess) if (!operators.has(referenceKey(item))) throw new Error(`catalog dataset ${datasetId} references missing preprocess operator ${referenceKey(item)}`);
|
|
164
180
|
}
|
|
165
181
|
}
|
|
182
|
+
function validateCatalogProject(catalog, targetProject) {
|
|
183
|
+
const projects = [...catalog.parameters, ...catalog.operators.map((item) => item.spec), ...catalog.features, ...catalog.feature_sets, ...catalog.datasets].map((item) => item.project ?? "default");
|
|
184
|
+
const uniqueProjects = [...new Set(projects)];
|
|
185
|
+
if (uniqueProjects.length > 1) throw new Error(`catalog contains multiple projects: ${uniqueProjects.sort().join(", ")}`);
|
|
186
|
+
const expected = targetProject ?? "default";
|
|
187
|
+
if (uniqueProjects.length && uniqueProjects[0] !== expected) throw new Error(`catalog project ${uniqueProjects[0]} does not match target project ${expected}`);
|
|
188
|
+
}
|
|
166
189
|
function canonical(value) { if (Array.isArray(value)) return value.map(canonical); if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((name) => [name, canonical(value[name])])); return value; }
|
|
167
190
|
function equal(left, right) { return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); }
|
|
168
191
|
function assertImmutable(label, assetKey, candidate, existing) { if (existing && !equal(candidate, existing)) { const error = new Error(`${label} version is immutable: ${assetKey}`); error.code = "EEXIST"; throw error; } }
|
|
169
192
|
function sha256File(file) { return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`; }
|
|
170
193
|
|
|
171
194
|
async function applyCatalog(catalog, client, dryRun, targetProject) {
|
|
172
|
-
const current = await snapshot(client, targetProject); validateCatalog(catalog, current); const groups = { parameters: catalog.parameters.length, operators: catalog.operators.length, features: catalog.features.length, feature_sets: catalog.feature_sets.length, datasets: catalog.datasets.length }; const summary = Object.fromEntries(Object.entries(groups).map(([name, count]) => [name, { declared: count, planned: 0, applied: 0, unchanged: 0 }]));
|
|
195
|
+
validateCatalogProject(catalog, targetProject); const current = await snapshot(client, targetProject); validateCatalog(catalog, current); const groups = { parameters: catalog.parameters.length, operators: catalog.operators.length, features: catalog.features.length, feature_sets: catalog.feature_sets.length, datasets: catalog.datasets.length }; const summary = Object.fromEntries(Object.entries(groups).map(([name, count]) => [name, { declared: count, planned: 0, applied: 0, unchanged: 0 }]));
|
|
173
196
|
for (const asset of catalog.parameters) assertImmutable("parameter", key(asset), asset, current.parameters.get(key(asset)));
|
|
174
197
|
for (const item of catalog.operators) { const existing = current.operators.get(key(item.spec)); if (!existing) continue; let candidate = item.spec; if (item.package_path) { if (existing.code_hash !== sha256File(item.package_path) || !existing.code_artifact) throw new Error(`operator version is immutable: ${key(item.spec)}; publish a new version`); candidate = normalizeOperator({ ...item.spec, code_hash: existing.code_artifact.sha256, package_uri: existing.code_artifact.uri, code_artifact: existing.code_artifact }); } assertImmutable("operator", key(item.spec), candidate, existing); }
|
|
175
198
|
const plain = async (assets, existing, label, endpoint, keyFn = key, params) => { for (const asset of assets) { const assetKey = keyFn(asset); assertImmutable(label, assetKey, asset, existing.get(assetKey)); if (existing.has(assetKey)) summary[label].unchanged += 1; else if (dryRun) summary[label].planned += 1; else { await client.post(endpoint, asset, params); summary[label].applied += 1; } } };
|
|
176
199
|
try {
|
|
177
200
|
await plain(catalog.parameters, current.parameters, "parameters", "/parameters");
|
|
178
|
-
for (const item of catalog.operators) { const assetKey = key(item.spec); if (current.operators.has(assetKey)) summary.operators.unchanged += 1; else if (dryRun) summary.operators.planned += 1; else { let candidate = item.spec; if (item.package_path) { const uploaded = await client.uploadOperatorPackage(
|
|
201
|
+
for (const item of catalog.operators) { const assetKey = key(item.spec); if (current.operators.has(assetKey)) summary.operators.unchanged += 1; else if (dryRun) summary.operators.planned += 1; else { let candidate = item.spec; if (item.package_path) { const uploaded = await client.uploadOperatorPackage(item.spec.project, item.spec.name, item.spec.version, item.package_path); candidate = normalizeOperator({ ...item.spec, code_hash: uploaded.code_artifact.sha256, package_uri: uploaded.code_artifact.uri, code_artifact: uploaded.code_artifact }); } await client.post("/operators", candidate); summary.operators.applied += 1; } }
|
|
179
202
|
await plain(catalog.features, current.features, "features", "/features"); await plain(catalog.feature_sets, current.feature_sets, "feature_sets", "/feature-sets"); await plain(catalog.datasets, current.datasets, "datasets", "/datasets", datasetKey, { force: false });
|
|
180
203
|
} catch (error) { throw new Error(`catalog apply failed after progress=${JSON.stringify(canonical(summary))}: ${error.message}`); }
|
|
181
204
|
return { status: dryRun ? "validated" : "applied", profile: "server", catalog: { name: catalog.manifest.name, version: catalog.manifest.version, path: catalog.path }, summary };
|
|
182
205
|
}
|
|
183
206
|
|
|
184
|
-
module.exports = { applyCatalog, fetchAll, loadCatalog, normalizeDataset, normalizeFeature, normalizeFeatureSet, normalizeOperator, normalizeParameter, validateCatalog };
|
|
207
|
+
module.exports = { applyCatalog, fetchAll, loadCatalog, normalizeDataset, normalizeFeature, normalizeFeatureSet, normalizeOperator, normalizeRegistryDataset, normalizeRegistryOperator, normalizeParameter, validateCatalog, validateCatalogProject };
|
|
@@ -6,14 +6,18 @@ const fs = require("fs");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { configPath, expandHome, loadApiUrl, normalizeApiUrl, saveApiUrl } = require("./config");
|
|
8
8
|
const { PlatformApiClient } = require("./http");
|
|
9
|
-
const { applyCatalog, loadCatalog, normalizeDataset } = require("./catalog");
|
|
9
|
+
const { applyCatalog, loadCatalog, normalizeDataset, normalizeRegistryDataset } = require("./catalog");
|
|
10
10
|
const { version: CLIENT_VERSION } = require("../package.json");
|
|
11
11
|
|
|
12
12
|
const BUSINESS_COMMANDS = new Set([
|
|
13
13
|
"version", "create-project", "list-projects", "get-project", "delete-project",
|
|
14
14
|
"configure", "show-config", "health", "list-parameters", "list-operators", "list-features",
|
|
15
|
-
"list-feature-sets", "list-datasets", "
|
|
16
|
-
"
|
|
15
|
+
"list-feature-sets", "list-datasets", "get-dataset", "list-jobs", "resolve-manifest",
|
|
16
|
+
"resolve-dataset", "build-dataset", "build-registered-dataset",
|
|
17
|
+
"add-parameter", "add-feature", "add-feature-set", "add-operator", "publish-operator",
|
|
18
|
+
"add-dataset", "update-dataset", "delete-dataset", "list-dataset-artifacts",
|
|
19
|
+
"register-operator", "show-operator-specs", "delete-operator", "run-operator",
|
|
20
|
+
"get-job", "wait-job", "retry-job", "cancel-job", "get-dataset-artifact", "download-dataset-artifact", "fetch-inference-data", "apply",
|
|
17
21
|
]);
|
|
18
22
|
|
|
19
23
|
const COMMAND_USAGE = {
|
|
@@ -29,12 +33,30 @@ const COMMAND_USAGE = {
|
|
|
29
33
|
"list-operators": "list-operators [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
30
34
|
"list-features": "list-features [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
31
35
|
"list-feature-sets": "list-feature-sets [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
32
|
-
"list-datasets": "list-datasets [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
36
|
+
"list-datasets": "list-datasets [--project PROJECT] [-q QUERY] [--limit N] [--offset N] [--include-deleted]",
|
|
37
|
+
"add-parameter": "add-parameter SPEC_JSON",
|
|
38
|
+
"add-feature": "add-feature SPEC_JSON",
|
|
39
|
+
"add-feature-set": "add-feature-set SPEC_JSON",
|
|
40
|
+
"add-operator": "add-operator SPEC_JSON",
|
|
41
|
+
"publish-operator": "publish-operator SPEC_JSON --package PATH",
|
|
42
|
+
"add-dataset": "add-dataset MANIFEST_JSON [--force]",
|
|
43
|
+
"update-dataset": "update-dataset MANIFEST_JSON [--force]",
|
|
44
|
+
"delete-dataset": "delete-dataset DATASET_ID DATASET_VERSION [--project PROJECT]",
|
|
45
|
+
"list-dataset-artifacts": "list-dataset-artifacts [--project PROJECT] [--dataset-id ID] [--dataset-version VERSION] [--include-deleted] [--limit N] [--offset N]",
|
|
46
|
+
"get-dataset": "get-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--include-deleted]",
|
|
33
47
|
"list-jobs": "list-jobs [-q QUERY] [--limit N] [--offset N]",
|
|
34
48
|
"resolve-manifest": "resolve-manifest MANIFEST [--out PATH]",
|
|
35
|
-
"
|
|
49
|
+
"resolve-dataset": "resolve-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--out PATH]",
|
|
50
|
+
"build-dataset": "build-dataset MANIFEST [--engine chronon] [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
|
|
51
|
+
"build-registered-dataset": "build-registered-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--engine chronon] [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
|
|
36
52
|
"get-job": "get-job JOB_ID",
|
|
37
53
|
"wait-job": "wait-job JOB_ID [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
|
|
54
|
+
"retry-job": "retry-job JOB_ID",
|
|
55
|
+
"cancel-job": "cancel-job JOB_ID",
|
|
56
|
+
"register-operator": "register-operator SPEC_JSON",
|
|
57
|
+
"show-operator-specs": "show-operator-specs [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
58
|
+
"delete-operator": "delete-operator NAME VERSION [--project PROJECT]",
|
|
59
|
+
"run-operator": "run-operator NAME VERSION [--project PROJECT] [--config JSON]",
|
|
38
60
|
"get-dataset-artifact": "get-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT]",
|
|
39
61
|
"download-dataset-artifact": "download-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT] --out-dir PATH [--force]",
|
|
40
62
|
"fetch-inference-data": "fetch-inference-data MANIFEST --cutoff-time TIMESTAMP [--max-workers N] [--max-source-lag-hours HOURS] [--no-validate-freshness] [--allow-missing]",
|
|
@@ -48,7 +70,10 @@ function usage(command) {
|
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
function parse(args) {
|
|
51
|
-
const
|
|
73
|
+
const timeoutValue = process.env.ML_PLATFORM_API_TIMEOUT_SECONDS
|
|
74
|
+
|| process.env.DATA_PLATFORM_DEMO_API_TIMEOUT_SECONDS
|
|
75
|
+
|| 30;
|
|
76
|
+
const options = { profile: "server", timeout: finiteNumber(timeoutValue, "--request-timeout-seconds") };
|
|
52
77
|
let index = 0;
|
|
53
78
|
while (index < args.length && args[index].startsWith("--")) {
|
|
54
79
|
const flag = args[index++];
|
|
@@ -62,7 +87,7 @@ function parse(args) {
|
|
|
62
87
|
if (!(options.timeout > 0)) throw new Error("--request-timeout-seconds must be positive");
|
|
63
88
|
options.command = args[index++];
|
|
64
89
|
options.rest = args.slice(index);
|
|
65
|
-
options.apiUrl ??= process.env.ML_PLATFORM_API_URL || loadApiUrl();
|
|
90
|
+
options.apiUrl ??= process.env.ML_PLATFORM_API_URL || process.env.DATA_PLATFORM_DEMO_API_URL || loadApiUrl();
|
|
66
91
|
return options;
|
|
67
92
|
}
|
|
68
93
|
|
|
@@ -101,7 +126,7 @@ function positional(rest, label) { const value = rest.shift(); if (!value || val
|
|
|
101
126
|
|
|
102
127
|
function client(options) {
|
|
103
128
|
if (options.profile !== "server") throw new Error("the business client supports only --profile server");
|
|
104
|
-
if (!options.apiUrl) throw new Error("server profile requires --api-url, ML_PLATFORM_API_URL, or a saved configure target");
|
|
129
|
+
if (!options.apiUrl) throw new Error("server profile requires --api-url, ML_PLATFORM_API_URL, DATA_PLATFORM_DEMO_API_URL, or a saved configure target");
|
|
105
130
|
return new PlatformApiClient(options.apiUrl, options.timeout);
|
|
106
131
|
}
|
|
107
132
|
|
|
@@ -112,6 +137,54 @@ function readObject(file) {
|
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
function readDataset(file) { return normalizeDataset(readObject(file)); }
|
|
140
|
+
function readSpec(file) { return readObject(file); }
|
|
141
|
+
|
|
142
|
+
function datasetEndpoint(project, datasetId, datasetVersion) {
|
|
143
|
+
return `/datasets/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(datasetVersion)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function registeredDataset(api, project, datasetId, datasetVersion, includeDeleted = false) {
|
|
147
|
+
const payload = await api.get(datasetEndpoint(project, datasetId, datasetVersion), { include_deleted: includeDeleted });
|
|
148
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("registered dataset response must be a JSON object");
|
|
149
|
+
return payload;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildOptions(rest) {
|
|
153
|
+
const engine = take(rest, "--engine", "chronon");
|
|
154
|
+
if (engine !== "chronon") throw new Error("--engine must be chronon");
|
|
155
|
+
const sourceMode = take(rest, "--source-mode");
|
|
156
|
+
if (sourceMode !== undefined && sourceMode !== "direct") throw new Error("--source-mode must be direct");
|
|
157
|
+
const maxParallelism = integer(take(rest, "--max-parallelism", 1), "--max-parallelism");
|
|
158
|
+
if (maxParallelism < 1) throw new Error("--max-parallelism must be at least 1");
|
|
159
|
+
const wait = boolean(rest, "--wait");
|
|
160
|
+
const pollInterval = finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval");
|
|
161
|
+
if (!(pollInterval > 0)) throw new Error("--poll-interval must be positive");
|
|
162
|
+
const rawTimeout = take(rest, "--wait-timeout-seconds");
|
|
163
|
+
const waitTimeout = rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds");
|
|
164
|
+
if (waitTimeout !== undefined && !(waitTimeout > 0)) throw new Error("--wait-timeout-seconds must be positive");
|
|
165
|
+
return {
|
|
166
|
+
engine,
|
|
167
|
+
sourceMode,
|
|
168
|
+
upload: boolean(rest, "--upload-chronon-metadata"),
|
|
169
|
+
partitionDuration: take(rest, "--partition-duration"),
|
|
170
|
+
maxParallelism,
|
|
171
|
+
resume: !boolean(rest, "--no-resume"),
|
|
172
|
+
wait,
|
|
173
|
+
pollInterval,
|
|
174
|
+
waitTimeout,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function submitBuild(api, manifest, build) {
|
|
179
|
+
let result = await api.post("/datasets/build", manifest, { engine: build.engine, source_mode: build.sourceMode, upload_chronon_metadata: build.upload, partition_duration: build.partitionDuration, max_parallelism: build.maxParallelism, resume: build.resume });
|
|
180
|
+
let exitCode = 0;
|
|
181
|
+
if (build.wait) {
|
|
182
|
+
if (!result || typeof result !== "object" || !result.job_id) throw new Error("server build did not return a job_id to wait for");
|
|
183
|
+
result = await waitForJob(api, String(result.job_id), build.pollInterval, build.waitTimeout);
|
|
184
|
+
if (["failed", "cancelled"].includes(result.status)) exitCode = 1;
|
|
185
|
+
}
|
|
186
|
+
return { result, exitCode };
|
|
187
|
+
}
|
|
115
188
|
|
|
116
189
|
async function waitForJob(api, jobId, pollInterval, timeoutSeconds) {
|
|
117
190
|
if (!(pollInterval > 0)) throw new Error("job poll interval must be positive");
|
|
@@ -157,12 +230,28 @@ async function runBusinessCli(argv) {
|
|
|
157
230
|
} else if (options.command === "list-projects") result = await client(options).get("/projects");
|
|
158
231
|
else if (options.command === "get-project") result = await client(options).get(`/projects/${encodeURIComponent(positional(rest, "project name"))}`);
|
|
159
232
|
else if (options.command === "delete-project") result = await client(options).delete(`/projects/${encodeURIComponent(positional(rest, "project name"))}`);
|
|
160
|
-
else if (
|
|
233
|
+
else if (["add-parameter", "add-feature", "add-feature-set", "add-operator", "register-operator"].includes(options.command)) {
|
|
234
|
+
const spec = readSpec(positional(rest, "spec JSON"));
|
|
235
|
+
const endpoint = { "add-parameter": "/parameters", "add-feature": "/features", "add-feature-set": "/feature-sets", "add-operator": "/operators", "register-operator": "/operators" }[options.command];
|
|
236
|
+
result = await client(options).post(endpoint, spec);
|
|
237
|
+
} else if (options.command === "publish-operator") {
|
|
238
|
+
const specPath = positional(rest, "OperatorSpec JSON"); const packagePath = take(rest, "--package");
|
|
239
|
+
if (!packagePath) throw new Error("--package is required");
|
|
240
|
+
const spec = readSpec(specPath); const api = client(options); const project = spec.project || "default";
|
|
241
|
+
const upload = await api.uploadOperatorPackage(project, spec.name, spec.version, packagePath);
|
|
242
|
+
if (!upload || typeof upload.code_artifact !== "object") throw new Error("operator package upload response has no code_artifact");
|
|
243
|
+
const published = { ...spec, code_hash: upload.code_artifact.sha256, package_uri: upload.code_artifact.uri, code_artifact: upload.code_artifact };
|
|
244
|
+
result = { status: "published", profile: "server", path: null, operator: published, registration: await api.post("/operators", published) };
|
|
245
|
+
} else if (options.command === "get-dataset") {
|
|
246
|
+
const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default");
|
|
247
|
+
result = await registeredDataset(client(options), project, datasetId, datasetVersion, boolean(rest, "--include-deleted"));
|
|
248
|
+
}
|
|
249
|
+
else if (options.command.startsWith("list-") && !["list-jobs", "list-dataset-artifacts"].includes(options.command)) {
|
|
161
250
|
const endpoints = { "list-parameters": "/parameters", "list-operators": "/operators", "list-features": "/features", "list-feature-sets": "/feature-sets", "list-datasets": "/datasets" };
|
|
162
251
|
const project = take(rest, "--project");
|
|
163
|
-
const query = take(rest, "--q", take(rest, "-q")); const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
|
|
164
|
-
if (rawLimit !== undefined || rawOffset !== undefined) { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get(endpoints[options.command], { q: query, limit, offset, project }); }
|
|
165
|
-
else result = await fetchAll(client(options), endpoints[options.command], { q: query, project });
|
|
252
|
+
const query = take(rest, "--q", take(rest, "-q")); const includeDeleted = options.command === "list-datasets" && boolean(rest, "--include-deleted"); const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
|
|
253
|
+
if (rawLimit !== undefined || rawOffset !== undefined) { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get(endpoints[options.command], { q: query, limit, offset, project, include_deleted: includeDeleted }); }
|
|
254
|
+
else result = await fetchAll(client(options), endpoints[options.command], { q: query, project, include_deleted: includeDeleted });
|
|
166
255
|
} else if (options.command === "list-jobs") {
|
|
167
256
|
const query = take(rest, "--q", take(rest, "-q")); const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
|
|
168
257
|
if (rawLimit !== undefined || rawOffset !== undefined) { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get("/jobs", { q: query, limit, offset }); }
|
|
@@ -173,26 +262,52 @@ async function runBusinessCli(argv) {
|
|
|
173
262
|
result = await client(options).post("/datasets/resolve", readDataset(manifest));
|
|
174
263
|
const out = take(rest, "--out");
|
|
175
264
|
if (out) { const target = resolvedPath(out); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, `${JSON.stringify(result, null, 2)}\n`); }
|
|
265
|
+
} else if (options.command === "resolve-dataset") {
|
|
266
|
+
const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default"); const api = client(options);
|
|
267
|
+
const manifest = normalizeRegistryDataset(await registeredDataset(api, project, datasetId, datasetVersion));
|
|
268
|
+
result = await api.post("/datasets/resolve", manifest);
|
|
269
|
+
const out = take(rest, "--out");
|
|
270
|
+
if (out) { const target = resolvedPath(out); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, `${JSON.stringify(result, null, 2)}\n`); }
|
|
176
271
|
} else if (options.command === "build-dataset") {
|
|
177
272
|
const manifest = positional(rest, "manifest");
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
273
|
+
const build = buildOptions(rest); const api = client(options);
|
|
274
|
+
({ result, exitCode } = await submitBuild(api, readDataset(manifest), build));
|
|
275
|
+
} else if (options.command === "build-registered-dataset") {
|
|
276
|
+
const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default"); const build = buildOptions(rest); const api = client(options);
|
|
277
|
+
const manifest = normalizeRegistryDataset(await registeredDataset(api, project, datasetId, datasetVersion));
|
|
278
|
+
({ result, exitCode } = await submitBuild(api, manifest, build));
|
|
279
|
+
} else if (["add-dataset", "update-dataset"].includes(options.command)) {
|
|
280
|
+
const manifest = readDataset(positional(rest, "manifest JSON")); const force = boolean(rest, "--force"); const project = manifest.project || "default";
|
|
281
|
+
if (options.command === "add-dataset") result = await client(options).post("/datasets", manifest, { force });
|
|
282
|
+
else result = await client(options).put(`/datasets/${encodeURIComponent(project)}/${encodeURIComponent(manifest.dataset_id)}/${encodeURIComponent(manifest.dataset_version)}`, manifest, { force });
|
|
283
|
+
} else if (options.command === "delete-dataset") {
|
|
284
|
+
const id = positional(rest, "dataset id"); const version = positional(rest, "dataset version"); const project = take(rest, "--project", "default");
|
|
285
|
+
result = await client(options).delete(`/datasets/${encodeURIComponent(project)}/${encodeURIComponent(id)}/${encodeURIComponent(version)}`);
|
|
286
|
+
} else if (options.command === "list-dataset-artifacts") {
|
|
287
|
+
const project = take(rest, "--project"); const datasetId = take(rest, "--dataset-id"); const datasetVersion = take(rest, "--dataset-version"); const includeDeleted = boolean(rest, "--include-deleted");
|
|
288
|
+
result = await listCollection(client(options), "/dataset-artifacts", { project, dataset_id: datasetId, dataset_version: datasetVersion, include_deleted: includeDeleted }, rest);
|
|
289
|
+
} else if (options.command === "show-operator-specs") {
|
|
290
|
+
const project = take(rest, "--project"); const query = take(rest, "--q", take(rest, "-q"));
|
|
291
|
+
result = await listCollection(client(options), "/operators", { project, q: query }, rest);
|
|
292
|
+
} else if (options.command === "delete-operator") {
|
|
293
|
+
const name = positional(rest, "operator name"); const version = positional(rest, "operator version"); const project = take(rest, "--project", "default");
|
|
294
|
+
result = await client(options).delete(`/operators/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
|
|
295
|
+
} else if (options.command === "run-operator") {
|
|
296
|
+
const name = positional(rest, "operator name"); const version = positional(rest, "operator version"); const project = take(rest, "--project", "default"); const rawConfig = take(rest, "--config", "{}");
|
|
297
|
+
let config; try { config = JSON.parse(rawConfig); } catch (_) { throw new Error("--config must be valid JSON"); }
|
|
298
|
+
result = await client(options).post(`/operators/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}/run`, config);
|
|
190
299
|
} else if (options.command === "get-job") result = await client(options).get(`/jobs/${encodeURIComponent(positional(rest, "job id"))}`);
|
|
191
300
|
else if (options.command === "wait-job") {
|
|
192
301
|
const jobId = positional(rest, "job id"); const poll = finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval"); const rawTimeout = take(rest, "--wait-timeout-seconds");
|
|
193
302
|
result = await waitForJob(client(options), jobId, poll, rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds"));
|
|
194
303
|
if (["failed", "cancelled"].includes(result.status)) exitCode = 1;
|
|
195
304
|
}
|
|
305
|
+
else if (options.command === "cancel-job") {
|
|
306
|
+
result = await client(options).post(`/jobs/${encodeURIComponent(positional(rest, "job id"))}/cancel`);
|
|
307
|
+
}
|
|
308
|
+
else if (options.command === "retry-job") {
|
|
309
|
+
result = await client(options).post(`/jobs/${encodeURIComponent(positional(rest, "job id"))}/retry`);
|
|
310
|
+
}
|
|
196
311
|
else if (options.command === "get-dataset-artifact") {
|
|
197
312
|
const dataset = positional(rest, "dataset id"); const hash = positional(rest, "manifest hash"); const project = take(rest, "--project", "default");
|
|
198
313
|
result = await client(options).get(`/dataset-artifacts/${encodeURIComponent(project)}/${encodeURIComponent(dataset)}/${encodeURIComponent(hash)}`);
|
|
@@ -202,7 +317,7 @@ async function runBusinessCli(argv) {
|
|
|
202
317
|
result = await client(options).downloadDatasetArtifact(project, dataset, hash, resolvedPath(outDir), boolean(rest, "--force"));
|
|
203
318
|
} else if (options.command === "fetch-inference-data") {
|
|
204
319
|
const manifest = positional(rest, "manifest"); const cutoffTime = take(rest, "--cutoff-time"); if (!cutoffTime) throw new Error("--cutoff-time is required");
|
|
205
|
-
const realtime = {}; const maxWorkers = take(rest, "--max-workers"); const maxLag = take(rest, "--max-source-lag-hours"); if (maxWorkers !== undefined) realtime.max_workers = integer(maxWorkers, "--max-workers"); if (maxLag !== undefined) realtime.max_source_lag_hours = finiteNumber(maxLag, "--max-source-lag-hours"); if (boolean(rest, "--no-validate-freshness")) realtime.validate_freshness = false; if (boolean(rest, "--allow-missing")) realtime.allow_missing = true;
|
|
320
|
+
const realtime = {}; const maxWorkers = take(rest, "--max-workers"); const maxLag = take(rest, "--max-source-lag-hours"); if (maxWorkers !== undefined) { realtime.max_workers = integer(maxWorkers, "--max-workers"); if (realtime.max_workers < 1) throw new Error("--max-workers must be at least 1"); } if (maxLag !== undefined) { realtime.max_source_lag_hours = finiteNumber(maxLag, "--max-source-lag-hours"); if (realtime.max_source_lag_hours < 0) throw new Error("--max-source-lag-hours must be non-negative"); } if (boolean(rest, "--no-validate-freshness")) realtime.validate_freshness = false; if (boolean(rest, "--allow-missing")) realtime.allow_missing = true;
|
|
206
321
|
const request = { manifest: readDataset(manifest), cutoff_time: cutoffTime }; if (Object.keys(realtime).length) request.realtime_fetch = realtime; result = await client(options).post("/inference-data/fetch", request);
|
|
207
322
|
} else if (options.command === "apply") {
|
|
208
323
|
const directory = positional(rest, "catalog directory"); const dryRun = boolean(rest, "--dry-run"); const targetProject = take(rest, "--project");
|
|
@@ -215,7 +330,16 @@ async function runBusinessCli(argv) {
|
|
|
215
330
|
|
|
216
331
|
async function fetchAll(api, endpoint, params = {}) { const items = []; for (let offset = 0; ; offset += 500) { const page = await api.get(endpoint, { ...params, limit: 500, offset }); if (Array.isArray(page)) return page; if (!page || !Array.isArray(page.items)) throw new Error(`${endpoint} returned an invalid paginated response`); items.push(...page.items); if (page.total == null || items.length >= page.total || page.items.length < 500) return items; } }
|
|
217
332
|
|
|
218
|
-
|
|
333
|
+
async function listCollection(api, endpoint, params, rest) {
|
|
334
|
+
const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
|
|
335
|
+
if (rawLimit === undefined && rawOffset === undefined) return fetchAll(api, endpoint, params);
|
|
336
|
+
const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0);
|
|
337
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500");
|
|
338
|
+
if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0");
|
|
339
|
+
return api.get(endpoint, { ...params, limit, offset });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = { BUSINESS_COMMANDS, fetchAll, listCollection, parse, runBusinessCli, usage, waitForJob };
|
|
219
343
|
|
|
220
344
|
if (require.main === module) {
|
|
221
345
|
runBusinessCli(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => {
|
|
@@ -11,11 +11,15 @@ function expandHome(value) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function configPath() {
|
|
14
|
-
|
|
14
|
+
for (const variable of ["ML_PLATFORM_CONFIG_PATH", "DATA_PLATFORM_DEMO_CONFIG_PATH"]) {
|
|
15
|
+
if (process.env[variable]?.trim()) return path.resolve(expandHome(process.env[variable]));
|
|
16
|
+
}
|
|
15
17
|
const base = process.env.XDG_CONFIG_HOME
|
|
16
18
|
? path.resolve(process.env.XDG_CONFIG_HOME)
|
|
17
19
|
: path.join(os.homedir(), ".config");
|
|
18
|
-
|
|
20
|
+
const primary = path.join(base, "ml-platform", "config.json");
|
|
21
|
+
const legacy = path.join(base, "data-platform-demo", "config.json");
|
|
22
|
+
return !fs.existsSync(primary) && fs.existsSync(legacy) ? legacy : primary;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
function normalizeApiUrl(value) {
|