Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions .github/workflows/test-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: 0.8.22
version: 0.9.18
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: test-and-publish
Expand All @@ -39,9 +39,38 @@ jobs:
env:
PDFREST_API_KEY: ${{ secrets.PDFREST_API_KEY }}

examples:
name: Examples (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
permissions:
id-token: write
contents: read
packages: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: 0.9.18
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: test-and-publish
cache-dependency-glob: uv.lock
- name: Run examples with nox
run: uvx nox --python ${{ matrix.python-version }} --session examples
env:
PDFREST_API_KEY: ${{ secrets.PDFREST_API_KEY }}

publish:
name: Publish to CodeArtifact
needs: tests
needs:
- tests
- examples
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
Expand All @@ -60,7 +89,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: 0.8.22
version: 0.9.18
enable-cache: true
cache-suffix: pre-commit
cache-dependency-glob: uv.lock
Expand All @@ -77,4 +106,4 @@ jobs:
- name: Build distribution artifacts
run: uv build --python 3.11
- name: Publish package to CodeArtifact
run: uv publish --index cit-pypi
run: uv publish --publish-url=https://datalogics-304774597385.d.codeartifact.us-east-2.amazonaws.com/pypi/cit-pypi/ --username __token__
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,21 @@
- Write pytest tests: files named `test_*.py`, test functions `test_*`, fixtures
in `conftest.py` where shared.

- Follow ruff’s SIM117 rule: when combining context managers (e.g., a client and
`pytest.RaisesGroup`), use a single `with (...)` statement instead of nesting
them to keep tests idiomatic and lint-clean.

- Cover both client transports in every new test module (unit and live suites):
add distinct test cases (not parameterized branches) that exercise each
assertion through `PdfRestClient` and `AsyncPdfRestClient` so sync/async
behaviour stays independently verifiable.

- When endpoints may raise `PdfRestErrorGroup` (or any future pdfRest-specific
exception groups), assert them with `pytest.RaisesGroup`/`pytest.RaisesExc`,
and use the `check=` hook to confirm the outer group is the expected class so
each inner error is validated individually rather than matching the group
message alone.

- Ensure high-value coverage of public functions and edge cases; document intent
in test docstrings when non-obvious.

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
Python client library for the PDFRest service. The project is managed with
[uv](https://docs.astral.sh/uv/) and targets Python 3.9 and newer.

## Running examples

```bash
uvx nox -s examples
uv run nox -s run-example -- examples/delete/delete_example.py
```

## Getting started

```bash
Expand Down
8 changes: 7 additions & 1 deletion TESTING_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,16 @@ iteration required.
- Local validation failures (`ValidationError`, `ValueError`) that should
prevent HTTP calls.
- Server/transport failures (`PdfRestApiError`, `PdfRestAuthenticationError`,
`PdfRestTimeoutError`, `PdfRestTransportError`).
`PdfRestTimeoutError`, `PdfRestTransportError`, `PdfRestErrorGroup`, etc.).
- When behaviour should short-circuit locally (bad UUIDs, empty query lists,
missing profiles), configure the transport to raise if invoked so the test
proves no HTTP request occurs.
- When endpoints intentionally raise pdfRest-specific `ExceptionGroup`
subclasses (such as `PdfRestErrorGroup` produced by delete failures), capture
them with `pytest.RaisesGroup`/`pytest.RaisesExc`, and use the `check=` hook
to assert the aggregate is the expected group class. This verifies both the
group message and each individual member (`PdfRestDeleteError`, future custom
errors) instead of relying on the aggregate text alone.

## Additional Expectations

Expand Down
20 changes: 20 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Examples

Each example script includes [PEP 723](https://peps.python.org/pep-0723/)
metadata so `uv` can create a disposable environment and install the script's
dependencies without touching the project-wide virtualenv. Run them directly
with `uv run` instead of relying on `--project` mode:

```bash
# Default (Python 3.11+)
uv run examples/delete/delete_example.py

# Version-specific overrides
uv run --python 3.10 examples/delete/python-3.10/delete_example.py
```

The commands above read `PDFREST_API_KEY` from your environment (you can manage
that via `.env` if desired), upload the checked-in sample assets under
`examples/resources/`, and exercise the async client end-to-end. Use
`uvx nox -s examples` when you want to execute every example across the
supported interpreter matrix.
52 changes: 52 additions & 0 deletions examples/delete/delete_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["pdfrest", "python-dotenv"]
# ///
"""Delete files with pdfRest's async client on Python 3.11+.

This sample shows how to:

1. Upload a local resource so we have a file id to delete.
2. Delete that file successfully.
3. Demonstrate how `PdfRestErrorGroup` behaves when we try to delete the same
file again (Python 3.11 also allows `except* PdfRestDeleteError` if you want
to tighten the example even further).

Run with `uv run --project ../.. python delete_example.py`; the script uses the
checked-in `examples/resources/report.pdf` sample so no additional setup is
required.
"""

from __future__ import annotations

import asyncio
from pathlib import Path

from dotenv import load_dotenv

from pdfrest import AsyncPdfRestClient, PdfRestDeleteError

RESOURCE = Path(__file__).resolve().parents[1] / "resources" / "report.pdf"


async def delete_with_except_star() -> None:
load_dotenv()
async with AsyncPdfRestClient() as client:
uploaded = (await client.files.create_from_paths([RESOURCE]))[0]
print(f"Uploaded {uploaded.name} with id={uploaded.id}")

await client.files.delete(uploaded)
print("First deletion succeeded.\n")

print("Attempting to delete the same file again to trigger errors...")
try:
await client.files.delete(uploaded)
except* PdfRestDeleteError as group:
for error in group.exceptions:
print(f"- Cleanup failed for {error.file_id}: {error.detail}")
else: # pragma: no cover - would require server bug
print("Second deletion unexpectedly succeeded.")


if __name__ == "__main__": # pragma: no cover - manual example
asyncio.run(delete_with_except_star())
48 changes: 48 additions & 0 deletions examples/delete/python-3.10/delete_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# /// script
# requires-python = "==3.10"
# dependencies = ["pdfrest", "exceptiongroup", "python-dotenv"]
# ///
"""Delete files with pdfRest's async client on Python 3.10.

Python 3.10 lacks the built-in `except*` syntax, so this example uses the
`exceptiongroup` backport to catch `PdfRestErrorGroup` and inspect individual
`PdfRestDeleteError` instances when cleanup fails.

Run with `uv run --project ../.. python delete_example.py`; the shared
`examples/resources/report.pdf` sample ships with the repository.
"""

from __future__ import annotations

import asyncio
from pathlib import Path

from dotenv import load_dotenv
from exceptiongroup import BaseExceptionGroup, catch

from pdfrest import AsyncPdfRestClient, PdfRestDeleteError

RESOURCE = Path(__file__).resolve().parents[2] / "resources" / "report.pdf"


def _log_delete_errors(group: BaseExceptionGroup) -> None:
for error in group.exceptions:
print(f"- Cleanup failed for {error.file_id}: {error.detail}")


async def delete_with_exceptiongroup_catch() -> None:
load_dotenv()
async with AsyncPdfRestClient() as client:
uploaded = (await client.files.create_from_paths([RESOURCE]))[0]
print(f"Uploaded {uploaded.name} with id={uploaded.id}")

await client.files.delete(uploaded)
print("First deletion succeeded.\n")

print("Attempting to delete the same file again to trigger errors...")
with catch({PdfRestDeleteError: _log_delete_errors}):
await client.files.delete(uploaded)


if __name__ == "__main__": # pragma: no cover - manual example
asyncio.run(delete_with_exceptiongroup_catch())
4 changes: 4 additions & 0 deletions examples/delete/python-3.10/ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Extend the `ruff.toml` file in the examples directory...

extend = "../../ruff.toml"
target-version = "py310"
Binary file added examples/resources/report.pdf
Binary file not shown.
4 changes: 4 additions & 0 deletions examples/ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Extend the `pyproject.toml` file in the parent directory...

extend = "../pyproject.toml"
target-version = "py311"
Loading