From a07c5d4ed89b06a0d12e6b16e6719a1c808ebd30 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Mon, 30 Mar 2026 16:14:04 +0000 Subject: [PATCH] feat: polish repo with docs overhaul, CI matrix, DX improvements, and community templates - Rewrite README with full API reference, auth docs, framework integrations, and cache management - Move SECURITY.md and CONFIGURATION.md to .github/ and docs/ respectively - Add GitHub issue templates (bug report, feature request), PR template, and dependabot config - Configure CI matrix strategy for Python version testing - Add lefthook pre-commit hooks for ruff format and lint checks - Add `task check:fix` command for auto-fixing lint issues - Add pytest-xdist for parallel test execution, bump coverage threshold to 90% - Clean up examples with consistent import style - Remove empty adapters/__init__.py module Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md => .claude/CLAUDE.md | 14 +- .github/ISSUE_TEMPLATE/bug_report.yml | 32 ++++ .github/ISSUE_TEMPLATE/feature_request.yml | 18 ++ .github/PULL_REQUEST_TEMPLATE.md | 7 + SECURITY.md => .github/SECURITY.md | 0 .github/dependabot.yml | 19 +++ .github/workflows/ci.yml | 6 +- README.md | 156 ++++++++++++++---- Taskfile.yml | 6 + CONFIGURATION.md => docs/configuration.md | 23 +-- examples/README.md | 20 ++- examples/basics/bundle_resources.py | 5 +- examples/basics/inspect_manifest.py | 5 +- examples/basics/pull_bundle.py | 11 +- examples/claude/export_plugin.py | 9 +- examples/claude/install_project_skills.py | 5 +- examples/openai/hosted_inline_skill.py | 5 +- examples/openai/local_shell_skill.py | 11 +- examples/pydantic_ai/dynamic_instructions.py | 5 +- .../pydantic_ai/instructions_from_bundle.py | 5 +- examples/pydantic_ai/structured_output.py | 5 +- lefthook.yml | 52 ++---- pyproject.toml | 4 +- src/musher/__init__.py | 2 +- src/musher/adapters/__init__.py | 1 - uv.lock | 24 +++ 26 files changed, 292 insertions(+), 158 deletions(-) rename CLAUDE.md => .claude/CLAUDE.md (63%) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md rename SECURITY.md => .github/SECURITY.md (100%) create mode 100644 .github/dependabot.yml rename CONFIGURATION.md => docs/configuration.md (81%) delete mode 100644 src/musher/adapters/__init__.py diff --git a/CLAUDE.md b/.claude/CLAUDE.md similarity index 63% rename from CLAUDE.md rename to .claude/CLAUDE.md index c32ea36..9cbbd2b 100644 --- a/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -6,15 +6,18 @@ Python SDK for programmatically pulling and using Musher bundle assets. ``` src/musher/ -├── _types.py # Enums, BundleRef, OCI constants (no deps) +├── _types.py # Enums, BundleRef (no deps) ├── _errors.py # Exception hierarchy (no deps) ├── _paths.py # Platform-aware directory resolution (deps: platformdirs) ├── _config.py # Global configuration (deps: _paths) +├── _auth.py # Credential resolution chain (deps: _paths) ├── _bundle.py # Pydantic models for bundles/manifests (deps: _types) -├── _cache.py # XDG-compliant disk cache (deps: _types, _errors) -├── _oci.py # Low-level OCI interaction (deps: _types, _errors) +├── _cache.py # Content-addressable disk cache (deps: _types, _errors) +├── _cache_info.py # Cache inspection types (no deps) +├── _http.py # HTTP transport with error mapping (deps: _types, _errors) +├── _handles.py # Typed resource handles (deps: _types, _export) +├── _export.py # Framework export dataclasses (no deps) ├── _client.py # Client + AsyncClient (deps: all above) -├── adapters/ # Future framework adapters (LangChain, LlamaIndex) └── __init__.py # Public API re-exports ``` @@ -27,6 +30,7 @@ task check:lint # Ruff linter task check:types # basedpyright type checker task check:test # pytest task check:format # Format with ruff +task check:fix # Auto-fix lint issues and format ``` ## Code Standards @@ -35,4 +39,4 @@ task check:format # Format with ruff - Pydantic models use `alias_generator=to_camel` for camelCase wire format - All public API is re-exported from `__init__.py` - Internal modules are prefixed with `_` (private) -- Stub methods raise `NotImplementedError` until implemented +- `BundleCache` is internal — not exported in `__all__` diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..4bb8648 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,32 @@ +name: Bug Report +description: Report a bug in the Musher Python SDK +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: Description + description: A clear description of the bug. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Minimal code or steps to reproduce the issue. + validations: + required: true + - type: input + id: version + attributes: + label: SDK version + description: "Output of: python -c \"import musher; print(musher.__version__)\"" + validations: + required: true + - type: input + id: python-version + attributes: + label: Python version + description: "Output of: python --version" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..cb8739f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,18 @@ +name: Feature Request +description: Suggest a feature for the Musher Python SDK +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this solve? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: How would you like this to work? + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4b0487c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## Summary + + + +## Test plan + + diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..88c1dee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + groups: + python-deps: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3be344d..b20b8cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,10 @@ permissions: jobs: check: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.13"] steps: - uses: actions/checkout@v6 @@ -25,7 +29,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: ${{ matrix.python-version }} - run: task setup diff --git a/README.md b/README.md index 1e950c5..5ccddbd 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,184 @@ +
+ # Musher Python SDK [![CI](https://github.com/musher-dev/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/musher-dev/python-sdk/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/musher-sdk)](https://pypi.org/project/musher-sdk/) -Python SDK for the [Musher](https://musher.dev) bundle distribution platform. Pull versioned AI agent asset bundles (prompts, configs, tool definitions) into your Python applications. +Python SDK for the [Musher](https://musher.dev) bundle distribution platform. Pull versioned AI agent asset bundles — prompts, tool definitions, agent specs, and skills — into your Python applications. + +
## Installation +Requires **Python 3.13+**. + ```bash pip install musher-sdk ``` +## Authentication + +Set your API key as an environment variable: + +```bash +export MUSHER_API_KEY="msk_..." +``` + +The SDK resolves credentials automatically in this order: + +1. `MUSHER_API_KEY` environment variable +2. OS keyring (`musher/{hostname}`) +3. Credential file (`/credentials//api-key`, must be `0600`) + +You can also pass a token directly: + +```python +musher.configure(token="msk_...") +``` + ## Quick Start ```python import musher -# Explicit token -musher.configure(token="your-token") - bundle = musher.pull("myorg/my-bundle:1.0.0") -for asset in bundle.files(): - print(f"{asset.logical_path}: {len(asset.text())} chars") +for f in bundle.files(): + print(f"{f.logical_path}: {len(f.text())} chars") ``` ### Async ```python +import musher + async with musher.AsyncClient() as client: bundle = await client.pull("myorg/my-bundle:1.0.0") ``` -### Sync +### Sync Client ```python +import musher + with musher.Client() as client: + bundle = client.pull("myorg/my-bundle:1.0.0") + result = client.resolve("myorg/my-bundle:1.0.0") - asset = client.fetch_asset("asset-id", version="1.0.0") + + asset = client.fetch_asset( + "prompts/system.md", + namespace="myorg", + slug="my-bundle", + version="1.0.0", + ) ``` -## Configuration +## Working with Bundles -### Credential Chain +Bundles provide typed accessors for each resource type: -The SDK resolves credentials automatically in this order: +```python +bundle = musher.pull("myorg/my-bundle:1.0.0") -1. **Environment variables** — `MUSHER_API_KEY` -2. **OS keyring** — host-scoped service `musher/{hostname}` -3. **File fallback** — `/credentials//api-key` (must be `0600` permissions) +# Prompts +prompt = bundle.prompt("system") +print(prompt.text()) + +# Skills +for skill in bundle.skills(): + print(skill.name, skill.description) + +# Toolsets and agent specs +toolset = bundle.toolset("search-tools") +data = toolset.parse_json() + +spec = bundle.agent_spec("reviewer") +config = spec.parse_json() +``` + +Filter to a subset of resources with `select()`: + +```python +selection = bundle.select(skills=["code-review"], prompts=["system"]) +``` + +## Framework Integrations + +The SDK integrates with popular AI agent frameworks: + +- **Claude** — export bundles as Claude plugins or install skills to `.claude/skills/` ([examples](examples/claude/)) +- **OpenAI Agents** — export skills as local directories or inline zips for the OpenAI shell tool ([examples](examples/openai/)) +- **PydanticAI** — use bundle prompts as agent instructions with structured output ([examples](examples/pydantic_ai/)) + +## Configuration ### Registry URL -The registry URL is resolved from environment variables: +```bash +export MUSHER_API_URL="https://custom-registry.example.com" +``` -- `MUSHER_API_URL` -- Default: `https://api.musher.dev` +Default: `https://api.musher.dev` ### Programmatic Configuration ```python +from pathlib import Path import musher -# All parameters are optional — omitted values auto-discover musher.configure( - token="your-token", - registry_url="https://custom.dev", - cache_dir=Path("/tmp/cache"), + token="msk_...", + registry_url="https://custom-registry.example.com", + cache_dir=Path("/tmp/musher-cache"), + verify_checksums=True, + timeout=30.0, + max_retries=2, ) ``` -### Cache Behavior +All parameters are optional — omitted values are auto-discovered. + +See [Configuration](docs/configuration.md) for the full reference (directory layout, credential chain, cache structure, TTL defaults). + +### Cache The SDK uses a content-addressable disk cache: -- Blobs are stored by SHA-256 hash (shared across registries) -- Manifests and refs are partitioned by registry hostname -- Manifests have a configurable TTL (default 24h); refs default to 5min -- `clean()` removes expired entries and garbage-collects unreferenced blobs +- Blobs stored by SHA-256 hash, shared across registries +- Manifests and refs partitioned by registry hostname +- Manifests TTL: 24h; refs TTL: 5min +- `cache_clean()` removes expired entries and garbage-collects unreferenced blobs + +Cache management functions are available at the module level and on the `Client`: + +```python +import musher + +info = musher.cache_info() # cache statistics +musher.cache_remove("myorg/my-bundle:1.0.0") # remove a specific bundle +musher.cache_clean() # remove expired entries +musher.cache_clear() # remove all cached data +path = musher.cache_path() # cache directory path +``` -## What's Implemented +## Features -- `resolve()` — resolve bundle references to manifests -- `fetch_asset()` — fetch individual assets by ID -- `pull()` — resolve + fetch all assets + verify checksums +- `pull()` / `pull_async()` — resolve + fetch all assets + verify checksums +- `resolve()` / `resolve_async()` — resolve bundle references to manifests +- `fetch_asset()` — fetch individual assets by logical path - Sync (`Client`) and async (`AsyncClient`) clients +- Typed handles: `SkillHandle`, `PromptHandle`, `ToolsetHandle`, `AgentSpecHandle` +- `bundle.select()` — filter resources by type - Content-addressable cache with TTL and garbage collection -- Typed handles: skills, prompts, toolsets, agent specs -- `export_claude_plugin()` — export bundle as a Claude plugin -- `install_claude_skills()` — install skills to a directory +- Cache management: `cache_info()`, `cache_remove()`, `cache_clear()`, `cache_clean()`, `cache_path()` +- `export_claude_plugin()` / `install_claude_skills()` — Claude integration +- `export_openai_local_skill()` / `export_openai_inline_skill()` — OpenAI Agents integration + +## Examples + +See the [examples/](examples/) directory for runnable code samples covering basic usage, Claude, OpenAI Agents, and PydanticAI integrations. ## License diff --git a/Taskfile.yml b/Taskfile.yml index 7b26310..3e74588 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -19,6 +19,12 @@ tasks: - task: check:types - task: check:test + check:fix: + desc: Auto-fix lint issues and format + cmds: + - uv run ruff check --fix . + - uv run ruff format . + check:format: desc: Format code with ruff cmds: diff --git a/CONFIGURATION.md b/docs/configuration.md similarity index 81% rename from CONFIGURATION.md rename to docs/configuration.md index c5a7049..d9d9622 100644 --- a/CONFIGURATION.md +++ b/docs/configuration.md @@ -32,8 +32,7 @@ On Windows, the SDK uses a flat layout under `%LOCALAPPDATA%\musher\` with categ | Variable | Purpose | |---|---| -| `MUSHER_API_URL` | Registry URL (checked first) | -| `MUSHER_BASE_URL` | Registry URL alias | +| `MUSHER_API_URL` | Registry URL | Default: `https://api.musher.dev` @@ -46,22 +45,9 @@ The SDK resolves credentials in this order, stopping at the first match: 1. **Environment variables** — `MUSHER_API_KEY` 2. **OS keyring** — service `musher/{hostname}`, username `api-key` - Hostname is derived from the registry URL (e.g. `musher/api.musher.dev`) -3. **Profile config file** — `/config.toml` - - Format: `[profile.] api_key = "..."` - - Default profile: `"default"` -4. **File fallback** — `/api-key` +3. **File fallback** — `/credentials//api-key` - Must have `0600` permissions (owner-only); rejected otherwise -### Profile Config Format - -```toml -[profile.default] -api_key = "mush_prod_..." - -[profile.staging] -api_key = "mush_staging_..." -``` - ## Cache Structure ``` @@ -93,14 +79,13 @@ $cache_root/ ```python import musher +from pathlib import Path musher.configure( token="...", # explicit token - api_key="...", # alias for token registry_url="https://...", # explicit registry URL - api_url="https://...", # alias for registry_url cache_dir=Path("..."), # override cache directory ) ``` -When neither `token` nor `api_key` is provided, the credential chain is used automatically. When neither `registry_url` nor `api_url` is provided, the URL env var chain is checked. +When `token` is not provided, the credential chain is used automatically. When `registry_url` is not provided, the URL env var chain is checked. diff --git a/examples/README.md b/examples/README.md index efa6b9f..503c142 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,7 @@ # Musher SDK Examples +> Part of the [Musher Python SDK](../README.md). + Examples for the Musher Python SDK, organized by use case. ## Prerequisites @@ -38,10 +40,18 @@ Credentials are auto-discovered in this order: To override explicitly in code: `musher.configure(token="your-token")`. -## Running +## Bundles + +Examples use published bundles from [hub.musher.dev](https://hub.musher.dev/): -Replace placeholder bundle references (e.g. `"acme/prompt-library:1.2.0"`) with -real ones from your Musher registry, then: +| Bundle | Version | Used by | +|--------|---------|---------| +| `musher-examples/agent-toolkit` | 2.0.0 | `basics/pull_bundle.py`, `basics/inspect_manifest.py` | +| `musher-examples/prompt-library` | 1.2.0 | `basics/bundle_resources.py`, `pydantic_ai/*` | +| `musher-examples/code-review-kit` | 1.2.0 | `claude/*`, `openai/local_shell_skill.py` | +| `musher-examples/data-workflows` | 1.0.0 | `openai/hosted_inline_skill.py` | + +## Running ```bash uv run python examples/openai/hosted_inline_skill.py @@ -61,7 +71,3 @@ uv run python examples/openai/hosted_inline_skill.py | `pydantic_ai/instructions_from_bundle.py` | Use bundle prompts as PydanticAI agent instructions | `pydantic-ai` | Working | | `pydantic_ai/structured_output.py` | Structured output with bundle-managed prompts | `pydantic-ai` | Working | | `pydantic_ai/dynamic_instructions.py` | Dynamic instructions and tools from bundles | `pydantic-ai` | Working | - -All examples use implemented SDK methods. Bundle references (e.g. -`"acme/prompt-library:1.2.0"`) are placeholders — replace them with a real -bundle ref from your Musher registry before running. diff --git a/examples/basics/bundle_resources.py b/examples/basics/bundle_resources.py index d9eadcf..0778ac1 100644 --- a/examples/basics/bundle_resources.py +++ b/examples/basics/bundle_resources.py @@ -2,13 +2,10 @@ import musher -# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") -bundle = musher.pull("acme/prompt-library:1.2.0") +bundle = musher.pull("musher-examples/prompt-library:1.2.0") # Access versioned prompts by name system_prompt = bundle.prompt("system") diff --git a/examples/basics/inspect_manifest.py b/examples/basics/inspect_manifest.py index 2a0c549..7de6f6a 100644 --- a/examples/basics/inspect_manifest.py +++ b/examples/basics/inspect_manifest.py @@ -2,10 +2,7 @@ import musher -# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - -result = musher.resolve("acme/agent-toolkit:2.0.0") +result = musher.resolve("musher-examples/agent-toolkit:2.0.0") print(f"Bundle: {result.ref} v{result.version}") print(f"State: {result.state}") diff --git a/examples/basics/pull_bundle.py b/examples/basics/pull_bundle.py index 2499943..7b16ef5 100644 --- a/examples/basics/pull_bundle.py +++ b/examples/basics/pull_bundle.py @@ -2,22 +2,19 @@ import musher -# NOTE: Bundle references below (e.g. "acme/agent-toolkit:2.0.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") -bundle = musher.pull("acme/agent-toolkit:2.0.0") +bundle = musher.pull("musher-examples/agent-toolkit:2.0.0") # List all files for fh in bundle.files(): print(f"{fh.logical_path} ({fh.media_type or 'unknown'})") # Get a specific file -prompt = bundle.file("prompts/main.txt") +prompt = bundle.file("prompts/system.md") if prompt: - print(f"Prompt content: {prompt.text()}") + print(f"System prompt: {prompt.text()[:120]}...") # For reproducible deployments, pass a digest ref instead of a version tag: -# "acme/agent-toolkit@sha256:abc123def456" +# "musher-examples/agent-toolkit@sha256:abc123def456" diff --git a/examples/claude/export_plugin.py b/examples/claude/export_plugin.py index b4f2a9a..6b0e23e 100644 --- a/examples/claude/export_plugin.py +++ b/examples/claude/export_plugin.py @@ -13,20 +13,17 @@ import musher -# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") -bundle = musher.pull("acme/engineering-workflows:2.0.0") +bundle = musher.pull("musher-examples/code-review-kit:1.2.0") # Select only the skills needed for this session selection = bundle.select(skills=["researching-repos", "drafting-release-notes"]) # Export as a local Claude plugin with a namespaced plugin name. -# Skills will be accessible as "team-workflows:researching-repos", etc. -plugin = selection.export_claude_plugin("team-workflows", dest=Path("./plugins")) +# Skills will be accessible as "code-review:researching-repos", etc. +plugin = selection.export_claude_plugin("code-review", dest=Path("./plugins")) print(f"Plugin exported to: {plugin.path}") # Verify only the selected skills are present diff --git a/examples/claude/install_project_skills.py b/examples/claude/install_project_skills.py index 8d95109..fbccb20 100644 --- a/examples/claude/install_project_skills.py +++ b/examples/claude/install_project_skills.py @@ -13,13 +13,10 @@ import musher -# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") -bundle = musher.pull("acme/engineering-workflows:2.0.0") +bundle = musher.pull("musher-examples/code-review-kit:1.2.0") # Install specific skills to the project-level Claude skills directory. # clean=True removes stale Musher-managed skill installs for this bundle diff --git a/examples/openai/hosted_inline_skill.py b/examples/openai/hosted_inline_skill.py index ebf5f79..31ba673 100644 --- a/examples/openai/hosted_inline_skill.py +++ b/examples/openai/hosted_inline_skill.py @@ -9,15 +9,12 @@ import musher -# NOTE: Bundle references below (e.g. "acme/data-workflows:2.0.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") async def main() -> None: - bundle = musher.pull("acme/data-workflows:2.0.0") + bundle = musher.pull("musher-examples/data-workflows:1.0.0") skill = bundle.skill("csv-insights") inline = skill.export_openai_inline_skill() diff --git a/examples/openai/local_shell_skill.py b/examples/openai/local_shell_skill.py index 30af950..c360f6f 100644 --- a/examples/openai/local_shell_skill.py +++ b/examples/openai/local_shell_skill.py @@ -18,9 +18,6 @@ import musher -# NOTE: Bundle references below (e.g. "acme/engineering-workflows:2.0.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") @@ -65,12 +62,12 @@ async def __call__(self, request: ShellCommandRequest) -> ShellResult: async def main() -> None: - bundle = musher.pull("acme/engineering-workflows:2.0.0") - skill = bundle.skill("repo-maintainer") + bundle = musher.pull("musher-examples/code-review-kit:1.2.0") + skill = bundle.skill("researching-repos") local = skill.export_openai_local_skill(dest=PROJECT_DIR / ".musher" / "openai" / "skills") agent = Agent( - name="Repo Triage Assistant", + name="Repo Research Assistant", model="gpt-4.1", instructions="Use the local skill when it helps. Keep the answer concise and actionable.", tools=[ @@ -83,7 +80,7 @@ async def main() -> None: result = await Runner.run( agent, - "Use the repo-maintainer skill to inspect this repository and tell me the first onboarding issue I should fix.", + "Use the researching-repos skill to explore this repository and summarize its tech stack, structure, and conventions.", ) print(result.final_output) diff --git a/examples/pydantic_ai/dynamic_instructions.py b/examples/pydantic_ai/dynamic_instructions.py index 9a4bcad..5b1d27d 100644 --- a/examples/pydantic_ai/dynamic_instructions.py +++ b/examples/pydantic_ai/dynamic_instructions.py @@ -10,13 +10,10 @@ import musher -# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") -bundle = musher.pull("acme/prompt-library:1.2.0") +bundle = musher.pull("musher-examples/prompt-library:1.2.0") agent = Agent("openai:gpt-4o") diff --git a/examples/pydantic_ai/instructions_from_bundle.py b/examples/pydantic_ai/instructions_from_bundle.py index 479b920..66dff6c 100644 --- a/examples/pydantic_ai/instructions_from_bundle.py +++ b/examples/pydantic_ai/instructions_from_bundle.py @@ -10,13 +10,10 @@ import musher -# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") -bundle = musher.pull("acme/prompt-library:1.2.0") +bundle = musher.pull("musher-examples/prompt-library:1.2.0") # Load versioned instructions from the bundle instructions_text = bundle.prompt("system").text() diff --git a/examples/pydantic_ai/structured_output.py b/examples/pydantic_ai/structured_output.py index f857417..d72be67 100644 --- a/examples/pydantic_ai/structured_output.py +++ b/examples/pydantic_ai/structured_output.py @@ -11,9 +11,6 @@ import musher -# NOTE: Bundle references below (e.g. "acme/prompt-library:1.2.0") are -# placeholders. Replace with a real bundle ref from your Musher registry. - # Credentials auto-discovered from MUSHER_API_KEY env var, keyring, # or credential file. To override: musher.configure(token="your-token") @@ -27,7 +24,7 @@ class IncidentSummary(BaseModel): action_items: list[str] -bundle = musher.pull("acme/prompt-library:1.2.0") +bundle = musher.pull("musher-examples/prompt-library:1.2.0") instructions_text = bundle.prompt("system").text() # The agent returns an IncidentSummary instead of free-form text diff --git a/lefthook.yml b/lefthook.yml index f6f019e..b3b89f3 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,42 +1,10 @@ -# EXAMPLE USAGE: -# -# Refer for explanation to following link: -# https://lefthook.dev/configuration/ -# -# pre-push: -# jobs: -# - name: packages audit -# tags: -# - frontend -# - security -# run: yarn audit -# -# - name: gems audit -# tags: -# - backend -# - security -# run: bundle audit -# -# pre-commit: -# parallel: true -# jobs: -# - run: yarn eslint {staged_files} -# glob: "*.{js,ts,jsx,tsx}" -# -# - name: rubocop -# glob: "*.rb" -# exclude: -# - config/application.rb -# - config/routes.rb -# run: bundle exec rubocop --force-exclusion -- {all_files} -# -# - name: govet -# files: git ls-files -m -# glob: "*.go" -# run: go vet -- {files} -# -# - script: "hello.js" -# runner: node -# -# - script: "hello.go" -# runner: go run +pre-commit: + parallel: true + jobs: + - name: ruff-format + glob: "*.py" + run: uv run ruff format --check {staged_files} + + - name: ruff-lint + glob: "*.py" + run: uv run ruff check {staged_files} diff --git a/pyproject.toml b/pyproject.toml index e623f86..bfc395d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dev = [ "pytest-asyncio>=1.0.0", "pytest-cov>=4.0.0", "respx>=0.20.0", + "pytest-xdist>=3.0", ] [build-system] @@ -43,9 +44,10 @@ asyncio_mode = "auto" addopts = [ "--strict-markers", "--strict-config", + "-n=auto", "--cov=musher", "--cov-report=term-missing", - "--cov-fail-under=80", + "--cov-fail-under=90", ] [tool.basedpyright] diff --git a/src/musher/__init__.py b/src/musher/__init__.py index 37049dc..1350d24 100644 --- a/src/musher/__init__.py +++ b/src/musher/__init__.py @@ -10,7 +10,7 @@ from musher._auth import resolve_registry_url from musher._bundle import Asset, Bundle, Manifest, ManifestAsset, ResolveResult -from musher._cache import BundleCache +from musher._cache import BundleCache # internal; not re-exported in __all__ from musher._cache_info import CachedBundle, CachedBundleVersion, CacheInfo from musher._client import AsyncClient, Client from musher._config import MusherConfig, configure, get_config diff --git a/src/musher/adapters/__init__.py b/src/musher/adapters/__init__.py deleted file mode 100644 index 1164773..0000000 --- a/src/musher/adapters/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Framework adapters (LangChain, LlamaIndex, etc.) — future packages.""" diff --git a/uv.lock b/uv.lock index a176aa8..cff7f72 100644 --- a/uv.lock +++ b/uv.lock @@ -641,6 +641,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -1453,6 +1462,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-xdist" }, { name = "respx" }, { name = "ruff" }, ] @@ -1476,6 +1486,7 @@ dev = [ { name = "pytest", specifier = ">=8.3.0,<9.0.0" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=4.0.0" }, + { name = "pytest-xdist", specifier = ">=3.0" }, { name = "respx", specifier = ">=0.20.0" }, { name = "ruff", specifier = ">=0.15.2" }, ] @@ -2166,6 +2177,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"