Skip to content

feat(client): opt-in retry with backoff honoring Retry-After - #58

Merged
Datata1 merged 1 commit into
mainfrom
refactor/07-retry-support
Jul 12, 2026
Merged

feat(client): opt-in retry with backoff honoring Retry-After#58
Datata1 merged 1 commit into
mainfrom
refactor/07-retry-support

Conversation

@Datata1

Copy link
Copy Markdown
Owner

Implements plans/refactor/07-retry-support.md (final ticket):

  • RetryConfig frozen dataclass (max_retries=0, backoff_factor=0.5, retry_statuses={429,502,503,504}, retry_methods={GET,HEAD,PUT,DELETE}), exported from the top-level package and accepted by CodesphereSDK(retry=...)
  • Retry loop in APIHttpClient.request: idempotent methods only by default (POST opt-in via retry_methods), retries on the configured statuses and on httpx timeout/connect errors; delay honors Retry-After (delta-seconds or HTTP-date, past dates clamp to 0) and falls back to exponential backoff with jitter; exhausted retries raise exactly what the non-retry path would (RateLimitError with retry_after populated, TimeoutError, ...); each retry is logged at debug level
  • Plain ~40-line loop, no retry dependency reintroduced
  • Default max_retries=0 means zero behavior change unless opted in (tested: single request on failure with the default config)
  • 17 respx tests: Retry-After honored (429->200 with exactly 2 requests and >=1s sleep), exhaustion, exponential backoff bounds, non-retryable 500, POST gating and opt-in, transport-error retries, SDK wire-through, Retry-After parsing matrix
  • README documents RetryConfig

Implements plans/refactor/07-retry-support.md (final ticket):
- RetryConfig frozen dataclass (max_retries=0, backoff_factor=0.5,
retry_statuses={429,502,503,504}, retry_methods={GET,HEAD,PUT,DELETE}),
exported from the top-level package and accepted by
CodesphereSDK(retry=...)
- Retry loop in APIHttpClient.request: idempotent methods only by
default (POST opt-in via retry_methods), retries on the configured
statuses and on httpx timeout/connect errors; delay honors Retry-After
(delta-seconds or HTTP-date, past dates clamp to 0) and falls back to
exponential backoff with jitter; exhausted retries raise exactly what
the non-retry path would (RateLimitError with retry_after populated,
TimeoutError, ...); each retry is logged at debug level
- Plain ~40-line loop, no retry dependency reintroduced
- Default max_retries=0 means zero behavior change unless opted in
(tested: single request on failure with the default config)
- 17 respx tests: Retry-After honored (429->200 with exactly 2 requests
and >=1s sleep), exhaustion, exponential backoff bounds, non-retryable
500, POST gating and opt-in, transport-error retries, SDK wire-through,
Retry-After parsing matrix
- README documents RetryConfig
@Datata1Datata1 self-assigned this Jul 12, 2026
@Datata1
Datata1 merged commit 1a6c672 into mainJul 12, 2026
3 of 4 checks passed
@github-actions

Copy link
Copy Markdown

🛡️ Bandit Security Scan Results

✅ No security issues found by Bandit.

@github-actions

Copy link
Copy Markdown

Coverage

Pytest Coverage Report
FileStmtsMissBranchBrPartCoverMissing
__init__.py10000100%
client.py40060100%
config.py16000100%
exceptions.py940380100%
http_client.py98322296%124->162, 135->136, 136–137, 162
utils.py1024179%9->10, 10–11
core
__init__.py4000100%
base.py55020100%
handler.py59318096%80–82
operations.py15000100%
resources
__init__.py0000100%
resources/metadata
__init__.py3000100%
operations.py6000100%
resources.py13000100%
schemas.py28000100%
resources/team
__init__.py5000100%
operations.py8000100%
resources.py11000100%
schemas.py27000100%
resources/team/domain
__init__.py3000100%
operations.py11000100%
resources.py16000100%
schemas.py5550091%67, 69, 72, 75–76
resources/team/usage
__init__.py3000100%
operations.py4000100%
resources.py4328292%99->102, 102, 162->165, 165
schemas.py7076584%66->67, 67, 72–73, 76, 84–86
resources/workspace
__init__.py5000100%
envVars.py4000100%
operations.py12000100%
resources.py1624280%13->14, 14, 19->20, 20
schemas.py99146681%100–101, 103–109, 115–116, 118, 142, 161
resources/workspace/env_vars
__init__.py3000100%
operations.py7000100%
resources.py26212484%33->35, 35–36, 38->exit
schemas.py4000100%
resources/workspace/git
__init__.py3000100%
operations.py8000100%
resources.py15040100%
schemas.py4000100%
resources/workspace/landscape
__init__.py3000100%
operations.py12000100%
resources.py96736889%28->29, 29, 129->132, 135->138, 161->162, 162–163, 165->160, 168->169, 169, 173–175
schemas.py2902910489%113–114, 117, 122–123, 126–127, 130, 135–137, 140–141, 144, 251, 258–259, 334–336, 338–340, 342–343, 458–459, 466–467
resources/workspace/logs
__init__.py3000100%
operations.py5000100%
resources.py17550603763%66->68, 68, 97–98, 104, 106–108, 110, 113–114, 117, 120–121, 123–126, 128–129, 132–133, 135–136, 138–139, 141–143, 145–146, 148–152, 154–155, 157–159, 161–166, 179–180, 252–253
schemas.py22000100%
TOTAL15191262367189%

Test Execution Summary

TestsSkippedFailuresErrorsTime
3830 💤0 ❌0 🔥4.344s ⏱️

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Datata1