Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ PlaneClient
- `plane/client/` — `PlaneClient` (API key / access token auth) and `OAuthClient` (OAuth 2.0 flows).
- `plane/errors/` — `PlaneError` → `HttpError`, `ConfigurationError`.
- `plane/config.py` — `Configuration` and `RetryConfig` dataclasses.
- `plane/api/v2/` — the v2 surface (`client.v2`). **Migration complete**: all 90
- `plane/api/v2/` — the v2 surface (`client.v2`). **Migration complete**: all 92
`V2Resource` subclasses in the package (`tests/v2/tree_walk.py`'s
`all_resource_classes()`) are on the flat shape and swept by the rule tests.
`tests/v2/tree_walk.py`'s `UNMIGRATED_RESOURCES` — the opt-out list the sweeps
Expand All @@ -87,25 +87,25 @@ PlaneClient
never grow, so it cannot silently regain a member. Count resources, not
grouping nodes: `wiki` and `group_sync` hold no `V2Resource` base, `path` or
`operations` of their own — they only group children (`wiki.pages`,
`wiki.collections`, `group_sync.config`) — so neither is in the 90.
`wiki.collections`, `group_sync.config`) — so neither is in the 92.

The whole project band is wired onto `client.v2.workspaces.projects`:
`states`, `labels`, `work_items`, `cycles`, `milestones`, `modules`,
`estimates`, `intakes`, `members`, `views`, `features`, `permissions`,
`work_item_templates`, `worklogs`, `pages`, `automations`,
`work_item_properties`, `work_item_types` and `workflows` — nineteen
children, and a fetched project reaches every one of them that is itself
navigable (`project.cycles.list()`, `project.permissions.me()`). Nineteen of
the 90 classes are navigable — `workspaces`, `projects`, `work_items`,
navigable (`project.cycles.list()`, `project.permissions.me()`). Twenty of
the 92 classes are navigable — `workspaces`, `projects`, `work_items`,
`cycles`, `milestones`, `modules`, `estimates`, `webhooks`, `collections`,
`customers`, `initiatives`, `releases`, `work_item_types`,
`work_item_properties`, `automations` and `workflows` (three of those —
`work_item_properties`, `automations`, `workflows` and `dashboards` (three of those —
automations, work item types and work item properties — each have a separate
project-scoped and workspace-scoped resource class, each independently
navigable, which is where 16 families become 19 classes) — a fetched row
navigable, which is where 17 families become 20 classes) — a fetched row
reaches its child with no ids repeated. `workspaces` is the root of that set
and the last to join it: `client.v2.workspaces.retrieve("acme")` answers a
`LoadedWorkspace` reaching all 25 workspace-scoped families
`LoadedWorkspace` reaching all 26 workspace-scoped families
(`workspace.projects.list()`), and its children address it by `slug`, never
the UUID `id`, which that path segment does not accept. `estimates`' child is
`estimate_points`, not `points` — `Estimate.points` is itself an API field,
Expand Down Expand Up @@ -162,8 +162,8 @@ PlaneClient
- **Loaded rows.** A resource with children (`projects`, `work_items`,
`cycles`, `milestones`, `modules`, `estimates`, `webhooks`, `collections`,
`customers`, `initiatives`, `releases`, `work_item_types`,
`work_item_properties`, `automations`, `workflows`, `workspaces` — 19 of the
90 classes) returns a `Loaded` row from `retrieve`/`list`/`iterate`, not a bare
`work_item_properties`, `automations`, `workflows`, `dashboards`, `workspaces` —
20 of the 92 classes) returns a `Loaded` row from `retrieve`/`list`/`iterate`, not a bare
pydantic model: it carries its own data and reaches its own children with none of the
ids repeated (`project.states.list()`, `work_item.comments.list()`,
`cycle.work_items.add(["w1"])`).
Expand Down Expand Up @@ -245,9 +245,10 @@ PlaneClient
per `Loaded` subclass — `project.py`, `work_item.py`, `cycle.py`,
`milestone.py`, `module.py`, `estimate.py`, `webhook.py`, `collection.py`,
`customer.py`, `initiative.py`, `release.py`, `work_item_type.py`,
`work_item_property.py`, `automation.py`, `workflow.py`, `workspace.py`; copy
`work_item_property.py`, `automation.py`, `workflow.py`, `dashboard.py`,
`workspace.py`; copy
whichever is closest in shape (single bridge-only child vs. several plain-CRUD
children). `workspace.py` is the `bind1` exemplar and the widest, at 25
children). `workspace.py` is the `bind1` exemplar and the widest, at 26
children; the two grouping nodes (`wiki`, `group_sync`) are deliberately not
among them — neither holds a `V2Resource` base, so neither is a child a row can
bind, and they are reached from the namespace instead.
Expand Down
48 changes: 40 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,16 +200,17 @@ work_items = client.work_items.list(

`client.v2` reaches the v2 surface. v1 resources on the client are unchanged.

The v2 surface is complete: every one of the 90 `V2Resource` subclasses in the
The v2 surface is complete: every one of the 92 `V2Resource` subclasses in the
package (`tests/v2/tree_walk.py`'s `all_resource_classes()`, the enumeration the
test suite itself sweeps) is on the flat shape described below and reachable
through `client.v2`. Counting resources means not counting grouping nodes:
`wiki` and `group_sync` hold no `V2Resource` base, `path` or `operations` of
their own — they only group children (`wiki.pages`, `wiki.collections`,
`group_sync.config`) — and are outside the 90.
`group_sync.config`) — and are outside the 92.

Wired directly on `client.v2.workspaces`: `artifacts`, `assets`, `audit_logs`,
`automations`, `customer_properties`, `customers`, `features`, `initiatives`,
`automations`, `customer_properties`, `customers`, `dashboards` (`.widgets`),
`features`, `initiatives`,
`invitations`, `members`, `permission_schemes`, `permissions`, `projects`,
`releases` (`.labels`, `.tags`, `.comments`, `.links`, `.changelog`,
`.work_items`), `roles`, `stickies`, `teamspaces`, `views`, `webhooks` (with
Expand Down Expand Up @@ -276,9 +277,10 @@ client.v2.workspaces.projects.states.list(slug="acme", project="ENG")

A resource with children (`projects`, `work_items`, `cycles`, `milestones`,
`modules`, `estimates`, `webhooks`, `collections`, `customers`, `initiatives`,
`releases`, `work_item_types`, `work_item_properties`, `automations` and
`workflows` — 18 of the 90 classes, some families having a separate
project-scoped and workspace-scoped resource, each independently navigable)
`releases`, `work_item_types`, `work_item_properties`, `automations`,
`workflows`, `dashboards` and `workspaces` — 20 of the 92 classes, some families
having a separate project-scoped and workspace-scoped resource, each independently
navigable)
doesn't just hand back a bare pydantic model from `retrieve`/`list`/`iterate` —
it hands back a row that carries its own data *and* already knows where it
lives, so the row's own children are reached with none of the ids repeated:
Expand Down Expand Up @@ -310,7 +312,8 @@ log; `collections` via `.members`/`.pages`; `customers` via `.requests`,
`.changelog`, `.work_items`; `work_item_types` via `.properties`;
`work_item_properties` via `.property_options` (plus `.contexts` on the
workspace-scoped resource only); `automations` via `.edges`, `.nodes`,
`.activities`; and `workflows` via `.states`, `.transitions`. A work item
`.activities`; `workflows` via `.states`, `.transitions`; and `dashboards` via
`.widgets`. A work item
itself reaches all seven of its own children this way — `comments`,
`attachments`, `links`, `worklogs`, `activities`, `relations`, `dependencies`.

Expand All @@ -325,6 +328,35 @@ class that declares a `loaded_model` and fails if its row's navigation
properties don't match its resource's own children exactly — see
[the four rules the tests enforce](#the-four-rules-the-tests-enforce) below.

### Dashboards and widgets

A dashboard is a workspace-level page of charts over the work items in its projects; each
widget is one chart. A fetched dashboard reaches its widgets directly, and `chart()` returns
the data the widget displays (its shape depends on the chart type):

```python
from plane.models.v2 import CreateDashboard, CreateWidget

dashboards = client.v2.workspaces.dashboards
dashboard = dashboards.create("acme", CreateDashboard(name="Engineering", project_ids=[project_id]))

widget = dashboard.widgets.create(
CreateWidget(
name="By priority",
chart_type="BAR_CHART",
chart_model="STACKED",
x_axis_property="PRIORITY",
y_axis_metric="WORK_ITEM_COUNT",
group_by="STATE_GROUPS",
)
)
chart = dashboard.widgets.chart(widget.id) # chart.data, chart.chart_schema, paging fields
chart = dashboard.widgets.chart(widget.id, page=2)
everything = dashboard.widgets.charts() # every widget's first page in one call
dashboards.widget_types("acme") # chart types, their models, plan availability
dashboards.copy("acme", dashboard.id) # copies settings, filters and widgets
```

### Sparse responses raise, they don't lie

Every read field except `id` is optional at the model level, because `?fields=`
Expand Down Expand Up @@ -369,7 +401,7 @@ scripts to prove it, rather than trusting it by inspection.
### The five rules the tests enforce

Five properties of the surface are each enforced by a sweep in `tests/v2/`, over
every one of the 90 resource classes (`tests/v2/tree_walk.py`'s
every one of the 92 resource classes (`tests/v2/tree_walk.py`'s
`all_resource_classes()`) rather than a hand-picked subset — so a newly added
resource is covered the moment it exists, with nothing to remember to add it to:

Expand Down
7 changes: 6 additions & 1 deletion plane/api/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
`users`/`user_assets` have no workspace in their path and sit on `V2Namespace`
directly.

The 19 `Loaded*` row types are re-exported here too. They are the declared return
The 20 `Loaded*` row types are re-exported here too. They are the declared return
type of every `retrieve`/`list`/`iterate`/`find_by_*`/verb on a navigable resource,
so a caller must be able to *name* one -- to annotate a variable, a helper's
parameter or a test fixture -- without importing out of the private `_loaded`
Expand All @@ -26,6 +26,7 @@
from ._loaded.collection import LoadedCollection
from ._loaded.customer import LoadedCustomer
from ._loaded.cycle import LoadedCycle
from ._loaded.dashboard import LoadedDashboard
from ._loaded.estimate import LoadedEstimate
from ._loaded.initiative import LoadedInitiative
from ._loaded.milestone import LoadedMilestone
Expand All @@ -46,6 +47,7 @@
from .customer_properties import CustomerProperties
from .customers import Customers
from .cycles import Cycles
from .dashboards import Dashboards, Widgets
from .estimates import Estimates
from .features import ProjectFeatures, WorkspaceFeatures
from .group_sync import GroupSync
Expand Down Expand Up @@ -100,6 +102,7 @@ def __init__(self, config: Configuration) -> None:
"Customers",
"CustomerProperties",
"Cycles",
"Dashboards",
"Estimates",
"FieldError",
"FieldNotRequested",
Expand All @@ -111,6 +114,7 @@ def __init__(self, config: Configuration) -> None:
"LoadedCollection",
"LoadedCustomer",
"LoadedCycle",
"LoadedDashboard",
"LoadedEstimate",
"LoadedInitiative",
"LoadedMilestone",
Expand Down Expand Up @@ -151,6 +155,7 @@ def __init__(self, config: Configuration) -> None:
"Users",
"V2Namespace",
"Webhooks",
"Widgets",
"Wiki",
"WikiPages",
"WorkItemProperties",
Expand Down
Loading
Loading