mainsequence 9.0.5__tar.gz → 9.0.6__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.5/mainsequence.egg-info → mainsequence-9.0.6}/PKG-INFO +4 -5
- {mainsequence-9.0.5 → mainsequence-9.0.6}/README.md +2 -2
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +100 -2
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +11 -3
- mainsequence-9.0.6/mainsequence/client/_transport.py +154 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/client.py +17 -11
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/inference.py +2 -2
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/models_helpers.py +2 -2
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/utils.py +43 -115
- mainsequence-9.0.6/mainsequence/server/__init__.py +1 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/server/caller_assertions.py +3 -7
- {mainsequence-9.0.5 → mainsequence-9.0.6/mainsequence.egg-info}/PKG-INFO +4 -5
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence.egg-info/SOURCES.txt +2 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence.egg-info/requires.txt +1 -3
- {mainsequence-9.0.5 → mainsequence-9.0.6}/pyproject.toml +2 -5
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_auth_bootstrap.py +3 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_auth_precedence.py +3 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_caller_assertions.py +3 -4
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_dependency_extras.py +32 -0
- mainsequence-9.0.6/tests/test_http_retry_budget.py +434 -0
- mainsequence-9.0.5/mainsequence/server/__init__.py +0 -1
- {mainsequence-9.0.5 → mainsequence-9.0.6}/LICENSE +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/api.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/cli.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/config.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/__init__.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/models_user.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/code_repository_context.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/logconf.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence/server/fastapi.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/setup.cfg +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_cli.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_cli_secure_storage.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_client.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_code_repository_runtime_context.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_code_repository_skills.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_fastapi_request_identity.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_filter_normalization.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_inference_client.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_logconf.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.5 → mainsequence-9.0.6}/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.
|
|
3
|
+
Version: 9.0.6
|
|
4
4
|
Summary: Main Sequence SDK
|
|
5
5
|
Author-email: Main Sequence GmbH <dev@main-sequence.io>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,13 +25,12 @@ Requires-Dist: protobuf>=6.33.5
|
|
|
25
25
|
Requires-Dist: psutil
|
|
26
26
|
Requires-Dist: pydantic<3,>=2.11
|
|
27
27
|
Requires-Dist: pygments>=2.20.0
|
|
28
|
+
Requires-Dist: PyJWT[crypto]<3,>=2.10
|
|
28
29
|
Requires-Dist: pyyaml
|
|
29
30
|
Requires-Dist: requests>=2.33.0
|
|
30
31
|
Requires-Dist: structlog
|
|
31
32
|
Requires-Dist: typer
|
|
32
33
|
Requires-Dist: urllib3>=2.7.0
|
|
33
|
-
Provides-Extra: server
|
|
34
|
-
Requires-Dist: PyJWT[crypto]<3,>=2.10; extra == "server"
|
|
35
34
|
Dynamic: license-file
|
|
36
35
|
|
|
37
36
|
<p align="center">
|
|
@@ -75,7 +74,7 @@ Main package areas:
|
|
|
75
74
|
authentication, platform resources, and local CodeRepository operations
|
|
76
75
|
- `mainsequence.instrumentation` and `mainsequence.logconf`: SDK tracing and
|
|
77
76
|
logging integration
|
|
78
|
-
- `mainsequence.server`:
|
|
77
|
+
- `mainsequence.server`: server-side caller assertion verification
|
|
79
78
|
- `mainsequence.scaffold_skills`: reusable, version-pinned skill copying for
|
|
80
79
|
this SDK and extension libraries
|
|
81
80
|
|
|
@@ -148,7 +147,7 @@ another repository, use the explicitly scoped `filter_admin` path. See
|
|
|
148
147
|
[Resource releases](docs/knowledge/infrastructure/resource_releases.md) for
|
|
149
148
|
supported filters and examples.
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
The standard `mainsequence` installation includes PyJWT and cryptography for the
|
|
152
151
|
framework-independent [caller assertion verifier](docs/knowledge/server/caller_assertions.md).
|
|
153
152
|
Applications remain responsible for resource authorization and storage
|
|
154
153
|
operations.
|
|
@@ -39,7 +39,7 @@ Main package areas:
|
|
|
39
39
|
authentication, platform resources, and local CodeRepository operations
|
|
40
40
|
- `mainsequence.instrumentation` and `mainsequence.logconf`: SDK tracing and
|
|
41
41
|
logging integration
|
|
42
|
-
- `mainsequence.server`:
|
|
42
|
+
- `mainsequence.server`: server-side caller assertion verification
|
|
43
43
|
- `mainsequence.scaffold_skills`: reusable, version-pinned skill copying for
|
|
44
44
|
this SDK and extension libraries
|
|
45
45
|
|
|
@@ -112,7 +112,7 @@ another repository, use the explicitly scoped `filter_admin` path. See
|
|
|
112
112
|
[Resource releases](docs/knowledge/infrastructure/resource_releases.md) for
|
|
113
113
|
supported filters and examples.
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
The standard `mainsequence` installation includes PyJWT and cryptography for the
|
|
116
116
|
framework-independent [caller assertion verifier](docs/knowledge/server/caller_assertions.md).
|
|
117
117
|
Applications remain responsible for resource authorization and storage
|
|
118
118
|
operations.
|
|
@@ -248,8 +248,8 @@ The platform deploys from the push according to the repository's
|
|
|
248
248
|
pushes deploy:
|
|
249
249
|
|
|
250
250
|
- omitted or `null`: every push to the branch deploys;
|
|
251
|
-
- a regular expression: a
|
|
252
|
-
branch's latest commit.
|
|
251
|
+
- a regular expression: a matching tag deploys the commit it points at, whether
|
|
252
|
+
that is the branch's latest commit or an older commit on the branch.
|
|
253
253
|
|
|
254
254
|
Automatic deployment and `tag_regex` are set in the workflow file only; a
|
|
255
255
|
`ResourceRelease` or `Job` update does not accept them. The Main Sequence
|
|
@@ -300,6 +300,104 @@ only the commits CI tagged. Details:
|
|
|
300
300
|
Do not call `sync-after-commit` or any backend repair endpoint. The GitHub
|
|
301
301
|
branch-push webhook owns backend repository reconciliation.
|
|
302
302
|
|
|
303
|
+
### Development Deploys Every Push, Production Deploys Tags
|
|
304
|
+
|
|
305
|
+
A common setup uses two workflow files scoped to Environments. On the
|
|
306
|
+
production branch only the first applies; on the development branch only the
|
|
307
|
+
second:
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
# .mainsequence/workflows/api-production.yaml
|
|
311
|
+
api_version: "2.3.0"
|
|
312
|
+
name: api-production
|
|
313
|
+
scope:
|
|
314
|
+
environments: [production]
|
|
315
|
+
resources:
|
|
316
|
+
- key: api
|
|
317
|
+
kind: fastapi
|
|
318
|
+
spec:
|
|
319
|
+
source_path: api/main.py
|
|
320
|
+
automatic_deployment: true
|
|
321
|
+
automatic_redeployment:
|
|
322
|
+
enabled: true
|
|
323
|
+
tag_regex: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
# .mainsequence/workflows/api-development.yaml
|
|
328
|
+
api_version: "2.3.0"
|
|
329
|
+
name: api-development
|
|
330
|
+
scope:
|
|
331
|
+
environments: [development] # the development Environment's exact name
|
|
332
|
+
resources:
|
|
333
|
+
- key: api
|
|
334
|
+
kind: fastapi
|
|
335
|
+
spec:
|
|
336
|
+
source_path: api/main.py
|
|
337
|
+
automatic_deployment: true
|
|
338
|
+
automatic_redeployment:
|
|
339
|
+
enabled: true
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
A push to the development branch deploys at once. A merge to `main` deploys
|
|
343
|
+
nothing until a matching tag points at a commit on `main`; with the release job
|
|
344
|
+
above, CI creates that tag after the tests pass. Take the exact fields from the
|
|
345
|
+
branch's workflow template and validate each file through the branch's
|
|
346
|
+
`validate-workflow` action before committing it, as the
|
|
347
|
+
`platform_operations/orchestration_and_releases` skill describes.
|
|
348
|
+
|
|
349
|
+
### Creating A Release Tag
|
|
350
|
+
|
|
351
|
+
The platform reacts to the tag push, however the tag was made. The short tag
|
|
352
|
+
name must fully match `tag_regex`, and the tagged commit must be on the
|
|
353
|
+
target's branch (its latest commit or an older one).
|
|
354
|
+
|
|
355
|
+
- CI, as in the release job above. This is the recommended path.
|
|
356
|
+
- By hand, only when the user asks for it:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
git tag v1.3.0 <commit>
|
|
360
|
+
git push origin v1.3.0
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Lightweight and annotated tags both work. Push one tag at a time: GitHub
|
|
364
|
+
sends no event when more than three tags are pushed at once, so
|
|
365
|
+
`git push --tags` can deploy nothing.
|
|
366
|
+
- A GitHub Release: publishing a release with a new tag, in the web UI or with
|
|
367
|
+
`gh release create v1.3.0 --target main`, creates the tag, and that tag
|
|
368
|
+
deploys like a pushed one.
|
|
369
|
+
|
|
370
|
+
A tag created with the Actions `GITHUB_TOKEN` starts no other GitHub Actions
|
|
371
|
+
workflow, but it still reaches Main Sequence. Deleting a tag deploys nothing
|
|
372
|
+
and removes nothing. A tag that does not match, or whose commit is only on
|
|
373
|
+
another branch, deploys nothing for that target.
|
|
374
|
+
|
|
375
|
+
### Going Back To An Older Version
|
|
376
|
+
|
|
377
|
+
Two ways work. Use either only when the user asks for it.
|
|
378
|
+
|
|
379
|
+
- Revert and release a new version: `git revert <commit>`, raise the patch
|
|
380
|
+
version with `uv version --bump patch`, commit, push, and let CI tag the
|
|
381
|
+
result. This works for every target, including every-push targets, and keeps
|
|
382
|
+
the version moving forward.
|
|
383
|
+
- For a target with a `tag_regex`, push the older version's tag again. GitHub
|
|
384
|
+
sends an event only when a tag is created, so delete the tag on GitHub and
|
|
385
|
+
push it again; it still points at the same commit:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
git fetch --tags origin
|
|
389
|
+
git push origin :refs/tags/v1.2.0
|
|
390
|
+
git push origin v1.2.0
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The target redeploys that commit exactly as it was released: its code, its
|
|
394
|
+
image and its workflow file, whose settings replace the current ones. The
|
|
395
|
+
commit must still be on the branch and the tag must match the target's
|
|
396
|
+
current `tag_regex`. Every-push targets ignore the tag. Re-pushing a tag
|
|
397
|
+
also re-runs any GitHub Actions workflow that the tag triggers, and a tag
|
|
398
|
+
ruleset may forbid deleting it; use the revert route then. The next release
|
|
399
|
+
tag deploys a newer version again.
|
|
400
|
+
|
|
303
401
|
## Diagnose Partial Completion Before Retrying
|
|
304
402
|
|
|
305
403
|
Never rerun an entire mutating workflow blindly.
|
|
@@ -91,8 +91,16 @@ the SDK or CodeRepository code.
|
|
|
91
91
|
|
|
92
92
|
Every file requires the backend-advertised `api_version`, a name, and resource
|
|
93
93
|
declarations. Use the current template for accepted fields and resource kinds.
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
A declaration owns its target. On the next push of the branch head, these
|
|
95
|
+
delete the target:
|
|
96
|
+
|
|
97
|
+
- removing the declaration or its file;
|
|
98
|
+
- a `scope` that excludes the branch's Environment.
|
|
99
|
+
|
|
100
|
+
Renaming a file or key deletes the old target and creates a new one, with new
|
|
101
|
+
UIDs and URLs. An invalid file deletes nothing. The branch's Harness Agent
|
|
102
|
+
stays while any workflow file declares a `harness_agent`, so moving its
|
|
103
|
+
declaration keeps the same Agent.
|
|
96
104
|
|
|
97
105
|
Do not hide important recurring schedules in ad hoc shell history or one-off manual commands.
|
|
98
106
|
|
|
@@ -224,7 +232,7 @@ Automatic deployment and its `tag_regex` are set in the repository's `.mainseque
|
|
|
224
232
|
The declaration's `tag_regex` decides which pushes deploy:
|
|
225
233
|
|
|
226
234
|
- omitted or `null`: every push to the branch deploys
|
|
227
|
-
- a regular expression: a
|
|
235
|
+
- a regular expression: a matching tag deploys the commit it points at, whether that is the branch's latest commit or an older commit on the branch
|
|
228
236
|
|
|
229
237
|
The platform does not create or name tags. Release tags come from the repository's own CI, for example a GitHub Actions workflow that tags the version declared in `pyproject.toml` after the tests pass. The maintenance skill `.agents/skills/mainsequence/maintenance/code_repository_maintenance/SKILL.md` has an example. `mainsequence code-repository sync` only refreshes `uv.lock`, the local environment, and `requirements.txt`; it does not commit, tag, or push.
|
|
230
238
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""One bounded retry policy for SDK HTTP sessions."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from requests.adapters import HTTPAdapter
|
|
10
|
+
from urllib3.exceptions import InvalidHeader
|
|
11
|
+
from urllib3.util import Retry, Timeout
|
|
12
|
+
|
|
13
|
+
DEFAULT_ALLOWED_METHODS = frozenset({"HEAD", "GET", "OPTIONS"})
|
|
14
|
+
DEFAULT_STATUS_FORCELIST = (429, 500, 502, 503, 504)
|
|
15
|
+
DEFAULT_TIMEOUT = (5.0, 120.0)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RequestBudget:
|
|
19
|
+
def __init__(self, timeout):
|
|
20
|
+
if timeout is None:
|
|
21
|
+
timeout = DEFAULT_TIMEOUT
|
|
22
|
+
if isinstance(timeout, Timeout):
|
|
23
|
+
connect, read = timeout.connect_timeout, timeout.read_timeout
|
|
24
|
+
total = timeout.total
|
|
25
|
+
elif isinstance(timeout, tuple):
|
|
26
|
+
connect, read = timeout
|
|
27
|
+
total = None
|
|
28
|
+
else:
|
|
29
|
+
connect = read = total = timeout
|
|
30
|
+
self.connect = self._seconds(connect)
|
|
31
|
+
self.read = self._seconds(read)
|
|
32
|
+
self.deadline = time.monotonic() + self._seconds(
|
|
33
|
+
self.connect + self.read if total is None else total
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _seconds(value):
|
|
38
|
+
seconds = float(value)
|
|
39
|
+
if not math.isfinite(seconds) or seconds <= 0:
|
|
40
|
+
raise ValueError("Request timeouts must be positive, finite seconds.")
|
|
41
|
+
return seconds
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def remaining(self):
|
|
45
|
+
return self.deadline - time.monotonic()
|
|
46
|
+
|
|
47
|
+
def timeout(self):
|
|
48
|
+
remaining = self.remaining
|
|
49
|
+
if remaining <= 0:
|
|
50
|
+
raise requests.Timeout("The SDK request's total timeout budget is exhausted.")
|
|
51
|
+
return Timeout(
|
|
52
|
+
total=remaining,
|
|
53
|
+
connect=min(self.connect, remaining),
|
|
54
|
+
read=min(self.read, remaining),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_ACTIVE_BUDGET: ContextVar[RequestBudget | None] = ContextVar("sdk_request_budget", default=None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@contextmanager
|
|
62
|
+
def request_budget(timeout):
|
|
63
|
+
budget = RequestBudget(timeout)
|
|
64
|
+
parent = _ACTIVE_BUDGET.get()
|
|
65
|
+
if parent is not None:
|
|
66
|
+
budget.deadline = min(budget.deadline, parent.deadline)
|
|
67
|
+
token = _ACTIVE_BUDGET.set(budget)
|
|
68
|
+
try:
|
|
69
|
+
yield budget
|
|
70
|
+
finally:
|
|
71
|
+
_ACTIVE_BUDGET.reset(token)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def remaining_timeout(timeout):
|
|
75
|
+
budget = _ACTIVE_BUDGET.get()
|
|
76
|
+
if budget is None:
|
|
77
|
+
return timeout
|
|
78
|
+
connect, read = timeout
|
|
79
|
+
remaining = budget.timeout().total
|
|
80
|
+
return Timeout(total=remaining, connect=min(connect, remaining), read=min(read, remaining))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DeadlineHTTPAdapter(HTTPAdapter):
|
|
84
|
+
def __init__(self, *, retries, backoff_factor):
|
|
85
|
+
if not isinstance(retries, int) or retries < 0:
|
|
86
|
+
raise ValueError("retries must be a non-negative integer.")
|
|
87
|
+
if not math.isfinite(backoff_factor) or backoff_factor < 0:
|
|
88
|
+
raise ValueError("backoff_factor must be non-negative and finite.")
|
|
89
|
+
# urllib3 must never add a second retry loop or resend a write on connect failure.
|
|
90
|
+
super().__init__(
|
|
91
|
+
max_retries=Retry(
|
|
92
|
+
total=0,
|
|
93
|
+
connect=0,
|
|
94
|
+
read=False,
|
|
95
|
+
status=0,
|
|
96
|
+
other=0,
|
|
97
|
+
allowed_methods=DEFAULT_ALLOWED_METHODS,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
self.retries = retries
|
|
101
|
+
self.backoff_factor = backoff_factor
|
|
102
|
+
|
|
103
|
+
def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
|
|
104
|
+
budget = RequestBudget(timeout)
|
|
105
|
+
parent = _ACTIVE_BUDGET.get()
|
|
106
|
+
if parent is not None:
|
|
107
|
+
budget.deadline = min(budget.deadline, parent.deadline)
|
|
108
|
+
retryable = request.method in DEFAULT_ALLOWED_METHODS
|
|
109
|
+
for attempt in range(self.retries + 1):
|
|
110
|
+
response = None
|
|
111
|
+
error = None
|
|
112
|
+
try:
|
|
113
|
+
response = super().send(
|
|
114
|
+
request,
|
|
115
|
+
stream=stream,
|
|
116
|
+
timeout=budget.timeout(),
|
|
117
|
+
verify=verify,
|
|
118
|
+
cert=cert,
|
|
119
|
+
proxies=proxies,
|
|
120
|
+
)
|
|
121
|
+
if not stream:
|
|
122
|
+
# Read before deciding to retry, so body failures share the same budget.
|
|
123
|
+
_ = response.content
|
|
124
|
+
if not retryable or response.status_code not in DEFAULT_STATUS_FORCELIST:
|
|
125
|
+
return response
|
|
126
|
+
except requests.exceptions.SSLError:
|
|
127
|
+
raise
|
|
128
|
+
except (
|
|
129
|
+
requests.ConnectionError,
|
|
130
|
+
requests.Timeout,
|
|
131
|
+
requests.exceptions.ChunkedEncodingError,
|
|
132
|
+
) as exc:
|
|
133
|
+
error = exc
|
|
134
|
+
|
|
135
|
+
delay = min(self.backoff_factor * 2**attempt, Retry.DEFAULT_BACKOFF_MAX)
|
|
136
|
+
if error is None:
|
|
137
|
+
try:
|
|
138
|
+
retry_after = self.max_retries.get_retry_after(response)
|
|
139
|
+
except InvalidHeader:
|
|
140
|
+
retry_after = None
|
|
141
|
+
if retry_after is not None:
|
|
142
|
+
delay = max(delay, retry_after)
|
|
143
|
+
|
|
144
|
+
if not retryable or attempt == self.retries or delay >= budget.remaining:
|
|
145
|
+
if error is not None:
|
|
146
|
+
if response is not None:
|
|
147
|
+
response.close()
|
|
148
|
+
raise error
|
|
149
|
+
return response
|
|
150
|
+
if response is not None:
|
|
151
|
+
response.close()
|
|
152
|
+
time.sleep(delay)
|
|
153
|
+
|
|
154
|
+
raise AssertionError("Unreachable retry state")
|
|
@@ -9,7 +9,7 @@ import requests
|
|
|
9
9
|
from mainsequence.defaults import resolve_backend_endpoint
|
|
10
10
|
|
|
11
11
|
from .exceptions import raise_for_response
|
|
12
|
-
from .utils import AuthLoaders, build_session
|
|
12
|
+
from .utils import AuthLoaders, build_session, make_request
|
|
13
13
|
from .utils import loaders as _global_loaders
|
|
14
14
|
from .utils import session as _global_session
|
|
15
15
|
|
|
@@ -35,6 +35,7 @@ class MainSequenceClient:
|
|
|
35
35
|
- Uses ONE shared loaders + session (from utils) by default
|
|
36
36
|
- Can be reconfigured once at startup
|
|
37
37
|
"""
|
|
38
|
+
|
|
38
39
|
_default: MainSequenceClient | None = None
|
|
39
40
|
|
|
40
41
|
def __init__(
|
|
@@ -72,15 +73,18 @@ class MainSequenceClient:
|
|
|
72
73
|
timeout: tuple[float, float] | float | None = None,
|
|
73
74
|
) -> Any:
|
|
74
75
|
url = self.config.api_root.rstrip("/") + "/" + path.lstrip("/")
|
|
75
|
-
to =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
to = self.config.timeout if timeout is None else timeout
|
|
77
|
+
payload = {"params": params, "json": json}
|
|
78
|
+
if files is not None:
|
|
79
|
+
payload["files"] = files
|
|
80
|
+
r = make_request(
|
|
81
|
+
self.session,
|
|
82
|
+
method,
|
|
83
|
+
url,
|
|
84
|
+
self.loaders,
|
|
85
|
+
payload=payload,
|
|
86
|
+
time_out=to,
|
|
87
|
+
)
|
|
84
88
|
|
|
85
89
|
raise_for_response(r, payload={"params": params, "json": json})
|
|
86
90
|
|
|
@@ -95,7 +99,9 @@ class MainSequenceClient:
|
|
|
95
99
|
"""
|
|
96
100
|
if cls._default is None:
|
|
97
101
|
# Reuse the existing global singletons from utils to avoid two sessions in prod.
|
|
98
|
-
cls._default = cls(
|
|
102
|
+
cls._default = cls(
|
|
103
|
+
MainSequenceClientConfig(), loaders=_global_loaders, session=_global_session
|
|
104
|
+
)
|
|
99
105
|
return cls._default
|
|
100
106
|
|
|
101
107
|
@classmethod
|
|
@@ -69,8 +69,8 @@ class InferenceClient:
|
|
|
69
69
|
if self.organization_environment_uid is not None:
|
|
70
70
|
query["organization_environment_uid"] = self.organization_environment_uid
|
|
71
71
|
url = f"{utils.API_ENDPOINT.rstrip('/')}/{path}"
|
|
72
|
-
#
|
|
73
|
-
#
|
|
72
|
+
# Inference opts out of safe-read retries as well: an uncertain outcome
|
|
73
|
+
# must preserve the caller's explicit idempotency key.
|
|
74
74
|
with utils.build_session(loaders=utils.loaders, retries=0) as session:
|
|
75
75
|
headers = dict(utils.loaders.refresh_headers(force=False, session=session))
|
|
76
76
|
if key is not None:
|
|
@@ -107,8 +107,8 @@ class AutomaticRedeploymentPolicy(BaseModel):
|
|
|
107
107
|
description=(
|
|
108
108
|
"Regular expression matched against repository tags, set by `tag_regex` "
|
|
109
109
|
"in the repository's `.mainsequence/workflows/*.yaml`. Null deploys every "
|
|
110
|
-
"push; a regex deploys
|
|
111
|
-
"latest commit."
|
|
110
|
+
"push; a regex deploys the commit a matching tag points at, whether it is "
|
|
111
|
+
"the branch's latest commit or an older commit on the branch."
|
|
112
112
|
),
|
|
113
113
|
examples=[None, r"^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$"],
|
|
114
114
|
)
|
|
@@ -17,26 +17,21 @@ from uuid import UUID, getnode
|
|
|
17
17
|
|
|
18
18
|
import psutil
|
|
19
19
|
import requests
|
|
20
|
-
from requests.adapters import HTTPAdapter
|
|
21
20
|
from requests.structures import CaseInsensitiveDict
|
|
22
|
-
from urllib3.util.retry import Retry
|
|
23
21
|
|
|
24
22
|
from mainsequence.defaults import resolve_backend_endpoint
|
|
25
23
|
from mainsequence.logconf import logger
|
|
26
24
|
|
|
25
|
+
from ._transport import DEFAULT_ALLOWED_METHODS as DEFAULT_ALLOWED_METHODS
|
|
26
|
+
from ._transport import DEFAULT_STATUS_FORCELIST as DEFAULT_STATUS_FORCELIST
|
|
27
|
+
from ._transport import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
|
|
28
|
+
from ._transport import DeadlineHTTPAdapter, remaining_timeout, request_budget
|
|
29
|
+
|
|
27
30
|
# ---- Backend defaults (single source of truth) ----
|
|
28
31
|
MAINSEQUENCE_ENDPOINT = resolve_backend_endpoint()
|
|
29
32
|
API_ENDPOINT = f"{MAINSEQUENCE_ENDPOINT}/api/v1"
|
|
30
33
|
AUTH_ENDPOINT = MAINSEQUENCE_ENDPOINT.rstrip("/")
|
|
31
34
|
|
|
32
|
-
DEFAULT_STATUS_FORCELIST = (429, 500, 502, 503, 504)
|
|
33
|
-
DEFAULT_ALLOWED_METHODS = frozenset(["HEAD", "GET", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"])
|
|
34
|
-
|
|
35
|
-
# requests supports either a float or (connect, read). Preserve previous ~120s read behavior,
|
|
36
|
-
# but add a sane connect timeout.
|
|
37
|
-
DEFAULT_TIMEOUT: tuple[float, float] = (5.0, 120.0)
|
|
38
|
-
|
|
39
|
-
|
|
40
35
|
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
41
36
|
|
|
42
37
|
|
|
@@ -383,7 +378,7 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
383
378
|
"credential_secret": credential_secret,
|
|
384
379
|
},
|
|
385
380
|
headers={"Content-Type": "application/json"},
|
|
386
|
-
timeout=self.timeout,
|
|
381
|
+
timeout=remaining_timeout(self.timeout),
|
|
387
382
|
allow_redirects=False,
|
|
388
383
|
)
|
|
389
384
|
if response.status_code < 200 or response.status_code >= 300:
|
|
@@ -515,7 +510,7 @@ class JWTAuthProvider(BaseAuthProvider):
|
|
|
515
510
|
self.refresh_url,
|
|
516
511
|
json={"refresh": self.refresh_token},
|
|
517
512
|
headers={"Content-Type": "application/json"},
|
|
518
|
-
timeout=self.timeout,
|
|
513
|
+
timeout=remaining_timeout(self.timeout),
|
|
519
514
|
)
|
|
520
515
|
|
|
521
516
|
if r.status_code in _REFUSED_RENEWAL_STATUSES:
|
|
@@ -564,7 +559,7 @@ class JWTAuthProvider(BaseAuthProvider):
|
|
|
564
559
|
self.obtain_url,
|
|
565
560
|
json={"username": username, "password": password},
|
|
566
561
|
headers={"Content-Type": "application/json"},
|
|
567
|
-
timeout=self.timeout,
|
|
562
|
+
timeout=remaining_timeout(self.timeout),
|
|
568
563
|
)
|
|
569
564
|
r.raise_for_status()
|
|
570
565
|
data = r.json()
|
|
@@ -671,63 +666,46 @@ def make_request(
|
|
|
671
666
|
):
|
|
672
667
|
from requests.models import Response
|
|
673
668
|
|
|
674
|
-
TIMEOFF = 0.25
|
|
675
|
-
TRIES = int(15 // TIMEOFF)
|
|
676
669
|
timeout = DEFAULT_TIMEOUT if time_out is None else time_out
|
|
677
670
|
payload = {} if payload is None else payload
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return session.get
|
|
682
|
-
elif r_type == "POST":
|
|
683
|
-
return session.post
|
|
684
|
-
elif r_type == "PUT":
|
|
685
|
-
return session.put
|
|
686
|
-
elif r_type == "PATCH":
|
|
687
|
-
return session.patch
|
|
688
|
-
elif r_type == "DELETE":
|
|
689
|
-
return session.delete
|
|
690
|
-
else:
|
|
691
|
-
raise NotImplementedError(f"Unsupported method: {r_type}")
|
|
671
|
+
r_type = r_type.upper()
|
|
672
|
+
if r_type not in DEFAULT_ALLOWED_METHODS | {"POST", "PUT", "PATCH", "DELETE"}:
|
|
673
|
+
raise NotImplementedError(f"Unsupported method: {r_type}")
|
|
692
674
|
|
|
693
675
|
request_kwargs = {}
|
|
694
676
|
if r_type in ("POST", "PATCH") and "files" in payload:
|
|
695
|
-
request_kwargs
|
|
696
|
-
request_kwargs["
|
|
677
|
+
request_kwargs = dict(payload)
|
|
678
|
+
request_kwargs["data"] = request_kwargs.pop("json", {})
|
|
697
679
|
s.headers.pop("Content-Type", None)
|
|
698
680
|
else:
|
|
699
|
-
request_kwargs = payload
|
|
700
|
-
|
|
701
|
-
req =
|
|
702
|
-
keep_request = True
|
|
703
|
-
counter = 0
|
|
704
|
-
auth_retried = False
|
|
681
|
+
request_kwargs = dict(payload)
|
|
682
|
+
request_kwargs.setdefault("allow_redirects", False)
|
|
683
|
+
req = getattr(s, r_type.lower())
|
|
705
684
|
|
|
706
685
|
if accept_gzip:
|
|
707
686
|
s.headers.setdefault("Accept-Encoding", "gzip")
|
|
708
687
|
|
|
709
|
-
|
|
688
|
+
with request_budget(timeout) as budget:
|
|
710
689
|
try:
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
break
|
|
690
|
+
for auth_attempt in range(2):
|
|
691
|
+
if loaders is not None:
|
|
692
|
+
budget.timeout()
|
|
693
|
+
s.headers.update(loaders.refresh_headers(force=bool(auth_attempt), session=s))
|
|
694
|
+
|
|
695
|
+
start_time = time.perf_counter()
|
|
696
|
+
logger.debug(f"Requesting {r_type} from {url}")
|
|
697
|
+
r = req(url, timeout=budget.timeout(), **request_kwargs)
|
|
698
|
+
duration = time.perf_counter() - start_time
|
|
699
|
+
logger.debug(f"{url} took {duration:.4f} seconds.")
|
|
700
|
+
if (
|
|
701
|
+
r.status_code != 401
|
|
702
|
+
or loaders is None
|
|
703
|
+
or auth_attempt
|
|
704
|
+
or r_type not in DEFAULT_ALLOWED_METHODS
|
|
705
|
+
):
|
|
706
|
+
return r
|
|
707
|
+
r.close()
|
|
708
|
+
logger.warning("Error 401; forcing auth refresh once within the request budget")
|
|
731
709
|
|
|
732
710
|
except AuthError as e:
|
|
733
711
|
logger.warning(f"Auth error for {url}: {e}")
|
|
@@ -736,32 +714,15 @@ def make_request(
|
|
|
736
714
|
r.error_type = "auth_error"
|
|
737
715
|
r.status_code = 401
|
|
738
716
|
r._content = str(e).encode("utf-8")
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
except requests.exceptions.ConnectionError:
|
|
743
|
-
logger.exception(f"Error connecting {url}")
|
|
744
|
-
except TypeError as e:
|
|
745
|
-
logger.exception(f"Type error for {url} exception {e}")
|
|
746
|
-
raise e
|
|
747
|
-
except Exception as e:
|
|
748
|
-
logger.exception(f"Error connecting {url} exception {e}")
|
|
749
|
-
|
|
750
|
-
counter += 1
|
|
751
|
-
if counter >= TRIES:
|
|
752
|
-
keep_request = False
|
|
717
|
+
return r
|
|
718
|
+
except requests.RequestException as e:
|
|
719
|
+
logger.warning(f"Request {r_type} to {url} failed: {e}")
|
|
753
720
|
r = Response()
|
|
754
721
|
r.code = "expired"
|
|
755
722
|
r.error_type = "expired"
|
|
756
723
|
r.status_code = 500
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
logger.debug(
|
|
760
|
-
f"Trying request again after {TIMEOFF}s - Counter: {counter}/{TRIES} - URL: {url}"
|
|
761
|
-
)
|
|
762
|
-
time.sleep(TIMEOFF)
|
|
763
|
-
|
|
764
|
-
return r
|
|
724
|
+
r._content = str(e).encode("utf-8")
|
|
725
|
+
return r
|
|
765
726
|
|
|
766
727
|
|
|
767
728
|
def build_session(
|
|
@@ -779,23 +740,7 @@ def build_session(
|
|
|
779
740
|
if accept_gzip:
|
|
780
741
|
s.headers.setdefault("Accept-Encoding", "gzip")
|
|
781
742
|
|
|
782
|
-
|
|
783
|
-
total=retries,
|
|
784
|
-
connect=retries,
|
|
785
|
-
read=retries,
|
|
786
|
-
status=retries,
|
|
787
|
-
backoff_factor=backoff_factor,
|
|
788
|
-
status_forcelist=DEFAULT_STATUS_FORCELIST,
|
|
789
|
-
respect_retry_after_header=True,
|
|
790
|
-
raise_on_status=False,
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
try:
|
|
794
|
-
retry_cfg = Retry(allowed_methods=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
|
|
795
|
-
except TypeError:
|
|
796
|
-
retry_cfg = Retry(method_whitelist=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
|
|
797
|
-
|
|
798
|
-
adapter = HTTPAdapter(max_retries=retry_cfg)
|
|
743
|
+
adapter = DeadlineHTTPAdapter(retries=retries, backoff_factor=backoff_factor)
|
|
799
744
|
s.mount("https://", adapter)
|
|
800
745
|
s.mount("http://", adapter)
|
|
801
746
|
return s
|
|
@@ -929,24 +874,7 @@ def _install_retry_adapters_in_place(
|
|
|
929
874
|
Configure retry adapters on an EXISTING session object (do not rebind 'session').
|
|
930
875
|
This is critical so 'from utils import session' users still get the updated behavior.
|
|
931
876
|
"""
|
|
932
|
-
|
|
933
|
-
total=retries,
|
|
934
|
-
connect=retries,
|
|
935
|
-
read=retries,
|
|
936
|
-
status=retries,
|
|
937
|
-
backoff_factor=backoff_factor,
|
|
938
|
-
status_forcelist=DEFAULT_STATUS_FORCELIST,
|
|
939
|
-
respect_retry_after_header=True,
|
|
940
|
-
raise_on_status=False,
|
|
941
|
-
)
|
|
942
|
-
|
|
943
|
-
# urllib3 compatibility across versions
|
|
944
|
-
try:
|
|
945
|
-
retry_cfg = Retry(allowed_methods=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
|
|
946
|
-
except TypeError:
|
|
947
|
-
retry_cfg = Retry(method_whitelist=DEFAULT_ALLOWED_METHODS, **retry_kwargs)
|
|
948
|
-
|
|
949
|
-
adapter = HTTPAdapter(max_retries=retry_cfg)
|
|
877
|
+
adapter = DeadlineHTTPAdapter(retries=retries, backoff_factor=backoff_factor)
|
|
950
878
|
|
|
951
879
|
# Close old adapters' pools (best-effort), then mount new ones
|
|
952
880
|
for prefix in ("https://", "http://"):
|