mainsequence 4.1.1__tar.gz → 4.1.3__tar.gz
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.
- {mainsequence-4.1.1 → mainsequence-4.1.3}/PKG-INFO +1 -1
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +16 -1
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/dtype_codec.py +20 -19
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_metatables.py +75 -42
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/utils.py +13 -4
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/build_operations.py +50 -98
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/persist_managers.py +11 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/run_operations.py +41 -63
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/sqlalchemy_contracts.py +7 -4
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-4.1.1 → mainsequence-4.1.3}/pyproject.toml +1 -1
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_build_operations_hashing.py +75 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_data_node_update_flow.py +133 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_pod_project_resolution.py +3 -55
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_run_configuration.py +64 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_update_runner_uid_runtime.py +85 -4
- {mainsequence-4.1.1 → mainsequence-4.1.3}/LICENSE +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/README.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/__main__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/bootstrap.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/api.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/cli.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/config.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/project_status.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/cli/ui.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/base.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/client.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/app_component.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/connections.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/data_models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/workspace.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/fastapi/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/fastapi/auth.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/client/models_user.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/compute_validation.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/defaults.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/logconf.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/__main__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/compiled_sql.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/config.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/configuration_models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/future_registry.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/hashing.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/utils.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/SOURCES.txt +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/setup.cfg +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_auth_precedence.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_cli.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_client.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_command_center_app_component_models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_command_center_data_models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_command_center_models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_data_access_mixin_dimension_audit.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_data_node_storage_dimension_queries.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_dependency_extras.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_duckdb_interface_dimensions.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_filter_normalization.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_logconf.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_meta_tables_client_models.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_project_batch_jobs_from_file.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_secret_client_model.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_source_table_configuration.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_sqlite_interface_dimensions.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_update_statistics.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_update_uid_guards.py +0 -0
- {mainsequence-4.1.1 → mainsequence-4.1.3}/tests/test_workspace_snapshot.py +0 -0
{mainsequence-4.1.1 → mainsequence-4.1.3}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md
RENAMED
|
@@ -161,6 +161,16 @@ The DataNode constructor should accept:
|
|
|
161
161
|
- a registered `storage_table: type[PlatformTimeIndexMetaData]`
|
|
162
162
|
- optional `hash_namespace`
|
|
163
163
|
|
|
164
|
+
The constructor `storage_table` is the output storage contract. Keep it out of
|
|
165
|
+
`DataNodeConfiguration`.
|
|
166
|
+
|
|
167
|
+
If the DataNode needs to select another DataNode's storage table as a
|
|
168
|
+
dependency, put that dependency storage reference in the config as
|
|
169
|
+
`type[PlatformTimeIndexMetaData]`. Do not add an extra constructor argument for
|
|
170
|
+
dependency storage tables. Config values of this type are hashed by the bound
|
|
171
|
+
`TimeIndexMetaData.uid` from `StorageClass.__time_index_metadata__`, so the class
|
|
172
|
+
must be registered or bound before DataNode construction.
|
|
173
|
+
|
|
164
174
|
Do not accept `test_node`. It has been removed. Use explicit
|
|
165
175
|
`hash_namespace(...)` or `hash_namespace="..."`.
|
|
166
176
|
|
|
@@ -287,6 +297,9 @@ Time index must be datetime64[ns, UTC]
|
|
|
287
297
|
|
|
288
298
|
Dependencies belong in constructor setup and `dependencies()`.
|
|
289
299
|
|
|
300
|
+
Dependency storage-table selection belongs in `DataNodeConfiguration`, because
|
|
301
|
+
changing it changes the dependency graph and update identity.
|
|
302
|
+
|
|
290
303
|
Do not construct dependency graphs dynamically inside `update()`.
|
|
291
304
|
|
|
292
305
|
### 8. Asset-Scoped Updates Must Be Explicit
|
|
@@ -318,7 +331,8 @@ surface for new DataNode work.
|
|
|
318
331
|
|
|
319
332
|
When reviewing an existing DataNode, look for:
|
|
320
333
|
|
|
321
|
-
- storage contract hidden in `DataNodeConfiguration`
|
|
334
|
+
- output storage contract hidden in `DataNodeConfiguration`
|
|
335
|
+
- dependency storage table passed as an ad hoc constructor argument
|
|
322
336
|
- old `RecordDefinition` or `DataNodeMetaData` schema patterns
|
|
323
337
|
- `update_only`, `runtime_only`, or `ignore_from_storage_hash`
|
|
324
338
|
- `test_node=True`
|
|
@@ -339,6 +353,7 @@ Do not claim success until you have checked:
|
|
|
339
353
|
- the relevant docs were read first
|
|
340
354
|
- storage is a registered or bound `PlatformTimeIndexMetaData` class
|
|
341
355
|
- the DataNode constructor requires `storage_table`
|
|
356
|
+
- dependency storage-table references live in config and are registered or bound
|
|
342
357
|
- config fields are updater-scoped by default
|
|
343
358
|
- no removed hash metadata markers remain
|
|
344
359
|
- no `test_node` usage remains
|
|
@@ -163,9 +163,7 @@ def record_definitions_to_column_dtypes_map(
|
|
|
163
163
|
)
|
|
164
164
|
|
|
165
165
|
if duplicate_columns:
|
|
166
|
-
raise ValueError(
|
|
167
|
-
f"Duplicate DataNode record column names: {sorted(duplicate_columns)}"
|
|
168
|
-
)
|
|
166
|
+
raise ValueError(f"Duplicate DataNode record column names: {sorted(duplicate_columns)}")
|
|
169
167
|
|
|
170
168
|
return column_dtypes_map
|
|
171
169
|
|
|
@@ -247,10 +245,14 @@ def token_to_pandas_series(
|
|
|
247
245
|
is_time_index: bool = False,
|
|
248
246
|
nullable: bool = True,
|
|
249
247
|
) -> pd.Series:
|
|
250
|
-
normalized =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
normalized = (
|
|
249
|
+
TIMESTAMP_TZ
|
|
250
|
+
if is_time_index
|
|
251
|
+
else normalize_dtype_token(
|
|
252
|
+
token,
|
|
253
|
+
remote=False,
|
|
254
|
+
allow_naive_datetime=True,
|
|
255
|
+
)
|
|
254
256
|
)
|
|
255
257
|
if normalized == TIMESTAMP_TZ:
|
|
256
258
|
return pd.to_datetime(series, errors="coerce", utc=True)
|
|
@@ -319,17 +321,15 @@ def _serialize_timestamp_tz_value(value: Any) -> str | None:
|
|
|
319
321
|
raise ValueError(
|
|
320
322
|
f"Remote datetime value {value!r} must include an explicit timezone offset or Z."
|
|
321
323
|
)
|
|
322
|
-
if isinstance(value, (pd.Timestamp, datetime.datetime)) and not _timestamp_is_timezone_aware(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
)
|
|
324
|
+
if isinstance(value, (pd.Timestamp, datetime.datetime)) and not _timestamp_is_timezone_aware(
|
|
325
|
+
value
|
|
326
|
+
):
|
|
327
|
+
raise ValueError(f"Remote datetime value {value!r} must be timezone-aware.")
|
|
326
328
|
timestamp = pd.to_datetime(value, errors="raise", utc=True)
|
|
327
329
|
if not isinstance(timestamp, pd.Timestamp):
|
|
328
330
|
timestamp = pd.Timestamp(timestamp).tz_convert("UTC")
|
|
329
331
|
if timestamp.tzinfo is None:
|
|
330
|
-
raise ValueError(
|
|
331
|
-
f"Remote datetime value {value!r} must be timezone-aware."
|
|
332
|
-
)
|
|
332
|
+
raise ValueError(f"Remote datetime value {value!r} must be timezone-aware.")
|
|
333
333
|
timestamp = timestamp.tz_convert("UTC")
|
|
334
334
|
return timestamp.isoformat().replace("+00:00", "Z")
|
|
335
335
|
|
|
@@ -375,11 +375,8 @@ def prepare_dataframe_for_remote_write(
|
|
|
375
375
|
if column_name not in prepared.columns:
|
|
376
376
|
continue
|
|
377
377
|
normalized = normalize_dtype_token(token, remote=True)
|
|
378
|
-
if normalized not in {DATE, TIMESTAMP_TZ}:
|
|
379
|
-
continue
|
|
380
378
|
prepared[column_name] = [
|
|
381
|
-
serialize_remote_value(value, normalized)
|
|
382
|
-
for value in prepared[column_name].tolist()
|
|
379
|
+
serialize_remote_value(value, normalized) for value in prepared[column_name].tolist()
|
|
383
380
|
]
|
|
384
381
|
return prepared
|
|
385
382
|
|
|
@@ -459,7 +456,11 @@ def sqlalchemy_type_to_token(column_type: Any, *, remote: bool = True) -> str:
|
|
|
459
456
|
"by the remote TS Manager contract."
|
|
460
457
|
)
|
|
461
458
|
return LOCAL_DATETIME_NAIVE
|
|
462
|
-
if
|
|
459
|
+
if (
|
|
460
|
+
timezone is True
|
|
461
|
+
or "with time zone" in normalized_backend
|
|
462
|
+
or "timestamptz" in normalized_backend
|
|
463
|
+
):
|
|
463
464
|
return TIMESTAMP_TZ
|
|
464
465
|
if remote:
|
|
465
466
|
raise ValueError(
|
|
@@ -1244,9 +1244,6 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
|
|
|
1244
1244
|
)
|
|
1245
1245
|
|
|
1246
1246
|
|
|
1247
|
-
LOGICAL_COLUMN_DTYPES_ATTR = "mainsequence_column_dtypes_map"
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
1247
|
def _warn_legacy_compat(message: str, *, stacklevel: int = 3) -> None:
|
|
1251
1248
|
warnings.warn(
|
|
1252
1249
|
f"Deprecated TDAG compatibility path: {message}",
|
|
@@ -2143,7 +2140,6 @@ class DataNodeUpdate(TableUpdateNode, BaseObjectOrm):
|
|
|
2143
2140
|
remote_dtypes: bool = True,
|
|
2144
2141
|
allow_naive_datetime: bool = False,
|
|
2145
2142
|
):
|
|
2146
|
-
logical_column_dtypes_map = data_frame.attrs.get(LOGICAL_COLUMN_DTYPES_ATTR)
|
|
2147
2143
|
record_column_dtypes_map = record_definitions_to_column_dtypes_map(
|
|
2148
2144
|
records,
|
|
2149
2145
|
remote=remote_dtypes,
|
|
@@ -2178,40 +2174,6 @@ class DataNodeUpdate(TableUpdateNode, BaseObjectOrm):
|
|
|
2178
2174
|
"DataNode records declare columns not present in the DataFrame: "
|
|
2179
2175
|
f"{missing_record_columns}"
|
|
2180
2176
|
)
|
|
2181
|
-
if logical_column_dtypes_map is not None:
|
|
2182
|
-
logical_column_dtypes_map = normalize_column_dtypes_map(
|
|
2183
|
-
logical_column_dtypes_map,
|
|
2184
|
-
remote=remote_dtypes,
|
|
2185
|
-
allow_naive_datetime=allow_naive_datetime,
|
|
2186
|
-
)
|
|
2187
|
-
missing_logical_columns = [
|
|
2188
|
-
column_name
|
|
2189
|
-
for column_name in logical_column_dtypes_map
|
|
2190
|
-
if column_name not in data_frame.columns
|
|
2191
|
-
]
|
|
2192
|
-
if missing_logical_columns:
|
|
2193
|
-
raise ValueError(
|
|
2194
|
-
"Logical column dtype contract contains columns not present "
|
|
2195
|
-
f"in the DataFrame: {missing_logical_columns}"
|
|
2196
|
-
)
|
|
2197
|
-
conflicting_declared_dtypes = {
|
|
2198
|
-
column_name: {
|
|
2199
|
-
"record_dtype": record_column_dtypes_map[column_name],
|
|
2200
|
-
"logical_dtype": logical_column_dtypes_map[column_name],
|
|
2201
|
-
}
|
|
2202
|
-
for column_name in logical_column_dtypes_map
|
|
2203
|
-
if (
|
|
2204
|
-
column_name in record_column_dtypes_map
|
|
2205
|
-
and logical_column_dtypes_map[column_name]
|
|
2206
|
-
!= record_column_dtypes_map[column_name]
|
|
2207
|
-
)
|
|
2208
|
-
}
|
|
2209
|
-
if conflicting_declared_dtypes:
|
|
2210
|
-
raise ValueError(
|
|
2211
|
-
"Logical column dtype contract conflicts with DataNode records: "
|
|
2212
|
-
f"{conflicting_declared_dtypes}"
|
|
2213
|
-
)
|
|
2214
|
-
column_dtypes_map.update(logical_column_dtypes_map)
|
|
2215
2177
|
column_dtypes_map.update(record_column_dtypes_map)
|
|
2216
2178
|
|
|
2217
2179
|
data_frame = data_frame.replace({np.nan: None})
|
|
@@ -3970,10 +3932,70 @@ class UpdateStatistics(BaseUpdateStatistics):
|
|
|
3970
3932
|
def _assign_nested_coordinate(root: dict[Any, Any], keys: list[Any], value: Any) -> None:
|
|
3971
3933
|
if not keys:
|
|
3972
3934
|
return
|
|
3935
|
+
normalized_keys = [_normalize_update_stat_key(key) for key in keys]
|
|
3973
3936
|
sub = root
|
|
3974
|
-
for key in
|
|
3975
|
-
|
|
3976
|
-
|
|
3937
|
+
for key in normalized_keys[:-1]:
|
|
3938
|
+
existing = sub.setdefault(key, {})
|
|
3939
|
+
if not isinstance(existing, dict):
|
|
3940
|
+
raise ValueError("Update statistics coordinate keys collide after JSON normalization.")
|
|
3941
|
+
sub = existing
|
|
3942
|
+
final_key = normalized_keys[-1]
|
|
3943
|
+
if final_key in sub:
|
|
3944
|
+
raise ValueError("Update statistics coordinate keys collide after JSON normalization.")
|
|
3945
|
+
sub[final_key] = value
|
|
3946
|
+
|
|
3947
|
+
|
|
3948
|
+
def _normalize_update_stat_key(key: Any) -> str:
|
|
3949
|
+
if isinstance(key, str):
|
|
3950
|
+
return key
|
|
3951
|
+
if isinstance(key, np.generic):
|
|
3952
|
+
key = key.item()
|
|
3953
|
+
if isinstance(key, datetime.datetime):
|
|
3954
|
+
value = serialize_to_json({"_": key})["_"]
|
|
3955
|
+
return str(value)
|
|
3956
|
+
if isinstance(key, datetime.date):
|
|
3957
|
+
return key.isoformat()
|
|
3958
|
+
if key is None:
|
|
3959
|
+
return "null"
|
|
3960
|
+
if isinstance(key, bool):
|
|
3961
|
+
return "true" if key else "false"
|
|
3962
|
+
if isinstance(key, int | float):
|
|
3963
|
+
try:
|
|
3964
|
+
return json.dumps(key, allow_nan=False)
|
|
3965
|
+
except (TypeError, ValueError):
|
|
3966
|
+
return str(key)
|
|
3967
|
+
|
|
3968
|
+
normalized = serialize_to_json({key: None})
|
|
3969
|
+
normalized_key = next(iter(normalized.keys()))
|
|
3970
|
+
if normalized_key is None:
|
|
3971
|
+
return "null"
|
|
3972
|
+
if isinstance(normalized_key, bool):
|
|
3973
|
+
return "true" if normalized_key else "false"
|
|
3974
|
+
if isinstance(normalized_key, int | float):
|
|
3975
|
+
try:
|
|
3976
|
+
return json.dumps(normalized_key, allow_nan=False)
|
|
3977
|
+
except (TypeError, ValueError):
|
|
3978
|
+
return str(normalized_key)
|
|
3979
|
+
return str(normalized_key)
|
|
3980
|
+
|
|
3981
|
+
|
|
3982
|
+
def _normalize_update_stat_mapping_keys(value: Any) -> Any:
|
|
3983
|
+
if isinstance(value, Mapping):
|
|
3984
|
+
normalized: dict[str, Any] = {}
|
|
3985
|
+
for key, item in value.items():
|
|
3986
|
+
normalized_key = _normalize_update_stat_key(key)
|
|
3987
|
+
if normalized_key in normalized:
|
|
3988
|
+
raise ValueError(
|
|
3989
|
+
"Update statistics coordinate keys collide after JSON normalization: "
|
|
3990
|
+
f"{normalized_key!r}."
|
|
3991
|
+
)
|
|
3992
|
+
normalized[normalized_key] = _normalize_update_stat_mapping_keys(item)
|
|
3993
|
+
return normalized
|
|
3994
|
+
if isinstance(value, list):
|
|
3995
|
+
return [_normalize_update_stat_mapping_keys(item) for item in value]
|
|
3996
|
+
if isinstance(value, tuple):
|
|
3997
|
+
return [_normalize_update_stat_mapping_keys(item) for item in value]
|
|
3998
|
+
return value
|
|
3977
3999
|
|
|
3978
4000
|
|
|
3979
4001
|
def get_index_progress_chunk_stats(chunk_df, time_index_name, index_names):
|
|
@@ -4081,6 +4103,11 @@ class LastUpdateMultiIndexStatsPayload(BaseModel):
|
|
|
4081
4103
|
index_progress: dict[str, Any] = Field(default_factory=dict)
|
|
4082
4104
|
index_min: dict[str, Any] = Field(default_factory=dict)
|
|
4083
4105
|
|
|
4106
|
+
@model_validator(mode="before")
|
|
4107
|
+
@classmethod
|
|
4108
|
+
def _normalize_mapping_keys(cls, value: Any) -> Any:
|
|
4109
|
+
return _normalize_update_stat_mapping_keys(value)
|
|
4110
|
+
|
|
4084
4111
|
def to_payload(self) -> dict[str, Any]:
|
|
4085
4112
|
return {
|
|
4086
4113
|
"_GLOBAL_": self.global_stats,
|
|
@@ -4099,6 +4126,11 @@ class LastUpdateIndexTimePayload(BaseModel):
|
|
|
4099
4126
|
multi_index_stats: LastUpdateMultiIndexStatsPayload | None = None
|
|
4100
4127
|
multi_index_column_stats: dict[str, Any] | None = Field(default_factory=dict)
|
|
4101
4128
|
|
|
4129
|
+
@model_validator(mode="before")
|
|
4130
|
+
@classmethod
|
|
4131
|
+
def _normalize_mapping_keys(cls, value: Any) -> Any:
|
|
4132
|
+
return _normalize_update_stat_mapping_keys(value)
|
|
4133
|
+
|
|
4102
4134
|
@model_validator(mode="after")
|
|
4103
4135
|
def _validate_shape(self):
|
|
4104
4136
|
top_level_progress_keys = [
|
|
@@ -4164,7 +4196,8 @@ def build_last_update_index_time_payload(
|
|
|
4164
4196
|
}
|
|
4165
4197
|
)
|
|
4166
4198
|
|
|
4167
|
-
|
|
4199
|
+
normalized_payload = _normalize_update_stat_mapping_keys(raw_payload)
|
|
4200
|
+
return LastUpdateIndexTimePayload.model_validate(normalized_payload).to_nested_payload()
|
|
4168
4201
|
|
|
4169
4202
|
|
|
4170
4203
|
class HistoricalUpdateRecord:
|
|
@@ -9,11 +9,11 @@ import socket
|
|
|
9
9
|
import subprocess
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
|
-
import uuid
|
|
13
12
|
from dataclasses import dataclass, field
|
|
14
13
|
from decimal import Decimal
|
|
15
14
|
from enum import Enum
|
|
16
15
|
from typing import TypedDict
|
|
16
|
+
from uuid import UUID, getnode
|
|
17
17
|
|
|
18
18
|
import psutil
|
|
19
19
|
import pytz
|
|
@@ -761,6 +761,9 @@ def serialize_to_json(kwargs):
|
|
|
761
761
|
if isinstance(v, Decimal):
|
|
762
762
|
return str(v)
|
|
763
763
|
|
|
764
|
+
if isinstance(v, UUID):
|
|
765
|
+
return str(v)
|
|
766
|
+
|
|
764
767
|
if isinstance(v, datetime.datetime):
|
|
765
768
|
dt = v
|
|
766
769
|
if dt.tzinfo is None:
|
|
@@ -776,13 +779,19 @@ def serialize_to_json(kwargs):
|
|
|
776
779
|
return v.model_dump()
|
|
777
780
|
|
|
778
781
|
if isinstance(v, dict):
|
|
779
|
-
return {k: to_jsonable(x) for k, x in v.items()}
|
|
782
|
+
return {to_json_key(k): to_jsonable(x) for k, x in v.items()}
|
|
780
783
|
if isinstance(v, (list, tuple)):
|
|
781
784
|
return [to_jsonable(x) for x in v]
|
|
782
785
|
|
|
783
786
|
return v
|
|
784
787
|
|
|
785
|
-
|
|
788
|
+
def to_json_key(value):
|
|
789
|
+
key = to_jsonable(value)
|
|
790
|
+
if key is None or isinstance(key, str | int | float | bool):
|
|
791
|
+
return key
|
|
792
|
+
return str(key)
|
|
793
|
+
|
|
794
|
+
return {to_json_key(k): to_jsonable(v) for k, v in kwargs.items()}
|
|
786
795
|
|
|
787
796
|
|
|
788
797
|
def _linux_machine_id() -> str | None:
|
|
@@ -839,7 +848,7 @@ def bios_uuid() -> str:
|
|
|
839
848
|
return mid
|
|
840
849
|
|
|
841
850
|
# Tier 4 – MAC address (uuid.getnode). Always available.
|
|
842
|
-
return f"{
|
|
851
|
+
return f"{getnode():012x}"
|
|
843
852
|
|
|
844
853
|
|
|
845
854
|
def _install_retry_adapters_in_place(
|
{mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/build_operations.py
RENAMED
|
@@ -13,22 +13,19 @@ from enum import Enum
|
|
|
13
13
|
from functools import singledispatch
|
|
14
14
|
from types import SimpleNamespace
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
|
+
from uuid import UUID
|
|
16
17
|
|
|
17
18
|
from pydantic import BaseModel
|
|
18
19
|
|
|
19
20
|
from mainsequence.client import BaseObjectOrm
|
|
20
21
|
from mainsequence.client.models_helpers import get_model_class
|
|
21
22
|
from mainsequence.client.models_metatables import _resolve_local_pod_project
|
|
22
|
-
from mainsequence.instrumentation import tracer, tracer_instrumentator
|
|
23
23
|
from mainsequence.meta_tables.pydantic_metadata import (
|
|
24
24
|
is_serialized_pydantic_model,
|
|
25
25
|
serialize_pydantic_model,
|
|
26
26
|
strip_pydantic_hash_exclusions,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
from .namespacing import disable_hash_namespace
|
|
30
|
-
from .persist_managers import PersistManager
|
|
31
|
-
|
|
32
29
|
if TYPE_CHECKING:
|
|
33
30
|
from .data_nodes import APIDataNode, DataNode
|
|
34
31
|
|
|
@@ -70,11 +67,56 @@ def _serialize_api_timeserie(value: APIDataNode) -> dict[str, Any]:
|
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
|
|
70
|
+
def _import_qualified_name(module_name: str, qualname: str) -> Any:
|
|
71
|
+
module = importlib.import_module(module_name)
|
|
72
|
+
value: Any = module
|
|
73
|
+
for part in qualname.split("."):
|
|
74
|
+
value = getattr(value, part)
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_platform_time_index_metadata_class(value: Any) -> bool:
|
|
79
|
+
try:
|
|
80
|
+
from mainsequence.meta_tables.sqlalchemy_contracts import PlatformTimeIndexMetaData
|
|
81
|
+
except ImportError:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
return isinstance(value, type) and issubclass(value, PlatformTimeIndexMetaData)
|
|
86
|
+
except TypeError:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@serialize_argument.register(type)
|
|
91
|
+
def _(value: type[Any]) -> Any:
|
|
92
|
+
if not _is_platform_time_index_metadata_class(value):
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
time_index_metadata = value.get_time_index_metadata()
|
|
96
|
+
uid = getattr(time_index_metadata, "uid", None)
|
|
97
|
+
if uid in (None, ""):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"PlatformTimeIndexMetaData config values must be registered or bound "
|
|
100
|
+
"before they can be hashed."
|
|
101
|
+
)
|
|
102
|
+
return {
|
|
103
|
+
"__type__": "platform_time_index_metadata",
|
|
104
|
+
"uid": str(uid),
|
|
105
|
+
"module": value.__module__,
|
|
106
|
+
"qualname": value.__qualname__,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
73
110
|
@serialize_argument.register(datetime.datetime)
|
|
74
111
|
def _(value: datetime.datetime) -> str:
|
|
75
112
|
return value.isoformat()
|
|
76
113
|
|
|
77
114
|
|
|
115
|
+
@serialize_argument.register(UUID)
|
|
116
|
+
def _(value: UUID) -> str:
|
|
117
|
+
return str(value)
|
|
118
|
+
|
|
119
|
+
|
|
78
120
|
@serialize_argument.register(BaseModel)
|
|
79
121
|
def _(value: BaseModel) -> dict[str, Any]:
|
|
80
122
|
"""Serialization logic for any Pydantic BaseModel."""
|
|
@@ -180,6 +222,8 @@ def parse_dictionary_before_hashing(dictionary: dict[str, Any]) -> dict[str, Any
|
|
|
180
222
|
# The value["items"] are already serialized dicts
|
|
181
223
|
|
|
182
224
|
local_ts_dict_to_hash[key] = [v["unique_identifier"] for v in value["items"]]
|
|
225
|
+
elif value.get("__type__") == "platform_time_index_metadata":
|
|
226
|
+
local_ts_dict_to_hash[key] = value["uid"]
|
|
183
227
|
else:
|
|
184
228
|
# recursively apply hash signature
|
|
185
229
|
local_ts_dict_to_hash[key] = parse_dictionary_before_hashing(value)
|
|
@@ -327,6 +371,8 @@ class ConfigRebuilder(BaseRebuilder):
|
|
|
327
371
|
return build_model(value)
|
|
328
372
|
|
|
329
373
|
def _handle_complex_type(self, value: dict, **kwargs) -> Any:
|
|
374
|
+
if value.get("__type__") == "platform_time_index_metadata":
|
|
375
|
+
return _import_qualified_name(value["module"], value["qualname"])
|
|
330
376
|
# Special case for ORM lists within the generic complex type handler
|
|
331
377
|
if value.get("__type__") == "orm_model_list":
|
|
332
378
|
return [build_model(item) for item in value["items"]]
|
|
@@ -434,97 +480,3 @@ def create_config(
|
|
|
434
480
|
remote_initial_configuration=remote_config,
|
|
435
481
|
build_configuration_json_schema=build_configuration_json_schema,
|
|
436
482
|
)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def rebuild_and_set_from_update_hash(
|
|
440
|
-
update_hash: str,
|
|
441
|
-
data_source_uid: str,
|
|
442
|
-
set_dependencies_df: bool = False,
|
|
443
|
-
graph_depth_limit: int = 1,
|
|
444
|
-
) -> DataNode:
|
|
445
|
-
"""
|
|
446
|
-
Rebuilds a DataNode from stored configuration and attaches runtime sessions.
|
|
447
|
-
|
|
448
|
-
Args:
|
|
449
|
-
update_hash: The local hash ID of the DataNode.
|
|
450
|
-
data_source_uid: The data source UID.
|
|
451
|
-
set_dependencies_df: Whether to set the dependencies DataFrame.
|
|
452
|
-
graph_depth_limit: The depth limit for graph traversal.
|
|
453
|
-
|
|
454
|
-
Returns:
|
|
455
|
-
The rebuilt DataNode object.
|
|
456
|
-
"""
|
|
457
|
-
ts = rebuild_from_configuration(
|
|
458
|
-
update_hash=update_hash,
|
|
459
|
-
data_source=data_source_uid,
|
|
460
|
-
)
|
|
461
|
-
if set_dependencies_df:
|
|
462
|
-
ts.set_relation_tree()
|
|
463
|
-
ts._set_state_with_sessions(
|
|
464
|
-
graph_depth=0,
|
|
465
|
-
graph_depth_limit=graph_depth_limit,
|
|
466
|
-
include_client_objects=False,
|
|
467
|
-
)
|
|
468
|
-
ts.logger.debug(f"ts {update_hash} rebuilt from configuration")
|
|
469
|
-
return ts
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
@tracer.start_as_current_span("TS: Rebuild From Configuration")
|
|
473
|
-
def rebuild_from_configuration(update_hash: str, data_source: str | object) -> DataNode:
|
|
474
|
-
"""
|
|
475
|
-
Rebuilds a DataNode instance from its configuration.
|
|
476
|
-
|
|
477
|
-
Args:
|
|
478
|
-
update_hash: The local hash ID of the DataNode.
|
|
479
|
-
data_source: The data source UID or object.
|
|
480
|
-
|
|
481
|
-
Returns:
|
|
482
|
-
The rebuilt DataNode instance.
|
|
483
|
-
"""
|
|
484
|
-
import importlib
|
|
485
|
-
|
|
486
|
-
tracer_instrumentator.append_attribute_to_current_span("update_hash", update_hash)
|
|
487
|
-
|
|
488
|
-
data_source_uid = getattr(data_source, "uid", data_source)
|
|
489
|
-
data_node_update = PersistManager.UPDATE_CLASS.get_or_none(
|
|
490
|
-
update_hash=update_hash,
|
|
491
|
-
remote_table__data_source__uid=str(data_source_uid),
|
|
492
|
-
include_relations_detail=True,
|
|
493
|
-
)
|
|
494
|
-
if data_node_update is None:
|
|
495
|
-
raise ValueError(
|
|
496
|
-
f"DataNodeUpdate {update_hash!r} with data source {data_source_uid!r} was not found."
|
|
497
|
-
)
|
|
498
|
-
storage_table = data_node_update.data_node_storage
|
|
499
|
-
persist_manager = PersistManager.get_from_storage_table(
|
|
500
|
-
update_hash=update_hash,
|
|
501
|
-
storage_table=storage_table,
|
|
502
|
-
data_node_update=data_node_update,
|
|
503
|
-
)
|
|
504
|
-
try:
|
|
505
|
-
time_serie_config = persist_manager.local_build_configuration
|
|
506
|
-
except Exception as e:
|
|
507
|
-
raise e
|
|
508
|
-
|
|
509
|
-
try:
|
|
510
|
-
mod = importlib.import_module(time_serie_config["time_series_class_import_path"]["module"])
|
|
511
|
-
TimeSerieClass = getattr(
|
|
512
|
-
mod, time_serie_config["time_series_class_import_path"]["qualname"]
|
|
513
|
-
)
|
|
514
|
-
except Exception as e:
|
|
515
|
-
raise e
|
|
516
|
-
|
|
517
|
-
time_serie_class_name = time_serie_config["time_series_class_import_path"]["qualname"]
|
|
518
|
-
|
|
519
|
-
time_serie_config.pop("time_series_class_import_path")
|
|
520
|
-
time_serie_config = DeserializerManager().rebuild_serialized_config(
|
|
521
|
-
time_serie_config, time_serie_class_name=time_serie_class_name
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
# IMPORTANT:
|
|
525
|
-
# When rebuilding from stored config, ignore any ambient test namespace.
|
|
526
|
-
# If the stored config includes 'hash_namespace' (test tables), it will still be passed explicitly.
|
|
527
|
-
with disable_hash_namespace():
|
|
528
|
-
re_build_ts = TimeSerieClass(**time_serie_config)
|
|
529
|
-
|
|
530
|
-
return re_build_ts
|
{mainsequence-4.1.1 → mainsequence-4.1.3}/mainsequence/meta_tables/data_nodes/persist_managers.py
RENAMED
|
@@ -438,11 +438,22 @@ class BasePersistManager:
|
|
|
438
438
|
data=temp_df,
|
|
439
439
|
data_source=self.data_source,
|
|
440
440
|
overwrite=overwrite,
|
|
441
|
+
source_table_schema=self._source_table_schema(),
|
|
441
442
|
)
|
|
442
443
|
|
|
443
444
|
persisted = True
|
|
444
445
|
return persisted
|
|
445
446
|
|
|
447
|
+
def _source_table_schema(self) -> dict[str, Any]:
|
|
448
|
+
time_index_name, index_names, column_dtypes_map = (
|
|
449
|
+
self.storage_metadata._require_time_indexed_table_contract()
|
|
450
|
+
)
|
|
451
|
+
return {
|
|
452
|
+
"time_index_name": time_index_name,
|
|
453
|
+
"index_names": list(index_names),
|
|
454
|
+
"column_dtypes_map": dict(column_dtypes_map),
|
|
455
|
+
}
|
|
456
|
+
|
|
446
457
|
def get_update_statistics_for_table(self) -> UpdateStatistics:
|
|
447
458
|
return self.storage_metadata.get_data_updates()
|
|
448
459
|
|