Metadata-Version: 2.4
Name: teamstorm-public-sdk
Version: 1.0.0
Summary: A typed Python SDK for the TeamStorm CWM Public API
Author-email: Aktush <sergey.aktush@gmail.com>
Maintainer-email: Aktush <sergey.aktush@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/saktush/teamstorm-public-sdk
Project-URL: Repository, https://github.com/saktush/teamstorm-public-sdk
Project-URL: Issues, https://github.com/saktush/teamstorm-public-sdk/issues
Project-URL: Changelog, https://github.com/saktush/teamstorm-public-sdk/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/saktush/teamstorm-public-sdk/blob/main/README.md
Keywords: teamstorm,cwm,sdk,api-client,project-management,pydantic
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Office/Business
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31.0
Requires-Dist: pydantic>=2.0
Provides-Extra: examples
Requires-Dist: pandas>=2.1.0; extra == "examples"
Requires-Dist: openpyxl>=3.1.2; extra == "examples"
Requires-Dist: python-dotenv>=0.9.9; extra == "examples"
Provides-Extra: dev
Requires-Dist: flake8>=6.0; extra == "dev"
Requires-Dist: flake8-pyproject>=1.2; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# teamstorm

![PyPI](https://img.shields.io/pypi/v/teamstorm.svg)
![Python versions](https://img.shields.io/pypi/pyversions/teamstorm.svg)
![License](https://img.shields.io/pypi/l/teamstorm.svg)

**teamstorm** is a typed Python SDK for the TeamStorm **CWM Public API**
(`/cwm/public/api/v1`). It covers all 159 operations in the published OpenAPI
spec (100%), models every request/response body as a [pydantic v2](https://docs.pydantic.dev/)
class, and ships [PEP 561](https://peps.python.org/pep-0561/) type information
(`py.typed`) so `mypy`/`pyright` understand it out of the box.

## Install

```bash
pip install teamstorm-public-sdk
```

Requires Python >= 3.11. The only runtime dependencies are `requests` and
`pydantic`.

## Quickstart

```python
from teamstorm.client import TsClient
from teamstorm.sdk import TsSDK

client = TsClient(
    base_url="https://your-cwm-host",
    token="YOUR_API_TOKEN",
)
ts = TsSDK(client)

workspace = ts.workspaces.get("YOUR_WORKSPACE_KEY")
print(workspace.name)
```

Every `*API` method takes its own `workspace_key` argument on each call, so
one `TsClient`/`TsSDK` can be reused across multiple workspaces.

A runnable version of this snippet, with a couple more calls layered on top,
is at [`examples/quickstart.py`](examples/quickstart.py).

## Authentication

Every request carries an `Authorization: PrivateToken <token>` header, set
once on the client's `requests.Session` at construction time. Get a token
from your TeamStorm instance's user settings (personal API token generation).

`base_url` must start with `https://` -- construction raises `ValueError`
otherwise, since the token is a bearer credential and must never be sent over
plaintext HTTP:

```python
from teamstorm.client import TsClient

try:
    TsClient(base_url="http://your-cwm-host", token="t")
except ValueError as e:
    print(e)
```

Pass `allow_insecure=True` to lift this check. It exists for tests against a
local/mock server -- never set it with a real token, since it means the
`Authorization` header travels in the clear.

## Core concepts

### The `TsSDK` facade

`TsSDK` wraps a `TsClient` and exposes every resource as a lazy property --
nothing is imported or constructed until you actually access it. Each
property returns a plain, stateless `*API` object bound to the same client.

| Property | Class | Covers |
|---|---|---|
| `workspaces` | `WorkspacesAPI` | Workspaces: the top-level container for folders, workitems and documents |
| `folders` | `FoldersAPI` | Folders: the tree that organizes agile boards, sprints and workitems |
| `agile` | `AgileAPI` | Agile board configuration attached to a folder |
| `sprints` | `SprintsAPI` | Sprints (iterations): dates, team membership, backlog sprint |
| `workitems` | `WorkitemsAPI` | Workitems (tasks/stories/bugs): CRUD, filtering, counts, attribute values |
| `users` | `UsersAPI` | Tenant-wide user accounts: lookup and block/unblock |
| `types` | `TypesAPI` | Workitem types (e.g. "Bug", "Story") and their assignable attributes |
| `attributes` | `AttributesAPI` | Custom attribute definitions and their UniSelect/Tag options |
| `statuses` | `StatusesAPI` | Workflow statuses and the global status categories they roll up to |
| `workflows` | `WorkflowsAPI` | Workflows: the named graph of statuses and transitions a type is bound to |
| `roles` | `RolesAPI` | Permission roles assignable to users and groups |
| `workspace_users` | `WorkspaceUsersAPI` | Workspace-scoped user membership and per-workspace role assignments |
| `workspace_groups` | `WorkspaceGroupsAPI` | Workspace-scoped group membership and per-workspace role assignments |
| `groups` | `GroupsAPI` | Tenant-wide user groups (identity groups), independent of any workspace |
| `providers` | `ProvidersAPI` | Identity providers (SSO/OpenID connections) configured on this instance |
| `links` | `LinksAPI` | Typed workitem-to-workitem links (blocks, relates-to, ...) and their types |
| `workitem_comments` | `WorkitemCommentsAPI` | Comments on a workitem, including per-comment visibility |
| `document_comments` | `DocumentCommentsAPI` | Comments on a document |
| `documents` | `DocumentsAPI` | Documents core CRUD, plus block/unblock |
| `document_versions` | `DocumentVersionsAPI` | Read-only version history for a document |
| `document_statuses` | `DocumentStatusesAPI` | Workspace-configurable named statuses documents can be tagged with |
| `document_workitem_links` | `DocumentWorkitemLinksAPI` | Untyped links between a document and one or more workitems, both directions |
| `workitem_sharing` | `WorkitemSharingAPI` | Per-user/per-group sharing permissions granted on a workitem |
| `document_sharing` | `DocumentSharingAPI` | Per-user/per-group sharing permissions granted on a document |
| `portfolios` | `PortfoliosAPI` | Portfolios: named groupings of portfolio elements above the workitem level |
| `portfolio_elements` | `PortfolioElementsAPI` | Portfolio elements and their links to the workitems they group |
| `workitem_attachments` | `WorkitemAttachmentsAPI` | File attachments on a workitem, including all stored versions |
| `document_attachments` | `DocumentAttachmentsAPI` | File attachments on a document, including all stored versions |
| `time_tracking` | `TimeTrackingAPI` | Time tracking entries logged against workitems, tenant-wide |
| `queries` | `QueriesAPI` | Saved queries: their workitem results and visibility settings |
| `git_integration_tokens` | `GitIntegrationTokensAPI` | Git integration tokens (GitLab/GitFlic) for a workspace |
| `open_id` | `OpenIdAPI` | OpenID connections and pre-provisioned users for SSO login |

That's 32 properties covering all 159 spec operations; see
[`docs/sdk-coverage.md`](docs/sdk-coverage.md) for the full operation-to-method
table.

### Typed models in, typed models out

Every request body is a pydantic model and every response is validated into
one on the way back -- no raw `dict` crosses the SDK boundary:

```python
from uuid import UUID

from teamstorm.models.workitems import CreateWorkitemRequestBody, PatchWorkitemRequestBody

item = ts.workitems.create(
    "YOUR_WORKSPACE_KEY",
    CreateWorkitemRequestBody(
        name="Fix login bug",
        type="Bug",
        parent_id=UUID("00000000-0000-0000-0000-000000000000"),  # a folder or workitem id
    ),
)

updated = ts.workitems.patch(
    "YOUR_WORKSPACE_KEY",
    workitem_id=item.id,
    body=PatchWorkitemRequestBody(description="Root-caused; fix incoming."),
)
```

`item` and `updated` are both `WorkitemModel` instances -- editor
autocomplete and `mypy`/`pyright` see every field.

### The PATCH null rule

This is a real trap: pydantic models in this SDK default `model_dump()` to
`exclude_none=True`, which is right for POST bodies but wrong for PATCH --
it can't tell "I didn't touch this field" apart from "I want to clear it".
PATCH methods dump their body with `exclude_unset=True, exclude_none=False`
instead, so:

- a field you **never assign** on the body is omitted from the request ->
  the server leaves it unchanged;
- a field you **explicitly set to `None`** is sent as JSON `null` -> the
  server clears it.

```python
from teamstorm.models.workitems import PatchWorkitemRequestBody

# Clears "assignee" server-side (explicit null is sent):
clear_assignee = PatchWorkitemRequestBody(assignee=None)

# Leaves "assignee" untouched; only "description" changes:
only_description = PatchWorkitemRequestBody(description="Root-caused; fix incoming.")
```

Passing `assignee=None` to the constructor is different from never mentioning
`assignee` at all, even though both leave the field's *value* at its default
of `None` -- pydantic tracks which fields were actually assigned
(`model_fields_set`), and that's what `exclude_unset=True` looks at. See
`teamstorm/models/base.py`'s `TsBaseModel.model_dump` docstring for the full
explanation.

### Pagination

Every `list()` method fetches **all** pages before returning -- you always
get a complete `list[SomeModel]`, never a single page:

```python
all_workitems = ts.workitems.list("YOUR_WORKSPACE_KEY")
```

For very large result sets, use `client.iter_all()` to stream items lazily,
one page at a time, instead of buffering everything in memory. It operates
at the raw-JSON level (the same path a resource's `list()` method would
build), so validate each item yourself against the matching model:

```python
from teamstorm.models.workitems import WorkitemModel

for raw in client.iter_all("/workspaces/YOUR_WORKSPACE_KEY/workitems"):
    item = WorkitemModel.model_validate(raw)
    print(item.key, item.name)
```

Default page size is 500 items per request (`maxItemsCount`), overridable via
the `max_items_count` parameter most `list()` methods accept, or via
`params={"maxItemsCount": ...}` for `iter_all()`/`get_all()`.

### Error handling

Every API/transport failure -- a non-2xx response that survived all
retries, or a transport error that persisted across every retry attempt --
raises `ApiError`, the one exception type you need to catch:

```python
from teamstorm.client import ApiError

try:
    ts.workspaces.get("does-not-exist")
except ApiError as e:
    print(e.status)   # HTTP status code, or None for a pure transport failure
    print(e.method)   # e.g. "GET"
    print(e.path)     # API path, without base_url/api_prefix
    print(e.url)      # full request URL
    print(e.details)  # raw response body text (truncated), when available
```

### Timeouts and retries

```python
from teamstorm.client import TsClient, TimeoutConfig, RetryConfig

client = TsClient(
    base_url="https://your-cwm-host",
    token="YOUR_API_TOKEN",
    timeout=TimeoutConfig(connect_s=5.0, read_s=30.0),
    retry=RetryConfig(max_attempts=3, base_delay_s=1.0, max_delay_s=10.0, jitter_s=0.5),
)
```

`TimeoutConfig` sets the `(connect, read)` timeout passed to every `requests`
call. `RetryConfig` governs the retry/backoff policy applied to transport
failures and to `429`/`5xx` responses: exponential backoff
(`base_delay_s * 2 ** attempt`, capped at `max_delay_s`) plus random jitter
(up to `jitter_s`), honoring the server's `Retry-After` header on a `429`
when present. The default policy is 5 total attempts (the first try plus up
to 4 retries).

## Type checking

The package ships a `py.typed` marker, so `mypy`/`pyright` infer concrete
types all the way from an SDK property, through the method call, down to a
model field -- no `Any` in the chain:

```python
from teamstorm.client import TsClient
from teamstorm.sdk import TsSDK

client = TsClient(base_url="https://your-cwm-host", token="YOUR_API_TOKEN")
ts = TsSDK(client)

reveal_type(ts.workspaces)              # WorkspacesAPI
workspace = ts.workspaces.get("YOUR_WORKSPACE_KEY")
reveal_type(workspace)                  # WorkspaceModel
reveal_type(workspace.name)             # str
```

Running `mypy` on the snippet above reports exactly that chain:

```text
note: Revealed type is "teamstorm.api.workspaces.WorkspacesAPI"
note: Revealed type is "teamstorm.models.workspaces.WorkspaceModel"
note: Revealed type is "str"
```

## API coverage

159/159 spec operations implemented, grouped here by functional area (see
[`docs/sdk-coverage.md`](docs/sdk-coverage.md) for the full 159-row
operation-to-method table and the 35-tag breakdown it's generated from):

| Area | Ops | SDK properties |
|---|---|---|
| Workspaces & folders | 10 | `workspaces`, `folders` |
| Agile & sprints | 9 | `agile`, `sprints` |
| Workitems (CRUD, filters, attribute values) | 10 | `workitems` |
| Comments (workitem & document) | 9 | `workitem_comments`, `document_comments` |
| Links (workitem & document) | 8 | `links`, `document_workitem_links` |
| Sharing (workitem & document) | 8 | `workitem_sharing`, `document_sharing` |
| Attachments (workitem & document) | 18 | `workitem_attachments`, `document_attachments` |
| Catalog config (attributes, types, workflows, statuses) | 24 | `attributes`, `types`, `workflows`, `statuses` |
| Roles | 5 | `roles` |
| Users & groups (tenant + workspace scoped) | 18 | `users`, `groups`, `workspace_users`, `workspace_groups` |
| Documents (core, versions, statuses) | 12 | `documents`, `document_versions`, `document_statuses` |
| Portfolios & elements | 12 | `portfolios`, `portfolio_elements` |
| Time tracking & saved queries | 5 | `time_tracking`, `queries` |
| Integrations (git tokens, OpenID, providers) | 11 | `git_integration_tokens`, `open_id`, `providers` |
| **Total** | **159** | 32 properties |

## Examples

`examples/` is a runnable, standalone example of consuming this SDK -- it is
**not** part of the published `teamstorm` package. See
[`examples/README.md`](examples/README.md) for the `import_toolkit` example
application (Excel/CSV -> TeamStorm sprint/task import, and a UniSelect
attribute-option sync tool between workspaces), and
[`examples/quickstart.py`](examples/quickstart.py) for a runnable version of
the Quickstart above.

## Contributing, changelog, license

- Development setup, running tests, linting/formatting/type-checking, and the
  release procedure: [`CONTRIBUTING.md`](CONTRIBUTING.md).
- Release history and migration notes: [`CHANGELOG.md`](CHANGELOG.md).
- License: [MIT](LICENSE).
