From e91c0dced3818d2955252a63a605f1cf1a1e83b0 Mon Sep 17 00:00:00 2001
From: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
Date: Thu, 21 Dec 2023 07:37:57 -0800
Subject: [PATCH 1/9] Adds a probably versioned documentation website built
with Material for MkDocs
---
.github/dependabot.yml | 1 -
.github/workflows/ci.yml | 43 ++++++++++++++++++-
.gitignore | 4 ++
.pre-commit-config.yaml | 2 +
CHANGELOG.md | 6 +++
README.md | 2 +-
docs/.override/main.html | 8 ++++
docs/changelog.md | 80 +++++++++++++++++++++++++++++++++++
docs/index.md | 78 ++++++++++++++++++++++++++++++++++
docs/roadmap.md | 6 +++
docs/usage.md | 91 ++++++++++++++++++++++++++++++++++++++++
mkdocs.yml | 78 ++++++++++++++++++++++++++++++++++
pyproject.toml | 23 +++++++++-
tests/utils.py | 4 +-
14 files changed, 421 insertions(+), 5 deletions(-)
create mode 100644 docs/.override/main.html
create mode 100644 docs/changelog.md
create mode 100644 docs/index.md
create mode 100644 docs/roadmap.md
create mode 100644 docs/usage.md
create mode 100644 mkdocs.yml
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 6e47ec0..5da91f4 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,7 +6,6 @@ updates:
# Enable version updates for Python
- package-ecosystem: "pip"
target-branch: "develop"
- # Look for a `Pipfile` in the `root` directory
directory: "/"
# Check for updates once a week
schedule:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aa3ceab..3b0faae 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -172,7 +172,48 @@ jobs:
path: dist/*
if-no-files-found: error
retention-days: 7
-
+ documentation:
+ name: Documentation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ needs:
+ - lint
+ steps:
+ -
+ uses: actions/checkout@v4
+ -
+ name: Set up Python 3.10
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ cache: 'pip'
+ -
+ name: Install Hatch
+ run: |
+ pip3 --quiet install --upgrade hatch
+ -
+ name: Build Documentation
+ run: |
+ hatch run docs:build
+ -
+ uses: actions/upload-artifact@v3
+ with:
+ name: documentation
+ path: site/*
+ if-no-files-found: error
+ retention-days: 7
+ -
+ name: Configure Git user
+ if: startsWith(github.ref, 'refs/tags/')
+ run: |
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
+ git config --local user.name "github-actions[bot]"
+ -
+ name: Deploy documentation
+ if: startsWith(github.ref, 'refs/tags/')
+ run: |
+ hatch run docs:deploy ${{ github.ref_name }}
create-release:
name: Release
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 79740f8..36fa998 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,7 @@ venv/
# Possible PDF generated from testing
tests/outputs/**
+
+# Documentation site
+site/
+.cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 60646ef..32c058b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,6 +11,8 @@ repos:
- id: check-json
exclude: "tsconfig.*json"
- id: check-yaml
+ args:
+ - "--unsafe"
- id: check-toml
- id: check-executables-have-shebangs
- id: end-of-file-fixer
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e37b036..496d4b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Added
+
+- Documentation site built with Github Pages and Material for MkDocs
+
## [0.4.1] - 2023-12-11
### Fixed
diff --git a/README.md b/README.md
index fd6e2c1..8ef4d16 100644
--- a/README.md
+++ b/README.md
@@ -95,7 +95,7 @@ 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:
+ with client.chromium.url_to_pdf() as route:
response = route.url("https://hello.world").orient(PageOrientation.Landscape).run()
Path("my-world.pdf").write_bytes(response.content)
```
diff --git a/docs/.override/main.html b/docs/.override/main.html
new file mode 100644
index 0000000..0af326a
--- /dev/null
+++ b/docs/.override/main.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block outdated %}
+ You're not viewing the latest version.
+
+ Click here to go to latest.
+
+{% endblock %}
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 0000000..496d4b0
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1,80 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Documentation site built with Github Pages and Material for MkDocs
+
+## [0.4.1] - 2023-12-11
+
+### Fixed
+
+- Implemented an internal workaround for older Gotenberg versions and their handling of non-latin filenames.
+ - When detected, the files will be copied into a temporary directory and the filename cleaned
+ - Gotenberg 8.0.0 will start implementing something similar once released
+- The pulled Gotenberg image is now inspected, allowing local re-creation of failures against specific digests
+- The `:edge` tag testing is now allowed to fail
+
+## [0.4.0] - 2023-12-04
+
+### Changed
+
+- Removed some certain special cases from coverage
+- Updated `pre-commit` hook versions
+- Updated how pytest is configured, so it will apply to any invocation
+- Updated test running image to log at warning or lower using text format
+- Updated test running image from 7.9.2 to 7.10.1
+- For the moment, send both `pdfa` and `pdfFormat` for compatibility with 7.9 and 7.10
+ - See [here](https://github.com/stumpylog/gotenberg-client/issues/5#issuecomment-1839081129) for some subtle differences in what these options mean
+
+### Added
+
+- Added new test job against Gotenberg's `:edge` tag
+
+## [0.3.0] - 2023-10-17
+
+### Added
+
+- Support for the output filename and request tracing for all routes
+
+### Removed
+
+- References to compression and Brotli. Gotenberg doesn't seem to ever compress response data
+
+### Fixed
+
+- An issue with the sorting of merging PDFs. Expanded testing to cover the merged ordering
+
+### Changed
+
+- Multiple merge calls on the same route will maintain the ordering of all files, rather than just per merge call
+
+## [0.2.0] - 2023-10-16
+
+### Added
+
+- CodeQL scanning via GitHub
+- Codecov.io coverage shield
+
+### Changed
+
+- Updated pypa/gh-action-pypi-publish from 1.8.8 to 1.8.10
+- Updated actions/checkout from 3 to 4
+- Mis-spelled `gotenerg_url` for a `Client` is now `host` and no longer keyword only
+
+## [0.1.0] - 2023-10-15
+
+### Added
+
+- Chromium conversion routes
+- LibreOffice conversion routes
+- PDF/A conversion route
+- PDF merge route
+- Health status route
+- Testing and typing all setup and passing
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..1407e74
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,78 @@
+# Gotenberg Python Client
+
+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.
+
+## 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 much as possible
+- Nearly full test coverage run against an actual Gotenberg server for multiple Python and PyPy versions
+
+## 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()
+```
diff --git a/docs/roadmap.md b/docs/roadmap.md
new file mode 100644
index 0000000..eafbe90
--- /dev/null
+++ b/docs/roadmap.md
@@ -0,0 +1,6 @@
+# Roadmap
+
+## Webhooks
+
+Implement adding the [webhooks](https://gotenberg.dev/docs/webhook) via headers as defined in the
+documentation.
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..accffe9
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,91 @@
+# Usage
+
+## Installation
+
+```console
+pip install gotenberg-client
+```
+
+## 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
+
+## Client
+
+First, you obtain a `GotenbergClient`. As seen below, the host
+where Gotenberg can be found is required, with optional configuration of
+global timeouts, the log level (for this library and httpx/httpcore) as
+well as control over the usage of HTTP/2.
+
+```python
+class GotenbergClient:
+
+ def __init__(
+ self,
+ host: str,
+ *,
+ timeout: float = 30.0,
+ log_level: int = logging.ERROR,
+ http2: bool = True,
+ ):
+ ....
+```
+
+The client should live as long as you will be communicating with Gotenberg as
+this allows the connection to remain open, saving some time to re-negotiate
+a connection.
+
+To ensure proper cleanup of connection, it is suggested to use the client as
+a context manager. If not using as a context manager, the user should call
+`.close()`, preferably inside a `finally` block.
+
+## Routes
+
+The library supports almost all the [routes](https://gotenberg.dev/docs/routes)
+defined by the Gotenberg API. Only the Prometheus metrics endpoint is not
+implemented.
+
+To utilize a route, you first select the module which provides it, then the
+actual operation to carry out. For example, using Chromium to convert
+HTML into a PDF would look like this:
+
+```python
+with GotenbergClient("http://localhost:3000") as client:
+ with client.chromium.html_to_pdf() as route:
+ ....
+```
+
+The exact options of each route vary, according to the Gotenberg documentation. Many routes
+share some common options, such as controlling page size or setting the PDF/A format output.
+
+Configuration of a route will always return the the route, allowing chaining of configuration,
+as seen here:
+
+```python
+from gotenberg_client import GotenbergClient
+from gotenberg_client.options import A4
+
+with GotenbergClient("http://localhost:3000") as client:
+ with client.chromium.markdown_to_pdf() as route:
+ response = (
+ route.index("main.html")
+ .markdown_file("readme.md")
+ .size(A4)
+ .resource("styles.css")
+ .fail_on_exceptions()
+ .run()
+ )
+```
+
+Once all configuration is completed, call `.run()`. This actually sends the information to
+Gotenberg with all form data as has been configured. At the moment, it returns the full
+[`httpx.Response`](https://www.python-httpx.org/api/#response), with the content of the response
+being the resulting PDF or zip file, depending on the route and configurations.
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..d346aa9
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,78 @@
+site_name: Gotenberg Client
+
+site_url: https://stumpylog.github.io/gotenberg-client/
+site_author: Trenton H
+
+repo_name: stumpylog/gotenberg-client
+repo_url: https://github.com/stumpylog/gotenberg-client
+
+nav:
+ - "index.md"
+ - "usage.md"
+ - "roadmap.md"
+ - "changelog.md"
+
+markdown_extensions:
+ - admonition
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - toc:
+ permalink: true
+
+theme:
+ name: material
+ custom_dir: docs/.override
+ features:
+ - navigation.tabs
+ - navigation.sections
+ - navigation.top
+ - toc.integrate
+ - search.suggest
+ - content.code.annotate
+ - pymdownx.superfences
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - footnotes
+ font:
+ text: Roboto
+ code: Roboto Mono
+ palette:
+ # Palette toggle for light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ # Palette toggle for dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+
+plugins:
+ - search
+ - social
+ - minify:
+ minify_html: true
+ minify_js: true
+ minify_css: true
+
+extra:
+ version:
+ provider: mike
+ analytics:
+ provider: google
+ property: G-YHHSXXB0FT
+ consent:
+ title: Cookie consent
+ description: >-
+ We use cookies to recognize your repeated visits and preferences, as well
+ as to measure the effectiveness of our documentation and whether users
+ find what they're searching for. With your consent, you're helping us to
+ make our documentation better.
diff --git a/pyproject.toml b/pyproject.toml
index 1c88a51..dd9e23d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,7 +34,7 @@ dependencies = [
]
[project.urls]
-Documentation = "https://github.com/stumpylog/gotenberg-client/#readme"
+Documentation = "https://stumpylog.github.io/gotenberg-client/"
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"
@@ -95,6 +95,27 @@ dependencies = [
check = ["pre-commit run --all-files"]
update = ["pre-commit autoupdate"]
+[tool.hatch.envs.docs]
+template = "docs"
+detached = true
+dependencies = [
+ "mkdocs-material[imaging]~=9.5.2",
+ "mike~=2.0.0",
+ "mkdocs-minify-plugin~=0.7.1"
+]
+
+[tool.hatch.envs.docs.scripts]
+new = ["mkdocs new ."]
+build = ["mkdocs build"]
+serve = [
+ "mkdocs serve"
+]
+mike-help = ["mike serve --help"]
+deploy = [
+ "mike deploy --push --branch gh-pages --remote origin --update-aliases latest {args}",
+ "mike set-default --branch gh-pages --remote origin --push latest"
+]
+
[tool.hatch.envs.lint]
detached = true
dependencies = [
diff --git a/tests/utils.py b/tests/utils.py
index 1578004..d224a05 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -71,12 +71,14 @@ def extract_text(pdf_path: Path) -> str:
Using pdftotext from poppler, extracts the text of a PDF into a file,
then reads the file contents and returns it
"""
+ pdf_to_text = shutil.which("pdftotext")
+ assert pdf_to_text is not None
with tempfile.NamedTemporaryFile(
mode="w+",
) as tmp:
subprocess.run(
[ # noqa: S603
- shutil.which("pdftotext"),
+ pdf_to_text,
"-q",
"-layout",
"-enc",
From ff2a656fe4803108b5488db215d344bab6073b16 Mon Sep 17 00:00:00 2001
From: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
Date: Thu, 21 Dec 2023 08:30:08 -0800
Subject: [PATCH 2/9] Adds new route method run_with_retry, allowing re-running
of a route if the server returns a server error
---
CHANGELOG.md | 1 +
src/gotenberg_client/_base.py | 60 +++++++++++++++++++++++++
src/gotenberg_client/_utils.py | 2 +-
tests/conftest.py | 3 +-
tests/test_convert_chromium_html.py | 11 ++---
tests/test_convert_chromium_markdown.py | 5 +--
tests/test_convert_chromium_url.py | 5 +--
tests/test_convert_libre_office.py | 25 +++++------
tests/test_convert_pdf_a.py | 7 +--
tests/test_health.py | 2 +-
tests/test_merge.py | 15 ++++---
tests/test_misc_stuff.py | 54 ++++++++++++++++++----
tests/utils.py | 46 -------------------
13 files changed, 139 insertions(+), 97 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 496d4b0..95e1f54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Documentation site built with Github Pages and Material for MkDocs
+- New method `.run_with_retry` for routes, which allows the route to be rerun as configured, with progressive backoff if the server returns a server error
## [0.4.1] - 2023-12-11
diff --git a/src/gotenberg_client/_base.py b/src/gotenberg_client/_base.py
index 2c46707..8b8f471 100644
--- a/src/gotenberg_client/_base.py
+++ b/src/gotenberg_client/_base.py
@@ -5,12 +5,15 @@
from contextlib import ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory
+from time import sleep
from types import TracebackType
from typing import Dict
from typing import Optional
from typing import Type
+from typing import Union
from httpx import Client
+from httpx import HTTPStatusError
from httpx import Response
from httpx._types import RequestFiles
@@ -21,6 +24,10 @@
logger = logging.getLogger(__name__)
+class UnreachableCodeError(Exception):
+ pass
+
+
class BaseRoute:
"""
The base implementation of a Gotenberg API route. Anything settings or
@@ -75,6 +82,59 @@ def run(self) -> Response:
resp.raise_for_status()
return resp
+ def run_with_retry(
+ self,
+ *,
+ max_retry_count: int = 5,
+ initial_retry_wait: Union[float, int] = 5.0,
+ retry_scale: Union[float, int] = 2.0,
+ ) -> Response:
+ """
+ For whatever reason, Gotenberg often returns HTTP 503 errors, even with the same files.
+ Hopefully v8 will improve upon this with its updates, but this is provided for convenience.
+
+ This function will retry the given method/function up to X times, with larger backoff
+ periods between each attempt, in hopes the issue resolves itself during
+ one attempt to parse.
+
+ This will wait the following (by default):
+ - Attempt 1 - 5s following failure
+ - Attempt 2 - 10s following failure
+ - Attempt 3 - 20s following failure
+ - Attempt 4 - 40s following failure
+ - Attempt 5 - 80s following failure
+
+ """
+ retry_time = initial_retry_wait
+ current_retry_count = 0
+
+ while current_retry_count < max_retry_count:
+ current_retry_count = current_retry_count + 1
+
+ try:
+ return self.run()
+ except HTTPStatusError as e:
+ logger.warning(f"HTTP error: {e}", stacklevel=1)
+
+ # This only handles status codes which are 5xx, indicating the server had a problem
+ # Not 4xx, with probably means a problem with the request
+ if not e.response.is_server_error:
+ raise
+
+ # Don't do the extra waiting, return right away
+ if current_retry_count >= max_retry_count:
+ raise
+
+ except Exception as e: # pragma: no cover
+ logger.warning(f"Unexpected error: {e}", stacklevel=1)
+ if current_retry_count > -max_retry_count:
+ raise
+
+ sleep(retry_time)
+ retry_time = retry_time * retry_scale
+
+ raise UnreachableCodeError # pragma: no cover
+
def _get_files(self) -> RequestFiles:
"""
Deals with opening all provided files for multi-part uploads, including
diff --git a/src/gotenberg_client/_utils.py b/src/gotenberg_client/_utils.py
index 72977aa..6f8c5fd 100644
--- a/src/gotenberg_client/_utils.py
+++ b/src/gotenberg_client/_utils.py
@@ -13,7 +13,7 @@ def optional_to_form(value: Optional[Union[bool, int, float, str]], name: str) -
Quick helper to convert an optional type into a form data field
with the given name or no changes if the value is None
"""
- if value is None:
+ if value is None: # pragma: no cover
return {}
else:
return {name: str(value).lower()}
diff --git a/tests/conftest.py b/tests/conftest.py
index e394eec..ad727a8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,13 +6,14 @@
import pytest
-from gotenberg_client._client import GotenbergClient
+from gotenberg_client import GotenbergClient
GOTENBERG_URL: Final[str] = os.getenv("GOTENBERG_URL", "http://localhost:3000")
SAMPLE_DIR: Final[Path] = Path(__file__).parent.resolve() / "samples"
SAVE_DIR: Final[Path] = Path(__file__).parent.resolve() / "outputs"
SAVE_OUTPUTS: Final[bool] = "SAVE_TEST_OUTPUT" in os.environ
+
if SAVE_OUTPUTS:
shutil.rmtree(SAVE_DIR, ignore_errors=True)
SAVE_DIR.mkdir()
diff --git a/tests/test_convert_chromium_html.py b/tests/test_convert_chromium_html.py
index 178e5f8..925e00b 100644
--- a/tests/test_convert_chromium_html.py
+++ b/tests/test_convert_chromium_html.py
@@ -14,7 +14,6 @@
from tests.conftest import SAMPLE_DIR
from tests.conftest import SAVE_DIR
from tests.conftest import SAVE_OUTPUTS
-from tests.utils import call_run_with_server_error_handling
from tests.utils import verify_stream_contains
@@ -23,7 +22,7 @@ def test_basic_convert(self, client: GotenbergClient):
test_file = SAMPLE_DIR / "basic.html"
with client.chromium.html_to_pdf() as route:
- resp = call_run_with_server_error_handling(route.index(test_file))
+ resp = route.index(test_file).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -37,7 +36,7 @@ def test_convert_with_header_footer(self, client: GotenbergClient):
footer_file = SAMPLE_DIR / "footer.html"
with client.chromium.html_to_pdf() as route:
- resp = call_run_with_server_error_handling(route.index(test_file).header(header_file).footer(footer_file))
+ resp = route.index(test_file).header(header_file).footer(footer_file).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -50,9 +49,7 @@ def test_convert_additional_files(self, client: GotenbergClient):
style = SAMPLE_DIR / "style.css"
with client.chromium.html_to_pdf() as route:
- resp = call_run_with_server_error_handling(
- route.index(test_file).resource(img).resource(font).resource(style),
- )
+ resp = route.index(test_file).resource(img).resource(font).resource(style).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -69,7 +66,7 @@ def test_convert_pdfa_1a_format(self, client: GotenbergClient, gt_format: PdfAFo
test_file = SAMPLE_DIR / "basic.html"
with client.chromium.html_to_pdf() as route:
- resp = call_run_with_server_error_handling(route.index(test_file).pdf_format(gt_format))
+ resp = route.index(test_file).pdf_format(gt_format).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
diff --git a/tests/test_convert_chromium_markdown.py b/tests/test_convert_chromium_markdown.py
index 2e78e96..fbd5465 100644
--- a/tests/test_convert_chromium_markdown.py
+++ b/tests/test_convert_chromium_markdown.py
@@ -2,7 +2,6 @@
from gotenberg_client._client import GotenbergClient
from tests.conftest import SAMPLE_DIR
-from tests.utils import call_run_with_server_error_handling
class TestConvertChromiumUrlRoute:
@@ -13,9 +12,7 @@ def test_basic_convert(self, client: GotenbergClient):
font = SAMPLE_DIR / "font.woff"
style = SAMPLE_DIR / "style.css"
with client.chromium.markdown_to_pdf() as route:
- resp = call_run_with_server_error_handling(
- route.index(index).markdown_files(md_files).resources([img, font]).resource(style),
- )
+ resp = route.index(index).markdown_files(md_files).resources([img, font]).resource(style).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
diff --git a/tests/test_convert_chromium_url.py b/tests/test_convert_chromium_url.py
index 5e4afb6..8f090ae 100644
--- a/tests/test_convert_chromium_url.py
+++ b/tests/test_convert_chromium_url.py
@@ -6,16 +6,13 @@
from gotenberg_client._client import GotenbergClient
from gotenberg_client._convert.chromium import EmulatedMediaType
-from tests.utils import call_run_with_server_error_handling
from tests.utils import verify_stream_contains
class TestConvertChromiumUrlRoute:
def test_basic_convert(self, client: GotenbergClient):
with client.chromium.url_to_pdf() as route:
- resp = call_run_with_server_error_handling(
- route.url("https://en.wikipedia.org/wiki/William_Edward_Sanders"),
- )
+ resp = route.url("https://en.wikipedia.org/wiki/William_Edward_Sanders").run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
diff --git a/tests/test_convert_libre_office.py b/tests/test_convert_libre_office.py
index 506ee69..f75bf4f 100644
--- a/tests/test_convert_libre_office.py
+++ b/tests/test_convert_libre_office.py
@@ -12,14 +12,13 @@
from tests.conftest import SAMPLE_DIR
from tests.conftest import SAVE_DIR
from tests.conftest import SAVE_OUTPUTS
-from tests.utils import call_run_with_server_error_handling
class TestLibreOfficeConvert:
def test_libre_office_convert_docx_format(self, client: GotenbergClient):
test_file = SAMPLE_DIR / "sample.docx"
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(route.convert(test_file))
+ resp = route.convert(test_file).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -31,7 +30,7 @@ def test_libre_office_convert_docx_format(self, client: GotenbergClient):
def test_libre_office_convert_odt_format(self, client: GotenbergClient):
test_file = SAMPLE_DIR / "sample.odt"
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(route.convert(test_file))
+ resp = route.convert(test_file).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -43,7 +42,7 @@ def test_libre_office_convert_odt_format(self, client: GotenbergClient):
def test_libre_office_convert_xlsx_format(self, client: GotenbergClient):
test_file = SAMPLE_DIR / "sample.xlsx"
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(route.convert(test_file))
+ resp = route.convert(test_file).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -55,7 +54,7 @@ def test_libre_office_convert_xlsx_format(self, client: GotenbergClient):
def test_libre_office_convert_ods_format(self, client: GotenbergClient):
test_file = SAMPLE_DIR / "sample.ods"
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(route.convert(test_file))
+ resp = route.convert(test_file).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -66,8 +65,8 @@ def test_libre_office_convert_ods_format(self, client: GotenbergClient):
def test_libre_office_convert_multiples_format(self, client: GotenbergClient):
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(
- route.convert_files([SAMPLE_DIR / "sample.docx", SAMPLE_DIR / "sample.odt"]).no_merge(),
+ resp = (
+ route.convert_files([SAMPLE_DIR / "sample.docx", SAMPLE_DIR / "sample.odt"]).no_merge().run_with_retry()
)
assert resp.status_code == codes.OK
@@ -79,9 +78,7 @@ def test_libre_office_convert_multiples_format(self, client: GotenbergClient):
def test_libre_office_convert_multiples_format_merged(self, client: GotenbergClient):
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(
- route.convert_files([SAMPLE_DIR / "sample.docx", SAMPLE_DIR / "sample.odt"]).merge(),
- )
+ resp = route.convert_files([SAMPLE_DIR / "sample.docx", SAMPLE_DIR / "sample.odt"]).merge().run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -94,8 +91,10 @@ def test_libre_office_convert_std_lib_mime(self, client: GotenbergClient):
with patch("gotenberg_client._utils.guess_mime_type") as mocked_guess_mime_type:
mocked_guess_mime_type.side_effect = guess_mime_type_stdlib
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(
- route.convert_files([SAMPLE_DIR / "sample.docx", SAMPLE_DIR / "sample.odt"]).no_merge(),
+ resp = (
+ route.convert_files([SAMPLE_DIR / "sample.docx", SAMPLE_DIR / "sample.odt"])
+ .no_merge()
+ .run_with_retry()
)
assert resp.status_code == codes.OK
@@ -117,7 +116,7 @@ def test_libre_office_convert_xlsx_format_pdfa(
):
test_file = SAMPLE_DIR / "sample.xlsx"
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(route.convert(test_file).pdf_format(gt_format))
+ resp = route.convert(test_file).pdf_format(gt_format).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
diff --git a/tests/test_convert_pdf_a.py b/tests/test_convert_pdf_a.py
index aed572e..ddabdf7 100644
--- a/tests/test_convert_pdf_a.py
+++ b/tests/test_convert_pdf_a.py
@@ -10,7 +10,6 @@
from tests.conftest import SAMPLE_DIR
from tests.conftest import SAVE_DIR
from tests.conftest import SAVE_OUTPUTS
-from tests.utils import call_run_with_server_error_handling
class TestPdfAConvert:
@@ -26,7 +25,7 @@ def test_pdf_a_single_file(
):
test_file = SAMPLE_DIR / "sample1.pdf"
with client.pdf_a.to_pdfa() as route:
- resp = call_run_with_server_error_handling(route.convert(test_file).pdf_format(gt_format))
+ resp = route.convert(test_file).pdf_format(gt_format).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
@@ -53,9 +52,7 @@ def test_pdf_a_multiple_file(
other_test_file = Path(temp_dir) / "sample2.pdf"
other_test_file.write_bytes(test_file.read_bytes())
with client.pdf_a.to_pdfa() as route:
- resp = call_run_with_server_error_handling(
- route.convert_files([test_file, other_test_file]).pdf_format(gt_format),
- )
+ resp = route.convert_files([test_file, other_test_file]).pdf_format(gt_format).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
diff --git a/tests/test_health.py b/tests/test_health.py
index ff58223..d4360d3 100644
--- a/tests/test_health.py
+++ b/tests/test_health.py
@@ -11,6 +11,6 @@ def test_health_endpoint(
assert status.overall == StatusOptions.Up
assert status.chromium is not None
assert status.chromium.status == StatusOptions.Up
- if "uno" in status.data:
+ if "uno" in status.data: # pragma: no cover
assert status.uno is not None
assert status.uno.status == StatusOptions.Up
diff --git a/tests/test_merge.py b/tests/test_merge.py
index 7b3187d..39871b6 100644
--- a/tests/test_merge.py
+++ b/tests/test_merge.py
@@ -11,7 +11,6 @@
from tests.conftest import SAMPLE_DIR
from tests.conftest import SAVE_DIR
from tests.conftest import SAVE_OUTPUTS
-from tests.utils import call_run_with_server_error_handling
from tests.utils import extract_text
@@ -27,12 +26,13 @@ def test_merge_files_pdf_a(
pike_format: str,
):
with client.merge.merge() as route:
- resp = call_run_with_server_error_handling(
- route.merge([SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"]).pdf_format(
+ resp = (
+ route.merge([SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"])
+ .pdf_format(
gt_format,
- ),
+ )
+ .run_with_retry()
)
-
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
assert resp.headers["Content-Type"] == "application/pdf"
@@ -56,8 +56,9 @@ def test_merge_multiple_file(
else:
with client.merge.merge() as route:
# By default, these would not merge correctly, as it happens alphabetically
- route.merge([SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"])
- resp = call_run_with_server_error_handling(route)
+ resp = route.merge(
+ [SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"],
+ ).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
diff --git a/tests/test_misc_stuff.py b/tests/test_misc_stuff.py
index b71e5dd..7e42470 100644
--- a/tests/test_misc_stuff.py
+++ b/tests/test_misc_stuff.py
@@ -3,11 +3,13 @@
import uuid
from pathlib import Path
+import pytest
+from httpx import HTTPStatusError
from httpx import codes
+from pytest_httpx import HTTPXMock
from gotenberg_client._client import GotenbergClient
from tests.conftest import SAMPLE_DIR
-from tests.utils import call_run_with_server_error_handling
class TestMiscFunctionality:
@@ -17,10 +19,12 @@ def test_trace_id_header(
):
trace_id = str(uuid.uuid4())
with client.merge.merge() as route:
- resp = call_run_with_server_error_handling(
- route.merge([SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"]).trace(
+ resp = (
+ route.merge([SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"])
+ .trace(
trace_id,
- ),
+ )
+ .run_with_retry()
)
assert resp.status_code == codes.OK
@@ -35,10 +39,12 @@ def test_output_filename(
):
filename = "my-cool-file"
with client.merge.merge() as route:
- resp = call_run_with_server_error_handling(
- route.merge([SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"]).output_name(
+ resp = (
+ route.merge([SAMPLE_DIR / "z_first_merge.pdf", SAMPLE_DIR / "a_merge_second.pdf"])
+ .output_name(
filename,
- ),
+ )
+ .run_with_retry()
)
assert resp.status_code == codes.OK
@@ -62,8 +68,40 @@ def test_libre_office_convert_cyrillic(self, client: GotenbergClient):
)
with client.libre_office.to_pdf() as route:
- resp = call_run_with_server_error_handling(route.convert(copy))
+ resp = route.convert(copy).run_with_retry()
assert resp.status_code == codes.OK
assert "Content-Type" in resp.headers
assert resp.headers["Content-Type"] == "application/pdf"
+
+
+class TestServerErrorRetry:
+ def test_server_error_retry(self, client: GotenbergClient, httpx_mock: HTTPXMock):
+ # Response 1
+ httpx_mock.add_response(method="POST", status_code=codes.INTERNAL_SERVER_ERROR)
+ # Response 2
+ httpx_mock.add_response(method="POST", status_code=codes.SERVICE_UNAVAILABLE)
+ # Response 3
+ httpx_mock.add_response(method="POST", status_code=codes.GATEWAY_TIMEOUT)
+ # Response 4
+ httpx_mock.add_response(method="POST", status_code=codes.BAD_GATEWAY)
+ # Response 5
+ httpx_mock.add_response(method="POST", status_code=codes.SERVICE_UNAVAILABLE)
+
+ test_file = SAMPLE_DIR / "basic.html"
+
+ with client.chromium.html_to_pdf() as route:
+ with pytest.raises(HTTPStatusError) as exc_info:
+ _ = route.index(test_file).run_with_retry(initial_retry_wait=0.1, retry_scale=0.1)
+ assert exc_info.value.response.status_code == codes.SERVICE_UNAVAILABLE
+
+ def test_not_a_server_error(self, client: GotenbergClient, httpx_mock: HTTPXMock):
+ # Response 1
+ httpx_mock.add_response(method="POST", status_code=codes.NOT_FOUND)
+
+ test_file = SAMPLE_DIR / "basic.html"
+
+ with client.chromium.html_to_pdf() as route:
+ with pytest.raises(HTTPStatusError) as exc_info:
+ _ = route.index(test_file).run_with_retry(initial_retry_wait=0.1, retry_scale=0.1)
+ assert exc_info.value.response.status_code == codes.NOT_FOUND
diff --git a/tests/utils.py b/tests/utils.py
index d224a05..cf3805b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,18 +1,12 @@
import shutil
import subprocess
import tempfile
-import time
-import warnings
from pathlib import Path
-from httpx import HTTPStatusError
-from httpx import Response
from httpx._multipart import DataField
from httpx._multipart import FileField
from httpx._multipart import MultipartStream
-from gotenberg_client._base import BaseRoute
-
def verify_stream_contains(key: str, value: str, stream: MultipartStream):
for item in stream.fields:
@@ -26,46 +20,6 @@ def verify_stream_contains(key: str, value: str, stream: MultipartStream):
raise AssertionError(msg)
-def call_run_with_server_error_handling(route: BaseRoute) -> Response:
- """
- For whatever reason, the images started during the test pipeline like to
- segfault sometimes, crash and otherwise fail randomly, when run with the
- exact files that usually pass.
-
- So, this function will retry the given method/function up to 3 times, with larger backoff
- periods between each attempt, in hopes the issue resolves itself during
- one attempt to parse.
-
- This will wait the following:
- - Attempt 1 - 5s following failure
- - Attempt 2 - 10s following failure
- - Attempt 3 - 20s following failure
- - Attempt 4 - 40s following failure
- - Attempt 5 - 80s following failure
-
- """
- result = None
- succeeded = False
- retry_time = 5.0
- retry_count = 0
- max_retry_count = 5
-
- while retry_count < max_retry_count and not succeeded:
- try:
- return route.run()
- except HTTPStatusError as e: # pragma: no cover
- warnings.warn(f"HTTP error: {e}", stacklevel=1)
- except Exception as e: # pragma: no cover
- warnings.warn(f"Unexpected error: {e}", stacklevel=1)
-
- retry_count = retry_count + 1
-
- time.sleep(retry_time)
- retry_time = retry_time * 2.0
-
- return result
-
-
def extract_text(pdf_path: Path) -> str:
"""
Using pdftotext from poppler, extracts the text of a PDF into a file,
From 6ef285ddf9bee574eab1283a71fc8e67599ff27b Mon Sep 17 00:00:00 2001
From: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
Date: Thu, 21 Dec 2023 08:37:06 -0800
Subject: [PATCH 3/9] Links the documentation job into the workflow
requirements
---
.github/workflows/ci.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3b0faae..930936b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -223,6 +223,7 @@ jobs:
needs:
- build
- test
+ - documentation
steps:
-
uses: actions/checkout@v4
@@ -259,6 +260,7 @@ jobs:
needs:
- build
- test
+ - documentation
steps:
-
uses: actions/download-artifact@v3
From 6026dc24971003211072f97e4700d535926adb2c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Jan 2024 11:39:18 +0000
Subject: [PATCH 4/9] Bump github/codeql-action from 2 to 3
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)
---
updated-dependencies:
- dependency-name: github/codeql-action
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/codeql.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 2a8e2c1..21bc942 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v3
# âšī¸ Command-line programs to run using the OS shell.
# đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
From ebd85b321dcb10c5dedf5552c9ec243a580f46fe Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Jan 2024 11:39:25 +0000
Subject: [PATCH 5/9] Bump actions/setup-python from 4 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)
---
updated-dependencies:
- dependency-name: actions/setup-python
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 930936b..bb41d90 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4
-
name: Set up Python 3.10
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
@@ -72,7 +72,7 @@ jobs:
sudo apt-get install --yes --no-install-recommends poppler-utils
-
name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -122,7 +122,7 @@ jobs:
sudo apt-get install --yes --no-install-recommends poppler-utils
-
name: Set up Python 3.11
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: 'pip'
@@ -153,7 +153,7 @@ jobs:
uses: actions/checkout@v4
-
name: Set up Python 3.10
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
@@ -184,7 +184,7 @@ jobs:
uses: actions/checkout@v4
-
name: Set up Python 3.10
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
From e3f735aaf336af74a172bd7e8ec48244bc5db07f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Jan 2024 11:39:30 +0000
Subject: [PATCH 6/9] Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/upload-artifact
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bb41d90..ea0e40c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -166,7 +166,7 @@ jobs:
run: |
hatch build --clean
-
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: artifacts
path: dist/*
@@ -197,7 +197,7 @@ jobs:
run: |
hatch run docs:build
-
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: documentation
path: site/*
From 996c6502d568b2564ed3d6665e2eed90f952ebce Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 1 Jan 2024 11:39:36 +0000
Subject: [PATCH 7/9] Bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/download-artifact
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ea0e40c..dc65120 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -228,7 +228,7 @@ jobs:
-
uses: actions/checkout@v4
-
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: artifacts
path: dist
@@ -263,7 +263,7 @@ jobs:
- documentation
steps:
-
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: artifacts
path: dist
From 225026afee671a97c0715f818e45c1a1bcff3d3a Mon Sep 17 00:00:00 2001
From: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
Date: Thu, 11 Jan 2024 08:47:40 -0800
Subject: [PATCH 8/9] Squashed commit of the following:
commit 8aa15dbde03aaf79bfaebf0f497c5a9e01f9f26d
Author: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
Date: Thu Jan 11 08:42:01 2024 -0800
Adds support for the Gotenberg webhooks
---
.pre-commit-config.yaml | 6 +--
CHANGELOG.md | 5 +++
src/gotenberg_client/_client.py | 35 +++++++++++++++-
src/gotenberg_client/options.py | 4 ++
tests/test_misc_stuff.py | 71 +++++++++++++++++++++++++++++++++
5 files changed, 117 insertions(+), 4 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 32c058b..f810274 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -39,10 +39,10 @@ repos:
exclude: "(^Pipfile\\.lock$)"
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.1.6'
+ rev: 'v0.1.11'
hooks:
- id: ruff
- - repo: https://github.com/psf/black
- rev: 23.11.0
+ - repo: https://github.com/psf/black-pre-commit-mirror
+ rev: 23.12.1
hooks:
- id: black
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95e1f54..49a4ab2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Documentation site built with Github Pages and Material for MkDocs
- New method `.run_with_retry` for routes, which allows the route to be rerun as configured, with progressive backoff if the server returns a server error
+- Support for Gotenberg [Webhooks](https://gotenberg.dev/docs/webhook)
+
+### Deprecated
+
+- Support for Gotenberg 7.x. This will likely be the last release to support 7.x, as the options for PDF/A have been changed
## [0.4.1] - 2023-12-11
diff --git a/src/gotenberg_client/_client.py b/src/gotenberg_client/_client.py
index 9677d20..9e6195d 100644
--- a/src/gotenberg_client/_client.py
+++ b/src/gotenberg_client/_client.py
@@ -15,6 +15,7 @@
from gotenberg_client._health import HealthCheckApi
from gotenberg_client._merge import MergeApi
from gotenberg_client._typing_compat import Self
+from gotenberg_client.options import HttpMethods
class GotenbergClient:
@@ -44,12 +45,44 @@ def __init__(
self.merge = MergeApi(self._client)
self.health = HealthCheckApi(self._client)
- def add_headers(self, header: Dict[str, str]) -> None: # pragma: no cover
+ def add_headers(self, header: Dict[str, str]) -> None:
"""
Updates the httpx Client headers with the given values
"""
self._client.headers.update(header)
+ def add_webhook_url(self, url: str) -> None:
+ """
+ Adds the webhook URL to the headers
+ """
+ self.add_headers({"Gotenberg-Webhook-Url": url})
+
+ def add_error_webhook_url(self, url: str) -> None:
+ """
+ Adds the webhook error URL to the headers
+ """
+ self.add_headers({"Gotenberg-Webhook-Error-Url": url})
+
+ def set_webhook_http_method(self, method: HttpMethods = "PUT") -> None:
+ """
+ Sets the HTTP method Gotenberg will use to call the hooks
+ """
+ self.add_headers({"Gotenberg-Webhook-Method": method})
+
+ def set_error_webhook_http_method(self, method: HttpMethods = "PUT") -> None:
+ """
+ Sets the HTTP method Gotenberg will use to call the hooks
+ """
+ self.add_headers({"Gotenberg-Webhook-Error-Method": method})
+
+ def set_webhook_extra_headers(self, extra_headers: Dict[str, str]) -> None:
+ """
+ Sets the HTTP method Gotenberg will use to call the hooks
+ """
+ from json import dumps
+
+ self.add_headers({"Gotenberg-Webhook-Extra-Http-Headers": dumps(extra_headers)})
+
def __enter__(self) -> Self:
return self
diff --git a/src/gotenberg_client/options.py b/src/gotenberg_client/options.py
index d2ef4ae..b6404c6 100644
--- a/src/gotenberg_client/options.py
+++ b/src/gotenberg_client/options.py
@@ -5,6 +5,7 @@
import enum
from typing import Dict
from typing import Final
+from typing import Literal
from typing import Optional
from typing import Union
@@ -102,3 +103,6 @@ def to_form(self) -> Dict[str, str]:
return {"emulatedMediaType": "screen"}
else: # pragma: no cover
raise NotImplementedError(self.value)
+
+
+HttpMethods = Literal["POST", "PATCH", "PUT"]
diff --git a/tests/test_misc_stuff.py b/tests/test_misc_stuff.py
index 7e42470..799d1c8 100644
--- a/tests/test_misc_stuff.py
+++ b/tests/test_misc_stuff.py
@@ -1,10 +1,13 @@
import shutil
import tempfile
import uuid
+from json import dumps
+from json import loads
from pathlib import Path
import pytest
from httpx import HTTPStatusError
+from httpx import Request
from httpx import codes
from pytest_httpx import HTTPXMock
@@ -105,3 +108,71 @@ def test_not_a_server_error(self, client: GotenbergClient, httpx_mock: HTTPXMock
with pytest.raises(HTTPStatusError) as exc_info:
_ = route.index(test_file).run_with_retry(initial_retry_wait=0.1, retry_scale=0.1)
assert exc_info.value.response.status_code == codes.NOT_FOUND
+
+
+class TestWebhookHeaders:
+ def test_webhook_basic_headers(self, client: GotenbergClient, httpx_mock: HTTPXMock):
+ httpx_mock.add_response(method="POST", status_code=codes.OK)
+
+ client.add_webhook_url("http://myapi:3000/on-success")
+ client.add_error_webhook_url("http://myapi:3000/on-error")
+
+ test_file = SAMPLE_DIR / "basic.html"
+ with client.chromium.html_to_pdf() as route:
+ _ = route.index(test_file).run_with_retry()
+
+ requests = httpx_mock.get_requests()
+
+ assert len(requests) == 1
+
+ request: Request = requests[0]
+
+ assert "Gotenberg-Webhook-Url" in request.headers
+ assert request.headers["Gotenberg-Webhook-Url"] == "http://myapi:3000/on-success"
+ assert "Gotenberg-Webhook-Error-Url" in request.headers
+ assert request.headers["Gotenberg-Webhook-Error-Url"] == "http://myapi:3000/on-error"
+
+ def test_webhook_http_methods(self, client: GotenbergClient, httpx_mock: HTTPXMock):
+ httpx_mock.add_response(method="POST", status_code=codes.OK)
+
+ client.add_webhook_url("http://myapi:3000/on-success")
+ client.set_webhook_http_method("POST")
+ client.add_error_webhook_url("http://myapi:3000/on-error")
+ client.set_error_webhook_http_method("GET")
+
+ test_file = SAMPLE_DIR / "basic.html"
+ with client.chromium.html_to_pdf() as route:
+ _ = route.index(test_file).run_with_retry()
+
+ requests = httpx_mock.get_requests()
+
+ assert len(requests) == 1
+
+ request: Request = requests[0]
+
+ assert "Gotenberg-Webhook-Method" in request.headers
+ assert request.headers["Gotenberg-Webhook-Method"] == "POST"
+ assert "Gotenberg-Webhook-Error-Method" in request.headers
+ assert request.headers["Gotenberg-Webhook-Error-Method"] == "GET"
+
+ def test_webhook_extra_headers(self, client: GotenbergClient, httpx_mock: HTTPXMock):
+ httpx_mock.add_response(method="POST", status_code=codes.OK)
+
+ headers = {"Token": "mytokenvalue"}
+ headers_str = dumps(headers)
+
+ client.set_webhook_extra_headers(headers)
+
+ test_file = SAMPLE_DIR / "basic.html"
+ with client.chromium.html_to_pdf() as route:
+ _ = route.index(test_file).run_with_retry()
+
+ requests = httpx_mock.get_requests()
+
+ assert len(requests) == 1
+
+ request: Request = requests[0]
+
+ assert "Gotenberg-Webhook-Extra-Http-Headers" in request.headers
+ assert request.headers["Gotenberg-Webhook-Extra-Http-Headers"] == headers_str
+ assert loads(request.headers["Gotenberg-Webhook-Extra-Http-Headers"]) == headers
From 65c7e58ec9e485d208d2d61a084b944ecdbe2236 Mon Sep 17 00:00:00 2001
From: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
Date: Thu, 11 Jan 2024 08:49:34 -0800
Subject: [PATCH 9/9] Bumps version to 0.5.0
---
CHANGELOG.md | 2 +-
src/gotenberg_client/__about__.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49a4ab2..ac8834c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased]
+## [0.5.0] - 2024-01-11
### Added
diff --git a/src/gotenberg_client/__about__.py b/src/gotenberg_client/__about__.py
index 87873dc..3ea9ac5 100644
--- a/src/gotenberg_client/__about__.py
+++ b/src/gotenberg_client/__about__.py
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-present Trenton H
#
# SPDX-License-Identifier: MPL-2.0
-__version__ = "0.4.1"
+__version__ = "0.5.0"