Metadata-Version: 2.5
Name: nexus-raw
Version: 0.22.1
Summary: The Python reference client for nexus-raw: a minimal Nexus raw-storage client on the Python standard library
Project-URL: Homepage, https://lipen.github.io/nexus-raw/
Project-URL: Repository, https://github.com/Lipen/nexus-raw
Project-URL: Documentation, https://lipen.github.io/nexus-raw/reference/api/
Author: lipen
License-Expression: MIT
Keywords: artifacts,devops,nexus,nexus-raw,sha256
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: System :: Archiving
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# The Python reference client

`nxr.py` is the second client of the protocol: a minimal reference implementation on the Python standard library, python3.10+, nothing to install.
It proves the claim the project makes about its own wire format: the protocol is implementable from scratch over bare HTTP, with `http.client` for the wire and `hashlib` for the digests.
The authoritative text it follows lives in `crates/nexus-raw-core/src/protocol.md`.

## What it covers

Six verbs over the store: `get`, `put`, `head`, `sha`, `ls`, `channel get/set`.
Deletion is deliberately absent.
The interesting rules are all client-side and all present:

- `put` writes the bytes, then the `.sha256` marker of the same name, strictly in that order.
  A crash between the two leaves a markerless object, which every reader refuses.
  The marker's name field is the percent-decoded final path segment, refused when it is empty, `.`/`..`, out of grammar, or `.sha256`-suffixed.
- `get` downloads the bytes and the sibling and refuses unless the digest matches.
  Completion is bytes plus the sibling: anything less is an exception, never a silent return.
- A 2xx without `Content-Length` is present-but-unverifiable, never absent (`head` reports `length=None`).
- 401/403 fail fast with no retries.
  5xx and transport failures retry with backoff (4 attempts, 0.5s x 2^n, capped, jittered).
  A retryable status (429 or 5xx) honors its `Retry-After` pause, clamped to 1..=60 seconds, in place of the backoff.
- Redirects are never followed: a 301 surfaces as `HttpError` with the status.
- The per-operation socket timeout is the stall watchdog: a frozen server aborts the attempt, a slow-but-moving transfer never trips it.
- `ls` walks `/service/rest/v1/search/assets` with continuation tokens, capped at 1000 pages, and diagnoses a repository-scoped 400 as "missing or not raw".
  With `directory`, it scopes the walk the way a directory listing does: the search asks with `q=<directory>/` and the client keeps the paths under that whole-segment prefix, so the server's loose `q` can only widen the pages, never the answer.
- Markers parse strictly (`sha256sum -c` shape: LF only, exactly two spaces, 64 lowercase hex, trailing newline), because a loose parse would bless foreign markers.
  The reference decodes marker bytes as ASCII, one notch stricter than the Rust core's lossy UTF-8: harmless today, since both sides compare digests only, but a porter may loosen it the same way.

Every error is an exception carrying `.status` (the HTTP status, `None` for pure transport failures) and `.hint` (the action line a CLI would print).
Every attempt lands in `.requests` as a `(method, path)` pair, the record the conformance suite asserts on.

## Run the conformance suite

```bash
cargo build -q -p mock-nexus
python3 -m unittest discover clients/python
```

The suite spawns `mock-nexus` itself, one process per test on a free port, and reads the scenario list live from `mock-nexus --print-scenarios`.
To point it at another binary, set `MOCK_NEXUS_BIN` to the path.

## The scenario contract and the skip table

The mock's scenario list is the conformance contract, and the suite refuses to drift from it:
`test_unit_every_scenario_is_classified` requires every listed scenario to have a test method named `test_<scenario with dashes as underscores>` or a documented row in `SKIP`, in both directions.
A scenario added to `crates/mock-nexus/src/scenario.rs` fails this suite until it is classified, which is the point.

The current skip rows, with the reasons:

| Scenario | Reason |
|:---------|:-------|
| `doc-drift` | The drift toggle is a library-only knob (`MockNexus::enable_drift`), so the binary serves plain storage. the client-side half, `version.json` being an ordinary name, is pinned under `atomic` |
| `no-service` | The scenario only breaks `/service/rest/v1/repositories`, and the client has no service-repos surface |
| `readonly` | The scenario only answers 403 to `DELETE`, and the client implements no delete |

## Lint, types and the suite

The client is gated like the rest of the repository.
`just check` runs `just test-python`, which builds the mock binary and runs the suite below, and the prek hooks run ruff (lint plus format check) and ty over the module.

Ruff's width is 120 and E501 is off on purpose: the repository mandates unwrapped sentences in comments and docstrings, and a line-length complaint would fight that rule on exactly those lines.
The configuration lives in `pyproject.toml`.
Only the shipped module `nxr.py` is type-checked, the suite is lint-checked, so a loose fixture in a test never blocks a commit.

## Porting note

What a real integrator must implement, at minimum, beyond the verbs:

1. Basic auth attached to every request, resolved once per invocation.
2. The retry classification: retry transport failures and 5xx with bounded backoff, honor `Retry-After` on any retryable status, never retry 401/403, never follow redirects.
3. An attempt-level stall bound (a read/write timeout), because a server can hold a connection forever.
4. The marker order on upload, the digest check on download, and the strict marker parse.
5. The lengthless-answer rule: a 2xx without `Content-Length` is unverifiable, never empty.
6. `Accept-Encoding: identity` on every request, unless the client also implements the `zstd` GET decoding, so digests always describe the stored bytes.

Deliberate simplifications of this reference, safe to outgrow: bodies are held in memory (no streaming, no Range resume, no part files), one connection per attempt (no keep-alive), and TLS is whatever the runtime verifies by default (the protocol's `--tls-insecure` escape hatch has no equivalent here).
