pdfdancer-client-python 0.2.22__py3-none-any.whl → 0.2.24__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.
pdfdancer/__init__.py CHANGED
@@ -10,6 +10,7 @@ from .exceptions import (
10
10
  FontNotFoundException,
11
11
  HttpClientException,
12
12
  PdfDancerException,
13
+ RateLimitException,
13
14
  SessionException,
14
15
  ValidationException,
15
16
  )
@@ -80,6 +81,7 @@ __all__ = [
80
81
  "ValidationException",
81
82
  "HttpClientException",
82
83
  "SessionException",
84
+ "RateLimitException",
83
85
  "set_ssl_verify",
84
86
  ]
85
87
 
pdfdancer/exceptions.py CHANGED
@@ -62,3 +62,20 @@ class ValidationException(PdfDancerException):
62
62
  """
63
63
 
64
64
  pass
65
+
66
+
67
+ class RateLimitException(PdfDancerException):
68
+ """
69
+ Exception raised when the API rate limit is exceeded (HTTP 429).
70
+ Includes retry-after information if provided by the server.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ message: str,
76
+ retry_after: Optional[int] = None,
77
+ response: Optional[httpx.Response] = None,
78
+ ):
79
+ super().__init__(message)
80
+ self.retry_after = retry_after
81
+ self.response = response
pdfdancer/pdfdancer_v1.py CHANGED
@@ -11,6 +11,7 @@ import gzip
11
11
  import json
12
12
  import logging
13
13
  import os
14
+ import sys
14
15
  import time
15
16
  from datetime import datetime, timezone
16
17
  from pathlib import Path
@@ -24,6 +25,7 @@ from .exceptions import (
24
25
  FontNotFoundException,
25
26
  HttpClientException,
26
27
  PdfDancerException,
28
+ RateLimitException,
27
29
  SessionException,
28
30
  ValidationException,
29
31
  )
@@ -234,6 +236,30 @@ def _is_retryable_error(error: Exception) -> bool:
234
236
  return any(msg in error_msg for msg in retryable_messages)
235
237
 
236
238
 
239
+ def _get_retry_after_delay(response: httpx.Response) -> Optional[int]:
240
+ """
241
+ Extract Retry-After delay from response headers.
242
+
243
+ Args:
244
+ response: HTTP response with potential Retry-After header
245
+
246
+ Returns:
247
+ Delay in seconds, or None if header not present or invalid
248
+ """
249
+ retry_after = response.headers.get("Retry-After")
250
+ if not retry_after:
251
+ return None
252
+
253
+ try:
254
+ # Retry-After can be either a number of seconds or an HTTP date
255
+ # Try parsing as integer first (seconds)
256
+ return int(retry_after)
257
+ except ValueError:
258
+ # If not a number, it might be an HTTP date - ignore for now
259
+ # Most rate limiting APIs use seconds
260
+ return None
261
+
262
+
237
263
  class PageClient:
238
264
  def __init__(
239
265
  self,
@@ -708,38 +734,102 @@ class PDFDancer:
708
734
 
709
735
  Raises:
710
736
  HttpClientException: If token request fails
737
+ RateLimitException: If rate limit is exceeded
711
738
  """
739
+ # Create temporary client without authentication
740
+ temp_client = httpx.Client(http2=True, verify=not DISABLE_SSL_VERIFY)
741
+ max_retries = 3
742
+ retry_backoff_factor = 1.0
743
+
712
744
  try:
713
- # Create temporary client without authentication
714
- temp_client = httpx.Client(http2=True, verify=not DISABLE_SSL_VERIFY)
745
+ last_error: Optional[Exception] = None
746
+ attempt = 0
715
747
 
716
- headers = {"X-Fingerprint": Fingerprint.generate()}
748
+ while attempt <= max_retries:
749
+ try:
750
+ headers = {"X-Fingerprint": Fingerprint.generate()}
717
751
 
718
- response = temp_client.post(
719
- cls._cleanup_url_path(base_url, "/keys/anon"),
720
- headers=headers,
721
- timeout=timeout if timeout > 0 else None,
722
- )
752
+ response = temp_client.post(
753
+ cls._cleanup_url_path(base_url, "/keys/anon"),
754
+ headers=headers,
755
+ timeout=timeout if timeout > 0 else None,
756
+ )
723
757
 
724
- response.raise_for_status()
725
- token_data = response.json()
758
+ response.raise_for_status()
759
+ token_data = response.json()
726
760
 
727
- # Extract token from response (matches Java AnonTokenResponse structure)
728
- if isinstance(token_data, dict) and "token" in token_data:
729
- return token_data["token"]
730
- else:
731
- raise HttpClientException("Invalid anonymous token response format")
761
+ # Extract token from response (matches Java AnonTokenResponse structure)
762
+ if isinstance(token_data, dict) and "token" in token_data:
763
+ return token_data["token"]
764
+ else:
765
+ raise HttpClientException(
766
+ "Invalid anonymous token response format"
767
+ )
732
768
 
733
- except httpx.HTTPStatusError as e:
734
- raise HttpClientException(
735
- f"Failed to obtain anonymous token: HTTP {e.response.status_code}",
736
- response=e.response,
737
- cause=e,
738
- ) from None
739
- except httpx.RequestError as e:
740
- raise HttpClientException(
741
- f"Failed to obtain anonymous token: {str(e)}", response=None, cause=e
742
- ) from None
769
+ except httpx.HTTPStatusError as e:
770
+ # Handle 429 (rate limit) with retry
771
+ if e.response.status_code == 429 and attempt < max_retries:
772
+ retry_after = _get_retry_after_delay(e.response)
773
+ if retry_after is not None:
774
+ delay = retry_after
775
+ else:
776
+ # Use exponential backoff if no Retry-After header
777
+ delay = retry_backoff_factor * (2**attempt)
778
+
779
+ # Always log 429 to stderr for visibility
780
+ print(
781
+ f"Rate limit (429) on POST /keys/anon - retrying in {delay}s "
782
+ f"(attempt {attempt + 1}/{max_retries})",
783
+ file=sys.stderr,
784
+ )
785
+ if DEBUG:
786
+ print(
787
+ f"{time.time()}|POST /keys/anon - Rate limit exceeded (429), "
788
+ f"retrying in {delay}s (attempt {attempt + 1}/{max_retries})"
789
+ )
790
+ time.sleep(delay)
791
+ attempt += 1
792
+ continue
793
+
794
+ # Raise RateLimitException for 429 after exhausting retries
795
+ if e.response.status_code == 429:
796
+ retry_after = _get_retry_after_delay(e.response)
797
+ print(
798
+ "Rate limit (429) on POST /keys/anon - max retries exhausted",
799
+ file=sys.stderr,
800
+ )
801
+ raise RateLimitException(
802
+ "Rate limit exceeded when obtaining anonymous token",
803
+ retry_after=retry_after,
804
+ response=e.response,
805
+ ) from None
806
+
807
+ # Other HTTP status errors
808
+ raise HttpClientException(
809
+ f"Failed to obtain anonymous token: HTTP {e.response.status_code}",
810
+ response=e.response,
811
+ cause=e,
812
+ ) from None
813
+ except httpx.RequestError as e:
814
+ last_error = e
815
+ raise HttpClientException(
816
+ f"Failed to obtain anonymous token: {str(e)}",
817
+ response=None,
818
+ cause=e,
819
+ ) from None
820
+
821
+ # Should not reach here, but handle just in case
822
+ if last_error:
823
+ raise HttpClientException(
824
+ f"Failed to obtain anonymous token after {max_retries + 1} attempts: {str(last_error)}",
825
+ response=None,
826
+ cause=last_error,
827
+ ) from None
828
+ else:
829
+ raise HttpClientException(
830
+ f"Failed to obtain anonymous token after {max_retries + 1} attempts",
831
+ response=None,
832
+ )
743
833
  finally:
744
834
  temp_client.close()
745
835
 
@@ -1029,7 +1119,9 @@ class PDFDancer:
1029
1119
  b'Content-Disposition: form-data; name="pdf"; filename="document.pdf"\r\n'
1030
1120
  )
1031
1121
  body_parts.append(b"Content-Type: application/pdf\r\n")
1032
- body_parts.append(b"\r\n") # End of headers, no Content-Transfer-Encoding
1122
+ body_parts.append(
1123
+ b"\r\n"
1124
+ ) # End of headers, no Content-Transfer-Encoding
1033
1125
  body_parts.append(self._pdf_bytes)
1034
1126
  body_parts.append(b"\r\n")
1035
1127
  body_parts.append(f"--{boundary}--\r\n".encode("utf-8"))
@@ -1042,11 +1134,17 @@ class PDFDancer:
1042
1134
  original_size = len(uncompressed_body)
1043
1135
  compressed_size = len(compressed_body)
1044
1136
  compression_ratio = (
1045
- (1 - compressed_size / original_size) * 100 if original_size > 0 else 0
1137
+ (1 - compressed_size / original_size) * 100
1138
+ if original_size > 0
1139
+ else 0
1046
1140
  )
1047
1141
 
1048
1142
  if DEBUG:
1049
- retry_info = f" (attempt {attempt + 1}/{self._max_retries + 1})" if attempt > 0 else ""
1143
+ retry_info = (
1144
+ f" (attempt {attempt + 1}/{self._max_retries + 1})"
1145
+ if attempt > 0
1146
+ else ""
1147
+ )
1050
1148
  print(
1051
1149
  f"{time.time()}|POST /session/create{retry_info} - original size: {original_size} bytes, "
1052
1150
  f"compressed size: {compressed_size} bytes, "
@@ -1083,9 +1181,47 @@ class PDFDancer:
1083
1181
  return session_id
1084
1182
 
1085
1183
  except httpx.HTTPStatusError as e:
1086
- # HTTP status errors are not retried (these are application-level errors)
1184
+ # Handle 429 (rate limit) with retry
1185
+ if e.response.status_code == 429 and attempt < self._max_retries:
1186
+ retry_after = _get_retry_after_delay(e.response)
1187
+ if retry_after is not None:
1188
+ delay = retry_after
1189
+ else:
1190
+ # Use exponential backoff if no Retry-After header
1191
+ delay = self._retry_backoff_factor * (2**attempt)
1192
+
1193
+ # Always log 429 to stderr for visibility
1194
+ print(
1195
+ f"Rate limit (429) on POST /session/create - retrying in {delay}s "
1196
+ f"(attempt {attempt + 1}/{self._max_retries})",
1197
+ file=sys.stderr,
1198
+ )
1199
+ if DEBUG:
1200
+ print(
1201
+ f"{time.time()}|POST /session/create - Rate limit exceeded (429), "
1202
+ f"retrying in {delay}s (attempt {attempt + 1}/{self._max_retries})"
1203
+ )
1204
+ time.sleep(delay)
1205
+ attempt += 1
1206
+ continue
1207
+
1208
+ # Other HTTP status errors are not retried (these are application-level errors)
1087
1209
  self._handle_authentication_error(e.response)
1088
1210
  error_message = self._extract_error_message(e.response)
1211
+
1212
+ # Raise RateLimitException for 429 after exhausting retries
1213
+ if e.response.status_code == 429:
1214
+ retry_after = _get_retry_after_delay(e.response)
1215
+ print(
1216
+ "Rate limit (429) on POST /session/create - max retries exhausted",
1217
+ file=sys.stderr,
1218
+ )
1219
+ raise RateLimitException(
1220
+ f"Rate limit exceeded: {error_message}",
1221
+ retry_after=retry_after,
1222
+ response=e.response,
1223
+ ) from None
1224
+
1089
1225
  raise HttpClientException(
1090
1226
  f"Failed to create session: {error_message}",
1091
1227
  response=e.response,
@@ -1097,7 +1233,7 @@ class PDFDancer:
1097
1233
  # Check if this is a retryable error
1098
1234
  if _is_retryable_error(e) and attempt < self._max_retries:
1099
1235
  # Calculate exponential backoff delay
1100
- delay = self._retry_backoff_factor * (2 ** attempt)
1236
+ delay = self._retry_backoff_factor * (2**attempt)
1101
1237
  if DEBUG:
1102
1238
  print(
1103
1239
  f"{time.time()}|POST /session/create - Retryable error: {str(e)}, "
@@ -1157,9 +1293,7 @@ class PDFDancer:
1157
1293
  except ValueError as exc:
1158
1294
  raise ValidationException(str(exc)) from exc
1159
1295
  except TypeError:
1160
- raise ValidationException(
1161
- f"Invalid page_size type: {type(page_size)}"
1162
- )
1296
+ raise ValidationException(f"Invalid page_size type: {type(page_size)}")
1163
1297
 
1164
1298
  # Handle orientation
1165
1299
  if orientation is not None:
@@ -1187,7 +1321,11 @@ class PDFDancer:
1187
1321
  request_body = json.dumps(request_data)
1188
1322
  request_size = len(request_body.encode("utf-8"))
1189
1323
  if DEBUG:
1190
- retry_info = f" (attempt {attempt + 1}/{self._max_retries + 1})" if attempt > 0 else ""
1324
+ retry_info = (
1325
+ f" (attempt {attempt + 1}/{self._max_retries + 1})"
1326
+ if attempt > 0
1327
+ else ""
1328
+ )
1191
1329
  print(
1192
1330
  f"{time.time()}|POST /session/new{retry_info} - request size: {request_size} bytes"
1193
1331
  )
@@ -1220,9 +1358,47 @@ class PDFDancer:
1220
1358
  return session_id
1221
1359
 
1222
1360
  except httpx.HTTPStatusError as e:
1223
- # HTTP status errors are not retried (these are application-level errors)
1361
+ # Handle 429 (rate limit) with retry
1362
+ if e.response.status_code == 429 and attempt < self._max_retries:
1363
+ retry_after = _get_retry_after_delay(e.response)
1364
+ if retry_after is not None:
1365
+ delay = retry_after
1366
+ else:
1367
+ # Use exponential backoff if no Retry-After header
1368
+ delay = self._retry_backoff_factor * (2**attempt)
1369
+
1370
+ # Always log 429 to stderr for visibility
1371
+ print(
1372
+ f"Rate limit (429) on POST /session/new - retrying in {delay}s "
1373
+ f"(attempt {attempt + 1}/{self._max_retries})",
1374
+ file=sys.stderr,
1375
+ )
1376
+ if DEBUG:
1377
+ print(
1378
+ f"{time.time()}|POST /session/new - Rate limit exceeded (429), "
1379
+ f"retrying in {delay}s (attempt {attempt + 1}/{self._max_retries})"
1380
+ )
1381
+ time.sleep(delay)
1382
+ attempt += 1
1383
+ continue
1384
+
1385
+ # Other HTTP status errors are not retried (these are application-level errors)
1224
1386
  self._handle_authentication_error(e.response)
1225
1387
  error_message = self._extract_error_message(e.response)
1388
+
1389
+ # Raise RateLimitException for 429 after exhausting retries
1390
+ if e.response.status_code == 429:
1391
+ retry_after = _get_retry_after_delay(e.response)
1392
+ print(
1393
+ "Rate limit (429) on POST /session/new - max retries exhausted",
1394
+ file=sys.stderr,
1395
+ )
1396
+ raise RateLimitException(
1397
+ f"Rate limit exceeded: {error_message}",
1398
+ retry_after=retry_after,
1399
+ response=e.response,
1400
+ ) from None
1401
+
1226
1402
  raise HttpClientException(
1227
1403
  f"Failed to create blank PDF session: {error_message}",
1228
1404
  response=e.response,
@@ -1234,7 +1410,7 @@ class PDFDancer:
1234
1410
  # Check if this is a retryable error
1235
1411
  if _is_retryable_error(e) and attempt < self._max_retries:
1236
1412
  # Calculate exponential backoff delay
1237
- delay = self._retry_backoff_factor * (2 ** attempt)
1413
+ delay = self._retry_backoff_factor * (2**attempt)
1238
1414
  if DEBUG:
1239
1415
  print(
1240
1416
  f"{time.time()}|POST /session/new - Retryable error: {str(e)}, "
@@ -1246,7 +1422,9 @@ class PDFDancer:
1246
1422
  else:
1247
1423
  # Non-retryable error or exhausted retries
1248
1424
  raise HttpClientException(
1249
- f"Failed to create blank PDF session: {str(e)}", response=None, cause=e
1425
+ f"Failed to create blank PDF session: {str(e)}",
1426
+ response=None,
1427
+ cause=e,
1250
1428
  ) from None
1251
1429
 
1252
1430
  # Should not reach here, but handle just in case
@@ -1289,7 +1467,11 @@ class PDFDancer:
1289
1467
  request_body = json.dumps(data)
1290
1468
  request_size = len(request_body.encode("utf-8"))
1291
1469
  if DEBUG:
1292
- retry_info = f" (attempt {attempt + 1}/{self._max_retries + 1})" if attempt > 0 else ""
1470
+ retry_info = (
1471
+ f" (attempt {attempt + 1}/{self._max_retries + 1})"
1472
+ if attempt > 0
1473
+ else ""
1474
+ )
1293
1475
  print(
1294
1476
  f"{time.time()}|{method} {path}{retry_info} - request size: {request_size} bytes"
1295
1477
  )
@@ -1327,9 +1509,47 @@ class PDFDancer:
1327
1509
  return response
1328
1510
 
1329
1511
  except httpx.HTTPStatusError as e:
1330
- # HTTP status errors are not retried (these are application-level errors)
1512
+ # Handle 429 (rate limit) with retry
1513
+ if e.response.status_code == 429 and attempt < self._max_retries:
1514
+ retry_after = _get_retry_after_delay(e.response)
1515
+ if retry_after is not None:
1516
+ delay = retry_after
1517
+ else:
1518
+ # Use exponential backoff if no Retry-After header
1519
+ delay = self._retry_backoff_factor * (2**attempt)
1520
+
1521
+ # Always log 429 to stderr for visibility
1522
+ print(
1523
+ f"Rate limit (429) on {method} {path} - retrying in {delay}s "
1524
+ f"(attempt {attempt + 1}/{self._max_retries})",
1525
+ file=sys.stderr,
1526
+ )
1527
+ if DEBUG:
1528
+ print(
1529
+ f"{time.time()}|{method} {path} - Rate limit exceeded (429), "
1530
+ f"retrying in {delay}s (attempt {attempt + 1}/{self._max_retries})"
1531
+ )
1532
+ time.sleep(delay)
1533
+ attempt += 1
1534
+ continue
1535
+
1536
+ # Other HTTP status errors are not retried (these are application-level errors)
1331
1537
  self._handle_authentication_error(e.response)
1332
1538
  error_message = self._extract_error_message(e.response)
1539
+
1540
+ # Raise RateLimitException for 429 after exhausting retries
1541
+ if e.response.status_code == 429:
1542
+ retry_after = _get_retry_after_delay(e.response)
1543
+ print(
1544
+ f"Rate limit (429) on {method} {path} - max retries exhausted",
1545
+ file=sys.stderr,
1546
+ )
1547
+ raise RateLimitException(
1548
+ f"Rate limit exceeded: {error_message}",
1549
+ retry_after=retry_after,
1550
+ response=e.response,
1551
+ ) from None
1552
+
1333
1553
  raise HttpClientException(
1334
1554
  f"API request failed: {error_message}", response=e.response, cause=e
1335
1555
  ) from None
@@ -1339,7 +1559,7 @@ class PDFDancer:
1339
1559
  # Check if this is a retryable error
1340
1560
  if _is_retryable_error(e) and attempt < self._max_retries:
1341
1561
  # Calculate exponential backoff delay
1342
- delay = self._retry_backoff_factor * (2 ** attempt)
1562
+ delay = self._retry_backoff_factor * (2**attempt)
1343
1563
  if DEBUG:
1344
1564
  print(
1345
1565
  f"{time.time()}|{method} {path} - Retryable error: {str(e)}, "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.22
3
+ Version: 0.2.24
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License:
@@ -207,7 +207,9 @@ License:
207
207
  limitations under the License.
208
208
 
209
209
  Project-URL: Homepage, https://www.pdfdancer.com/
210
- Project-URL: Repository, https://github.com/MenschMachine/pdfdancer-client-python
210
+ Project-URL: Documentation, https://www.pdfdancer.com/
211
+ Project-URL: Source, https://github.com/MenschMachine/pdfdancer-client-python
212
+ Project-URL: Issues, https://github.com/MenschMachine/pdfdancer-client-python/issues
211
213
  Classifier: Development Status :: 4 - Beta
212
214
  Classifier: Intended Audience :: Developers
213
215
  Classifier: License :: OSI Approved :: Apache Software License
@@ -1,16 +1,16 @@
1
- pdfdancer/__init__.py,sha256=cQcdVubVoEXEFyWXtbIzXLb0OaMNm4JkOqFQpsbMysU,2307
2
- pdfdancer/exceptions.py,sha256=7XQum7xZj19e1__UvX0jhr5bL_FeaKVByaT0I3mjS9c,1599
1
+ pdfdancer/__init__.py,sha256=8E5dsMRws_zZZpM0i8k3zc34wHauCFZOuVa5BkumuBE,2357
2
+ pdfdancer/exceptions.py,sha256=mJacmUJTPGUsB8Bo_FgfeXYkvZkH5OPJCVBfilBVmQo,2058
3
3
  pdfdancer/fingerprint.py,sha256=eL3PHPgv-knMya7s95RXg3qzzpkAA1aevxqb6tuOb34,3061
4
4
  pdfdancer/image_builder.py,sha256=MdSvZYU7-tq5HcuIpj2cfsd1iJKL9nryp87pCGlMnpM,1888
5
5
  pdfdancer/models.py,sha256=feEZc7kr5aN2QgIHpayCWksPLhxJVnukkFfe_Ri1E_w,49003
6
6
  pdfdancer/page_builder.py,sha256=ecEK0lXk-7CoWEDOftZ-GwgRfNu5h7AD_J__LNkxJwI,3092
7
7
  pdfdancer/paragraph_builder.py,sha256=yXdn2hoxpJYcUVAmSEOgoq-ApGIe9k9GWkokIIQWIJA,20489
8
8
  pdfdancer/path_builder.py,sha256=2w0LPJo1u8bSjGAbYevTtOF4VD8M9B4SLe_wSLIYJx8,23917
9
- pdfdancer/pdfdancer_v1.py,sha256=rh3W0YYWOlF_9Q598y1tRZ5JD_WcAj9IZT8IuWS-A6Y,108560
9
+ pdfdancer/pdfdancer_v1.py,sha256=pjNwBHF_tPCXv7fnjxcm-6m5g_u91773rtvz9j_W9Yg,118088
10
10
  pdfdancer/types.py,sha256=9F5LqgNVz48s0Q6lGgOcIfIbvE3vkPI_Vr4fOQqFk2k,13225
11
- pdfdancer_client_python-0.2.22.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
12
- pdfdancer_client_python-0.2.22.dist-info/licenses/NOTICE,sha256=xaC4l-IChAmtViNDie8ZWzUk0O6XRMyzOl0zLmVZ2HE,232
13
- pdfdancer_client_python-0.2.22.dist-info/METADATA,sha256=OXKDeRe_8huC1COwCQiHyvQPAt-oU405iO1VNVanXBk,24668
14
- pdfdancer_client_python-0.2.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- pdfdancer_client_python-0.2.22.dist-info/top_level.txt,sha256=ICwSVRpcCKrdBF9QlaX9Y0e_N3Nk1p7QVxadGOnbxeY,10
16
- pdfdancer_client_python-0.2.22.dist-info/RECORD,,
11
+ pdfdancer_client_python-0.2.24.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
12
+ pdfdancer_client_python-0.2.24.dist-info/licenses/NOTICE,sha256=xaC4l-IChAmtViNDie8ZWzUk0O6XRMyzOl0zLmVZ2HE,232
13
+ pdfdancer_client_python-0.2.24.dist-info/METADATA,sha256=_uluVSNqy7WP2TZQhTj97nYRtDcOmxElytYGocW71wQ,24804
14
+ pdfdancer_client_python-0.2.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ pdfdancer_client_python-0.2.24.dist-info/top_level.txt,sha256=ICwSVRpcCKrdBF9QlaX9Y0e_N3Nk1p7QVxadGOnbxeY,10
16
+ pdfdancer_client_python-0.2.24.dist-info/RECORD,,