pdfbridge-python 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdfbridge_python-1.0.0/.coverage +0 -0
- pdfbridge_python-1.0.0/PKG-INFO +133 -0
- pdfbridge_python-1.0.0/README.md +112 -0
- pdfbridge_python-1.0.0/pyproject.toml +42 -0
- pdfbridge_python-1.0.0/src/pdfbridge/__init__.py +20 -0
- pdfbridge_python-1.0.0/src/pdfbridge/client.py +125 -0
- pdfbridge_python-1.0.0/src/pdfbridge/models.py +76 -0
- pdfbridge_python-1.0.0/tests/__init__.py +0 -0
- pdfbridge_python-1.0.0/tests/test_sdk.py +115 -0
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pdfbridge-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The official Python SDK for the PDFBridge API. Generate pixel-perfect PDFs from HTML/URLs.
|
|
5
|
+
Project-URL: Homepage, https://pdfbridge.xyz
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/techhspyder/pdfbridge-python/issues
|
|
7
|
+
Author-email: TechhSpyder <hello@techhspyder.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Requires-Dist: pydantic>=2.0.0
|
|
15
|
+
Requires-Dist: requests>=2.25.1
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: responses>=0.23.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
<img src="https://assets.pdfbridge.xyz/logo.svg" alt="PDFBridge Logo" width="200"/>
|
|
24
|
+
<h1>pdfbridge-python</h1>
|
|
25
|
+
<p>The official Python SDK for the <a href="https://pdfbridge.xyz">PDFBridge API</a>. Generate pixel-perfect PDFs from HTML or URLs.</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
[](https://badge.fury.io/py/pdfbridge-python)
|
|
29
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
[](https://pypi.org/project/pdfbridge-python/)
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Typed Inputs & Outputs**: Fully powered by Pydantic V2 for strict runtime validation and autocomplete.
|
|
35
|
+
- **Convenient Sync Methods**: Out-of-the-box `generate_and_wait` blocking wrapper so you don't have to write your own pollers.
|
|
36
|
+
- **Ghost Mode Native**: Fetch raw PDF bytes directly into memory. No intermediate storage or URLs.
|
|
37
|
+
- **Bulk Conversions**: Convert up to 1,000 documents simultaneously.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install pdfbridge-python
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import os
|
|
51
|
+
from pdfbridge import PDFBridge
|
|
52
|
+
|
|
53
|
+
# Initialize with your secret API key
|
|
54
|
+
# Can also be picked up automatically via the PDFBRIDGE_API_KEY environment variable.
|
|
55
|
+
client = PDFBridge(api_key="pk_live_your_key_here")
|
|
56
|
+
|
|
57
|
+
# Generate and wait for completion (blocks execution until PDF is ready)
|
|
58
|
+
status = client.generate_and_wait(
|
|
59
|
+
url="https://github.com",
|
|
60
|
+
filename="github_page.pdf",
|
|
61
|
+
options={
|
|
62
|
+
"format": "A4",
|
|
63
|
+
"printBackground": True
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
print(f"Success! Download PDF from: {status.pdfUrl}")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Advanced Usage
|
|
71
|
+
|
|
72
|
+
### 👻 Ghost Mode (Memory-Native)
|
|
73
|
+
|
|
74
|
+
Ghost Mode returns the direct `bytes` of the generated PDF without saving it to Cloud Storage. This is perfect for securely piping documents to your own S3 bucket or streaming directly to users.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Returns raw bytes natively
|
|
78
|
+
pdf_bytes = client.generate(
|
|
79
|
+
html="<h1>Highly Confidential Financial Data</h1>",
|
|
80
|
+
ghostMode=True,
|
|
81
|
+
options={
|
|
82
|
+
"format": "Letter",
|
|
83
|
+
"margin": "1in"
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
with open("secure_report.pdf", "wb") as f:
|
|
88
|
+
f.write(pdf_bytes)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 🚀 Bulk Generation
|
|
92
|
+
|
|
93
|
+
Queue up to 1,000 PDF generation jobs natively in a single API call.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
bulk_job = client.generate_bulk(
|
|
97
|
+
webhookUrl="https://api.yourdomain.com/webhooks/pdfbridge",
|
|
98
|
+
jobs=[
|
|
99
|
+
{"url": "https://example.com/invoice/123", "filename": "INV-123.pdf"},
|
|
100
|
+
{"url": "https://example.com/invoice/124", "filename": "INV-124.pdf"},
|
|
101
|
+
{"url": "https://example.com/invoice/125", "filename": "INV-125.pdf"}
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(f"Queued {len(bulk_job.jobs)} items.")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 🧠Templates & Variables
|
|
109
|
+
|
|
110
|
+
Pass dynamic data into your saved HTML templates.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
client.generate_and_wait(
|
|
114
|
+
templateId="tmpl_987654321",
|
|
115
|
+
variables={
|
|
116
|
+
"customer_name": "Jane Doe",
|
|
117
|
+
"total_amount": "$45.00"
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Error Handling
|
|
123
|
+
|
|
124
|
+
The SDK raises `PDFBridgeError` on any HTTP failures, authentication issues, or data malformations.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from pdfbridge import PDFBridgeError
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
client.generate(url="invalid-url")
|
|
131
|
+
except PDFBridgeError as e:
|
|
132
|
+
print(f"API Failed with status {e.status_code}: {str(e)}")
|
|
133
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://assets.pdfbridge.xyz/logo.svg" alt="PDFBridge Logo" width="200"/>
|
|
3
|
+
<h1>pdfbridge-python</h1>
|
|
4
|
+
<p>The official Python SDK for the <a href="https://pdfbridge.xyz">PDFBridge API</a>. Generate pixel-perfect PDFs from HTML or URLs.</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/py/pdfbridge-python)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://pypi.org/project/pdfbridge-python/)
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Typed Inputs & Outputs**: Fully powered by Pydantic V2 for strict runtime validation and autocomplete.
|
|
14
|
+
- **Convenient Sync Methods**: Out-of-the-box `generate_and_wait` blocking wrapper so you don't have to write your own pollers.
|
|
15
|
+
- **Ghost Mode Native**: Fetch raw PDF bytes directly into memory. No intermediate storage or URLs.
|
|
16
|
+
- **Bulk Conversions**: Convert up to 1,000 documents simultaneously.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pdfbridge-python
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import os
|
|
30
|
+
from pdfbridge import PDFBridge
|
|
31
|
+
|
|
32
|
+
# Initialize with your secret API key
|
|
33
|
+
# Can also be picked up automatically via the PDFBRIDGE_API_KEY environment variable.
|
|
34
|
+
client = PDFBridge(api_key="pk_live_your_key_here")
|
|
35
|
+
|
|
36
|
+
# Generate and wait for completion (blocks execution until PDF is ready)
|
|
37
|
+
status = client.generate_and_wait(
|
|
38
|
+
url="https://github.com",
|
|
39
|
+
filename="github_page.pdf",
|
|
40
|
+
options={
|
|
41
|
+
"format": "A4",
|
|
42
|
+
"printBackground": True
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
print(f"Success! Download PDF from: {status.pdfUrl}")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Advanced Usage
|
|
50
|
+
|
|
51
|
+
### 👻 Ghost Mode (Memory-Native)
|
|
52
|
+
|
|
53
|
+
Ghost Mode returns the direct `bytes` of the generated PDF without saving it to Cloud Storage. This is perfect for securely piping documents to your own S3 bucket or streaming directly to users.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# Returns raw bytes natively
|
|
57
|
+
pdf_bytes = client.generate(
|
|
58
|
+
html="<h1>Highly Confidential Financial Data</h1>",
|
|
59
|
+
ghostMode=True,
|
|
60
|
+
options={
|
|
61
|
+
"format": "Letter",
|
|
62
|
+
"margin": "1in"
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
with open("secure_report.pdf", "wb") as f:
|
|
67
|
+
f.write(pdf_bytes)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 🚀 Bulk Generation
|
|
71
|
+
|
|
72
|
+
Queue up to 1,000 PDF generation jobs natively in a single API call.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
bulk_job = client.generate_bulk(
|
|
76
|
+
webhookUrl="https://api.yourdomain.com/webhooks/pdfbridge",
|
|
77
|
+
jobs=[
|
|
78
|
+
{"url": "https://example.com/invoice/123", "filename": "INV-123.pdf"},
|
|
79
|
+
{"url": "https://example.com/invoice/124", "filename": "INV-124.pdf"},
|
|
80
|
+
{"url": "https://example.com/invoice/125", "filename": "INV-125.pdf"}
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
print(f"Queued {len(bulk_job.jobs)} items.")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 🧠Templates & Variables
|
|
88
|
+
|
|
89
|
+
Pass dynamic data into your saved HTML templates.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client.generate_and_wait(
|
|
93
|
+
templateId="tmpl_987654321",
|
|
94
|
+
variables={
|
|
95
|
+
"customer_name": "Jane Doe",
|
|
96
|
+
"total_amount": "$45.00"
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Error Handling
|
|
102
|
+
|
|
103
|
+
The SDK raises `PDFBridgeError` on any HTTP failures, authentication issues, or data malformations.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pdfbridge import PDFBridgeError
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
client.generate(url="invalid-url")
|
|
110
|
+
except PDFBridgeError as e:
|
|
111
|
+
print(f"API Failed with status {e.status_code}: {str(e)}")
|
|
112
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pdfbridge-python"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "The official Python SDK for the PDFBridge API. Generate pixel-perfect PDFs from HTML/URLs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "TechhSpyder", email = "hello@techhspyder.com" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"requests>=2.25.1",
|
|
23
|
+
"pydantic>=2.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
"Homepage" = "https://pdfbridge.xyz"
|
|
28
|
+
"Bug Tracker" = "https://github.com/techhspyder/pdfbridge-python/issues"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=7.0.0",
|
|
33
|
+
"pytest-cov>=4.0.0",
|
|
34
|
+
"responses>=0.23.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/pdfbridge"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
addopts = "-v --cov=pdfbridge"
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .client import PDFBridge, PDFBridgeError
|
|
2
|
+
from .models import (
|
|
3
|
+
ConvertRequest,
|
|
4
|
+
BulkConvertRequest,
|
|
5
|
+
ConvertResponse,
|
|
6
|
+
BulkConvertResponse,
|
|
7
|
+
JobStatusResponse,
|
|
8
|
+
PdfOptions,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"PDFBridge",
|
|
13
|
+
"PDFBridgeError",
|
|
14
|
+
"ConvertRequest",
|
|
15
|
+
"BulkConvertRequest",
|
|
16
|
+
"ConvertResponse",
|
|
17
|
+
"BulkConvertResponse",
|
|
18
|
+
"JobStatusResponse",
|
|
19
|
+
"PdfOptions",
|
|
20
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Optional, Union, Dict, Any
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
ConvertRequest,
|
|
8
|
+
BulkConvertRequest,
|
|
9
|
+
ConvertResponse,
|
|
10
|
+
BulkConvertResponse,
|
|
11
|
+
JobStatusResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PDFBridgeError(Exception):
|
|
16
|
+
def __init__(self, message: str, status_code: Optional[int] = None, metadata: Optional[Dict[str, Any]] = None):
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.metadata = metadata
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PDFBridge:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: Optional[str] = None,
|
|
26
|
+
base_url: str = "https://api.pdfbridge.xyz/api/v1",
|
|
27
|
+
max_retries: int = 2,
|
|
28
|
+
):
|
|
29
|
+
self.api_key = api_key or os.environ.get("PDFBRIDGE_API_KEY")
|
|
30
|
+
if not self.api_key:
|
|
31
|
+
raise PDFBridgeError(
|
|
32
|
+
"API Key is required to initialize the PDFBridge Client. Pass it explicitly or set PDFBRIDGE_API_KEY in your environment."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
self.base_url = base_url.rstrip("/")
|
|
36
|
+
self.max_retries = max_retries
|
|
37
|
+
self.session = requests.Session()
|
|
38
|
+
self.session.headers.update({
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"x-api-key": self.api_key,
|
|
41
|
+
"User-Agent": "pdfbridge-python/1.0.0",
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
def _request(self, method: str, path: str, **kwargs) -> Any:
|
|
45
|
+
url = f"{self.base_url}{path if path.startswith('/') else '/' + path}"
|
|
46
|
+
attempt = 0
|
|
47
|
+
|
|
48
|
+
while attempt <= self.max_retries:
|
|
49
|
+
try:
|
|
50
|
+
response = self.session.request(method, url, **kwargs)
|
|
51
|
+
|
|
52
|
+
if response.status_code == 204:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Handle binary ghost mode response
|
|
56
|
+
content_type = response.headers.get("content-type", "")
|
|
57
|
+
if "application/json" not in content_type and response.ok:
|
|
58
|
+
return response.content
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
data = response.json()
|
|
62
|
+
except ValueError:
|
|
63
|
+
data = {}
|
|
64
|
+
|
|
65
|
+
if not response.ok:
|
|
66
|
+
raise PDFBridgeError(
|
|
67
|
+
data.get("message") or data.get("error") or f"Request failed with status {response.status_code}",
|
|
68
|
+
status_code=response.status_code,
|
|
69
|
+
metadata=data,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
except requests.exceptions.RequestException as e:
|
|
75
|
+
if attempt == self.max_retries:
|
|
76
|
+
raise PDFBridgeError(f"Network error: {str(e)}")
|
|
77
|
+
attempt += 1
|
|
78
|
+
time.sleep((2 ** attempt) * 0.5)
|
|
79
|
+
|
|
80
|
+
raise PDFBridgeError("Unknown network error occurred.")
|
|
81
|
+
|
|
82
|
+
def generate(self, **kwargs) -> Union[ConvertResponse, bytes]:
|
|
83
|
+
"""
|
|
84
|
+
Start a new URL or HTML to PDF conversion job.
|
|
85
|
+
If ghostMode=True is passed, this immediately returns raw PDF bytes.
|
|
86
|
+
Otherwise, returns a ConvertResponse map containing the jobId.
|
|
87
|
+
"""
|
|
88
|
+
payload = ConvertRequest.model_validate(kwargs)
|
|
89
|
+
res = self._request("POST", "/convert", json=payload.model_dump(exclude_none=True))
|
|
90
|
+
|
|
91
|
+
if payload.ghostMode:
|
|
92
|
+
return res # returns bytes directly
|
|
93
|
+
return ConvertResponse.model_validate(res)
|
|
94
|
+
|
|
95
|
+
def generate_and_wait(self, poll_interval_ms: int = 2000, **kwargs) -> JobStatusResponse:
|
|
96
|
+
"""
|
|
97
|
+
Start a new job and automatically poll the status endpoint until COMPLETED or FAILED.
|
|
98
|
+
Returns the final JobStatusResponse containing the pdfUrl.
|
|
99
|
+
"""
|
|
100
|
+
payload = ConvertRequest.model_validate(kwargs)
|
|
101
|
+
if payload.ghostMode:
|
|
102
|
+
raise PDFBridgeError("Cannot use generate_and_wait with ghostMode=True. Using ghostMode natively returns the bytes immediately on the standard .generate() method.")
|
|
103
|
+
|
|
104
|
+
init_response = self.generate(**kwargs)
|
|
105
|
+
job_id = init_response.jobId
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
time.sleep(poll_interval_ms / 1000.0)
|
|
109
|
+
status = self.get_job(job_id)
|
|
110
|
+
|
|
111
|
+
if status.status == "COMPLETED":
|
|
112
|
+
return status
|
|
113
|
+
if status.status == "FAILED":
|
|
114
|
+
raise PDFBridgeError(f"Job failed: {status.error}", metadata=status.model_dump())
|
|
115
|
+
|
|
116
|
+
def generate_bulk(self, **kwargs) -> BulkConvertResponse:
|
|
117
|
+
"""Start a bulk conversion job for up to 1,000 documents at once."""
|
|
118
|
+
payload = BulkConvertRequest.model_validate(kwargs)
|
|
119
|
+
res = self._request("POST", "/convert/bulk", json=payload.model_dump(exclude_none=True))
|
|
120
|
+
return BulkConvertResponse.model_validate(res)
|
|
121
|
+
|
|
122
|
+
def get_job(self, job_id: str) -> JobStatusResponse:
|
|
123
|
+
"""Retrieve the status of an existing conversion job."""
|
|
124
|
+
res = self._request("GET", f"/jobs/{job_id}")
|
|
125
|
+
return JobStatusResponse.model_validate(res)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any, Union
|
|
2
|
+
from pydantic import BaseModel, Field, HttpUrl, model_validator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PdfOptions(BaseModel):
|
|
6
|
+
format: Optional[str] = Field(None, description="A4, Letter, Legal, Tabloid, Ledger, A3")
|
|
7
|
+
landscape: Optional[bool] = None
|
|
8
|
+
printBackground: Optional[bool] = None
|
|
9
|
+
scale: Optional[float] = Field(None, ge=0.1, le=2.0)
|
|
10
|
+
margin: Optional[str] = None
|
|
11
|
+
marginTop: Optional[str] = None
|
|
12
|
+
marginBottom: Optional[str] = None
|
|
13
|
+
marginLeft: Optional[str] = None
|
|
14
|
+
marginRight: Optional[str] = None
|
|
15
|
+
displayHeaderFooter: Optional[bool] = None
|
|
16
|
+
headerTemplate: Optional[str] = None
|
|
17
|
+
footerTemplate: Optional[str] = None
|
|
18
|
+
preferCSSPageSize: Optional[bool] = None
|
|
19
|
+
width: Optional[str] = None
|
|
20
|
+
height: Optional[str] = None
|
|
21
|
+
waitDelay: Optional[str] = None
|
|
22
|
+
userAgent: Optional[str] = None
|
|
23
|
+
waitForSelector: Optional[str] = None
|
|
24
|
+
metadata: Optional[Dict[str, str]] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConvertRequest(BaseModel):
|
|
28
|
+
url: Optional[str] = None
|
|
29
|
+
html: Optional[str] = None
|
|
30
|
+
filename: Optional[str] = None
|
|
31
|
+
webhookUrl: Optional[str] = None
|
|
32
|
+
ghostMode: Optional[bool] = None
|
|
33
|
+
tailwind: Optional[bool] = None
|
|
34
|
+
extractMetadata: Optional[bool] = None
|
|
35
|
+
templateId: Optional[str] = None
|
|
36
|
+
variables: Optional[Dict[str, Any]] = None
|
|
37
|
+
options: Optional[PdfOptions] = None
|
|
38
|
+
|
|
39
|
+
@model_validator(mode='after')
|
|
40
|
+
def check_source_provided(self) -> 'ConvertRequest':
|
|
41
|
+
if not self.url and not self.html and not self.templateId:
|
|
42
|
+
raise ValueError("You must provide either 'url', 'html', or 'templateId'")
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BulkConvertRequest(BaseModel):
|
|
47
|
+
jobs: List[ConvertRequest] = Field(..., min_length=1, max_length=1000)
|
|
48
|
+
ghostMode: Optional[bool] = None
|
|
49
|
+
webhookUrl: Optional[str] = None
|
|
50
|
+
extractMetadata: Optional[bool] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ConvertResponse(BaseModel):
|
|
54
|
+
message: str
|
|
55
|
+
jobId: str
|
|
56
|
+
statusUrl: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class JobItem(BaseModel):
|
|
60
|
+
jobId: str
|
|
61
|
+
statusUrl: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BulkConvertResponse(BaseModel):
|
|
65
|
+
message: str
|
|
66
|
+
jobs: List[JobItem]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class JobStatusResponse(BaseModel):
|
|
70
|
+
id: str
|
|
71
|
+
status: str # 'PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'
|
|
72
|
+
pdfUrl: Optional[str] = None
|
|
73
|
+
error: Optional[str] = None
|
|
74
|
+
createdAt: str
|
|
75
|
+
updatedAt: str
|
|
76
|
+
metadata: Optional[Any] = None
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pytest
|
|
3
|
+
import responses
|
|
4
|
+
from pdfbridge import PDFBridge, PDFBridgeError
|
|
5
|
+
from pdfbridge.models import JobStatusResponse, ConvertResponse
|
|
6
|
+
|
|
7
|
+
API_KEY = "pk_test_123"
|
|
8
|
+
BASE_URL = "https://api.pdfbridge.xyz/api/v1"
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def client():
|
|
12
|
+
return PDFBridge(api_key=API_KEY)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_init_without_api_key(monkeypatch):
|
|
16
|
+
monkeypatch.delenv("PDFBRIDGE_API_KEY", raising=False)
|
|
17
|
+
with pytest.raises(PDFBridgeError) as exc:
|
|
18
|
+
PDFBridge()
|
|
19
|
+
assert "API Key is required" in str(exc.value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_init_with_env_api_key(monkeypatch):
|
|
23
|
+
monkeypatch.setenv("PDFBRIDGE_API_KEY", "pk_test_env")
|
|
24
|
+
client = PDFBridge()
|
|
25
|
+
assert client.api_key == "pk_test_env"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@responses.activate
|
|
29
|
+
def test_generate_success(client):
|
|
30
|
+
responses.add(
|
|
31
|
+
responses.POST,
|
|
32
|
+
f"{BASE_URL}/convert",
|
|
33
|
+
json={"message": "Converted successfully", "jobId": "job-123", "statusUrl": "/jobs/job-123"},
|
|
34
|
+
status=202,
|
|
35
|
+
)
|
|
36
|
+
res = client.generate(url="https://example.com")
|
|
37
|
+
assert isinstance(res, ConvertResponse)
|
|
38
|
+
assert res.jobId == "job-123"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@responses.activate
|
|
42
|
+
def test_generate_auth_error(client):
|
|
43
|
+
responses.add(
|
|
44
|
+
responses.POST,
|
|
45
|
+
f"{BASE_URL}/convert",
|
|
46
|
+
json={"error": "Unauthorized"},
|
|
47
|
+
status=401,
|
|
48
|
+
)
|
|
49
|
+
with pytest.raises(PDFBridgeError) as exc:
|
|
50
|
+
client.generate(url="https://example.com")
|
|
51
|
+
assert "Unauthorized" in str(exc.value)
|
|
52
|
+
assert exc.value.status_code == 401
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_generate_payload_validation(client):
|
|
56
|
+
# Missing url, html, or templateId should fail fast locally
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
client.generate(ghostMode=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@responses.activate
|
|
62
|
+
def test_generate_ghost_mode(client):
|
|
63
|
+
binary_pdf = b"%PDF-1.4 mock binary data"
|
|
64
|
+
responses.add(
|
|
65
|
+
responses.POST,
|
|
66
|
+
f"{BASE_URL}/convert",
|
|
67
|
+
body=binary_pdf,
|
|
68
|
+
content_type="application/pdf",
|
|
69
|
+
status=200,
|
|
70
|
+
)
|
|
71
|
+
res = client.generate(html="<h1>Secret</h1>", ghostMode=True)
|
|
72
|
+
assert res == binary_pdf
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@responses.activate
|
|
76
|
+
def test_generate_and_wait(client):
|
|
77
|
+
# 1. Mock the initial POST
|
|
78
|
+
responses.add(
|
|
79
|
+
responses.POST,
|
|
80
|
+
f"{BASE_URL}/convert",
|
|
81
|
+
json={"message": "Accepted", "jobId": "job-abc", "statusUrl": "/jobs/job-abc"},
|
|
82
|
+
status=202,
|
|
83
|
+
)
|
|
84
|
+
# 2. Mock the first Poll (PROCESSING)
|
|
85
|
+
responses.add(
|
|
86
|
+
responses.GET,
|
|
87
|
+
f"{BASE_URL}/jobs/job-abc",
|
|
88
|
+
json={
|
|
89
|
+
"id": "job-abc",
|
|
90
|
+
"status": "PROCESSING",
|
|
91
|
+
"createdAt": "2026-03-01T12:00:00Z",
|
|
92
|
+
"updatedAt": "2026-03-01T12:00:01Z"
|
|
93
|
+
},
|
|
94
|
+
status=200,
|
|
95
|
+
)
|
|
96
|
+
# 3. Mock the final Poll (COMPLETED)
|
|
97
|
+
responses.add(
|
|
98
|
+
responses.GET,
|
|
99
|
+
f"{BASE_URL}/jobs/job-abc",
|
|
100
|
+
json={
|
|
101
|
+
"id": "job-abc",
|
|
102
|
+
"status": "COMPLETED",
|
|
103
|
+
"pdfUrl": "https://s3.amazonaws.com/bucket/doc.pdf",
|
|
104
|
+
"createdAt": "2026-03-01T12:00:00Z",
|
|
105
|
+
"updatedAt": "2026-03-01T12:00:05Z"
|
|
106
|
+
},
|
|
107
|
+
status=200,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Run with tiny poll interval for fast testing
|
|
111
|
+
res = client.generate_and_wait(url="https://example.com", poll_interval_ms=10)
|
|
112
|
+
|
|
113
|
+
assert isinstance(res, JobStatusResponse)
|
|
114
|
+
assert res.status == "COMPLETED"
|
|
115
|
+
assert res.pdfUrl == "https://s3.amazonaws.com/bucket/doc.pdf"
|