mainsequence 3.18.2__tar.gz → 3.18.4__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 (179) hide show
  1. {mainsequence-3.18.2 → mainsequence-3.18.4}/PKG-INFO +1 -1
  2. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/api.py +30 -2
  3. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/cli.py +136 -25
  4. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/config.py +5 -4
  5. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/utils.py +117 -1
  6. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/logconf.py +48 -9
  7. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/PKG-INFO +1 -1
  8. {mainsequence-3.18.2 → mainsequence-3.18.4}/pyproject.toml +1 -1
  9. mainsequence-3.18.4/tests/test_auth_precedence.py +537 -0
  10. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_cli.py +181 -2
  11. mainsequence-3.18.2/tests/test_auth_precedence.py +0 -214
  12. {mainsequence-3.18.2 → mainsequence-3.18.4}/LICENSE +0 -0
  13. {mainsequence-3.18.2 → mainsequence-3.18.4}/README.md +0 -0
  14. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/AGENTS.md +0 -0
  15. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  16. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
  17. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
  18. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
  19. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
  20. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
  21. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
  22. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
  23. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/data_publishing/simple_tables/SKILL.md +0 -0
  24. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  25. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/maintenance/local_journal/SKILL.md +0 -0
  26. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/markets_platform/assets_and_translation/SKILL.md +0 -0
  27. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/markets_platform/instruments_and_pricing/SKILL.md +0 -0
  28. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/markets_platform/virtualfundbuilder/SKILL.md +0 -0
  29. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  30. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  31. {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
  32. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/__init__.py +0 -0
  33. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/__main__.py +0 -0
  34. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/bootstrap.py +0 -0
  35. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/__init__.py +0 -0
  36. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/browser_auth.py +0 -0
  37. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/docker_utils.py +0 -0
  38. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/doctor.py +0 -0
  39. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/local_ops.py +0 -0
  40. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/model_filters.py +0 -0
  41. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/project_status.py +0 -0
  42. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/pydantic_cli.py +0 -0
  43. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/sdk_utils.py +0 -0
  44. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/ssh_utils.py +0 -0
  45. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/ui.py +0 -0
  46. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/__init__.py +0 -0
  47. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/agent.py +0 -0
  48. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/agent_runtime_models.py +0 -0
  49. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/base.py +0 -0
  50. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/client.py +0 -0
  51. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/__init__.py +0 -0
  52. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/app_component.py +0 -0
  53. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/data_models.py +0 -0
  54. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/workspace.py +0 -0
  55. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
  56. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  57. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
  58. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/data_sources_interfaces/timescale.py +0 -0
  59. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/exceptions.py +0 -0
  60. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/fastapi/__init__.py +0 -0
  61. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/fastapi/auth.py +0 -0
  62. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_helpers.py +0 -0
  63. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_simple_tables.py +0 -0
  64. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_tdag.py +0 -0
  65. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_user.py +0 -0
  66. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_vam.py +0 -0
  67. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/compute_validation.py +0 -0
  68. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/__init__.py +0 -0
  69. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/__init__.py +0 -0
  70. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/config.toml +0 -0
  71. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
  72. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_1_base64.txt +0 -0
  73. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_2_base64.txt +0 -0
  74. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_3_base64.txt +0 -0
  75. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_4_base64.txt +0 -0
  76. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_5_base64.txt +0 -0
  77. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/logo.png +0 -0
  78. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/__init__.py +0 -0
  79. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/asset_select.py +0 -0
  80. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/date_settings.py +0 -0
  81. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/logged_user.py +0 -0
  82. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/core/__init__.py +0 -0
  83. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/core/theme.py +0 -0
  84. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/instruments/__init__.py +0 -0
  85. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/instruments/streamlit_form_factory.py +0 -0
  86. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
  87. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/scaffold.py +0 -0
  88. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instrumentation/__init__.py +0 -0
  89. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instrumentation/utils.py +0 -0
  90. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/__init__.py +0 -0
  91. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/data_interface/__init__.py +0 -0
  92. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/data_interface/data_interface.py +0 -0
  93. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/__init__.py +0 -0
  94. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/base_instrument.py +0 -0
  95. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/bond.py +0 -0
  96. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/callability.py +0 -0
  97. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/interest_rate_swap.py +0 -0
  98. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/json_codec.py +0 -0
  99. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/position.py +0 -0
  100. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/ql_fields.py +0 -0
  101. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/__init__.py +0 -0
  102. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/__init__.py +0 -0
  103. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/curve_codec.py +0 -0
  104. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/nodes.py +0 -0
  105. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/registry.py +0 -0
  106. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/__init__.py +0 -0
  107. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/bond_pricer.py +0 -0
  108. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/indices.py +0 -0
  109. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/indices_builders.py +0 -0
  110. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/swap_pricer.py +0 -0
  111. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/settings.py +0 -0
  112. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/utils.py +0 -0
  113. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/runtime_flags.py +0 -0
  114. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/__init__.py +0 -0
  115. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/__main__.py +0 -0
  116. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/base_persist_managers.py +0 -0
  117. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/config.py +0 -0
  118. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/configuration_models.py +0 -0
  119. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/__init__.py +0 -0
  120. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/build_operations.py +0 -0
  121. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/data_nodes.py +0 -0
  122. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/filters.py +0 -0
  123. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/models.py +0 -0
  124. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/namespacing.py +0 -0
  125. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/persist_managers.py +0 -0
  126. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/run_operations.py +0 -0
  127. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/utils.py +0 -0
  128. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/filters.py +0 -0
  129. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/future_registry.py +0 -0
  130. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/pydantic_metadata.py +0 -0
  131. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/__init__.py +0 -0
  132. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/filters.py +0 -0
  133. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/models.py +0 -0
  134. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/persist_managers.py +0 -0
  135. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/schema.py +0 -0
  136. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/table_nodes.py +0 -0
  137. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/utils.py +0 -0
  138. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/__init__.py +0 -0
  139. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
  140. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +0 -0
  141. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +0 -0
  142. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +0 -0
  143. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +0 -0
  144. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +0 -0
  145. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +0 -0
  146. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/prices/__init__.py +0 -0
  147. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +0 -0
  148. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/prices/utils.py +0 -0
  149. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +0 -0
  150. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +0 -0
  151. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/enums.py +0 -0
  152. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/models.py +0 -0
  153. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/portfolio_nodes.py +0 -0
  154. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
  155. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +0 -0
  156. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +0 -0
  157. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/utils.py +0 -0
  158. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/SOURCES.txt +0 -0
  159. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/dependency_links.txt +0 -0
  160. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/entry_points.txt +0 -0
  161. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/requires.txt +0 -0
  162. {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/top_level.txt +0 -0
  163. {mainsequence-3.18.2 → mainsequence-3.18.4}/setup.cfg +0 -0
  164. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_build_operations_hashing.py +0 -0
  165. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_cli_browser_auth.py +0 -0
  166. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_client.py +0 -0
  167. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_command_center_app_component_models.py +0 -0
  168. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_command_center_models.py +0 -0
  169. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_dependency_extras.py +0 -0
  170. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_filter_normalization.py +0 -0
  171. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_instruments.py +0 -0
  172. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_logconf.py +0 -0
  173. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_logged_user_components.py +0 -0
  174. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_models_user_request_bound_auth.py +0 -0
  175. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_pod_project_resolution.py +0 -0
  176. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_project_batch_jobs_from_file.py +0 -0
  177. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_run_configuration.py +0 -0
  178. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_simple_tables_configuration_hashing.py +0 -0
  179. {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_simple_tables_persistence.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 3.18.2
3
+ Version: 3.18.4
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
@@ -178,6 +178,28 @@ def refresh_access() -> str:
178
178
  NotLoggedIn: if refresh is missing or refresh fails
179
179
  """
180
180
  refresh = _refresh_token()
181
+ runtime_mode = (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip().lower() == "runtime_credential"
182
+
183
+ if not refresh and runtime_mode:
184
+ try:
185
+ from mainsequence.client.utils import RuntimeCredentialAuthProvider
186
+ except Exception as exc:
187
+ raise NotLoggedIn(f"Runtime credential auth is unavailable: {exc}") from exc
188
+
189
+ token_url = f"{backend_url().rstrip('/')}/orm/api/pods/runtime-credentials/token/"
190
+ try:
191
+ RuntimeCredentialAuthProvider(token_url=token_url).refresh(force=True)
192
+ except Exception as exc:
193
+ raise NotLoggedIn(f"Runtime credential exchange failed: {exc}") from exc
194
+
195
+ access = (os.environ.get("MAINSEQUENCE_ACCESS_TOKEN") or "").strip()
196
+ if not access:
197
+ raise NotLoggedIn("Runtime credential exchange did not produce MAINSEQUENCE_ACCESS_TOKEN.")
198
+
199
+ tokens = get_tokens()
200
+ save_tokens(tokens.get("username") or "", access, "")
201
+ return access
202
+
181
203
  if not refresh:
182
204
  raise NotLoggedIn("Not logged in. Run `mainsequence login`.")
183
205
 
@@ -422,7 +444,8 @@ def get_logged_user_details() -> dict[str, Any]:
422
444
 
423
445
  The CLI does not naturally run inside a request context, so this bridge resolves
424
446
  the current user id from the authenticated API session and temporarily binds
425
- `X-User-ID` into `mainsequence.client.models_user._CURRENT_AUTH_HEADERS`
447
+ `X-User-ID` plus `Authorization` into
448
+ `mainsequence.client.models_user._CURRENT_AUTH_HEADERS`
426
449
  before calling the SDK method.
427
450
  """
428
451
  tokens = get_tokens()
@@ -486,7 +509,12 @@ def get_logged_user_details() -> dict[str, Any]:
486
509
 
487
510
  BaseObjectOrm.ROOT_URL = root_url
488
511
  ClientUser.ROOT_URL = root_url
489
- headers_token = current_auth_headers.set({"X-User-ID": str(user_id)})
512
+ headers_token = current_auth_headers.set(
513
+ {
514
+ "X-User-ID": str(user_id),
515
+ "Authorization": f"Bearer {access}",
516
+ }
517
+ )
490
518
 
491
519
  user = ClientUser.get_logged_user()
492
520
  if isinstance(user, dict):
@@ -226,6 +226,7 @@ from .ssh_utils import (
226
226
  from .ui import error, info, print_kv, print_table, status, success, warn
227
227
 
228
228
  JSON_OUTPUT_CONTEXT_KEY = "json_output"
229
+ LOGIN_DEFAULT_BACKEND_URL = "https://api.main-sequence.io"
229
230
 
230
231
 
231
232
  class MainSequenceGroup(typer.core.TyperGroup):
@@ -756,6 +757,28 @@ def _require_login() -> dict:
756
757
  raise typer.Exit(1) from e
757
758
 
758
759
 
760
+ def _runtime_credential_mode_enabled() -> bool:
761
+ return (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip().lower() == "runtime_credential"
762
+
763
+
764
+ def _exchange_runtime_credential_for_cli_login(backend_url: str) -> str:
765
+ try:
766
+ from mainsequence.client.utils import RuntimeCredentialAuthProvider
767
+ except Exception as exc:
768
+ raise ApiError(f"Runtime credential auth is unavailable: {exc}") from exc
769
+
770
+ token_url = f"{backend_url.rstrip('/')}/orm/api/pods/runtime-credentials/token/"
771
+ try:
772
+ RuntimeCredentialAuthProvider(token_url=token_url).refresh(force=True)
773
+ except Exception as exc:
774
+ raise ApiError(f"Runtime credential exchange failed: {exc}") from exc
775
+
776
+ access = (os.environ.get("MAINSEQUENCE_ACCESS_TOKEN") or "").strip()
777
+ if not access:
778
+ raise ApiError("Runtime credential exchange did not produce MAINSEQUENCE_ACCESS_TOKEN.")
779
+ return access
780
+
781
+
759
782
  def _resolve_project_dir(project_id: int | None, path: str | None) -> pathlib.Path:
760
783
  """
761
784
  Resolve project directory by:
@@ -2034,10 +2057,15 @@ def login(
2034
2057
  Persists auth tokens in the active CLI auth store so subsequent
2035
2058
  CLI invocations can run without re-authentication. Backend/base-folder
2036
2059
  overrides passed to `login` are scoped to the current terminal session.
2060
+ When no backend is provided, login defaults to `https://api.main-sequence.io`.
2037
2061
 
2038
2062
  Interactive login uses browser-based authentication and finishes with
2039
2063
  standard JWT access/refresh tokens persisted by the CLI.
2040
2064
 
2065
+ If `MAINSEQUENCE_AUTH_MODE=runtime_credential`, login exchanges the
2066
+ configured runtime credential for a short-lived access token instead of
2067
+ opening the browser or persisting CLI JWT tokens.
2068
+
2041
2069
  Parameters
2042
2070
  ----------
2043
2071
  backend:
@@ -2066,9 +2094,21 @@ def login(
2066
2094
  mainsequence login --access-token "$TOKEN" --refresh-token "$REFRESH"
2067
2095
  mainsequence login --access-token "$TOKEN" --refresh-token "$REFRESH" --backend http://127.0.0.1:8000 --projects-base mainsequence-dev
2068
2096
  mainsequence login --export
2097
+ MAINSEQUENCE_AUTH_MODE=runtime_credential mainsequence login
2069
2098
  ```
2070
2099
  """
2071
2100
  using_jwt = bool((access_token or "").strip() or (refresh_token or "").strip())
2101
+ using_runtime_credential = _runtime_credential_mode_enabled()
2102
+
2103
+ if using_runtime_credential and using_jwt:
2104
+ error(
2105
+ "Runtime credential login cannot be combined with "
2106
+ "--access-token/--refresh-token."
2107
+ )
2108
+ raise typer.Exit(1)
2109
+
2110
+ if using_runtime_credential and no_open:
2111
+ warn("--no-open is ignored when MAINSEQUENCE_AUTH_MODE=runtime_credential.")
2072
2112
 
2073
2113
  if not using_jwt and backend and "@" in backend:
2074
2114
  error(
@@ -2081,7 +2121,8 @@ def login(
2081
2121
  if cfg.normalize_backend_url(backend) != cfg.normalize_backend_url(backend_option):
2082
2122
  error("Pass backend either positionally or with --backend, not both.")
2083
2123
  raise typer.Exit(1)
2084
- effective_backend_input = backend_option if backend_option is not None else backend
2124
+ explicit_backend_input = backend_option if backend_option is not None else backend
2125
+ effective_backend_input = explicit_backend_input if explicit_backend_input is not None else LOGIN_DEFAULT_BACKEND_URL
2085
2126
 
2086
2127
  if projects_base and projects_base_option:
2087
2128
  if cfg.normalize_mainsequence_path(projects_base) != cfg.normalize_mainsequence_path(projects_base_option):
@@ -2100,28 +2141,39 @@ def login(
2100
2141
  raise typer.Exit(1)
2101
2142
 
2102
2143
  current_backend = cfg.backend_url()
2103
- normalized_backend = cfg.normalize_backend_url(effective_backend_input) if effective_backend_input else None
2144
+ normalized_backend = cfg.normalize_backend_url(effective_backend_input)
2104
2145
 
2105
- if normalized_backend and normalized_backend != current_backend:
2146
+ if explicit_backend_input is not None and normalized_backend != current_backend:
2106
2147
  if not effective_projects_base_input:
2107
2148
  error("When using a different backend, you must also specify a projects base folder.")
2108
2149
  raise typer.Exit(1)
2109
2150
 
2110
2151
  previous_backend_override = os.environ.get("MAIN_SEQUENCE_BACKEND_URL")
2111
- if normalized_backend:
2112
- os.environ["MAIN_SEQUENCE_BACKEND_URL"] = normalized_backend
2152
+ os.environ["MAIN_SEQUENCE_BACKEND_URL"] = normalized_backend
2113
2153
 
2114
2154
  try:
2115
- if using_jwt:
2155
+ if using_runtime_credential:
2156
+ access = _exchange_runtime_credential_for_cli_login(normalized_backend)
2157
+ persisted = cfg.save_tokens("", access, "")
2158
+ res = {
2159
+ "username": "",
2160
+ "backend": normalized_backend,
2161
+ "access": access,
2162
+ "refresh": "",
2163
+ "persisted": bool(persisted),
2164
+ "auth_mode": "runtime_credential",
2165
+ }
2166
+ elif using_jwt:
2116
2167
  os.environ.pop(cfg.ENV_USERNAME, None)
2117
2168
  os.environ.pop(cfg.LEGACY_ENV_USERNAME, None)
2118
2169
  persisted = cfg.save_tokens("", (access_token or "").strip(), (refresh_token or "").strip())
2119
2170
  res = {
2120
2171
  "username": "",
2121
- "backend": normalized_backend or current_backend,
2172
+ "backend": normalized_backend,
2122
2173
  "access": (access_token or "").strip(),
2123
2174
  "refresh": (refresh_token or "").strip(),
2124
2175
  "persisted": bool(persisted),
2176
+ "auth_mode": "jwt",
2125
2177
  }
2126
2178
  else:
2127
2179
  def _emit_auth_url(url: str) -> None:
@@ -2146,10 +2198,11 @@ def login(
2146
2198
 
2147
2199
  res = {
2148
2200
  "username": username,
2149
- "backend": normalized_backend or current_backend,
2201
+ "backend": normalized_backend,
2150
2202
  "access": access,
2151
2203
  "refresh": refresh,
2152
2204
  "persisted": bool(persisted),
2205
+ "auth_mode": "jwt",
2153
2206
  }
2154
2207
  except BrowserAuthError as e:
2155
2208
  error(f"Browser login failed: {e}")
@@ -2158,26 +2211,26 @@ def login(
2158
2211
  error(f"Login failed: {e}")
2159
2212
  raise typer.Exit(1) from e
2160
2213
  finally:
2161
- if normalized_backend:
2162
- if previous_backend_override is None:
2163
- os.environ.pop("MAIN_SEQUENCE_BACKEND_URL", None)
2164
- else:
2165
- os.environ["MAIN_SEQUENCE_BACKEND_URL"] = previous_backend_override
2214
+ if previous_backend_override is None:
2215
+ os.environ.pop("MAIN_SEQUENCE_BACKEND_URL", None)
2216
+ else:
2217
+ os.environ["MAIN_SEQUENCE_BACKEND_URL"] = previous_backend_override
2166
2218
 
2167
- if normalized_backend or effective_projects_base_input:
2168
- cfg.set_session_overrides(
2169
- backend_url=normalized_backend,
2170
- mainsequence_path=effective_projects_base_input,
2171
- )
2172
- else:
2173
- cfg.clear_session_overrides()
2219
+ cfg.set_session_overrides(
2220
+ backend_url=normalized_backend,
2221
+ mainsequence_path=effective_projects_base_input,
2222
+ )
2174
2223
 
2175
2224
  if export:
2176
2225
  access = (res.get("access") or "").replace('"', '\\"')
2177
2226
  refresh = (res.get("refresh") or "").replace('"', '\\"')
2178
2227
  username = (res.get("username") or "").replace('"', '\\"')
2228
+ auth_mode = (res.get("auth_mode") or "").replace('"', '\\"')
2229
+ if auth_mode:
2230
+ typer.echo(f'export MAINSEQUENCE_AUTH_MODE="{auth_mode}"')
2179
2231
  typer.echo(f'export MAINSEQUENCE_ACCESS_TOKEN="{access}"')
2180
- typer.echo(f'export MAINSEQUENCE_REFRESH_TOKEN="{refresh}"')
2232
+ if refresh:
2233
+ typer.echo(f'export MAINSEQUENCE_REFRESH_TOKEN="{refresh}"')
2181
2234
  if username:
2182
2235
  typer.echo(f'export MAINSEQUENCE_USERNAME="{username}"')
2183
2236
  return
@@ -2188,11 +2241,16 @@ def login(
2188
2241
  typer.echo("MAIN SEQUENCE")
2189
2242
  if res.get("username"):
2190
2243
  success(f"Signed in as {res['username']} (Backend: {res['backend']})")
2244
+ elif res.get("auth_mode") == "runtime_credential":
2245
+ success(f"Signed in with runtime credential (Backend: {res['backend']})")
2191
2246
  else:
2192
2247
  success(f"Signed in with JWT tokens (Backend: {res['backend']})")
2193
2248
  info(f"Projects base folder: {base}")
2194
2249
  auth_store_label = cfg.auth_persistence_label()
2195
- if res.get("persisted", True):
2250
+ if res.get("auth_mode") == "runtime_credential":
2251
+ info(f"Runtime credential access token is persisted in {auth_store_label}; no CLI JWT refresh token exists.")
2252
+ info("When the access token expires, CLI will re-exchange the runtime credential automatically.")
2253
+ elif res.get("persisted", True):
2196
2254
  info(f"Auth tokens are persisted in {auth_store_label} for subsequent CLI commands.")
2197
2255
  else:
2198
2256
  warn(f"Could not persist auth tokens in {auth_store_label}. Use --export for shell-based auth.")
@@ -2815,7 +2873,7 @@ def settings_set_base(path: str = typer.Argument(..., help="New projects base fo
2815
2873
 
2816
2874
  @settings.command("set-backend")
2817
2875
  def settings_set_backend(
2818
- url: str = typer.Argument(..., help="Backend base URL, e.g. https://api.main-sequence.app")
2876
+ url: str = typer.Argument(..., help="Backend base URL, e.g. https://api.main-sequence.io")
2819
2877
  ):
2820
2878
  """
2821
2879
  Set backend base URL used by CLI API calls.
@@ -2823,12 +2881,12 @@ def settings_set_backend(
2823
2881
  Parameters
2824
2882
  ----------
2825
2883
  url:
2826
- Backend base URL (for example `https://api.main-sequence.app`).
2884
+ Backend base URL (for example `https://api.main-sequence.io`).
2827
2885
 
2828
2886
  Examples
2829
2887
  --------
2830
2888
  ```bash
2831
- mainsequence settings set-backend https://api.main-sequence.app
2889
+ mainsequence settings set-backend https://api.main-sequence.io
2832
2890
  ```
2833
2891
  """
2834
2892
  out = cfg.set_backend_url(url)
@@ -2837,6 +2895,59 @@ def settings_set_backend(
2837
2895
  success(f"Backend URL set to: {out.get('backend_url')}")
2838
2896
 
2839
2897
 
2898
+ def _settings_reset_impl() -> dict:
2899
+ """
2900
+ Reset persistent CLI settings to standard defaults and clear session overrides.
2901
+ """
2902
+ standard_backend = cfg.normalize_backend_url(LOGIN_DEFAULT_BACKEND_URL)
2903
+ standard_base = cfg.normalize_mainsequence_path(cfg.DEFAULTS.get("mainsequence_path"))
2904
+ pathlib.Path(standard_base).mkdir(parents=True, exist_ok=True)
2905
+ out = cfg.set_config(
2906
+ {
2907
+ "backend_url": standard_backend,
2908
+ "mainsequence_path": standard_base,
2909
+ }
2910
+ )
2911
+ cfg.clear_session_overrides()
2912
+ return out
2913
+
2914
+
2915
+ @settings.command("reset")
2916
+ def settings_reset():
2917
+ """
2918
+ Reset CLI settings to standard defaults.
2919
+
2920
+ Resets backend URL to `https://api.main-sequence.io`, base folder to the
2921
+ default `~/mainsequence`, and clears current terminal session overrides.
2922
+
2923
+ Examples
2924
+ --------
2925
+ ```bash
2926
+ mainsequence settings reset
2927
+ ```
2928
+ """
2929
+ out = _settings_reset_impl()
2930
+ if _emit_json(out):
2931
+ return
2932
+ success("Settings reset to standard defaults.")
2933
+ info(f"Backend URL: {out.get('backend_url')}")
2934
+ info(f"Projects base folder: {out.get('mainsequence_path')}")
2935
+
2936
+
2937
+ @settings.command("refresh")
2938
+ def settings_refresh():
2939
+ """
2940
+ Alias for `settings reset`.
2941
+
2942
+ Examples
2943
+ --------
2944
+ ```bash
2945
+ mainsequence settings refresh
2946
+ ```
2947
+ """
2948
+ settings_reset()
2949
+
2950
+
2840
2951
  # ---------- sdk group ----------
2841
2952
 
2842
2953
 
@@ -62,7 +62,7 @@ KEYCHAIN_SERVICE = "MainSequenceCLI.auth"
62
62
  KEYCHAIN_ACCOUNT = "default"
63
63
 
64
64
  DEFAULTS = {
65
- "backend_url": os.environ.get("MAIN_SEQUENCE_BACKEND_URL", "https://api.main-sequence.app/"),
65
+ "backend_url": os.environ.get("MAIN_SEQUENCE_BACKEND_URL", "https://api.main-sequence.io/"),
66
66
  "mainsequence_path": str(pathlib.Path.home() / "mainsequence"),
67
67
  "version": 1,
68
68
  }
@@ -212,7 +212,7 @@ def set_backend_url(url: str) -> dict:
212
212
  Convenience helper to set backend_url in config.json.
213
213
 
214
214
  Args:
215
- url: Backend base URL (e.g. https://api.main-sequence.app)
215
+ url: Backend base URL (e.g. https://api.main-sequence.io)
216
216
 
217
217
  Returns:
218
218
  dict: updated config
@@ -350,12 +350,13 @@ def get_tokens() -> dict:
350
350
  """
351
351
  Return auth tokens from environment variables, with persistent-store fallback.
352
352
  """
353
+ runtime_mode = (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip().lower() == "runtime_credential"
353
354
  tokens = {
354
355
  "username": os.environ.get(ENV_USERNAME) or os.environ.get(LEGACY_ENV_USERNAME, ""),
355
356
  "access": os.environ.get(ENV_ACCESS) or os.environ.get(LEGACY_ENV_ACCESS, ""),
356
357
  "refresh": os.environ.get(ENV_REFRESH) or os.environ.get(LEGACY_ENV_REFRESH, ""),
357
358
  }
358
- if tokens["access"] and tokens["refresh"]:
359
+ if tokens["access"] and (tokens["refresh"] or runtime_mode):
359
360
  return tokens
360
361
 
361
362
  for secret in (_read_secure_tokens(), _read_local_tokens()):
@@ -366,7 +367,7 @@ def get_tokens() -> dict:
366
367
  "access": tokens["access"] or secret.get("access", ""),
367
368
  "refresh": tokens["refresh"] or secret.get("refresh", ""),
368
369
  }
369
- if tokens["access"] and tokens["refresh"]:
370
+ if tokens["access"] and (tokens["refresh"] or runtime_mode):
370
371
  break
371
372
  return tokens
372
373
 
@@ -86,6 +86,9 @@ def _default_auth_provider_kind() -> str | None:
86
86
  has_access = _env_has_value("MAINSEQUENCE_ACCESS_TOKEN")
87
87
  has_refresh = _env_has_value("MAINSEQUENCE_REFRESH_TOKEN")
88
88
 
89
+ if mode == "runtime_credential":
90
+ return "runtime_credential"
91
+
89
92
  if mode == "session_jwt":
90
93
  if has_access or has_refresh:
91
94
  return "session_jwt"
@@ -176,6 +179,114 @@ class SessionJWTAuthProvider(BaseAuthProvider):
176
179
  return None
177
180
 
178
181
 
182
+ @dataclass
183
+ class RuntimeCredentialAuthProvider(BaseAuthProvider):
184
+ credential_id: str | None = None
185
+ credential_secret: str | None = None
186
+ token_url: str = f"{API_ENDPOINT}/pods/runtime-credentials/token/"
187
+ token_type: str = "Bearer"
188
+ refresh_skew_seconds: int = 30
189
+ timeout: tuple[float, float] = DEFAULT_TIMEOUT
190
+ expires_at: float | None = None
191
+ _lock: threading.RLock = field(default_factory=threading.RLock, init=False, repr=False)
192
+
193
+ def __post_init__(self):
194
+ if self.credential_id is None:
195
+ self.credential_id = os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID")
196
+ if self.credential_secret is None:
197
+ self.credential_secret = os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET")
198
+
199
+ def _current_access_token(self) -> str | None:
200
+ return (os.getenv("MAINSEQUENCE_ACCESS_TOKEN") or "").strip() or None
201
+
202
+ def _needs_exchange(self) -> bool:
203
+ access_token = self._current_access_token()
204
+ if not access_token:
205
+ return True
206
+
207
+ if self.expires_at is not None:
208
+ return self.expires_at <= time.time() + self.refresh_skew_seconds
209
+
210
+ exp = _decode_jwt_exp(access_token)
211
+ if exp is None:
212
+ # Access-only JWT behavior: use opaque/uninspectable access until a 401 forces exchange.
213
+ return False
214
+
215
+ return exp <= int(time.time()) + self.refresh_skew_seconds
216
+
217
+ def _require_credentials(self) -> tuple[str, str]:
218
+ credential_id = (self.credential_id or "").strip()
219
+ credential_secret = (self.credential_secret or "").strip()
220
+ if not credential_id:
221
+ raise AuthError(
222
+ "MAINSEQUENCE_RUNTIME_CREDENTIAL_ID is required when "
223
+ "MAINSEQUENCE_AUTH_MODE=runtime_credential."
224
+ )
225
+ if not credential_secret:
226
+ raise AuthError(
227
+ "MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET is required when "
228
+ "MAINSEQUENCE_AUTH_MODE=runtime_credential."
229
+ )
230
+ return credential_id, credential_secret
231
+
232
+ def refresh(
233
+ self,
234
+ *,
235
+ force: bool = False,
236
+ session: requests.Session | None = None,
237
+ ) -> None:
238
+ _ = session
239
+ with self._lock:
240
+ if not force and not self._needs_exchange():
241
+ return
242
+
243
+ credential_id, credential_secret = self._require_credentials()
244
+ response = requests.post(
245
+ self.token_url,
246
+ json={
247
+ "credential_id": credential_id,
248
+ "credential_secret": credential_secret,
249
+ },
250
+ headers={"Content-Type": "application/json"},
251
+ timeout=self.timeout,
252
+ )
253
+ if response.status_code < 200 or response.status_code >= 300:
254
+ raise AuthError(
255
+ "Runtime credential exchange failed with status "
256
+ f"{response.status_code}."
257
+ )
258
+
259
+ data = response.json()
260
+ access = str(data.get("access") or "").strip()
261
+ if not access:
262
+ raise AuthError("Runtime credential exchange response did not include access token.")
263
+
264
+ token_type = str(data.get("token_type") or self.token_type or "Bearer").strip()
265
+ self.token_type = token_type or "Bearer"
266
+
267
+ expires_in_raw = data.get("expires_in")
268
+ try:
269
+ expires_in = int(expires_in_raw)
270
+ except (TypeError, ValueError):
271
+ expires_in = None
272
+ self.expires_at = time.time() + expires_in if expires_in and expires_in > 0 else None
273
+ os.environ["MAINSEQUENCE_ACCESS_TOKEN"] = access
274
+
275
+ def get_headers(self) -> CaseInsensitiveDict:
276
+ if self._needs_exchange():
277
+ self.refresh(force=False)
278
+
279
+ access_token = self._current_access_token()
280
+ if not access_token:
281
+ raise AuthError("MAINSEQUENCE_ACCESS_TOKEN is missing after runtime credential exchange.")
282
+
283
+ return CaseInsensitiveDict(
284
+ {
285
+ "Authorization": f"{self.token_type} {access_token}",
286
+ }
287
+ )
288
+
289
+
179
290
  @dataclass
180
291
  class JWTAuthProvider(BaseAuthProvider):
181
292
  access_token: str | None = None
@@ -310,6 +421,9 @@ def build_default_auth_provider() -> BaseAuthProvider:
310
421
  if provider_kind == "session_jwt":
311
422
  return SessionJWTAuthProvider()
312
423
 
424
+ if provider_kind == "runtime_credential":
425
+ return RuntimeCredentialAuthProvider()
426
+
313
427
  if provider_kind == "jwt":
314
428
  return JWTAuthProvider()
315
429
 
@@ -330,7 +444,9 @@ class AuthLoaders:
330
444
  def _provider(self) -> BaseAuthProvider:
331
445
  provider_kind = _default_auth_provider_kind()
332
446
 
333
- if provider_kind == "session_jwt" and not isinstance(self.provider, SessionJWTAuthProvider):
447
+ if provider_kind == "runtime_credential" and not isinstance(self.provider, RuntimeCredentialAuthProvider):
448
+ self.provider = RuntimeCredentialAuthProvider()
449
+ elif provider_kind == "session_jwt" and not isinstance(self.provider, SessionJWTAuthProvider):
334
450
  self.provider = SessionJWTAuthProvider()
335
451
  elif provider_kind == "jwt" and not isinstance(self.provider, JWTAuthProvider):
336
452
  self.provider = JWTAuthProvider()
@@ -1,27 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import inspect
3
4
  import logging
4
5
  import logging.config
5
6
  import os
7
+ import sys
8
+ import traceback
9
+ from collections.abc import Mapping
6
10
  from pathlib import Path
11
+ from typing import Any
7
12
 
8
13
  import requests
9
14
  import structlog
10
15
  from requests.structures import CaseInsensitiveDict
16
+ from structlog.contextvars import bind_contextvars, unbind_contextvars
11
17
  from structlog.dev import ConsoleRenderer
18
+ from structlog.stdlib import BoundLogger
12
19
 
13
20
  from .instrumentation import OTelJSONRenderer
14
21
  from .runtime_flags import is_running_in_pod
15
22
 
16
23
  logger = None
17
- import inspect
18
- import sys
19
- import traceback
20
- from collections.abc import Mapping
21
- from typing import Any
22
-
23
- from structlog.contextvars import bind_contextvars, unbind_contextvars
24
- from structlog.stdlib import BoundLogger
25
24
 
26
25
 
27
26
  def ensure_dir(file_path):
@@ -85,7 +84,44 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
85
84
 
86
85
  return headers, False
87
86
 
87
+ def _exchange_runtime_credential() -> bool:
88
+ credential_id = (os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID") or "").strip()
89
+ credential_secret = (os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET") or "").strip()
90
+ if not credential_id or not credential_secret:
91
+ return False
92
+
93
+ try:
94
+ token_resp = requests.post(
95
+ f"{_backend_base_url()}/orm/api/pods/runtime-credentials/token/",
96
+ headers={"Content-Type": "application/json"},
97
+ json={
98
+ "credential_id": credential_id,
99
+ "credential_secret": credential_secret,
100
+ },
101
+ timeout=timeout_s,
102
+ )
103
+ except Exception:
104
+ return False
105
+
106
+ if token_resp.status_code < 200 or token_resp.status_code >= 300:
107
+ return False
108
+
109
+ try:
110
+ data = token_resp.json()
111
+ except Exception:
112
+ return False
113
+
114
+ access_token = str(data.get("access") or "").strip()
115
+ if not access_token:
116
+ return False
117
+
118
+ os.environ["MAINSEQUENCE_ACCESS_TOKEN"] = access_token
119
+ return True
120
+
88
121
  def _refresh_access_token() -> bool:
122
+ if auth_mode == "runtime_credential":
123
+ return _exchange_runtime_credential()
124
+
89
125
  if auth_mode == "session_jwt":
90
126
  return False
91
127
 
@@ -127,12 +163,15 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
127
163
  )
128
164
 
129
165
  if (
130
- auth_mode != "session_jwt"
166
+ auth_mode == "jwt"
131
167
  and not os.getenv("MAINSEQUENCE_ACCESS_TOKEN")
132
168
  and os.getenv("MAINSEQUENCE_REFRESH_TOKEN")
133
169
  ):
134
170
  _refresh_access_token()
135
171
 
172
+ if auth_mode == "runtime_credential" and not os.getenv("MAINSEQUENCE_ACCESS_TOKEN"):
173
+ _refresh_access_token()
174
+
136
175
  headers, using_jwt = _auth_headers()
137
176
 
138
177
  job_run_id = (os.getenv("JOB_RUN_ID") or "").strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 3.18.2
3
+ Version: 3.18.4
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "3.18.2"
7
+ version = "3.18.4"
8
8
  description = "Main Sequence SDK "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"