pdfdancer-client-python 0.2.3__tar.gz → 0.2.5__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.
Files changed (42) hide show
  1. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/.gitignore +1 -0
  2. {pdfdancer_client_python-0.2.3/src/pdfdancer_client_python.egg-info → pdfdancer_client_python-0.2.5}/PKG-INFO +1 -1
  3. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/pyproject.toml +1 -1
  4. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer/pdfdancer_v1.py +26 -0
  5. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer/types.py +0 -15
  6. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5/src/pdfdancer_client_python.egg-info}/PKG-INFO +1 -1
  7. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer_client_python.egg-info/SOURCES.txt +1 -0
  8. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/__init__.py +1 -1
  9. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/test_acroform.py +5 -5
  10. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/test_form_x_objects.py +1 -1
  11. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/test_image.py +1 -1
  12. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/test_page.py +1 -1
  13. pdfdancer_client_python-0.2.5/tests/test_authentication.py +36 -0
  14. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/.github/workflows/ci.yml +0 -0
  15. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/CLAUDE.md +0 -0
  16. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/README.md +0 -0
  17. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/docs/openapi.yml +0 -0
  18. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/release.py +0 -0
  19. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/requirements-dev.txt +0 -0
  20. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/requirements.txt +0 -0
  21. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/setup.cfg +0 -0
  22. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer/__init__.py +0 -0
  23. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer/exceptions.py +0 -0
  24. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer/image_builder.py +0 -0
  25. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer/models.py +0 -0
  26. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer/paragraph_builder.py +0 -0
  27. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer_client_python.egg-info/dependency_links.txt +0 -0
  28. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer_client_python.egg-info/requires.txt +0 -0
  29. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/src/pdfdancer_client_python.egg-info/top_level.txt +0 -0
  30. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/__init__.py +0 -0
  31. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/test_line.py +0 -0
  32. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/test_paragraph.py +0 -0
  33. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/e2e/test_path.py +0 -0
  34. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/fixtures/DancingScript-Regular.ttf +0 -0
  35. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/fixtures/JetBrainsMono-Regular.ttf +0 -0
  36. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/fixtures/ObviouslyAwesome.pdf +0 -0
  37. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/fixtures/basic-paths.pdf +0 -0
  38. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/fixtures/form-xobject-example.pdf +0 -0
  39. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/fixtures/logo-80.png +0 -0
  40. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/fixtures/mixed-form-types.pdf +0 -0
  41. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/test_models.py +0 -0
  42. {pdfdancer_client_python-0.2.3 → pdfdancer_client_python-0.2.5}/tests/test_openapi_compliance.py +0 -0
@@ -6,3 +6,4 @@ dist/
6
6
  src/pdfdancer_python.egg-info
7
7
  .aider*
8
8
  src/pdfdancer_client_python.egg-info
9
+ /logs/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pdfdancer-client-python"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "Python client for PDFDancer API"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -138,6 +138,12 @@ class PDFDancer:
138
138
  env_token = os.getenv("PDFDANCER_TOKEN")
139
139
  resolved_token = env_token.strip() if env_token and env_token.strip() else None
140
140
 
141
+ if resolved_token is None:
142
+ raise ValidationException(
143
+ "Missing PDFDancer API token. Pass a token via the `token` argument "
144
+ "or set the PDFDANCER_TOKEN environment variable."
145
+ )
146
+
141
147
  env_base_url = os.getenv("PDFDANCER_BASE_URL")
142
148
  resolved_base_url = base_url or (env_base_url.strip() if env_base_url and env_base_url.strip() else None)
143
149
  if resolved_base_url is None:
@@ -259,6 +265,22 @@ class PDFDancer:
259
265
  # If JSON parsing fails, return response content or status
260
266
  return response.text or f"HTTP {response.status_code}"
261
267
 
268
+ def _handle_authentication_error(self, response: Optional[requests.Response]) -> None:
269
+ """
270
+ Translate authentication failures into a clear, actionable validation error.
271
+ """
272
+ if response is None:
273
+ return
274
+
275
+ if response.status_code in (401, 403):
276
+ details = self._extract_error_message(response)
277
+ raise ValidationException(
278
+ "Authentication with the PDFDancer API failed. "
279
+ "Confirm that your API token is valid, has not expired, and is supplied via "
280
+ "the `token` argument or the PDFDANCER_TOKEN environment variable. "
281
+ f"Server response: {details}"
282
+ )
283
+
262
284
  def _create_session(self) -> str:
263
285
  """
264
286
  Creates a new PDF processing session by uploading the PDF data.
@@ -274,6 +296,7 @@ class PDFDancer:
274
296
  timeout=self._read_timeout if self._read_timeout > 0 else None
275
297
  )
276
298
 
299
+ self._handle_authentication_error(response)
277
300
  response.raise_for_status()
278
301
  session_id = response.text.strip()
279
302
 
@@ -283,6 +306,7 @@ class PDFDancer:
283
306
  return session_id
284
307
 
285
308
  except requests.exceptions.RequestException as e:
309
+ self._handle_authentication_error(getattr(e, 'response', None))
286
310
  error_message = self._extract_error_message(getattr(e, 'response', None))
287
311
  raise HttpClientException(f"Failed to create session: {error_message}",
288
312
  response=getattr(e, 'response', None), cause=e) from None
@@ -316,10 +340,12 @@ class PDFDancer:
316
340
  except (json.JSONDecodeError, KeyError):
317
341
  pass
318
342
 
343
+ self._handle_authentication_error(response)
319
344
  response.raise_for_status()
320
345
  return response
321
346
 
322
347
  except requests.exceptions.RequestException as e:
348
+ self._handle_authentication_error(getattr(e, 'response', None))
323
349
  error_message = self._extract_error_message(getattr(e, 'response', None))
324
350
  raise HttpClientException(f"API request failed: {error_message}", response=getattr(e, 'response', None),
325
351
  cause=e) from None
@@ -31,21 +31,6 @@ class PDFObjectBase:
31
31
  self.internal_id = internal_id
32
32
  self.object_type = object_type
33
33
 
34
- # --------------------------------------------------------------
35
- # Core properties
36
- # --------------------------------------------------------------
37
- def internal_id(self) -> str:
38
- """Internal PDFDancer object identifier, e.g. 'PATH_000023'."""
39
- return self.internal_id
40
-
41
- def type(self) -> ObjectType:
42
- """Enum value representing the PDF object type."""
43
- return self.object_type
44
-
45
- def position(self) -> Position:
46
- """The geometric position of the object on its page."""
47
- return self.position
48
-
49
34
  @property
50
35
  def page_index(self) -> int:
51
36
  """Page index where this object resides."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ src/pdfdancer_client_python.egg-info/dependency_links.txt
20
20
  src/pdfdancer_client_python.egg-info/requires.txt
21
21
  src/pdfdancer_client_python.egg-info/top_level.txt
22
22
  tests/__init__.py
23
+ tests/test_authentication.py
23
24
  tests/test_models.py
24
25
  tests/test_openapi_compliance.py
25
26
  tests/e2e/__init__.py
@@ -6,7 +6,7 @@ import requests
6
6
 
7
7
 
8
8
  def _get_base_url():
9
- return os.getenv('PDFDANCER_BASE_URL', 'http://localhost:8080')
9
+ return os.getenv('PDFDANCER_BASE_URL', 'https://api.pdfdancer.com')
10
10
 
11
11
 
12
12
  def _read_token() -> str | None:
@@ -9,9 +9,9 @@ def test_find_form_fields():
9
9
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
10
10
  form_fields = pdf.select_form_fields()
11
11
  assert len(form_fields) == 10
12
- assert form_fields[0].type() == ObjectType.TEXT_FIELD
13
- assert form_fields[4].type() == ObjectType.CHECK_BOX
14
- assert form_fields[6].type() == ObjectType.RADIO_BUTTON
12
+ assert form_fields[0].object_type == ObjectType.TEXT_FIELD
13
+ assert form_fields[4].object_type == ObjectType.CHECK_BOX
14
+ assert form_fields[6].object_type == ObjectType.RADIO_BUTTON
15
15
 
16
16
  # Verify not all fields at origin
17
17
  all_at_origin = all(
@@ -26,7 +26,7 @@ def test_find_form_fields():
26
26
  first_form = pdf.page(0).select_form_fields_at(290, 460)
27
27
  assert len(first_form) == 1
28
28
  f = first_form[0]
29
- assert f.type() == ObjectType.RADIO_BUTTON
29
+ assert f.object_type == ObjectType.RADIO_BUTTON
30
30
  assert f.internal_id == "FORM_FIELD_000008"
31
31
 
32
32
 
@@ -73,7 +73,7 @@ def test_edit_form_fields():
73
73
  f = fields[0]
74
74
  assert f.name == "firstName"
75
75
  assert f.value is None
76
- assert f.type() == ObjectType.TEXT_FIELD
76
+ assert f.object_type == ObjectType.TEXT_FIELD
77
77
  assert f.internal_id == "FORM_FIELD_000001"
78
78
 
79
79
  f.edit().value("Donald Duck").apply()
@@ -11,7 +11,7 @@ def test_delete_form(tmp_path: Path):
11
11
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
12
12
  forms = pdf.select_forms()
13
13
  assert len(forms) == 17
14
- assert forms[0].type() == ObjectType.FORM_X_OBJECT
14
+ assert forms[0].object_type == ObjectType.FORM_X_OBJECT
15
15
 
16
16
  # Delete all form XObjects
17
17
  for form in forms:
@@ -13,7 +13,7 @@ def test_find_images():
13
13
  with PDFDancer.open(pdf_path, token=token, base_url=base_url, timeout=30.0) as pdf:
14
14
  images = pdf.select_images()
15
15
  assert len(images) == 3
16
- assert images[0].type() == ObjectType.IMAGE
16
+ assert images[0].object_type == ObjectType.IMAGE
17
17
 
18
18
  images_page0 = pdf.page(0).select_images()
19
19
  assert len(images_page0) == 2
@@ -9,7 +9,7 @@ def test_get_pages():
9
9
  pages = pdf.pages()
10
10
  assert pages is not None
11
11
  assert len(pages) == 12
12
- assert pages[0].type() == ObjectType.PAGE
12
+ assert pages[0].object_type == ObjectType.PAGE
13
13
 
14
14
 
15
15
  def test_get_page():
@@ -0,0 +1,36 @@
1
+ import requests
2
+ import pytest
3
+
4
+ from pdfdancer import ValidationException
5
+ from pdfdancer.pdfdancer_v1 import PDFDancer
6
+
7
+
8
+ def test_open_without_token_reports_actionable_message():
9
+ with pytest.raises(ValidationException) as exc_info:
10
+ PDFDancer.open(pdf_data=b"%PDF", token="")
11
+
12
+ message = str(exc_info.value)
13
+ assert "Missing PDFDancer API token" in message
14
+ assert "PDFDANCER_TOKEN" in message
15
+
16
+
17
+ def test_create_session_unauthorized_reports_guidance(monkeypatch):
18
+ class FakeResponse:
19
+ status_code = 401
20
+ text = "Unauthorized"
21
+
22
+ def json(self):
23
+ return {"message": "Unauthorized"}
24
+
25
+ def fake_post(self, *args, **kwargs):
26
+ return FakeResponse()
27
+
28
+ monkeypatch.setattr(requests.Session, "post", fake_post)
29
+
30
+ with pytest.raises(ValidationException) as exc_info:
31
+ PDFDancer(token="bad-token", pdf_data=b"%PDF", base_url="https://api.example.com")
32
+
33
+ message = str(exc_info.value)
34
+ assert "Authentication with the PDFDancer API failed" in message
35
+ assert "Server response: Unauthorized" in message
36
+ assert "PDFDANCER_TOKEN" in message