pdfbolt 1.0.0__py3-none-any.whl
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.
- pdfbolt/__init__.py +114 -0
- pdfbolt/_utils.py +107 -0
- pdfbolt/_version.py +1 -0
- pdfbolt/client.py +34 -0
- pdfbolt/direct_result.py +68 -0
- pdfbolt/errors.py +50 -0
- pdfbolt/http.py +156 -0
- pdfbolt/models.py +305 -0
- pdfbolt/py.typed +1 -0
- pdfbolt/rate_limit.py +45 -0
- pdfbolt/resources/__init__.py +1 -0
- pdfbolt/resources/async_conversions.py +94 -0
- pdfbolt/resources/direct.py +70 -0
- pdfbolt/resources/sync.py +81 -0
- pdfbolt/resources/usage.py +22 -0
- pdfbolt/types.py +202 -0
- pdfbolt/webhooks.py +91 -0
- pdfbolt-1.0.0.dist-info/METADATA +405 -0
- pdfbolt-1.0.0.dist-info/RECORD +21 -0
- pdfbolt-1.0.0.dist-info/WHEEL +4 -0
- pdfbolt-1.0.0.dist-info/licenses/LICENSE +21 -0
pdfbolt/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from ._version import VERSION
|
|
2
|
+
from .client import PDFBolt
|
|
3
|
+
from .direct_result import DirectConversionResult
|
|
4
|
+
from .errors import (
|
|
5
|
+
PDFBoltAPIError,
|
|
6
|
+
PDFBoltConfigurationError,
|
|
7
|
+
PDFBoltError,
|
|
8
|
+
PDFBoltNetworkError,
|
|
9
|
+
PDFBoltValidationError,
|
|
10
|
+
PDFBoltWebhookSignatureError,
|
|
11
|
+
)
|
|
12
|
+
from .models import (
|
|
13
|
+
AsyncConversionJob,
|
|
14
|
+
AsyncConversionWebhookEvent,
|
|
15
|
+
OneTimeCredits,
|
|
16
|
+
RateLimitInfo,
|
|
17
|
+
RateLimitWindow,
|
|
18
|
+
RecurringCredits,
|
|
19
|
+
SyncConversionResult,
|
|
20
|
+
UsageSummary,
|
|
21
|
+
)
|
|
22
|
+
from .types import (
|
|
23
|
+
AsyncConversionWebhookStatus,
|
|
24
|
+
AsyncConvertParams,
|
|
25
|
+
AsyncHtmlParams,
|
|
26
|
+
AsyncOptions,
|
|
27
|
+
AsyncTemplateParams,
|
|
28
|
+
AsyncUrlParams,
|
|
29
|
+
CompressionLevel,
|
|
30
|
+
ContentDisposition,
|
|
31
|
+
ConversionErrorCode,
|
|
32
|
+
ConversionOptions,
|
|
33
|
+
DirectConvertParams,
|
|
34
|
+
DirectHtmlParams,
|
|
35
|
+
DirectOptions,
|
|
36
|
+
DirectTemplateParams,
|
|
37
|
+
DirectUrlParams,
|
|
38
|
+
DomainCookie,
|
|
39
|
+
EmulateMediaType,
|
|
40
|
+
HttpCredentials,
|
|
41
|
+
Margin,
|
|
42
|
+
MarginDimension,
|
|
43
|
+
PageDimension,
|
|
44
|
+
PaperFormat,
|
|
45
|
+
PDFBoltCookie,
|
|
46
|
+
PrintProduction,
|
|
47
|
+
SyncConversionStatus,
|
|
48
|
+
SyncConvertParams,
|
|
49
|
+
SyncHtmlParams,
|
|
50
|
+
SyncOptions,
|
|
51
|
+
SyncTemplateParams,
|
|
52
|
+
SyncUrlParams,
|
|
53
|
+
UrlCookie,
|
|
54
|
+
ViewportSize,
|
|
55
|
+
WaitForSelector,
|
|
56
|
+
WaitUntil,
|
|
57
|
+
)
|
|
58
|
+
from .webhooks import Webhooks, webhooks
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"AsyncConvertParams",
|
|
62
|
+
"AsyncConversionJob",
|
|
63
|
+
"AsyncConversionWebhookEvent",
|
|
64
|
+
"AsyncConversionWebhookStatus",
|
|
65
|
+
"AsyncHtmlParams",
|
|
66
|
+
"AsyncOptions",
|
|
67
|
+
"AsyncTemplateParams",
|
|
68
|
+
"AsyncUrlParams",
|
|
69
|
+
"CompressionLevel",
|
|
70
|
+
"ContentDisposition",
|
|
71
|
+
"ConversionErrorCode",
|
|
72
|
+
"ConversionOptions",
|
|
73
|
+
"DirectConvertParams",
|
|
74
|
+
"DirectConversionResult",
|
|
75
|
+
"DirectHtmlParams",
|
|
76
|
+
"DirectOptions",
|
|
77
|
+
"DirectTemplateParams",
|
|
78
|
+
"DirectUrlParams",
|
|
79
|
+
"DomainCookie",
|
|
80
|
+
"EmulateMediaType",
|
|
81
|
+
"HttpCredentials",
|
|
82
|
+
"Margin",
|
|
83
|
+
"MarginDimension",
|
|
84
|
+
"OneTimeCredits",
|
|
85
|
+
"PDFBolt",
|
|
86
|
+
"PDFBoltAPIError",
|
|
87
|
+
"PDFBoltCookie",
|
|
88
|
+
"PDFBoltConfigurationError",
|
|
89
|
+
"PDFBoltError",
|
|
90
|
+
"PDFBoltNetworkError",
|
|
91
|
+
"PDFBoltValidationError",
|
|
92
|
+
"PDFBoltWebhookSignatureError",
|
|
93
|
+
"PageDimension",
|
|
94
|
+
"PaperFormat",
|
|
95
|
+
"PrintProduction",
|
|
96
|
+
"RateLimitInfo",
|
|
97
|
+
"RateLimitWindow",
|
|
98
|
+
"RecurringCredits",
|
|
99
|
+
"SyncConvertParams",
|
|
100
|
+
"SyncConversionResult",
|
|
101
|
+
"SyncConversionStatus",
|
|
102
|
+
"SyncHtmlParams",
|
|
103
|
+
"SyncOptions",
|
|
104
|
+
"SyncTemplateParams",
|
|
105
|
+
"SyncUrlParams",
|
|
106
|
+
"UrlCookie",
|
|
107
|
+
"UsageSummary",
|
|
108
|
+
"VERSION",
|
|
109
|
+
"ViewportSize",
|
|
110
|
+
"Webhooks",
|
|
111
|
+
"WaitForSelector",
|
|
112
|
+
"WaitUntil",
|
|
113
|
+
"webhooks",
|
|
114
|
+
]
|
pdfbolt/_utils.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import PDFBoltValidationError
|
|
8
|
+
|
|
9
|
+
RAW_NESTED_KEYS = {
|
|
10
|
+
"additionalWebhookHeaders",
|
|
11
|
+
"additional_webhook_headers",
|
|
12
|
+
"extraHTTPHeaders",
|
|
13
|
+
"extra_http_headers",
|
|
14
|
+
"templateData",
|
|
15
|
+
"template_data",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
API_KEY_OVERRIDES = {
|
|
19
|
+
"apply_extra_http_headers_to_all_resources": "applyExtraHTTPHeadersToAllResources",
|
|
20
|
+
"extra_http_headers": "extraHTTPHeaders",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def encode_base64(value: str) -> str:
|
|
25
|
+
return base64.b64encode(value.encode("utf-8")).decode("ascii")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def encode_header_footer_templates(params: dict[str, Any]) -> dict[str, Any]:
|
|
29
|
+
encoded = dict(params)
|
|
30
|
+
for key in ("header_template", "headerTemplate", "footer_template", "footerTemplate"):
|
|
31
|
+
value = encoded.get(key)
|
|
32
|
+
if isinstance(value, str):
|
|
33
|
+
encoded[key] = encode_base64(value)
|
|
34
|
+
return encoded
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def split_request_options(params: Mapping[str, Any]) -> tuple[dict[str, Any], float | None]:
|
|
38
|
+
body: dict[str, Any] = {}
|
|
39
|
+
request_timeout = None
|
|
40
|
+
|
|
41
|
+
for key, value in params.items():
|
|
42
|
+
if key == "request_timeout":
|
|
43
|
+
request_timeout = _optional_timeout(value)
|
|
44
|
+
else:
|
|
45
|
+
body[key] = value
|
|
46
|
+
|
|
47
|
+
return body, request_timeout
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def to_api_body(params: Mapping[str, Any]) -> dict[str, Any]:
|
|
51
|
+
return {_to_api_key(key): _to_api_value(value, key=key) for key, value in params.items()}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def require_string_field(params: Mapping[str, Any], field_name: str, method_name: str) -> str:
|
|
55
|
+
value = params.get(field_name)
|
|
56
|
+
if not isinstance(value, str):
|
|
57
|
+
raise PDFBoltValidationError(f"{field_name} is required when using {method_name}().")
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def require_object_field(
|
|
62
|
+
params: Mapping[str, Any],
|
|
63
|
+
field_name: str,
|
|
64
|
+
method_name: str,
|
|
65
|
+
) -> Mapping[str, Any]:
|
|
66
|
+
value = params.get(field_name)
|
|
67
|
+
if not isinstance(value, Mapping):
|
|
68
|
+
raise PDFBoltValidationError(f"{field_name} must be an object when using {method_name}().")
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def merge_params(required: Mapping[str, Any], optional: Mapping[str, Any]) -> dict[str, Any]:
|
|
73
|
+
return {**required, **optional}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _to_api_value(value: Any, *, key: str) -> Any:
|
|
77
|
+
if key in RAW_NESTED_KEYS:
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
if isinstance(value, Mapping):
|
|
81
|
+
return to_api_body(value)
|
|
82
|
+
|
|
83
|
+
if isinstance(value, list):
|
|
84
|
+
return [_to_api_value(item, key="") for item in value]
|
|
85
|
+
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _to_api_key(key: str) -> str:
|
|
90
|
+
if key in API_KEY_OVERRIDES:
|
|
91
|
+
return API_KEY_OVERRIDES[key]
|
|
92
|
+
|
|
93
|
+
if "_" not in key:
|
|
94
|
+
return key
|
|
95
|
+
|
|
96
|
+
parts = key.split("_")
|
|
97
|
+
return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _optional_timeout(value: Any) -> float | None:
|
|
101
|
+
if value is None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
if isinstance(value, int | float) and not isinstance(value, bool):
|
|
105
|
+
return float(value)
|
|
106
|
+
|
|
107
|
+
raise PDFBoltValidationError("request_timeout must be a number of seconds.")
|
pdfbolt/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "1.0.0"
|
pdfbolt/client.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from .http import DEFAULT_BASE_URL, DEFAULT_REQUEST_TIMEOUT_SECONDS, PDFBoltHttpClient
|
|
6
|
+
from .resources.async_conversions import AsyncConversionsResource
|
|
7
|
+
from .resources.direct import DirectResource
|
|
8
|
+
from .resources.sync import SyncResource
|
|
9
|
+
from .resources.usage import UsageResource
|
|
10
|
+
from .webhooks import Webhooks, webhooks
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PDFBolt:
|
|
14
|
+
webhooks: Webhooks = webhooks
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
api_key: str,
|
|
20
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
21
|
+
request_timeout: float = DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
22
|
+
session: requests.Session | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
http = PDFBoltHttpClient(
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
base_url=base_url,
|
|
27
|
+
request_timeout=request_timeout,
|
|
28
|
+
session=session,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
self.direct = DirectResource(http)
|
|
32
|
+
self.sync = SyncResource(http)
|
|
33
|
+
self.async_conversions = AsyncConversionsResource(http)
|
|
34
|
+
self.usage = UsageResource(http)
|
pdfbolt/direct_result.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .errors import PDFBoltNetworkError
|
|
10
|
+
from .models import RateLimitInfo
|
|
11
|
+
from .rate_limit import read_header, read_number_header, read_rate_limit_info
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DirectConversionResult:
|
|
15
|
+
def __init__(self, *, body: bytes, headers: Mapping[str, str]) -> None:
|
|
16
|
+
self.headers = headers
|
|
17
|
+
self.content_type = _normalize_content_type(read_header(headers, "content-type"))
|
|
18
|
+
self.content_disposition = read_header(headers, "content-disposition")
|
|
19
|
+
self.conversion_cost = read_number_header(headers, "x-pdfbolt-conversion-cost")
|
|
20
|
+
self.filename = _parse_content_disposition_filename(self.content_disposition)
|
|
21
|
+
self.rate_limit: RateLimitInfo = read_rate_limit_info(headers)
|
|
22
|
+
self.base64: str | None
|
|
23
|
+
|
|
24
|
+
if self.content_type == "text/plain":
|
|
25
|
+
try:
|
|
26
|
+
self.base64 = body.decode("utf-8").strip()
|
|
27
|
+
if not self.base64:
|
|
28
|
+
raise ValueError("empty Base64 response")
|
|
29
|
+
self.buffer = base64.b64decode(self.base64, validate=True)
|
|
30
|
+
except (UnicodeDecodeError, binascii.Error, ValueError) as error:
|
|
31
|
+
raise PDFBoltNetworkError(
|
|
32
|
+
"PDFBolt API returned a malformed Base64 direct response."
|
|
33
|
+
) from error
|
|
34
|
+
else:
|
|
35
|
+
self.base64 = None
|
|
36
|
+
self.buffer = body
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def size(self) -> int:
|
|
40
|
+
return len(self.buffer)
|
|
41
|
+
|
|
42
|
+
def save(self, file_path: str | Path) -> None:
|
|
43
|
+
Path(file_path).write_bytes(self.buffer)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _normalize_content_type(value: str | None) -> str:
|
|
47
|
+
if not value:
|
|
48
|
+
return "application/pdf"
|
|
49
|
+
|
|
50
|
+
content_types = [
|
|
51
|
+
part.split(";", 1)[0].strip().lower()
|
|
52
|
+
for part in value.split(",")
|
|
53
|
+
if part.split(";", 1)[0].strip()
|
|
54
|
+
]
|
|
55
|
+
if "application/pdf" in content_types:
|
|
56
|
+
return "application/pdf"
|
|
57
|
+
if "text/plain" in content_types:
|
|
58
|
+
return "text/plain"
|
|
59
|
+
|
|
60
|
+
return content_types[0] if content_types else "application/pdf"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_content_disposition_filename(content_disposition: str | None) -> str | None:
|
|
64
|
+
if not content_disposition:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
match = re.search(r'(?:^|;)\s*filename="?([^";]+)"?', content_disposition, re.IGNORECASE)
|
|
68
|
+
return match.group(1) if match else None
|
pdfbolt/errors.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
from .models import RateLimitInfo
|
|
6
|
+
from .rate_limit import read_rate_limit_info
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PDFBoltError(Exception):
|
|
10
|
+
"""Base class for all PDFBolt SDK errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PDFBoltAPIError(PDFBoltError):
|
|
14
|
+
"""Raised when the PDFBolt API returns an HTTP error response."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
message: str,
|
|
20
|
+
status_code: int,
|
|
21
|
+
timestamp: str | None = None,
|
|
22
|
+
error_code: str | None = None,
|
|
23
|
+
error_message: str | None = None,
|
|
24
|
+
headers: Mapping[str, str] | None = None,
|
|
25
|
+
raw_body: str | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.timestamp = timestamp
|
|
30
|
+
self.error_code = error_code
|
|
31
|
+
self.error_message = error_message
|
|
32
|
+
self.rate_limit: RateLimitInfo = read_rate_limit_info(headers)
|
|
33
|
+
self.headers = headers
|
|
34
|
+
self.raw_body = raw_body
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PDFBoltNetworkError(PDFBoltError):
|
|
38
|
+
"""Raised when the SDK did not receive a usable HTTP response."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PDFBoltWebhookSignatureError(PDFBoltError):
|
|
42
|
+
"""Raised when webhook signature verification fails."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PDFBoltValidationError(PDFBoltError):
|
|
46
|
+
"""Raised before a request is sent when high-level helper parameters are invalid."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PDFBoltConfigurationError(PDFBoltError):
|
|
50
|
+
"""Raised when the SDK client is missing required configuration."""
|
pdfbolt/http.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from ._version import VERSION
|
|
10
|
+
from .errors import PDFBoltAPIError, PDFBoltConfigurationError, PDFBoltNetworkError
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.pdfbolt.com"
|
|
13
|
+
DEFAULT_REQUEST_TIMEOUT_SECONDS = 120.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PDFBoltHttpClient:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
api_key: str,
|
|
21
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
22
|
+
request_timeout: float = DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
23
|
+
session: requests.Session | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
if not api_key:
|
|
26
|
+
raise PDFBoltConfigurationError("PDFBolt API key is required.")
|
|
27
|
+
|
|
28
|
+
self._api_key = api_key
|
|
29
|
+
self._base_url = base_url.rstrip("/")
|
|
30
|
+
self._request_timeout = request_timeout
|
|
31
|
+
self._session = session or requests.Session()
|
|
32
|
+
|
|
33
|
+
def request_json(
|
|
34
|
+
self,
|
|
35
|
+
method: str,
|
|
36
|
+
path: str,
|
|
37
|
+
*,
|
|
38
|
+
body: Mapping[str, Any] | None = None,
|
|
39
|
+
request_timeout: float | None = None,
|
|
40
|
+
) -> tuple[dict[str, Any], Mapping[str, str]]:
|
|
41
|
+
response = self._request(
|
|
42
|
+
method,
|
|
43
|
+
path,
|
|
44
|
+
body=body,
|
|
45
|
+
request_timeout=request_timeout,
|
|
46
|
+
accept="application/json",
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
data = response.json()
|
|
50
|
+
except ValueError as error:
|
|
51
|
+
raise PDFBoltNetworkError(
|
|
52
|
+
f"PDFBolt API returned a malformed JSON response for {path} "
|
|
53
|
+
f"(status {response.status_code})."
|
|
54
|
+
) from error
|
|
55
|
+
|
|
56
|
+
if not isinstance(data, dict):
|
|
57
|
+
raise PDFBoltNetworkError(
|
|
58
|
+
f"PDFBolt API returned a malformed JSON response for {path} "
|
|
59
|
+
f"(status {response.status_code})."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return data, response.headers
|
|
63
|
+
|
|
64
|
+
def request_binary(
|
|
65
|
+
self,
|
|
66
|
+
method: str,
|
|
67
|
+
path: str,
|
|
68
|
+
*,
|
|
69
|
+
body: Mapping[str, Any] | None = None,
|
|
70
|
+
request_timeout: float | None = None,
|
|
71
|
+
) -> tuple[bytes, Mapping[str, str]]:
|
|
72
|
+
response = self._request(
|
|
73
|
+
method,
|
|
74
|
+
path,
|
|
75
|
+
body=body,
|
|
76
|
+
request_timeout=request_timeout,
|
|
77
|
+
accept="application/pdf, text/plain, application/json",
|
|
78
|
+
)
|
|
79
|
+
return response.content, response.headers
|
|
80
|
+
|
|
81
|
+
def _request(
|
|
82
|
+
self,
|
|
83
|
+
method: str,
|
|
84
|
+
path: str,
|
|
85
|
+
*,
|
|
86
|
+
body: Mapping[str, Any] | None,
|
|
87
|
+
request_timeout: float | None,
|
|
88
|
+
accept: str,
|
|
89
|
+
) -> requests.Response:
|
|
90
|
+
timeout = self._request_timeout if request_timeout is None else request_timeout
|
|
91
|
+
headers = self._headers(accept=accept, has_body=body is not None)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
response = self._session.request(
|
|
95
|
+
method,
|
|
96
|
+
f"{self._base_url}{path}",
|
|
97
|
+
headers=headers,
|
|
98
|
+
json=body,
|
|
99
|
+
timeout=timeout,
|
|
100
|
+
)
|
|
101
|
+
except requests.Timeout as error:
|
|
102
|
+
raise PDFBoltNetworkError(f"PDFBolt request timed out after {timeout}s.") from error
|
|
103
|
+
except Exception as error:
|
|
104
|
+
raise PDFBoltNetworkError(
|
|
105
|
+
"PDFBolt request failed before receiving a response."
|
|
106
|
+
) from error
|
|
107
|
+
|
|
108
|
+
if response.status_code >= 400:
|
|
109
|
+
raise _create_api_error(response)
|
|
110
|
+
|
|
111
|
+
return response
|
|
112
|
+
|
|
113
|
+
def _headers(self, *, accept: str, has_body: bool) -> dict[str, str]:
|
|
114
|
+
headers = {
|
|
115
|
+
"Accept": accept,
|
|
116
|
+
"API-KEY": self._api_key,
|
|
117
|
+
"User-Agent": f"pdfbolt-python/{VERSION}",
|
|
118
|
+
}
|
|
119
|
+
if has_body:
|
|
120
|
+
headers["Content-Type"] = "application/json"
|
|
121
|
+
return headers
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _create_api_error(response: requests.Response) -> PDFBoltAPIError:
|
|
125
|
+
raw_body = response.text
|
|
126
|
+
parsed = _parse_json_object(raw_body)
|
|
127
|
+
error_message = _read_string(parsed, "errorMessage")
|
|
128
|
+
message = (
|
|
129
|
+
error_message
|
|
130
|
+
or response.reason
|
|
131
|
+
or (f"PDFBolt API request failed with status {response.status_code}.")
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return PDFBoltAPIError(
|
|
135
|
+
message=message,
|
|
136
|
+
status_code=response.status_code,
|
|
137
|
+
timestamp=_read_string(parsed, "timestamp"),
|
|
138
|
+
error_code=_read_string(parsed, "errorCode"),
|
|
139
|
+
error_message=error_message,
|
|
140
|
+
headers=response.headers,
|
|
141
|
+
raw_body=raw_body,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _parse_json_object(raw_body: str) -> dict[str, Any] | None:
|
|
146
|
+
try:
|
|
147
|
+
parsed = json.loads(raw_body)
|
|
148
|
+
except json.JSONDecodeError:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
return parsed if isinstance(parsed, dict) else None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _read_string(source: dict[str, Any] | None, key: str) -> str | None:
|
|
155
|
+
value = source.get(key) if source else None
|
|
156
|
+
return value if isinstance(value, str) else None
|