mainsequence 9.0.21.dev63__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.
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/PKG-INFO +1 -1
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/api.py +22 -2
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/cli.py +4 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/agent_runtime_models.py +1 -1
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/utils.py +4 -4
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/logconf.py +38 -10
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/SOURCES.txt +2 -1
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/pyproject.toml +1 -1
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_auth_precedence.py +13 -5
- mainsequence-9.0.22.dev65/tests/test_cli_request_budget.py +232 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_code_repository_skills.py +1 -1
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_http_retry_budget.py +1 -1
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_logconf.py +120 -5
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_runtime_credential_exchange.py +4 -4
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/LICENSE +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/README.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.21.dev63/mainsequence/client → mainsequence-9.0.22.dev65/mainsequence}/_transport.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/config.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/__init__.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/client.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/inference.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/models_user.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/code_repository_context.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/runtime_credential_exchange.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/server/__init__.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/server/caller_assertions.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/server/fastapi.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/setup.cfg +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_auth_bootstrap.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_caller_assertions.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_cli_secure_storage.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_client.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_code_repository_runtime_context.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_credential_repr.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_dependency_extras.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_fastapi_mcp.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_fastapi_request_identity.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_inference_client.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_user_application_facts.py +0 -0
|
@@ -63,7 +63,27 @@ MCP_PROTOCOL_VERSION = "2025-11-25"
|
|
|
63
63
|
MCP_CLIENT_NAME = "mainsequence-cli"
|
|
64
64
|
MCP_MAX_RESOURCE_PAGES = 100
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
|
|
67
|
+
class _CliSession(requests.Session):
|
|
68
|
+
"""The CLI's HTTP session.
|
|
69
|
+
|
|
70
|
+
A request sent without its own timeout gets the SDK's default request
|
|
71
|
+
budget: 5 s to connect, 120 s to read and 125 s in total, inside any budget
|
|
72
|
+
that is already running.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def request(self, method, url, **kwargs):
|
|
76
|
+
if kwargs.get("timeout") is not None:
|
|
77
|
+
return super().request(method, url, **kwargs)
|
|
78
|
+
# The CLI imports the SDK client on its first request, not when it starts.
|
|
79
|
+
from mainsequence._transport import DEFAULT_TIMEOUT, request_budget
|
|
80
|
+
|
|
81
|
+
with request_budget(DEFAULT_TIMEOUT) as budget:
|
|
82
|
+
kwargs["timeout"] = budget.timeout()
|
|
83
|
+
return super().request(method, url, **kwargs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
S = _CliSession()
|
|
67
87
|
S.headers.update({"Content-Type": "application/json"})
|
|
68
88
|
|
|
69
89
|
|
|
@@ -494,7 +514,7 @@ def _mcp_json_rpc(
|
|
|
494
514
|
default request budget: 5 s to connect, 120 s to read, 125 s in total.
|
|
495
515
|
"""
|
|
496
516
|
# The CLI imports the SDK client only where it needs it.
|
|
497
|
-
from mainsequence.
|
|
517
|
+
from mainsequence._transport import DEFAULT_TIMEOUT, request_budget
|
|
498
518
|
|
|
499
519
|
payload = {
|
|
500
520
|
"jsonrpc": "2.0",
|
|
@@ -1946,6 +1946,10 @@ def login(
|
|
|
1946
1946
|
except ApiError as e:
|
|
1947
1947
|
error(f"Login failed: {e}")
|
|
1948
1948
|
raise typer.Exit(1) from e
|
|
1949
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
|
1950
|
+
# Never echo the exception: a transport error can carry request details.
|
|
1951
|
+
error(f"The backend at {normalized_backend} could not be reached ({type(e).__name__}).")
|
|
1952
|
+
raise typer.Exit(1) from e
|
|
1949
1953
|
finally:
|
|
1950
1954
|
if previous_backend_override is None:
|
|
1951
1955
|
os.environ.pop("MAINSEQUENCE_ENDPOINT", None)
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/agent_runtime_models.py
RENAMED
|
@@ -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
|
|
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
|
|
32
|
-
from
|
|
33
|
-
from
|
|
34
|
-
from
|
|
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 =
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 =
|
|
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().
|
|
@@ -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
|
|
@@ -69,6 +69,7 @@ tests/test_caller_assertions.py
|
|
|
69
69
|
tests/test_canonical_api_route_contract.py
|
|
70
70
|
tests/test_cli_auth_contract.py
|
|
71
71
|
tests/test_cli_browser_auth.py
|
|
72
|
+
tests/test_cli_request_budget.py
|
|
72
73
|
tests/test_cli_secure_storage.py
|
|
73
74
|
tests/test_client.py
|
|
74
75
|
tests/test_code_repository_branch_contract.py
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Every CLI request ends within the SDK's default request budget.
|
|
2
|
+
|
|
3
|
+
The tests cross the real HTTP boundary with a local server. A short stand-in for
|
|
4
|
+
the default budget (5 s connect, 120 s read, 125 s total) keeps them fast: the
|
|
5
|
+
CLI reads the default when each request starts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
from contextlib import ExitStack
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
import requests
|
|
16
|
+
from typer.testing import CliRunner
|
|
17
|
+
|
|
18
|
+
from mainsequence import _transport as transport
|
|
19
|
+
from mainsequence.cli import api as cli_api
|
|
20
|
+
from mainsequence.cli import cli as cli_mod
|
|
21
|
+
from tests._http_server import answer_json, answer_sequence, never_answer, serve
|
|
22
|
+
|
|
23
|
+
runner = CliRunner()
|
|
24
|
+
|
|
25
|
+
TOKEN_PATH = "/auth/cli/token/"
|
|
26
|
+
HANDOFF_START_PATH = "/auth/mcp/cli-handoff/start/"
|
|
27
|
+
HANDOFF_CALLBACK_PATH = "/auth/mcp/cli-handoff/callback/"
|
|
28
|
+
LOGOUT_PATH = "/auth/jwt-token/logout/"
|
|
29
|
+
REVOKE_PATH = "/auth/cli/revoke/"
|
|
30
|
+
REFRESH_PATH = "/auth/jwt-token/token/refresh/"
|
|
31
|
+
ME_PATH = "/api/v1/users/me/"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def cli_backend(monkeypatch):
|
|
36
|
+
"""Serve routes as the CLI's backend, with a saved session; return (backend, calls)."""
|
|
37
|
+
monkeypatch.delenv("MAINSEQUENCE_AUTH_MODE", raising=False)
|
|
38
|
+
monkeypatch.setattr(cli_api, "_access_token", lambda: "saved-jwt")
|
|
39
|
+
monkeypatch.setattr(cli_api, "_refresh_token", lambda: "saved-refresh")
|
|
40
|
+
with ExitStack() as servers:
|
|
41
|
+
|
|
42
|
+
def start(routes):
|
|
43
|
+
url, calls = servers.enter_context(serve(routes))
|
|
44
|
+
backend = url.rstrip("/")
|
|
45
|
+
monkeypatch.setattr(cli_api, "backend_url", lambda: backend)
|
|
46
|
+
monkeypatch.setattr(cli_mod.cfg, "backend_url", lambda: backend)
|
|
47
|
+
return backend, calls
|
|
48
|
+
|
|
49
|
+
yield start
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def short_default_budget(monkeypatch):
|
|
54
|
+
monkeypatch.setattr(transport, "DEFAULT_TIMEOUT", (0.2, 0.3))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sent(calls):
|
|
58
|
+
return [(method, path) for method, path, _body in calls]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize(
|
|
62
|
+
("method", "path", "call"),
|
|
63
|
+
[
|
|
64
|
+
(
|
|
65
|
+
"POST",
|
|
66
|
+
TOKEN_PATH,
|
|
67
|
+
lambda backend: cli_api.exchange_cli_authorization_code(
|
|
68
|
+
code="code", code_verifier="verifier", redirect_uri="http://127.0.0.1/callback"
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"POST",
|
|
73
|
+
HANDOFF_START_PATH,
|
|
74
|
+
lambda backend: cli_api.start_mcp_cli_handoff(state="state", code_challenge="c"),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"POST",
|
|
78
|
+
HANDOFF_CALLBACK_PATH,
|
|
79
|
+
lambda backend: cli_api.poll_mcp_cli_handoff(
|
|
80
|
+
redirect_uri=f"{backend}{HANDOFF_CALLBACK_PATH}",
|
|
81
|
+
handoff_uid="handoff",
|
|
82
|
+
state="state",
|
|
83
|
+
code_verifier="verifier",
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
("POST", REFRESH_PATH, lambda backend: cli_api.refresh_access()),
|
|
87
|
+
("GET", ME_PATH, lambda backend: cli_api.authed("GET", ME_PATH)),
|
|
88
|
+
],
|
|
89
|
+
ids=["login-code-exchange", "mcp-handoff-start", "mcp-handoff-poll", "refresh", "authed"],
|
|
90
|
+
)
|
|
91
|
+
def test_request_to_a_backend_that_never_answers_fails_within_the_default_budget(
|
|
92
|
+
cli_backend, short_default_budget, method, path, call
|
|
93
|
+
):
|
|
94
|
+
backend, calls = cli_backend({(method, path): never_answer()})
|
|
95
|
+
started = time.monotonic()
|
|
96
|
+
with pytest.raises(requests.Timeout):
|
|
97
|
+
call(backend)
|
|
98
|
+
elapsed = time.monotonic() - started
|
|
99
|
+
|
|
100
|
+
assert elapsed < 0.8
|
|
101
|
+
assert _sent(calls) == [(method, path)]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_logout_ends_within_the_default_budget_when_the_backend_never_answers(
|
|
105
|
+
cli_backend, short_default_budget
|
|
106
|
+
):
|
|
107
|
+
backend, calls = cli_backend(
|
|
108
|
+
{("POST", REVOKE_PATH): never_answer(), ("POST", LOGOUT_PATH): never_answer()}
|
|
109
|
+
)
|
|
110
|
+
started = time.monotonic()
|
|
111
|
+
revoke = cli_api.logout_cli_session()
|
|
112
|
+
logout = cli_api.logout_jwt_session()
|
|
113
|
+
elapsed = time.monotonic() - started
|
|
114
|
+
|
|
115
|
+
assert elapsed < 1.6
|
|
116
|
+
assert revoke["attempted"] is True
|
|
117
|
+
assert revoke["revoked"] is False
|
|
118
|
+
assert revoke["method"] == "error"
|
|
119
|
+
assert logout is False
|
|
120
|
+
assert _sent(calls) == [("POST", REVOKE_PATH), ("POST", LOGOUT_PATH)]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_token_renewal_inside_an_mcp_request_is_bounded_too(cli_backend, short_default_budget):
|
|
124
|
+
backend, calls = cli_backend(
|
|
125
|
+
{
|
|
126
|
+
("POST", "/mcp"): answer_json(401, {"detail": "expired"}),
|
|
127
|
+
("POST", REFRESH_PATH): never_answer(),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
started = time.monotonic()
|
|
131
|
+
with pytest.raises(cli_api.ApiError, match="MCP resources/list request timed out"):
|
|
132
|
+
cli_api._mcp_json_rpc("resources/list", {}, request_id=7)
|
|
133
|
+
elapsed = time.monotonic() - started
|
|
134
|
+
|
|
135
|
+
assert elapsed < 0.8
|
|
136
|
+
assert _sent(calls) == [("POST", "/mcp"), ("POST", REFRESH_PATH)]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_authenticated_command_reports_a_backend_that_never_answers(
|
|
140
|
+
cli_backend, short_default_budget
|
|
141
|
+
):
|
|
142
|
+
backend, calls = cli_backend({("GET", ME_PATH): never_answer()})
|
|
143
|
+
|
|
144
|
+
result = runner.invoke(cli_mod.app, ["code-repository", "list"])
|
|
145
|
+
|
|
146
|
+
assert result.exit_code == 1
|
|
147
|
+
assert isinstance(result.exception, SystemExit)
|
|
148
|
+
assert result.output == f"The backend at {backend} could not be reached (ReadTimeout).\n"
|
|
149
|
+
assert _sent(calls) == [("GET", ME_PATH)]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.parametrize(
|
|
153
|
+
"failure", [requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout]
|
|
154
|
+
)
|
|
155
|
+
def test_login_says_when_the_backend_could_not_be_reached(monkeypatch, failure):
|
|
156
|
+
monkeypatch.delenv("MAINSEQUENCE_AUTH_MODE", raising=False)
|
|
157
|
+
monkeypatch.setattr(cli_mod.cfg, "backend_url", lambda: "https://backend.example")
|
|
158
|
+
|
|
159
|
+
def unreachable(**_kwargs):
|
|
160
|
+
raise failure("HTTPSConnectionPool(host='backend.example', port=443): Read timed out.")
|
|
161
|
+
|
|
162
|
+
monkeypatch.setattr(cli_mod, "login_via_browser", unreachable)
|
|
163
|
+
|
|
164
|
+
result = runner.invoke(cli_mod.app, ["login", "--no-status"])
|
|
165
|
+
|
|
166
|
+
assert result.exit_code == 1
|
|
167
|
+
# The command ended itself; an exception that escapes it is what prints a traceback.
|
|
168
|
+
assert isinstance(result.exception, SystemExit)
|
|
169
|
+
assert (
|
|
170
|
+
f"The backend at https://backend.example could not be reached ({failure.__name__})."
|
|
171
|
+
in result.output
|
|
172
|
+
)
|
|
173
|
+
assert "HTTPSConnectionPool" not in result.output
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_refresh_and_the_authed_retry_work_against_a_responsive_backend(cli_backend, monkeypatch):
|
|
177
|
+
saved = []
|
|
178
|
+
monkeypatch.setattr(cli_api, "get_tokens", lambda: {"username": "user@example.com"})
|
|
179
|
+
monkeypatch.setattr(cli_api, "save_tokens", lambda *args: saved.append(args) or True)
|
|
180
|
+
backend, calls = cli_backend(
|
|
181
|
+
{
|
|
182
|
+
("GET", ME_PATH): answer_sequence(
|
|
183
|
+
answer_json(401, {"detail": "expired"}),
|
|
184
|
+
answer_json(200, {"user": {"username": "user@example.com"}}),
|
|
185
|
+
),
|
|
186
|
+
("POST", REFRESH_PATH): answer_json(
|
|
187
|
+
200, {"access": "renewed-jwt", "refresh": "renewed-refresh"}
|
|
188
|
+
),
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
response = cli_api.authed("GET", ME_PATH)
|
|
193
|
+
|
|
194
|
+
assert response.status_code == 200
|
|
195
|
+
assert response.json() == {"user": {"username": "user@example.com"}}
|
|
196
|
+
assert _sent(calls) == [("GET", ME_PATH), ("POST", REFRESH_PATH), ("GET", ME_PATH)]
|
|
197
|
+
assert json.loads(calls[1][2]) == {"refresh": "saved-refresh"}
|
|
198
|
+
assert saved == [("user@example.com", "renewed-jwt", "renewed-refresh")]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_login_code_exchange_works_against_a_responsive_backend(cli_backend):
|
|
202
|
+
backend, calls = cli_backend(
|
|
203
|
+
{("POST", TOKEN_PATH): answer_json(200, {"access": "jwt", "refresh": "refresh"})}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
tokens = cli_api.exchange_cli_authorization_code(
|
|
207
|
+
code="code", code_verifier="verifier", redirect_uri="http://127.0.0.1/callback"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert tokens == {"backend": backend, "access": "jwt", "refresh": "refresh"}
|
|
211
|
+
assert json.loads(calls[0][2]) == {
|
|
212
|
+
"client_id": cli_api.CLI_BROWSER_CLIENT_ID,
|
|
213
|
+
"code": "code",
|
|
214
|
+
"code_verifier": "verifier",
|
|
215
|
+
"redirect_uri": "http://127.0.0.1/callback",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_logout_revoke_works_against_a_responsive_backend(cli_backend):
|
|
220
|
+
backend, calls = cli_backend(
|
|
221
|
+
{("POST", REVOKE_PATH): answer_json(200, {"detail": "CLI refresh token revoked."})}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
result = cli_api.logout_cli_session()
|
|
225
|
+
|
|
226
|
+
assert result == {
|
|
227
|
+
"attempted": True,
|
|
228
|
+
"revoked": True,
|
|
229
|
+
"method": "cli_revoke",
|
|
230
|
+
"detail": "CLI refresh token revoked.",
|
|
231
|
+
}
|
|
232
|
+
assert json.loads(calls[0][2]) == {"refresh": "saved-refresh"}
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_code_repository_skills.py
RENAMED
|
@@ -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
|
|
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("
|
|
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("
|
|
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
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_runtime_credential_exchange.py
RENAMED
|
@@ -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.
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/cli/code_repository_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/compute_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/github_issues.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/models_foundry.py
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/models_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/client/observability.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/code_repository_context.py
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/code_repository_skills.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/instrumentation/__init__.py
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/instrumentation/utils.py
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/repository_identity_security.py
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/runtime_credential_exchange.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence/server/caller_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/mainsequence.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_canonical_api_route_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_environment_resource_context.py
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_environment_response_contracts.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_fastapi_request_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_models_user_request_bound_auth.py
RENAMED
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_notification_uid_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_tolerant_response_reading.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.21.dev63 → mainsequence-9.0.22.dev65}/tests/test_user_application_facts.py
RENAMED
|
File without changes
|