Metadata-Version: 2.4
Name: polarion-rest-client
Version: 25.6.10
Summary: Polarion REST API client, wrappers & helpers
Author-Email: emesika <elimesika@gmail.com>
License-Expression: MIT
Requires-Python: >=3.10
Requires-Dist: attrs>=23.1.0
Requires-Dist: httpx>=0.23.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: python-dateutil>=2.9.0
Requires-Dist: beautifulsoup4<5,>=4.12
Requires-Dist: pandas<3,>=2.0
Requires-Dist: truststore>=0.10.4
Description-Content-Type: text/markdown

# Polarion REST API Client

[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
[![PDM](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev/)

A Python client for the [Polarion ALM](https://www.siemens.com/polarion) 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`](https://github.com/openapi-generators/openapi-python-client)
- **High-level resource wrappers** with full CRUD support for Projects, Work Items, Work Item Attachments, Documents, Document Parts, Document Comments, Document Attachments, and Document Table extraction
- **Sync and Async** — every operation works with both `PolarionClient` (sync) and `PolarionAsyncClient` (async)
- **Automatic pagination** — fetch all pages transparently with `page_size=-1` or iterate lazily with `.iter()`
- **Batch operations** — bulk create, update, and delete work items in a single request
- **Typed error hierarchy** — `JSONAPIError` subclasses (`Unauthorized`, `Forbidden`, `NotFound`, `Conflict`, `ServerError`) for precise exception handling
- **Environment-based configuration** — configure via environment variables or `.env` files (with `python-dotenv` support)
- **SSL via truststore** — uses the OS certificate store by default for enterprise environments

---

## Project Structure

```text
.
├── 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
│       ├── 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
│       └── 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
├── pyproject.toml                          # PDM / PEP 621 metadata
├── CHANGELOG.md
├── LICENSE
└── README.md
```

---

## Requirements

- Python **3.10+**
- [PDM](https://pdm.fming.dev/latest/) (`pipx install pdm` recommended)

---

## Installation

### From PyPI

```bash
pip install polarion-rest-client
```

### From Source

```bash
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

```bash
POLARION_URL=https://polarion.example.com
POLARION_TOKEN=your-personal-access-token
POLARION_VERIFY_SSL=true
POLARION_TIMEOUT=30.0
```

---

## Quick Start

### Synchronous

```python
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

```python
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

```python
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
```

### WorkItem

```python
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

```python
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"])
```

### Document

```python
from polarion_rest_client.document import Document

api = Document(pc)

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")
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

```python
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")
for part in api.iter("proj", "space", "my-doc"):
    print(part["id"])
```

### DocumentComment

```python
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

```python
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.

```python
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
```

```python
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`) |

```bash
# 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

# Advanced work item operations (enum options, workflow, move, global list)
python examples/workitem/workitem_advanced.py

# Table extraction
python examples/document/document_tables.py
```

---

## Regeneration Workflow

The generated client can be rebuilt from an updated OpenAPI specification.

1. Place or update the upstream spec:

```text
codegen/polarion-openapi.json
```

2. Run the pipeline (clean, validate, generate, copy):

```bash
pdm run regenerate-client
```

3. Verify the import:

```bash
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:

```bash
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:

```bash
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

```bash
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

```bash
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:

```bash
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:

```bash
pdm run python scripts/set_version.py XX.YY.01
```

For example, if upgrading to Polarion 25.12:

```bash
pdm run python scripts/set_version.py 25.12.01
```

#### 7. Run the test suite

```bash
# 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:

```bash
# 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.md` with 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

```bash
pdm run pre_build
pdm build
```

Sanity-check the wheel:

```bash
pip install dist/polarion_rest_client-*.whl
python -c "import polarion_rest_client; print(polarion_rest_client.__version__)"
```

#### 11. Commit and release

```bash
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](#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.).

1. Place overrides under `codegen/custom_templates/` using the **same relative paths** as the upstream templates.
2. The regeneration script automatically passes `--custom-template-path` when this directory exists.

To discover the upstream template paths:

```bash
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:

```bash
pdm run python scripts/set_version.py 25.06.08
```

---

## Tests

Tests are parameterized to run in both sync and async modes.

```bash
pdm run pytest
```

Test markers:

- `unit` — fast tests, no external services
- `integration` — tests that hit a real Polarion server

```bash
pdm run pytest -m unit
pdm run pytest -m integration
```

---

## Contributing

1. Create a feature branch:

```bash
git checkout -b feature/my-feature
```

2. Run the full pipeline before committing:

```bash
pdm run pre_build
```

3. Run tests:

```bash
pdm run pytest
```

4. Lint:

```bash
flake8 . --exclude ./venv --max-line-length=120
```

5. Commit following [Conventional Commits](https://www.conventionalcommits.org/) 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

1. Update `CHANGELOG.md`
2. Set version: `pdm run python scripts/set_version.py XX.YY.ZZ`
3. Regenerate and build:

```bash
pdm run pre_build
pdm build
```

4. Dry-run on TestPyPI:

```bash
pdm publish -r testpypi --username __token__ --password "$TESTPYPI_TOKEN"
```

5. Verify from TestPyPI:

```bash
pip install -i https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple \
    polarion-rest-client==<version>
```

6. Publish to PyPI:

```bash
pdm publish --username __token__ --password "$PYPI_TOKEN"
```

7. Tag and push:

```bash
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](https://test.pypi.org/account/register/) | Required for dry-run | `TESTPYPI_TOKEN` — Entire account (first upload) |
| [PyPI](https://pypi.org/account/register/) | 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](./LICENSE).
