Polarion REST API Client
A Python client for the Polarion ALM REST API, combining an auto-generated low-level client (from the upstream OpenAPI spec) with high-level resource wrappers for common operations.
Both synchronous and asynchronous usage are supported out of the box.
Features
- Auto-generated client from the Polarion OpenAPI specification via
openapi-python-client - High-level resource wrappers with full CRUD support for Projects, Work Items, Work Item Attachments, Work Item Comments, Work Item Approvals, Work Item Work Records, Linked Work Items, Externally Linked Work Items, Linked OSLC Resources, Collections, Custom Fields, Enumerations, Feature Selections, Icons, Jobs, License, Documents, Document Parts, Document Comments, Document Attachments, and Document Table extraction
- Sync and Async — every operation works with both
PolarionClient(sync) andPolarionAsyncClient(async) - Automatic pagination — fetch all pages transparently with
page_size=-1or iterate lazily with.iter() - Batch operations — bulk create, update, and delete work items in a single request
- Typed error hierarchy —
JSONAPIErrorsubclasses (Unauthorized,Forbidden,NotFound,Conflict,ServerError) for precise exception handling - Environment-based configuration — configure via environment variables or
.envfiles (withpython-dotenvsupport) - SSL via truststore — uses the OS certificate store by default for enterprise environments
Project Structure
.
├── codegen/ # OpenAPI specs + generation config
│ ├── polarion-openapi.json # upstream Polarion REST spec (input)
│ ├── polarion-openapi-clean.json # cleaned spec (derived)
│ └── client-config.yaml # openapi-python-client config
├── scripts/ # helper scripts
│ ├── clean_rest_spec.py # applies Polarion-specific fixes to the spec
│ ├── rest_json_validator.py # JSON / OpenAPI validation
│ ├── regenerate_polarion_rest_client.sh # end-to-end: clean → validate → generate → copy
│ └── set_version.py # set package version in pyproject.toml
├── src/
│ └── polarion_rest_client/ # installable package
│ ├── __init__.py # public API surface
│ ├── client.py # PolarionClient & PolarionAsyncClient
│ ├── session.py # get_env_vars() environment loader
│ ├── error.py # typed exception hierarchy
│ ├── resource.py # PolarionResource base class
│ ├── paging.py # generic pagination utilities
│ ├── project.py # Project resource
│ ├── workitem.py # WorkItem resource
│ ├── workitem_attachment.py # WorkItemAttachment resource
│ ├── workitem_comment.py # WorkItemComment resource
│ ├── workitem_approval.py # WorkItemApproval resource
│ ├── workitem_workrecord.py # WorkItemWorkRecord resource
│ ├── workitem_linked.py # WorkItemLinked resource
│ ├── workitem_externally_linked.py # WorkItemExternallyLinked resource
│ ├── workitem_linked_oslc.py # WorkItemLinkedOslc resource
│ ├── document.py # Document resource
│ ├── document_part.py # DocumentPart resource
│ ├── document_comment.py # DocumentComment resource
│ ├── document_attachment.py # DocumentAttachment resource
│ ├── document_table_utils.py # HTML table extraction helpers
│ ├── metadata.py # Metadata resource (global)
│ ├── collection.py # Collection resource
│ ├── custom_field.py # CustomField resource
│ ├── enumeration.py # Enumeration resource
│ └── openapi/ # auto-generated client (committed)
├── tests/ # unit + integration tests
├── examples/ # runnable usage examples
│ ├── common.py # shared helpers for examples
│ ├── workitem/ # work item examples
│ ├── document/ # document examples
│ ├── collection/ # collection examples
│ ├── custom_field/ # custom field examples
│ └── enumeration/ # enumeration examples
├── pyproject.toml # PDM / PEP 621 metadata
├── CHANGELOG.md
├── LICENSE
└── README.md
Requirements
- Python 3.10+
- PDM (
pipx install pdmrecommended)
Installation
From PyPI
pip install polarion-rest-client
From Source
git clone https://gitlab.com/elimesika-group/polarion-rest-client.git
cd polarion-rest-client
pdm install --dev
Configuration
The client reads configuration from environment variables. Set them in your shell or place them in a .env file (requires python-dotenv).
Required Variables
| Variable | Description |
|---|---|
POLARION_URL |
Base URL of the Polarion server (e.g., https://polarion.example.com) |
Authentication (one of the following)
| Variable | Description |
|---|---|
POLARION_TOKEN |
Personal access token (preferred) |
POLARION_USERNAME + POLARION_PASSWORD |
Basic authentication credentials |
Optional Variables
| Variable | Default | Description |
|---|---|---|
POLARION_TOKEN_PREFIX |
Bearer |
Authorization header prefix for token auth |
POLARION_VERIFY_SSL |
true |
Set to false to disable SSL verification |
POLARION_TIMEOUT |
30.0 |
Request timeout in seconds |
Example .env File
POLARION_URL=https://polarion.example.com
POLARION_TOKEN=your-personal-access-token
POLARION_VERIFY_SSL=true
POLARION_TIMEOUT=30.0
Quick Start
Synchronous
import polarion_rest_client as prc
from polarion_rest_client.project import Project
pc = prc.PolarionClient(**prc.get_env_vars())
project_api = Project(pc)
project = project_api.get("my-project-id")
print(project)
Asynchronous
import asyncio
import polarion_rest_client as prc
from polarion_rest_client.project import Project
async def main():
async with prc.PolarionAsyncClient(**prc.get_env_vars()) as pc:
project_api = Project(pc)
project = await project_api.get("my-project-id")
print(project)
asyncio.run(main())
High-Level Resources
Each resource wrapper accepts either a PolarionClient (sync) or PolarionAsyncClient (async) and provides a consistent API.
Project
from polarion_rest_client.project import Project
api = Project(pc)
api.list() # list all projects
api.get("project-id") # get a single project
api.create("new-project", tracker_prefix="NP") # create (async job, waits by default)
api.patch("project-id", name="New Name") # update attributes
api.delete("project-id") # delete (async job)
api.exists("project-id") # check existence (returns bool)
api.list_templates() # list project templates
# New in 25.12
api.move("project-id", location="/new/path") # move project (async job)
api.get_fields_metadata("project-id", resource_type="workitems")
# Test Parameter Definitions
api.list_test_parameter_definitions("project-id")
api.get_test_parameter_definition("project-id", "MyParam")
api.create_test_parameter_definitions("project-id", names=["ParamA", "ParamB"])
api.delete_test_parameter_definition("project-id", "ParamA")
api.delete_test_parameter_definitions("project-id", ids=["project-id/ParamB"])
WorkItem
from polarion_rest_client.workitem import WorkItem
api = WorkItem(pc)
api.create("proj", wi_type="task", title="My Task")
api.get("proj", "WI-001")
api.update("proj", "WI-001", title="Updated Title")
api.delete("proj", ["WI-001", "WI-002"])
# Pagination
items = api.list("proj", page_size=50) # single page
all_items = api.list("proj", page_size=-1) # fetch all pages
for item in api.iter("proj", page_size=100): # lazy iteration
print(item["id"])
# Batch operations
api.update_many("proj", [
{"id": "WI-001", "attributes": {"severity": "critical"}},
{"id": "WI-002", "attributes": {"severity": "major"}},
])
api.update_many_same_attrs("proj", ["WI-001", "WI-002"],
attributes={"status": "approved"})
# Search
api.find_by_title("proj", "My Task")
# Global (cross-project) operations
all_items = api.list_all(page_size=50)
for item in api.iter_all(page_size=100):
print(item["id"])
api.update_all([{"id": "proj/WI-001", "attributes": {"severity": "critical"}}])
api.delete_all([{"id": "proj/WI-001"}])
# Enum options & workflow actions
api.get_available_enum_options("proj", "WI-001", "status")
api.get_available_enum_options_for_type("proj", "status", wi_type="task")
api.get_current_enum_options("proj", "WI-001", "status")
api.get_workflow_actions("proj", "WI-001")
# Relationships (generic JSON:API)
api.get_relationships("proj", "WI-001", "categories")
api.create_relationships("proj", "WI-001", "categories",
[{"type": "categories", "id": "proj/cat-1"}])
api.update_relationships("proj", "WI-001", "categories",
[{"type": "categories", "id": "proj/cat-2"}])
api.delete_relationships("proj", "WI-001", "categories",
[{"type": "categories", "id": "proj/cat-2"}])
# Move to/from document
api.move_to_document("proj", "WI-001", target_document="proj/space/doc-name")
api.move_from_document("proj", "WI-001")
# Test parameter definitions
api.list_test_parameter_definitions("proj", "WI-001")
api.get_test_parameter_definition("proj", "WI-001", "param-id")
WorkItemAttachment
from polarion_rest_client.workitem_attachment import WorkItemAttachment
api = WorkItemAttachment(pc)
api.list("proj", "WI-001")
api.get("proj", "WI-001", "attachment-id")
api.get_content("proj", "WI-001", "attachment-id") # returns bytes
api.create("proj", "WI-001",
file_data=b"...", file_name="diagram.png", mime_type="image/png")
api.update("proj", "WI-001", "attachment-id", title="New Title")
api.delete("proj", "WI-001", "attachment-id")
for att in api.iter("proj", "WI-001"):
print(att["id"])
WorkItemComment
from polarion_rest_client.workitem_comment import WorkItemComment
api = WorkItemComment(pc)
api.list("proj", "WI-001")
api.get("proj", "WI-001", "comment-id")
api.create("proj", "WI-001", text="Review needed")
api.update("proj", "WI-001", "comment-id", resolved=True)
api.reply("proj", "WI-001", "comment-id", text="Done")
api.resolve("proj", "WI-001", "comment-id")
api.unresolve("proj", "WI-001", "comment-id")
for c in api.iter("proj", "WI-001"):
print(c["id"])
WorkItemApproval
from polarion_rest_client.workitem_approval import WorkItemApproval
api = WorkItemApproval(pc)
api.list("proj", "WI-001")
api.get("proj", "WI-001", "user-id")
api.create("proj", "WI-001", user_id="user-id")
api.update("proj", "WI-001", "user-id", status="approved")
api.approve("proj", "WI-001", "user-id")
api.disapprove("proj", "WI-001", "user-id")
api.reset("proj", "WI-001", "user-id")
api.delete("proj", "WI-001", "user-id")
# Batch update
api.update_many("proj", "WI-001", items=[
{"id": "proj/WI-001/user-a", "status": "approved"},
{"id": "proj/WI-001/user-b", "status": "disapproved"},
])
# Batch delete
api.delete_many("proj", "WI-001",
ids=["proj/WI-001/user-a", "proj/WI-001/user-b"])
for a in api.iter("proj", "WI-001"):
print(a["id"], a.get("attributes", {}).get("status"))
WorkItemWorkRecord
from polarion_rest_client.workitem_workrecord import WorkItemWorkRecord
api = WorkItemWorkRecord(pc)
api.list("proj", "WI-001")
api.get("proj", "WI-001", "record-id")
api.create("proj", "WI-001", user_id="user-id", time_spent="2h", date="2026-04-26", comment="Design work")
api.delete("proj", "WI-001", "record-id")
# Batch delete
api.delete_many("proj", "WI-001",
ids=["proj/WI-001/rec-1", "proj/WI-001/rec-2"])
for r in api.iter("proj", "WI-001"):
print(r["id"], r.get("attributes", {}).get("timeSpent"))
WorkItemLinked
from polarion_rest_client.workitem_linked import WorkItemLinked
api = WorkItemLinked(pc)
api.list("proj", "WI-001")
api.get("proj", "WI-001", role_id="relates_to", target_project_id="proj", linked_work_item_id="WI-002")
api.create("proj", "WI-001", target_project_id="proj", target_work_item_id="WI-002", role="relates_to")
api.update("proj", "WI-001", "relates_to", "proj", "WI-002", suspect=True)
api.delete("proj", "WI-001", "relates_to", "proj", "WI-002")
# Batch delete
api.delete_many("proj", "WI-001", ids=["proj/WI-001/relates_to/proj/WI-002"])
for lk in api.iter("proj", "WI-001"):
print(lk["id"], lk.get("attributes", {}).get("role"))
# Backlinks (incoming links, new in 25.12)
api.list_backlinks("proj", "WI-002")
api.create_backlink("proj", "WI-002",
source_project_id="proj",
source_work_item_id="WI-003",
role="relates_to")
for bl in api.iter_backlinks("proj", "WI-002"):
print(bl["id"])
WorkItemExternallyLinked
from polarion_rest_client.workitem_externally_linked import WorkItemExternallyLinked
api = WorkItemExternallyLinked(pc)
api.list("proj", "WI-001")
api.get("proj", "WI-001", role_id="relates_to", hostname="polarion.example.com",
target_project_id="remote-proj", linked_work_item_id="WI-002")
api.create("proj", "WI-001",
work_item_uri="https://polarion.example.com/polarion/rest/v1/#/project/remote-proj/workitem?id=WI-002",
role="relates_to")
api.delete("proj", "WI-001", role_id="relates_to", hostname="polarion.example.com",
target_project_id="remote-proj", linked_work_item_id="WI-002")
# Batch delete
api.delete_many("proj", "WI-001",
ids=["proj/WI-001/relates_to/polarion.example.com/remote-proj/WI-002"])
for lk in api.iter("proj", "WI-001"):
print(lk["id"], lk.get("attributes", {}).get("workItemURI"))
WorkItemLinkedOslc
from polarion_rest_client.workitem_linked_oslc import WorkItemLinkedOslc
api = WorkItemLinkedOslc(pc)
# List linked OSLC resources on a work item
links = api.list("proj", "WI-001", fields=["@all"])
# Create a link to an external OSLC URI
created = api.create(
"proj", "WI-001",
uri="https://example.com/oslc/resources/CR-42",
role="http://open-services.net/ns/cm#relatedChangeRequest",
label="Related change request",
)
created_id = created["id"]
# Batch-delete by full compound IDs (role/URI segments are
# percent-encoded automatically for the DELETE body)
api.delete("proj", "WI-001", ids=[created_id])
for lk in api.iter("proj", "WI-001"):
attrs = lk.get("attributes") or {}
print(lk["id"], attrs.get("uri"), attrs.get("role"))
Note: Polarion returns linked-OSLC IDs with raw role/resource URIs.
delete()percent-encodes those URI segments automatically, so you can pass IDs exactly as returned bylist/create.
Metadata
from polarion_rest_client.metadata import Metadata
api = Metadata(pc)
# Server metadata (version, build, REST API config)
meta = api.get_metadata()
attrs = meta["data"]["attributes"]
print(attrs["version"], attrs["build"], attrs["timezone"])
# Global fields metadata (non-project-scoped)
fm = api.get_fields_metadata(resource_type="workitems")
for field_id, info in fm["data"]["attributes"].items():
print(f"{field_id}: {info['label']} [{info['type']['kind']}]")
Collection
from polarion_rest_client.collection import Collection
api = Collection(pc)
# List / Get / Create / Update / Delete
api.list("proj", page_size=50)
api.get("proj", "collection-id")
api.create("proj", name="Sprint-42", description="Sprint 42 deliverables")
api.update("proj", "collection-id", name="Sprint-42 Final")
api.delete("proj", "collection-id")
# Batch delete
api.delete_many("proj", ids=["proj/col-1", "proj/col-2"])
# Close / Reopen / Reuse (clone to another project)
api.close("proj", "collection-id")
api.reopen("proj", "collection-id")
api.reuse("proj", "collection-id",
target_project_id="other-proj",
target_collection_name="Sprint-42-copy")
# Iteration
for col in api.iter("proj", page_size=50, fields=["@all"]):
print(col["id"], (col.get("attributes") or {}).get("name"))
# Relationships (documents, rich_pages, test_runs, upstream_collections)
api.get_relationships("proj", "collection-id", "documents")
api.create_relationships("proj", "collection-id", "documents",
[{"type": "documents", "id": "proj/space/my-doc"}])
api.update_relationships("proj", "collection-id", "documents",
[{"type": "documents", "id": "proj/space/my-doc"}])
api.delete_relationships("proj", "collection-id", "documents",
[{"type": "documents", "id": "proj/space/my-doc"}])
CustomField
from polarion_rest_client.custom_field import CustomField
api = CustomField(pc)
# Get custom field definitions (project-scoped)
result = api.get("workitems", "task", project_id="proj", fields=["@all"])
for f in (result.get("data") or {}).get("attributes", {}).get("fields", []):
print(f["id"], f["name"], f.get("type", {}).get("kind"))
# Get custom field definitions (global)
result = api.get("workitems", "task", fields=["@all"])
# Create a custom field definition (project-scoped)
api.create("workitems", "task",
field_definitions=[{
"id": "myCustomField",
"name": "My Custom Field",
"type": {"kind": "string"},
"description": "A custom text field",
}],
project_id="proj",
)
# Create a custom field definition (global)
api.create("workitems", "task",
field_definitions=[{
"id": "globalField",
"name": "Global Field",
"type": {"kind": "boolean"},
}],
)
# Update custom field definitions (project-scoped)
api.update("workitems", "task",
field_definitions=[{
"id": "myCustomField",
"name": "Renamed Field",
"type": {"kind": "string"},
"required": True,
}],
project_id="proj",
)
# Delete custom field definitions
api.delete("workitems", "task", project_id="proj") # project-scoped
api.delete("workitems", "task") # global
Enumeration
from polarion_rest_client.enumeration import Enumeration
api = Enumeration(pc)
# List enumerations (project-scoped)
result = api.list(project_id="proj", page_size=50)
for item in (result.get("data") or []):
print(item["id"])
# List enumerations (global)
result = api.list(page_size=50)
# Get a single enumeration by composite key
enum = api.get("plans", "myEnum", "~", project_id="proj", fields=["@all"])
options = enum["data"]["attributes"]["options"]
# Create a new enumeration (project-scoped)
created = api.create(
"plans", "myCustomEnum", "~",
options=[
{"id": "opt1", "name": "Option One", "default": True},
{"id": "opt2", "name": "Option Two"},
],
project_id="proj",
)
# Update enumeration options (replaces the entire options list)
api.update(
"plans", "myCustomEnum", "~",
options=[
{"id": "opt1", "name": "Option One Renamed", "default": True},
{"id": "opt2", "name": "Option Two"},
{"id": "opt3", "name": "Option Three"},
],
project_id="proj",
)
# Delete an enumeration (project-scoped)
api.delete("plans", "myCustomEnum", "~", project_id="proj")
# Delete an enumeration (global — affects all projects)
api.delete("plans", "myCustomEnum", "~")
Note: The composite key for enumerations consists of
enum_context(e.g."plans","testing","documents", or"~"for work-item/general),enum_name, andtarget_type(e.g."task"or"~"for all types). The"~"wildcard is valid in URL path parameters (GET/PATCH/DELETE) but is automatically omitted from POST request bodies where it is not accepted.
Feature Selection
from polarion_rest_client.feature_selection import FeatureSelection
api = FeatureSelection(pc)
# List feature selections for a work item
result = api.list("proj", "WI-001", page_size=50)
for item in (result.get("data") or []):
print(item["id"], item["attributes"]["selectionType"])
# Get a single feature selection by its 5-part composite key
fs = api.get(
"proj", "WI-001",
selection_type_id="included",
target_project_id="proj",
target_work_item_id="WI-010",
)
print(fs["data"]["attributes"]["selectionType"])
# Iterate over all feature selections (auto-pagination)
for fs in api.iter("proj", "WI-001"):
print(fs["id"])
# Historical view at a specific revision
result = api.list("proj", "WI-001", revision="1234")
Note: Feature selections are a read-only sub-resource. The Polarion API does not expose POST, PATCH, or DELETE endpoints for feature selections. The resource ID is a 5-part composite key:
{parentProject}/{parentWorkItem}/{selectionType}/{targetProject}/{targetWorkItem}. TheselectionTypecan be"included","excluded", or"implicitly-included".
Icon
from polarion_rest_client.icon import Icon
api = Icon(pc)
# List built-in default icons
result = api.list_default(page_size=50)
for item in (result.get("data") or []):
print(item["id"], item["attributes"]["iconUrl"])
# Get a single default icon
icon = api.get_default("default/task.gif", fields=["@all"])
# List global (server-wide) custom icons
result = api.list(page_size=50)
# List project-scoped icons
result = api.list(project_id="proj", page_size=50)
# Get a single icon (global or project-scoped)
icon = api.get("myicon.gif")
icon = api.get("myicon.gif", project_id="proj")
# Upload a new icon (project-scoped)
with open("myicon.gif", "rb") as f:
created = api.create([("myicon.gif", f.read())], project_id="proj")
# Upload multiple icons at once (global)
created = api.create([
("icon1.gif", icon1_bytes),
("icon2.png", icon2_bytes),
])
Note: Icons are immutable after creation — the Polarion API does not expose PATCH or DELETE endpoints for icons. Icons exist in three scopes: default (built-in, read-only), global (server-wide), and project (project-scoped). The
create()method accepts a list of(filename, content)tuples; files and metadata entries are matched by order.
Job
from polarion_rest_client.job import Job
api = Job(pc)
# List all jobs (paginated)
result = api.list(page_size=50, fields=["@all"])
for item in (result.get("data") or []):
attrs = item.get("attributes") or {}
status = (attrs.get("status") or {}).get("type", "?")
print(f"{item['id']} name={attrs.get('name')} status={status}")
# Get a single job by ID
job = api.get("job-123", fields=["@all"])
# Execute a job
result = api.execute(
"jobs.cleanup", # job factory ID
"My Cleanup Job", # human-readable name
scope="system", # "system", "project:{projectId}", or "path:/{path}"
params={"dryRun": "true"},
)
job_id = result["data"]["id"]
# Wait for a job to finish (polls until OK/FAILED/CANCELLED)
final = api.wait(job_id, timeout_s=120, poll_every_s=5)
# Download the job log (text/plain)
log_text = api.get_log_content(job_id)
# Download a result file (raw bytes)
file_bytes = api.get_result_file(job_id, "report.zip")
Note: The
execute()method returns a 202 Accepted response with the newly created job resource. Usewait()to poll until the job reaches a terminal state (OK,FAILED, orCANCELLED). Theget_log_content()method returns the log as a UTF-8 string, whileget_result_file()returns raw bytes for any download file produced by the job.
License
from polarion_rest_client.license import License
api = License(pc)
# Get product license limits and default slots
lic = api.get(fields=["@all"])
limits = lic["data"]["attributes"]["limits"]
# List / get license assignments
result = api.list_assignments(page_size=50, active_only=True)
asn = api.get_assignment("alice", fields=["@all"])
# Update default license slots
api.update(
default_base_license_slot="concurrent/named/default",
default_add_on_license_slots=["addon/named/qa"],
)
# Batch-update user assignments
api.update_assignments([
{
"id": "alice",
"base_slot": "concurrent/named/default",
"add_on_slots": ["addon/named/qa"],
},
])
# List / get / create / delete license slots
slots = api.list_slots("concurrent", page_size=50)
slot = api.get_slot("concurrent", "named", "default")
created = api.create_slots("concurrent", slots=[{"group": "qa-team", "total": 5}])
api.delete_slots("concurrent", ids=["concurrent/named/qa-team"])
Note: Write operations (
update,update_assignments,create_slots,delete_slots) mutate server-wide license configuration. Prefer read-only methods unless you intend to change license defaults or slot allocations.
Document
from polarion_rest_client.document import Document
api = Document(pc)
# CRUD
api.create("proj", "space", module_name="my-doc", title="My Document")
api.get("proj", "space", "my-doc")
api.update("proj", "space", "my-doc", title="Updated Title")
# Listing (new in 25.12)
api.list("proj", "space", page_size=50) # space-scoped
api.list_project("proj", page_size=50) # project-scoped (all spaces)
api.list_all(page_size=50) # cross-project
for doc in api.iter("proj", "space"): # space-scoped iterator
print(doc["id"])
for doc in api.iter_project("proj"): # project-scoped iterator
print(doc["id"])
for doc in api.iter_all(): # cross-project iterator
print(doc["id"])
# Search by title
api.find_by_title("proj", "space", "My Document", limit=5)
# Actions
api.branch("proj", "space", "my-doc", target_project_id="other-proj")
api.copy("proj", "space", "my-doc", target_document_name="my-doc-copy")
api.merge_from_master("proj", "space", "my-doc")
api.merge_to_master("proj", "space", "my-doc")
DocumentPart
from polarion_rest_client.document_part import DocumentPart
api = DocumentPart(pc)
api.list("proj", "space", "my-doc", page_size=-1)
api.get("proj", "space", "my-doc", "part-id", fields_parts="@all")
api.create("proj", "space", "my-doc", part_type="workitem", work_item_id="WI-001")
api.create("proj", "space", "my-doc", part_type="workitem",
work_item_id="WI-002", layout=1) # layout attr (new in 25.12)
for part in api.iter("proj", "space", "my-doc"):
print(part["id"])
# Move a part (new in 25.12) — parent is required with before/after
api.move("proj", "space", "my-doc", "workitem_WI-002",
parent="proj/space/my-doc/workitem_WI-001")
api.move("proj", "space", "my-doc", "workitem_WI-003",
after="proj/space/my-doc/workitem_WI-002",
parent="proj/space/my-doc/workitem_WI-001")
DocumentComment
from polarion_rest_client.document_comment import DocumentComment
api = DocumentComment(pc)
api.list("proj", "space", "my-doc")
api.get("proj", "space", "my-doc", "comment-id")
api.create("proj", "space", "my-doc", text="Review needed")
api.update("proj", "space", "my-doc", "comment-id", resolved=True)
api.reply("proj", "space", "my-doc", "comment-id", text="Done")
api.resolve("proj", "space", "my-doc", "comment-id")
api.unresolve("proj", "space", "my-doc", "comment-id")
DocumentAttachment
from polarion_rest_client.document_attachment import DocumentAttachment
api = DocumentAttachment(pc)
api.list("proj", "space", "my-doc")
api.get("proj", "space", "my-doc", "attachment-id")
api.create("proj", "space", "my-doc",
file_data=b"...", file_name="diagram.png", mime_type="image/png")
api.update("proj", "space", "my-doc", "attachment-id", title="Updated Title")
content = api.get_content("proj", "space", "my-doc", "attachment-id")
Document Table Extraction
Extract structured data from HTML tables embedded in Polarion documents into pandas DataFrames.
from polarion_rest_client.document_table_utils import (
extract_document_tables_by_columns,
)
df = extract_document_tables_by_columns(
pc, "proj", "space", "my-doc",
expected_columns=["Name", "Version", "Status"],
)
print(df)
Error Handling
The client provides a typed exception hierarchy rooted in PolarionError:
PolarionError
├── HTTPStatusError # non-2xx without JSON:API errors payload
└── JSONAPIError # JSON:API errors[] present
├── Unauthorized # 401
├── Forbidden # 403
├── NotFound # 404
├── Conflict # 409
└── ServerError # 5xx
from polarion_rest_client import NotFound, Unauthorized
try:
project_api.get("nonexistent")
except NotFound as e:
print(f"Not found: {e.detail}")
except Unauthorized:
print("Check your credentials")
Examples
The examples/ directory contains runnable scripts demonstrating common workflows. Set the following environment variables before running:
| Variable | Description |
|---|---|
POLARION_TEST_PROJECT_ID |
Project ID for examples |
POLARION_TEST_SPACE_ID |
Space ID (defaults to _default) |
POLARION_TEST_WI_TYPE |
Work item type (defaults to task) |
# Work item CRUD
python examples/workitem/workitem_CRUD.py
# Document CRUD
python examples/document/document_CRUD.py
# Document attachment CRUD
python examples/document/document_attachment_CRUD.py
# Document comment CRUD
python examples/document/document_comment_CRUD.py
# Pagination
python examples/workitem/workitem_paging.py
python examples/document/document_paging.py
# Batch updates
python examples/workitem/workitem_batch_update.py
# Work item attachment CRUD
python examples/workitem/workitem_attachment_CRUD.py
# Work item comment CRUD
python examples/workitem/workitem_comment_CRUD.py
# Work item approval CRUD
python examples/workitem/workitem_approval_CRUD.py
# Work item work record CRUD
python examples/workitem/workitem_workrecord_CRUD.py
# Linked work items CRUD
python examples/workitem/workitem_linked_CRUD.py
# Externally linked work items CRUD
python examples/workitem/workitem_externally_linked_CRUD.py
# Linked OSLC Resources (baseline → create → verify → delete → restore)
python examples/workitem/workitem_linked_oslc_CRUD.py
# Advanced work item operations (enum options, workflow, move, global list)
python examples/workitem/workitem_advanced.py
# Collection CRUD (create, get, update, close, reopen, batch delete)
python examples/collection/collection_CRUD.py
# Custom Field CRUD (get, create, update, delete — project & global)
python examples/custom_field/custom_field_CRUD.py
# Enumeration CRUD (list, get, create, update, delete — project & global read-only)
python examples/enumeration/enumeration_CRUD.py
# Global metadata
python examples/metadata.py
# Table extraction
python examples/document/document_tables.py
Regeneration Workflow
The generated client can be rebuilt from an updated OpenAPI specification.
- Place or update the upstream spec:
codegen/polarion-openapi.json
- Run the pipeline (clean, validate, generate, copy):
pdm run regenerate-client
- Verify the import:
python -c "import polarion_rest_client.openapi as pkg; print('OK:', pkg.__name__)"
The generated package under src/polarion_rest_client/openapi/ is committed so that consumers can use the client without running the generator.
Bumping to a New Polarion Version (Developer Guide)
When a new Polarion server version is released and a new OpenAPI specification becomes available, follow this end-to-end procedure to update the client.
Prerequisites
- Access to the new Polarion REST API JSON specification (exported from the target server)
- A working development environment (
pdm install --dev) - Access to a Polarion test server running the new version (for integration tests)
Step-by-Step Procedure
1. Obtain the new OpenAPI specification
Export the REST API JSON from the new Polarion server (typically available at https://<server>/polarion/rest/v1/openapi.json) and place it at:
cp /path/to/new-spec.json codegen/polarion-openapi.json
2. Clean the specification
The upstream spec contains known quirks that must be patched before code generation. Run the cleaner:
python scripts/clean_rest_spec.py \
codegen/polarion-openapi.json \
codegen/polarion-openapi-clean.json
The cleaner applies the following deterministic fixes:
| Fix | What it does |
|---|---|
octet_stream_upload |
Normalizes application/octet-stream upload schemas to string/binary |
wildcard_error_responses |
Expands vendor 4XX-5XX entries into concrete 4xx/5xx responses |
missing_downloads_items |
Adds missing items for array schemas in jobsSingle*Response |
error_source_nullability |
Marks error source objects as nullable |
expand_reference_only_components |
Wraps bare $ref component schemas in allOf for the generator |
Review the output. If the new spec introduces new quirks that break generation, add a fixer function in scripts/clean_rest_spec.py, register it in apply_all_fixes(), and document it in the table above.
3. Validate the cleaned specification
python scripts/rest_json_validator.py codegen/polarion-openapi-clean.json
This checks JSON syntax and validates the spec against the OpenAPI schema. Fix any errors before proceeding.
4. Regenerate the client
pdm run regenerate-client
This runs the full pipeline (clean, validate, generate, copy into src/polarion_rest_client/openapi/). Alternatively, steps 2-4 are equivalent to this single command.
5. Check for breaking changes in the generated code
After regeneration, verify that the high-level wrappers still compile and function correctly:
python -c "import polarion_rest_client.openapi as pkg; print('OK:', pkg.__name__)"
Common issues to watch for:
| Symptom | Likely cause | Resolution |
|---|---|---|
ImportError in a resource module |
Endpoint or model was renamed/removed in the new spec | Update the import paths in the affected wrapper (e.g., project.py, workitem.py) |
TypeError on a wrapper method call |
Generated function signature changed (new/renamed params) | Run resolve_page_params() diagnostics; update keyword arguments in the wrapper |
| New endpoints available | Polarion added new REST resources | Consider adding new high-level wrappers under src/polarion_rest_client/ |
| Generator warnings about unsupported schemas | New spec patterns the cleaner doesn't handle yet | Add a new fixer in scripts/clean_rest_spec.py |
6. Update the package version
Bump the version to reflect the new Polarion server version. The format is XX.YY.ZZ where XX.YY matches the Polarion version and ZZ starts at 01 for a new server release:
pdm run python scripts/set_version.py XX.YY.01
For example, if upgrading to Polarion 25.12:
pdm run python scripts/set_version.py 25.12.01
7. Run the test suite
# Unit tests (no server required)
pdm run pytest -m unit
# Integration tests (requires POLARION_* env vars pointing to the new server)
pdm run pytest -m integration
Fix any failures caused by API changes before proceeding.
8. Verify the examples
The examples/ directory contains runnable scripts that exercise the high-level wrappers against a real server. Run them all to confirm that the new spec hasn't broken any workflows:
# Ensure env vars point to the new Polarion server
export POLARION_URL=https://polarion-new.example.com
export POLARION_TOKEN=...
export POLARION_TEST_PROJECT_ID=my-test-project
# Run the full example suite
bash examples/run_all.sh
If any example fails, investigate whether the issue is in the generated client (step 5) or the high-level wrapper, and fix accordingly.
9. Update documentation
- Update
CHANGELOG.mdwith the new version and a summary of changes - If new high-level resources were added, update the High-Level Resources section of this README
- If new environment variables or configuration options were introduced, update the Configuration section
10. Build and verify
pdm run pre_build
pdm build
Sanity-check the wheel:
pip install dist/polarion_rest_client-*.whl
python -c "import polarion_rest_client; print(polarion_rest_client.__version__)"
11. Commit and release
git add -A
git commit -m "chore(release): bump to Polarion XX.YY (vXX.YY.01)"
git tag -a vXX.YY.01 -m "Polarion XX.YY, patch 01"
git push origin HEAD && git push origin vXX.YY.01
Then follow the Releases and Publishing section to publish to PyPI.
Custom Templates (Advanced)
You can override the Jinja templates used by openapi-python-client to customize the generated code (naming conventions, default headers, timeouts, docstrings, etc.).
- Place overrides under
codegen/custom_templates/using the same relative paths as the upstream templates. - The regeneration script automatically passes
--custom-template-pathwhen this directory exists.
To discover the upstream template paths:
python -c "
import importlib.resources as r, openapi_python_client as opc
print((r.files(opc) / 'templates').as_posix())
"
Copy only the files you want to override; the rest fall back to defaults.
Versioning
Package versions follow the format XX.YY.ZZ:
| Segment | Meaning |
|---|---|
XX.YY |
Polarion server version (e.g., 25.06) |
ZZ |
Client patch number (e.g., 08) |
PEP 440 normalizes 25.06.08 to 25.6.8 on PyPI. Git tags use the human-friendly form (v25.06.08).
Set the version with:
pdm run python scripts/set_version.py 25.06.08
Tests
Tests are parameterized to run in both sync and async modes.
pdm run pytest
Test markers:
unit— fast tests, no external servicesintegration— tests that hit a real Polarion server
pdm run pytest -m unit
pdm run pytest -m integration
Contributing
- Create a feature branch:
git checkout -b feature/my-feature
- Run the full pipeline before committing:
pdm run pre_build
- Run tests:
pdm run pytest
- Lint:
flake8 . --exclude ./venv --max-line-length=120
- Commit following Conventional Commits and open a Merge Request with a clear description, issue references, and test evidence.
Avoid manual edits to files under
src/polarion_rest_client/openapi/— they are regenerated.
Releases and Publishing
Quick Release Checklist
- Update
CHANGELOG.md - Set version:
pdm run python scripts/set_version.py XX.YY.ZZ - Regenerate and build:
pdm run pre_build
pdm build
- Dry-run on TestPyPI:
pdm publish -r testpypi --username __token__ --password "$TESTPYPI_TOKEN"
- Verify from TestPyPI:
pip install -i https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple \
polarion-rest-client==<version>
- Publish to PyPI:
pdm publish --username __token__ --password "$PYPI_TOKEN"
- Tag and push:
git tag -a vXX.YY.ZZ -m "Polarion XX.YY, patch ZZ"
git push origin vXX.YY.ZZ
Token Setup (One-Time)
| Registry | Registration | Token Scope |
|---|---|---|
| TestPyPI | Required for dry-run | TESTPYPI_TOKEN — Entire account (first upload) |
| PyPI | Required for release | PYPI_TOKEN — Entire account, then project-scoped |
Never commit tokens. Pass them via environment variables. After your first PyPI release, create a project-scoped token and revoke the wide one.
Troubleshooting
| Error | Fix |
|---|---|
| "File already exists" / 400 | Bump patch version, rebuild, retry |
| 403 "user isn't allowed to upload" | Use an Entire account token for first upload |
| Name conflict | Change [project].name in pyproject.toml |
| Packaging issues | rm -rf dist && pdm run pre_build && pdm build |
License
Licensed under the MIT License.
Release files for polarion-rest-client 25.12.13
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| polarion_rest_client-25.12.13.tar.gz | 919.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| polarion_rest_client-25.12.13-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 4.2 MB
Release files / polarion_rest_client-25.12.13.tar.gz
| Download URL | polarion_rest_client-25.12.13.tar.gz |
|---|---|
| Size | 919.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4418ad91af3c489ecb04fa5f327acce3c5b73aa15839b17ba45eb8f661cef2ed
|
|
BLAKE2b-256 checksum How to use checksums |
d3b7c302321ab75545025f3eb2d84289668ef4440eca275fb085df73fc783733
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.28.0 CPython/3.12.13 Linux/5.15.154+
|
Release files / polarion_rest_client-25.12.13-py3-none-any.whl
| Download URL | polarion_rest_client-25.12.13-py3-none-any.whl |
|---|---|
| Size | 3.3 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e4a44197ad54cc48e05cd7590c6794d0509928a480c3c82717d775b727eeee17
|
|
BLAKE2b-256 checksum How to use checksums |
c833b2a6a5a7e3d6b78c9c021ac231402a2b646f8576a951f9610a5e35199406
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.28.0 CPython/3.12.13 Linux/5.15.154+
|