From eecd04191c7e58093024fc61a89661943ece9d7c Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 31 May 2026 23:37:25 +0300 Subject: [PATCH 1/5] docs(release): design for 0.1.0 release prep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decisions: - Delete CHANGELOG.md (modern-di convention; release notes on GitHub Releases). - Add .github/workflows/publish.yml mirroring modern-di's release flow. - Trim README.md to ship only what's actually shipped (drop the Retry/ RetryBudget/Bulkhead/Timeout/Observability/OTel bullets until those Epics land). Status banner explicitly names the unshipped categories. - pyproject.toml version stays "0"; publish recipe overrides with `uv version $GITHUB_REF_NAME`. - Tag format: bare 0.1.0 (no v prefix), matching modern-di. Out of scope: production code changes, mkdocs site (Epic 6), Trusted Publishers / Sigstore release flow (Story 6-5). Includes the manual post-merge sequence (PyPI name verification, secret check, GitHub Release create) — those execute by the maintainer after the prep PR merges. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-31-release-0.1.0-prep-design.md | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-31-release-0.1.0-prep-design.md diff --git a/docs/superpowers/specs/2026-05-31-release-0.1.0-prep-design.md b/docs/superpowers/specs/2026-05-31-release-0.1.0-prep-design.md new file mode 100644 index 0000000..bd95490 --- /dev/null +++ b/docs/superpowers/specs/2026-05-31-release-0.1.0-prep-design.md @@ -0,0 +1,261 @@ +# Release 0.1.0 prep (design) + +- **Date:** 2026-05-31 +- **Status:** approved, ready for plan +- **Scope:** Prep the repository for the first PyPI release (`0.1.0` alpha). Single PR delivers all repo-side edits; the GitHub Release that triggers the publish workflow is a separate manual action by the maintainer. Out of scope: writing user docs site (Epic 6), Trusted Publishers / Sigstore release flow (Story 6-5), feature work toward v1.0. +- **Why now:** Epic 1 is complete (PRs #1–#13). The middleware foundation (2-1/2-2/2-3) is shipped. `AsyncClient` is the working public surface. Users can do meaningful work with the current API; further iteration benefits from real consumer feedback. Modern-di-style alpha release model: tag now, iterate, no CHANGELOG file (release notes live on GitHub Releases). + +## Decisions + +| Decision | Choice | +| --- | --- | +| Release model | Alpha. `0.1.0` declares the API in flux until v1.0; minor releases may break things. Matches modern-di's pre-1.0 posture. | +| CHANGELOG.md | **Delete.** modern-di in the same org doesn't have one. Release notes live on GitHub Releases (one entry per tag). | +| README.md | Strict trim: only document what actually ships. Remove the resilience-middleware / observability / OTel bullets. Add an explicit "what's not yet shipped" line under the status banner. | +| pyproject.toml | Untouched. `version = "0"` stays as the placeholder; the publish recipe overrides via `uv version $GITHUB_REF_NAME`. | +| Tag format | Bare `0.1.0` (no `v` prefix). Matches modern-di. | +| Publish workflow | Add `.github/workflows/publish.yml` mirroring modern-di's. Triggered by `release: published`. Runs `just publish`. | +| Justfile | Untouched. `just publish` already does `rm -rf dist && uv version $GITHUB_REF_NAME && uv build && uv publish --token $PYPI_TOKEN`. | +| PyPI name | Must verify ownership of `httpware` on PyPI before tagging. The package URL returns HTTP 200; check via `pypi.org/pypi/httpware/json` whether the owner is `modern-python` (our project) or a third party. | +| Engineering.md roadmap | Unchanged for this PR. Adding "Released" markers is a tiny follow-up commit (not in scope here). | +| Out-of-scope feature work | None added in this PR. The repo ships what's already on `main` at `204d463`. | + +## What ships in 0.1.0 — public surface + +| Symbol | Import | +| --- | --- | +| `AsyncClient` | `from httpware import AsyncClient` | +| `Request`, `Response`, `StreamResponse` | `from httpware import Request, Response, StreamResponse` | +| `Limits`, `Timeout`, `ClientConfig` | `from httpware import Limits, Timeout, ClientConfig` | +| `Transport`, `Httpx2Transport`, `RecordedTransport` | `from httpware import …` | +| `ResponseDecoder`, `PydanticDecoder` | `from httpware import ResponseDecoder, PydanticDecoder` | +| `MsgspecDecoder` | `from httpware.decoders.msgspec import MsgspecDecoder` (gated by `[msgspec]` extra) | +| `Middleware`, `Next` | `from httpware import Middleware, Next` | +| `before_request`, `after_response`, `on_error` | `from httpware import …` | +| Exception hierarchy | `from httpware import StatusError, TransportError, TimeoutError, ClientError, ServerStatusError, ClientStatusError, STATUS_TO_EXCEPTION, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, UnprocessableEntityError, RateLimitedError, InternalServerError, ServiceUnavailableError` | + +Behind the seam: `httpware._internal.chain.compose`, `httpware._internal.import_checker`. + +## What 0.1.0 does NOT ship + +- `auth=` parameter on `AsyncClient` (Story 2-4). +- `data=` / `files=` body params on HTTP methods. +- Transport reference-counting on `with_options` views. +- Retry / timeout / bulkhead / RetryBudget middleware (Epic 3). +- Streaming (`AsyncClient.stream`) — `StreamResponse` is a placeholder stub (Epic 4). +- Observability hooks / OpenTelemetry middleware (Epic 5). +- `niquests` transport (declared as an extra but not yet implemented). +- User documentation site (mkdocs / readthedocs — Epic 6). +- Migration guide from `community-of-python/base-client` (Story 6-1). +- Public benchmark suite (Story 6-3). +- CI enforcement gates beyond what's currently in `ci.yml` (Story 6-4). +- Trusted Publishers + Sigstore release flow (Story 6-5; we use the simpler token-based publish for now). +- CHANGELOG.md. + +## Edits in the prep PR + +**Deleted files:** +- `CHANGELOG.md` — release notes live on GitHub Releases going forward. + +**New files:** +- `.github/workflows/publish.yml` — mirrors modern-di. Triggered by `release: published`, runs `just publish`. Reads `PYPI_TOKEN` from repo secrets. + +**Modified files:** +- `README.md` — strict trim per Section 2 of the brainstorm. Remove the "Highlights" bullets for unshipped features. Update the status banner to name the unshipped categories (resilience middleware, streaming, observability). Drop the CHANGELOG link. +- `CONTRIBUTING.md` — check for and remove any reference to `CHANGELOG.md`. If the existing CONTRIBUTING doesn't mention it, no change needed. +- `CLAUDE.md` — same check; remove `CHANGELOG.md` references if present. + +**Files NOT touched:** +- `pyproject.toml` — `version = "0"` stays; publish recipe overrides. +- `Justfile` — `publish` recipe already correct. +- `.github/workflows/ci.yml` — no changes to test/lint matrix. +- `src/httpware/**` — no code changes. +- `tests/**` — no test changes. +- `LICENSE`, `SECURITY.md` — unchanged. +- `docs/engineering.md` — internal roadmap stays; "Released" markers are a follow-up. +- `docs/archive/**` — bmad archive untouched. + +## `.github/workflows/publish.yml` content + +```yaml +name: Publish Package + +on: + release: + types: + - published + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + - run: just publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} +``` + +Byte-for-byte identical to modern-di's `publish.yml`. No customization needed. + +## README.md content (final) + +```markdown +# httpware + +[![Test](https://github.com/modern-python/httpware/actions/workflows/ci.yml/badge.svg)](https://github.com/modern-python/httpware/actions/workflows/ci.yml) +[![PyPI version](https://badge.fury.io/py/httpware.svg)](https://pypi.org/project/httpware/) +[![Python versions](https://img.shields.io/pypi/pyversions/httpware.svg)](https://pypi.org/project/httpware/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +**Async HTTP client framework for Python.** + +`httpware` is a typed, async HTTP client library built on `httpx2` with a protocol-based seam so the transport is swappable. Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces respx for transport-level tests. + +> **Status:** Pre-1.0 (0.1.0 alpha). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped — track progress on GitHub. + +## Install + +\`\`\`bash +pip install httpware +\`\`\` + +Optional extras: + +\`\`\`bash +pip install httpware[msgspec] # MsgspecDecoder +\`\`\` + +(`otel`, `niquests`, and `all` extras are declared but their integrations have not shipped yet.) + +## Quickstart + +\`\`\`python +from httpware import AsyncClient +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name: str + + +async def main() -> None: + async with AsyncClient(base_url="https://api.example.com") as client: + user = await client.get("/users/1", response_model=User) + print(user.name) +\`\`\` + +## What ships in 0.1.0 + +- **`AsyncClient`** — eight HTTP method shortcuts (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `request`) with typed `response_model` overloads; per-call overrides for `headers`, `params`, `cookies`, `timeout`, `json`, `content`; httpx-style `base_url` join; `with_options(...)` returns a view sharing the same transport. +- **Transport-agnostic seam.** `httpx2` is confined to `httpware.transports.httpx2.Httpx2Transport`. Implement the `Transport` protocol to swap backends. +- **Middleware foundation.** `Middleware` protocol, `Next` type alias, recursive-closure `compose()` chain composition, and phase decorators (`@before_request`, `@after_response`, `@on_error`). +- **Pluggable response decoding.** `PydanticDecoder` (default) with cached `TypeAdapter`; `MsgspecDecoder` via `httpware[msgspec]`. +- **`RecordedTransport`** — built-in test double with a route table, observed-request list, and `aclose_calls` counter. +- **Status-keyed exception hierarchy** — `StatusError`, 4xx / 5xx subclasses, plain typed fields (`status: int`, `body: bytes`, `headers`, `json`, `request_method`, `request_url`). Pickleable; userinfo redacted in `__repr__`. +- **No `httpx2` exception types** leak through `httpware`. The transport seam maps them to `httpware` exceptions. + +## Part of `modern-python` + +Browse the full list of templates and libraries in [`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index. + +## License + +MIT — see [LICENSE](./LICENSE). +``` + +The triple-backticks above use backslash-escapes in this spec because the spec itself is markdown; in the actual `README.md` they're real triple-backticks. + +## Release execution (post-merge, manual maintainer actions) + +These steps are NOT part of the PR. They run after the prep PR merges to `main`. + +1. **Verify PyPI name ownership:** + + ```bash + curl -s https://pypi.org/pypi/httpware/json \ + | python -c "import sys, json; d=json.load(sys.stdin); info=d.get('info', {}); print('home:', info.get('home_page')); print('project_urls:', info.get('project_urls')); print('author:', info.get('author'))" + ``` + + Decision tree: + - Homepage / project_urls point to `github.com/modern-python/httpware` → name is ours, proceed. + - Different owner → STOP. Rename or open a PyPI name-transfer request. Don't tag. + - Returns 404 → name is free, proceed. + +2. **Verify `PYPI_TOKEN` secret exists on the repo:** + + ```bash + gh secret list --repo modern-python/httpware | grep PYPI_TOKEN + ``` + + If absent, add it via `gh secret set PYPI_TOKEN --repo modern-python/httpware` with a PyPI account token scoped to the `httpware` project. + +3. **Create the GitHub Release (triggers publish workflow):** + + ```bash + gh release create 0.1.0 \ + --title "0.1.0 — initial alpha" \ + --notes-file - <<'EOF' + Initial public alpha of `httpware`. + + **Includes:** + - AsyncClient with 8 HTTP method shortcuts and typed `response_model` overloads + - Transport-agnostic seam (`httpx2` confined to `Httpx2Transport`) + - Middleware foundation: `Middleware` protocol, `compose()`, phase decorators + - Pluggable response decoders: `PydanticDecoder` (default), `MsgspecDecoder` via `[msgspec]` extra + - `RecordedTransport` test double + - Status-keyed exception hierarchy + + **Not yet shipped (next releases):** + - Resilience middleware (retry, timeout, bulkhead) — Epic 3 + - Streaming (`AsyncClient.stream`) — Epic 4 + - Observability / OpenTelemetry — Epic 5 + - `auth=` parameter on AsyncClient — Story 2-4 + - `data=` / `files=` body params + + Public API is subject to change between minor releases until v1.0. + EOF + ``` + + The release publication triggers `publish.yml` → `just publish` → `uv version 0.1.0 && uv build && uv publish --token`. The workflow's status appears in the Actions tab. + +4. **Smoke-check the published package:** + + ```bash + # In a fresh venv: + pip install httpware==0.1.0 + python -c "import httpware; from httpware import AsyncClient, Middleware, RecordedTransport; print('OK:', httpware.__file__)" + ``` + + Expected: `OK: …/site-packages/httpware/__init__.py`. If import fails or names are missing, a `0.1.1` patch follows the same release cycle. + +## Constraints and invariants + +- **No production code changes.** This PR ships only documentation and CI workflow edits. +- **No `httpx2` import added.** `tests/test_no_httpx2_leakage.py` keeps passing. +- **`just lint-ci` keeps passing.** ruff, ty, eof-fixer all clean. +- **`just test` keeps passing.** 273 tests at 100% line coverage on the source. + +## Risks and mitigations + +| Risk | Mitigation | +| --- | --- | +| PyPI name `httpware` is owned by someone else. | The maintainer verifies via the PyPI JSON API before tagging. If owned, stop and rename or transfer. No code-level change anticipated for this case. | +| `PYPI_TOKEN` secret missing. | `gh secret list` check before tagging; add via `gh secret set` if absent. | +| The publish workflow fails mid-build (e.g., `uv build` error on a CI runner that differs from local). | The tag stays; the workflow can be re-run from the Actions tab. If a content change is needed, a `0.1.1` follow-up tag publishes the fix. The repo doesn't accumulate state from a failed publish. | +| Users install 0.1.0 and try to use `Retry` / `Timeout` / OTel middleware (which aren't shipped). | README's status banner names the unshipped categories explicitly. Each feature's import would fail loudly (`ImportError`), not silently. | +| Documentation site URL (`https://httpware.readthedocs.io`) referenced in pyproject.toml doesn't resolve. | Keep the URL as a forward-pointer (matches modern-di which has the same pattern). Users see the broken link; not a release blocker. Epic 6's mkdocs site lights it up later. | +| Renaming the package later (if PyPI name is contested) is disruptive. | The maintainer verifies name ownership BEFORE any consumer can depend on the published name. The 0.1.0 wheel never lands on PyPI unless the name is ours. | + +## Definition of done + +- `CHANGELOG.md` deleted. +- `.github/workflows/publish.yml` exists with the modern-di-pattern content. +- `README.md` updated to the trimmed content from Section 2 of the brainstorm / the README block above. +- `CONTRIBUTING.md` and `CLAUDE.md` checked for `CHANGELOG.md` references; updated if any were present. +- `just test` and `just lint-ci` clean. +- `tests/test_no_httpx2_leakage.py` still passes. +- PR `chore/release-0.1.0-prep` lands on `main` via merge. +- Post-merge actions (PyPI name check, secret verification, GitHub Release creation) execute successfully; `0.1.0` lands on PyPI; `pip install httpware==0.1.0` works in a clean venv. From b125f5308ef57472d939bda4ef81b045751c5ff9 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 31 May 2026 23:44:37 +0300 Subject: [PATCH 2/5] chore(release): delete CHANGELOG.md; remove contributing-guide reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit modern-di in the same org doesn't ship a CHANGELOG.md — release notes live on GitHub Releases (one entry per tag). This commit removes the file and the one CONTRIBUTING.md bullet that asked contributors to update an `Unreleased` section. README.md still has a stale CHANGELOG link; the README is fully rewritten in a follow-up commit on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 29 ----------------------------- CONTRIBUTING.md | 3 +-- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 5cd4f54..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,29 +0,0 @@ -# Changelog - -All notable changes to `httpware` will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added - -- Initial project scaffold: `src/httpware/` package, `py.typed` marker, `pyproject.toml` with `uv_build` backend. -- Org conventions ported from `modern-python/modern-di`: `Justfile`, `.github/workflows/ci.yml`, `[tool.ruff]` config, `[tool.pytest.ini_options]`, dev and lint dep groups. -- Declared dependencies: `httpx2>=2.0.0,<3.0`, `pydantic>=2.0,<3.0`. -- Declared install extras: `[msgspec]`, `[otel]`, `[niquests]`, `[all]`. -- `SECURITY.md` with 90-day private-disclosure window. -- `CONTRIBUTING.md` with development workflow. -- `CLAUDE.md` with AI-agent guidance. -- Core data types: `Request`, `Response`, `Limits`, `Timeout`, `ClientConfig` — frozen+slotted dataclasses with `with_*` immutability helpers on `Request` and computed `text`/`json()` accessors on `Response` (Story 1.2). -- Status-keyed exception hierarchy with plain typed fields: `ClientError`, `TransportError`, `TimeoutError`, `StatusError`, `ClientStatusError`/`ServerStatusError` bases, 9 leaf classes (`BadRequestError` … `ServiceUnavailableError`), `STATUS_TO_EXCEPTION` lookup dict (Story 1.3). `StatusError` is picklable and deep-copyable via custom `__reduce__`; `__repr__` and the summary message strip `user:pass@` userinfo from the request URL; `headers` is stored as a read-only `MappingProxyType` so caller mutations after `raise` do not bleed into the exception. `TimeoutError` multi-inherits from `builtins.TimeoutError` (revisits architecture Decision 3) so `except builtins.TimeoutError` (the form `asyncio.wait_for` raises) also catches httpware-raised timeouts. -- `Transport` protocol (`@runtime_checkable`) and default `Httpx2Transport` adapter; `StreamResponse` placeholder for Story 4.1 protocol typing; the wire `method` is uppercased at the seam and `httpx2` exceptions (`TimeoutException`, `HTTPError`, `InvalidURL`, `CookieConflict`, and the closed-client `RuntimeError`) are mapped to `httpware.TimeoutError` / `httpware.TransportError` (with the original exception's message preserved on the mapped instance) so no `httpx2` exception escapes the library; lazy `httpx2.AsyncClient` construction is guarded by an `asyncio.Lock` so concurrent first-calls share one client; `httpx2` is confined to `src/httpware/transports/httpx2.py` (Story 1.4). -- `ResponseDecoder` protocol (`@runtime_checkable`) and default `PydanticDecoder` adapter — single-parse-pass JSON decoding via `pydantic.TypeAdapter.validate_json(bytes)`; a module-level `@functools.lru_cache(maxsize=None)` factory (`_get_adapter`) memoizes one `TypeAdapter` per `response_model` across the process so warm-path requests pay zero adapter-construction cost; `pydantic.ValidationError` surfaces unchanged to the caller (Story 1.5). -- `Middleware` protocol (`@runtime_checkable`) and `Next` callable type alias (`Callable[[Request], Awaitable[Response]]`); private `compose(middlewares, transport)` chain composer at `httpware._internal.chain` using a recursive closure fold with `transport.__call__` as the bottom of the chain. No exception handling inside `compose`, so `asyncio.CancelledError` and user-raised exceptions propagate untouched (Story 2.1). -- Phase-shortcut decorators `@before_request`, `@after_response`, `@on_error` for lifecycle hooks without authoring a full `Middleware` class. `@on_error` catches `Exception` only (so `asyncio.CancelledError` propagates); its handler may return a `Response` to recover or `None` to re-raise (Story 2.2). -- Request and Response immutability helper expansion: `Request.with_headers`, `with_cookie`, `with_cookies`, `with_extension`, `with_extensions`; `Response.with_headers`, `with_status`. Plural helpers merge mappings (incoming keys override existing); singular helpers add or replace a single entry. No validation, no header-key normalization — matches the existing `with_header` semantics from Story 1.2 (Story 2.3). -- `MsgspecDecoder` opt-in `ResponseDecoder` adapter behind the `[msgspec]` extra at `httpware.decoders.msgspec`; `msgspec.json.decode(content, type=model)` in a single C-level parse pass. Accepts `msgspec.Struct`, dataclasses, attrs, NamedTuples, TypedDicts, and builtin/container types as `model` (pydantic models use `PydanticDecoder` instead). `msgspec.ValidationError` and `msgspec.DecodeError` propagate unchanged. Module import is safe without the extra (gated by `httpware._internal.import_checker.is_msgspec_installed`); only `MsgspecDecoder()` construction raises `ImportError` with an install hint when the extra is missing. `import httpware` does NOT eagerly load `msgspec` — `MsgspecDecoder` is reachable only via `from httpware.decoders.msgspec import MsgspecDecoder` (Story 1.6). -- `AsyncClient` — the v0.1.0 public surface. Construct with keyword-only `base_url`, `default_headers`, `default_query`, `timeout` (accepts `Timeout` instance, float seconds, or `None`), `limits`, `transport` (defaults to `Httpx2Transport`), `decoder` (defaults to `PydanticDecoder`), and `middleware` (`Sequence[Middleware]`, composed via `httpware._internal.chain.compose` at construction). Eight HTTP method shortcuts (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `request`) with `@typing.overload`-based `response_model` typing — passing `response_model=type[T]` returns `T`, otherwise `Response`. Per-call overrides for `headers`, `params`, `cookies`, `timeout`; body params `json` (auto-encoded with `Content-Type: application/json`, typed as `JsonValue` recursive alias) and `content` (raw bytes; mutually exclusive). `base_url` joins with the path using an httpx-style prefix; absolute URLs (`http(s)://`) bypass. `from_url(base_url, **kwargs)` classmethod factory. Async context-manager lifecycle: the original client owns the transport and closes it on `__aexit__`; views returned by `with_options(**overrides)` share the transport and are no-ops on close. `with_options` accepts a keyword allowlist (`base_url`, `default_headers`, `default_query`, `timeout`, `decoder`, `middleware`); `limits` and `transport` are not overridable. Out of scope and deferred: `auth=` (Story 2.4), `data=`/`files=` body params, transport reference-counting, streaming (Epic 4), observability (Epic 5) (Story 1.7). -- `RecordedTransport` built-in `Transport` test double at `httpware.transports.recorded` (also re-exported as `httpware.RecordedTransport`). Construct with `routes: Mapping[(method, url), Response | BaseException]` and a configurable `default` for the no-match case (`None` → `RuntimeError("No route for METHOD URL")` per archive AC; `Response` → returned; `BaseException` → raised). Method names are uppercased on insert and lookup. Routes fire indefinitely on repeat matches. Exposes `transport.requests: list[Request]`, `transport.last_request` (property), and `transport.aclose_calls: int` for assertion patterns. `add_route(method, url, response_or_exception)` allows incremental setup. `stream()` raises `NotImplementedError` — streaming lands in Epic 4 (Story 4-1). Replaces the five in-tree test stubs (`_FakeTransport`, `_OkTransport`, `_FailingTransport`, two `_RecordingTransport` variants, `_TrackingTransport`) accumulated through Stories 2-1 and 1-7 (Story 1.8). - -[Unreleased]: https://github.com/modern-python/httpware/commits/main diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fddc62..f3c5b55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,8 +18,7 @@ just test # pytest with coverage 2. **Branch from `main`**, use a descriptive name (`feat/retry-budget-jitter`, `fix/transport-cancel-leak`). 3. **Run `just lint` and `just test`** locally before pushing. CI will reject changes that fail either. 4. **Add tests** for any code change. Property-based tests (via Hypothesis) are required for concurrency-sensitive code (retry budget, bulkhead, retry interleaving). -5. **Update `CHANGELOG.md`** in the `Unreleased` section. -6. **Open a pull request** against `main`. PR titles use conventional-commits style (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`). +5. **Open a pull request** against `main`. PR titles use conventional-commits style (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`). ## Code style From 729660ca82969a919bc1ca8a152b3b463cdcbcaf Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 31 May 2026 23:45:52 +0300 Subject: [PATCH 3/5] ci(release): add publish.yml workflow for PyPI release on tag Mirrors the modern-di publish workflow byte-for-byte. Triggered when a GitHub Release is published; runs the existing `just publish` recipe which extracts the version from $GITHUB_REF_NAME, builds, and uploads to PyPI with the PYPI_TOKEN repo secret. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/publish.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b637272 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,17 @@ +name: Publish Package + +on: + release: + types: + - published + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + - run: just publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} From 080d08857dab85ebee471dc42b97711aa29ecdde Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 31 May 2026 23:48:08 +0300 Subject: [PATCH 4/5] docs(release): trim README to what 0.1.0 actually ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the "Highlights" bullets for resilience middleware (Retry, RetryBudget, Bulkhead, Timeout), the observability paragraph, and the "first-class OpenTelemetry" line — none of those Epics have shipped. Adds an explicit unshipped-categories list to the status banner so users know what's missing before they install. Replaces the bare CHANGELOG link with nothing (the file is gone; release notes live on GitHub Releases). The "What ships in 0.1.0" section enumerates the shipped public surface: AsyncClient, transport-agnostic seam, middleware foundation, PydanticDecoder + MsgspecDecoder, RecordedTransport, status-keyed exception hierarchy. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5deb381..7b1db91 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ [![Python versions](https://img.shields.io/pypi/pyversions/httpware.svg)](https://pypi.org/project/httpware/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**Resilience-first async HTTP client framework for Python.** +**Async HTTP client framework for Python.** -`httpware` is to Python what Polly is to .NET and resilience4j is to the JVM — a canonical resilience-first HTTP framework. The public API is transport-agnostic (the underlying client is `httpx2` by default, sitting behind a swappable `Transport` protocol). Retries, timeouts, bulkheads, and a Finagle-style **retry budget** ship as composable middleware. Tests use a `RecordedTransport` and never see the underlying client. +`httpware` is a typed, async HTTP client library built on `httpx2` with a protocol-based seam so the transport is swappable. Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces respx for transport-level tests. -> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0. See [CHANGELOG.md](./CHANGELOG.md). +> **Status:** Pre-1.0 (0.1.0 alpha). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped — track progress on GitHub. ## Install @@ -20,12 +20,11 @@ pip install httpware Optional extras: ```bash -pip install httpware[msgspec] # msgspec ResponseDecoder -pip install httpware[otel] # OpenTelemetry instrumentation -pip install httpware[niquests] # niquests transport -pip install httpware[all] # all of the above +pip install httpware[msgspec] # MsgspecDecoder ``` +(`otel`, `niquests`, and `all` extras are declared but their integrations have not shipped yet.) + ## Quickstart ```python @@ -44,25 +43,19 @@ async def main() -> None: print(user.name) ``` -## Highlights - -- **Transport-agnostic API.** No `httpx2` symbols leak through `httpware`. Swap to a different backend with one constructor argument. -- **Onion middleware** with phase shortcuts (`@before_request`, `@after_response`, `@on_error`). Built-in middleware: `Retry`, `RetryBudget`, `Bulkhead`, `Timeout`, `Observability`. -- **Retry budget by default** — token-bucket admission control (Finagle defaults). Caps retry storms before they happen. -- **Pluggable validation.** Default pydantic decoder with cached `TypeAdapter`; msgspec decoder via extras; bring your own. -- **`RecordedTransport` for tests.** A 3-line fixture replaces respx routes and transport-level mocking. -- **Status-keyed exceptions** with plain fields (`status: int`, `body: bytes`, `headers`, `json`). No transport exception types in user code. -- **First-class OpenTelemetry** instrumentation via `httpware[otel]`. - -## Documentation +## What ships in 0.1.0 -Full docs (in progress): https://httpware.readthedocs.io +- **`AsyncClient`** — eight HTTP method shortcuts (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `request`) with typed `response_model` overloads; per-call overrides for `headers`, `params`, `cookies`, `timeout`, `json`, `content`; httpx-style `base_url` join; `with_options(...)` returns a view sharing the same transport. +- **Transport-agnostic seam.** `httpx2` is confined to `httpware.transports.httpx2.Httpx2Transport`. Implement the `Transport` protocol to swap backends. +- **Middleware foundation.** `Middleware` protocol, `Next` type alias, recursive-closure `compose()` chain composition, and phase decorators (`@before_request`, `@after_response`, `@on_error`). +- **Pluggable response decoding.** `PydanticDecoder` (default) with cached `TypeAdapter`; `MsgspecDecoder` via `httpware[msgspec]`. +- **`RecordedTransport`** — built-in test double with a route table, observed-request list, and `aclose_calls` counter. +- **Status-keyed exception hierarchy** — `StatusError`, 4xx / 5xx subclasses, plain typed fields (`status: int`, `body: bytes`, `headers`, `json`, `request_method`, `request_url`). Pickleable; userinfo redacted in `__repr__`. +- **No `httpx2` exception types** leak through `httpware`. The transport seam maps them to `httpware` exceptions. ## Part of `modern-python` -Browse the full list of templates and libraries in -[`modern-python`](https://github.com/modern-python) — see the org profile for the -categorized index. +Browse the full list of templates and libraries in [`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index. ## License From e9c5c970b9d9f25c3263c54ad12bbc8943c009b2 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 31 May 2026 23:48:58 +0300 Subject: [PATCH 5/5] docs(release): implementation plan for 0.1.0 release prep Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-31-release-0.1.0-prep-plan.md | 497 ++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md diff --git a/docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md b/docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md new file mode 100644 index 0000000..b79621d --- /dev/null +++ b/docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md @@ -0,0 +1,497 @@ +# Release 0.1.0 prep Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Land a single PR (`chore/release-0.1.0-prep`) that deletes `CHANGELOG.md`, adds `.github/workflows/publish.yml`, and trims `README.md` to ship only what's actually in 0.1.0. After merge, a manual maintainer-driven sequence verifies PyPI name ownership and the `PYPI_TOKEN` secret, then creates the GitHub Release that triggers the publish workflow. + +**Architecture:** Documentation and CI-config-only change. No production code touched; no test changes. The repo at `main` (currently `204d463`) is the release artifact; this PR just removes the misleading CHANGELOG.md, adds the publish workflow, and trims the README. + +**Tech Stack:** GitHub Actions, `uv publish`, PyPI. No new runtime deps. + +**Branch:** `chore/release-0.1.0-prep` (already created; spec commit `eecd041` is on it). + +**Spec:** `docs/superpowers/specs/2026-05-31-release-0.1.0-prep-design.md`. + +--- + +## File Structure + +**Deleted files:** +- `CHANGELOG.md` — release notes live on GitHub Releases going forward (modern-di convention). + +**New files:** +- `.github/workflows/publish.yml` — triggered by `release: published`, runs `just publish`. + +**Modified files:** +- `README.md` — strict trim to what ships in 0.1.0; status banner names the unshipped categories. +- `CONTRIBUTING.md` — remove the bullet that says "Update `CHANGELOG.md`". + +**Files NOT touched:** +- `pyproject.toml` — `version = "0"` stays; publish recipe overrides via `uv version $GITHUB_REF_NAME`. +- `Justfile` — `publish` recipe is already correct. +- `.github/workflows/ci.yml` — test/lint matrix unchanged. +- `src/httpware/**`, `tests/**` — no code or test changes. +- `LICENSE`, `SECURITY.md`, `CLAUDE.md` — unchanged (CLAUDE.md verified to have no CHANGELOG references). +- `docs/engineering.md` — internal roadmap stays; "Released" markers are a follow-up. +- `docs/archive/**` — bmad archive untouched. + +--- + +## Task 1: Delete `CHANGELOG.md` and scrub `CONTRIBUTING.md` + +Remove the changelog file and the one CONTRIBUTING.md bullet that referenced it. + +**Files:** +- Delete: `CHANGELOG.md` +- Modify: `CONTRIBUTING.md` + +- [ ] **Step 1: Verify the current state** + +Run: `git status --short && git branch --show-current` +Expected: branch is `chore/release-0.1.0-prep`. Working tree has the untracked plan file `docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md` (this file) and nothing else. + +Run: `/usr/bin/grep -in "changelog" CONTRIBUTING.md CLAUDE.md README.md` +Expected output: +``` +CONTRIBUTING.md::5. **Update `CHANGELOG.md`** in the `Unreleased` section. +``` +(Plus possibly a reference in `README.md` line ~67 mentioning the changelog link. The README will be fully rewritten in Task 3.) + +If `CLAUDE.md` shows any matches, stop and flag — the spec assumed it had none. + +- [ ] **Step 2: Delete `CHANGELOG.md`** + +Run: `git rm CHANGELOG.md` +Expected: `rm 'CHANGELOG.md'`. The file is staged for deletion. + +- [ ] **Step 3: Remove the CHANGELOG bullet from `CONTRIBUTING.md`** + +Read `CONTRIBUTING.md` to find the line `5. **Update `CHANGELOG.md`** in the `Unreleased` section.` It is part of a numbered list. Two cases: + +- **Case A:** the list is `1. … 2. … 3. … 4. … 5. 6. …`. Deleting bullet 5 means renumbering subsequent items so the list stays contiguous (1, 2, 3, 4, 5 instead of 1, 2, 3, 4, 6). +- **Case B:** bullet 5 is the last item. Delete the line; no renumbering needed. + +Inspect the file first (`uv run head -50 CONTRIBUTING.md` or just read it), then apply the edit. If Case A, also adjust subsequent numbers in the same edit so the list reads consecutively. + +- [ ] **Step 4: Verify no CHANGELOG references remain anywhere except docs/archive/** + +Run: `/usr/bin/grep -rIn "changelog" --include="*.md" --include="*.toml" --include="*.yml" --include="*.yaml" . --exclude-dir=docs/archive --exclude-dir=.venv --exclude-dir=.git --exclude-dir=docs/superpowers` +Expected: no matches (or only matches inside `docs/superpowers/specs/2026-05-31-release-0.1.0-prep-design.md` describing this very deletion, which is fine). + +If `README.md` still has a CHANGELOG reference, leave it — Task 3 rewrites the README entirely. + +- [ ] **Step 5: Commit** + +```bash +git add CONTRIBUTING.md +git commit -m "$(cat <<'EOF' +chore(release): delete CHANGELOG.md; remove contributing-guide reference + +modern-di in the same org doesn't ship a CHANGELOG.md — release notes +live on GitHub Releases (one entry per tag). This commit removes the +file and the one CONTRIBUTING.md bullet that asked contributors to +update an `Unreleased` section. + +README.md still has a stale CHANGELOG link; the README is fully +rewritten in a follow-up commit on this branch. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 2: Add `.github/workflows/publish.yml` + +Mirror modern-di's publish workflow byte-for-byte. + +**Files:** +- Create: `.github/workflows/publish.yml` + +- [ ] **Step 1: Create the workflow file** + +Create `.github/workflows/publish.yml`: + +```yaml +name: Publish Package + +on: + release: + types: + - published + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v2 + - uses: astral-sh/setup-uv@v3 + - run: just publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} +``` + +This mirrors `/Users/kevinsmith/src/pypi/modern-di/.github/workflows/publish.yml`. Triggered when a GitHub Release is published. Runs `just publish` which does `rm -rf dist && uv version $GITHUB_REF_NAME && uv build && uv publish --token $PYPI_TOKEN`. `$GITHUB_REF_NAME` is the tag name (e.g., `0.1.0`). + +- [ ] **Step 2: Lint-check the YAML** + +Run: `uv run python -c "import yaml; yaml.safe_load(open('.github/workflows/publish.yml'))"` +Expected: silent success (no output, no exception). + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/publish.yml +git commit -m "$(cat <<'EOF' +ci(release): add publish.yml workflow for PyPI release on tag + +Mirrors the modern-di publish workflow byte-for-byte. Triggered when a +GitHub Release is published; runs the existing `just publish` recipe +which extracts the version from $GITHUB_REF_NAME, builds, and uploads +to PyPI with the PYPI_TOKEN repo secret. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 3: Rewrite `README.md` + +Replace the current README with the trimmed version from the spec. Removes the bullets for unshipped features (Retry/RetryBudget/Bulkhead/Timeout/Observability/OTel) and adds a status banner that names what's not yet shipped. + +**Files:** +- Modify: `README.md` (full rewrite) + +- [ ] **Step 1: Replace the contents of `README.md`** + +Open `README.md` and replace the entire file with this content: + +```markdown +# httpware + +[![Test](https://github.com/modern-python/httpware/actions/workflows/ci.yml/badge.svg)](https://github.com/modern-python/httpware/actions/workflows/ci.yml) +[![PyPI version](https://badge.fury.io/py/httpware.svg)](https://pypi.org/project/httpware/) +[![Python versions](https://img.shields.io/pypi/pyversions/httpware.svg)](https://pypi.org/project/httpware/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +**Async HTTP client framework for Python.** + +`httpware` is a typed, async HTTP client library built on `httpx2` with a protocol-based seam so the transport is swappable. Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces respx for transport-level tests. + +> **Status:** Pre-1.0 (0.1.0 alpha). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped — track progress on GitHub. + +## Install + +​```bash +pip install httpware +​``` + +Optional extras: + +​```bash +pip install httpware[msgspec] # MsgspecDecoder +​``` + +(`otel`, `niquests`, and `all` extras are declared but their integrations have not shipped yet.) + +## Quickstart + +​```python +from httpware import AsyncClient +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name: str + + +async def main() -> None: + async with AsyncClient(base_url="https://api.example.com") as client: + user = await client.get("/users/1", response_model=User) + print(user.name) +​``` + +## What ships in 0.1.0 + +- **`AsyncClient`** — eight HTTP method shortcuts (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `request`) with typed `response_model` overloads; per-call overrides for `headers`, `params`, `cookies`, `timeout`, `json`, `content`; httpx-style `base_url` join; `with_options(...)` returns a view sharing the same transport. +- **Transport-agnostic seam.** `httpx2` is confined to `httpware.transports.httpx2.Httpx2Transport`. Implement the `Transport` protocol to swap backends. +- **Middleware foundation.** `Middleware` protocol, `Next` type alias, recursive-closure `compose()` chain composition, and phase decorators (`@before_request`, `@after_response`, `@on_error`). +- **Pluggable response decoding.** `PydanticDecoder` (default) with cached `TypeAdapter`; `MsgspecDecoder` via `httpware[msgspec]`. +- **`RecordedTransport`** — built-in test double with a route table, observed-request list, and `aclose_calls` counter. +- **Status-keyed exception hierarchy** — `StatusError`, 4xx / 5xx subclasses, plain typed fields (`status: int`, `body: bytes`, `headers`, `json`, `request_method`, `request_url`). Pickleable; userinfo redacted in `__repr__`. +- **No `httpx2` exception types** leak through `httpware`. The transport seam maps them to `httpware` exceptions. + +## Part of `modern-python` + +Browse the full list of templates and libraries in [`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index. + +## License + +MIT — see [LICENSE](./LICENSE). +``` + +**IMPORTANT:** when you write this to `README.md`, the three triple-backtick fences (around `bash`, `bash`, `python`) above show as zero-width-joiner-prefixed because this plan file is itself markdown. Replace each `​​```` with a real triple-backtick fence (three actual `` ` `` characters) before saving. Verify after the write that the markdown renders correctly. + +- [ ] **Step 2: Verify the README content** + +Run: `/usr/bin/grep -in "retry\|bulkhead\|RetryBudget\|observabil\|otel\|opentelemetry" README.md` +Expected: at most one match — the line in the status banner that says "Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped". Any other matches indicate stale content from the old README; remove them. + +Run: `/usr/bin/grep -in "changelog" README.md` +Expected: no matches. + +Run: `wc -l README.md` +Expected: roughly 55–70 lines (the old README was ~80; the trim drops ~10). + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "$(cat <<'EOF' +docs(release): trim README to what 0.1.0 actually ships + +Removes the "Highlights" bullets for resilience middleware (Retry, +RetryBudget, Bulkhead, Timeout), the observability paragraph, and the +"first-class OpenTelemetry" line — none of those Epics have shipped. + +Adds an explicit unshipped-categories list to the status banner so users +know what's missing before they install. + +Replaces the bare CHANGELOG link with nothing (the file is gone; release +notes live on GitHub Releases). + +The "What ships in 0.1.0" section enumerates the shipped public surface: +AsyncClient, transport-agnostic seam, middleware foundation, +PydanticDecoder + MsgspecDecoder, RecordedTransport, status-keyed +exception hierarchy. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 4: Verify, push, PR, merge + +End-to-end sanity check before the PR opens. + +- [ ] **Step 1: Run the full test suite** + +Run: `just test` +Expected: 273 passed, 1 deselected (perf), 100% line coverage. (Unchanged from `main`; this PR touches no test code.) + +- [ ] **Step 2: Run lint** + +Run: `just lint-ci` +Expected: clean. + +- [ ] **Step 3: Confirm the working tree state** + +Run: `git status --short` +Expected: only the untracked plan file `docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md`. + +Run: `git log --oneline main..HEAD` +Expected: four commits — spec (`docs(release): design for 0.1.0 release prep`), Task 1 (`chore(release): delete CHANGELOG.md…`), Task 2 (`ci(release): add publish.yml…`), Task 3 (`docs(release): trim README…`). + +Run: `git diff --stat main..HEAD` +Expected: `CHANGELOG.md` deleted, `CONTRIBUTING.md` modified (~1 line), `.github/workflows/publish.yml` added, `README.md` modified (~80 lines changed), plus the spec and plan files. + +- [ ] **Step 4: Stage and commit the plan file** + +```bash +git add docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md +git commit -m "docs(release): implementation plan for 0.1.0 release prep + +Co-Authored-By: Claude Opus 4.7 (1M context) " +``` + +- [ ] **Step 5: Push the branch** + +Run: `git push -u origin chore/release-0.1.0-prep` +Expected: push succeeds. + +- [ ] **Step 6: Open the PR** + +```bash +gh pr create --title "chore: prep 0.1.0 release" --body "$(cat <<'EOF' +## Summary + +Repo-side prep for the `0.1.0` PyPI release. No production code or test changes. + +- **Deletes `CHANGELOG.md`.** Release notes live on GitHub Releases (one entry per tag), matching the modern-di convention in the same org. +- **Adds `.github/workflows/publish.yml`.** Mirrors modern-di's publish workflow byte-for-byte. Triggered by `release: published`; runs the existing `just publish` recipe which sets the version from `$GITHUB_REF_NAME`, builds, and uploads to PyPI with the `PYPI_TOKEN` secret. +- **Trims `README.md`.** Drops the "Highlights" bullets for unshipped features (Retry/RetryBudget/Bulkhead/Timeout/Observability/OpenTelemetry). Status banner explicitly names the unshipped categories so users know what's missing. Replaces the bare CHANGELOG link with the deleted-file convention. +- **Removes the CHANGELOG bullet from `CONTRIBUTING.md`.** + +`pyproject.toml`, `Justfile`, `ci.yml`, and the source tree are untouched. + +After this PR merges, the maintainer: +1. Verifies the PyPI name `httpware` is owned by `modern-python` (or free). +2. Verifies `PYPI_TOKEN` is present in repo secrets. +3. Creates a GitHub Release tagged `0.1.0`, which triggers `publish.yml` → `just publish` → PyPI. + +Spec + plan: `docs/superpowers/specs/2026-05-31-release-0.1.0-prep-design.md`, `docs/superpowers/plans/2026-05-31-release-0.1.0-prep-plan.md`. + +## Test plan + +- [x] No production code / test changes; `just test` shows 273 passed, 1 deselected, 100% line coverage (unchanged from `main`). +- [x] `just lint-ci` clean. +- [x] `tests/test_no_httpx2_leakage.py` still passes. +- [x] `grep -i changelog` returns no matches in any project file (except the spec/plan describing the deletion). +- [x] `python -c "import yaml; yaml.safe_load(open('.github/workflows/publish.yml'))"` — workflow file parses. +- [ ] CI green on all matrix entries. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +- [ ] **Step 7: Wait for CI** + +Run: `gh pr checks ` (the number is printed by `gh pr create`). +Expected: all checks green. Since this PR touches no test code, all `pytest (3.x)` jobs pass identically to `main`. The `lint` job runs against the unchanged source and passes. + +- [ ] **Step 8: Merge** + +Once CI is green: + +Run: `gh pr merge --merge --delete-branch` +Run: `git checkout main && git pull --ff-only && git log --oneline -3` + +The repo is now release-ready. Proceed to Task 5 — the manual maintainer-driven release sequence. + +--- + +## Task 5: Maintainer-driven post-merge release sequence + +These steps run outside the PR workflow. The maintainer executes them after `chore/release-0.1.0-prep` merges to `main`. **An agent cannot complete this task autonomously without explicit maintainer authorization** because it (a) creates a public PyPI release that is hard to retract and (b) may require PyPI-account decisions if the name is contested. + +- [ ] **Step 1: Verify PyPI name ownership** + +Run: +```bash +curl -sS https://pypi.org/pypi/httpware/json \ + | python -c "import sys, json; d=json.load(sys.stdin); info=d.get('info', {}); print('home_page:', info.get('home_page')); print('project_urls:', info.get('project_urls')); print('author:', info.get('author')); print('latest_version:', info.get('version'))" +``` + +Decision tree: +- Output shows `home_page` containing `github.com/modern-python/httpware` OR `project_urls` includes our repo URL → name is ours. Proceed. +- Output shows a different homepage / author → STOP. The name is owned by a third party. Options: (a) rename the package (edit `pyproject.toml` `name`, `Justfile publish` recipe, README); (b) open a PyPI name-transfer request (https://pypi.org/help/#name-claim). Do NOT proceed to Step 3. +- The `curl` returns a 404 → name is free. Proceed; the first publish will reserve it. + +- [ ] **Step 2: Verify `PYPI_TOKEN` secret exists** + +Run: `gh secret list --repo modern-python/httpware | grep PYPI_TOKEN` +Expected: a line like `PYPI_TOKEN Updated YYYY-MM-DD`. + +If absent: create a PyPI API token scoped to the `httpware` project at https://pypi.org/manage/account/token/, then add it: + +```bash +gh secret set PYPI_TOKEN --repo modern-python/httpware +# (paste the token when prompted) +``` + +- [ ] **Step 3: Create the GitHub Release** + +```bash +gh release create 0.1.0 \ + --repo modern-python/httpware \ + --title "0.1.0 — initial alpha" \ + --notes-file - <<'EOF' +Initial public alpha of `httpware`. + +**Includes:** +- AsyncClient with 8 HTTP method shortcuts and typed `response_model` overloads +- Transport-agnostic seam (`httpx2` confined to `Httpx2Transport`) +- Middleware foundation: `Middleware` protocol, `compose()`, phase decorators +- Pluggable response decoders: `PydanticDecoder` (default), `MsgspecDecoder` via `[msgspec]` extra +- `RecordedTransport` test double +- Status-keyed exception hierarchy + +**Not yet shipped (next releases):** +- Resilience middleware (retry, timeout, bulkhead) — Epic 3 +- Streaming (`AsyncClient.stream`) — Epic 4 +- Observability / OpenTelemetry — Epic 5 +- `auth=` parameter on AsyncClient — Story 2-4 +- `data=` / `files=` body params + +Public API is subject to change between minor releases until v1.0. +EOF +``` + +The release publication triggers `publish.yml`. The workflow runs `just publish`: +1. `rm -rf dist` +2. `uv version 0.1.0` (sets pyproject's `version` field from `$GITHUB_REF_NAME = "0.1.0"`). +3. `uv build` → produces `dist/httpware-0.1.0-py3-none-any.whl` and `dist/httpware-0.1.0.tar.gz`. +4. `uv publish --token $PYPI_TOKEN` → uploads to PyPI. + +- [ ] **Step 4: Watch the publish workflow run** + +Run: `gh run list --repo modern-python/httpware --workflow=publish.yml --limit 3` +Expected: a `queued` or `in_progress` run for the `0.1.0` tag. + +Run: `gh run watch --repo modern-python/httpware` (the ID is in the list output). +Expected: workflow completes with `conclusion: success`. + +If the workflow fails: +- **Build failure** (`uv build`): inspect the run log via `gh run view --log-failed --repo modern-python/httpware`. Common cause: a missing build artifact or dep declaration. The tag stays; iterate via a `0.1.1` patch. +- **Publish failure** (`uv publish`): likely `PYPI_TOKEN` invalid / scope mismatch / package already exists with a conflicting version. Re-run the workflow from the Actions tab once the underlying issue is fixed. + +- [ ] **Step 5: Smoke-check the published package** + +Run in a fresh venv: +```bash +mkdir /tmp/httpware-smoke && cd /tmp/httpware-smoke +uv venv && source .venv/bin/activate +pip install httpware==0.1.0 +python -c " +import httpware +from httpware import AsyncClient, Middleware, RecordedTransport, PydanticDecoder, before_request +print('OK:', httpware.__file__) +print('AsyncClient:', AsyncClient) +print('Middleware:', Middleware) +print('RecordedTransport:', RecordedTransport) +print('PydanticDecoder:', PydanticDecoder) +print('before_request:', before_request) +" +``` + +Expected: all imports succeed; `httpware.__file__` ends with `…/site-packages/httpware/__init__.py`. + +If a name import fails: the wheel is incomplete. Investigate via `pip show -f httpware` to see what landed. Fix on `main`, tag `0.1.1`, re-release. + +- [ ] **Step 6: (Optional, recommended) Confirm install with the `[msgspec]` extra** + +```bash +cd /tmp/httpware-smoke +deactivate && rm -rf .venv && uv venv && source .venv/bin/activate +pip install "httpware[msgspec]==0.1.0" +python -c "from httpware.decoders.msgspec import MsgspecDecoder; print('OK:', MsgspecDecoder)" +``` + +Expected: `OK: `. + +- [ ] **Step 7: Announce (optional)** + +The 0.1.0 release lands on PyPI. Communicate via the org's preferred channel (GitHub Discussions, README badge auto-updates, etc.). This step is purely social; no command to run. + +--- + +## Definition of done + +- `CHANGELOG.md` is gone from the repo. +- `.github/workflows/publish.yml` exists with the modern-di-pattern content. +- `README.md` is rewritten per the spec; no references to unshipped features (Retry/RetryBudget/Bulkhead/Timeout/Observability/OTel) as if they exist; status banner names what's missing. +- `CONTRIBUTING.md` has no `CHANGELOG.md` reference. +- `just test` shows 273 passed (unchanged); `just lint-ci` clean. +- `tests/test_no_httpx2_leakage.py` and `tests/test_optional_extras_isolation.py` still pass. +- PR `chore/release-0.1.0-prep` lands on `main` via merge. +- PyPI name verified as owned by `modern-python` (or free) by the maintainer. +- `PYPI_TOKEN` secret present on the repo. +- GitHub Release `0.1.0` created; `publish.yml` runs to completion; `pip install httpware==0.1.0` works in a clean venv and the public names import successfully.