mainsequence 9.0.21.dev64__tar.gz → 9.0.22.dev65__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 (106) hide show
  1. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/PKG-INFO +1 -1
  2. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/api.py +2 -2
  3. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/agent_runtime_models.py +1 -1
  4. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/utils.py +4 -4
  5. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/logconf.py +38 -10
  6. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/PKG-INFO +1 -1
  7. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/SOURCES.txt +1 -1
  8. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/pyproject.toml +1 -1
  9. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_auth_precedence.py +13 -5
  10. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_cli_request_budget.py +1 -1
  11. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_code_repository_skills.py +1 -1
  12. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_http_retry_budget.py +1 -1
  13. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_logconf.py +120 -5
  14. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_runtime_credential_exchange.py +4 -4
  15. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/LICENSE +0 -0
  16. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/README.md +0 -0
  17. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/AGENTS.md +0 -0
  18. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  19. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  20. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
  21. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  22. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
  23. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
  24. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  25. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  26. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
  27. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/__init__.py +0 -0
  28. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/__main__.py +0 -0
  29. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/_request_identity.py +0 -0
  30. {mainsequence-9.0.21.dev64/mainsequence/client → mainsequence-9.0.22.dev65/mainsequence}/_transport.py +0 -0
  31. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/bootstrap.py +0 -0
  32. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/__init__.py +0 -0
  33. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/browser_auth.py +0 -0
  34. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/cli.py +0 -0
  35. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/code_repository_status.py +0 -0
  36. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/config.py +0 -0
  37. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/docker_utils.py +0 -0
  38. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/doctor.py +0 -0
  39. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/local_ops.py +0 -0
  40. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/model_filters.py +0 -0
  41. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/pydantic_cli.py +0 -0
  42. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/sdk_utils.py +0 -0
  43. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/ssh_utils.py +0 -0
  44. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/cli/ui.py +0 -0
  45. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/__init__.py +0 -0
  46. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/base.py +0 -0
  47. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/client.py +0 -0
  48. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/compute_validation.py +0 -0
  49. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/exceptions.py +0 -0
  50. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/github_issues.py +0 -0
  51. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/inference.py +0 -0
  52. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/models_foundry.py +0 -0
  53. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/models_helpers.py +0 -0
  54. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/models_user.py +0 -0
  55. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/observability.py +0 -0
  56. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/client/value_sets.py +0 -0
  57. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/code_repository_context.py +0 -0
  58. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/code_repository_skills.py +0 -0
  59. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/defaults.py +0 -0
  60. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/instrumentation/__init__.py +0 -0
  61. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/instrumentation/utils.py +0 -0
  62. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/repository_identity_security.py +0 -0
  63. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/runtime_credential_exchange.py +0 -0
  64. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/runtime_flags.py +0 -0
  65. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/scaffold_skills.py +0 -0
  66. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/server/__init__.py +0 -0
  67. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/server/caller_assertions.py +0 -0
  68. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence/server/fastapi.py +0 -0
  69. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/dependency_links.txt +0 -0
  70. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/entry_points.txt +0 -0
  71. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/requires.txt +0 -0
  72. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/top_level.txt +0 -0
  73. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/setup.cfg +0 -0
  74. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_auth_bootstrap.py +0 -0
  75. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_caller_assertions.py +0 -0
  76. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_canonical_api_route_contract.py +0 -0
  77. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_cli_auth_contract.py +0 -0
  78. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_cli_browser_auth.py +0 -0
  79. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_cli_secure_storage.py +0 -0
  80. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_client.py +0 -0
  81. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_code_repository_branch_contract.py +0 -0
  82. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_code_repository_documentation_contract.py +0 -0
  83. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_code_repository_runtime_context.py +0 -0
  84. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_credential_repr.py +0 -0
  85. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_dependency_extras.py +0 -0
  86. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_dev_release_version.py +0 -0
  87. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_environment_resource_context.py +0 -0
  88. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_environment_response_contracts.py +0 -0
  89. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_fastapi_mcp.py +0 -0
  90. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_fastapi_request_identity.py +0 -0
  91. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_github_issues_client.py +0 -0
  92. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_independent_context.py +0 -0
  93. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_inference_client.py +0 -0
  94. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_instrumentation.py +0 -0
  95. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_job_client.py +0 -0
  96. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_local_ops.py +0 -0
  97. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_models_user_request_bound_auth.py +0 -0
  98. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_notification_uid_contract.py +0 -0
  99. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_observability_client.py +0 -0
  100. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_scaffold_skills.py +0 -0
  101. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_secret_client_model.py +0 -0
  102. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_source_ownership.py +0 -0
  103. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_ssh_utils.py +0 -0
  104. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_tolerant_response_reading.py +0 -0
  105. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_tolerant_value_sets.py +0 -0
  106. {mainsequence-9.0.21.dev64 → mainsequence-9.0.22.dev65}/tests/test_user_application_facts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.21.dev64
3
+ Version: 9.0.22.dev65
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -76,7 +76,7 @@ class _CliSession(requests.Session):
76
76
  if kwargs.get("timeout") is not None:
77
77
  return super().request(method, url, **kwargs)
78
78
  # The CLI imports the SDK client on its first request, not when it starts.
79
- from mainsequence.client._transport import DEFAULT_TIMEOUT, request_budget
79
+ from mainsequence._transport import DEFAULT_TIMEOUT, request_budget
80
80
 
81
81
  with request_budget(DEFAULT_TIMEOUT) as budget:
82
82
  kwargs["timeout"] = budget.timeout()
@@ -514,7 +514,7 @@ def _mcp_json_rpc(
514
514
  default request budget: 5 s to connect, 120 s to read, 125 s in total.
515
515
  """
516
516
  # The CLI imports the SDK client only where it needs it.
517
- from mainsequence.client._transport import DEFAULT_TIMEOUT, request_budget
517
+ from mainsequence._transport import DEFAULT_TIMEOUT, request_budget
518
518
 
519
519
  payload = {
520
520
  "jsonrpc": "2.0",
@@ -12,7 +12,7 @@ from typing import Any, ClassVar, Literal
12
12
  import requests
13
13
  from pydantic import ConfigDict, Field, model_validator
14
14
 
15
- from ._transport import request_budget, request_within_budget
15
+ from .._transport import request_budget, request_within_budget
16
16
  from .base import (
17
17
  BaseObjectOrm,
18
18
  BasePydanticModel,
@@ -28,10 +28,10 @@ from mainsequence.runtime_credential_exchange import (
28
28
  identity_token_file_from_environment,
29
29
  )
30
30
 
31
- from ._transport import DEFAULT_ALLOWED_METHODS as DEFAULT_ALLOWED_METHODS
32
- from ._transport import DEFAULT_STATUS_FORCELIST as DEFAULT_STATUS_FORCELIST
33
- from ._transport import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
34
- from ._transport import DeadlineHTTPAdapter, remaining_timeout, request_budget
31
+ from .._transport import DEFAULT_ALLOWED_METHODS as DEFAULT_ALLOWED_METHODS
32
+ from .._transport import DEFAULT_STATUS_FORCELIST as DEFAULT_STATUS_FORCELIST
33
+ from .._transport import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
34
+ from .._transport import DeadlineHTTPAdapter, remaining_timeout, request_budget
35
35
 
36
36
  # ---- Backend defaults (single source of truth) ----
37
37
  MAINSEQUENCE_ENDPOINT = resolve_backend_endpoint()
@@ -5,7 +5,6 @@ import logging
5
5
  import logging.config
6
6
  import os
7
7
  import sys
8
- import time
9
8
  import traceback
10
9
  from collections.abc import Mapping
11
10
  from importlib.metadata import PackageNotFoundError
@@ -20,6 +19,7 @@ from structlog.contextvars import bind_contextvars, unbind_contextvars
20
19
  from structlog.dev import ConsoleRenderer
21
20
  from structlog.stdlib import BoundLogger
22
21
 
22
+ from ._transport import DeadlineHTTPAdapter, request_budget
23
23
  from .defaults import resolve_backend_endpoint
24
24
  from .instrumentation import OTelJSONRenderer
25
25
  from .repository_identity_security import UNSUPPORTED_SOURCE_IDENTITY_ENV_NAMES
@@ -76,14 +76,27 @@ class CustomConsoleRenderer(ConsoleRenderer):
76
76
  return rendered
77
77
 
78
78
 
79
- def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
79
+ def _request_job_startup_state(*, timeout_s: float | None = None) -> dict[str, Any]:
80
80
  """
81
81
  Fetch startup state from backend using current env vars (JWT auth, endpoint, job_run_uid).
82
82
  Safe to call later after auth when access/refresh tokens become available.
83
+
84
+ The whole read, token renewal included, ends within one request budget:
85
+ ``timeout_s`` seconds in total, or the SDK's default request budget when it is
86
+ None. The GET is retried like any other SDK read. The startup state only
87
+ enriches the logger, so when the backend cannot be reached or does not answer
88
+ in time this reports it on stderr and returns an empty state instead of
89
+ failing the import.
83
90
  """
84
91
  if not is_running_in_pod():
85
92
  return {}
86
93
 
94
+ with request_budget(timeout_s) as budget:
95
+ return _read_job_startup_state(budget)
96
+
97
+
98
+ def _read_job_startup_state(budget) -> dict[str, Any]:
99
+
87
100
  auth_mode = (os.getenv("MAINSEQUENCE_AUTH_MODE") or "jwt").strip().lower()
88
101
 
89
102
  def _backend_base_url() -> str:
@@ -110,7 +123,7 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
110
123
  token_url,
111
124
  headers={"Content-Type": "application/json"},
112
125
  json=body,
113
- timeout=timeout_s,
126
+ timeout=budget.timeout(),
114
127
  allow_redirects=False,
115
128
  )
116
129
 
@@ -119,7 +132,7 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
119
132
  _post,
120
133
  credential_id=(os.getenv(RUNTIME_CREDENTIAL_ID_ENV) or "").strip(),
121
134
  identity_token_file=identity_token_file_from_environment(),
122
- deadline=time.monotonic() + timeout_s,
135
+ deadline=budget.deadline,
123
136
  )
124
137
  except Exception:
125
138
  return False
@@ -155,7 +168,7 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
155
168
  f"{_backend_base_url()}/auth/jwt-token/token/refresh/",
156
169
  headers={"Content-Type": "application/json"},
157
170
  json={"refresh": refresh_token},
158
- timeout=timeout_s,
171
+ timeout=budget.timeout(),
159
172
  )
160
173
  except Exception:
161
174
  return False
@@ -202,10 +215,25 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
202
215
  endpoint = f"{_backend_base_url()}/api/v1/job-runs/{job_run_uid}/startup-state/"
203
216
  params: dict[str, Any] = {}
204
217
 
205
- resp = requests.get(endpoint, headers=headers, params=params, timeout=timeout_s)
206
- if resp.status_code in {401, 403} and using_jwt and _refresh_access_token():
207
- headers, _ = _auth_headers()
208
- resp = requests.get(endpoint, headers=headers, params=params, timeout=timeout_s)
218
+ try:
219
+ with requests.Session() as session:
220
+ adapter = DeadlineHTTPAdapter(retries=3, backoff_factor=0.5)
221
+ session.mount("https://", adapter)
222
+ session.mount("http://", adapter)
223
+ resp = session.get(endpoint, headers=headers, params=params, timeout=budget.timeout())
224
+ if resp.status_code in {401, 403} and using_jwt and _refresh_access_token():
225
+ headers, _ = _auth_headers()
226
+ resp = session.get(
227
+ endpoint, headers=headers, params=params, timeout=budget.timeout()
228
+ )
229
+ except requests.RequestException as exc:
230
+ # The exception text is not echoed: it can carry the request URL and headers.
231
+ print(
232
+ "mainsequence: the Job startup state could not be read "
233
+ f"({type(exc).__name__}); continuing without it.",
234
+ file=sys.stderr,
235
+ )
236
+ return {}
209
237
 
210
238
  if resp.status_code != 200:
211
239
  # Do not spam stdout during CLI usage when auth context is not present yet.
@@ -426,7 +454,7 @@ def build_application_logger(application_name: str = "ms-sdk", **metadata):
426
454
  return logger
427
455
 
428
456
 
429
- def refresh_application_logger_bindings(*, timeout_s: float = 10.0) -> BoundLogger:
457
+ def refresh_application_logger_bindings(*, timeout_s: float | None = None) -> BoundLogger:
430
458
  """
431
459
  Importable helper: call this AFTER the user authenticates / token exists.
432
460
  It re-fetches backend state and binds the same keys as build_application_logger().
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.21.dev64
3
+ Version: 9.0.22.dev65
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -14,6 +14,7 @@ agent_scaffold/skills/sdk_code_repository_execution/SKILL.md
14
14
  mainsequence/__init__.py
15
15
  mainsequence/__main__.py
16
16
  mainsequence/_request_identity.py
17
+ mainsequence/_transport.py
17
18
  mainsequence/bootstrap.py
18
19
  mainsequence/code_repository_context.py
19
20
  mainsequence/code_repository_skills.py
@@ -44,7 +45,6 @@ mainsequence/cli/sdk_utils.py
44
45
  mainsequence/cli/ssh_utils.py
45
46
  mainsequence/cli/ui.py
46
47
  mainsequence/client/__init__.py
47
- mainsequence/client/_transport.py
48
48
  mainsequence/client/agent_runtime_models.py
49
49
  mainsequence/client/base.py
50
50
  mainsequence/client/client.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "9.0.21.dev64"
7
+ version = "9.0.22.dev65"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -22,6 +22,12 @@ _UNSUPPORTED_ENVIRONMENT_UID_ENV = (
22
22
  )
23
23
 
24
24
 
25
+ def _stub_startup_state_get(monkeypatch, fake_get):
26
+ monkeypatch.setattr(
27
+ requests.Session, "get", lambda _session, url, **kwargs: fake_get(url, **kwargs)
28
+ )
29
+
30
+
25
31
  class _FakeResponse:
26
32
  def __init__(self, status_code: int, payload: dict | None = None):
27
33
  self.status_code = status_code
@@ -107,7 +113,7 @@ def test_logconf_refreshes_jwt_before_startup_state_request(monkeypatch):
107
113
  post_calls.append({"url": url, **kwargs})
108
114
  return _FakeResponse(200, {"access": "jwt-access"})
109
115
 
110
- monkeypatch.setattr(requests, "get", _fake_get)
116
+ _stub_startup_state_get(monkeypatch, _fake_get)
111
117
  monkeypatch.setattr(requests, "post", _fake_post)
112
118
 
113
119
  _load_mainsequence_submodule("mainsequence.logconf")
@@ -143,11 +149,14 @@ def test_logconf_runtime_credential_exchanges_before_startup_state_request(monke
143
149
  },
144
150
  )
145
151
 
146
- monkeypatch.setattr(requests, "get", _fake_get)
152
+ _stub_startup_state_get(monkeypatch, _fake_get)
147
153
  monkeypatch.setattr(requests, "post", _fake_post)
148
154
 
149
155
  _load_mainsequence_submodule("mainsequence.logconf")
150
156
 
157
+ # The exchange receives what is left of the startup-state read's request budget.
158
+ timeout = post_calls[0].pop("timeout")
159
+ assert 0 < timeout.total <= 125.0
151
160
  assert post_calls == [
152
161
  {
153
162
  "url": "https://backend.example/api/v1/runtime-credentials/token/",
@@ -156,7 +165,6 @@ def test_logconf_runtime_credential_exchanges_before_startup_state_request(monke
156
165
  "credential_id": "cred-id",
157
166
  "workload_identity_token": "projected-token",
158
167
  },
159
- "timeout": 10.0,
160
168
  "allow_redirects": False,
161
169
  }
162
170
  ]
@@ -192,7 +200,7 @@ def test_logconf_runtime_credential_retries_after_auth_failure(monkeypatch, tmp_
192
200
  },
193
201
  )
194
202
 
195
- monkeypatch.setattr(requests, "get", _fake_get)
203
+ _stub_startup_state_get(monkeypatch, _fake_get)
196
204
  monkeypatch.setattr(requests, "post", _fake_post)
197
205
 
198
206
  _load_mainsequence_submodule("mainsequence.logconf")
@@ -594,7 +602,7 @@ def test_logconf_session_jwt_does_not_refresh(monkeypatch):
594
602
  post_calls.append({"url": url, **kwargs})
595
603
  return _FakeResponse(200, {"access": "unexpected"})
596
604
 
597
- monkeypatch.setattr(requests, "get", _fake_get)
605
+ _stub_startup_state_get(monkeypatch, _fake_get)
598
606
  monkeypatch.setattr(requests, "post", _fake_post)
599
607
 
600
608
  _load_mainsequence_submodule("mainsequence.logconf")
@@ -15,9 +15,9 @@ import pytest
15
15
  import requests
16
16
  from typer.testing import CliRunner
17
17
 
18
+ from mainsequence import _transport as transport
18
19
  from mainsequence.cli import api as cli_api
19
20
  from mainsequence.cli import cli as cli_mod
20
- from mainsequence.client import _transport as transport
21
21
  from tests._http_server import answer_json, answer_sequence, never_answer, serve
22
22
 
23
23
  runner = CliRunner()
@@ -8,9 +8,9 @@ from pathlib import Path
8
8
 
9
9
  import pytest
10
10
 
11
+ from mainsequence import _transport as transport
11
12
  from mainsequence.cli import api as cli_api
12
13
  from mainsequence.cli.api import ApiError
13
- from mainsequence.client import _transport as transport
14
14
  from mainsequence.code_repository_skills import (
15
15
  PLATFORM_ONTOLOGY_URI,
16
16
  PLATFORM_SKILL_URI_PREFIX,
@@ -7,7 +7,7 @@ from unittest.mock import Mock, PropertyMock
7
7
  import pytest
8
8
  import requests
9
9
 
10
- from mainsequence.client import _transport as transport
10
+ from mainsequence import _transport as transport
11
11
  from mainsequence.client import utils
12
12
  from mainsequence.client.client import MainSequenceClient, MainSequenceClientConfig
13
13
  from mainsequence.client.exceptions import (
@@ -1,10 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import sys
5
+ import time
4
6
 
5
7
  import pytest
6
8
  import requests
9
+ from urllib3.util import Timeout
7
10
 
11
+ from tests._http_server import answer_after, answer_json, answer_sequence, never_answer, serve
8
12
  from tests._support import RETIRED_RUNTIME_CONTEXT_ENV_NAMES as _RUNTIME_CONTEXT_ENV_NAMES
9
13
  from tests._support import load_sdk_submodule as _load_mainsequence_submodule
10
14
 
@@ -40,9 +44,9 @@ def test_logconf_import_skips_job_startup_state_request_outside_pod(monkeypatch)
40
44
 
41
45
  def _fake_get(*args, **kwargs):
42
46
  calls.append((args, kwargs))
43
- raise AssertionError("requests.get should not be called outside pod runtime")
47
+ raise AssertionError("the startup state should not be read outside pod runtime")
44
48
 
45
- monkeypatch.setattr(requests, "get", _fake_get)
49
+ monkeypatch.setattr(requests.Session, "get", lambda _s, *a, **kw: _fake_get(*a, **kw))
46
50
 
47
51
  logconf = _load_mainsequence_submodule("mainsequence.logconf")
48
52
 
@@ -92,9 +96,9 @@ def test_logconf_import_skips_job_startup_state_request_without_job_run_uid(monk
92
96
 
93
97
  def _fake_get(*args, **kwargs):
94
98
  calls.append((args, kwargs))
95
- raise AssertionError("requests.get should not be called without JOB_RUN_UID")
99
+ raise AssertionError("the startup state should not be read without JOB_RUN_UID")
96
100
 
97
- monkeypatch.setattr(requests, "get", _fake_get)
101
+ monkeypatch.setattr(requests.Session, "get", lambda _s, *a, **kw: _fake_get(*a, **kw))
98
102
 
99
103
  logconf = _load_mainsequence_submodule("mainsequence.logconf")
100
104
 
@@ -132,7 +136,7 @@ def test_logconf_import_requests_job_run_detail_startup_state(monkeypatch):
132
136
  )
133
137
  return _FakeResponse()
134
138
 
135
- monkeypatch.setattr(requests, "get", _fake_get)
139
+ monkeypatch.setattr(requests.Session, "get", lambda _s, *a, **kw: _fake_get(*a, **kw))
136
140
 
137
141
  logconf = _load_mainsequence_submodule("mainsequence.logconf")
138
142
 
@@ -147,3 +151,114 @@ def test_logconf_import_requests_job_run_detail_startup_state(monkeypatch):
147
151
  assert bindings["job_run_uid"] == "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
148
152
  assert "project_id" not in bindings
149
153
  assert "data_source_id" not in bindings
154
+
155
+
156
+ # The Job startup-state read (#215) crosses the real HTTP boundary with a local
157
+ # server. A short stand-in for the SDK's default request budget keeps it fast.
158
+
159
+ JOB_RUN_UID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
160
+ STARTUP_STATE_PATH = f"/api/v1/job-runs/{JOB_RUN_UID}/startup-state/"
161
+ REFRESH_PATH = "/auth/jwt-token/token/refresh/"
162
+
163
+
164
+ @pytest.fixture
165
+ def job_logconf(monkeypatch):
166
+ """Load logconf outside a Job, then return it with the Job's environment set."""
167
+ for name in ("MAINSEQUENCE_ACCESS_TOKEN", "MAINSEQUENCE_REFRESH_TOKEN", "JOB_RUN_UID"):
168
+ monkeypatch.delenv(name, raising=False)
169
+ logconf = _load_mainsequence_submodule("mainsequence.logconf")
170
+ transport = sys.modules["mainsequence._transport"]
171
+
172
+ def start(base_url, *, auth_mode="session_jwt", default_timeout=(0.2, 0.3)):
173
+ monkeypatch.setattr(transport, "DEFAULT_TIMEOUT", default_timeout)
174
+ monkeypatch.setenv("MAINSEQUENCE_ENDPOINT", base_url.rstrip("/"))
175
+ monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", auth_mode)
176
+ monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "secret-access-token")
177
+ monkeypatch.setenv("JOB_RUN_UID", JOB_RUN_UID)
178
+ return logconf
179
+
180
+ return start
181
+
182
+
183
+ def _startup_gets(calls):
184
+ return [call for call in calls if call[:2] == ("GET", STARTUP_STATE_PATH)]
185
+
186
+
187
+ def test_logger_builds_when_the_startup_state_read_times_out(job_logconf, capsys):
188
+ with serve({("GET", STARTUP_STATE_PATH): never_answer()}) as (url, calls):
189
+ logconf = job_logconf(url)
190
+ started = time.monotonic()
191
+ logger = logconf.build_application_logger()
192
+ elapsed = time.monotonic() - started
193
+
194
+ assert elapsed < 0.5 + 0.3
195
+ assert len(_startup_gets(calls)) == 1
196
+ bound = logconf.dump_structlog_bound_logger(logger)["bound_context"]
197
+ assert "job_run_uid" not in bound
198
+ err = capsys.readouterr().err
199
+ assert "the Job startup state could not be read (ReadTimeout)" in err
200
+ assert "secret-access-token" not in err
201
+
202
+
203
+ def test_logger_builds_when_the_backend_cannot_be_reached(job_logconf, capsys):
204
+ with serve({}) as (url, _calls):
205
+ pass # the port is closed again once the server stops
206
+ logconf = job_logconf(url)
207
+
208
+ logger = logconf.build_application_logger()
209
+
210
+ assert logger is not None
211
+ assert "could not be read (ConnectionError)" in capsys.readouterr().err
212
+
213
+
214
+ def test_startup_state_read_retries_a_failed_safe_read_within_its_budget(job_logconf):
215
+ state = {"job_run_uid": JOB_RUN_UID, "command_id": 12}
216
+ route = answer_sequence(
217
+ answer_after(1.0, 200, {}), answer_json(503, {}), answer_json(200, state)
218
+ )
219
+ with serve({("GET", STARTUP_STATE_PATH): route}) as (url, calls):
220
+ logconf = job_logconf(url, default_timeout=Timeout(connect=0.3, read=0.3, total=3.0))
221
+ logger = logconf.build_application_logger()
222
+
223
+ assert len(_startup_gets(calls)) == 3
224
+ bound = logconf.dump_structlog_bound_logger(logger)["bound_context"]
225
+ assert bound["job_run_uid"] == JOB_RUN_UID
226
+ assert bound["command_id"] == 12
227
+
228
+
229
+ def test_denied_startup_state_is_not_treated_as_granted(job_logconf, capsys):
230
+ route = answer_json(403, {"detail": "denied"})
231
+ with serve({("GET", STARTUP_STATE_PATH): route}) as (url, calls):
232
+ logconf = job_logconf(url)
233
+ assert logconf._request_job_startup_state() == {}
234
+
235
+ assert len(_startup_gets(calls)) == 1
236
+ assert capsys.readouterr().err == ""
237
+
238
+
239
+ def test_failed_token_renewal_after_a_denial_shares_the_budget(job_logconf, monkeypatch):
240
+ routes = {
241
+ ("GET", STARTUP_STATE_PATH): answer_json(401, {}),
242
+ ("POST", REFRESH_PATH): never_answer(),
243
+ }
244
+ with serve(routes) as (url, calls):
245
+ logconf = job_logconf(url, auth_mode="jwt")
246
+ monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "secret-refresh-token")
247
+ started = time.monotonic()
248
+ assert logconf._request_job_startup_state() == {}
249
+ elapsed = time.monotonic() - started
250
+
251
+ assert elapsed < 0.5 + 0.3
252
+ assert [call[:2] for call in calls] == [("GET", STARTUP_STATE_PATH), ("POST", REFRESH_PATH)]
253
+
254
+
255
+ def test_refreshing_logger_bindings_honours_an_explicit_total_timeout(job_logconf, capsys):
256
+ with serve({("GET", STARTUP_STATE_PATH): never_answer()}) as (url, _calls):
257
+ logconf = job_logconf(url, default_timeout=(5.0, 120.0))
258
+ started = time.monotonic()
259
+ logger = logconf.refresh_application_logger_bindings(timeout_s=0.3)
260
+ elapsed = time.monotonic() - started
261
+
262
+ assert logger is not None
263
+ assert elapsed < 0.3 + 0.3
264
+ assert "could not be read (ReadTimeout)" in capsys.readouterr().err
@@ -138,7 +138,7 @@ def test_no_code_path_reads_the_secret_variable(monkeypatch, runtime):
138
138
  # os.getenv() reads os.environ, so every lookup by name goes through the recorder.
139
139
  monkeypatch.setattr(os, "environ", environ)
140
140
  exchange = _Exchange(monkeypatch, _granted(), _granted())
141
- monkeypatch.setattr(requests, "get", lambda url, **kwargs: _Answer(200, {}))
141
+ monkeypatch.setattr(requests.Session, "get", lambda _session, url, **kwargs: _Answer(200, {}))
142
142
 
143
143
  # Importing the client configures the logger, whose start-up exchange runs first.
144
144
  utils = load_sdk_submodule("mainsequence.client.utils")
@@ -306,7 +306,7 @@ def test_exchange_retries_are_bounded(monkeypatch, runtime):
306
306
  def test_each_attempt_receives_only_the_remaining_budget(monkeypatch, runtime):
307
307
  _use_token_file(monkeypatch, runtime)
308
308
  utils = load_sdk_submodule("mainsequence.client.utils")
309
- transport = importlib.import_module("mainsequence.client._transport")
309
+ transport = importlib.import_module("mainsequence._transport")
310
310
  exchange = _Exchange(monkeypatch, _Answer(503, headers={"Retry-After": "4"}), _granted())
311
311
  now = [100.0]
312
312
 
@@ -434,7 +434,7 @@ def test_logger_start_up_exchange_sends_the_identity_token_and_retries(monkeypat
434
434
  authorizations.append(kwargs["headers"].get("Authorization"))
435
435
  return _Answer(200, {"job_run_uid": JOB_RUN_UID})
436
436
 
437
- monkeypatch.setattr(requests, "get", _get)
437
+ monkeypatch.setattr(requests.Session, "get", lambda _session, url, **kw: _get(url, **kw))
438
438
 
439
439
  load_sdk_submodule("mainsequence.logconf")
440
440
 
@@ -452,7 +452,7 @@ def test_logger_start_up_exchange_needs_the_identity_token_file(monkeypatch, run
452
452
  monkeypatch.setenv(SECRET_ENV, SECRET)
453
453
  monkeypatch.setenv("JOB_RUN_UID", JOB_RUN_UID)
454
454
  exchange = _Exchange(monkeypatch)
455
- monkeypatch.setattr(requests, "get", lambda url, **kwargs: _Answer(401))
455
+ monkeypatch.setattr(requests.Session, "get", lambda _session, url, **kwargs: _Answer(401))
456
456
 
457
457
  load_sdk_submodule("mainsequence.logconf")
458
458