Skip to content
Musher Docs

Python SDK Concepts

This page explains the core concepts behind the Python SDK's object model. Understanding these will help you choose the right function for each use case.

Resolve vs Pull

The SDK offers two ways to interact with a bundle:

  • resolve() — fetches manifest metadata only. No asset content is downloaded. Returns a ResolveResult.
  • pull() — fetches metadata and downloads all asset content, verifying each asset's SHA-256 checksum. Returns a Bundle.

Use resolve() when you only need to check which version a reference points to, list what assets exist, or validate a bundle reference — without paying the cost of downloading files.

python
import musher

# Metadata only — no downloads
result = musher.resolve("acme/code-review-kit:^1.0")

if result.manifest:
    for layer in result.manifest.layers:
        print(f"{layer.logical_path} ({layer.asset_type})")

# Full download — assets verified and cached locally
bundle = musher.pull("acme/code-review-kit:^1.0")

for fh in bundle.files():
    print(fh.text())

Manifest Layers vs Typed Handles

These two concepts represent different views of the same bundle content, depending on whether you used resolve() or pull().

Manifest layers

A ResolveResult contains a manifest whose layers list describes each asset as a raw entry: its logical path, asset type, digest, and size. Layers are metadata — they tell you what's in the bundle without giving you the content.

Typed handles

A Bundle provides typed accessor methods that return handle objects for each asset category:

  • bundle.files()list[FileHandle]
  • bundle.skills()list[SkillHandle]
  • bundle.prompts()list[PromptHandle]
  • bundle.toolsets()list[ToolsetHandle]
  • bundle.agent_specs()list[AgentSpecHandle]

Each handle gives you the actual content via methods like .text(), .bytes(), or .parse_json(). Handles are the primary way you interact with bundle assets after pulling.

python
bundle = musher.pull("acme/code-review-kit")

# Typed handle access
for skill in bundle.skills():
    print(f"{skill.name}: {skill.description}")
    print(skill.skill_md().text())

# Single asset lookup (raises KeyError if not found)
toolset = bundle.toolset("lint-rules")
config = toolset.parse_json()

Client Architecture

The SDK provides three ways to make calls:

Module-level functions

musher.pull() and musher.resolve() use a shared global client internally. This is the simplest path and works for most applications.

Synchronous client

Client is a synchronous wrapper that uses a background event loop internally. This design makes it safe to use in Jupyter notebooks, scripts, and other environments that may already have a running event loop. Use it as a context manager to ensure proper cleanup:

python
from musher import Client

with Client() as client:
    bundle = client.pull("acme/code-review-kit")

Asynchronous client

AsyncClient is a native asyncio client for applications that are already async. All methods return awaitables:

python
from musher import AsyncClient

async with AsyncClient() as client:
    bundle = await client.pull("acme/code-review-kit")

Credential Resolution

The SDK discovers credentials automatically by checking four sources in order, using the first one found:

  1. Programmatic — pass a token directly via configure() or the client constructor (takes precedence over all other sources)
  2. MUSHER_API_KEY environment variable — simplest option for CI and local development
  3. OS keyring (musher/<host>) — set by the Musher CLI after musher auth login
  4. Credential file (~/.local/share/musher/credentials/<host>/api-key) — file-based fallback