Skip to content

Latest commit

 

History

History
139 lines (113 loc) · 6.75 KB

File metadata and controls

139 lines (113 loc) · 6.75 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Plane Python SDK (plane-sdk on PyPI, v0.3.0) — a synchronous, type-annotated Python client for the Plane API. Built on requests + pydantic v2, targeting Python 3.10+.

Common Commands

# Install for development
pip install -e .
pip install -r requirements.txt

# Run all unit tests (requires env vars, see below)
pytest tests/unit/

# Run a specific test file or test
pytest tests/unit/test_projects.py
pytest tests/unit/test_projects.py::TestProjectsAPICRUD::test_create_project

# Integration/script tests (excluded by default via addopts)
pytest tests/scripts/ --override-ini="addopts="

# Formatting & linting
black plane tests
ruff check plane tests
ruff check --fix plane tests

# Type checking
mypy plane

Required Environment Variables for Tests

Tests make real HTTP requests (no mocking). Set these before running:

  • PLANE_BASE_URL — API base URL
  • PLANE_API_KEY or PLANE_ACCESS_TOKEN — authentication (exactly one)
  • WORKSPACE_SLUG — test workspace
  • AGENT_SLUG — (optional) needed only for agent run tests

Architecture

Client → Resource → Model pattern

PlaneClient is the single entry point. It holds a Configuration and exposes resource objects as attributes:

PlaneClient
  ├── .projects      → Projects(BaseResource)
  ├── .work_items    → WorkItems(BaseResource)
  │     ├── .comments
  │     ├── .attachments
  │     ├── .links
  │     └── ...sub-resources
  ├── .cycles        → Cycles(BaseResource)
  └── ...15+ resources

Key directories

  • plane/api/ — Resource classes. Every resource extends BaseResource which handles HTTP methods, auth headers, URL building (/api/v1/...), retry via urllib3.Retry, and response parsing.
  • plane/models/ — Pydantic v2 models. Three kinds per resource:
    • Response models (e.g. Project): extra="allow" for forward compatibility with new API fields.
    • Request DTOs (e.g. CreateProject, UpdateProject): extra="ignore" to be strict about inputs.
    • Query param models (e.g. PaginatedQueryParams): extra="ignore".
  • plane/client/PlaneClient (API key / access token auth) and OAuthClient (OAuth 2.0 flows).
  • plane/errors/PlaneErrorHttpError, ConfigurationError.
  • plane/config.pyConfiguration and RetryConfig dataclasses.
  • plane/api/v2/ — the v2 surface. The chain is the only public form: client.v2.workspace(slug) (Workspace, plane/api/v2/workspace.py) and .project(project) (Project, plane/api/v2/project.py) are zero-I/O locators that bind slug/project_id once; every v2 resource hangs off one of them as a plain attribute (.wiki on Workspace is itself a small locator, Wiki in plane/api/v2/wiki.py, holding .pages/.collections). client.v2.users / .user_assets are the only resources kept directly on V2Namespace (the 6 operations with no workspace in their path). _kernel/ holds the shared machinery: V2Resource.__init__(transport, **scope) stores the bound scope, and _collection_url/_detail_url merge it with any explicitly passed path params (explicit wins) — a resource constructed with no scope (most offline tests do this) behaves exactly as if every path param were passed per call, so a resource's methods work identically whether or not it was reached through the chain. No public v2 method takes workspace_slug/project parameters — the locator supplies both; leaf ids (work_item_id, release_id, ...) stay as the first positional argument. _generated/constants.py is produced by scripts/generate_v2_constants.py from the api_v2 OpenAPI golden and must never be hand-edited.
    • Bridges. Membership between two resources (.../cycles/{id}/work-items/, .../releases/{id}/labels/, .../collections/{id}/members/, ...) is never a manage_*(add=, remove=) method. It is a sub-resource (proj.cycles.work_items, ws.initiatives.projects; on a catalog resource such as ws.releases.labels the verbs sit next to the CRUD) exposing exactly add(parent_id, ids) -> list[str] and remove(parent_id, ids) -> list[str], both delegating to V2Resource._bridge(key=, ids=, **path_params). The kernel POSTs {"add": [...]} or {"remove": [...]} only, rejects 0 or >100 ids with ValueError before the request, and returns the response's added/removed list. A class whose own path is not the bridge URL sets bridge_path. The bridge class declares the golden's single manage operationId under the "bridge" key of operations. The *Manage* request/response models stay in plane/models/v2/* as the bridge's model, but are not exported from plane.models.v2.
    • Bridge verbs copy the web app CTA. Properties on a work item type: link/unlink (unlink deletes the property's values on every work item of the type). Members of anything else: add/remove. workflows.states.attach is a different bridge (POST {state_ids}) and keeps its name.
    • Lookups. find_by_<key> is server-side via _find_one only where the golden list op has that filter (roles.find_by_slug, estimates.points.find_by_key, find_by_name on properties/options/contexts); property name is the machine key, not the display_name label.
  • plane/models/v2/ — v2 pydantic models. Read models mark every field except id optional, because ?fields= and collection deferral can omit any of them.

Sub-resource pattern

Resources with children (work_items, customers, initiatives, teamspaces) instantiate sub-resource objects in __init__:

class WorkItems(BaseResource):
    def __init__(self, config):
        super().__init__(config, "/workspaces/")
        self.comments = WorkItemComments(config)
        self.attachments = WorkItemAttachments(config)

URL convention

All API endpoints end with a trailing /. URLs are built as {base_path}/api/v1{resource_base_path}/{endpoint}/.

Coding Conventions

  • Line length: 100 (Black + Ruff)
  • Use X | None not Optional[X]; use list[str] not List[str] (Python 3.10+ builtins)
  • Import abstract types from collections.abc (e.g. Mapping, Iterable)
  • Ruff rules: E, F, I (isort), UP (pyupgrade), B (bugbear)
  • Never use "Issue" in endpoint or parameter names — always use "Work Item"
  • Auth is mutually exclusive: api_key XOR access_token (raises ConfigurationError if both/neither)
  • Resource methods accept Pydantic DTOs, serialize with model_dump(exclude_none=True), and validate responses with Model.model_validate()
  • All resources follow CRUD verbs: create, retrieve, update, delete, list