mainsequence 4.0.0__tar.gz → 4.0.5__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.0 → mainsequence-4.0.5}/PKG-INFO +1 -1
  2. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +67 -10
  3. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/bootstrap.py +2 -1
  4. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/api.py +139 -31
  5. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/cli.py +250 -221
  6. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/config.py +117 -58
  7. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/project_status.py +30 -6
  8. mainsequence-4.0.5/mainsequence/client/__init__.py +16 -0
  9. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/models_tdag.py +71 -55
  10. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/utils.py +0 -16
  11. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/build_operations.py +3 -3
  12. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/meta_tables/__init__.py +9 -0
  13. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/meta_tables/hashing.py +54 -0
  14. mainsequence-4.0.5/mainsequence/tdag/meta_tables/sqlalchemy_contracts.py +1066 -0
  15. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence.egg-info/PKG-INFO +1 -1
  16. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence.egg-info/SOURCES.txt +0 -1
  17. {mainsequence-4.0.0 → mainsequence-4.0.5}/pyproject.toml +1 -1
  18. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_cli.py +235 -81
  19. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_data_node_update_flow.py +34 -0
  20. mainsequence-4.0.5/tests/test_meta_tables_sqlalchemy_contracts.py +551 -0
  21. mainsequence-4.0.5/tests/test_pod_project_resolution.py +177 -0
  22. mainsequence-4.0.0/mainsequence/client/__init__.py +0 -8
  23. mainsequence-4.0.0/mainsequence/client/data_sources_interfaces/timescale.py +0 -524
  24. mainsequence-4.0.0/mainsequence/tdag/meta_tables/sqlalchemy_contracts.py +0 -476
  25. mainsequence-4.0.0/tests/test_meta_tables_sqlalchemy_contracts.py +0 -209
  26. mainsequence-4.0.0/tests/test_pod_project_resolution.py +0 -98
  27. {mainsequence-4.0.0 → mainsequence-4.0.5}/LICENSE +0 -0
  28. {mainsequence-4.0.0 → mainsequence-4.0.5}/README.md +0 -0
  29. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/AGENTS.md +0 -0
  30. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/a2a_communication/SKILL.md +0 -0
  31. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  32. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/command_center/adapter_from_api/SKILL.md +0 -0
  33. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  34. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  35. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/command_center/connections/SKILL.md +0 -0
  36. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  37. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  38. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/command_center/workspace_design/SKILL.md +0 -0
  39. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  40. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  41. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  42. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  43. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/maintenance/local_journal/SKILL.md +0 -0
  44. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  45. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  46. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  47. {mainsequence-4.0.0 → mainsequence-4.0.5}/agent_scaffold/skills/project_to_agent/SKILL.md +0 -0
  48. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/__init__.py +0 -0
  49. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/__main__.py +0 -0
  50. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/__init__.py +0 -0
  51. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/browser_auth.py +0 -0
  52. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/docker_utils.py +0 -0
  53. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/doctor.py +0 -0
  54. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/local_ops.py +0 -0
  55. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/model_filters.py +0 -0
  56. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/pydantic_cli.py +0 -0
  57. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/sdk_utils.py +0 -0
  58. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/ssh_utils.py +0 -0
  59. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/cli/ui.py +0 -0
  60. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/agent_runtime_models.py +0 -0
  61. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/base.py +0 -0
  62. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/client.py +0 -0
  63. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/command_center/__init__.py +0 -0
  64. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/command_center/app_component.py +0 -0
  65. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/command_center/connections.py +0 -0
  66. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/command_center/data_models.py +0 -0
  67. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/command_center/workspace.py +0 -0
  68. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  69. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  70. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  71. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/exceptions.py +0 -0
  72. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/fastapi/__init__.py +0 -0
  73. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/fastapi/auth.py +0 -0
  74. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/models_helpers.py +0 -0
  75. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/models_metatables.py +0 -0
  76. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/client/models_user.py +0 -0
  77. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/compute_validation.py +0 -0
  78. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/defaults.py +0 -0
  79. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/instrumentation/__init__.py +0 -0
  80. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/instrumentation/utils.py +0 -0
  81. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/logconf.py +0 -0
  82. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/runtime_flags.py +0 -0
  83. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/__init__.py +0 -0
  84. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/__main__.py +0 -0
  85. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/base_persist_managers.py +0 -0
  86. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/config.py +0 -0
  87. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/configuration_models.py +0 -0
  88. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/__init__.py +0 -0
  89. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/data_nodes.py +0 -0
  90. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/filters.py +0 -0
  91. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/models.py +0 -0
  92. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/namespacing.py +0 -0
  93. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/persist_managers.py +0 -0
  94. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/run_operations.py +0 -0
  95. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/data_nodes/utils.py +0 -0
  96. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/filters.py +0 -0
  97. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/future_registry.py +0 -0
  98. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/meta_tables/compiled_sql.py +0 -0
  99. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/pydantic_metadata.py +0 -0
  100. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence/tdag/utils.py +0 -0
  101. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence.egg-info/dependency_links.txt +0 -0
  102. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence.egg-info/entry_points.txt +0 -0
  103. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence.egg-info/requires.txt +0 -0
  104. {mainsequence-4.0.0 → mainsequence-4.0.5}/mainsequence.egg-info/top_level.txt +0 -0
  105. {mainsequence-4.0.0 → mainsequence-4.0.5}/setup.cfg +0 -0
  106. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_auth_precedence.py +0 -0
  107. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_build_operations_hashing.py +0 -0
  108. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_cli_browser_auth.py +0 -0
  109. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_client.py +0 -0
  110. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_command_center_app_component_models.py +0 -0
  111. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_command_center_data_models.py +0 -0
  112. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_command_center_models.py +0 -0
  113. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  114. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_data_node_search_join_filters.py +0 -0
  115. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_data_node_storage_dimension_queries.py +0 -0
  116. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_dependency_extras.py +0 -0
  117. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_filter_normalization.py +0 -0
  118. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_logconf.py +0 -0
  119. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_meta_tables_client_models.py +0 -0
  120. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_models_user_request_bound_auth.py +0 -0
  121. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_project_batch_jobs_from_file.py +0 -0
  122. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_run_configuration.py +0 -0
  123. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_source_table_configuration.py +0 -0
  124. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_update_runner_uid_runtime.py +0 -0
  125. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_update_statistics.py +0 -0
  126. {mainsequence-4.0.0 → mainsequence-4.0.5}/tests/test_update_uid_guards.py +0 -0
  127. {mainsequence-4.0.0 → mainsequence-4.0.5}/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.0
3
+ Version: 4.0.5
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
@@ -15,7 +15,8 @@ This skill is for schema-driven application tables registered through TS Manager
15
15
 
16
16
  - define SQLAlchemy/Core or ORM table models for `MetaTable` registration
17
17
  - choose `platform_managed` or `external_registered` management mode
18
- - build registration requests from resolved SQLAlchemy metadata
18
+ - register platform-managed tables through the model class API
19
+ - build registration requests from resolved SQLAlchemy metadata when inspection is useful
19
20
  - define indexes and foreign keys in the table contract
20
21
  - design governed compiled SQL read and write operations
21
22
  - review table contracts for physical-name, namespace, and identifier issues
@@ -67,7 +68,7 @@ Before changing code, collect or infer:
67
68
  - expected read patterns
68
69
  - expected mutation patterns
69
70
  - whether TS Manager should create the physical table
70
- - the target `DynamicTableDataSource` UID
71
+ - for `external_registered`, the target `DynamicTableDataSource` UID
71
72
 
72
73
  If ownership of the physical table lifecycle is unclear, stop before choosing a management mode.
73
74
 
@@ -92,15 +93,67 @@ Do not hand-build contract fragments when the SQLAlchemy helper can derive them.
92
93
 
93
94
  ### 2. Use storage-hash physical names for backend-managed tables
94
95
 
95
- For `platform_managed`, use `metatable_tablename(...)` as the SQLAlchemy `__tablename__`.
96
+ For `platform_managed`, inherit from `PlatformManagedMetaTable`.
96
97
 
97
- The backend expects that deterministic physical name to match the registration storage hash.
98
+ The mixin derives the SQLAlchemy physical table name from storage-relevant configuration and table shape. Do not hand-write `__tablename__` for normal backend-managed tables.
99
+
100
+ Schema must come from SQLAlchemy table metadata, usually `__table_args__ = {"schema": "public"}` or the tuple form ending in `{"schema": ...}`. Do not add a separate MetaTable-specific schema attribute.
101
+
102
+ Register through the class API:
103
+
104
+ ```python
105
+ account_meta_table = Account.register(
106
+ description="Example account table.",
107
+ labels=["sdk-example"],
108
+ )
109
+ ```
110
+
111
+ For platform-managed registration, the data source is resolved from the active Main Sequence project/session, the same way DataNode does. Do not require or thread a `data_source_uid` through normal platform-managed example code.
112
+
113
+ Only call `build_registration_request()` when the task explicitly needs to inspect or validate the payload before registration.
114
+
115
+ For SDK examples, use a plain namespace constant:
116
+
117
+ ```python
118
+ NAMESPACE = "sdk-examples"
119
+ ```
120
+
121
+ Do not add an environment variable for namespace in examples.
122
+
123
+ Do not add generic labels such as `"meta-table"` or `"platform-managed"` to examples. Keep labels specific to the example or domain.
124
+
125
+ Do not add a `MAINSEQUENCE_META_TABLE_REGISTER` toggle in registration examples. Registration examples should register directly.
98
126
 
99
127
  ### 3. Register parent tables before child tables
100
128
 
101
129
  Foreign-key contracts reference the target `MetaTable` UID.
102
130
 
103
- Register parent tables first, then pass their UIDs when building child table registration requests.
131
+ For `PlatformManagedMetaTable`, register parent tables first and then register child tables normally. The SDK inspects SQLAlchemy foreign-key constraints and resolves each target `MetaTable` by looking up the already registered table in the same data source, schema, and physical table name.
132
+
133
+ Example registration order:
134
+
135
+ ```python
136
+ account_meta_table = Account.register(...)
137
+ asset_meta_table = Asset.register(...)
138
+ ```
139
+
140
+ The child registration will fail if the parent table has not already been registered, because there is no target `MetaTable.uid` for the backend FK contract.
141
+
142
+ Do not pass `target_meta_tables` or `target_meta_table_uid_by_fullname` in the normal platform-managed path. Use explicit FK target mappings only for edge cases where automatic lookup is ambiguous or impossible.
143
+
144
+ For `external_registered`, there is no platform-managed parent lookup through the model class. Register the parent first, then build the child registration request with the parent UID mapped by target table fullname:
145
+
146
+ ```python
147
+ account_meta_table = MetaTable.register(account_request)
148
+ asset_request = external_registered_registration_request_from_sqlalchemy_model(
149
+ Asset,
150
+ data_source_uid=data_source_uid,
151
+ target_meta_table_uid_by_fullname={
152
+ Account.__table__.fullname: account_meta_table.uid,
153
+ },
154
+ )
155
+ asset_meta_table = MetaTable.register(asset_request)
156
+ ```
104
157
 
105
158
  ### 4. Governed operations declare scope
106
159
 
@@ -119,9 +172,12 @@ Do not hardcode platform-managed physical names manually.
119
172
  When reviewing an existing MetaTable workflow, look for:
120
173
 
121
174
  - missing namespace or identifier
122
- - backend-managed models that do not use `metatable_tablename(...)`
175
+ - backend-managed models that do not inherit `PlatformManagedMetaTable`
176
+ - backend-managed examples that use namespace environment variables instead of a plain `sdk-examples` namespace
177
+ - duplicate schema sources outside SQLAlchemy table metadata
123
178
  - external tables registered with unstable physical names
124
- - foreign keys that do not reference target MetaTable UIDs
179
+ - platform-managed child tables that are registered before parent tables
180
+ - external child registrations that do not map foreign-key targets to registered parent `MetaTable.uid` values
125
181
  - compiled SQL operations without complete table scope
126
182
  - raw SQL that hardcodes stale physical names
127
183
  - a table that should really be modeled as a DataNode instead
@@ -132,7 +188,7 @@ Do not claim success until you have checked:
132
188
 
133
189
  - the table contract matches the intended row contract
134
190
  - indexes are intentional
135
- - foreign keys point to the correct dependency targets
191
+ - foreign keys resolve to the correct dependency targets
136
192
  - management mode is correct
137
193
  - backend-managed physical names match the storage hash
138
194
  - registration returns a `MetaTable.uid`
@@ -141,13 +197,14 @@ Do not claim success until you have checked:
141
197
  For related tables, also check:
142
198
 
143
199
  - aliases are readable
144
- - parent table UIDs are passed into child contracts
200
+ - platform-managed parent tables are registered before child tables
201
+ - external child registration requests map FK targets to the registered parent UIDs
145
202
  - query results still match the expected response contract
146
203
 
147
204
  ## This Skill Must Stop And Escalate When
148
205
 
149
206
  - physical table lifecycle ownership is unclear
150
- - the target data source is unknown
207
+ - the target data source is unknown for an `external_registered` workflow
151
208
  - the task really requires a time-series published table
152
209
  - the workflow requires direct database credentials outside TS Manager governance
153
210
  - the task is actually an API or orchestration problem
@@ -20,7 +20,7 @@ def _read_local_env_values(env_path: pathlib.Path) -> dict[str, str]:
20
20
  return {}
21
21
 
22
22
  values: dict[str, str] = {}
23
- for key in ("MAINSEQUENCE_ENDPOINT", "MAIN_SEQUENCE_PROJECT_ID"):
23
+ for key in ("MAINSEQUENCE_ENDPOINT", "MAIN_SEQUENCE_PROJECT_UID", "MAIN_SEQUENCE_PROJECT_ID"):
24
24
  match = re.search(rf"(?m)^{re.escape(key)}=(.+?)\s*$", content)
25
25
  if match:
26
26
  values[key] = match.group(1).strip()
@@ -35,6 +35,7 @@ def prime_runtime_env() -> None:
35
35
  local_values = _read_local_env_values(pathlib.Path.cwd() / ".env")
36
36
 
37
37
  _set_if_missing("MAINSEQUENCE_ENDPOINT", local_values.get("MAINSEQUENCE_ENDPOINT"))
38
+ _set_if_missing("MAIN_SEQUENCE_PROJECT_UID", local_values.get("MAIN_SEQUENCE_PROJECT_UID"))
38
39
  _set_if_missing("MAIN_SEQUENCE_PROJECT_ID", local_values.get("MAIN_SEQUENCE_PROJECT_ID"))
39
40
 
40
41
  try:
@@ -432,6 +432,7 @@ def _run_sdk_model_operation(
432
432
  class_name: str,
433
433
  operation,
434
434
  project_id_env: int | str | None = None,
435
+ project_uid_env: str | None = None,
435
436
  ):
436
437
  tokens = get_tokens()
437
438
  access = (tokens.get("access") or "").strip()
@@ -447,6 +448,7 @@ def _run_sdk_model_operation(
447
448
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
448
449
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
449
450
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
451
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
450
452
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
451
453
  }
452
454
 
@@ -464,6 +466,12 @@ def _run_sdk_model_operation(
464
466
  else:
465
467
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
466
468
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
469
+ if project_uid_env is not None:
470
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = str(project_uid_env)
471
+ elif project_id_env is not None and not str(project_id_env).strip().isdigit():
472
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = str(project_id_env)
473
+ else:
474
+ os.environ.pop("MAIN_SEQUENCE_PROJECT_UID", None)
467
475
  if project_id_env is not None:
468
476
  os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_id_env)
469
477
  else:
@@ -521,20 +529,30 @@ def _run_sdk_model_operation(
521
529
 
522
530
  def get_current_user_profile() -> dict:
523
531
  """
524
- Return current user profile (username + organization name) via backend endpoints.
532
+ Return current user profile (username + organization name) via the canonical user-details endpoint.
525
533
 
526
534
  Returns:
527
535
  dict: {"username": "...", "organization": "..."} or {}
528
536
  """
529
- who = authed("GET", AUTH_PATHS["ping"])
530
- d = who.json() if who.ok else {}
531
- uid = d.get("id") or d.get("pk") or (d.get("user") or {}).get("id") or d.get("user_id")
532
- if not uid:
533
- return {}
534
- full = authed("GET", f"/user/api/user/{uid}/")
535
- u = full.json() if full.ok else {}
536
- org_name = (u.get("organization") or {}).get("name") or u.get("organization_name") or ""
537
- return {"username": u.get("username") or "", "organization": org_name}
537
+ details = authed("GET", "/user/api/user/get_user_details/")
538
+ payload = details.json() if details.ok else {}
539
+ user = payload.get("user") if isinstance(payload, dict) else {}
540
+ if not isinstance(user, dict):
541
+ user = {}
542
+ organization = user.get("organization") if isinstance(user, dict) else {}
543
+ if not isinstance(organization, dict):
544
+ organization = {}
545
+ payload_organization = payload.get("organization") if isinstance(payload, dict) else {}
546
+ if not isinstance(payload_organization, dict):
547
+ payload_organization = {}
548
+ org_name = (
549
+ organization.get("name")
550
+ or payload_organization.get("name")
551
+ or payload.get("organization_name")
552
+ or payload.get("organization")
553
+ or ""
554
+ )
555
+ return {"username": user.get("username") or payload.get("username") or "", "organization": org_name}
538
556
 
539
557
 
540
558
  def get_logged_user_details() -> dict[str, Any]:
@@ -676,6 +694,65 @@ def get_projects() -> list[dict]:
676
694
  return data.get("results") or []
677
695
 
678
696
 
697
+ def _normalize_project_reference(project_ref: int | str) -> str:
698
+ normalized = str(project_ref).strip()
699
+ if not normalized:
700
+ raise ApiError("Project UID is required.")
701
+ return normalized
702
+
703
+
704
+ def _project_matches_reference(project_payload: dict[str, Any], project_ref: str) -> bool:
705
+ return (
706
+ str(project_payload.get("uid") or "").strip() == project_ref
707
+ or str(project_payload.get("id") or "").strip() == project_ref
708
+ )
709
+
710
+
711
+ def resolve_project(project_ref: int | str) -> dict[str, Any]:
712
+ normalized_ref = _normalize_project_reference(project_ref)
713
+
714
+ try:
715
+ payload = get_project(normalized_ref)
716
+ if isinstance(payload, dict) and payload:
717
+ return payload
718
+ except ApiError:
719
+ pass
720
+
721
+ for project_payload in get_projects():
722
+ if isinstance(project_payload, dict) and _project_matches_reference(project_payload, normalized_ref):
723
+ return project_payload
724
+
725
+ raise ApiError(f"Project not found: {normalized_ref}")
726
+
727
+
728
+ def resolve_project_uid(project_ref: int | str) -> str:
729
+ normalized_ref = _normalize_project_reference(project_ref)
730
+ if normalized_ref.isdigit():
731
+ return normalized_ref
732
+
733
+ payload = resolve_project(project_ref)
734
+ normalized_uid = str(payload.get("uid") or "").strip()
735
+ if normalized_uid:
736
+ return normalized_uid
737
+ if not normalized_ref.isdigit():
738
+ return normalized_ref
739
+ raise ApiError(f"Project UID is not available for project reference: {normalized_ref}")
740
+
741
+
742
+ def resolve_project_row_id(project_ref: int | str) -> int:
743
+ normalized_ref = _normalize_project_reference(project_ref)
744
+ if normalized_ref.isdigit():
745
+ return int(normalized_ref)
746
+
747
+ payload = resolve_project(project_ref)
748
+ row_id = payload.get("id")
749
+ if row_id is None:
750
+ if normalized_ref.isdigit():
751
+ return int(normalized_ref)
752
+ raise ApiError(f"Backend row id is not available for project reference: {normalized_ref}")
753
+ return int(row_id)
754
+
755
+
679
756
  def search_projects(
680
757
  q: str,
681
758
  *,
@@ -1395,7 +1472,7 @@ def get_agent_run(
1395
1472
 
1396
1473
  def get_project(project_id: int | str) -> dict:
1397
1474
  """
1398
- Fetch a single project by id.
1475
+ Fetch a single project by public reference.
1399
1476
  """
1400
1477
  r = authed("GET", f"/orm/api/pods/projects/{project_id}/")
1401
1478
  if not r.ok:
@@ -1748,6 +1825,7 @@ def get_project_data_node_updates(project_id: int | str, *, timeout: int | None
1748
1825
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
1749
1826
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
1750
1827
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
1828
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
1751
1829
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
1752
1830
  }
1753
1831
 
@@ -1765,7 +1843,10 @@ def get_project_data_node_updates(project_id: int | str, *, timeout: int | None
1765
1843
  else:
1766
1844
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
1767
1845
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
1768
- os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_id)
1846
+ project_uid = resolve_project_uid(project_id)
1847
+ project_row_id = resolve_project_row_id(project_id)
1848
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = project_uid
1849
+ os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_row_id)
1769
1850
 
1770
1851
  from mainsequence.client import utils as _client_utils
1771
1852
  from mainsequence.client.base import BaseObjectOrm
@@ -1782,7 +1863,7 @@ def get_project_data_node_updates(project_id: int | str, *, timeout: int | None
1782
1863
  BaseObjectOrm.ROOT_URL = root_url
1783
1864
  ClientProject.ROOT_URL = root_url
1784
1865
 
1785
- project = ClientProject.get(pk=project_id, timeout=timeout)
1866
+ project = ClientProject.get(pk=project_uid, timeout=timeout)
1786
1867
  updates = project.get_data_nodes_updates(timeout=timeout)
1787
1868
 
1788
1869
  out: list[dict[str, Any]] = []
@@ -1839,14 +1920,16 @@ def sync_project_after_commit(project_id: int | str, *, timeout: int | None = No
1839
1920
  - delegates request behavior and payload parsing to `Project.sync_project_after_commit()`
1840
1921
  """
1841
1922
  try:
1923
+ project_uid = resolve_project_uid(project_id)
1842
1924
  payload = _run_sdk_model_operation(
1843
1925
  module_name="mainsequence.client.models_tdag",
1844
1926
  class_name="Project",
1845
1927
  operation=lambda ClientProject: ClientProject.sync_project_after_commit(
1846
- int(project_id),
1928
+ project_uid,
1847
1929
  timeout=timeout,
1848
1930
  ),
1849
- project_id_env=project_id,
1931
+ project_id_env=resolve_project_row_id(project_id),
1932
+ project_uid_env=project_uid,
1850
1933
  )
1851
1934
  if payload is None:
1852
1935
  return None
@@ -1911,6 +1994,7 @@ def create_project_image(
1911
1994
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
1912
1995
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
1913
1996
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
1997
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
1914
1998
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
1915
1999
  }
1916
2000
 
@@ -1927,7 +2011,10 @@ def create_project_image(
1927
2011
  else:
1928
2012
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
1929
2013
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
1930
- os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(related_project_id)
2014
+ project_uid = resolve_project_uid(related_project_id)
2015
+ project_row_id = resolve_project_row_id(related_project_id)
2016
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = project_uid
2017
+ os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_row_id)
1931
2018
 
1932
2019
  from mainsequence.client import utils as _client_utils
1933
2020
  from mainsequence.client.base import BaseObjectOrm
@@ -1946,7 +2033,7 @@ def create_project_image(
1946
2033
 
1947
2034
  created = ClientProjectImage.create(
1948
2035
  project_repo_hash=project_repo_hash,
1949
- related_project_id=int(related_project_id),
2036
+ related_project_id=project_row_id,
1950
2037
  base_image_id=base_image_id,
1951
2038
  timeout=timeout,
1952
2039
  )
@@ -2018,6 +2105,7 @@ def list_project_images(
2018
2105
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
2019
2106
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
2020
2107
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
2108
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
2021
2109
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
2022
2110
  }
2023
2111
 
@@ -2034,7 +2122,10 @@ def list_project_images(
2034
2122
  else:
2035
2123
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
2036
2124
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
2037
- os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(related_project_id)
2125
+ project_uid = resolve_project_uid(related_project_id)
2126
+ project_row_id = resolve_project_row_id(related_project_id)
2127
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = project_uid
2128
+ os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_row_id)
2038
2129
 
2039
2130
  from mainsequence.client import utils as _client_utils
2040
2131
  from mainsequence.client.base import BaseObjectOrm
@@ -2052,7 +2143,7 @@ def list_project_images(
2052
2143
  ClientProjectImage.ROOT_URL = root_url
2053
2144
 
2054
2145
  merged_filters = dict(filters or {})
2055
- merged_filters["related_project__id__in"] = [int(related_project_id)]
2146
+ merged_filters["related_project__id__in"] = [project_row_id]
2056
2147
  images = ClientProjectImage.filter(timeout=timeout, **merged_filters)
2057
2148
 
2058
2149
  out: list[dict[str, Any]] = []
@@ -2243,6 +2334,7 @@ def list_project_jobs(
2243
2334
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
2244
2335
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
2245
2336
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
2337
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
2246
2338
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
2247
2339
  }
2248
2340
 
@@ -2259,7 +2351,10 @@ def list_project_jobs(
2259
2351
  else:
2260
2352
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
2261
2353
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
2262
- os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_id)
2354
+ project_uid = resolve_project_uid(project_id)
2355
+ project_row_id = resolve_project_row_id(project_id)
2356
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = project_uid
2357
+ os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_row_id)
2263
2358
 
2264
2359
  from mainsequence.client import utils as _client_utils
2265
2360
  from mainsequence.client.base import BaseObjectOrm
@@ -2279,7 +2374,7 @@ def list_project_jobs(
2279
2374
  extra_filters = dict(filters or {})
2280
2375
  jobs = ClientJob.filter(
2281
2376
  timeout=timeout,
2282
- **{**extra_filters, "project__id": int(project_id)},
2377
+ **{**extra_filters, "project__id": project_row_id},
2283
2378
  )
2284
2379
 
2285
2380
  out: list[dict[str, Any]] = []
@@ -2355,6 +2450,7 @@ def list_project_resources(
2355
2450
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
2356
2451
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
2357
2452
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
2453
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
2358
2454
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
2359
2455
  }
2360
2456
 
@@ -2371,7 +2467,10 @@ def list_project_resources(
2371
2467
  else:
2372
2468
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
2373
2469
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
2374
- os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_id)
2470
+ project_uid = resolve_project_uid(project_id)
2471
+ project_row_id = resolve_project_row_id(project_id)
2472
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = project_uid
2473
+ os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_row_id)
2375
2474
 
2376
2475
  from mainsequence.client import utils as _client_utils
2377
2476
  from mainsequence.client.base import BaseObjectOrm
@@ -2391,7 +2490,7 @@ def list_project_resources(
2391
2490
  merged_filters: dict[str, Any] = dict(filters or {})
2392
2491
  merged_filters.update(
2393
2492
  {
2394
- "project__id": int(project_id),
2493
+ "project__id": project_row_id,
2395
2494
  "repo_commit_sha": str(repo_commit_sha).strip(),
2396
2495
  }
2397
2496
  )
@@ -4210,6 +4309,7 @@ def create_project_job(
4210
4309
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
4211
4310
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
4212
4311
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
4312
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
4213
4313
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
4214
4314
  }
4215
4315
 
@@ -4226,7 +4326,10 @@ def create_project_job(
4226
4326
  else:
4227
4327
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
4228
4328
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
4229
- os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_id)
4329
+ project_uid = resolve_project_uid(project_id)
4330
+ project_row_id = resolve_project_row_id(project_id)
4331
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = project_uid
4332
+ os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_row_id)
4230
4333
 
4231
4334
  from mainsequence.client import utils as _client_utils
4232
4335
  from mainsequence.client.base import BaseObjectOrm
@@ -4245,7 +4348,7 @@ def create_project_job(
4245
4348
 
4246
4349
  created = ClientJob.create(
4247
4350
  name=name,
4248
- project_id=int(project_id),
4351
+ project_id=project_row_id,
4249
4352
  execution_path=execution_path,
4250
4353
  app_name=app_name,
4251
4354
  task_schedule=task_schedule,
@@ -4331,6 +4434,7 @@ def schedule_batch_project_jobs(
4331
4434
  "MAINSEQUENCE_ACCESS_TOKEN": os.environ.get("MAINSEQUENCE_ACCESS_TOKEN"),
4332
4435
  "MAINSEQUENCE_REFRESH_TOKEN": os.environ.get("MAINSEQUENCE_REFRESH_TOKEN"),
4333
4436
  "MAINSEQUENCE_ENDPOINT": os.environ.get("MAINSEQUENCE_ENDPOINT"),
4437
+ "MAIN_SEQUENCE_PROJECT_UID": os.environ.get("MAIN_SEQUENCE_PROJECT_UID"),
4334
4438
  "MAIN_SEQUENCE_PROJECT_ID": os.environ.get("MAIN_SEQUENCE_PROJECT_ID"),
4335
4439
  }
4336
4440
 
@@ -4347,7 +4451,10 @@ def schedule_batch_project_jobs(
4347
4451
  else:
4348
4452
  os.environ.pop("MAINSEQUENCE_REFRESH_TOKEN", None)
4349
4453
  os.environ["MAINSEQUENCE_ENDPOINT"] = endpoint
4350
- os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_id)
4454
+ project_uid = resolve_project_uid(project_id)
4455
+ project_row_id = resolve_project_row_id(project_id)
4456
+ os.environ["MAIN_SEQUENCE_PROJECT_UID"] = project_uid
4457
+ os.environ["MAIN_SEQUENCE_PROJECT_ID"] = str(project_row_id)
4351
4458
 
4352
4459
  from mainsequence.client import utils as _client_utils
4353
4460
  from mainsequence.client.base import BaseObjectOrm
@@ -4366,7 +4473,7 @@ def schedule_batch_project_jobs(
4366
4473
 
4367
4474
  created = ClientJob.bulk_get_or_create(
4368
4475
  yaml_file=file_path,
4369
- project_id=int(project_id),
4476
+ project_id=project_row_id,
4370
4477
  strict=bool(strict),
4371
4478
  timeout=timeout,
4372
4479
  )
@@ -4866,13 +4973,14 @@ def create_project(
4866
4973
 
4867
4974
  def delete_project(project_id: int | str, *, delete_repositories: bool = False) -> dict[str, Any] | None:
4868
4975
  """
4869
- Delete a project by id.
4976
+ Delete a project by public reference.
4870
4977
 
4871
4978
  Mirrors backend behavior:
4872
- - DELETE /orm/api/pods/projects/{id}/
4979
+ - DELETE /orm/api/pods/projects/{uid}/
4873
4980
  - optional query param delete_repositories=true
4874
4981
  """
4875
- path = f"/orm/api/pods/projects/{project_id}/"
4982
+ project_uid = resolve_project_uid(project_id)
4983
+ path = f"/orm/api/pods/projects/{project_uid}/"
4876
4984
  if delete_repositories:
4877
4985
  path = f"{path}?delete_repositories=true"
4878
4986
 
@@ -4942,7 +5050,7 @@ def add_deploy_key(project_id: int | str, key_title: str, public_key: str) -> No
4942
5050
  """
4943
5051
  r = authed(
4944
5052
  "POST",
4945
- f"/orm/api/pods/projects/{project_id}/add_deploy_key/",
5053
+ f"/orm/api/pods/projects/{resolve_project_uid(project_id)}/add_deploy_key/",
4946
5054
  {"key_title": key_title, "public_key": public_key},
4947
5055
  )
4948
5056
  r.raise_for_status()