Skip to content

Commit 6400afc

Browse files
feat: add advanced search api (#19)
1 parent ec524f9 commit 6400afc

File tree

5 files changed

+245
-2
lines changed

5 files changed

+245
-2
lines changed

CLAUDE.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Plane Python SDK (`plane-sdk` on PyPI, v0.2.4) — a synchronous, type-annotated Python client for the Plane API. Built on `requests` + `pydantic` v2, targeting Python 3.10+.
8+
9+
## Common Commands
10+
11+
```bash
12+
# Install for development
13+
pip install -e .
14+
pip install -r requirements.txt
15+
16+
# Run all unit tests (requires env vars, see below)
17+
pytest tests/unit/
18+
19+
# Run a specific test file or test
20+
pytest tests/unit/test_projects.py
21+
pytest tests/unit/test_projects.py::TestProjectsAPICRUD::test_create_project
22+
23+
# Integration/script tests (excluded by default via addopts)
24+
pytest tests/scripts/ --override-ini="addopts="
25+
26+
# Formatting & linting
27+
black plane tests
28+
ruff check plane tests
29+
ruff check --fix plane tests
30+
31+
# Type checking
32+
mypy plane
33+
```
34+
35+
### Required Environment Variables for Tests
36+
37+
Tests make real HTTP requests (no mocking). Set these before running:
38+
39+
- `PLANE_BASE_URL` — API base URL
40+
- `PLANE_API_KEY` or `PLANE_ACCESS_TOKEN` — authentication (exactly one)
41+
- `WORKSPACE_SLUG` — test workspace
42+
- `AGENT_SLUG` — (optional) needed only for agent run tests
43+
44+
## Architecture
45+
46+
### Client → Resource → Model pattern
47+
48+
`PlaneClient` is the single entry point. It holds a `Configuration` and exposes resource objects as attributes:
49+
50+
```
51+
PlaneClient
52+
├── .projects → Projects(BaseResource)
53+
├── .work_items → WorkItems(BaseResource)
54+
│ ├── .comments
55+
│ ├── .attachments
56+
│ ├── .links
57+
│ └── ...sub-resources
58+
├── .cycles → Cycles(BaseResource)
59+
└── ...15+ resources
60+
```
61+
62+
### Key directories
63+
64+
- `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.
65+
- `plane/models/` — Pydantic v2 models. Three kinds per resource:
66+
- **Response models** (e.g. `Project`): `extra="allow"` for forward compatibility with new API fields.
67+
- **Request DTOs** (e.g. `CreateProject`, `UpdateProject`): `extra="ignore"` to be strict about inputs.
68+
- **Query param models** (e.g. `PaginatedQueryParams`): `extra="ignore"`.
69+
- `plane/client/``PlaneClient` (API key / access token auth) and `OAuthClient` (OAuth 2.0 flows).
70+
- `plane/errors/``PlaneError``HttpError`, `ConfigurationError`.
71+
- `plane/config.py``Configuration` and `RetryConfig` dataclasses.
72+
73+
### Sub-resource pattern
74+
75+
Resources with children (work_items, customers, initiatives, teamspaces) instantiate sub-resource objects in `__init__`:
76+
77+
```python
78+
class WorkItems(BaseResource):
79+
def __init__(self, config):
80+
super().__init__(config, "/workspaces/")
81+
self.comments = WorkItemComments(config)
82+
self.attachments = WorkItemAttachments(config)
83+
```
84+
85+
### URL convention
86+
87+
All API endpoints end with a trailing `/`. URLs are built as `{base_path}/api/v1{resource_base_path}/{endpoint}/`.
88+
89+
## Coding Conventions
90+
91+
- Line length: 100 (Black + Ruff)
92+
- Use `X | None` not `Optional[X]`; use `list[str]` not `List[str]` (Python 3.10+ builtins)
93+
- Import abstract types from `collections.abc` (e.g. `Mapping`, `Iterable`)
94+
- Ruff rules: E, F, I (isort), UP (pyupgrade), B (bugbear)
95+
- Never use "Issue" in endpoint or parameter names — always use "Work Item"
96+
- Auth is mutually exclusive: `api_key` XOR `access_token` (raises `ConfigurationError` if both/neither)
97+
- Resource methods accept Pydantic DTOs, serialize with `model_dump(exclude_none=True)`, and validate responses with `Model.model_validate()`
98+
- All resources follow CRUD verbs: `create`, `retrieve`, `update`, `delete`, `list`

plane/api/work_items/base.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from __future__ import annotations
2+
13
from typing import Any
24

35
from ...models.query_params import RetrieveQueryParams, WorkItemQueryParams
46
from ...models.work_items import (
7+
AdvancedSearchResult,
8+
AdvancedSearchWorkItem,
59
CreateWorkItem,
610
PaginatedWorkItemResponse,
711
UpdateWorkItem,
@@ -189,3 +193,44 @@ def search(
189193
search_params.update(params.model_dump(exclude_none=True))
190194
response = self._get(f"{workspace_slug}/work-items/search", params=search_params)
191195
return WorkItemSearch.model_validate(response)
196+
197+
def advanced_search(
198+
self,
199+
workspace_slug: str,
200+
data: AdvancedSearchWorkItem,
201+
) -> list[AdvancedSearchResult]:
202+
"""Perform advanced search on work items with filters.
203+
204+
Supports text-based search via ``query`` and/or structured filters
205+
using recursive AND/OR groups.
206+
207+
Args:
208+
workspace_slug: The workspace slug identifier
209+
data: Advanced search request with query, filters, and limit
210+
211+
Example::
212+
213+
from plane.models.work_items import AdvancedSearchWorkItem
214+
215+
results = client.work_items.advanced_search(
216+
"my-workspace",
217+
AdvancedSearchWorkItem(
218+
query="new",
219+
filters={
220+
"and": [
221+
{"state_id": "state-uuid"},
222+
{"or": [
223+
{"priority": "high"},
224+
{"state_id": "other-state-uuid"},
225+
]},
226+
]
227+
},
228+
limit=100,
229+
),
230+
)
231+
"""
232+
response = self._post(
233+
f"{workspace_slug}/work-items/advanced-search",
234+
data.model_dump(exclude_none=True),
235+
)
236+
return [AdvancedSearchResult.model_validate(item) for item in response]

plane/models/work_items.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,56 @@ class WorkItemSearch(BaseModel):
215215
issues: list[WorkItemSearchItem]
216216

217217

218+
class AdvancedSearchWorkItem(BaseModel):
219+
"""Request model for advanced work item search with filters.
220+
221+
Filters support recursive AND/OR groups. Each filter condition is a
222+
single key-value dict (e.g. ``{"state_id": "..."}``). Groups are nested
223+
using ``"and"`` / ``"or"`` keys whose values are lists of conditions or
224+
sub-groups.
225+
226+
Example::
227+
228+
AdvancedSearchWorkItem(
229+
query="new",
230+
filters={
231+
"and": [
232+
{"state_id": "abc-123"},
233+
{"or": [
234+
{"priority": "high"},
235+
{"state_id": "def-456"},
236+
]},
237+
]
238+
},
239+
limit=100,
240+
)
241+
"""
242+
243+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
244+
245+
query: str | None = None
246+
filters: dict[str, Any] | None = None
247+
limit: int | None = None
248+
249+
250+
class AdvancedSearchResult(BaseModel):
251+
"""Advanced search result item."""
252+
253+
model_config = ConfigDict(extra="allow", populate_by_name=True)
254+
255+
id: str
256+
name: str
257+
sequence_id: int
258+
project_identifier: str
259+
project_id: str
260+
workspace_id: str
261+
type_id: str | None = None
262+
state_id: str | None = None
263+
priority: str | None = None
264+
target_date: str | None = None
265+
start_date: str | None = None
266+
267+
218268
class WorkItemActivity(BaseModel):
219269
"""Work item activity model."""
220270

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plane-sdk"
7-
version = "0.2.4"
7+
version = "0.2.5"
88
description = "Python SDK for Plane API"
99
readme = "README.md"
1010
requires-python = ">=3.10"

tests/unit/test_work_items.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from plane.client import PlaneClient
66
from plane.models.projects import Project
77
from plane.models.query_params import PaginatedQueryParams
8-
from plane.models.work_items import CreateWorkItem, UpdateWorkItem
8+
from plane.models.work_items import AdvancedSearchWorkItem, CreateWorkItem, UpdateWorkItem
99

1010

1111
class TestWorkItemsAPI:
@@ -38,6 +38,56 @@ def test_search_work_items(self, client: PlaneClient, workspace_slug: str) -> No
3838
assert hasattr(response, "issues")
3939
assert isinstance(response.issues, list)
4040

41+
def test_advanced_search_work_items(
42+
self, client: PlaneClient, workspace_slug: str
43+
) -> None:
44+
"""Test advanced search with query only."""
45+
data = AdvancedSearchWorkItem(query="test", limit=10)
46+
results = client.work_items.advanced_search(workspace_slug, data)
47+
assert isinstance(results, list)
48+
for item in results:
49+
assert item.id is not None
50+
assert item.name is not None
51+
assert item.sequence_id is not None
52+
assert item.project_id is not None
53+
assert item.workspace_id is not None
54+
55+
def test_advanced_search_with_filters(
56+
self, client: PlaneClient, workspace_slug: str
57+
) -> None:
58+
"""Test advanced search with filters."""
59+
data = AdvancedSearchWorkItem(
60+
filters={
61+
"and": [
62+
{"priority": "high"},
63+
]
64+
},
65+
limit=10,
66+
)
67+
results = client.work_items.advanced_search(workspace_slug, data)
68+
assert isinstance(results, list)
69+
for item in results:
70+
assert item.id is not None
71+
assert item.priority == "high"
72+
73+
def test_advanced_search_with_nested_filters(
74+
self, client: PlaneClient, workspace_slug: str
75+
) -> None:
76+
"""Test advanced search with nested AND/OR filters."""
77+
data = AdvancedSearchWorkItem(
78+
filters={
79+
"or": [
80+
{"priority": "high"},
81+
{"priority": "urgent"},
82+
]
83+
},
84+
limit=10,
85+
)
86+
results = client.work_items.advanced_search(workspace_slug, data)
87+
assert isinstance(results, list)
88+
for item in results:
89+
assert item.priority in ("high", "urgent")
90+
4191

4292
class TestWorkItemsAPICRUD:
4393
"""Test WorkItems API CRUD operations."""

0 commit comments

Comments
 (0)