pdfgate 0.0.2__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.
pdfgate/pdfgate.py ADDED
@@ -0,0 +1,370 @@
1
+ from typing import Any, Union, cast
2
+
3
+ from pdfgate.http_client import PDFGateHTTPClientAsync, PDFGateHTTPClientSync
4
+ from pdfgate.request_builder import RequestBuilder, get_domain_from_api_key
5
+ from pdfgate.response_builder import ResponseBuilder
6
+
7
+ from .errors import ParamsValidationError
8
+ from .params import (
9
+ CompressPDFParams,
10
+ ExtractPDFFormDataParams,
11
+ FlattenPDFParams,
12
+ GeneratePDFParams,
13
+ GetDocumentParams,
14
+ GetFileParams,
15
+ ProtectPDFParams,
16
+ WatermarkPDFParams,
17
+ )
18
+ from .responses import PDFGateDocument
19
+
20
+
21
+ class PDFGate:
22
+ """Client for the PDFGate API.
23
+
24
+ Attributes:
25
+ api_key (str):
26
+ API key used for authentication.
27
+ domain (str):
28
+ Base API domain derived from the API key.
29
+ request_builder (RequestBuilder):
30
+ Builds HTTP requests for the API from parameter objects.
31
+ sync_client (PDFGateHTTPClientSync):
32
+ Synchronous HTTP client that handles HTTP errors.
33
+ async_client (PDFGateHTTPClientAsync):
34
+ Asynchronous HTTP client that handles HTTP errors.
35
+ """
36
+
37
+ def __init__(self, api_key: str):
38
+ """Initialize a PDFGate client.
39
+
40
+ Args:
41
+ api_key: The API key to use. Must start with 'live_' or 'test_'.
42
+
43
+ Raises:
44
+ PDFGateError: If the API key is invalid.
45
+ """
46
+ self.domain = get_domain_from_api_key(api_key)
47
+ self.api_key = api_key
48
+ self.request_builder = RequestBuilder(api_key=api_key)
49
+ self.sync_client = PDFGateHTTPClientSync(api_key=api_key)
50
+ self.async_client = PDFGateHTTPClientAsync(api_key=api_key)
51
+
52
+ def get_document(self, params: GetDocumentParams) -> PDFGateDocument:
53
+ """Retrieve a stored document’s metadata (and optionally a fresh pre-signed download URL).
54
+
55
+ Returns:
56
+ A `PDFGateDocument` parsed from the JSON response.
57
+
58
+ Raises:
59
+ PDFGateError: If the HTTP request fails (including non-2xx responses) or if
60
+ there is a network/connection error. The error includes status code
61
+ and message when available.
62
+ """
63
+ request = self.request_builder.build_get_document(params)
64
+ response = self.sync_client.try_make_request(request)
65
+ result = cast(
66
+ PDFGateDocument, ResponseBuilder.build_response(response, json=True)
67
+ )
68
+
69
+ return result
70
+
71
+ async def get_document_async(self, params: GetDocumentParams) -> PDFGateDocument:
72
+ """Retrieve a stored document’s metadata (and optionally a fresh pre-signed download URL).
73
+
74
+ Returns:
75
+ A `PDFGateDocument` parsed from the JSON response.
76
+
77
+ Raises:
78
+ PDFGateError: If the HTTP request fails (including non-2xx responses) or if
79
+ there is a network/connection error. The error includes status code
80
+ and message when available.
81
+ """
82
+ request = self.request_builder.build_get_document(params)
83
+ response = await self.async_client.try_make_request_async(request)
84
+ result = cast(
85
+ PDFGateDocument, ResponseBuilder.build_response(response, json=True)
86
+ )
87
+
88
+ return result
89
+
90
+ def get_file(self, params: GetFileParams) -> bytes:
91
+ """Download a raw PDF file by its document ID.
92
+
93
+ Important: Accessing stored generated files requires enabling
94
+ “Save files” in the PDFGate Dashboard settings (disabled by default).
95
+
96
+ Returns:
97
+ The raw bytes of the file.
98
+ """
99
+ request = self.request_builder.build_get_file(params.document_id)
100
+ response = self.sync_client.try_make_request(request=request)
101
+
102
+ return response.content
103
+
104
+ async def get_file_async(self, params: GetFileParams) -> bytes:
105
+ """Download a raw PDF file by its document ID.
106
+
107
+ Important: Accessing stored generated files requires enabling
108
+ “Save files” in the PDFGate Dashboard settings (disabled by default).
109
+
110
+ Returns:
111
+ The raw bytes of the file.
112
+ """
113
+ request = self.request_builder.build_get_file(params.document_id)
114
+ response = await self.async_client.try_make_request_async(request=request)
115
+
116
+ return response.content
117
+
118
+ def generate_pdf(self, params: GeneratePDFParams) -> Union[bytes, PDFGateDocument]:
119
+ """Generate a PDF from a URL or raw HTML.
120
+
121
+ Returns:
122
+ Either the raw PDF bytes or a `PDFGateDocument` parsed from the JSON response.
123
+ """
124
+ if not params.html and not params.url:
125
+ raise ParamsValidationError(
126
+ "Either the 'html' or 'url' parameters must be provided to generate a PDF."
127
+ )
128
+
129
+ request = self.request_builder.build_generate_pdf(params)
130
+ response = self.sync_client.try_make_request(request)
131
+ result = ResponseBuilder.build_response(response, json=params.json_response)
132
+
133
+ return result
134
+
135
+ async def generate_pdf_async(
136
+ self, params: GeneratePDFParams
137
+ ) -> Union[bytes, PDFGateDocument]:
138
+ """Generate a PDF from a URL or raw HTML.
139
+
140
+ Returns:
141
+ Either the raw PDF bytes or a `PDFGateDocument` parsed from the JSON response.
142
+ """
143
+ if not params.html and not params.url:
144
+ raise ParamsValidationError(
145
+ "Either the 'html' or 'url' parameters must be provided to generate a PDF."
146
+ )
147
+
148
+ request = self.request_builder.build_generate_pdf(params)
149
+ response = await self.async_client.try_make_request_async(request)
150
+ result = ResponseBuilder.build_response(response, json=params.json_response)
151
+
152
+ return result
153
+
154
+ def flatten_pdf(self, params: FlattenPDFParams) -> Union[bytes, PDFGateDocument]:
155
+ """Flatten an interactive PDF into a static, non-editable PDF.
156
+
157
+ Args:
158
+ params: Either a `FlattenPDFByDocumentIdParams` or
159
+ `FlattenPDFBinaryParams` instance depending on whether the file
160
+ is provided as a document ID or raw binary data.
161
+
162
+ Returns:
163
+ Depending on the `json_response` flag in `params`, this method either
164
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
165
+ """
166
+ request = self.request_builder.build_flatten_pdf(params)
167
+ response = self.sync_client.try_make_request(request)
168
+ result = ResponseBuilder.build_response(response, json=params.json_response)
169
+
170
+ return result
171
+
172
+ async def flatten_pdf_async(
173
+ self, params: FlattenPDFParams
174
+ ) -> Union[bytes, PDFGateDocument]:
175
+ """Flatten an interactive PDF into a static, non-editable PDF.
176
+
177
+ Args:
178
+ params: Either a `FlattenPDFByDocumentIdParams` or
179
+ `FlattenPDFBinaryParams` instance depending on whether the file
180
+ is provided as a document ID or raw binary data.
181
+
182
+ Returns:
183
+ Depending on the `json_response` flag in `params`, this method either
184
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
185
+ """
186
+ request = self.request_builder.build_flatten_pdf(params)
187
+ response = await self.async_client.try_make_request_async(request)
188
+ result = ResponseBuilder.build_response(response, json=params.json_response)
189
+
190
+ return result
191
+
192
+ def extract_pdf_form_data(self, params: ExtractPDFFormDataParams) -> Any:
193
+ """Extract form field data from a fillable PDF and return it as JSON.
194
+
195
+ Args:
196
+ params: Either a `ExtractPDFFormDataByDocumentIdParams` or
197
+ `ExtractPDFFormDataBinaryParams` instance depending on whether the file
198
+ is provided as a document ID or raw binary data.
199
+
200
+ Returns:
201
+ JSON object mapping form field names to their values.
202
+ """
203
+ request = self.request_builder.build_extract_pdf_form_data(params)
204
+ response = self.sync_client.try_make_request(request)
205
+ result = ResponseBuilder.build_response(response, json=True)
206
+
207
+ return result
208
+
209
+ async def extract_pdf_form_data_async(
210
+ self, params: ExtractPDFFormDataParams
211
+ ) -> Any:
212
+ """Extract form field data from a fillable PDF and return it as JSON.
213
+
214
+ Args:
215
+ params: Either a `ExtractPDFFormDataByDocumentIdParams` or
216
+ `ExtractPDFFormDataBinaryParams` instance depending on whether the file
217
+ is provided as a document ID or raw binary data.
218
+
219
+ Returns:
220
+ JSON object mapping form field names to their values.
221
+ """
222
+ request = self.request_builder.build_extract_pdf_form_data(params)
223
+ response = await self.async_client.try_make_request_async(request)
224
+ result = ResponseBuilder.build_response(response, json=True)
225
+
226
+ return result
227
+
228
+ def protect_pdf(self, params: ProtectPDFParams) -> Union[bytes, PDFGateDocument]:
229
+ """Protect a PDF using encryption + optional permission restrictions.
230
+
231
+ Security options highlights:
232
+ - `algorithm`: `"AES256"` (default) or `"AES128"`.
233
+ - `user_password`: password required to open the PDF (optional).
234
+ - `owner_password`: full control password; required in some cases (e.g., AES256 with `user_password`).
235
+ - Restrictions: `disable_print`, `disable_copy`, `disable_editing`.
236
+ - `encrypt_metadata`: whether PDF metadata is encrypted (default `False`).
237
+
238
+ Args:
239
+ params: Either a `ProtectPDFByDocumentIdParams` or
240
+ `ProtectPDFBinaryParams` instance depending on whether the file
241
+ is provided as a document ID or raw binary data.
242
+
243
+ Returns:
244
+ Depending on the `json_response` flag in `params`, this method either
245
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
246
+ """
247
+ request = self.request_builder.build_protect_pdf(params)
248
+ response = self.sync_client.try_make_request(request)
249
+ result = ResponseBuilder.build_response(response, json=params.json_response)
250
+
251
+ return result
252
+
253
+ async def protect_pdf_async(
254
+ self, params: ProtectPDFParams
255
+ ) -> Union[bytes, PDFGateDocument]:
256
+ """Protect a PDF using encryption + optional permission restrictions.
257
+
258
+ Security options highlights:
259
+ - `algorithm`: `"AES256"` (default) or `"AES128"`.
260
+ - `user_password`: password required to open the PDF (optional).
261
+ - `owner_password`: full control password; required in some cases (e.g., AES256 with `user_password`).
262
+ - Restrictions: `disable_print`, `disable_copy`, `disable_editing`.
263
+ - `encrypt_metadata`: whether PDF metadata is encrypted (default `False`).
264
+
265
+ Args:
266
+ params: Either a `ProtectPDFByDocumentIdParams` or
267
+ `ProtectPDFBinaryParams` instance depending on whether the file
268
+ is provided as a document ID or raw binary data.
269
+
270
+ Returns:
271
+ Depending on the `json_response` flag in `params`, this method either
272
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
273
+ """
274
+ request = self.request_builder.build_protect_pdf(params)
275
+ response = await self.async_client.try_make_request_async(request)
276
+ result = ResponseBuilder.build_response(response, json=params.json_response)
277
+
278
+ return result
279
+
280
+ def compress_pdf(self, params: CompressPDFParams) -> Union[bytes, PDFGateDocument]:
281
+ """Compress a PDF document to reduce its size without changing its visual content.
282
+
283
+ Args:
284
+ params: Either a `CompressPDFByDocumentIdParams` or
285
+ `CompressPDFBinaryParams` instance depending on whether the file
286
+ is provided as a document ID or raw binary data.
287
+
288
+ Returns:
289
+ Depending on the `json_response` flag in `params`, this method either
290
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
291
+ """
292
+ request = self.request_builder.build_compress_pdf(params)
293
+ response = self.sync_client.try_make_request(request)
294
+ result = ResponseBuilder.build_response(response, json=params.json_response)
295
+
296
+ return result
297
+
298
+ async def compress_pdf_async(
299
+ self, params: CompressPDFParams
300
+ ) -> Union[bytes, PDFGateDocument]:
301
+ """Compress a PDF document to reduce its size without changing its visual content.
302
+
303
+ Args:
304
+ params: Either a `CompressPDFByDocumentIdParams` or
305
+ `CompressPDFBinaryParams` instance depending on whether the file
306
+ is provided as a document ID or raw binary data.
307
+
308
+ Returns:
309
+ Depending on the `json_response` flag in `params`, this method either
310
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
311
+ """
312
+ request = self.request_builder.build_compress_pdf(params)
313
+ response = await self.async_client.try_make_request_async(request)
314
+ result = ResponseBuilder.build_response(response, json=params.json_response)
315
+
316
+ return result
317
+
318
+ def watermark_pdf(self, params: WatermarkPDFParams) -> Any:
319
+ """Apply a text or image watermark to a PDF.
320
+
321
+ Watermark configuration highlights:
322
+ - `type` is required: `"text"` or `"image"`.
323
+ - For text watermarks: `text` required when `type="text"`.
324
+ - For image watermarks: upload `watermark` image file (`.png`, `.jpg`, `.jpeg`).
325
+ - Optional: `font` (standard PDF fonts), `fontFile`
326
+ (`.ttf`/`.otf` overrides `font`), `fontSize`, `fontColor`,
327
+ `opacity` (0..1), `xPosition`, `yPosition`, `imageWidth`, `imageHeight`,
328
+ `rotate` (0..360).
329
+
330
+ Args:
331
+ params: Either a `WatermarkPDFByDocumentIdParams` or
332
+ `WatermarkPDFByFileParams` instance depending on whether the file
333
+ is provided as a document ID or raw binary data.
334
+
335
+ Returns:
336
+ Depending on the `json_response` flag in `params`, this method either
337
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
338
+ """
339
+ request = self.request_builder.build_watermark_pdf(params)
340
+ response = self.sync_client.try_make_request(request)
341
+ result = ResponseBuilder.build_response(response, json=params.json_response)
342
+
343
+ return result
344
+
345
+ async def watermark_pdf_async(self, params: WatermarkPDFParams) -> Any:
346
+ """Apply a text or image watermark to a PDF.
347
+
348
+ Watermark configuration highlights:
349
+ - `type` is required: `"text"` or `"image"`.
350
+ - For text watermarks: `text` required when `type="text"`.
351
+ - For image watermarks: upload `watermark` image file (`.png`, `.jpg`, `.jpeg`).
352
+ - Optional: `font` (standard PDF fonts), `fontFile`
353
+ (`.ttf`/`.otf` overrides `font`), `fontSize`, `fontColor`,
354
+ `opacity` (0..1), `xPosition`, `yPosition`, `imageWidth`, `imageHeight`,
355
+ `rotate` (0..360).
356
+
357
+ Args:
358
+ params: Either a `WatermarkPDFByDocumentIdParams` or
359
+ `WatermarkPDFByFileParams` instance depending on whether the file
360
+ is provided as a document ID or raw binary data.
361
+
362
+ Returns:
363
+ Depending on the `json_response` flag in `params`, this method either
364
+ returns the raw PDF bytes or a `PDFGateDocument` instance.
365
+ """
366
+ request = self.request_builder.build_watermark_pdf(params)
367
+ response = await self.async_client.try_make_request_async(request)
368
+ result = ResponseBuilder.build_response(response, json=params.json_response)
369
+
370
+ return result
@@ -0,0 +1,252 @@
1
+ """Request construction utilities for PDFGate API calls."""
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from datetime import timedelta
5
+ from enum import Enum
6
+ import mimetypes
7
+ from typing import Any
8
+ import httpx
9
+ from pdfgate.config import Config
10
+ from pdfgate.dict_keys_converter import snake_to_camel
11
+ from pdfgate.errors import PDFGateError
12
+ from pdfgate.params import (
13
+ CompressPDFByDocumentIdParams,
14
+ CompressPDFParams,
15
+ ExtractPDFFormDataByDocumentIdParams,
16
+ ExtractPDFFormDataParams,
17
+ FileParam,
18
+ FlattenPDFByFileParams,
19
+ FlattenPDFParams,
20
+ GeneratePDFParams,
21
+ GetDocumentParams,
22
+ PDFGateParams,
23
+ ProtectPDFByDocumentIdParams,
24
+ ProtectPDFParams,
25
+ WatermarkPDFByDocumentIdParams,
26
+ WatermarkPDFParams,
27
+ WatermarkType,
28
+ )
29
+ from pdfgate.url_builder import URLBuilder
30
+
31
+ FormDataFileParam = tuple[str, bytes, str]
32
+
33
+
34
+ def get_domain_from_api_key(api_key: str) -> str:
35
+ """Return the API domain corresponding to an API key.
36
+
37
+ Args:
38
+ api_key:
39
+ API key string that should start with 'live_' or 'test_'.
40
+
41
+ Returns:
42
+ The domain (URL) for the API (production or sandbox).
43
+
44
+ Raises:
45
+ PDFGateError: If the API key format is invalid.
46
+ """
47
+ if api_key.startswith("live_"):
48
+ return Config.PRODUCTION_API_DOMAIN
49
+ elif api_key.startswith("test_"):
50
+ return Config.SANDBOX_API_DOMAIN
51
+ else:
52
+ raise PDFGateError(
53
+ "Invalid API key format. Expected to start with 'live_' or 'test_'."
54
+ )
55
+
56
+
57
+ def pdfgate_params_to_params_dict(instance: PDFGateParams) -> dict[str, Any]:
58
+ """Convert a params dataclass into a JSON-ready dict."""
59
+ params_dict = asdict(instance)
60
+ params_without_nulls: dict[str, Any] = {}
61
+ for k, v in params_dict.items():
62
+ if v is not None:
63
+ params_without_nulls[snake_to_camel(k)] = (
64
+ v.value if isinstance(v, Enum) else v
65
+ )
66
+
67
+ return params_without_nulls
68
+
69
+
70
+ @dataclass
71
+ class PDFGateRequest:
72
+ """Container for an HTTP request and its timeout."""
73
+
74
+ request: httpx.Request
75
+ timeout: int = int(
76
+ timedelta(seconds=Config.DEFAULT_TIMEOUT_SECONDS).total_seconds()
77
+ )
78
+
79
+
80
+ class RequestBuilder:
81
+ """Build HTTP requests for PDFGate endpoints."""
82
+
83
+ def __init__(self, api_key: str):
84
+ domain = get_domain_from_api_key(api_key)
85
+ self.url_builder = URLBuilder(domain)
86
+ self.api_key = api_key
87
+
88
+ def get_headers(self) -> dict[str, str]:
89
+ """Return the authorization header for API calls."""
90
+ return {"Authorization": f"Bearer {self.api_key}"}
91
+
92
+ def _get_request(self, url: str, params: dict[str, Any] = {}) -> httpx.Request:
93
+ """Create a GET request with standard headers."""
94
+ return httpx.Request("GET", url=url, headers=self.get_headers(), params=params)
95
+
96
+ def _json_post_request(self, url: str, json: dict[str, Any]) -> httpx.Request:
97
+ """Create a JSON POST request with standard headers."""
98
+ return httpx.Request("POST", url=url, headers=self.get_headers(), json=json)
99
+
100
+ def _multipart_post_request(
101
+ self, url: str, data: dict[str, Any] = {}, files: dict[str, Any] = {}
102
+ ) -> httpx.Request:
103
+ """Create a multipart POST request with standard headers."""
104
+ return httpx.Request(
105
+ "POST", url=url, headers=self.get_headers(), data=data, files=files
106
+ )
107
+
108
+ def _build_file_param(
109
+ self, file_param: FileParam, key: str
110
+ ) -> dict[str, FormDataFileParam]:
111
+ """Build a file parameter for multipart requests."""
112
+ if file_param.type is None:
113
+ mime_type, _encoding = mimetypes.guess_type(file_param.name)
114
+ mime_type = mime_type or "application/octet-stream"
115
+ else:
116
+ mime_type = file_param.type or "application/octet-stream"
117
+
118
+ return {key: (file_param.name, file_param.data, mime_type)}
119
+
120
+ def build_get_file(self, document_id: str) -> PDFGateRequest:
121
+ """Build a request to download a document's file content."""
122
+ url = self.url_builder.get_file_url(document_id)
123
+ request = self._get_request(url)
124
+
125
+ return PDFGateRequest(request=request)
126
+
127
+ def build_get_document(self, params: GetDocumentParams) -> PDFGateRequest:
128
+ """Build a request to fetch a document's metadata."""
129
+ params_dict: dict[str, int] = {}
130
+ if params.pre_signed_url_expires_in is not None:
131
+ params_dict["preSignedUrlExpiresIn"] = params.pre_signed_url_expires_in
132
+
133
+ url = self.url_builder.get_document_url(params.document_id)
134
+ request = self._get_request(url, params_dict)
135
+
136
+ return PDFGateRequest(request=request)
137
+
138
+ def build_generate_pdf(self, params: GeneratePDFParams) -> PDFGateRequest:
139
+ """Build a request to generate a PDF from HTML or URL."""
140
+ url = self.url_builder.generate_pdf_url()
141
+ params_without_nulls = pdfgate_params_to_params_dict(params)
142
+ request = self._json_post_request(url, json=params_without_nulls)
143
+ timeout = int(
144
+ timedelta(minutes=Config.GENERATE_PDF_TIMEOUT_MINUTES).total_seconds()
145
+ )
146
+
147
+ return PDFGateRequest(request=request, timeout=timeout)
148
+
149
+ def build_flatten_pdf(self, params: FlattenPDFParams) -> PDFGateRequest:
150
+ """Build a request to flatten a PDF from file or document ID."""
151
+ url = self.url_builder.flatten_pdf_url()
152
+ params_without_nulls = pdfgate_params_to_params_dict(params)
153
+ if isinstance(params, FlattenPDFByFileParams) and params.file is not None:
154
+ files = self._build_file_param(params_without_nulls.pop("file"), "file")
155
+ else:
156
+ files = {}
157
+
158
+ request = self._multipart_post_request(
159
+ url, data=params_without_nulls, files=files
160
+ )
161
+ timeout = int(
162
+ timedelta(minutes=Config.FLATTEN_PDF_TIMEOUT_MINUTES).total_seconds()
163
+ )
164
+
165
+ return PDFGateRequest(request=request, timeout=timeout)
166
+
167
+ def build_extract_pdf_form_data(
168
+ self, params: ExtractPDFFormDataParams
169
+ ) -> PDFGateRequest:
170
+ """Build a request to extract PDF form data."""
171
+ url = self.url_builder.extract_pdf_form_data_url()
172
+ if isinstance(params, ExtractPDFFormDataByDocumentIdParams):
173
+ params_dict = {"documentId": params.document_id}
174
+ request = self._multipart_post_request(url, data=params_dict)
175
+ else:
176
+ files = self._build_file_param(params.file, "file")
177
+ request = self._multipart_post_request(url, files=files)
178
+
179
+ return PDFGateRequest(request=request)
180
+
181
+ def build_protect_pdf(self, params: ProtectPDFParams) -> PDFGateRequest:
182
+ """Build a request to encrypt a PDF from file or document ID."""
183
+ url = self.url_builder.protect_pdf_url()
184
+ params_without_nulls = pdfgate_params_to_params_dict(params)
185
+ if isinstance(params, ProtectPDFByDocumentIdParams):
186
+ request = self._multipart_post_request(url=url, data=params_without_nulls)
187
+ else:
188
+ file_param = {"file": params_without_nulls.pop("file", None)}
189
+ request = self._multipart_post_request(
190
+ url=url,
191
+ data=params_without_nulls,
192
+ files=file_param,
193
+ )
194
+ timeout = int(
195
+ timedelta(minutes=Config.PROTECT_PDF_TIMEOUT_MINUTES).total_seconds()
196
+ )
197
+
198
+ return PDFGateRequest(request=request, timeout=timeout)
199
+
200
+ def build_compress_pdf(self, params: CompressPDFParams) -> PDFGateRequest:
201
+ """Build a request to compress a PDF from file or document ID."""
202
+ url = self.url_builder.compress_pdf_url()
203
+ params_without_nulls = pdfgate_params_to_params_dict(params)
204
+ if isinstance(params, CompressPDFByDocumentIdParams):
205
+ request = self._multipart_post_request(url=url, data=params_without_nulls)
206
+ else:
207
+ file_param = {"file": params_without_nulls.pop("file", None)}
208
+ request = self._multipart_post_request(
209
+ url=url,
210
+ data=params_without_nulls,
211
+ files=file_param,
212
+ )
213
+ timeout = int(
214
+ timedelta(minutes=Config.COMPRESS_PDF_TIMEOUT_MINUTES).total_seconds()
215
+ )
216
+
217
+ return PDFGateRequest(request=request, timeout=timeout)
218
+
219
+ def build_watermark_pdf(self, params: WatermarkPDFParams) -> PDFGateRequest:
220
+ """Build a request to watermark a PDF from file or document ID."""
221
+ url = self.url_builder.watermark_pdf_url()
222
+ params_dict = pdfgate_params_to_params_dict(params)
223
+ if isinstance(params, WatermarkPDFByDocumentIdParams):
224
+ if params.type == WatermarkType.TEXT:
225
+ request = self._multipart_post_request(url=url, data=params_dict)
226
+ else:
227
+ watermark_file_param = self._build_file_param(
228
+ params_dict.pop("watermark"), "watermark"
229
+ )
230
+ request = self._multipart_post_request(
231
+ url=url,
232
+ data=params_dict,
233
+ files=watermark_file_param,
234
+ )
235
+ else:
236
+ if params.type == WatermarkType.IMAGE:
237
+ watermark_file_param = self._build_file_param(
238
+ params_dict.pop("watermark"), "watermark"
239
+ )
240
+ else:
241
+ watermark_file_param = {}
242
+ file_param = self._build_file_param(params_dict.pop("file"), "file")
243
+ request = self._multipart_post_request(
244
+ url=url,
245
+ data=params_dict,
246
+ files={**file_param, **watermark_file_param},
247
+ )
248
+ timeout = int(
249
+ timedelta(minutes=Config.WATERMARK_PDF_TIMEOUT_MINUTES).total_seconds()
250
+ )
251
+
252
+ return PDFGateRequest(request=request, timeout=timeout)
@@ -0,0 +1,23 @@
1
+ """Helpers for converting HTTP responses into SDK return types."""
2
+
3
+ from typing import Optional, Union, cast
4
+
5
+ import httpx
6
+
7
+ from pdfgate.dict_keys_converter import convert_camel_keys_to_snake
8
+ from pdfgate.responses import PDFGateDocument
9
+
10
+
11
+ class ResponseBuilder:
12
+ """Build SDK responses from HTTP responses."""
13
+
14
+ @staticmethod
15
+ def build_response(
16
+ response: httpx.Response, json: Optional[bool] = False
17
+ ) -> Union[bytes, PDFGateDocument]:
18
+ """Convert an HTTP response into raw bytes or a document object."""
19
+ if json:
20
+ json_response = response.json()
21
+ return cast(PDFGateDocument, convert_camel_keys_to_snake(json_response))
22
+
23
+ return response.content
pdfgate/responses.py ADDED
@@ -0,0 +1,37 @@
1
+ """Response models for PDFGate API results."""
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Optional, TypedDict
6
+
7
+
8
+ class DocumentStatus(Enum):
9
+ COMPLETED = "completed"
10
+ PROCESSING = "processing"
11
+ EXPIRED = "expired"
12
+ FAILED = "failed"
13
+
14
+
15
+ class DocumentType(Enum):
16
+ """Document types reported by the PDFGate API."""
17
+
18
+ FROM_HTML = "from_html"
19
+ FLATTENED = "flattened"
20
+ WATERMARKED = "watermarked"
21
+ ENCRYPTED = "encrypted"
22
+ COMPRESSED = "compressed"
23
+ SIGNED = "signed"
24
+
25
+
26
+ class PDFGateDocument(TypedDict, total=False):
27
+ """Typed dictionary representing a PDFGate document response."""
28
+
29
+ id: str
30
+ status: DocumentStatus
31
+ created_at: datetime
32
+ expires_at: datetime
33
+ type: Optional[DocumentType]
34
+ file_url: Optional[str]
35
+ size: Optional[int]
36
+ metadata: Optional[dict[str, Any]]
37
+ derived_from: Optional[str]