From 6032043dac3ff1131e14b0145ea6435c1ba0a313 Mon Sep 17 00:00:00 2001 From: Mahmoud Ahmed Date: Tue, 27 Oct 2026 15:12:07 +0300 Subject: [PATCH 1/2] Add the python-wrap logo and a branded docs theme and home page --- README.md | 7 ++- docs/assets/logo-dark.svg | 17 ++++++ docs/assets/logo-light.svg | 17 ++++++ docs/assets/logo-mark.svg | 14 +++++ docs/index.md | 112 ++++++++++++++++++++++++++++++++----- docs/stylesheets/extra.css | 84 ++++++++++++++++++++++++++++ mkdocs.yml | 32 +++++++++-- 7 files changed, 263 insertions(+), 20 deletions(-) create mode 100644 docs/assets/logo-dark.svg create mode 100644 docs/assets/logo-light.svg create mode 100644 docs/assets/logo-mark.svg create mode 100644 docs/stylesheets/extra.css diff --git a/README.md b/README.md index 8a1d77b..c024309 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@
-# python-wrap + + + + python-wrap + + **The resilience and concurrency toolkit you add to every Python project.**
Retry (with `Retry-After`), circuit breaker, timeout, cache, rate limit, bulkhead, batching, async helpers, metrics and test helpers. One import, sync and async, zero dependencies. diff --git a/docs/assets/logo-dark.svg b/docs/assets/logo-dark.svg new file mode 100644 index 0000000..cc6748b --- /dev/null +++ b/docs/assets/logo-dark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + python-wrap + diff --git a/docs/assets/logo-light.svg b/docs/assets/logo-light.svg new file mode 100644 index 0000000..add7084 --- /dev/null +++ b/docs/assets/logo-light.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + python-wrap + diff --git a/docs/assets/logo-mark.svg b/docs/assets/logo-mark.svg new file mode 100644 index 0000000..b1f87e3 --- /dev/null +++ b/docs/assets/logo-mark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md index 5fc0592..8a34724 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,27 @@ -# python-wrap +--- +title: python-wrap +hide: + - navigation + - toc +--- -**The resilience and concurrency toolkit you add to every project.** Retry (with `Retry-After`), circuit breaker, timeout, cache, rate limit, bulkhead, batching, async helpers, metrics and test helpers, in one typed, async-aware package with no dependencies. +
+ + + +# The toolkit you add to every Python project + +

Retry (with Retry-After), circuit breaker, timeout, cache, rate limit, +bulkhead, batching, async helpers, metrics and test helpers. One import, sync and async, +fully typed, zero dependencies.

+ +[Get started](#install){ .md-button .md-button--primary } +[Choose a decorator](choosing.md){ .md-button } +[View on GitHub](https://github.com/LitePy/PythonWrap){ .md-button } + +
+ +## One decorator, the whole policy ```python from python_wrap import CircuitBreaker, memoize, policy, retry, retry_after_header, timeout @@ -168,25 +189,86 @@ Not sure which decorator you need? Start with [Choose a decorator](choosing.md). ## Principles -**Zero dependencies.** `dependencies = []` is a feature: python-wrap is safe to add to any project, including libraries. +
+ +- :material-package-variant-closed:{ .lg .middle } __Zero dependencies__ + + --- + + `dependencies = []` is a feature: python-wrap is safe to add to any project, including libraries. + +- :material-language-python:{ .lg .middle } __Typed__ + + --- + + Ships `py.typed`, passes `mypy --strict`, and preserves signatures with `ParamSpec`, so your editor still knows what the decorated function takes and returns. + +- :material-lightning-bolt:{ .lg .middle } __Async is first class__ + + --- + + Async functions get `async def` wrappers, waits use `asyncio` instead of blocking the loop, and caches share concurrent awaits. See [Async support](async.md). + +- :material-text-box-search-outline:{ .lg .middle } __Never configures logging__ + + --- + + Importing python-wrap does not touch the root logger. Decorators log to the `python_wrap` logger, which has a `NullHandler`. See [Observability](guides/observability.md#configuring-logging). + +- :material-chart-line:{ .lg .middle } __Observable and testable__ + + --- -**Typed.** The package ships `py.typed`, passes `mypy --strict`, and preserves signatures with `ParamSpec`, so your editor still knows what the decorated function takes and returns. + `count_calls` and one [event hook](guides/metrics.md) cover retries, caches, circuits and limiters; [`python_wrap.testing`](testing.md) makes tests instant. Overhead numbers: [Performance](performance.md). -**Async is first class.** Async functions get `async def` wrappers (so `inspect.iscoroutinefunction` stays true), retries and rate limits wait with `asyncio` instead of blocking the loop, and caches de-duplicate concurrent awaits. See [Async support](async.md). +- :material-alert-octagon-outline:{ .lg .middle } __Failures are explicit__ -**It never configures logging.** Importing python-wrap does not touch the root logger. Decorators log to the `python_wrap` logger (or the `logger=` you give them), which carries a `NullHandler`, so nothing is printed until your application configures logging. See [Observability](guides/observability.md#configuring-logging). + --- -**Observable and testable.** `count_calls` and one [event hook](guides/metrics.md) cover retries, caches, circuits and limiters, and [`python_wrap.testing`](testing.md) makes tests instant and deterministic. See [Performance](performance.md) for overhead numbers. + Its own exceptions (`TimeoutExpired`, `RateLimitExceeded`, `CircuitOpenError`, `BulkheadFull`, ...) also subclass the closest built-in, so `except TimeoutError` keeps working. -**Failures are explicit.** The package raises its own exceptions (`TimeoutExpired`, `RateLimitExceeded`, `CircuitOpenError`, `BulkheadFull`, `BatchError`, `ValidationError`, `ArgumentTypeError`), each also a subclass of the closest built-in, so `except TimeoutError` and `except ValueError` keep working. +
-**Honest about limits.** Some of these tools cannot do everything (a thread timeout cannot stop the work, for example). They are documented on one page: [Limitations](limitations.md). +Some of these tools cannot do everything (a thread timeout cannot stop the work, for example). Every such limit is on one page: [Limitations](limitations.md). ## Where to go next -- [Choose a decorator](choosing.md): a table by problem. -- Guides: [Resilience](guides/resilience.md), [Caching](guides/caching.md), [Concurrency](guides/concurrency.md), [Async toolkit](guides/async-toolkit.md), [Batching and hedging](guides/batching.md), [Observability](guides/observability.md), [Metrics and events](guides/metrics.md), [Validation](guides/validation.md), [State safety](guides/state.md), [Policies](guides/policies.md), [Developer tools](guides/devtools.md), [Configuration](guides/configuration.md). -- [Cookbook](cookbook/index.md): complete recipes for HTTP clients, crawlers, FastAPI, Django, CLIs and tests. -- [Testing](testing.md) and [Performance](performance.md). -- [Stacking decorators](stacking.md): order matters. -- [Migrating from 0.1](migration.md): every breaking change, with before and after code. +
+ +- :material-compass-outline:{ .lg .middle } __[Choose a decorator](choosing.md)__ + + --- + + A table by problem: what to reach for, and what to combine it with. + +- :material-book-open-variant:{ .lg .middle } __Guides__ + + --- + + [Resilience](guides/resilience.md), [Caching](guides/caching.md), [Concurrency](guides/concurrency.md), [Async toolkit](guides/async-toolkit.md), [Batching](guides/batching.md), [Observability](guides/observability.md), [Metrics](guides/metrics.md), [Validation](guides/validation.md), [State](guides/state.md), [Policies](guides/policies.md), [Dev tools](guides/devtools.md), [Configuration](guides/configuration.md). + +- :material-chef-hat:{ .lg .middle } __[Cookbook](cookbook/index.md)__ + + --- + + Complete recipes for HTTP clients, crawlers, FastAPI, Django, CLIs and tests. + +- :material-test-tube:{ .lg .middle } __[Testing](testing.md)__ + + --- + + `fast_mode`, a fake clock, spies and `eventually`: instant, deterministic tests. + +- :material-layers-triple-outline:{ .lg .middle } __[Stacking decorators](stacking.md)__ + + --- + + Order matters. The recommended order, and why `policy(...)` applies it for you. + +- :material-swap-horizontal:{ .lg .middle } __[Migrating from 0.1](migration.md)__ + + --- + + Every breaking change, with before and after code. + +
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..0b2b607 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,84 @@ +/* Brand: the teal of the logo. */ +:root, +[data-md-color-scheme="default"] { + --md-primary-fg-color: #0f766e; + --md-primary-fg-color--light: #2dd4bf; + --md-primary-fg-color--dark: #115e59; + --md-accent-fg-color: #0d9488; + --md-typeset-a-color: #0f766e; +} + +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #0d9488; + --md-primary-fg-color--light: #5eead4; + --md-primary-fg-color--dark: #0f766e; + --md-accent-fg-color: #5eead4; + --md-typeset-a-color: #5eead4; + --md-default-bg-color: #0b1112; +} + +.md-header, +.md-tabs { + background: linear-gradient(90deg, #115e59, #0d9488); +} + +.md-typeset h1 { + color: var(--md-default-fg-color); + font-weight: 700; + letter-spacing: -0.02em; +} + +.md-typeset h2 { + font-weight: 650; + letter-spacing: -0.01em; +} + +/* ── home page ──────────────────────────────────────────────────────────── */ + +.pw-hero { + text-align: center; + padding: 1.5rem 0 0.5rem; +} + +.pw-hero .pw-logo { + width: 5rem; + height: 5rem; + margin-bottom: 0.5rem; +} + +.pw-hero h1 { + font-size: 2.6rem; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.15; + margin: 0 0 0.75rem; +} + +.pw-hero .pw-tagline { + font-size: 1.15rem; + color: var(--md-default-fg-color--light); + max-width: 40rem; + margin: 0 auto 1.5rem; +} + +.pw-hero .md-button { + margin: 0.25rem; +} + +@media screen and (max-width: 44.9375em) { + .pw-hero h1 { + font-size: 1.9rem; + } + + .pw-hero .pw-tagline { + font-size: 1rem; + } +} + +.md-typeset .grid.cards > ul > li { + border-radius: 0.6rem; +} + +.md-typeset .grid.cards > ul > li:hover { + border-color: var(--md-accent-fg-color); +} diff --git a/mkdocs.yml b/mkdocs.yml index 8e2d4fd..8cbf73a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,8 +14,13 @@ validation: theme: name: material + logo: assets/logo-mark.svg + favicon: assets/logo-mark.svg icon: repo: fontawesome/brands/github + font: + text: Inter + code: JetBrains Mono palette: - media: "(prefers-color-scheme)" toggle: @@ -23,19 +28,22 @@ theme: name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default - primary: indigo - accent: indigo + primary: custom + accent: custom toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: indigo - accent: indigo + primary: custom + accent: custom toggle: icon: material/weather-night name: Follow the system features: + - navigation.instant + - navigation.instant.progress + - navigation.tracking - navigation.tabs - navigation.sections - navigation.top @@ -44,8 +52,14 @@ theme: - toc.follow - search.suggest - search.highlight + - search.share - content.code.copy + - content.code.annotate - content.tabs.link + - content.action.edit + +extra_css: + - stylesheets/extra.css extra: social: @@ -72,15 +86,25 @@ plugins: merge_init_into_class: true markdown_extensions: + - abbr - admonition - attr_list + - def_list + - footnotes + - md_in_html - tables - toc: permalink: true - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: anchor_linenums: true + line_spans: __span + pygments_lang_class: true - pymdownx.inlinehilite + - pymdownx.keys - pymdownx.snippets: base_path: [.] check_paths: true From 91346a04b707397c20cf4cc60bad5e2b84adf67f Mon Sep 17 00:00:00 2001 From: Mahmoud Ahmed Date: Tue, 27 Oct 2026 15:14:13 +0300 Subject: [PATCH 2/2] Check audit redaction on the arguments, not the record repr --- tests/test_config.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 79c6d27..0d55201 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,7 +9,7 @@ from python_wrap import _core from python_wrap.config import configure, configured, get_config, reset_config from python_wrap.deadline import deadline -from python_wrap.logs import audit, log_args +from python_wrap.logs import AuditRecord, audit, log_args from python_wrap.retry import retry, retry_stats, retrying @@ -202,8 +202,10 @@ def f(ssn: str, password: str) -> None: ... def g(ssn: str, password: str) -> None: ... g("123", "pw") - text = repr(records[0]) - assert "123" not in text and "pw" in text + record = records[0] + assert isinstance(record, AuditRecord) + # Only the arguments: the timestamp or duration can contain "123" by chance. + assert record.arguments == {"ssn": "***", "password": "pw"} def test_redact_extra_adds_to_defaults_everywhere(caplog: pytest.LogCaptureFixture) -> None: @@ -222,9 +224,9 @@ def f(ssn: str, password: str, name: str) -> None: ... def g(ssn: str, token: str, name: str, other: str) -> None: ... g("1", "s3cr3t", "bob", "keep") - text = repr(records[0]) - assert "bob" not in text and "s3cr3t" not in text and "'1'" not in text - assert "keep" in text + record = records[0] + assert isinstance(record, AuditRecord) + assert record.arguments == {"ssn": "***", "token": "***", "name": "***", "other": "keep"} def test_redact_extra_with_explicit_redact(caplog: pytest.LogCaptureFixture) -> None: