Metadata-Version: 2.4
Name: marionette-mc
Version: 0.1.0a1
Summary: Typed asyncio client for the Marionette Minecraft bridge
Project-URL: Repository, https://github.com/Prattlemob/marionette
Author: Prattlemob
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: websockets==15.0.1
Description-Content-Type: text/markdown

# marionette-mc

Typed asyncio client for the [Marionette](https://github.com/Prattlemob/marionette)
Minecraft bridge. Python 3.11+, protocol **2**, package **0.1.0a1** (independent
of mod versions). MIT, confirmed by the project owner. This is an unpublished
alpha candidate; no package upload or published-consumer acceptance is claimed.

From this checkout, install for development with `python -m pip install -e ./python`.
After an explicitly authorized publication, consumers must pin
`marionette-mc==0.1.0a1` rather than silently accepting new prereleases.
A local wheel installation verifies packaging only.

```python
import asyncio
from marionette_mc import connect

async def main():
    async with connect(required_capabilities=["bridgeSafety", "playerState"]) as client:
        print(client.hello)
        await client.configure(rate_divisor=1, sections=["player"])
        print(await client.next_observation(timeout=5))
        try:
            await client.input(forward=True)
            await asyncio.sleep(0.2)
        finally:
            await client.release()

asyncio.run(main())
```

Run only against a disposable, rendered world with a clear walking area.
Context exit closes the session even on exceptions/cancellation; controller loss
triggers the mod's release-all safety. `release()` explicitly clears queued
controller intent. Neither sending nor closing is an action acknowledgment.
Keep the asyncio event loop responsive: move blocking/CPU work off the loop.
The websockets library answers server pings automatically; there is no reconnect
or command replay policy in this package. Native connections omit Origin and
ignore environment proxies. Current local trust has no authentication.

## API and outcomes

- `connect(uri=..., role="controller" | "observer", sections=..., required_capabilities=...)`
  yields one `Client` after validating protocol 2 and required capability flags.
  `client.require(...)` checks additional capabilities. Missing flags are unsupported.
- `input(forward=..., back=..., left=..., right=..., jump=..., sneak=..., sprint=...,
  attack=..., use=..., tap=["jump", "attack", "use"], hotbar=0)` applies only supplied
  fields. Methods check capabilities for taps/interactions and guard observer roles.
- `look(yaw=..., pitch=..., mode="instant" | "delta" | "smooth", speed=...)` or
  `look(mode="smooth", x=..., y=..., z=..., speed=...)` controls the camera.
  Success is silent; use observations for convergence. No completion event exists.
- `configure(rate_divisor=1, sections=["player"])` selects this session's stream.
  `sections=[]` requests cadence-only frames. Omitted arguments leave settings unchanged.
- `inventory(op, menu=..., source=..., destination=..., hotbar=..., all=False,
  animated=False, timeout=...)` supports open/inspect/move/swap/equip/drop/close.
  Obtain `menu_ref(result["menu"])` from a recent inspect before mutations.
  Results describe client prediction, not authoritative server acknowledgment.
- `next_observation(timeout=...)` / `observations()` consume latest observations.
  A slow consumer skips frames; `observations_coalesced` counts replacements.
- `next_reply(timeout=...)` / `replies()` consume uncorrelated errors and late or
  unmatched inventory results. Awaited inventory errors raise `ServerError` with
  the original typed `.error`; uncorrelated errors remain in this reliable stream.
  Applications should service it alongside their observation/gameplay loop.
- `wait_closed()` returns local `Disconnect(code, reason, cause)`;
  `Disconnected.outcome` carries it when an operation/stream cannot continue.
  This is a Python lifecycle outcome, never a fabricated protocol event.
  Reliable replies already received can be drained after closure. Observations
  cannot be read as live state after disconnect.

TypedDict wire models (including nested player, effects, inventory menus/slots,
commands and all four incoming types) live in `marionette_mc.messages`. Decoding
checks required fields/types and retains unknown additive fields. `ServerError`,
`VersionError`, `CapabilityError`, `RoleError`, `CapacityError` and `InvalidMessage`
(the latter in `messages`) distinguish failure causes. Opening failures preserve
websockets/OS exceptions; handshake timeout is `TimeoutError`.

Each inventory call uses a unique session ID. `RequestTimeout.request_id` lets a
caller identify the later reply. Timeout or task cancellation **does not cancel
server work**. Late results/errors go to `next_reply()`; do not blindly resend a
move/drop. A disconnect leaves outstanding outcomes unknown. Re-inspect state
in a separately chosen new session before deciding what to do. To stop current
controller work, use release or close, understanding already-completed world
changes cannot be undone. An unmatched reply is not proof that it is safe to retry.

Default bounds: 64 reliable replies, 32 pending requests, one latest observation,
128 KiB incoming frames, 16 transport frames, 32 KiB write high-water mark,
64 KiB outgoing commands, 5-second connect/hello timeout, 10-second request/write
timeout and 1-second close timeout. Queue limits are configurable integers 1–1024.
Reliable overflow closes with an explicit local `CapacityError` cause; replies
beyond capacity are lost with unknown outcomes. The reader never waits for a
consumer queue and therefore continues servicing WebSocket liveness. Streams have
one logical consumer each; concurrent readers compete rather than receive broadcasts.

## Local verification and publication candidate

From the repository root:

```sh
python3.11 -m venv .venv
.venv/bin/pip install -r python/requirements-dev.txt
.venv/bin/pip install --no-build-isolation -e ./python
.venv/bin/python -m unittest discover -s python/tests -v
(cd python && ../.venv/bin/mypy)
.venv/bin/python -m build --no-isolation python
./gradlew build
```

`requirements-dev.txt` pins build/test dependencies, including transitives.
The shared `tests/fixtures/protocol2.json` ships in the sdist; Java's
`PythonCompatibilityTest` parses those commands and compares its actual message
builders to the same expected frames. Offline tests use real local WebSockets.
The wheel contains only the client, type marker and license/metadata. The sdist
also contains self-contained offline tests and fixtures. Neither includes worlds,
private harnesses, consumer policy, caches, credentials or mod build output.

Before publishing: review exact distributions and SHA256 hashes, resolve release
prerequisites (including the project's authentication/provisioning gate), establish
PyPI project ownership/credentials, obtain explicit upload authorization, then
upload only the reviewed artifacts. Registration alone is not upload permission.
After publication, a clean environment must install the exact published version
and pass rendered observation/movement/release smoke acceptance. Local wheel or
sdist success does not satisfy that milestone or consumer integration gate.
