Skip to content

Commit

Permalink
Initial development work on a Python client for the Gotenberg API, wi…
Browse files Browse the repository at this point in the history
…th most routes implemented
  • Loading branch information
stumpylog committed Oct 16, 2023
1 parent 2615219 commit 25b4b43
Show file tree
Hide file tree
Showing 46 changed files with 1,834 additions and 40 deletions.
12 changes: 7 additions & 5 deletions .github/workflows/test.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name: test
name: ci

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
branches:
- main
- develop

concurrency:
group: test-${{ github.head_ref }}
group: test-${{ github.ref_name }}
cancel-in-progress: true

env:
Expand Down Expand Up @@ -50,7 +51,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.8', 'pypy3.9']
# No pikepdf wheels for pypy3.8
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.9']

steps:
-
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
coverage.json

# Translations
*.mo
Expand Down Expand Up @@ -158,3 +159,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

tests/outputs/**
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- TBD
- Chromium conversion routes
- LibreOffice conversion routes
- PDF/A conversion route
- PDF merge route
- Health status route
- Testing and typing all setup and passing
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# Gotenberg API Client

[![PyPI - Version](https://img.shields.io/pypi/v/-.svg)](https://pypi.org/project/gotenberg-client)
[![PyPI - Version](https://img.shields.io/pypi/v/gotenberg-client.svg)](https://pypi.org/project/gotenberg-client)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/gotenberg-client.svg)](https://pypi.org/project/gotenberg-client)

---

## Table of Contents

- [Installation](#installation)
- [What](#what)
- [Why](#why)
- [Features](#features)
- [How](#how)
- [Examples](#examples)
- [License](#license)

## Installation
Expand All @@ -16,6 +21,101 @@
pip install gotenberg-client
```

## What

This is a Python client for interfacing with [Gotenberg](https://gotenberg.dev/), which in turn is a wrapper around
powerful tools for PDF generation and creation in various ways, using a stateless API. It's a very powerful tool
to generate and manipulate PDFs.

## Why

As far as I can tell, no active Python library exists to interface with the Gotenberg API.

### Features

- HTTP/2 enabled by default
- Abstract away the handling of multi-part/form-data and deal with `Path`s instead
- Based on the modern [httpx](https://github.com/encode/httpx) library
- Full support for type hinting and concrete return types as mush as possible
- Nearly full test coverage run against an actual Gotenberg server for multiple Python and PyPy versions

## How

All the routes and options from the Gotenberg routes are implemented, with the exception of the Prometheus metrics
endpoint. All the routes use the same format and general idea.

1. First, you add the file or files you want to process
1. Then, configure the endpoint with its various options the route supports
1. Finally, run the route and receive your resulting file

- Files will be PDF or ZIP, depending on what endpoint and its configuration. Endpoints which handle
multiple files, but don't merge them, return a ZIP archive of the resulting PDFs

### Examples

Converting a single HTML file into a PDF:

```python
from gotenberg_client import GotenbergClient

with GotenbergClient("http://localhost:3000") as client:
with client.chromium.html_to_pdf() as route:
response = route.index("my-index.html").run()
Path("my-index.pdf").write_bytes(response.content)
```

Converting an HTML file with additional resources into a PDF:

```python
from gotenberg_client import GotenbergClient

with GotenbergClient("http://localhost:3000") as client:
with client.chromium.html_to_pdf() as route:
response = route.index("my-index.html").resource("image.png").resource("style.css").run()
Path("my-index.pdf").write_bytes(response.content)
```

Converting an HTML file with additional resources into a PDF/A1a format:

```python
from gotenberg_client import GotenbergClient
from gotenberg_client.options import PdfAFormat

with GotenbergClient("http://localhost:3000") as client:
with client.chromium.html_to_pdf() as route:
response = route.index("my-index.html").resources(["image.png", "style.css"]).pdf_format(PdfAFormat.A1a).run()
Path("my-index.pdf").write_bytes(response.content)
```

Converting a URL into PDF, in landscape format

```python
from gotenberg_client import GotenbergClient
from gotenberg_client.options import PageOrientation

with GotenbergClient("http://localhost:3000") as client:
with client.chromium.html_to_pdf() as route:
response = route.url("https://hello.world").orient(PageOrientation.Landscape).run()
Path("my-world.pdf").write_bytes(response.content)
```

To ensure the proper clean up of all used resources, both the client and the route(s) should be
used as context manager. If for some reason you cannot, you should `.close` the client and any
routes:

```python
from gotenberg_client import GotenbergClient

try:
client = GotenbergClient("http://localhost:3000")
try:
route = client.merge(["myfile.pdf", "otherfile.pdf"]).run()
finally:
route.close()
finally:
client.close()
```

## License

`gotenberg-client` is distributed under the terms of the [MPL 2.0](https://spdx.org/licenses/MPL-2.0.html) license.
79 changes: 49 additions & 30 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ requires-python = ">=3.8"
license = "MPL-2.0"
keywords = []
authors = [
{ name = "Trenton H", email = "[email protected].com" },
{ name = "Trenton H", email = "rda0128ou@mozmail.com" },
]
classifiers = [
"Development Status :: 4 - Beta",
Expand All @@ -23,19 +23,27 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = ["httpx[http2] ~= 0.24"]
dependencies = [
"httpx[http2] ~= 0.24",
"typing-extensions; python_version < '3.11'"
]

[project.urls]
Documentation = "https://github.com/stumpylog/gotenberg-client/#readme"
Issues = "https://github.com/stumpylog/gotenberg-client/issues"
Source = "https://github.com/stumpylog/gotenberg-client/"
Changelog = "https://github.com/stumpylog/gotenberg-client/blob/main/CHANGELOG.md"

[project.optional-dependencies]
compression = ["httpx[http2,brotli] ~= 0.24"]
magic = ["python-magic"]

[tool.hatch.version]
path = "src/_/__about__.py"
path = "src/gotenberg_client/__about__.py"

[tool.hatch.build.targets.sdist]
exclude = [
Expand All @@ -45,19 +53,20 @@ exclude = [

[tool.hatch.envs.default]
dependencies = [
"coverage[toml] >= 7.0",
"pytest >= 7.0",
"coverage[toml] >= 7.3",
"pytest >= 7.4",
"pytest-sugar",
"pytest-cov",
"pytest-xdist",
"pytest-httpx ~= 0.26; python_version >= '3.9'",
"pytest-httpx ~= 0.22; python_version < '3.9'",
"pikepdf",
"python-magic",
"brotli",
]

[tool.hatch.envs.default.scripts]
version = "python3 --version"
test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
test = "pytest --pythonwarnings=all {args:tests}"
test-cov = "coverage run -m pytest --pythonwarnings=all {args:tests}"
cov-clear = "coverage erase"
cov-report = [
"- coverage combine",
Expand All @@ -70,12 +79,13 @@ cov = [
"cov-clear",
"test-cov",
"cov-report",
"cov-json"
"cov-json",
"cov-html"
]
pip-list = "pip list"

[[tool.hatch.envs.all.matrix]]
python = ["3.8", "3.9", "3.10", "3.11"]
python = ["3.8", "3.9", "3.10", "3.11", "3.12"]

[tool.hatch.envs.pre-commit]
dependencies = [
Expand All @@ -86,25 +96,27 @@ dependencies = [
check = ["pre-commit run --all-files"]
update = ["pre-commit autoupdate"]


[tool.hatch.envs.lint]
detached = true
dependencies = [
"black>=23.1.0",
"black>=23.9.1",
"mypy>=1.0.0",
"ruff>=0.0.243",
"ruff>=0.0.292",
"httpx",
]

[tool.hatch.envs.lint.scripts]
typing = "mypy --install-types --non-interactive {args:src/_ tests}"
typing = [
"mypy --version",
"mypy --install-types --non-interactive {args:src/gotenberg_client}"
]
style = [
"ruff {args:.}",
"black --check --diff {args:.}",
]
fmt = [
"black {args:.}",
"ruff --fix {args:.}",
"ruff {args:.}",
"style",
]
all = [
Expand All @@ -113,12 +125,14 @@ all = [
]

[tool.black]
target-version = ["py37"]
target-version = ["py38"]
line-length = 120
skip-string-normalization = true

[tool.ruff]
target-version = "py37"
fix = true
output-format = "grouped"
target-version = "py38"
line-length = 120
extend-select = [
"A",
Expand Down Expand Up @@ -165,14 +179,10 @@ ignore = [
# Ignore complexity
"C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915",
]
unfixable = [
# Don't touch unused imports
"F401",
]

[tool.ruff.isort]
force-single-line = true
known-first-party = ["gotenberg-client"]
known-first-party = ["gotenberg_client"]

[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "all"
Expand All @@ -182,28 +192,37 @@ ban-relative-imports = "all"
"tests/**/*" = ["PLR2004", "S101", "TID252"]

[tool.coverage.run]
source_pkgs = ["gotenberg-client", "tests"]
source_pkgs = ["gotenberg_client", "tests"]
branch = true
parallel = true
omit = [
"src/gotenberg-client/__about__.py",
"src/gotenberg_client/__about__.py",
"tests/conftest.py",
"tests/utils.py",
]

[tool.coverage.paths]
_ = ["src/gotenberg-client", "*/gotenberg-client/src/gotenberg-client"]
tests = ["tests", "*/gotenberg-client/tests"]
gotenberg_client = ["src/gotenberg_client", "*/gotenberg_client/src/gotenberg_client"]
tests = ["tests", "*/gotenberg_client/tests"]

[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"if SAVE_OUTPUTS:",
]

[tool.mypy]
#disallow_any_expr = true
#disallow_untyped_defs = true
#disallow_incomplete_defs = true
exclude = [
"tests/test_convert_chromium_html.py",
"tests/test_convert_chromium_url.py",
"tests/test_convert_chromium_markdown.py",
"tests/conftest.py",
]
disallow_any_expr = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
strict_optional = true

Expand Down
2 changes: 1 addition & 1 deletion src/gotenberg_client/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-present Trenton H <[email protected].com>
# SPDX-FileCopyrightText: 2023-present Trenton H <rda0128ou@mozmail.com>
#
# SPDX-License-Identifier: MPL-2.0
__version__ = "0.0.1"
5 changes: 4 additions & 1 deletion src/gotenberg_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: 2023-present Trenton H <[email protected].com>
# SPDX-FileCopyrightText: 2023-present Trenton H <rda0128ou@mozmail.com>
#
# SPDX-License-Identifier: MPL-2.0
from gotenberg_client._client import GotenbergClient

__all__ = ["GotenbergClient"]
Loading

0 comments on commit 25b4b43

Please sign in to comment.