targetprocess-py
An async Python library for the TargetProcess v1 REST API with immutable safety modes and comprehensive error handling.
Features
- Broad Entity Coverage: Typed resource managers for UserStory, Bug, Task, Feature, Epic, Request, TestCase, Time, Release, Iteration, TeamIteration, Project, Team, User, EntityState, Priority, Severity, Process, Workflow, EntityType, Term, CustomField, CustomActivity, CustomRule, Comment, Assignment, TeamAssignment, RoleEffort, Role, Attachment, Relation, and RelationType, plus a generic
entitiesaccessor for anything else - Immutable Safety Mode: Readonly/readwrite modes frozen at initialization, enforced at both the resource and request-handler layers
- Async-Only: Built on
httpx's async client; there is no sync client - Type-Safe Models: Pydantic v2 models for all entities, declaring the full value and reference field surface each type's
/metareports; snake_case field access throughout, and the 6 base Entity/NamedEntity fields (Id,ResourceType,CreateDate,ModifyDate,CustomFields,Name) also expose PascalCase property accessors matching the API - Resilient Requests: Automatic retry with exponential backoff on HTTP 429 and 5xx responses (honouring
Retry-Afterwhen present), plus non-blocking async rate limiting viaeasylimit - Real TP Semantics:
access_token-query-param auth (TP's actual token scheme; Basic auth as the header-based alternative),where=filter pass-through, andNext-URL pagination - Well Tested: High test coverage with pytest, mypy strict mode, ruff linting, and a recorded VCR-cassette integration suite proving behaviour against a real TargetProcess instance
Installation
pip install targetprocess-py
Or with uv, inside an existing uv project
(one with a pyproject.toml):
uv add targetprocess-py
For an environment-only install with no project, use uv pip install targetprocess-py.
Quick Start
import asyncio
from targetprocess import ClientMode, TargetProcessClient
async def main() -> None:
async with TargetProcessClient(
domain="example.tpondemand.com",
token="your-api-token", # sent as the access_token query param
mode=ClientMode.READONLY, # prevents accidental writes
) as client:
# list() is an async generator - iterate it directly
async for story in client.user_stories.list(
where="(EntityState.Name eq 'Open')", limit=10
):
print(f"{story.id}: {story.name}")
asyncio.run(main())
Core Concepts
Safety Modes
The client requires an immutable mode parameter at initialization:
readonly: Blocks all write operations (create, update, delete) - perfect for reporting and analyticsreadwrite: Allows all operations - use when modifications are needed
# Readonly client - safe for production automation
client = TargetProcessClient(..., mode="readonly")
await client.user_stories.create(...) # Raises ReadOnlyViolation immediately
# Readwrite client - for authorized operations
client = TargetProcessClient(..., mode="readwrite")
await client.user_stories.update(id=123, state="Done") # Works
The mode is frozen after initialization and cannot be changed, providing defense-in-depth for production workflows.
Resource Managers
Access TargetProcess entities through typed resource managers:
# Specific entity types
async for story in client.user_stories.list():
...
bug = await client.bugs.get(id=456)
task = await client.tasks.create(Name="Investigate outage")
# Generic entity access (when the type is only known at runtime)
entity = await client.entities.get("Bug", 789) # Returns a NamedEntity
Type-Safe Models
All entities use Pydantic models with snake_case field access. The six base
Entity/NamedEntity fields (Id, ResourceType, CreateDate, ModifyDate,
CustomFields, Name) also expose PascalCase property accessors matching the API;
every other field is snake_case-only:
story = await client.user_stories.get(id=123)
# Base fields: both work
print(story.Id, story.id) # Entity ID
print(story.Name, story.name) # Entity name
print(story.CreateDate, story.create_date) # Dates
# Everything else: snake_case only
print(story.entity_state)
Each model declares the full value and reference field surface its type's
/api/v1/{collection}/meta reports — 656 of 656 fields across the 32 modelled types,
measured against TP 2608.2.0.3147. So the fields callers reach for are typed and
discoverable, not just tolerated:
story = await client.user_stories.get(id=123)
print(story.creator.full_name) # who raised it (a UserRef)
print(story.priority.name) # entity-type-scoped Priority record
print(story.numeric_priority) # global ordering rank (a float), not a Priority
print(story.time_spent, story.time_remain, story.effort_todo)
print(story.tags, story.units, story.forecast_end_date)
The class hierarchy mirrors TP's own, so a field TP defines once is declared once:
Entity → NamedEntity → GeneralEntity (TP's General) → AssignableEntity
(TP's Assignable). The six work-item types extend AssignableEntity; Project,
Team, Release, Iteration, TeamIteration and TestCase extend GeneralEntity;
the lookup, join and content types extend Entity/NamedEntity directly.
Two things stay outside that surface, both deliberately. TP's collection
properties (Comments, Messages, Revisions, …) are hydrated only via include=
and arrive as an Items envelope, so they remain undeclared and land in
model_extra — the exceptions being CustomFields and AssignedUser. And
User.Password, which TP marks unreadable, is left undeclared: a read payload
never carries it, so a dump of one never has a password-shaped attribute. It
can still be set through users.update(id, Password=...).
scripts/check_model_coverage.py re-measures
coverage against a live instance, and with --validate also parses real records
through each model — the check /meta cannot make, since it says which properties
exist and not what TP puts in them. It is a dev-time tool, not a CI gate: it needs a
real token, which CI does not have.
Numeric fields TP computes carry no range constraint. A bound on a server-supplied
value fails the whole entity rather than the field, so one odd roll-up would abort
an entire list() page; TP documents no bounds to assert. Range checks belong on the
write side, where times.upsert already puts them.
Usage
All examples below assume an open client (see Quick Start).
For the full walkthrough — basic operations, querying, error handling,
async-vs-sync, and safety-mode practice — see the usage guide.
Runnable scripts live in examples/.
Resource managers
Each entity type is a property on the client, all sharing the same
get / list / create / update / delete surface, plus bulk
create_many / update_many (one POST /{collection}/bulk request for a
whole batch — one rate-limit slot instead of one per entity):
user_stories, bugs, tasks, features, epics, requests,
test_cases, times, releases, iterations, projects, teams, users,
entity_states, priorities, comments, assignments, team_assignments,
role_efforts, roles, attachments, relations, relation_types,
team_iterations, custom_fields, severities, processes, workflows,
entity_types, terms, custom_activities, custom_rules — plus
entities, the generic accessor that takes the entity type name as its first
argument and carries the full read-and-write surface for anything without a
typed manager, gated by the same safety mode as the typed resources.
The server bounds the write surface too. Each manager declares what its
collection's /meta reports (CanCreate / CanUpdate / CanDelete), and
an operation the server refuses raises ReadOnlyViolation in every mode,
before any request is sent: relation_types, entity_types and terms are
read-only on the server, and custom_rules is update-only (toggling
IsEnabled), so it refuses create and delete. The generic entities
accessor mirrors the same refusals by name.
Typed coverage is deliberately the common paths plus the lookup and
configuration collections; the long tail — the polymorphic bases, the people
and allocation collections, metadata and audit, and the work-item types not
yet modelled — stays on entities by design, and gains a typed manager on
demand. See SPEC.md § Typed versus generic coverage.
Filtering and field selection
where= is a TargetProcess filter expression, passed through verbatim (no
client-side rewriting); include= selects extra fields to hydrate:
async for bug in client.bugs.list(
where="(EntityState.IsFinal eq 'false')",
include=["EntityState", "AssignedUser"],
limit=50,
):
state = bug.entity_state.name if bug.entity_state else "—"
print(f"{bug.id}: {bug.name} [{state}]")
list() also passes TP's server-side sorting and payload-shaping parameters
through as typed keyword arguments — order_by / order_by_desc, exclude,
result_include, append, innertake, and an opt-in skip offset — see
the usage guide.
Pagination
list() is an async iterator that fetches pages lazily. limit caps the
total number of items yielded across all pages; page_size is the
per-request page size (take=), independent of limit:
# At most 200 items, fetched 50 per request
async for story in client.user_stories.list(limit=200, page_size=50):
...
# Unbounded: omit limit; pages are fetched as you iterate
async for story in client.user_stories.list():
...
limit=0 yields nothing and makes no network request. Pagination follows the
server's Next URL verbatim (never a locally recomputed skip), so short or
irregular pages never silently drop or duplicate items.
Error handling
Every library error subclasses TargetProcessError, so you can catch a
specific failure or the base class:
from targetprocess import (
AuthenticationError,
NotFoundError,
RateLimitError,
TargetProcessError,
)
try:
bug = await client.bugs.get(id=123)
except NotFoundError:
... # 404 — no such entity
except AuthenticationError:
... # 401 — bad or expired credentials
except RateLimitError:
... # 429 — persisted after the built-in retries
except TargetProcessError:
... # any other library error
A READONLY client raises ReadOnlyViolation if a write is attempted — see
Safety-mode best practices.
429 and 5xx responses on safe methods (get/list) are retried automatically
with exponential backoff (honouring Retry-After); writes are never
auto-retried. See the usage guide for the full
exception hierarchy and retry policy.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
ValueError: Provide exactly one of token or basic_auth |
Pass exactly one auth method to the client — a token or a basic_auth=(user, pass) tuple, never both or neither. |
AuthenticationError (401) |
The access_token / Basic credentials are wrong or expired. |
ReadOnlyViolation on a write |
The client is in READONLY mode. Construct it with mode=ClientMode.READWRITE for create/update/delete. |
AttributeError: mode is immutable … |
mode is frozen at construction — build a new client rather than reassigning it. |
ParseError |
The response body did not match the model (often an unexpected include= shape). The wrapped pydantic.ValidationError is in the exception message. |
NetworkError vs RateLimitError |
NetworkError means no HTTP response arrived (connection error, timeout, DNS); RateLimitError means a 429 persisted after retries. |
| Token appearing in logs | The access_token travels as a URL query parameter, so it can surface in server/proxy logs — do not enable httpx request-URL logging in production. |
Architecture
Five-layer architecture for clean separation of concerns:
- Transport Layer: HTTP communication (httpx), authentication, rate limiting (easylimit)
- Request Handler / Response Parser Layer: Query construction, pagination, retry/backoff, and error mapping
- Resource Layer: Entity-specific managers (UserStories, Bugs, Tasks, etc.)
- Model Layer: Pydantic models for type safety and validation
- Client Layer: Main entry point, safety mode enforcement, session management
See SPEC.md for the full specification, including detailed architecture documentation.
Development Status
Current Status: Alpha - core library complete
- ✅ Project structure and configuration
- ✅ Exception hierarchy
- ✅ Client safety modes (handler-layer + resource-layer enforcement)
- ✅ HTTP transport layer (access_token / Basic auth)
- ✅ API request/response handling (RequestHandler + ResponseParser), retry/backoff, rate limiting
- ✅ Entity models (42 model classes: the
Entity/NamedEntity/GeneralEntity/AssignableEntitybase chain, 6 nested shapes (EntityRef,EntityTypeRef,UserRef,RefWithImportance,CustomFieldValue,CustomFieldConfig), and 32 concrete entity types across the assignable, planning, organisational, workflow and configuration, join, supporting, and custom-field groups), each declaring its type's full/metafield surface - ✅ Resource managers (typed + generic)
- ✅ Recorded integration test suite against a real TargetProcess instance
Requirements
- Python 3.12+
- httpx >= 0.27.0
- pydantic >= 2.0.0
- easylimit >= 0.3.4
Contributing
Contributions are welcome! Please see SPEC.md for the current specification.
Secrets management
The library and example scripts read all credentials from environment
variables — never from a committed file. The relevant variables are documented
in .env.example (the committed template) and the
examples README:
| Variable | Purpose |
|---|---|
TP_DOMAIN |
TargetProcess instance domain |
TP_TOKEN |
API token (sent as the access_token query parameter) |
TP_WRITE_ALLOWED_PROJECT_IDS |
Project id allow-list for the write examples |
For local development, copy the template to .env (which is gitignored) and
export the values into your shell:
cp .env.example .env # fill in real values
set -a && . ./.env && set +a # export them for the current shell
.gitignore matches .env, suffixed variants such as .env.local, any
*.env file, and .envrc; .env.example is the only env-shaped file that is
tracked. CodeRabbit additionally treats a .env appearing in a diff as a
critical defect. Note that this is a review-time control, not a local hook —
nothing in .pre-commit-config.yaml blocks an env file that is force-added
past .gitignore.
For anything that runs outside a developer shell — CI, scheduled automation,
production — load these variables from a secrets manager (GitHub Actions
secrets, a vault, or your platform's equivalent) rather than from a .env
file. No real secret belongs in this repository: if a value ever needs to be
committed (e.g. an encrypted fixture), encrypt it with
SOPS first and commit only the ciphertext.
Development Setup
# Clone the repository
git clone https://github.com/man8/targetprocess-py
cd targetprocess-py
# Install with dev dependencies
uv sync --all-extras
# Install the git hooks (lint + format on commit, types on push)
uv run pre-commit install
# Run tests
uv run pytest -q
# Run type checking
uv run mypy --strict src
# Run linting
uv run ruff check .
uv run ruff format --check .
Quality gates
The hooks in .pre-commit-config.yaml run the same checks CI runs, so a
failure surfaces before the push rather than minutes later in a CI log. Running uv run pre-commit run --all-files --hook-stage pre-commit and then the same command with --hook-stage pre-push
reproduces the CI quality job exactly.
| Gate | Where it runs | Rule |
|---|---|---|
| Lint & format | pre-commit, CI | ruff check (including pep8-naming) and ruff format --check |
| Types | pre-push, CI | mypy --strict src |
| Tests & coverage | CI | pytest with branch coverage over src, gated at ≥90% |
| Cyclomatic complexity | pre-commit, CI | ruff C901, max-complexity = 10 |
| File size | pre-commit, CI | 1000 lines / 256 KiB per tracked file (scripts/check_large_files.py) |
| Debt markers | pre-commit, CI | TODO/FIXME/HACK/XXX must name an issue, e.g. TODO(#123) (scripts/check_todos.py) |
| Internal references | pre-commit, CI | No tracker-style issue references or agent session URLs in tracked files (scripts/check_internal_refs.py) |
| Copy-paste | CI | jscpd, failing above 3% duplication (.jscpd.json) |
Naming follows PEP 8 as enforced by ruff's N rules, with two documented exceptions in
pyproject.toml: the PascalCase property accessors in _base.py mirror the wire format the
TargetProcess API returns, and ReadOnlyViolation keeps its published name rather than gaining an
Error suffix.
One check runs outside CI: scripts/check_model_coverage.py
diffs the declared model fields against a live instance's /meta, which needs a token CI does not
have. tests/test_model_field_coverage.py pins the same surface offline and does run in CI.
Dependency updates
Dependency bumps are automated by Dependabot (.github/dependabot.yml),
which opens weekly pull requests for three ecosystems:
uv— regeneratesuv.lockonly (versioning-strategy: lockfile-only), so the lower bounds declared inpyproject.tomlare never rewritten: the supported floor stays a deliberate decision while the versions CI resolves stay current. Test/lint/type tooling is grouped into one PR; runtime majors (httpx,pydantic,easylimit) arrive individually because they surface in this library's public API.github-actions— refreshes the commit-SHA pins in.github/workflows/, which are safe but never float on their own.pre-commit— refreshes therev:pins in.pre-commit-config.yaml. The ruff and mypy hooks are not pinned there (they run viauv run), so theuvecosystem already covers them.
Bumps are merged through the same gates as any other change (ruff, ruff-format, mypy --strict,
pytest at ≥90% coverage, the pre-commit hook suite, duplication detection, CodeQL, review), never
pushed directly to main.
License
Copyright (c) 2025-2026 man 8 consulting (Louis Mandelstam). Released under the MIT License - see LICENSE for details.
Links
- Usage guide: docs/USAGE.md
- Examples: examples/
- Specification: SPEC.md
- TargetProcess API Docs: IBM TargetProcess v1 REST API
Motivation
Built to enable automation workflows against a production TargetProcess instance, and to serve as a foundation for:
- MCP server for TargetProcess integration
- Windmill workflow automation wrappers
- Reporting and analytics tools
- Bulk operations and data migrations
Designed as a high-quality open-source library following modern Python best practices.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file targetprocess_py-0.1.0.tar.gz.
File metadata
- Download URL: targetprocess_py-0.1.0.tar.gz
- Upload date:
- Size: 326.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
353b9f1226eed8d8dc427696625d5708b34326bf42fe384312f2503eb88e3298
|
|
| MD5 |
3854ba577161fb04f8b219423ebe276e
|
|
| BLAKE2b-256 |
c55e9333d87876a9c162a4f4376fae02c41afc13696c5af9a79849afaede8570
|
Provenance
The following attestation bundles were made for targetprocess_py-0.1.0.tar.gz:
Publisher:
release.yml on man8/targetprocess-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
targetprocess_py-0.1.0.tar.gz -
Subject digest:
353b9f1226eed8d8dc427696625d5708b34326bf42fe384312f2503eb88e3298 - Sigstore transparency entry: 2756796042
- Sigstore integration time:
-
Permalink:
man8/targetprocess-py@5002fcbe97d16f7344b6b536f2bdf7ea2f7296f9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/man8
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5002fcbe97d16f7344b6b536f2bdf7ea2f7296f9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file targetprocess_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: targetprocess_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 104.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d71931a77086c53fd4b39dc73d94b1818413a61298d81efa48a44db6ee84c1a1
|
|
| MD5 |
6474f2b4312ce903877ee2202d7c00b4
|
|
| BLAKE2b-256 |
684b090edb083b459764d1e08e1eddf23b5b0397683ca56f3699379e23b1854f
|
Provenance
The following attestation bundles were made for targetprocess_py-0.1.0-py3-none-any.whl:
Publisher:
release.yml on man8/targetprocess-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
targetprocess_py-0.1.0-py3-none-any.whl -
Subject digest:
d71931a77086c53fd4b39dc73d94b1818413a61298d81efa48a44db6ee84c1a1 - Sigstore transparency entry: 2756796065
- Sigstore integration time:
-
Permalink:
man8/targetprocess-py@5002fcbe97d16f7344b6b536f2bdf7ea2f7296f9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/man8
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5002fcbe97d16f7344b6b536f2bdf7ea2f7296f9 -
Trigger Event:
push
-
Statement type: