mnfst 0.1.0__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.
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.10", "3.13", "3.14"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - run: python -m pip install -e ".[dev]"
24
+ - run: python -m pytest -q
25
+ - run: python -m pip wheel --no-deps . --wheel-dir dist
@@ -0,0 +1,93 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - src/mnfst/version.py
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: publish-mnfst
12
+ cancel-in-progress: false
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ detect:
19
+ runs-on: ubuntu-latest
20
+ outputs:
21
+ version: ${{ steps.detect.outputs.version }}
22
+ steps:
23
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
24
+ with:
25
+ fetch-depth: 0
26
+ persist-credentials: false
27
+ - name: Detect version bump
28
+ id: detect
29
+ env:
30
+ BEFORE_SHA: ${{ github.event.before }}
31
+ AFTER_SHA: ${{ github.sha }}
32
+ MANUAL: ${{ github.event_name == 'workflow_dispatch' }}
33
+ run: |
34
+ current=$(git show "${AFTER_SHA}:src/mnfst/version.py" | sed -n 's/^__version__ = "\([^"]*\)"$/\1/p')
35
+ if [ -z "$current" ]; then
36
+ echo "::error::Could not read the current package version."
37
+ exit 1
38
+ fi
39
+ if [ "$MANUAL" = "true" ]; then
40
+ echo "version=$current" >> "$GITHUB_OUTPUT"
41
+ echo "Manual dispatch: publishing $current"
42
+ exit 0
43
+ fi
44
+ if [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
45
+ previous=$(git show "${BEFORE_SHA}:src/mnfst/version.py" 2>/dev/null | sed -n 's/^__version__ = "\([^"]*\)"$/\1/p')
46
+ else
47
+ previous=""
48
+ fi
49
+ if [ -n "$previous" ] && [ "$previous" != "$current" ]; then
50
+ echo "version=$current" >> "$GITHUB_OUTPUT"
51
+ echo "Version bumped: $previous -> $current"
52
+ else
53
+ echo "version=" >> "$GITHUB_OUTPUT"
54
+ echo "::notice::No version change; nothing to publish."
55
+ fi
56
+
57
+ build:
58
+ needs: detect
59
+ if: needs.detect.outputs.version != ''
60
+ runs-on: ubuntu-latest
61
+ steps:
62
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
63
+ with:
64
+ persist-credentials: false
65
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
66
+ with:
67
+ python-version: '3.14'
68
+ cache: pip
69
+ - run: python -m pip install build twine
70
+ - run: python -m pip install -e '.[dev]'
71
+ - run: python -m pytest -q
72
+ - run: python -m build
73
+ - run: python -m twine check dist/*
74
+ - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
75
+ with:
76
+ name: python-package-distributions
77
+ path: dist/
78
+ retention-days: 1
79
+
80
+ publish:
81
+ needs: build
82
+ runs-on: ubuntu-latest
83
+ environment:
84
+ name: pypi
85
+ url: https://pypi.org/p/mnfst
86
+ permissions:
87
+ id-token: write
88
+ steps:
89
+ - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
90
+ with:
91
+ name: python-package-distributions
92
+ path: dist/
93
+ - uses: pypa/gh-action-pypi-publish@v1.14.2
mnfst-0.1.0/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
6
+ .superpowers/
@@ -0,0 +1,50 @@
1
+ # SDK / app contract
2
+
3
+ The SDK talks to the configured Manifest API using `Authorization: Bearer <project key>` and `User-Agent: mnfst-python/<version>`.
4
+
5
+ ## Capture
6
+
7
+ `POST /v1/heal` receives:
8
+
9
+ ```json
10
+ {
11
+ "traceId": "unique-capture-id",
12
+ "request": {"method": "POST", "url": "https://example.com/orders", "headers": {}, "body": {"limit": 200}},
13
+ "response": {"statusCode": 400, "body": {"error": "limit must be at most 100"}, "truncated": false},
14
+ "responseTimeMs": 25
15
+ }
16
+ ```
17
+
18
+ Credential filtering and body limits are described in the README. Capture gates live in `gate.py`; the server owns repair policy.
19
+
20
+ A successful heal response may contain `status: patched|unverified`, `healAttemptId`, `operations` and `healedRequest` with `url`, `headers` or `body`. Only these two statuses authorize a retry. No patch, malformed responses and unavailable service return the original error response. HTTP 403 with `{"error":"project_disabled"}` suppresses healing for five minutes.
21
+
22
+ ## Apply
23
+
24
+ A healed URL replaces the URL only within the original origin. Headers set or replace case-insensitively; null removes a header. Content length is recalculated. Objects merge using the server's healed body as the authoritative copy of fields sent to the server; withheld local credential fields are restored. Non-object JSON replaces the body. An unreadable original body needs a replacement body before it can be retried.
25
+
26
+ Each captured failure permits one retry. A retry response, including another failure, is returned to the caller. A transport failure returns the original response. Successful response streams are not eagerly consumed.
27
+
28
+ ## Outcome
29
+
30
+ `PATCH /v1/heal-attempts/:id` sends exactly one of:
31
+
32
+ ```json
33
+ {"response":{"statusCode":200}}
34
+ ```
35
+
36
+ ```json
37
+ {"response":{"statusCode":400,"body":{"error":"raw upstream error"},"truncated":false}}
38
+ ```
39
+
40
+ ```json
41
+ {"failure":{"kind":"transport_error","message":"connection reset"}}
42
+ ```
43
+
44
+ ```json
45
+ {"failure":{"kind":"not_attempted","message":"replay_not_attempted"}}
46
+ ```
47
+
48
+ HTTP status must be 200–599. Failure messages are capped at 512 UTF-8 bytes after credential filtering. HTTP status zero is not a wire status. Transport failures and unattempted retries are inconclusive evidence; neither can verify or invalidate a patch. The server determines the verdict from the raw evidence, with the first accepted report winning.
49
+
50
+ Reports are best effort, bounded, and observable through logger warnings. The SDK sends the failed retry's raw body so the app can distinguish recurrence from a newly revealed issue. It does not assert `succeeded` or `failed` itself.
mnfst-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: mnfst
3
+ Version: 0.1.0
4
+ Summary: Heal failing JSON API requests on the fly.
5
+ Project-URL: Homepage, https://manifest.build
6
+ Project-URL: Repository, https://github.com/mnfst/manifest-python
7
+ Project-URL: Issues, https://github.com/mnfst/manifest-python/issues
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: anyio<5,>=4
10
+ Requires-Dist: httpx<1,>=0.24
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Requires-Dist: requests>=2.31; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Manifest for Python
17
+
18
+ [![CI](https://github.com/mnfst/manifest-python/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mnfst/manifest-python/actions/workflows/ci.yml)
19
+
20
+ Repair failed JSON API requests automatically. Works with `httpx` and `requests`, for everyday APIs and LLMs alike.
21
+
22
+ ```python
23
+ from mnfst import manifest
24
+
25
+ manifest()
26
+ # Keep making your API calls as usual.
27
+ ```
28
+
29
+ Your API rejects a request → Manifest finds a repair → the SDK retries once, locally.
30
+
31
+ ## Setup
32
+
33
+ ### 1. Install
34
+
35
+ Requires **Python 3.10+**:
36
+
37
+ ```sh
38
+ python -m venv .venv
39
+ source .venv/bin/activate
40
+ python -m pip install mnfst
41
+ ```
42
+
43
+ On Windows, activate with `.venv\Scripts\activate` instead. The package and import name are **mnfst**. `httpx` is included; install `requests` separately if you use it.
44
+
45
+ ### 2. Connect your project
46
+
47
+ Create a project in your Manifest dashboard and copy the project key shown during setup. In **Project Settings**, turn **Autofix** on to enable repairs.
48
+
49
+ ```sh
50
+ export MNFST_KEY='your-project-key'
51
+ ```
52
+
53
+ The SDK defaults to `https://api.manifest.build`. For a local app running on port 5310, also set:
54
+
55
+ ```sh
56
+ export MNFST_URL='http://127.0.0.1:5310'
57
+ ```
58
+
59
+ Your server must support the [SDK API contract](CONTRACT.md). The local app must already be running.
60
+
61
+ ### 3. Initialize before your requests
62
+
63
+ Call `manifest()` once at startup. Save this as `example.py`, replacing the example endpoint and payload with your own:
64
+
65
+ ```python
66
+ import httpx
67
+ from mnfst import manifest, flush
68
+
69
+ manifest(
70
+ on_heal=lambda event: print(
71
+ "[manifest]", event.heal_status, event.replay_status_code
72
+ )
73
+ )
74
+
75
+ try:
76
+ response = httpx.post(
77
+ "https://api.example.com/orders",
78
+ json={"limit": 500},
79
+ )
80
+ print(response.status_code, response.text)
81
+ finally:
82
+ flush(timeout=5)
83
+ ```
84
+
85
+ Run it with `python example.py`. For an API that rejects `limit: 500` and has a matching repair, Manifest can retry with a valid limit. Repairs depend on the API error and available patches.
86
+
87
+ The same initialization covers `httpx.AsyncClient` and `requests` calls using their standard transports.
88
+
89
+ ## Check that it works
90
+
91
+ Send a JSON request that your test API rejects with **400, 404 or 422**. Check the failure in your project's dashboard and the `on_heal` callback for the repair result. A successful request alone does not contact Manifest. `flush()` lets a short script wait for outcome reports before exiting.
92
+
93
+ ## What to expect
94
+
95
+ - **One retry.** Manifest returns a repair; the SDK sends the corrected request directly to your API.
96
+ - **Original error if healing is unavailable.** A heal call can add up to 60 seconds. If a retry returns an HTTP response, that response reaches your application.
97
+ - **Sync and async.** Standard `httpx` transports and `requests` adapters are covered process-wide. Custom transports and `aiohttp` are not intercepted.
98
+ - **Retry semantics still matter.** Use idempotency keys where needed; a repeated request can repeat side effects.
99
+
100
+ ## Privacy
101
+
102
+ Manifest receives failed request URLs, headers, JSON bodies and error responses. Known credential fields are masked or withheld, but nested secrets, prompts and business data can still be sent. Enable it only for traffic you permit your Manifest server to process and store.
103
+
104
+ ## More
105
+
106
+ [Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Node.js SDK](https://github.com/mnfst/manifest-node)
mnfst-0.1.0/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Manifest for Python
2
+
3
+ [![CI](https://github.com/mnfst/manifest-python/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mnfst/manifest-python/actions/workflows/ci.yml)
4
+
5
+ Repair failed JSON API requests automatically. Works with `httpx` and `requests`, for everyday APIs and LLMs alike.
6
+
7
+ ```python
8
+ from mnfst import manifest
9
+
10
+ manifest()
11
+ # Keep making your API calls as usual.
12
+ ```
13
+
14
+ Your API rejects a request → Manifest finds a repair → the SDK retries once, locally.
15
+
16
+ ## Setup
17
+
18
+ ### 1. Install
19
+
20
+ Requires **Python 3.10+**:
21
+
22
+ ```sh
23
+ python -m venv .venv
24
+ source .venv/bin/activate
25
+ python -m pip install mnfst
26
+ ```
27
+
28
+ On Windows, activate with `.venv\Scripts\activate` instead. The package and import name are **mnfst**. `httpx` is included; install `requests` separately if you use it.
29
+
30
+ ### 2. Connect your project
31
+
32
+ Create a project in your Manifest dashboard and copy the project key shown during setup. In **Project Settings**, turn **Autofix** on to enable repairs.
33
+
34
+ ```sh
35
+ export MNFST_KEY='your-project-key'
36
+ ```
37
+
38
+ The SDK defaults to `https://api.manifest.build`. For a local app running on port 5310, also set:
39
+
40
+ ```sh
41
+ export MNFST_URL='http://127.0.0.1:5310'
42
+ ```
43
+
44
+ Your server must support the [SDK API contract](CONTRACT.md). The local app must already be running.
45
+
46
+ ### 3. Initialize before your requests
47
+
48
+ Call `manifest()` once at startup. Save this as `example.py`, replacing the example endpoint and payload with your own:
49
+
50
+ ```python
51
+ import httpx
52
+ from mnfst import manifest, flush
53
+
54
+ manifest(
55
+ on_heal=lambda event: print(
56
+ "[manifest]", event.heal_status, event.replay_status_code
57
+ )
58
+ )
59
+
60
+ try:
61
+ response = httpx.post(
62
+ "https://api.example.com/orders",
63
+ json={"limit": 500},
64
+ )
65
+ print(response.status_code, response.text)
66
+ finally:
67
+ flush(timeout=5)
68
+ ```
69
+
70
+ Run it with `python example.py`. For an API that rejects `limit: 500` and has a matching repair, Manifest can retry with a valid limit. Repairs depend on the API error and available patches.
71
+
72
+ The same initialization covers `httpx.AsyncClient` and `requests` calls using their standard transports.
73
+
74
+ ## Check that it works
75
+
76
+ Send a JSON request that your test API rejects with **400, 404 or 422**. Check the failure in your project's dashboard and the `on_heal` callback for the repair result. A successful request alone does not contact Manifest. `flush()` lets a short script wait for outcome reports before exiting.
77
+
78
+ ## What to expect
79
+
80
+ - **One retry.** Manifest returns a repair; the SDK sends the corrected request directly to your API.
81
+ - **Original error if healing is unavailable.** A heal call can add up to 60 seconds. If a retry returns an HTTP response, that response reaches your application.
82
+ - **Sync and async.** Standard `httpx` transports and `requests` adapters are covered process-wide. Custom transports and `aiohttp` are not intercepted.
83
+ - **Retry semantics still matter.** Use idempotency keys where needed; a repeated request can repeat side effects.
84
+
85
+ ## Privacy
86
+
87
+ Manifest receives failed request URLs, headers, JSON bodies and error responses. Known credential fields are masked or withheld, but nested secrets, prompts and business data can still be sent. Enable it only for traffic you permit your Manifest server to process and store.
88
+
89
+ ## More
90
+
91
+ [Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Node.js SDK](https://github.com/mnfst/manifest-node)
@@ -0,0 +1,56 @@
1
+ # SDK guide
2
+
3
+ [← Quick start](../README.md)
4
+
5
+ ## Configuration
6
+
7
+ Call `manifest()` once at startup. Arguments override environment variables:
8
+
9
+ | Argument | Environment | Default |
10
+ | --- | --- | --- |
11
+ | `key` | `MNFST_KEY` | Missing key disables the SDK with a warning |
12
+ | `url` | `MNFST_URL` | `https://api.manifest.build` |
13
+ | `on_heal` | — | Optional callback receiving a `HealEvent` |
14
+
15
+ Use `url="http://127.0.0.1:5310"` with a local Manifest app. The hosted default requires a deployed, compatible app. Changing configuration after initialization requires a process restart.
16
+
17
+ ```python
18
+ from mnfst import manifest, flush
19
+
20
+ manifest(on_heal=lambda event: print(event.heal_status, event.replay_status_code))
21
+ # Before a short-lived process exits:
22
+ flush(timeout=5)
23
+ ```
24
+
25
+ Reports run in background threads. `flush` waits for outstanding reports within one total timeout; it does not guarantee delivery. Normal interpreter exit allows two seconds to flush. Failed or dropped reports emit warnings on the `mnfst` logger. Abrupt termination can lose reports.
26
+
27
+ ## Behavior and limits
28
+
29
+ - Standard httpx transports and requests adapters are instrumented process-wide, including existing clients. Custom transports, aiohttp, browsers and other languages are not covered.
30
+ - Eligible HTTP failures are sent to Manifest; successful calls, authentication failures, rate limits and server errors pass through. A network failure before an HTTP response also passes through.
31
+ - Manifest selects repairs. The SDK retries at most once per captured failure. The original error response is returned if healing is unavailable, no repair can be applied, or the retry has a transport error.
32
+ - A successful streaming retry remains streamed. Error capture reads a bounded prefix and preserves the original response bytes for the caller. Error reads use the caller's read timeout; healing adds up to 60 seconds, and the retry uses the caller's timeout.
33
+ - The sync heal worker pool permits eight concurrent calls. Excess calls fail open. Timed-out workers can continue in the background within that bound. Outcome reporting permits 64 concurrent reports per reporter.
34
+ - Request JSON is limited to 256 KiB and depth 64. Multipart, binary, streamed, oversized and invalid JSON bodies travel as `null`; they are not generally repairable. Response metadata is limited to 64 KiB; truncated errors are reported without retry. Gzip and deflate error prefixes are decoded within that limit; unsupported content encodings provide no body evidence.
35
+ - Retries can repeat side effects. Use APIs with safe retry semantics and caller-managed idempotency keys. Existing credentials and idempotency headers are retained unless explicitly changed by the repair. URL repairs must stay on the same origin.
36
+
37
+ ## Data sent to Manifest
38
+
39
+ Failed request URLs, headers, JSON bodies and error responses are sent to the configured server. Known credential names in query parameters and headers are masked. Credential-named **top-level** request body fields are withheld and restored for the retry.
40
+
41
+ This is not general data-loss prevention: nested fields, arbitrary secret names, personal data, prompts and response bodies may still contain sensitive content. Only enable it for traffic you permit Manifest to process and store. The server does not receive the original credential values masked by the SDK.
42
+
43
+ ## Development
44
+
45
+ ```sh
46
+ pip install -e '.[dev]'
47
+ pytest -q
48
+ # Optional: point only at a disposable app (creates a test customer/project).
49
+ MNFST_TEST_APP_URL=http://127.0.0.1:5310 pytest -q tests/test_live_app.py
50
+ ```
51
+
52
+ CI tests Python 3.10, 3.13 and 3.14 and builds the wheel. The live app test runs locally because the app repository is private; cross-repository CI needs separate checkout credentials. Validated against app commit `9ea359279577f99e4058b7600c75889b1a2c5881`.
53
+
54
+ See [CONTRACT.md](../CONTRACT.md) for the wire protocol. Transport failures require the app's explicit `failure` outcome support; they must never be reported as HTTP success.
55
+
56
+ Adapted from [guillaumegay13/autofix-python](https://github.com/guillaumegay13/autofix-python), source commit `9a82d8235a037392826f982626f49422f3828213`.
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "mnfst"
3
+ dynamic = ["version"]
4
+ readme = "README.md"
5
+ description = "Heal failing JSON API requests on the fly."
6
+ requires-python = ">=3.10"
7
+ dependencies = ["httpx>=0.24,<1", "anyio>=4,<5"]
8
+
9
+ [project.urls]
10
+ Homepage = "https://manifest.build"
11
+ Repository = "https://github.com/mnfst/manifest-python"
12
+ Issues = "https://github.com/mnfst/manifest-python/issues"
13
+
14
+ [project.optional-dependencies]
15
+ dev = ["pytest>=8", "requests>=2.31"]
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/mnfst"]
23
+
24
+ [tool.hatch.version]
25
+ path = "src/mnfst/version.py"
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
@@ -0,0 +1,43 @@
1
+ """manifest(): repair failing API requests on the fly, based on the API's error.
2
+
3
+ from mnfst import manifest
4
+ manifest() # once, at startup
5
+
6
+ Instruments the process's HTTP clients (httpx and requests). When a call your
7
+ app makes fails, the failing request and the API's error go to Phoenix; if the
8
+ server returns a repaired body, the call is retried once. Successes are never
9
+ touched. The surface is three options: key, url, on_heal — everything that
10
+ is policy (which providers and endpoints get healed, and how hard the server
11
+ tries) is server-side configuration, editable in the dashboard.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import atexit
16
+ import warnings
17
+ from typing import Callable, Optional
18
+
19
+ from .config import resolve_config
20
+ from .heal_api import HealEvent
21
+ from .outbound import flush, install_outbound, installed_config
22
+ from .version import VERSION
23
+
24
+ __all__ = ["manifest", "flush", "HealEvent", "VERSION"]
25
+
26
+ atexit.register(flush, 2.0)
27
+
28
+
29
+ def manifest(*, key: Optional[str] = None, url: Optional[str] = None,
30
+ on_heal: Optional[Callable] = None) -> None:
31
+ config = resolve_config(api_key=key, url=url, on_heal=on_heal)
32
+ if config.api_key is None:
33
+ warnings.warn("mnfst: MNFST_KEY is not set; mnfst is disabled.",
34
+ stacklevel=2)
35
+ return
36
+ # Patching is process-global and one-shot. A second call with different
37
+ # options cannot take effect, so say so instead of pretending.
38
+ existing = installed_config()
39
+ if existing is not None and existing != config:
40
+ warnings.warn("mnfst: already installed with a different configuration; "
41
+ "reconfiguring requires a restart. "
42
+ "The first configuration stays in effect.", stacklevel=2)
43
+ install_outbound(config)
@@ -0,0 +1,37 @@
1
+ """Options resolution: kwargs beat environment beats defaults.
2
+
3
+ The surface is deliberately tiny: credentials, server, and a local
4
+ observability hook. Everything that is policy — whether a given app,
5
+ provider, endpoint, or direction gets healed, and how long the server may
6
+ spend finding a fix — lives server-side, where it is editable in the
7
+ dashboard without a deploy. The SDK only replays when the server hands it
8
+ a healed body, so the server can enforce all of that with no client knob.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from dataclasses import dataclass
14
+ from typing import Callable, Optional
15
+
16
+ HOSTED_URL = "https://api.manifest.build"
17
+
18
+ # Hard client-side cap on a heal round-trip. Not configuration: fail-open
19
+ # needs a deadline even when the server misbehaves. How long the server
20
+ # actually spends investigating is server-side policy under this bound.
21
+ HEAL_TIMEOUT_SECONDS = 60.0
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class Config:
26
+ api_key: Optional[str]
27
+ base_url: str
28
+ on_heal: Optional[Callable]
29
+
30
+
31
+ def resolve_config(api_key: Optional[str] = None, url: Optional[str] = None,
32
+ on_heal: Optional[Callable] = None) -> Config:
33
+ return Config(
34
+ api_key=api_key or os.environ.get("MNFST_KEY") or None,
35
+ base_url=(url or os.environ.get("MNFST_URL") or HOSTED_URL).rstrip("/"),
36
+ on_heal=on_heal,
37
+ )
@@ -0,0 +1,51 @@
1
+ """Capture any method, but only request-side failures.
2
+
3
+ 400/404/422 are the statuses where the failure is the request's fault —
4
+ the only failures worth reporting and the only ones worth repairing.
5
+ 401/403 (auth), 402 (billing), 429 (rate limits) and every 5xx are excluded
6
+ by design: editing the request cannot help, and reporting them is noise.
7
+
8
+ That status gate is the client's only eligibility rule. Whether a captured
9
+ failure gets retried is the server's call: the SDK retries when Phoenix
10
+ hands back a healed request (CONTRACT §4).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from typing import Any, Optional
16
+
17
+ GATED_STATUSES = frozenset({400, 404, 422})
18
+ REQUEST_BODY_LIMIT = 262144 # past this a body is a payload, not a form to repair
19
+
20
+
21
+ def should_capture(status: int) -> bool:
22
+ return status in GATED_STATUSES
23
+
24
+
25
+ def parse_json_body(body_bytes: Optional[bytes]) -> Any:
26
+ """The request body as JSON — object, array or scalar — else None (absent,
27
+ huge, not JSON). Fails open on anything: deep nesting raises
28
+ RecursionError, and no parse failure may ever break the caller's request."""
29
+ if not body_bytes or len(body_bytes) > REQUEST_BODY_LIMIT:
30
+ return None
31
+ try:
32
+ parsed = json.loads(body_bytes)
33
+ return parsed if bounded_json(parsed) else None
34
+ except Exception:
35
+ return None
36
+
37
+
38
+ def bounded_json(value: Any, max_depth: int = 64) -> bool:
39
+ """Bound depth and work independently of interpreter recursion behavior."""
40
+ stack = [(value, 0)]
41
+ visited = 0
42
+ while stack:
43
+ value, depth = stack.pop()
44
+ visited += 1
45
+ if depth > max_depth or visited > 100_000:
46
+ return False
47
+ if isinstance(value, dict):
48
+ stack.extend((child, depth + 1) for child in value.values())
49
+ elif isinstance(value, list):
50
+ stack.extend((child, depth + 1) for child in value)
51
+ return True