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
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"

- name: Lint
run: ruff check .

- name: Typecheck
run: mypy src tests/typecheck_usage.py

- name: Test
run: pytest

- name: Build package
run: python -m build

- name: Check package metadata
run: twine check dist/*

- name: Smoke test package
run: python scripts/test_pack.py
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Official Python SDK for the PDFBolt API.

PDFBolt generates PDFs from HTTPS URLs, raw HTML, and published templates. See the [PDFBolt docs](https://pdfbolt.com/docs) and [OpenAPI reference](https://pdfbolt.com/docs/api-reference) for the full REST API. The SDK is typed, uses `requests`, and is intended for server-side Python applications.
PDFBolt generates PDFs from HTTPS URLs, raw HTML, and published templates. See the [PDFBolt docs](https://pdfbolt.com/docs) and [OpenAPI reference](https://pdfbolt.com/docs/api-reference) for the full REST API. The SDK is typed, uses `requests`, and is intended for server-side Python applications. Typed request dictionaries and keyword options are exported for type-aware editors and static checkers.

## Installation

Expand Down Expand Up @@ -359,17 +359,18 @@ PDFBoltValidationError
PDFBoltConfigurationError
```

Typed model exports include direct, sync, async job, webhook event, usage, and rate-limit result classes.
Typed request exports include conversion options, direct/sync/async request dictionaries, webhook event types, cookies, margins, dimensions, and other REST API parameter types. Typed model exports include direct, sync, async job, webhook event, usage, and rate-limit result classes.

## Development

```bash
python -m pip install -e ".[dev]"
ruff check .
mypy src
mypy src tests/typecheck_usage.py
pytest
python -m build
twine check dist/*
python scripts/test_pack.py
```

## Examples
Expand Down
1 change: 1 addition & 0 deletions examples/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Example modules used by SDK tests and local demos."""
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Typing :: Typed"
]
dependencies = [
Expand Down Expand Up @@ -55,6 +56,7 @@ select = ["E", "F", "I", "UP", "B"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]

[tool.mypy]
python_version = "3.11"
Expand Down
121 changes: 121 additions & 0 deletions scripts/test_pack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from __future__ import annotations

import glob
import os
import subprocess
import sys
import tarfile
import tempfile
import tomllib
import zipfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DIST = ROOT / "dist"
VERSION = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["version"]
REQUIRED_WHEEL_FILES = {
"pdfbolt/__init__.py",
"pdfbolt/types.py",
"pdfbolt/py.typed",
f"pdfbolt-{VERSION}.dist-info/METADATA",
f"pdfbolt-{VERSION}.dist-info/WHEEL",
f"pdfbolt-{VERSION}.dist-info/RECORD",
}
FORBIDDEN_WHEEL_PREFIXES = ("tests/", "examples/", "scripts/")
FORBIDDEN_SDIST_PREFIXES = (".git/", ".venv/", "build/", "dist/")


def main() -> None:
wheel = exactly_one(DIST / "*.whl")
sdist = exactly_one(DIST / "*.tar.gz")

check_wheel_contents(wheel)
check_sdist_contents(sdist)
check_installed_wheel(wheel)

print(f"Packed package {wheel.name} passed wheel, sdist, and import smoke checks.")


def exactly_one(pattern: Path) -> Path:
matches = [Path(match) for match in glob.glob(str(pattern))]
if len(matches) != 1:
raise SystemExit(f"Expected exactly one match for {pattern}, found {len(matches)}.")
return matches[0]


def check_wheel_contents(wheel: Path) -> None:
with zipfile.ZipFile(wheel) as archive:
names = set(archive.namelist())

missing = sorted(REQUIRED_WHEEL_FILES - names)
if missing:
raise SystemExit(f"Wheel is missing required files: {', '.join(missing)}")

forbidden = [
name for name in names if name == ".env" or name.startswith(FORBIDDEN_WHEEL_PREFIXES)
]
if forbidden:
raise SystemExit(f"Wheel should not include: {', '.join(sorted(forbidden))}")


def check_sdist_contents(sdist: Path) -> None:
with tarfile.open(sdist) as archive:
names = {strip_sdist_root(member.name) for member in archive.getmembers()}

required = {
"README.md",
"LICENSE",
"pyproject.toml",
"src/pdfbolt/__init__.py",
"src/pdfbolt/types.py",
"src/pdfbolt/py.typed",
"tests/test_client.py",
"tests/typecheck_usage.py",
}
missing = sorted(required - names)
if missing:
raise SystemExit(f"Sdist is missing required files: {', '.join(missing)}")

forbidden = [name for name in names if name.startswith(FORBIDDEN_SDIST_PREFIXES)]
if forbidden:
raise SystemExit(f"Sdist should not include: {', '.join(sorted(forbidden))}")


def check_installed_wheel(wheel: Path) -> None:
with tempfile.TemporaryDirectory(prefix="pdfbolt-python-pack-") as temp_dir:
target = Path(temp_dir) / "site"
subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"--quiet",
"--target",
str(target),
str(wheel),
],
check=True,
)

code = (
"import pdfbolt; "
"required = ['PDFBolt', 'DirectConversionResult', 'VERSION', "
"'DirectConvertParams', 'SyncConvertParams', 'AsyncConvertParams']; "
"missing = [name for name in required if not hasattr(pdfbolt, name)]; "
"assert not missing, f'missing exports: {missing}'"
)
subprocess.run(
[sys.executable, "-c", code],
check=True,
env={**os.environ, "PYTHONPATH": str(target)},
)


def strip_sdist_root(name: str) -> str:
parts = name.split("/", 1)
return parts[1] if len(parts) == 2 else name


if __name__ == "__main__":
main()
70 changes: 70 additions & 0 deletions src/pdfbolt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,96 @@
SyncConversionResult,
UsageSummary,
)
from .types import (
AsyncConversionWebhookStatus,
AsyncConvertParams,
AsyncHtmlParams,
AsyncOptions,
AsyncTemplateParams,
AsyncUrlParams,
CompressionLevel,
ContentDisposition,
ConversionErrorCode,
ConversionOptions,
DirectConvertParams,
DirectHtmlParams,
DirectOptions,
DirectTemplateParams,
DirectUrlParams,
DomainCookie,
EmulateMediaType,
HttpCredentials,
Margin,
MarginDimension,
PageDimension,
PaperFormat,
PDFBoltCookie,
PrintProduction,
SyncConversionStatus,
SyncConvertParams,
SyncHtmlParams,
SyncOptions,
SyncTemplateParams,
SyncUrlParams,
UrlCookie,
ViewportSize,
WaitForSelector,
WaitUntil,
)
from .webhooks import Webhooks, webhooks

__all__ = [
"AsyncConvertParams",
"AsyncConversionJob",
"AsyncConversionWebhookEvent",
"AsyncConversionWebhookStatus",
"AsyncHtmlParams",
"AsyncOptions",
"AsyncTemplateParams",
"AsyncUrlParams",
"CompressionLevel",
"ContentDisposition",
"ConversionErrorCode",
"ConversionOptions",
"DirectConvertParams",
"DirectConversionResult",
"DirectHtmlParams",
"DirectOptions",
"DirectTemplateParams",
"DirectUrlParams",
"DomainCookie",
"EmulateMediaType",
"HttpCredentials",
"Margin",
"MarginDimension",
"OneTimeCredits",
"PDFBolt",
"PDFBoltAPIError",
"PDFBoltCookie",
"PDFBoltConfigurationError",
"PDFBoltError",
"PDFBoltNetworkError",
"PDFBoltValidationError",
"PDFBoltWebhookSignatureError",
"PageDimension",
"PaperFormat",
"PrintProduction",
"RateLimitInfo",
"RateLimitWindow",
"RecurringCredits",
"SyncConvertParams",
"SyncConversionResult",
"SyncConversionStatus",
"SyncHtmlParams",
"SyncOptions",
"SyncTemplateParams",
"SyncUrlParams",
"UrlCookie",
"UsageSummary",
"VERSION",
"ViewportSize",
"Webhooks",
"WaitForSelector",
"WaitUntil",
"webhooks",
]
Loading
Loading