mainsequence 4.0.14__tar.gz → 4.0.16__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.
Files changed (127) hide show
  1. {mainsequence-4.0.14 → mainsequence-4.0.16}/PKG-INFO +1 -1
  2. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/data_sources_interfaces/duckdb.py +18 -58
  3. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/data_sources_interfaces/sqlite.py +17 -10
  4. mainsequence-4.0.16/mainsequence/client/dtype_codec.py +502 -0
  5. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/models_metatables.py +113 -2
  6. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/models_tdag.py +134 -101
  7. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/utils.py +3 -4
  8. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/build_operations.py +16 -2
  9. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/data_nodes.py +55 -34
  10. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/persist_managers.py +16 -12
  11. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/run_operations.py +64 -43
  12. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/meta_tables/compiled_sql.py +20 -0
  13. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/meta_tables/sqlalchemy_contracts.py +3 -42
  14. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence.egg-info/PKG-INFO +1 -1
  15. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence.egg-info/SOURCES.txt +1 -0
  16. {mainsequence-4.0.14 → mainsequence-4.0.16}/pyproject.toml +1 -1
  17. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_data_access_mixin_dimension_audit.py +71 -0
  18. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_data_node_search_join_filters.py +0 -1
  19. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_data_node_storage_dimension_queries.py +2 -3
  20. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_data_node_update_flow.py +57 -2
  21. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_meta_tables_client_models.py +106 -1
  22. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_meta_tables_sqlalchemy_contracts.py +45 -1
  23. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_pod_project_resolution.py +50 -1
  24. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_run_configuration.py +44 -1
  25. {mainsequence-4.0.14 → mainsequence-4.0.16}/LICENSE +0 -0
  26. {mainsequence-4.0.14 → mainsequence-4.0.16}/README.md +0 -0
  27. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/AGENTS.md +0 -0
  28. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  29. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  30. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  31. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  32. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  33. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  34. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  35. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  36. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  37. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  38. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  39. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  40. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  41. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  42. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/ms-markets/SKILL.md +0 -0
  43. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  44. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  45. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  46. {mainsequence-4.0.14 → mainsequence-4.0.16}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  47. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/__init__.py +0 -0
  48. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/__main__.py +0 -0
  49. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/bootstrap.py +0 -0
  50. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/__init__.py +0 -0
  51. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/api.py +0 -0
  52. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/browser_auth.py +0 -0
  53. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/cli.py +0 -0
  54. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/config.py +0 -0
  55. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/docker_utils.py +0 -0
  56. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/doctor.py +0 -0
  57. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/local_ops.py +0 -0
  58. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/model_filters.py +0 -0
  59. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/project_status.py +0 -0
  60. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/pydantic_cli.py +0 -0
  61. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/sdk_utils.py +0 -0
  62. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/ssh_utils.py +0 -0
  63. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/cli/ui.py +0 -0
  64. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/__init__.py +0 -0
  65. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/agent_runtime_models.py +0 -0
  66. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/base.py +0 -0
  67. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/client.py +0 -0
  68. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/command_center/__init__.py +0 -0
  69. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/command_center/app_component.py +0 -0
  70. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/command_center/connections.py +0 -0
  71. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/command_center/data_models.py +0 -0
  72. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/command_center/workspace.py +0 -0
  73. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  74. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  75. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/exceptions.py +0 -0
  76. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/fastapi/__init__.py +0 -0
  77. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/fastapi/auth.py +0 -0
  78. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/models_helpers.py +0 -0
  79. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/client/models_user.py +0 -0
  80. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/compute_validation.py +0 -0
  81. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/defaults.py +0 -0
  82. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/instrumentation/__init__.py +0 -0
  83. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/instrumentation/utils.py +0 -0
  84. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/logconf.py +0 -0
  85. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/runtime_flags.py +0 -0
  86. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/__init__.py +0 -0
  87. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/__main__.py +0 -0
  88. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/base_persist_managers.py +0 -0
  89. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/config.py +0 -0
  90. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/configuration_models.py +0 -0
  91. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/__init__.py +0 -0
  92. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/filters.py +0 -0
  93. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/models.py +0 -0
  94. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/namespacing.py +0 -0
  95. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/data_nodes/utils.py +0 -0
  96. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/filters.py +0 -0
  97. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/future_registry.py +0 -0
  98. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/meta_tables/__init__.py +0 -0
  99. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/meta_tables/hashing.py +0 -0
  100. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/pydantic_metadata.py +0 -0
  101. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence/tdag/utils.py +0 -0
  102. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence.egg-info/dependency_links.txt +0 -0
  103. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence.egg-info/entry_points.txt +0 -0
  104. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence.egg-info/requires.txt +0 -0
  105. {mainsequence-4.0.14 → mainsequence-4.0.16}/mainsequence.egg-info/top_level.txt +0 -0
  106. {mainsequence-4.0.14 → mainsequence-4.0.16}/setup.cfg +0 -0
  107. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_auth_precedence.py +0 -0
  108. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_build_operations_hashing.py +0 -0
  109. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_cli.py +0 -0
  110. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_cli_browser_auth.py +0 -0
  111. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_client.py +0 -0
  112. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_command_center_app_component_models.py +0 -0
  113. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_command_center_data_models.py +0 -0
  114. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_command_center_models.py +0 -0
  115. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_dependency_extras.py +0 -0
  116. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_duckdb_interface_dimensions.py +0 -0
  117. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_filter_normalization.py +0 -0
  118. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_logconf.py +0 -0
  119. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_models_user_request_bound_auth.py +0 -0
  120. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_project_batch_jobs_from_file.py +0 -0
  121. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_secret_client_model.py +0 -0
  122. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_source_table_configuration.py +0 -0
  123. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_sqlite_interface_dimensions.py +0 -0
  124. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_update_runner_uid_runtime.py +0 -0
  125. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_update_statistics.py +0 -0
  126. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_update_uid_guards.py +0 -0
  127. {mainsequence-4.0.14 → mainsequence-4.0.16}/tests/test_workspace_snapshot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 4.0.14
3
+ Version: 4.0.16
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License: MainSequence GmbH SDK License Agreement
@@ -14,6 +14,14 @@ from pyarrow import fs
14
14
 
15
15
  from mainsequence.logconf import logger as base_logger
16
16
 
17
+ from ..dtype_codec import (
18
+ TIMESTAMP_TZ,
19
+ backend_type_to_token,
20
+ pandas_dtype_to_token,
21
+ token_to_backend_type,
22
+ token_to_pandas_dtype,
23
+ token_to_pandas_series,
24
+ )
17
25
  from ..utils import DataFrequency, UniqueIdentifierRangeMap
18
26
 
19
27
 
@@ -422,7 +430,11 @@ class DuckDBInterface:
422
430
 
423
431
  # —— basic hygiene ——--------------------------------------------------
424
432
  df = df.copy()
425
- df[time_index_name] = pd.to_datetime(df[time_index_name], utc=True)
433
+ df[time_index_name] = token_to_pandas_series(
434
+ df[time_index_name],
435
+ TIMESTAMP_TZ,
436
+ is_time_index=True,
437
+ )
426
438
 
427
439
  # —— derive partition columns ——---------------------------------------
428
440
  partitions = self._partition_keys(df[time_index_name], data_frequency=data_frequency)
@@ -1364,23 +1376,7 @@ class DuckDBInterface:
1364
1376
  }
1365
1377
  for col, target_type in type_map.items():
1366
1378
  try:
1367
- if target_type == "datetime64[ns, UTC]":
1368
- arr = df[col].values
1369
- arr_ns = arr.astype("datetime64[ns]")
1370
- df[col] = pd.Series(
1371
- pd.DatetimeIndex(arr_ns, tz="UTC"),
1372
- index=df.index,
1373
- name=col,
1374
- )
1375
- elif target_type == "datetime64[ns]":
1376
- df[col] = pd.to_datetime(df[col], errors="coerce")
1377
- else:
1378
- if isinstance(
1379
- target_type, (pd.Int64Dtype, pd.BooleanDtype, pd.StringDtype)
1380
- ):
1381
- df[col] = df[col].astype(target_type, errors="ignore")
1382
- else:
1383
- df[col] = df[col].astype(target_type, errors="ignore")
1379
+ df[col] = token_to_pandas_series(df[col], target_type)
1384
1380
  except Exception as type_e:
1385
1381
  logger.warning(
1386
1382
  f"Could not coerce column '{col}' to type '{target_type}': {type_e}"
@@ -1530,15 +1526,8 @@ class DuckDBInterface:
1530
1526
  """
1531
1527
  Minimal dtype → DuckDB mapping. Extend as needed.
1532
1528
  """
1533
- if pd.api.types.is_datetime64_any_dtype(dtype) or pd.api.types.is_datetime64tz_dtype(dtype):
1534
- return "TIMESTAMPTZ"
1535
- if pd.api.types.is_integer_dtype(dtype):
1536
- return "BIGINT"
1537
- if pd.api.types.is_float_dtype(dtype):
1538
- return "DOUBLE"
1539
- if pd.api.types.is_bool_dtype(dtype):
1540
- return "BOOLEAN"
1541
- return "VARCHAR"
1529
+ token = pandas_dtype_to_token(dtype, remote=False, allow_naive_datetime=True)
1530
+ return token_to_backend_type(token, "duckdb")
1542
1531
 
1543
1532
  @staticmethod
1544
1533
  def _duck_to_pandas(duck_type: str, data_frequency: DataFrequency):
@@ -1548,34 +1537,5 @@ class DuckDBInterface:
1548
1537
  `df.astype({...})` gets pandas’ nullable dtypes.
1549
1538
  Extend as needed.
1550
1539
  """
1551
- dt = duck_type.upper()
1552
-
1553
- # --- datetimes ------------------------------------------------------
1554
- if dt in ("TIMESTAMPTZ", "TIMESTAMP WITH TIME ZONE"):
1555
- # keep the UTC tz-awareness
1556
- return "datetime64[ns, UTC]"
1557
-
1558
- if dt in (
1559
- "TIMESTAMP",
1560
- "DATETIME",
1561
- ):
1562
- # keep timezone if present; duckdb returns tz‑aware objects already,
1563
- # so no explicit 'UTC' suffix is needed here.
1564
- return "datetime64[ns]"
1565
- if dt == "DATE":
1566
- return "datetime64[ns]" # pandas treats it as midnight
1567
-
1568
- # --- integers -------------------------------------------------------
1569
- if dt in ("TINYINT", "SMALLINT", "INTEGER", "INT", "BIGINT"):
1570
- return pd.Int64Dtype() # nullable 64‑bit int
1571
-
1572
- # --- floats / numerics ---------------------------------------------
1573
- if dt in ("REAL", "FLOAT", "DOUBLE", "DECIMAL"):
1574
- return "float64"
1575
-
1576
- # --- booleans -------------------------------------------------------
1577
- if dt == "BOOLEAN":
1578
- return pd.BooleanDtype() # nullable boolean
1579
-
1580
- # --- everything else ------------------------------------------------
1581
- return pd.StringDtype() # pandas‘ native nullable string
1540
+ token = backend_type_to_token(duck_type, "duckdb", allow_naive_datetime=True)
1541
+ return token_to_pandas_dtype(token)
@@ -10,6 +10,12 @@ import pandas as pd
10
10
 
11
11
  from mainsequence.logconf import logger as base_logger
12
12
 
13
+ from ..dtype_codec import (
14
+ TIMESTAMP_TZ,
15
+ pandas_dtype_to_token,
16
+ token_to_backend_type,
17
+ token_to_pandas_series,
18
+ )
13
19
  from ..utils import UniqueIdentifierRangeMap
14
20
 
15
21
 
@@ -78,15 +84,12 @@ class SQLiteInterface:
78
84
 
79
85
  @staticmethod
80
86
  def _sqlite_type(series: pd.Series, *, is_time_index: bool = False) -> str:
81
- if is_time_index or pd.api.types.is_datetime64_any_dtype(series):
82
- return "TEXT"
83
- if pd.api.types.is_bool_dtype(series):
84
- return "INTEGER"
85
- if pd.api.types.is_integer_dtype(series):
86
- return "INTEGER"
87
- if pd.api.types.is_float_dtype(series):
88
- return "REAL"
89
- return "TEXT"
87
+ token = (
88
+ TIMESTAMP_TZ
89
+ if is_time_index
90
+ else pandas_dtype_to_token(series.dtype, remote=False, allow_naive_datetime=True)
91
+ )
92
+ return token_to_backend_type(token, "sqlite")
90
93
 
91
94
  @staticmethod
92
95
  def _sqlite_value(value: Any) -> Any:
@@ -442,7 +445,11 @@ class SQLiteInterface:
442
445
 
443
446
  df = pd.read_sql_query(query, self.con, params=params)
444
447
  if not df.empty and time_index_name in df.columns:
445
- df[time_index_name] = pd.to_datetime(df[time_index_name], utc=True)
448
+ df[time_index_name] = token_to_pandas_series(
449
+ df[time_index_name],
450
+ TIMESTAMP_TZ,
451
+ is_time_index=True,
452
+ )
446
453
  return df
447
454
 
448
455
  def time_index_minima(
@@ -0,0 +1,502 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import math
5
+ import re
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+ from uuid import UUID
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+ DATE = "date"
14
+ TIMESTAMP_TZ = "timestamp with time zone"
15
+ LOCAL_DATETIME_NAIVE = "datetime64[ns]"
16
+ STRING = "string"
17
+ UUID_TOKEN = "uuid"
18
+ JSON = "json"
19
+ JSONB = "jsonb"
20
+ BOOL = "bool"
21
+ INT16 = "int16"
22
+ INT32 = "int32"
23
+ INT64 = "int64"
24
+ FLOAT32 = "float32"
25
+ FLOAT64 = "float64"
26
+ NUMERIC = "numeric"
27
+
28
+ _OFFSET_RE = re.compile(r"(z|[+-]\d{2}:?\d{2})$", re.IGNORECASE)
29
+
30
+
31
+ def is_nullish(value: Any) -> bool:
32
+ if value is None:
33
+ return True
34
+ try:
35
+ if value is pd.NaT:
36
+ return True
37
+ if isinstance(value, float) and math.isnan(value):
38
+ return True
39
+ result = pd.isna(value)
40
+ except (TypeError, ValueError):
41
+ return False
42
+ if isinstance(result, (bool, np.bool_)):
43
+ return bool(result)
44
+ return False
45
+
46
+
47
+ def normalize_dtype_token(
48
+ value: Any,
49
+ *,
50
+ remote: bool = True,
51
+ allow_naive_datetime: bool = False,
52
+ ) -> str:
53
+ token = str(value or "").strip()
54
+ if not token:
55
+ raise ValueError("DType token is required.")
56
+
57
+ lowered = " ".join(token.replace("_", " ").strip().lower().split())
58
+ compact = lowered.replace(" ", "")
59
+
60
+ if lowered in {"date", "datetime.date"}:
61
+ return DATE
62
+
63
+ if lowered in {
64
+ "datetime",
65
+ "datetime.datetime",
66
+ "datetime64[ns, utc]",
67
+ "timestamp with time zone",
68
+ "timestamptz",
69
+ }:
70
+ return TIMESTAMP_TZ
71
+
72
+ if lowered in {
73
+ "datetime64[ns]",
74
+ "timestamp",
75
+ "timestamp without time zone",
76
+ "datetime without time zone",
77
+ "datetime naive",
78
+ }:
79
+ if remote and not allow_naive_datetime:
80
+ raise ValueError(
81
+ "Timezone-naive datetime dtypes are local-backend-only. "
82
+ "Remote writes require 'timestamp with time zone'."
83
+ )
84
+ return LOCAL_DATETIME_NAIVE
85
+
86
+ if lowered in {"object", "str", "string", "text", "varchar", "char", "unicode"}:
87
+ return STRING
88
+ if "varchar" in lowered:
89
+ return STRING
90
+ if compact in {"uuid", "uuidtype", "uuid_", "postgresqluuid"}:
91
+ return UUID_TOKEN
92
+ if lowered in {"json"}:
93
+ return JSON
94
+ if lowered in {"jsonb"}:
95
+ return JSONB
96
+ if lowered in {"bool", "boolean"}:
97
+ return BOOL
98
+ if lowered in {"int16", "smallint", "int2"}:
99
+ return INT16
100
+ if lowered in {"int32", "integer", "int", "int4"}:
101
+ return INT32
102
+ if lowered in {"int64", "biginteger", "bigint", "int8"}:
103
+ return INT64
104
+ if lowered in {"uint16", "uint32", "uint64"}:
105
+ return INT64
106
+ if lowered in {"float32", "real", "float4"}:
107
+ return FLOAT32
108
+ if lowered in {"float", "float64", "double", "double precision", "float8"}:
109
+ return FLOAT64
110
+ if lowered == "numeric" or lowered.startswith("numeric") or lowered == "decimal":
111
+ return NUMERIC
112
+
113
+ return token
114
+
115
+
116
+ def normalize_column_dtypes_map(
117
+ column_dtypes_map: Mapping[str, Any],
118
+ *,
119
+ remote: bool = True,
120
+ allow_naive_datetime: bool = False,
121
+ ) -> dict[str, str]:
122
+ return {
123
+ str(column_name): normalize_dtype_token(
124
+ dtype,
125
+ remote=remote,
126
+ allow_naive_datetime=allow_naive_datetime,
127
+ )
128
+ for column_name, dtype in dict(column_dtypes_map).items()
129
+ }
130
+
131
+
132
+ def _record_definition_field(record: Any, field_name: str) -> Any:
133
+ if isinstance(record, Mapping):
134
+ return record.get(field_name)
135
+ return getattr(record, field_name, None)
136
+
137
+
138
+ def record_definitions_to_column_dtypes_map(
139
+ records: Any,
140
+ *,
141
+ remote: bool = True,
142
+ allow_naive_datetime: bool = False,
143
+ ) -> dict[str, str]:
144
+ if not records:
145
+ return {}
146
+
147
+ column_dtypes_map: dict[str, str] = {}
148
+ duplicate_columns: set[str] = set()
149
+ for record in records:
150
+ column_name = _record_definition_field(record, "column_name")
151
+ dtype = _record_definition_field(record, "dtype")
152
+ if not column_name or not dtype:
153
+ raise ValueError(
154
+ "Record definitions must include non-empty 'column_name' and 'dtype' fields."
155
+ )
156
+ column_name = str(column_name)
157
+ if column_name in column_dtypes_map:
158
+ duplicate_columns.add(column_name)
159
+ column_dtypes_map[column_name] = normalize_dtype_token(
160
+ dtype,
161
+ remote=remote,
162
+ allow_naive_datetime=allow_naive_datetime,
163
+ )
164
+
165
+ if duplicate_columns:
166
+ raise ValueError(
167
+ f"Duplicate DataNode record column names: {sorted(duplicate_columns)}"
168
+ )
169
+
170
+ return column_dtypes_map
171
+
172
+
173
+ def pandas_dtype_to_token(
174
+ dtype: Any,
175
+ *,
176
+ remote: bool = True,
177
+ allow_naive_datetime: bool = False,
178
+ ) -> str:
179
+ if isinstance(dtype, pd.DatetimeTZDtype):
180
+ return TIMESTAMP_TZ
181
+ if pd.api.types.is_datetime64_any_dtype(dtype):
182
+ if remote and not allow_naive_datetime:
183
+ raise ValueError(
184
+ "Timezone-naive pandas datetime64[ns] is local-backend-only. "
185
+ "Remote writes require timezone-aware datetime64[ns, UTC]."
186
+ )
187
+ return LOCAL_DATETIME_NAIVE
188
+ if pd.api.types.is_bool_dtype(dtype):
189
+ return BOOL
190
+ if pd.api.types.is_integer_dtype(dtype):
191
+ name = str(dtype).lower()
192
+ if "16" in name:
193
+ return INT16
194
+ if "32" in name:
195
+ return INT32
196
+ return INT64
197
+ if pd.api.types.is_float_dtype(dtype):
198
+ return FLOAT32 if "32" in str(dtype).lower() else FLOAT64
199
+ if pd.api.types.is_string_dtype(dtype):
200
+ return STRING
201
+ return STRING
202
+
203
+
204
+ def pandas_dtypes_to_column_map(
205
+ dtypes: Mapping[str, Any],
206
+ *,
207
+ remote: bool = True,
208
+ allow_naive_datetime: bool = False,
209
+ ) -> dict[str, str]:
210
+ return {
211
+ str(column_name): pandas_dtype_to_token(
212
+ dtype,
213
+ remote=remote,
214
+ allow_naive_datetime=allow_naive_datetime,
215
+ )
216
+ for column_name, dtype in dtypes.items()
217
+ }
218
+
219
+
220
+ def token_to_pandas_dtype(token: Any, *, nullable: bool = True) -> Any:
221
+ normalized = normalize_dtype_token(token, remote=False, allow_naive_datetime=True)
222
+ if normalized == STRING:
223
+ return "string"
224
+ if normalized == UUID_TOKEN:
225
+ return "string"
226
+ if normalized in {JSON, JSONB}:
227
+ return "object"
228
+ if normalized in {INT16, INT32, INT64}:
229
+ return "Int64" if nullable else normalized
230
+ if normalized == BOOL:
231
+ return "boolean" if nullable else "bool"
232
+ if normalized in {FLOAT32, FLOAT64, NUMERIC}:
233
+ return "float64"
234
+ if normalized == TIMESTAMP_TZ:
235
+ return "datetime64[ns, UTC]"
236
+ if normalized == LOCAL_DATETIME_NAIVE:
237
+ return "datetime64[ns]"
238
+ if normalized == DATE:
239
+ return "object"
240
+ return str(token)
241
+
242
+
243
+ def token_to_pandas_series(
244
+ series: pd.Series,
245
+ token: Any,
246
+ *,
247
+ is_time_index: bool = False,
248
+ nullable: bool = True,
249
+ ) -> pd.Series:
250
+ normalized = TIMESTAMP_TZ if is_time_index else normalize_dtype_token(
251
+ token,
252
+ remote=False,
253
+ allow_naive_datetime=True,
254
+ )
255
+ if normalized == TIMESTAMP_TZ:
256
+ return pd.to_datetime(series, errors="coerce", utc=True)
257
+ if normalized == LOCAL_DATETIME_NAIVE:
258
+ parsed = pd.to_datetime(series, errors="coerce")
259
+ try:
260
+ if getattr(parsed.dt, "tz", None) is not None:
261
+ parsed = parsed.dt.tz_convert(None)
262
+ except (AttributeError, TypeError):
263
+ pass
264
+ return parsed
265
+ if normalized == DATE:
266
+ return pd.to_datetime(series, errors="coerce").dt.date
267
+ if normalized in {JSON, JSONB}:
268
+ return series
269
+ pandas_dtype = token_to_pandas_dtype(normalized, nullable=nullable)
270
+ if pandas_dtype == "object":
271
+ return series
272
+ return series.astype(pandas_dtype)
273
+
274
+
275
+ def is_temporal_token(token: Any) -> bool:
276
+ return normalize_dtype_token(token, remote=False, allow_naive_datetime=True) in {
277
+ DATE,
278
+ TIMESTAMP_TZ,
279
+ LOCAL_DATETIME_NAIVE,
280
+ }
281
+
282
+
283
+ def _string_has_explicit_timezone(value: str) -> bool:
284
+ stripped = value.strip()
285
+ if not stripped:
286
+ return True
287
+ return bool(_OFFSET_RE.search(stripped))
288
+
289
+
290
+ def _timestamp_is_timezone_aware(value: Any) -> bool:
291
+ if isinstance(value, pd.Timestamp):
292
+ return value.tzinfo is not None
293
+ if isinstance(value, datetime.datetime):
294
+ return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None
295
+ return False
296
+
297
+
298
+ def _serialize_date_value(value: Any) -> str | None:
299
+ if is_nullish(value):
300
+ return None
301
+ if isinstance(value, datetime.datetime):
302
+ return value.date().isoformat()
303
+ if isinstance(value, datetime.date):
304
+ return value.isoformat()
305
+ timestamp = pd.to_datetime(value, errors="raise")
306
+ if isinstance(timestamp, pd.Timestamp):
307
+ return timestamp.date().isoformat()
308
+ return str(value)
309
+
310
+
311
+ def _serialize_timestamp_tz_value(value: Any) -> str | None:
312
+ if is_nullish(value):
313
+ return None
314
+ if isinstance(value, np.datetime64):
315
+ raise ValueError(
316
+ "Timezone-naive numpy datetime64 values cannot be sent to remote TS Manager."
317
+ )
318
+ if isinstance(value, str) and not _string_has_explicit_timezone(value):
319
+ raise ValueError(
320
+ f"Remote datetime value {value!r} must include an explicit timezone offset or Z."
321
+ )
322
+ if isinstance(value, (pd.Timestamp, datetime.datetime)) and not _timestamp_is_timezone_aware(value):
323
+ raise ValueError(
324
+ f"Remote datetime value {value!r} must be timezone-aware."
325
+ )
326
+ timestamp = pd.to_datetime(value, errors="raise", utc=True)
327
+ if not isinstance(timestamp, pd.Timestamp):
328
+ timestamp = pd.Timestamp(timestamp).tz_convert("UTC")
329
+ if timestamp.tzinfo is None:
330
+ raise ValueError(
331
+ f"Remote datetime value {value!r} must be timezone-aware."
332
+ )
333
+ timestamp = timestamp.tz_convert("UTC")
334
+ return timestamp.isoformat().replace("+00:00", "Z")
335
+
336
+
337
+ def serialize_remote_value(value: Any, token: Any) -> Any:
338
+ normalized = normalize_dtype_token(token, remote=True)
339
+ if normalized == DATE:
340
+ return _serialize_date_value(value)
341
+ if normalized == TIMESTAMP_TZ:
342
+ return _serialize_timestamp_tz_value(value)
343
+ if is_nullish(value):
344
+ return None
345
+ if isinstance(value, UUID):
346
+ return str(value)
347
+ return value
348
+
349
+
350
+ def serialize_remote_parameters(
351
+ parameters: Mapping[str, Any] | list[Any],
352
+ parameter_types: Mapping[str, Any] | None,
353
+ ) -> dict[str, Any] | list[Any]:
354
+ if not parameter_types or not isinstance(parameters, Mapping):
355
+ return dict(parameters) if isinstance(parameters, Mapping) else list(parameters)
356
+ normalized_types = normalize_column_dtypes_map(parameter_types, remote=True)
357
+ serialized = dict(parameters)
358
+ for name, token in normalized_types.items():
359
+ if name in serialized:
360
+ serialized[name] = serialize_remote_value(serialized[name], token)
361
+ return serialized
362
+
363
+
364
+ def prepare_dataframe_for_remote_write(
365
+ df: pd.DataFrame,
366
+ *,
367
+ column_dtypes_map: Mapping[str, Any],
368
+ time_index_name: str,
369
+ ) -> pd.DataFrame:
370
+ normalized_map = normalize_column_dtypes_map(column_dtypes_map, remote=True)
371
+ prepared = df.copy()
372
+ if time_index_name not in normalized_map:
373
+ normalized_map[time_index_name] = TIMESTAMP_TZ
374
+ for column_name, token in normalized_map.items():
375
+ if column_name not in prepared.columns:
376
+ continue
377
+ normalized = normalize_dtype_token(token, remote=True)
378
+ if normalized not in {DATE, TIMESTAMP_TZ}:
379
+ continue
380
+ prepared[column_name] = [
381
+ serialize_remote_value(value, normalized)
382
+ for value in prepared[column_name].tolist()
383
+ ]
384
+ return prepared
385
+
386
+
387
+ def token_to_backend_type(token: Any, backend: str) -> str:
388
+ normalized = normalize_dtype_token(token, remote=False, allow_naive_datetime=True)
389
+ backend_key = backend.strip().lower()
390
+ if backend_key == "sqlite":
391
+ if normalized in {INT16, INT32, INT64, BOOL}:
392
+ return "INTEGER"
393
+ if normalized in {FLOAT32, FLOAT64, NUMERIC}:
394
+ return "REAL"
395
+ return "TEXT"
396
+ if backend_key in {"duckdb", "duck_db"}:
397
+ if normalized == DATE:
398
+ return "DATE"
399
+ if normalized == TIMESTAMP_TZ:
400
+ return "TIMESTAMPTZ"
401
+ if normalized == LOCAL_DATETIME_NAIVE:
402
+ return "TIMESTAMP"
403
+ if normalized in {INT16, INT32, INT64}:
404
+ return "BIGINT"
405
+ if normalized in {FLOAT32, FLOAT64, NUMERIC}:
406
+ return "DOUBLE"
407
+ if normalized == BOOL:
408
+ return "BOOLEAN"
409
+ return "VARCHAR"
410
+ raise ValueError(f"Unsupported dtype backend {backend!r}.")
411
+
412
+
413
+ def backend_type_to_token(
414
+ backend_type: Any,
415
+ backend: str,
416
+ *,
417
+ allow_naive_datetime: bool = True,
418
+ ) -> str:
419
+ raw = str(backend_type or "").strip().upper()
420
+ backend_key = backend.strip().lower()
421
+ if backend_key == "sqlite":
422
+ if raw == "INTEGER":
423
+ return INT64
424
+ if raw == "REAL":
425
+ return FLOAT64
426
+ return STRING
427
+ if backend_key in {"duckdb", "duck_db"}:
428
+ if raw in {"TIMESTAMPTZ", "TIMESTAMP WITH TIME ZONE"}:
429
+ return TIMESTAMP_TZ
430
+ if raw in {"TIMESTAMP", "DATETIME"}:
431
+ return normalize_dtype_token(
432
+ LOCAL_DATETIME_NAIVE,
433
+ remote=False,
434
+ allow_naive_datetime=allow_naive_datetime,
435
+ )
436
+ if raw == "DATE":
437
+ return DATE
438
+ if raw in {"TINYINT", "SMALLINT", "INTEGER", "INT", "BIGINT"}:
439
+ return INT64
440
+ if raw in {"REAL", "FLOAT", "DOUBLE", "DECIMAL"}:
441
+ return FLOAT64
442
+ if raw == "BOOLEAN":
443
+ return BOOL
444
+ return STRING
445
+ return normalize_dtype_token(str(backend_type), remote=False, allow_naive_datetime=True)
446
+
447
+
448
+ def sqlalchemy_type_to_token(column_type: Any, *, remote: bool = True) -> str:
449
+ type_name = type(column_type).__name__.lower()
450
+ backend_type = str(column_type).upper()
451
+ normalized_backend = " ".join(backend_type.lower().split())
452
+ timezone = getattr(column_type, "timezone", None)
453
+
454
+ if type_name in {"datetime"} or "timestamp" in normalized_backend:
455
+ if timezone is False or "without time zone" in normalized_backend:
456
+ if remote:
457
+ raise ValueError(
458
+ "Timezone-naive SQLAlchemy DateTime columns are not supported "
459
+ "by the remote TS Manager contract."
460
+ )
461
+ return LOCAL_DATETIME_NAIVE
462
+ if timezone is True or "with time zone" in normalized_backend or "timestamptz" in normalized_backend:
463
+ return TIMESTAMP_TZ
464
+ if remote:
465
+ raise ValueError(
466
+ "SQLAlchemy DateTime columns must set timezone=True for remote MetaTables."
467
+ )
468
+ return LOCAL_DATETIME_NAIVE
469
+
470
+ if type_name in {"date"} or normalized_backend == "date":
471
+ return DATE
472
+ if type_name in {"uuid", "uuidtype", "uuid_", "postgresqluuid"} or normalized_backend == "uuid":
473
+ return UUID_TOKEN
474
+ if type_name in {"string", "unicode", "text", "varchar", "char"}:
475
+ return STRING
476
+ if type_name in {"integer", "biginteger"}:
477
+ return INT64
478
+ if type_name in {"smallinteger"}:
479
+ return INT16
480
+ if type_name in {"float", "double", "doubleprecision"}:
481
+ return FLOAT64
482
+ if type_name in {"numeric", "decimal"}:
483
+ return NUMERIC
484
+ if type_name in {"boolean"}:
485
+ return BOOL
486
+ if type_name in {"json", "jsonb"}:
487
+ return JSONB if type_name == "jsonb" else JSON
488
+ return normalize_dtype_token(backend_type, remote=remote)
489
+
490
+
491
+ def sqlalchemy_backend_type(column_type: Any, token: Any) -> str:
492
+ normalized = normalize_dtype_token(token, remote=False, allow_naive_datetime=True)
493
+ if normalized == UUID_TOKEN:
494
+ return "UUID"
495
+ backend_type = str(column_type).upper()
496
+ if normalized == TIMESTAMP_TZ:
497
+ return "TIMESTAMP WITH TIME ZONE"
498
+ if normalized == LOCAL_DATETIME_NAIVE:
499
+ return "TIMESTAMP WITHOUT TIME ZONE"
500
+ if normalized == DATE:
501
+ return "DATE"
502
+ return backend_type or normalized