Skip to content

5.10.0 — 2026-10-12

← 5.11.0 | 5.9.0 → | ↑ 5.x series

Added

Helpers - Decorators

  • Suffix-less decorator names - retry, timeout, timing, singleton, ttl_cache, grpc_rate_limit and sqlalchemy_atomic, plus postgres_sqlalchemy_atomic, mysql_sqlalchemy_atomic, sqlite_sqlalchemy_atomic and starrocks_sqlalchemy_atomic, are now exported from archipy.helpers.decorators.
    • The old *_decorator names, the async_* names and the old module paths keep working and emit a DeprecationWarning that names the replacement
    • Implementations moved to modules that do not clash with the new function names (for example retry_policy.py and sqlalchemy_transaction.py); see Deprecated names
  • Automatic async detection - retry, timeout, timing, ttl_cache, measure_duration, count_calls, trace_span, trace_root and the SQLAlchemy atomic decorators accept async def functions, so the separate async_* variants are no longer needed.
  • Retry backoff and jitter - retry takes backoff and jitter, and validates max_retries, delay, backoff and jitter when the decorator is applied.

Configs - Email

  • EMAIL.CONNECTION_REFRESH_SECONDS - Replaces the hard-coded TOKEN_REFRESH_SECONDS constant; controls how long an SMTP connection is reused before it is reconnected.

Configs - Keycloak

  • KEYCLOAK.TOKEN_LEEWAY_SECONDS - Leeway in seconds applied when decoding tokens locally (default 0).

Changed

Adapters - Email

  • BREAKING: send failures raise UnavailableError - After the retries are used up, a failed send now raises UnavailableError instead of being swallowed.
    • The broken SMTP session is closed before each retry, and the message is sent in the same attempt after reconnecting
    • Oversized attachments map to InvalidArgumentError
    • STARTTLS is negotiated only when the server advertises it, so plain SMTP relays work

Adapters - Keycloak

  • BREAKING: expired tokens raise TokenExpiredError - Local decoding no longer applies python-keycloak's implicit 60 second leeway, so a recently expired token fails with TokenExpiredError instead of surfacing as INVALID_CREDENTIALS from UserInfo.
    • Set KEYCLOAK.TOKEN_LEEWAY_SECONDS to restore a grace period
    • Role checks read UserInfo, which needs the "Add to userinfo" mapper option in Keycloak

Adapters - Saman IPG

  • BREAKING: HTTP 5xx maps to UnavailableError - Server errors from the gateway are no longer reported as generic failures. TERMINAL_ID is validated as numeric.

Helpers - Decorators

  • timeout for async and threads - Async functions are bounded with asyncio.timeout. A synchronous function called outside the main thread raises FailedPreconditionError instead of running without a limit. Float seconds are supported and the previous SIGALRM handler and timer are restored.
  • retry semantics - max_retries is documented as the total number of attempts, and max_retries=0 now raises InvalidArgumentError when the decorator is applied instead of never calling the function.
  • singleton returns the class - isinstance checks and subclassing keep working on the decorated name.
  • ttl_cache thread safety - Cache access is guarded by a lock, keys include argument types, calls with unhashable arguments bypass the cache, and same-named cached methods in a class hierarchy no longer collide.
  • timing logging - Logs at DEBUG or lower, also when the function raises.

Helpers - OpenTelemetry

  • Cheaper metric decorators - measure_duration and count_calls read the config once per call and resolve instrument names when the decorator is applied.

Fixed

Helpers - Decorators

  • Atomic blocks and cancellation - CancelledError, KeyboardInterrupt and SystemExit pass through sqlalchemy_atomic unchanged instead of becoming InternalError, and a failing rollback no longer hides the original error.
    • The atomic marker is cleared when the outermost block exits
    • MySQL error codes map to deadlock, timeout and constraint errors
    • @sqlalchemy_atomic_decorator(db_type=...) with parentheses no longer fails with got multiple values for argument 'db_type'
  • Telemetry failures - An exporter error while recording a metric no longer replaces the function result, and interrupts are recorded with status cancelled.
  • trace_span and trace_class - Nested classes are left untouched, and capture_args lets the function raise its own error when the call arguments cannot be bound.
  • Retry exhaustion - The last exception is chained as the cause of ResourceExhaustedError.

Helpers - gRPC

  • rpc.server.active_requests on cancelled streams - The recorder's finish is idempotent and runs when a sync streaming RPC terminates, so cancelled or never-iterated streams no longer leave the counter incremented.

Adapters - Keycloak

  • Expired token handling - See the TokenExpiredError change above.

Tests

  • BDD coverage - Added scenarios for the email, Parsian IPG and Saman IPG adapters, the decorators, the Singleton metaclass, Keycloak auth dependencies and the gRPC helpers.
  • Real containers - The email feature runs against Mailpit containers (STARTTLS and plain SMTP), and the KeycloakUtils and AuthContextManager scenarios use real Keycloak tokens.
  • Merged features - Retry, timeout, timing and deprecation scenarios live in decorators.feature, and the gRPC rate-limit identifier and client interceptor scenarios in grpc_helpers.feature.

Chore

  • Reviewer nits - Fixed a pyproject.toml comment typo, added reasons to ty ignores and broad except blocks, used DatetimeUtils in the email adapter, and gave the TOTP secret-generation error an operation detail.

Dependencies

  • Raised lower bounds - pydantic>=2.14.1 and jdatetime>=6.1.1, matching uv.lock.