@zhuoyuezs/ml-platform 0.1.0 → 0.1.3
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 +97 -73
- package/checksums.json +12 -12
- 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 +37 -16
- package/runtime/business-client/src/cli.js +67 -15
- package/scripts/lib.js +303 -30
- package/scripts/main.js +103 -11
- package/skills/feature-management/SKILL.md +37 -8
- package/skills/feature-management/references/commands.md +37 -4
|
@@ -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}`; }
|
|
@@ -61,12 +61,13 @@ 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
63
|
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")) };
|
|
64
|
+
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
65
|
}
|
|
66
66
|
|
|
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
|
|
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")), 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; }
|
|
68
|
+
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); }
|
|
69
|
+
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", "")) }; }
|
|
70
|
+
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
71
|
function timestamp(value, label) {
|
|
71
72
|
requiredString(value, label);
|
|
72
73
|
const match = timestampPattern.exec(value);
|
|
@@ -90,15 +91,15 @@ function timestamp(value, label) {
|
|
|
90
91
|
return value;
|
|
91
92
|
}
|
|
92
93
|
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") }; }
|
|
94
|
+
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") }; }
|
|
95
|
+
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
96
|
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))
|
|
97
|
+
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
98
|
function distinct(left, right, label) { if (left === right) throw new Error(`${label} inputs must be different`); }
|
|
98
99
|
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
100
|
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
101
|
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", "")) }; }
|
|
102
|
+
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
103
|
function normalizeDataset(payload) {
|
|
103
104
|
object(payload, "dataset"); payload = Object.fromEntries(Object.entries(payload).filter(([name]) => name !== "deleted_at"));
|
|
104
105
|
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 +117,27 @@ function normalizeDataset(payload) {
|
|
|
116
117
|
unique(missingPolicies.map(parameterReferenceKey), "dataset parameter_missing_policies Parameter references");
|
|
117
118
|
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
119
|
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") };
|
|
120
|
+
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
121
|
if (missingPolicies.length) output.parameter_missing_policies = missingPolicies;
|
|
121
122
|
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;
|
|
123
|
+
if (payload.target != null) output.target = normalizeTarget(payload.target, parentProject);
|
|
124
|
+
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
125
|
return output;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function normalizeRegistryDataset(payload) {
|
|
129
|
+
const value = object(payload, "registered dataset");
|
|
130
|
+
const responseOnly = ["entity_keys", "label_materializations"];
|
|
131
|
+
const contract = Object.fromEntries(Object.entries(value).filter(([name]) => !responseOnly.includes(name)));
|
|
132
|
+
const normalized = normalizeDataset(contract);
|
|
133
|
+
for (const name of responseOnly) {
|
|
134
|
+
if (!hasOwn(value, name)) continue;
|
|
135
|
+
if (!Array.isArray(value[name])) throw new Error(`registered dataset ${name} must be a list`);
|
|
136
|
+
normalized[name] = structuredClone(value[name]);
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
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
142
|
function readJson(file) { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { throw new Error(`cannot read JSON asset ${file}: ${error.message}`); } }
|
|
129
143
|
function readObject(file) { return object(readJson(file), `JSON file ${file}`); }
|
|
@@ -145,7 +159,7 @@ function loadCatalog(directory) {
|
|
|
145
159
|
}
|
|
146
160
|
|
|
147
161
|
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 =
|
|
162
|
+
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
163
|
|
|
150
164
|
function requiresPackage(spec) { return ["custom_python", "feature", "spark_udf"].includes(spec.type) && spec.code_artifact == null && !String(spec.package_uri ?? "").startsWith("pythonpath://"); }
|
|
151
165
|
function validateCatalog(catalog, current) {
|
|
@@ -163,22 +177,29 @@ function validateCatalog(catalog, current) {
|
|
|
163
177
|
for (const item of dataset.preprocess) if (!operators.has(referenceKey(item))) throw new Error(`catalog dataset ${datasetId} references missing preprocess operator ${referenceKey(item)}`);
|
|
164
178
|
}
|
|
165
179
|
}
|
|
180
|
+
function validateCatalogProject(catalog, targetProject) {
|
|
181
|
+
const projects = [...catalog.parameters, ...catalog.operators.map((item) => item.spec), ...catalog.features, ...catalog.feature_sets, ...catalog.datasets].map((item) => item.project ?? "default");
|
|
182
|
+
const uniqueProjects = [...new Set(projects)];
|
|
183
|
+
if (uniqueProjects.length > 1) throw new Error(`catalog contains multiple projects: ${uniqueProjects.sort().join(", ")}`);
|
|
184
|
+
const expected = targetProject ?? "default";
|
|
185
|
+
if (uniqueProjects.length && uniqueProjects[0] !== expected) throw new Error(`catalog project ${uniqueProjects[0]} does not match target project ${expected}`);
|
|
186
|
+
}
|
|
166
187
|
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
188
|
function equal(left, right) { return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); }
|
|
168
189
|
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
190
|
function sha256File(file) { return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`; }
|
|
170
191
|
|
|
171
192
|
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 }]));
|
|
193
|
+
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
194
|
for (const asset of catalog.parameters) assertImmutable("parameter", key(asset), asset, current.parameters.get(key(asset)));
|
|
174
195
|
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
196
|
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
197
|
try {
|
|
177
198
|
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(
|
|
199
|
+
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
200
|
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
201
|
} catch (error) { throw new Error(`catalog apply failed after progress=${JSON.stringify(canonical(summary))}: ${error.message}`); }
|
|
181
202
|
return { status: dryRun ? "validated" : "applied", profile: "server", catalog: { name: catalog.manifest.name, version: catalog.manifest.version, path: catalog.path }, summary };
|
|
182
203
|
}
|
|
183
204
|
|
|
184
|
-
module.exports = { applyCatalog, fetchAll, loadCatalog, normalizeDataset, normalizeFeature, normalizeFeatureSet, normalizeOperator, normalizeParameter, validateCatalog };
|
|
205
|
+
module.exports = { applyCatalog, fetchAll, loadCatalog, normalizeDataset, normalizeFeature, normalizeFeatureSet, normalizeOperator, normalizeRegistryDataset, normalizeRegistryOperator, normalizeParameter, validateCatalog, validateCatalogProject };
|
|
@@ -6,13 +6,14 @@ 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", "
|
|
15
|
+
"list-feature-sets", "list-datasets", "get-dataset", "list-jobs", "resolve-manifest",
|
|
16
|
+
"resolve-dataset", "build-dataset", "build-registered-dataset",
|
|
16
17
|
"get-job", "wait-job", "get-dataset-artifact", "download-dataset-artifact", "fetch-inference-data", "apply",
|
|
17
18
|
]);
|
|
18
19
|
|
|
@@ -30,9 +31,12 @@ const COMMAND_USAGE = {
|
|
|
30
31
|
"list-features": "list-features [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
31
32
|
"list-feature-sets": "list-feature-sets [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
32
33
|
"list-datasets": "list-datasets [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
|
|
34
|
+
"get-dataset": "get-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--include-deleted]",
|
|
33
35
|
"list-jobs": "list-jobs [-q QUERY] [--limit N] [--offset N]",
|
|
34
36
|
"resolve-manifest": "resolve-manifest MANIFEST [--out PATH]",
|
|
37
|
+
"resolve-dataset": "resolve-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--out PATH]",
|
|
35
38
|
"build-dataset": "build-dataset MANIFEST [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
|
|
39
|
+
"build-registered-dataset": "build-registered-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
|
|
36
40
|
"get-job": "get-job JOB_ID",
|
|
37
41
|
"wait-job": "wait-job JOB_ID [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
|
|
38
42
|
"get-dataset-artifact": "get-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT]",
|
|
@@ -113,6 +117,50 @@ function readObject(file) {
|
|
|
113
117
|
|
|
114
118
|
function readDataset(file) { return normalizeDataset(readObject(file)); }
|
|
115
119
|
|
|
120
|
+
function datasetEndpoint(project, datasetId, datasetVersion) {
|
|
121
|
+
return `/datasets/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(datasetVersion)}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function registeredDataset(api, project, datasetId, datasetVersion, includeDeleted = false) {
|
|
125
|
+
const payload = await api.get(datasetEndpoint(project, datasetId, datasetVersion), { include_deleted: includeDeleted });
|
|
126
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("registered dataset response must be a JSON object");
|
|
127
|
+
return payload;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildOptions(rest) {
|
|
131
|
+
const sourceMode = take(rest, "--source-mode");
|
|
132
|
+
if (sourceMode !== undefined && sourceMode !== "direct") throw new Error("--source-mode must be direct");
|
|
133
|
+
const maxParallelism = integer(take(rest, "--max-parallelism", 1), "--max-parallelism");
|
|
134
|
+
if (maxParallelism < 1) throw new Error("--max-parallelism must be at least 1");
|
|
135
|
+
const wait = boolean(rest, "--wait");
|
|
136
|
+
const pollInterval = finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval");
|
|
137
|
+
if (!(pollInterval > 0)) throw new Error("--poll-interval must be positive");
|
|
138
|
+
const rawTimeout = take(rest, "--wait-timeout-seconds");
|
|
139
|
+
const waitTimeout = rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds");
|
|
140
|
+
if (waitTimeout !== undefined && !(waitTimeout > 0)) throw new Error("--wait-timeout-seconds must be positive");
|
|
141
|
+
return {
|
|
142
|
+
sourceMode,
|
|
143
|
+
upload: boolean(rest, "--upload-chronon-metadata"),
|
|
144
|
+
partitionDuration: take(rest, "--partition-duration"),
|
|
145
|
+
maxParallelism,
|
|
146
|
+
resume: !boolean(rest, "--no-resume"),
|
|
147
|
+
wait,
|
|
148
|
+
pollInterval,
|
|
149
|
+
waitTimeout,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function submitBuild(api, manifest, build) {
|
|
154
|
+
let result = await api.post("/datasets/build", manifest, { engine: "chronon", source_mode: build.sourceMode, upload_chronon_metadata: build.upload, partition_duration: build.partitionDuration, max_parallelism: build.maxParallelism, resume: build.resume });
|
|
155
|
+
let exitCode = 0;
|
|
156
|
+
if (build.wait) {
|
|
157
|
+
if (!result || typeof result !== "object" || !result.job_id) throw new Error("server build did not return a job_id to wait for");
|
|
158
|
+
result = await waitForJob(api, String(result.job_id), build.pollInterval, build.waitTimeout);
|
|
159
|
+
if (["failed", "cancelled"].includes(result.status)) exitCode = 1;
|
|
160
|
+
}
|
|
161
|
+
return { result, exitCode };
|
|
162
|
+
}
|
|
163
|
+
|
|
116
164
|
async function waitForJob(api, jobId, pollInterval, timeoutSeconds) {
|
|
117
165
|
if (!(pollInterval > 0)) throw new Error("job poll interval must be positive");
|
|
118
166
|
if (timeoutSeconds !== undefined && !(timeoutSeconds > 0)) throw new Error("job wait timeout must be positive");
|
|
@@ -157,6 +205,10 @@ async function runBusinessCli(argv) {
|
|
|
157
205
|
} else if (options.command === "list-projects") result = await client(options).get("/projects");
|
|
158
206
|
else if (options.command === "get-project") result = await client(options).get(`/projects/${encodeURIComponent(positional(rest, "project name"))}`);
|
|
159
207
|
else if (options.command === "delete-project") result = await client(options).delete(`/projects/${encodeURIComponent(positional(rest, "project name"))}`);
|
|
208
|
+
else if (options.command === "get-dataset") {
|
|
209
|
+
const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default");
|
|
210
|
+
result = await registeredDataset(client(options), project, datasetId, datasetVersion, boolean(rest, "--include-deleted"));
|
|
211
|
+
}
|
|
160
212
|
else if (options.command.startsWith("list-") && options.command !== "list-jobs") {
|
|
161
213
|
const endpoints = { "list-parameters": "/parameters", "list-operators": "/operators", "list-features": "/features", "list-feature-sets": "/feature-sets", "list-datasets": "/datasets" };
|
|
162
214
|
const project = take(rest, "--project");
|
|
@@ -173,20 +225,20 @@ async function runBusinessCli(argv) {
|
|
|
173
225
|
result = await client(options).post("/datasets/resolve", readDataset(manifest));
|
|
174
226
|
const out = take(rest, "--out");
|
|
175
227
|
if (out) { const target = resolvedPath(out); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, `${JSON.stringify(result, null, 2)}\n`); }
|
|
228
|
+
} else if (options.command === "resolve-dataset") {
|
|
229
|
+
const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default"); const api = client(options);
|
|
230
|
+
const manifest = normalizeRegistryDataset(await registeredDataset(api, project, datasetId, datasetVersion));
|
|
231
|
+
result = await api.post("/datasets/resolve", manifest);
|
|
232
|
+
const out = take(rest, "--out");
|
|
233
|
+
if (out) { const target = resolvedPath(out); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, `${JSON.stringify(result, null, 2)}\n`); }
|
|
176
234
|
} else if (options.command === "build-dataset") {
|
|
177
235
|
const manifest = positional(rest, "manifest");
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (boolean(rest, "--wait")) {
|
|
185
|
-
if (!result || typeof result !== "object" || !result.job_id) throw new Error("server build did not return a job_id to wait for");
|
|
186
|
-
const rawTimeout = take(rest, "--wait-timeout-seconds");
|
|
187
|
-
result = await waitForJob(api, String(result.job_id), finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval"), rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds"));
|
|
188
|
-
if (["failed", "cancelled"].includes(result.status)) exitCode = 1;
|
|
189
|
-
}
|
|
236
|
+
const build = buildOptions(rest); const api = client(options);
|
|
237
|
+
({ result, exitCode } = await submitBuild(api, readDataset(manifest), build));
|
|
238
|
+
} else if (options.command === "build-registered-dataset") {
|
|
239
|
+
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);
|
|
240
|
+
const manifest = normalizeRegistryDataset(await registeredDataset(api, project, datasetId, datasetVersion));
|
|
241
|
+
({ result, exitCode } = await submitBuild(api, manifest, build));
|
|
190
242
|
} else if (options.command === "get-job") result = await client(options).get(`/jobs/${encodeURIComponent(positional(rest, "job id"))}`);
|
|
191
243
|
else if (options.command === "wait-job") {
|
|
192
244
|
const jobId = positional(rest, "job id"); const poll = finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval"); const rawTimeout = take(rest, "--wait-timeout-seconds");
|
|
@@ -202,7 +254,7 @@ async function runBusinessCli(argv) {
|
|
|
202
254
|
result = await client(options).downloadDatasetArtifact(project, dataset, hash, resolvedPath(outDir), boolean(rest, "--force"));
|
|
203
255
|
} else if (options.command === "fetch-inference-data") {
|
|
204
256
|
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;
|
|
257
|
+
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
258
|
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
259
|
} else if (options.command === "apply") {
|
|
208
260
|
const directory = positional(rest, "catalog directory"); const dryRun = boolean(rest, "--dry-run"); const targetProject = take(rest, "--project");
|