Puck — Python client GitHub issue
- Installation
- Quick start
- Getting a Puck client
- Looking up a class
- Instantiation
- Calling methods
- Properties vs methods
- Return values
- Errors
- Versioning
- Sync vs async
- Open questions
vibecode
{"vibecode": { "doc": "puck-python", "role": "design sketch for the general shape of a Python implementation of the Puck protocol; covers package layout, lookup, instantiation, method dispatch, return-value handling, errors, versioning, and open questions", "status": "sketch_no_implementation_yet", "key_concepts": ["python_puck_client", "module_level_default_puck", "class_lookup_and_instantiation", "attribute_method_dispatch", "primitive_and_object_returns", "error_to_exception_mapping", "versioning_via_vN_uns_segment", "sync_first_async_later"], "audience": ["python_developers", "puck_client_implementors"], "example_universe": "Star Trek", "example_language": "Python" }}
This doc sketches the general shape of a Python implementation of the Puck protocol. Status: design sketch — no implementation yet. The protocol itself is specced in puck.md; this doc is about how a Python package would wrap it idiomatically.
The goal: a Python developer can pip install puck, point at a Puck server, and use remote objects with the same shape they'd use any Python library.
Installation GitHub issue
pip install puck
The package has minimal dependencies — an HTTP client (likely httpx) and dataclasses/typing from the standard library.
Quick start GitHub issue
import puck
Geo = puck.lookup('puck.uno/geo')
hq = Geo(lat=37.7980, lon=-122.4626)
hq.weather # current weather report
hq.congressional_district # returns another Puck object
That's the full surface for the common case: import, look up the class, instantiate, call methods.
Getting a Puck client GitHub issue
Module-level default. puck.lookup(...) and friends operate on a module-level default client configured from the environment (env vars, config file, or programmatic setup at import time):
import puck
Geo = puck.lookup('puck.uno/geo') # uses the module default client
Explicit client. When a script needs more than one client (different endpoints, different credentials), construct them explicitly:
prod = puck.Puck(endpoint='https://puck.acme.com', ...)
staging = puck.Puck(endpoint='https://staging.puck.acme.com', ...)
Geo = prod.lookup('puck.uno/geo')
Configuration sources (in precedence order, last wins):
- Defaults baked into the package
- Env vars (
PUCK_ENDPOINT,PUCK_TOKEN, ...) - A config file (
~/.puck/config.tomlor similar — TBD) - Constructor kwargs to
puck.Puck(...)
Looking up a class GitHub issue
Geo = puck.lookup('puck.uno/geo')
puck.lookup(uns) returns a Python class object. Under the hood it's a dynamically generated class that wraps the remote class's method table. The returned object is usable anywhere a Python class is: instantiate it, subclass it (if the package supports that), introspect it.
Failure case. If the UNS doesn't resolve (unknown, withdrawn, or a version segment that doesn't exist), lookup raises puck.NotFoundError.
Instantiation GitHub issue
Python's class-call syntax wraps the protocol-level constructor:
hq = Geo(lat=37.7980, lon=-122.4626)
The client library translates this to a wire-level .new(...) call on the remote class. All Puck params are named — every keyword arg you pass becomes a named entry in the wire-level params hash. Puck has no positional-parameter concept; the Python wrapper only emits keyword args on the wire.
The returned hq is a Python object holding a reference to the remote instance (a UNS for the instance, opaque to the developer).
Calling methods GitHub issue
Method calls look like ordinary Python:
hq.weather # no-arg method (see Properties vs methods below)
hq.map_image(zoom=14) # method with one keyword arg
hq.move(dx=5, dy=10) # method with multiple keyword args
Puck has no positional-parameter concept — every arg on the wire is a named entry in a JSON hash. Python callers always pass keyword args; positional calls at the Python level aren't supported.
Each call:
- Marshal the args to JSON.
- POST
{uns, method, params, chain}to the server. - Wait for the response.
- Unmarshal the result.
- Return it (as a primitive or another Puck wrapper — see Return values).
The library hides all five steps. The caller sees a method call.
Properties vs methods GitHub issue
Python distinguishes attribute access (hq.weather) from method calls (hq.weather()). The Puck protocol treats both as method dispatch. There are two ways the Python wrapper could handle this:
- Always require parens.
hq.weather()even for no-arg "properties." Mechanically simpler; less Pythonic. - Lazy attribute access.
hq.weathertriggers the remote call;hq.darken(0.2)works becausehq.darkenreturns a callable. Reads more Pythonic; needs careful__getattr__design.
Tentative direction: lazy attribute access, with parens still accepted (hq.weather() does the same thing as hq.weather). The remote class's method definition declares which form is conventional; the client follows. Open question.
Return values GitHub issue
A remote method's return value comes back as:
| Wire type | Python type |
|---|---|
| Number | int or float |
| String | str |
| Boolean | bool |
| Null | None (with optional flavor metadata on a wrapper — TBD) |
| Array | list of converted values |
| Hash | dict of converted values |
| Reference to a Puck object | wrapper instance that you can call methods on |
The Puck-object case is the interesting one. When a method returns puck.uno/congressional_district (as in the puck.md example), the Python wrapper transparently constructs a Python class instance representing the remote CongressionalDistrict. You don't have to do another lookup — the reference is enough.
district = hq.congressional_district # remote CongressionalDistrict instance
district.representative # remote method call on the new object
Errors GitHub issue
Protocol-level errors map to Python exceptions. All inherit from puck.PuckError:
| Protocol UNS | Python exception |
|---|---|
puck.uno/error/not_found |
puck.NotFoundError |
puck.uno/error/method_not_found |
puck.MethodNotFoundError |
puck.uno/error/transport |
puck.TransportError |
puck.uno/error/auth |
puck.AuthError |
| (any other registered Puck exception class) | puck.RemoteError with .uns attribute |
A remote method that itself raises an exception is propagated as a puck.RemoteError. The exception's .uns attribute carries the remote class's UNS, .message carries the message, and .stack_trace carries the preserved remote stack trace.
try:
hq.weather
except puck.TransportError as e:
log.warning(f'weather lookup failed: {e}')
except puck.RemoteError as e:
log.error(f'remote raised {e.uns}: {e.message}')
Versioning GitHub issue
The Puck protocol takes a deliberately light approach to versioning (see protocol.md § Versioning). The Python client doesn't expose a per-call version-window context manager — there's nothing for it to wrap. To use a specific API version, look up the class at its versioned UNS:
Geo = puck.lookup('puck.uno/geo/v2')
Caspian's blockchain-signed versioning of library identity is a separate story; if you need that, see blockchain.md.
Sync vs async GitHub issue
v1: synchronous only. Every method call blocks until the response arrives. This matches the most common Python usage pattern, keeps the library tiny, and avoids forcing every consumer into an async event loop.
Async support is a planned later addition — likely a parallel puck.async_ namespace (puck.async_.lookup(...), await
hq.weather) so sync and async users don't step on each other. Shape to be decided when async demand is real.
Open questions GitHub issue
These are flagged so the eventual implementation work knows what isn't decided yet.
- Property-vs-method dispatch (see §8).
- Subclassing remote Puck classes. Can a Python developer subclass
Geoto add local methods, then override one of the remote methods? How does dispatch resolve when both a local and a remote method exist? - Client-side caching. When is it safe to cache a remote method's result? Annotations from the server (cacheable / no-cache / TTL)? Per-instance vs per-class?
- Connection management. Long-lived HTTPS connection (keep-alive) vs request-per-call. Connection pooling. Timeouts.
- Authentication. Token in env var? Per-client credential set? Integration with
keyring? OAuth flow for interactive use? - Type stubs. Auto-generate
.pyistubs from a remote class's definition so editors and type-checkers know the method signatures? - Threading. Is the module-level default client thread-safe? Per-thread default clients?