mainsequence 6.0.20__tar.gz → 6.0.22__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 (126) hide show
  1. {mainsequence-6.0.20/mainsequence.egg-info → mainsequence-6.0.22}/PKG-INFO +1 -1
  2. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/api.py +1 -1
  3. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/cli.py +8 -6
  4. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/migrations.py +0 -9
  5. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/metatables/core.py +84 -54
  6. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/utils.py +16 -33
  7. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/logconf.py +23 -39
  8. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/__init__.py +2 -51
  9. mainsequence-6.0.22/mainsequence/runtime_context.py +145 -0
  10. {mainsequence-6.0.20 → mainsequence-6.0.22/mainsequence.egg-info}/PKG-INFO +1 -1
  11. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence.egg-info/SOURCES.txt +1 -0
  12. {mainsequence-6.0.20 → mainsequence-6.0.22}/pyproject.toml +1 -1
  13. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_auth_precedence.py +90 -5
  14. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_cli.py +1 -1
  15. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_cli_migrations.py +7 -8
  16. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_filter_normalization.py +32 -0
  17. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_logconf.py +18 -0
  18. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_meta_table_migrations.py +4 -8
  19. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_meta_tables_client_models.py +28 -5
  20. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_pod_project_resolution.py +150 -3
  21. {mainsequence-6.0.20 → mainsequence-6.0.22}/LICENSE +0 -0
  22. {mainsequence-6.0.20 → mainsequence-6.0.22}/README.md +0 -0
  23. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/AGENTS.md +0 -0
  24. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  25. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  26. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  27. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  28. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
  29. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
  30. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  31. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/maintenance/project-maintenance/SKILL.md +0 -0
  32. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  33. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  34. {mainsequence-6.0.20 → mainsequence-6.0.22}/agent_scaffold/skills/sdk_project_execution/SKILL.md +0 -0
  35. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/__init__.py +0 -0
  36. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/__main__.py +0 -0
  37. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/bootstrap.py +0 -0
  38. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/__init__.py +0 -0
  39. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/browser_auth.py +0 -0
  40. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/config.py +0 -0
  41. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/docker_utils.py +0 -0
  42. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/doctor.py +0 -0
  43. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/local_ops.py +0 -0
  44. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/model_filters.py +0 -0
  45. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/project_status.py +0 -0
  46. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/pydantic_cli.py +0 -0
  47. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/sdk_utils.py +0 -0
  48. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/ssh_utils.py +0 -0
  49. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/cli/ui.py +0 -0
  50. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/__init__.py +0 -0
  51. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/agent_runtime_models.py +0 -0
  52. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/base.py +0 -0
  53. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/client.py +0 -0
  54. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/compute_validation.py +0 -0
  55. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  56. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  57. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
  58. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
  59. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/dtype_codec.py +0 -0
  60. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/exceptions.py +0 -0
  61. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/fastapi/__init__.py +0 -0
  62. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/fastapi/auth.py +0 -0
  63. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/metatables/__init__.py +0 -0
  64. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/models_foundry.py +0 -0
  65. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/models_helpers.py +0 -0
  66. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/client/models_user.py +0 -0
  67. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/defaults.py +0 -0
  68. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/instrumentation/__init__.py +0 -0
  69. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/instrumentation/utils.py +0 -0
  70. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/__init__.py +0 -0
  71. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/__main__.py +0 -0
  72. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
  73. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
  74. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
  75. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
  76. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
  77. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/models.py +0 -0
  78. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
  79. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
  80. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
  81. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
  82. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/future_registry.py +0 -0
  83. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/hashing.py +0 -0
  84. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/alembic.py +0 -0
  85. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/env.py +0 -0
  86. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/provider.py +0 -0
  87. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/registry.py +0 -0
  88. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
  89. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
  90. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
  91. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
  92. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
  93. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/schema_names.py +0 -0
  94. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
  95. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/project_skills.py +0 -0
  96. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/runtime_flags.py +0 -0
  97. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence/scaffold_skills.py +0 -0
  98. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence.egg-info/dependency_links.txt +0 -0
  99. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence.egg-info/entry_points.txt +0 -0
  100. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence.egg-info/requires.txt +0 -0
  101. {mainsequence-6.0.20 → mainsequence-6.0.22}/mainsequence.egg-info/top_level.txt +0 -0
  102. {mainsequence-6.0.20 → mainsequence-6.0.22}/setup.cfg +0 -0
  103. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_build_operations_hashing.py +0 -0
  104. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_canonical_api_route_contract.py +0 -0
  105. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_cli_browser_auth.py +0 -0
  106. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_client.py +0 -0
  107. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_data_access_mixin_dimension_audit.py +0 -0
  108. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_data_node_storage_dimension_queries.py +0 -0
  109. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_data_node_update_flow.py +0 -0
  110. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_dependency_extras.py +0 -0
  111. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_duckdb_interface_dimensions.py +0 -0
  112. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_instrumentation.py +0 -0
  113. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
  114. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_models_user_request_bound_auth.py +0 -0
  115. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_project_batch_jobs_from_file.py +0 -0
  116. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_project_branch_contract.py +0 -0
  117. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_project_skills.py +0 -0
  118. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_run_configuration.py +0 -0
  119. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_scaffold_skills.py +0 -0
  120. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_schema_names.py +0 -0
  121. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_secret_client_model.py +0 -0
  122. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_source_table_configuration.py +0 -0
  123. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_sqlite_interface_dimensions.py +0 -0
  124. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_update_runner_uid_runtime.py +0 -0
  125. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_update_statistics.py +0 -0
  126. {mainsequence-6.0.20 → mainsequence-6.0.22}/tests/test_update_uid_guards.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 6.0.20
3
+ Version: 6.0.22
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: LicenseRef-MainSequence-Proprietary
@@ -4689,7 +4689,7 @@ def list_project_job_runs(
4689
4689
  ClientJobRun.ROOT_URL = root_url
4690
4690
 
4691
4691
  merged_filters = dict(filters or {})
4692
- merged_filters["job__uid"] = [str(job_uid)]
4692
+ merged_filters["job__uid"] = str(job_uid)
4693
4693
  runs = ClientJobRun.filter(timeout=timeout, **merged_filters)
4694
4694
  out: list[dict[str, Any]] = []
4695
4695
  for run in runs:
@@ -2163,7 +2163,8 @@ def _current_project_runtime_auth_env(backend_url: str) -> dict[str, str]:
2163
2163
  Return auth environment entries for local project `.env` provisioning.
2164
2164
 
2165
2165
  The output follows the active auth mode:
2166
- - runtime credential mode writes runtime credential keys and an exchanged access token
2166
+ - a backend-injected runtime credential mode preserves its credential keys
2167
+ and an exchanged access token
2167
2168
  - default JWT mode writes the current CLI session access/refresh token pair
2168
2169
  """
2169
2170
  if _runtime_credential_mode_enabled():
@@ -2308,9 +2309,11 @@ def login(
2308
2309
  Interactive login uses browser-based authentication and finishes with
2309
2310
  standard JWT access/refresh tokens persisted by the CLI.
2310
2311
 
2311
- If `MAINSEQUENCE_AUTH_MODE=runtime_credential`, login exchanges the
2312
- configured runtime credential for a short-lived access token instead of
2313
- opening the browser or persisting CLI JWT tokens.
2312
+ When a backend-launched process already has
2313
+ `MAINSEQUENCE_AUTH_MODE=runtime_credential`, login exchanges the injected
2314
+ runtime credential for a short-lived access token instead of opening the
2315
+ browser or persisting CLI JWT tokens. This is not a user branch/runtime
2316
+ selection mechanism.
2314
2317
 
2315
2318
  Parameters
2316
2319
  ----------
@@ -2345,7 +2348,6 @@ def login(
2345
2348
  mainsequence login --access-token "$TOKEN" --refresh-token "$REFRESH"
2346
2349
  mainsequence login --access-token "$TOKEN" --refresh-token "$REFRESH" --backend http://127.0.0.1:8000 --projects-base mainsequence-dev
2347
2350
  mainsequence login --export
2348
- MAINSEQUENCE_AUTH_MODE=runtime_credential mainsequence login
2349
2351
  ```
2350
2352
  """
2351
2353
  using_jwt = bool((access_token or "").strip() or (refresh_token or "").strip())
@@ -9222,7 +9224,7 @@ def project_job_runs_list_cmd(
9222
9224
  """
9223
9225
  List runs for a specific job.
9224
9226
 
9225
- Uses SDK client `JobRun.filter(job__uid=[job_uid])` as the single source of truth.
9227
+ Uses SDK client `JobRun.filter(job__uid=job_uid)` as the single source of truth.
9226
9228
 
9227
9229
  Examples
9228
9230
  --------
@@ -18,7 +18,6 @@ from mainsequence.client.metatables import (
18
18
  MetaTableMigrationConnectionRequest,
19
19
  TimeIndexMetaTable,
20
20
  )
21
- from mainsequence.client.metatables.core import _current_metatable_project_context
22
21
  from mainsequence.meta_tables.migrations import (
23
22
  AlembicMetaTableMigration,
24
23
  alembic_config_for_provider,
@@ -432,10 +431,8 @@ def _prepare_alembic_config(
432
431
  alembic_output: _AlembicOutput,
433
432
  prepare_provider_metatables: bool = True,
434
433
  ) -> tuple[Any, Any]:
435
- project_context = _current_metatable_project_context()
436
434
  _emit_status("Ensuring Alembic registry MetaTable...")
437
435
  registry_meta_table = migration.ensure_alembic_registry(
438
- project_context=project_context,
439
436
  timeout=timeout,
440
437
  on_metatable_reserved=_emit_metatable_reservation,
441
438
  )
@@ -443,7 +440,6 @@ def _prepare_alembic_config(
443
440
  if prepare_provider_metatables:
444
441
  _emit_status("Preparing platform-managed MetaTable reservations...")
445
442
  prepared = migration.prepare_for_alembic(
446
- project_context=project_context,
447
443
  timeout=timeout,
448
444
  on_metatable_reservation_request=_emit_metatable_reservation_request,
449
445
  on_metatable_reservation_status=_emit_status,
@@ -455,7 +451,6 @@ def _prepare_alembic_config(
455
451
  data_source_uid=migration._resolve_provider_data_source_uid(),
456
452
  meta_table_uids=[],
457
453
  owner_role_name=None,
458
- project_context=project_context,
459
454
  )
460
455
  _emit_status(
461
456
  "Prepared migration scope "
@@ -472,7 +467,6 @@ def _prepare_alembic_config(
472
467
  migration_namespace=migration.migration_namespace,
473
468
  migration_provider_key=migration.migration_provider_key,
474
469
  ttl_seconds=ttl_seconds,
475
- project_context=prepared.project_context,
476
470
  ),
477
471
  timeout=timeout,
478
472
  )
@@ -501,10 +495,8 @@ def _build_revision_alembic_config(
501
495
  _emit_status("Building local Alembic config for revision...")
502
496
  owner_role_name = None
503
497
  if requires_database and sqlalchemy_url in (None, ""):
504
- project_context = _current_metatable_project_context()
505
498
  _emit_status("Ensuring Alembic registry MetaTable for revision...")
506
499
  registry_meta_table = migration.ensure_alembic_registry(
507
- project_context=project_context,
508
500
  timeout=timeout,
509
501
  on_metatable_reserved=_emit_metatable_reservation,
510
502
  )
@@ -518,7 +510,6 @@ def _build_revision_alembic_config(
518
510
  migration_namespace=migration.migration_namespace,
519
511
  migration_provider_key=migration.migration_provider_key,
520
512
  ttl_seconds=ttl_seconds,
521
- project_context=project_context,
522
513
  ),
523
514
  timeout=timeout,
524
515
  )
@@ -31,6 +31,7 @@ from pydantic import (
31
31
  )
32
32
 
33
33
  from mainsequence.logconf import logger
34
+ from mainsequence.runtime_context import _get_backend_runtime_project_context_state
34
35
 
35
36
  from ..base import BaseObjectOrm, BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin
36
37
  from ..data_sources_interfaces import get_duckdb_interface_class, get_sqlite_interface_class
@@ -53,6 +54,7 @@ from ..utils import (
53
54
  bios_uuid,
54
55
  get_network_ip,
55
56
  is_process_running,
57
+ loaders,
56
58
  make_request,
57
59
  serialize_to_json,
58
60
  )
@@ -868,7 +870,6 @@ class ManagedMetaTableCollectionCreateRow(BasePydanticModel):
868
870
  physical_schema: str = Field(..., min_length=1)
869
871
  physical_table_name: str = Field(..., min_length=1)
870
872
  protect_from_deletion: bool = False
871
- project_context: MetaTableProjectContextRequest
872
873
  table_contract: dict[str, Any]
873
874
 
874
875
  @model_validator(mode="before")
@@ -930,7 +931,6 @@ class MetaTableMigrationConnectionRequest(BasePydanticModel):
930
931
  migration_namespace: str = ""
931
932
  migration_provider_key: str = ""
932
933
  ttl_seconds: int = Field(default=900, ge=60, le=3600)
933
- project_context: MetaTableProjectContextRequest | None = None
934
934
 
935
935
 
936
936
  class MetaTableMigrationConnection(BasePydanticModel):
@@ -947,7 +947,6 @@ class MetaTableMigrationConnection(BasePydanticModel):
947
947
 
948
948
 
949
949
  class ManagedMetaTableFinalizeRequest(BasePydanticModel):
950
- project_context: MetaTableProjectContextRequest | None = None
951
950
  meta_table_uids: list[str] = Field(
952
951
  ...,
953
952
  min_length=1,
@@ -1406,10 +1405,7 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1406
1405
  )
1407
1406
  if isinstance(payload, Mapping):
1408
1407
  payload = MetaTableMigrationConnectionRequest(**payload)
1409
- if payload.project_context is None:
1410
- payload = payload.model_copy(
1411
- update={"project_context": _current_metatable_project_context()}
1412
- )
1408
+ payload = _with_current_metatable_project_context(payload)
1413
1409
  response = self._post_detail_action(
1414
1410
  "migration-connection",
1415
1411
  payload,
@@ -1597,13 +1593,13 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1597
1593
  if request is not None and kwargs:
1598
1594
  raise ValueError("Pass either request or keyword fields, not both.")
1599
1595
  unvalidated_payload = _payload_json(request) if request is not None else dict(kwargs)
1600
- project_context = unvalidated_payload.pop("project_context", None)
1596
+ if "project_context" in unvalidated_payload:
1597
+ raise ValueError(
1598
+ "project_context is SDK-controlled and cannot be supplied by the caller."
1599
+ )
1601
1600
  payload = MetaTableRegistrationRequest.model_validate(unvalidated_payload)
1602
1601
  if payload.management_mode != "external_registered":
1603
- payload_json = _payload_json(payload)
1604
- if project_context is not None:
1605
- payload_json["project_context"] = project_context
1606
- payload = _with_current_metatable_project_context(payload_json)
1602
+ payload = _with_current_metatable_project_context(payload)
1607
1603
  response_json = cls._post_action(
1608
1604
  "register",
1609
1605
  payload,
@@ -1624,10 +1620,10 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1624
1620
  if on_status is not None:
1625
1621
  on_status(f"Serializing POST {url} payload...")
1626
1622
  payload_json = _payload_json_sequence(rows)
1627
- payload_json = _with_current_metatable_project_context_collection(payload_json)
1628
1623
  _ = [
1629
1624
  cls.COLLECTION_CREATE_ROW_MODEL.model_validate(row) for row in payload_json
1630
1625
  ]
1626
+ payload_json = _with_current_metatable_project_context_collection(payload_json)
1631
1627
  if on_status is not None:
1632
1628
  payload_size = len(json.dumps(payload_json, default=str))
1633
1629
  on_status(f"Serialized POST {url} payload bytes={payload_size}.")
@@ -1667,10 +1663,7 @@ class MetaTable(BasePydanticModel, LabelableObjectMixin, ShareableObjectMixin, B
1667
1663
  payload = request if request is not None else ManagedMetaTableFinalizeRequest(**kwargs)
1668
1664
  if isinstance(payload, Mapping):
1669
1665
  payload = ManagedMetaTableFinalizeRequest(**payload)
1670
- if payload.project_context is None:
1671
- payload = payload.model_copy(
1672
- update={"project_context": _current_metatable_project_context()}
1673
- )
1666
+ payload = _with_current_metatable_project_context(payload)
1674
1667
  response_json = cls._post_action(
1675
1668
  "finalize-managed",
1676
1669
  payload,
@@ -2458,15 +2451,26 @@ class DataNodeUpdate(TableUpdateNode, BaseObjectOrm):
2458
2451
  @classmethod
2459
2452
  def get_or_create(cls, **kwargs):
2460
2453
  url = cls.get_object_url() + "/get-or-create/"
2454
+ if "current_project_branch_uid" in kwargs:
2455
+ raise ValueError(
2456
+ "current_project_branch_uid is SDK-controlled and cannot be supplied "
2457
+ "by the caller."
2458
+ )
2461
2459
  kwargs = serialize_to_json(kwargs)
2462
- project_branch = _require_local_pod_project_branch("DataNodeUpdate.get_or_create")
2460
+ resolution = _resolve_local_pod_project()
2461
+ project_branch = resolution.project_branch
2462
+ if project_branch is None:
2463
+ project_branch = _require_local_pod_project_branch(
2464
+ "DataNodeUpdate.get_or_create"
2465
+ )
2463
2466
  project_branch_uid = str(getattr(project_branch, "uid", "") or "").strip()
2464
2467
  if not project_branch_uid:
2465
2468
  raise RuntimeError(
2466
2469
  "DataNodeUpdate.get_or_create requires a ProjectBranch uid, "
2467
2470
  "but the active branch does not expose one."
2468
2471
  )
2469
- kwargs["current_project_branch_uid"] = project_branch_uid
2472
+ if not resolution.backend_runtime_context:
2473
+ kwargs["current_project_branch_uid"] = project_branch_uid
2470
2474
  payload = {"json": kwargs}
2471
2475
  s = cls.build_session()
2472
2476
  r = make_request(s=s, loaders=cls.LOADERS, r_type="POST", url=url, payload=payload)
@@ -3406,14 +3410,14 @@ class TimeIndexMetaTable(MetaTable):
3406
3410
  if request is not None and kwargs:
3407
3411
  raise ValueError("Pass either request or keyword fields, not both.")
3408
3412
  unvalidated_payload = _payload_json(request) if request is not None else dict(kwargs)
3409
- project_context = unvalidated_payload.pop("project_context", None)
3413
+ if "project_context" in unvalidated_payload:
3414
+ raise ValueError(
3415
+ "project_context is SDK-controlled and cannot be supplied by the caller."
3416
+ )
3410
3417
  payload = TimeIndexMetaTableRegistrationRequest.model_validate(
3411
3418
  unvalidated_payload
3412
3419
  )
3413
- payload_json = _payload_json(payload)
3414
- if project_context is not None:
3415
- payload_json["project_context"] = project_context
3416
- payload_json = _with_current_metatable_project_context(payload_json)
3420
+ payload_json = _with_current_metatable_project_context(payload)
3417
3421
  request_payload = {"json": serialize_to_json(payload_json)}
3418
3422
  response = make_request(
3419
3423
  s=cls.build_session(),
@@ -4780,6 +4784,7 @@ class _PodProjectResolution:
4780
4784
  repository_branch: str | None
4781
4785
  status: str
4782
4786
  detail: str = ""
4787
+ backend_runtime_context: bool = False
4783
4788
 
4784
4789
  @property
4785
4790
  def remote_cache_key(self) -> tuple[str, str, str]:
@@ -4808,11 +4813,9 @@ def _reset_local_pod_project_resolution_cache() -> None:
4808
4813
 
4809
4814
  def _current_repository_branch() -> str | None:
4810
4815
  runtime_auth_mode = (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip()
4811
- runtime_project_branch_uid = (
4812
- os.environ.get("MAIN_SEQUENCE_PROJECT_BRANCH_UID") or ""
4813
- ).strip()
4814
- if runtime_project_branch_uid:
4815
- return (os.environ.get("MAINSEQUENCE_REPOSITORY_BRANCH") or "").strip() or None
4816
+ runtime_state = _get_backend_runtime_project_context_state()
4817
+ if runtime_state.verified and runtime_state.context is not None:
4818
+ return runtime_state.context.repository_branch
4816
4819
  if runtime_auth_mode in {"session_jwt", "runtime_credential"}:
4817
4820
  return None
4818
4821
  try:
@@ -4837,11 +4840,11 @@ def _build_local_pod_project_resolution(
4837
4840
  running_project_uid: str,
4838
4841
  running_project_branch_uid: str = "",
4839
4842
  repository_branch: str | None,
4843
+ backend_runtime_context: bool = False,
4840
4844
  ) -> _PodProjectResolution:
4841
4845
  from ..models_foundry import Project, ProjectBranch
4842
4846
 
4843
- runtime_auth_mode = (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip()
4844
- if runtime_auth_mode in {"session_jwt", "runtime_credential"} and (
4847
+ if backend_runtime_context and (
4845
4848
  not running_project_uid or not running_project_branch_uid
4846
4849
  ):
4847
4850
  return _PodProjectResolution(
@@ -4855,6 +4858,7 @@ def _build_local_pod_project_resolution(
4855
4858
  "Deployed runtime authentication did not provide a complete "
4856
4859
  "backend-issued ProjectBranch context."
4857
4860
  ),
4861
+ backend_runtime_context=True,
4858
4862
  )
4859
4863
 
4860
4864
  if not running_project_uid:
@@ -4994,6 +4998,7 @@ def _build_local_pod_project_resolution(
4994
4998
  project_branch=project_branch,
4995
4999
  repository_branch=resolved_repository_branch,
4996
5000
  status="resolved",
5001
+ backend_runtime_context=backend_runtime_context,
4997
5002
  )
4998
5003
 
4999
5004
 
@@ -5001,11 +5006,29 @@ def _resolve_local_pod_project(*, refresh: bool = False) -> _PodProjectResolutio
5001
5006
  global _POD_PROJECT_RESOLUTION_CACHE, _POD_PROJECT_RESOLUTION_CACHE_KEY
5002
5007
  global POD_PROJECT, POD_PROJECT_BRANCH
5003
5008
 
5004
- running_project_uid = (os.environ.get("MAIN_SEQUENCE_PROJECT_UID") or "").strip()
5005
- running_project_branch_uid = (
5006
- os.environ.get("MAIN_SEQUENCE_PROJECT_BRANCH_UID") or ""
5007
- ).strip()
5008
- repository_branch = _current_repository_branch()
5009
+ runtime_auth_mode = (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip()
5010
+ runtime_state = _get_backend_runtime_project_context_state()
5011
+ if runtime_auth_mode == "runtime_credential" and not runtime_state.verified:
5012
+ loaders.refresh_headers()
5013
+ runtime_state = _get_backend_runtime_project_context_state()
5014
+
5015
+ backend_runtime_context = runtime_auth_mode in {
5016
+ "session_jwt",
5017
+ "runtime_credential",
5018
+ }
5019
+ runtime_context = runtime_state.context if runtime_state.verified else None
5020
+ if backend_runtime_context and runtime_context is not None:
5021
+ running_project_uid = runtime_context.project_uid
5022
+ running_project_branch_uid = runtime_context.project_branch_uid
5023
+ repository_branch = runtime_context.repository_branch
5024
+ elif backend_runtime_context:
5025
+ running_project_uid = ""
5026
+ running_project_branch_uid = ""
5027
+ repository_branch = None
5028
+ else:
5029
+ running_project_uid = (os.environ.get("MAIN_SEQUENCE_PROJECT_UID") or "").strip()
5030
+ running_project_branch_uid = ""
5031
+ repository_branch = _current_repository_branch()
5009
5032
  cache_key = (
5010
5033
  running_project_uid,
5011
5034
  running_project_branch_uid,
@@ -5022,6 +5045,7 @@ def _resolve_local_pod_project(*, refresh: bool = False) -> _PodProjectResolutio
5022
5045
  running_project_uid=running_project_uid,
5023
5046
  running_project_branch_uid=running_project_branch_uid,
5024
5047
  repository_branch=repository_branch,
5048
+ backend_runtime_context=backend_runtime_context,
5025
5049
  )
5026
5050
  _POD_PROJECT_RESOLUTION_CACHE_KEY = cache_key
5027
5051
  POD_PROJECT = _POD_PROJECT_RESOLUTION_CACHE.project
@@ -5089,18 +5113,28 @@ def _current_metatable_project_context() -> MetaTableProjectContextRequest:
5089
5113
  return _metatable_project_context_from_resolution(_resolve_local_pod_project())
5090
5114
 
5091
5115
 
5116
+ def _metatable_project_context_for_request() -> MetaTableProjectContextRequest | None:
5117
+ resolution = _resolve_local_pod_project()
5118
+ if resolution.backend_runtime_context:
5119
+ if resolution.project_branch is None:
5120
+ _metatable_project_context_from_resolution(resolution)
5121
+ return None
5122
+ return _current_metatable_project_context()
5123
+
5124
+
5092
5125
  def _with_current_metatable_project_context(
5093
5126
  payload: Mapping[str, Any] | BasePydanticModel,
5094
5127
  ) -> dict[str, Any]:
5095
5128
  payload_json = _payload_json(payload)
5129
+ if "project_context" in payload_json:
5130
+ raise ValueError(
5131
+ "project_context is SDK-controlled and cannot be supplied by the caller."
5132
+ )
5096
5133
  if payload_json.get("management_mode") == "external_registered":
5097
5134
  return payload_json
5098
- project_context = payload_json.get("project_context")
5099
- if project_context is None:
5100
- project_context = _current_metatable_project_context()
5101
- else:
5102
- project_context = MetaTableProjectContextRequest.model_validate(project_context)
5103
- payload_json["project_context"] = project_context.model_dump(mode="json")
5135
+ project_context = _metatable_project_context_for_request()
5136
+ if project_context is not None:
5137
+ payload_json["project_context"] = project_context.model_dump(mode="json")
5104
5138
  return payload_json
5105
5139
 
5106
5140
 
@@ -5108,25 +5142,21 @@ def _with_current_metatable_project_context_collection(
5108
5142
  payloads: Sequence[Mapping[str, Any]],
5109
5143
  ) -> list[dict[str, Any]]:
5110
5144
  payload_rows = [dict(payload) for payload in payloads]
5145
+ if any("project_context" in row for row in payload_rows):
5146
+ raise ValueError(
5147
+ "project_context is SDK-controlled and cannot be supplied by the caller."
5148
+ )
5111
5149
  managed_rows = [
5112
5150
  row for row in payload_rows if row.get("management_mode") != "external_registered"
5113
5151
  ]
5114
5152
  if not managed_rows:
5115
5153
  return payload_rows
5116
5154
 
5117
- current_project_context: dict[str, str] | None = None
5118
- for row in managed_rows:
5119
- project_context = row.get("project_context")
5120
- if project_context is None:
5121
- if current_project_context is None:
5122
- current_project_context = _current_metatable_project_context().model_dump(
5123
- mode="json"
5124
- )
5155
+ project_context = _metatable_project_context_for_request()
5156
+ if project_context is not None:
5157
+ current_project_context = project_context.model_dump(mode="json")
5158
+ for row in managed_rows:
5125
5159
  row["project_context"] = current_project_context
5126
- else:
5127
- row["project_context"] = MetaTableProjectContextRequest.model_validate(
5128
- project_context
5129
- ).model_dump(mode="json")
5130
5160
  return payload_rows
5131
5161
 
5132
5162
 
@@ -23,6 +23,11 @@ from urllib3.util.retry import Retry
23
23
 
24
24
  from mainsequence.defaults import resolve_backend_endpoint
25
25
  from mainsequence.logconf import logger
26
+ from mainsequence.runtime_context import (
27
+ BackendRuntimeProjectContextError,
28
+ _get_backend_runtime_project_context_state,
29
+ _install_backend_runtime_project_context,
30
+ )
26
31
 
27
32
  # ---- Backend defaults (single source of truth) ----
28
33
  MAINSEQUENCE_ENDPOINT = resolve_backend_endpoint()
@@ -62,40 +67,16 @@ class AuthError(Exception):
62
67
  pass
63
68
 
64
69
 
65
- RUNTIME_PROJECT_CONTEXT_ENV = {
66
- "project_uid": "MAIN_SEQUENCE_PROJECT_UID",
67
- "project_branch_uid": "MAIN_SEQUENCE_PROJECT_BRANCH_UID",
68
- "repository_branch": "MAINSEQUENCE_REPOSITORY_BRANCH",
69
- "organization_project_environment_uid": (
70
- "MAIN_SEQUENCE_ORGANIZATION_PROJECT_ENVIRONMENT_UID"
71
- ),
72
- }
73
-
74
-
75
- def apply_runtime_project_context(payload: object) -> None:
70
+ def _apply_runtime_project_context_from_exchange(payload: object) -> None:
76
71
  if not isinstance(payload, dict):
77
- return
78
- context = payload.get("runtime_project_context")
79
- if context is None:
80
- return
81
- if not isinstance(context, dict):
82
- raise AuthError("runtime_project_context must be an object.")
83
- missing = [key for key in RUNTIME_PROJECT_CONTEXT_ENV if not context.get(key)]
84
- if missing:
85
- raise AuthError(
86
- "runtime_project_context is incomplete: " + ", ".join(sorted(missing))
72
+ raise AuthError("Runtime credential exchange response must be an object.")
73
+ try:
74
+ _install_backend_runtime_project_context(
75
+ payload,
76
+ source="runtime_credential_exchange",
87
77
  )
88
- for key, environment_name in RUNTIME_PROJECT_CONTEXT_ENV.items():
89
- expected = str(context[key])
90
- current = str(os.environ.get(environment_name) or "").strip()
91
- if current and current != expected:
92
- raise AuthError(
93
- "runtime_project_context does not match the existing deployed "
94
- f"context for {key}."
95
- )
96
- for key, environment_name in RUNTIME_PROJECT_CONTEXT_ENV.items():
97
- expected = str(context[key])
98
- os.environ[environment_name] = expected
78
+ except BackendRuntimeProjectContextError as exc:
79
+ raise AuthError(str(exc)) from exc
99
80
 
100
81
 
101
82
  def set_mainsequence_endpoint(endpoint: str) -> None:
@@ -238,6 +219,8 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
238
219
  return (os.getenv("MAINSEQUENCE_ACCESS_TOKEN") or "").strip() or None
239
220
 
240
221
  def _needs_exchange(self) -> bool:
222
+ if not _get_backend_runtime_project_context_state().verified:
223
+ return True
241
224
  access_token = self._current_access_token()
242
225
  if not access_token:
243
226
  return True
@@ -294,7 +277,7 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
294
277
  )
295
278
 
296
279
  data = response.json()
297
- apply_runtime_project_context(data)
280
+ _apply_runtime_project_context_from_exchange(data)
298
281
  access = str(data.get("access") or "").strip()
299
282
  if not access:
300
283
  raise AuthError(
@@ -21,42 +21,26 @@ from structlog.stdlib import BoundLogger
21
21
 
22
22
  from .defaults import resolve_backend_endpoint
23
23
  from .instrumentation import OTelJSONRenderer
24
+ from .runtime_context import (
25
+ RUNTIME_PROJECT_CONTEXT_ENV,
26
+ BackendRuntimeContextSource,
27
+ BackendRuntimeProjectContextError,
28
+ _get_backend_runtime_project_context_state,
29
+ _install_backend_runtime_project_context,
30
+ )
24
31
  from .runtime_flags import is_running_in_pod
25
32
 
26
33
  logger = None
27
34
 
28
- RUNTIME_PROJECT_CONTEXT_ENV = {
29
- "project_uid": "MAIN_SEQUENCE_PROJECT_UID",
30
- "project_branch_uid": "MAIN_SEQUENCE_PROJECT_BRANCH_UID",
31
- "repository_branch": "MAINSEQUENCE_REPOSITORY_BRANCH",
32
- "organization_project_environment_uid": (
33
- "MAIN_SEQUENCE_ORGANIZATION_PROJECT_ENVIRONMENT_UID"
34
- ),
35
- }
36
-
37
-
38
- def _apply_runtime_project_context(payload: Mapping[str, Any]) -> None:
39
- context = payload.get("runtime_project_context")
40
- if context is None:
41
- return
42
- if not isinstance(context, Mapping):
43
- raise RuntimeError("runtime_project_context must be an object.")
44
- missing = [key for key in RUNTIME_PROJECT_CONTEXT_ENV if not context.get(key)]
45
- if missing:
46
- raise RuntimeError(
47
- "runtime_project_context is incomplete: " + ", ".join(sorted(missing))
48
- )
49
- for key, environment_name in RUNTIME_PROJECT_CONTEXT_ENV.items():
50
- expected = str(context[key])
51
- current = str(os.environ.get(environment_name) or "").strip()
52
- if current and current != expected:
53
- raise RuntimeError(
54
- "runtime_project_context does not match the existing deployed "
55
- f"context for {key}."
56
- )
57
- for key, environment_name in RUNTIME_PROJECT_CONTEXT_ENV.items():
58
- expected = str(context[key])
59
- os.environ[environment_name] = expected
35
+ def _apply_runtime_project_context(
36
+ payload: Mapping[str, Any],
37
+ *,
38
+ source: BackendRuntimeContextSource,
39
+ ) -> None:
40
+ try:
41
+ _install_backend_runtime_project_context(payload, source=source)
42
+ except BackendRuntimeProjectContextError as exc:
43
+ raise RuntimeError(str(exc)) from exc
60
44
 
61
45
 
62
46
  def _get_sdk_version() -> str:
@@ -157,7 +141,7 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
157
141
  if not access_token:
158
142
  return False
159
143
 
160
- _apply_runtime_project_context(data)
144
+ _apply_runtime_project_context(data, source="runtime_credential_exchange")
161
145
  os.environ["MAINSEQUENCE_ACCESS_TOKEN"] = access_token
162
146
  return True
163
147
 
@@ -256,7 +240,8 @@ def _apply_additional_environment(startup_state: Mapping[str, Any]) -> None:
256
240
  "runtime_project_context_missing: JobRun startup state did not "
257
241
  "include backend-issued ProjectBranch context."
258
242
  )
259
- _apply_runtime_project_context(startup_state)
243
+ if "runtime_project_context" in startup_state:
244
+ _apply_runtime_project_context(startup_state, source="job_run_startup")
260
245
  extra = startup_state.get("additional_environment")
261
246
  if isinstance(extra, dict):
262
247
  for k, v in extra.items():
@@ -274,11 +259,10 @@ def _build_backend_bindings(
274
259
  "job_run_uid": startup_state.get("job_run_uid"),
275
260
  "command_id": startup_state.get("command_id"),
276
261
  }
277
- runtime_context = startup_state.get("runtime_project_context")
278
- if not isinstance(runtime_context, Mapping):
279
- runtime_context = {}
280
- for field, environment_name in RUNTIME_PROJECT_CONTEXT_ENV.items():
281
- bindings[field] = runtime_context.get(field) or os.environ.get(environment_name)
262
+ runtime_state = _get_backend_runtime_project_context_state()
263
+ runtime_context = runtime_state.context.as_dict() if runtime_state.context else {}
264
+ for field in RUNTIME_PROJECT_CONTEXT_ENV:
265
+ bindings[field] = runtime_context.get(field)
282
266
 
283
267
  if not bindings.get("project_branch_uid"):
284
268
  if "user_id" in startup_state: