pdfdancer-client-python 0.2.2__tar.gz → 0.2.4__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.

Potentially problematic release.


This version of pdfdancer-client-python might be problematic. Click here for more details.

Files changed (43) hide show
  1. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/.gitignore +1 -0
  2. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/CLAUDE.md +1 -1
  3. {pdfdancer_client_python-0.2.2/src/pdfdancer_client_python.egg-info → pdfdancer_client_python-0.2.4}/PKG-INFO +14 -13
  4. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/README.md +13 -12
  5. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/pyproject.toml +1 -1
  6. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer/pdfdancer_v1.py +53 -4
  7. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4/src/pdfdancer_client_python.egg-info}/PKG-INFO +14 -13
  8. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer_client_python.egg-info/SOURCES.txt +1 -1
  9. pdfdancer_client_python-0.2.4/tests/test_authentication.py +36 -0
  10. pdfdancer_client_python-0.2.2/test.py +0 -455
  11. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/.github/workflows/ci.yml +0 -0
  12. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/docs/openapi.yml +0 -0
  13. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/release.py +0 -0
  14. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/requirements-dev.txt +0 -0
  15. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/requirements.txt +0 -0
  16. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/setup.cfg +0 -0
  17. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer/__init__.py +0 -0
  18. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer/exceptions.py +0 -0
  19. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer/image_builder.py +0 -0
  20. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer/models.py +0 -0
  21. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer/paragraph_builder.py +0 -0
  22. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer/types.py +0 -0
  23. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer_client_python.egg-info/dependency_links.txt +0 -0
  24. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer_client_python.egg-info/requires.txt +0 -0
  25. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/src/pdfdancer_client_python.egg-info/top_level.txt +0 -0
  26. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/__init__.py +0 -0
  27. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/__init__.py +0 -0
  28. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/test_acroform.py +0 -0
  29. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/test_form_x_objects.py +0 -0
  30. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/test_image.py +0 -0
  31. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/test_line.py +0 -0
  32. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/test_page.py +0 -0
  33. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/test_paragraph.py +0 -0
  34. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/e2e/test_path.py +0 -0
  35. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/fixtures/DancingScript-Regular.ttf +0 -0
  36. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/fixtures/JetBrainsMono-Regular.ttf +0 -0
  37. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/fixtures/ObviouslyAwesome.pdf +0 -0
  38. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/fixtures/basic-paths.pdf +0 -0
  39. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/fixtures/form-xobject-example.pdf +0 -0
  40. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/fixtures/logo-80.png +0 -0
  41. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/fixtures/mixed-form-types.pdf +0 -0
  42. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/tests/test_models.py +0 -0
  43. {pdfdancer_client_python-0.2.2 → pdfdancer_client_python-0.2.4}/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/
@@ -46,7 +46,7 @@ client._delete(paragraphs[0])
46
46
  client._move(images[0], new_position)
47
47
 
48
48
  # Builder pattern (mirrors Java ParagraphBuilder)
49
- paragraph = (client.paragraph_builder()
49
+ paragraph = (client._paragraph_builder()
50
50
  .from_string("Text content")
51
51
  .with_font(Font("Arial", 12))
52
52
  .with_position(Position.at_page_coordinates(0, 100, 200))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: MIT
@@ -63,7 +63,7 @@ client._delete(paragraphs[0])
63
63
  client._move(images[0], Position.at_page_coordinates(0, 100, 200))
64
64
 
65
65
  # Builder pattern (mirrors Java ParagraphBuilder)
66
- paragraph = (client.paragraph_builder()
66
+ paragraph = (client._paragraph_builder()
67
67
  .from_string("Hello World")
68
68
  .with_font(Font("Arial", 12))
69
69
  .with_color(Color(255, 0, 0))
@@ -141,25 +141,26 @@ result = client.modify_text_line(ref, "new text")
141
141
  ```
142
142
 
143
143
  ### Builder Pattern
144
+
144
145
  ```python
145
146
  # Java: client.paragraphBuilder()
146
- builder = client.paragraph_builder()
147
+ builder = client._paragraph_builder()
147
148
 
148
149
  # Fluent interface (mirrors Java ParagraphBuilder)
149
150
  paragraph = (builder
150
- .from_string("Text content") # Java: fromString()
151
- .with_font(Font("Arial", 12)) # Java: withFont()
152
- .with_color(Color(255, 0, 0)) # Java: withColor()
153
- .with_line_spacing(1.5) # Java: withLineSpacing()
154
- .with_position(position) # Java: withPosition()
155
- .build()) # Java: build()
151
+ .from_string("Text content") # Java: fromString()
152
+ .with_font(Font("Arial", 12)) # Java: withFont()
153
+ .with_color(Color(255, 0, 0)) # Java: withColor()
154
+ .with_line_spacing(1.5) # Java: withLineSpacing()
155
+ .with_position(position) # Java: withPosition()
156
+ .build()) # Java: build()
156
157
 
157
158
  # Font file registration (Java: withFont(File, double))
158
159
  paragraph = (builder
159
- .with_font_file("custom.ttf", 14.0) # Java: withFont(File, double)
160
- .from_string("Custom font text")
161
- .with_position(position)
162
- .build())
160
+ .with_font_file("custom.ttf", 14.0) # Java: withFont(File, double)
161
+ .from_string("Custom font text")
162
+ .with_position(position)
163
+ .build())
163
164
  ```
164
165
 
165
166
  ### Position API
@@ -37,7 +37,7 @@ client._delete(paragraphs[0])
37
37
  client._move(images[0], Position.at_page_coordinates(0, 100, 200))
38
38
 
39
39
  # Builder pattern (mirrors Java ParagraphBuilder)
40
- paragraph = (client.paragraph_builder()
40
+ paragraph = (client._paragraph_builder()
41
41
  .from_string("Hello World")
42
42
  .with_font(Font("Arial", 12))
43
43
  .with_color(Color(255, 0, 0))
@@ -115,25 +115,26 @@ result = client.modify_text_line(ref, "new text")
115
115
  ```
116
116
 
117
117
  ### Builder Pattern
118
+
118
119
  ```python
119
120
  # Java: client.paragraphBuilder()
120
- builder = client.paragraph_builder()
121
+ builder = client._paragraph_builder()
121
122
 
122
123
  # Fluent interface (mirrors Java ParagraphBuilder)
123
124
  paragraph = (builder
124
- .from_string("Text content") # Java: fromString()
125
- .with_font(Font("Arial", 12)) # Java: withFont()
126
- .with_color(Color(255, 0, 0)) # Java: withColor()
127
- .with_line_spacing(1.5) # Java: withLineSpacing()
128
- .with_position(position) # Java: withPosition()
129
- .build()) # Java: build()
125
+ .from_string("Text content") # Java: fromString()
126
+ .with_font(Font("Arial", 12)) # Java: withFont()
127
+ .with_color(Color(255, 0, 0)) # Java: withColor()
128
+ .with_line_spacing(1.5) # Java: withLineSpacing()
129
+ .with_position(position) # Java: withPosition()
130
+ .build()) # Java: build()
130
131
 
131
132
  # Font file registration (Java: withFont(File, double))
132
133
  paragraph = (builder
133
- .with_font_file("custom.ttf", 14.0) # Java: withFont(File, double)
134
- .from_string("Custom font text")
135
- .with_position(position)
136
- .build())
134
+ .with_font_file("custom.ttf", 14.0) # Java: withFont(File, double)
135
+ .from_string("Custom font text")
136
+ .with_position(position)
137
+ .build())
137
138
  ```
138
139
 
139
140
  ### Position API
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pdfdancer-client-python"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Python client for PDFDancer API"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -6,6 +6,7 @@ Provides session-based PDF manipulation operations with strict validation.
6
6
  """
7
7
 
8
8
  import json
9
+ import os
9
10
  from pathlib import Path
10
11
  from typing import List, Optional, Union, BinaryIO
11
12
 
@@ -116,11 +117,39 @@ class PDFDancer:
116
117
  @classmethod
117
118
  def open(cls,
118
119
  pdf_data: Union[bytes, Path, str, BinaryIO],
119
- token: str,
120
- base_url: str = "http://localhost:8080",
120
+ token: Optional[str] = None,
121
+ base_url: Optional[str] = None,
121
122
  timeout: float = 30.0) -> "PDFDancer":
123
+ """
124
+ Create a client session, falling back to environment variables when needed.
125
+
126
+ Args:
127
+ pdf_data: PDF payload supplied directly or via filesystem handles.
128
+ token: Override for the API token; falls back to `PDFDANCER_TOKEN` environement variable.
129
+ base_url: Override for the API base URL; falls back to `PDFDANCER_BASE_URL`
130
+ or defaults to `https://api.pdfdancer.com`.
131
+ timeout: HTTP read timeout in seconds.
132
+
133
+ Returns:
134
+ A ready-to-use `PDFDancer` client instance.
135
+ """
136
+ resolved_token = token.strip() if token and token.strip() else None
137
+ if resolved_token is None:
138
+ env_token = os.getenv("PDFDANCER_TOKEN")
139
+ resolved_token = env_token.strip() if env_token and env_token.strip() else None
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
+
147
+ env_base_url = os.getenv("PDFDANCER_BASE_URL")
148
+ resolved_base_url = base_url or (env_base_url.strip() if env_base_url and env_base_url.strip() else None)
149
+ if resolved_base_url is None:
150
+ resolved_base_url = "https://api.pdfdancer.com"
122
151
 
123
- return PDFDancer(token, pdf_data, base_url, timeout)
152
+ return PDFDancer(resolved_token, pdf_data, resolved_base_url, timeout)
124
153
 
125
154
  def __init__(self, token: str, pdf_data: Union[bytes, Path, str, BinaryIO],
126
155
  base_url: str, read_timeout: float = 0):
@@ -236,6 +265,22 @@ class PDFDancer:
236
265
  # If JSON parsing fails, return response content or status
237
266
  return response.text or f"HTTP {response.status_code}"
238
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
+
239
284
  def _create_session(self) -> str:
240
285
  """
241
286
  Creates a new PDF processing session by uploading the PDF data.
@@ -251,6 +296,7 @@ class PDFDancer:
251
296
  timeout=self._read_timeout if self._read_timeout > 0 else None
252
297
  )
253
298
 
299
+ self._handle_authentication_error(response)
254
300
  response.raise_for_status()
255
301
  session_id = response.text.strip()
256
302
 
@@ -260,6 +306,7 @@ class PDFDancer:
260
306
  return session_id
261
307
 
262
308
  except requests.exceptions.RequestException as e:
309
+ self._handle_authentication_error(getattr(e, 'response', None))
263
310
  error_message = self._extract_error_message(getattr(e, 'response', None))
264
311
  raise HttpClientException(f"Failed to create session: {error_message}",
265
312
  response=getattr(e, 'response', None), cause=e) from None
@@ -293,10 +340,12 @@ class PDFDancer:
293
340
  except (json.JSONDecodeError, KeyError):
294
341
  pass
295
342
 
343
+ self._handle_authentication_error(response)
296
344
  response.raise_for_status()
297
345
  return response
298
346
 
299
347
  except requests.exceptions.RequestException as e:
348
+ self._handle_authentication_error(getattr(e, 'response', None))
300
349
  error_message = self._extract_error_message(getattr(e, 'response', None))
301
350
  raise HttpClientException(f"API request failed: {error_message}", response=getattr(e, 'response', None),
302
351
  cause=e) from None
@@ -811,7 +860,7 @@ class PDFDancer:
811
860
 
812
861
  # Builder Pattern Support
813
862
 
814
- def paragraph_builder(self) -> 'ParagraphBuilder':
863
+ def _paragraph_builder(self) -> 'ParagraphBuilder':
815
864
  """
816
865
  Creates a new ParagraphBuilder for fluent paragraph construction.
817
866
  Returns:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: MIT
@@ -63,7 +63,7 @@ client._delete(paragraphs[0])
63
63
  client._move(images[0], Position.at_page_coordinates(0, 100, 200))
64
64
 
65
65
  # Builder pattern (mirrors Java ParagraphBuilder)
66
- paragraph = (client.paragraph_builder()
66
+ paragraph = (client._paragraph_builder()
67
67
  .from_string("Hello World")
68
68
  .with_font(Font("Arial", 12))
69
69
  .with_color(Color(255, 0, 0))
@@ -141,25 +141,26 @@ result = client.modify_text_line(ref, "new text")
141
141
  ```
142
142
 
143
143
  ### Builder Pattern
144
+
144
145
  ```python
145
146
  # Java: client.paragraphBuilder()
146
- builder = client.paragraph_builder()
147
+ builder = client._paragraph_builder()
147
148
 
148
149
  # Fluent interface (mirrors Java ParagraphBuilder)
149
150
  paragraph = (builder
150
- .from_string("Text content") # Java: fromString()
151
- .with_font(Font("Arial", 12)) # Java: withFont()
152
- .with_color(Color(255, 0, 0)) # Java: withColor()
153
- .with_line_spacing(1.5) # Java: withLineSpacing()
154
- .with_position(position) # Java: withPosition()
155
- .build()) # Java: build()
151
+ .from_string("Text content") # Java: fromString()
152
+ .with_font(Font("Arial", 12)) # Java: withFont()
153
+ .with_color(Color(255, 0, 0)) # Java: withColor()
154
+ .with_line_spacing(1.5) # Java: withLineSpacing()
155
+ .with_position(position) # Java: withPosition()
156
+ .build()) # Java: build()
156
157
 
157
158
  # Font file registration (Java: withFont(File, double))
158
159
  paragraph = (builder
159
- .with_font_file("custom.ttf", 14.0) # Java: withFont(File, double)
160
- .from_string("Custom font text")
161
- .with_position(position)
162
- .build())
160
+ .with_font_file("custom.ttf", 14.0) # Java: withFont(File, double)
161
+ .from_string("Custom font text")
162
+ .with_position(position)
163
+ .build())
163
164
  ```
164
165
 
165
166
  ### Position API
@@ -5,7 +5,6 @@ pyproject.toml
5
5
  release.py
6
6
  requirements-dev.txt
7
7
  requirements.txt
8
- test.py
9
8
  .github/workflows/ci.yml
10
9
  docs/openapi.yml
11
10
  src/pdfdancer/__init__.py
@@ -21,6 +20,7 @@ src/pdfdancer_client_python.egg-info/dependency_links.txt
21
20
  src/pdfdancer_client_python.egg-info/requires.txt
22
21
  src/pdfdancer_client_python.egg-info/top_level.txt
23
22
  tests/__init__.py
23
+ tests/test_authentication.py
24
24
  tests/test_models.py
25
25
  tests/test_openapi_compliance.py
26
26
  tests/e2e/__init__.py
@@ -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
@@ -1,455 +0,0 @@
1
- """
2
- Comprehensive test showcasing all PDFDancer Python API features.
3
- This file demonstrates the complete functionality of the pdfdancer library.
4
- """
5
-
6
- from pathlib import Path
7
-
8
- from pdfdancer import (
9
- ClientV1, Font, Position, Color, Image, Paragraph, ObjectType,
10
- Point, BoundingRect, PositionMode, ShapeType,
11
- FontNotFoundException
12
- )
13
-
14
-
15
- # noinspection PyUnusedLocal
16
- def find_operations(client):
17
- """Test all find operations for different object types."""
18
- # Find all paragraphs
19
- paragraphs = client._find_paragraphs()
20
-
21
- # Find images
22
- images = client._find_images()
23
-
24
- # Find forms
25
- forms = client._find_form_x_objects()
26
-
27
- # Find paths
28
- paths = client._find_paths()
29
-
30
- # Find text lines
31
- text_lines = client._find_text_lines()
32
-
33
- # Find with specific object type and position
34
- specific_position = Position.at_page_coordinates(0, 100, 100)
35
- paragraphs_at_pos = client._find(ObjectType.PARAGRAPH, specific_position)
36
-
37
- return paragraphs, text_lines # Keep only what's needed for object_manipulation
38
-
39
-
40
- # noinspection PyUnusedLocal
41
- def page_management(client):
42
- """Test page management operations."""
43
- # Get all pages
44
- pages = client.get_pages()
45
-
46
- # Get specific page
47
- first_page = None
48
- if pages:
49
- first_page = client._get_page(0)
50
-
51
-
52
- # noinspection PyUnusedLocal
53
- def position_system(client):
54
- """Test position and coordinate system operations."""
55
- # Different ways to create positions
56
- page_pos = Position.at_page(0)
57
- coord_pos = Position.at_page_coordinates(0, 50, 100)
58
-
59
- # Position manipulation
60
- moved_pos = coord_pos.copy()
61
- moved_pos.move_x(25.0)
62
- moved_pos.move_y(15.0)
63
-
64
- # Point and BoundingRect
65
- point = Point(10.0, 20.0)
66
- bounding_rect = BoundingRect(0.0, 0.0, 100.0, 50.0)
67
-
68
-
69
- # noinspection PyUnusedLocal
70
- def font_management(client):
71
- """Test font creation and management operations."""
72
- # Create fonts
73
- arial_font = Font("Arial", 12.0)
74
- times_font = Font("Times New Roman", 14.0)
75
-
76
- # Find fonts in document
77
- found_fonts = None
78
- try:
79
- found_fonts = client.find_fonts("Arial", 12)
80
- except FontNotFoundException as e:
81
- pass
82
- except Exception as e:
83
- pass
84
-
85
-
86
- # noinspection PyUnusedLocal
87
- def color_system(client):
88
- """Test color creation and management operations."""
89
- # Create colors
90
- red_color = Color(255, 0, 0)
91
- blue_color = Color(0, 0, 255, 128) # With alpha
92
-
93
-
94
- # noinspection PyUnusedLocal
95
- def paragraph_operations(client):
96
- """Test paragraph builder and manipulation operations."""
97
- arial_font = Font("Arial", 12.0)
98
- arial_font_large = Font("Arial", 14.0) # Use Arial instead of Times New Roman
99
- red_color = Color(255, 0, 0)
100
- blue_color = Color(0, 0, 255, 128)
101
-
102
- # Basic paragraph creation
103
- basic_paragraph = (client.paragraph_builder()
104
- .from_string("Basic paragraph with default settings")
105
- .with_font(arial_font)
106
- .with_position(Position.at_page_coordinates(0, 50, 200))
107
- .build())
108
- client._add_paragraph(basic_paragraph)
109
-
110
- # Advanced paragraph with all features
111
- advanced_paragraph = (client.paragraph_builder()
112
- .from_string("Advanced paragraph\nwith multiple lines\nand styling", red_color)
113
- .with_font(arial_font_large)
114
- .with_line_spacing(1.5)
115
- .with_color(blue_color)
116
- .with_position(Position.at_page_coordinates(0, 50, 250))
117
- .build())
118
- client._add_paragraph(advanced_paragraph)
119
-
120
-
121
- # noinspection PyUnusedLocal
122
- def object_manipulation(client, paragraphs, text_lines):
123
- """Test object manipulation operations including delete, move, and modify."""
124
- arial_font = Font("Arial", 14.0) # Use Arial instead of Times New Roman
125
- red_color = Color(255, 0, 0)
126
-
127
- if paragraphs:
128
- # Delete operation
129
- first_paragraph = paragraphs[0]
130
- client._delete(first_paragraph)
131
-
132
- # Move operation (if we have more paragraphs)
133
- if len(paragraphs) > 1:
134
- second_paragraph = paragraphs[1]
135
- new_position = Position.at_page_coordinates(0, 100, 300)
136
- client._move(second_paragraph, new_position)
137
-
138
- # Modify operations
139
- if len(paragraphs) > 2:
140
- third_paragraph = paragraphs[2]
141
-
142
- # Modify with new paragraph object
143
- modified_paragraph = Paragraph(
144
- position=Position.at_page_coordinates(0, 50, 350),
145
- text_lines=["Modified paragraph text"],
146
- font=arial_font,
147
- color=red_color
148
- )
149
- client._modify_paragraph(third_paragraph, modified_paragraph)
150
-
151
- # Modify with just text
152
- if len(paragraphs) > 3:
153
- fourth_paragraph = paragraphs[3]
154
- client._modify_paragraph(fourth_paragraph, "Simple text modification")
155
-
156
- # Text line modification
157
- if text_lines:
158
- client.modify_text_line(text_lines[0], "Modified text line")
159
-
160
-
161
- # noinspection PyUnusedLocal
162
- def text_line_operations(client):
163
- """Test text line specific operations and showcases."""
164
- # Find all text lines in document
165
- all_text_lines = client._find_text_lines()
166
-
167
- # Find text lines at specific position
168
- specific_position = Position.at_page_coordinates(0, 100, 100)
169
- text_lines_at_position = client._find_text_lines(specific_position)
170
-
171
- # Find text lines on entire page
172
- page_position = Position.at_page(0)
173
- text_lines_on_page = client._find_text_lines(page_position)
174
-
175
- # Demonstrate text line modification if text lines are found
176
- modified_text_line = None
177
- if all_text_lines:
178
- first_text_line = all_text_lines[0]
179
-
180
- # Get text line properties via ObjectRef
181
- text_line_id = first_text_line.get_internal_id()
182
- text_line_type = first_text_line.get_type()
183
- text_line_position = first_text_line.get_position()
184
-
185
- # Modify text line content
186
- client.modify_text_line(first_text_line, "Modified text line content")
187
- modified_text_line = first_text_line
188
-
189
- # Demonstrate position modification for text lines
190
- if len(all_text_lines) > 1:
191
- second_text_line = all_text_lines[1]
192
- new_position = Position.at_page_coordinates(0, 120, 150)
193
- client._move(second_text_line, new_position)
194
-
195
-
196
- # noinspection PyUnusedLocal
197
- def form_operations(client):
198
- """Test form handling operations."""
199
- # Find all forms in document
200
- all_forms = client._find_form_x_objects()
201
-
202
- # Find forms at specific position
203
- specific_position = Position.at_page_coordinates(0, 150, 200)
204
- forms_at_position = client._find_form_x_objects(specific_position)
205
-
206
- # Find forms on entire page
207
- page_position = Position.at_page(0)
208
- forms_on_page = client._find_form_x_objects(page_position)
209
-
210
- # Demonstrate form manipulation if forms are found
211
- manipulated_form = None
212
- if all_forms:
213
- first_form = all_forms[0]
214
-
215
- # Get form properties via ObjectRef
216
- form_id = first_form.get_internal_id()
217
- form_type = first_form.get_type()
218
- form_position = first_form.get_position()
219
-
220
- # Demonstrate position modification for forms
221
- if len(all_forms) > 1:
222
- second_form = all_forms[1]
223
- new_position = Position.at_page_coordinates(0, 180, 250)
224
- client._move(second_form, new_position)
225
- manipulated_form = second_form
226
-
227
- # Demonstrate deletion (use last form to preserve others for testing)
228
- if len(all_forms) > 2:
229
- last_form = all_forms[-1]
230
- client._delete(last_form)
231
-
232
-
233
- # noinspection PyUnusedLocal
234
- def path_operations(client):
235
- """Test path handling operations."""
236
- # Find all paths in document
237
- all_paths = client._find_paths()
238
-
239
- # Find paths at specific position
240
- specific_position = Position.at_page_coordinates(0, 100, 150)
241
- paths_at_position = client._find_paths(specific_position)
242
-
243
- # Find paths on entire page
244
- page_position = Position.at_page(0)
245
- paths_on_page = client._find_paths(page_position)
246
-
247
- # Demonstrate path manipulation if paths are found
248
- manipulated_path = None
249
- if all_paths:
250
- first_path = all_paths[0]
251
-
252
- # Get path properties via ObjectRef
253
- path_id = first_path.get_internal_id()
254
- path_type = first_path.get_type()
255
- path_position = first_path.get_position()
256
-
257
- # Demonstrate position modification for paths
258
- if len(all_paths) > 1:
259
- second_path = all_paths[1]
260
- new_position = Position.at_page_coordinates(0, 120, 180)
261
- client._move(second_path, new_position)
262
- manipulated_path = second_path
263
-
264
- # Demonstrate deletion (use last path to preserve others for testing)
265
- if len(all_paths) > 2:
266
- last_path = all_paths[-1]
267
- client._delete(last_path)
268
-
269
-
270
- # noinspection PyUnusedLocal
271
- def register_font_operations(client):
272
- """Test font registration operations using JetBrainsMono-Regular.ttf."""
273
- # Register font using file path
274
- font_path = Path("tests/fixtures/JetBrainsMono-Regular.ttf")
275
- registered_font_name = client.register_font(font_path)
276
-
277
- # Create a Font object using the registered font
278
- jetbrains_font = Font(registered_font_name, 14.0)
279
-
280
- # Use the registered font in a paragraph
281
- paragraph_with_custom_font = (client.paragraph_builder()
282
- .from_string("This text uses JetBrains Mono font")
283
- .with_font(jetbrains_font)
284
- .with_position(Position.at_page_coordinates(0, 50, 450))
285
- .build())
286
- client._add_paragraph(paragraph_with_custom_font)
287
-
288
- # Register font using bytes (alternative method)
289
- with open(font_path, 'rb') as f:
290
- font_bytes = f.read()
291
- registered_font_name_bytes = client.register_font(font_bytes)
292
-
293
- # Create another Font object using the bytes-registered font
294
- jetbrains_font_bytes = Font(registered_font_name_bytes, 16.0)
295
-
296
-
297
- # noinspection PyUnusedLocal
298
- def image_operations(client):
299
- """Test image handling operations using logo-80.png."""
300
- # Load actual image file
301
- image_path = Path("tests/fixtures/logo-80.png")
302
- with open(image_path, 'rb') as f:
303
- image_bytes = f.read()
304
-
305
- # Create image object with real image data
306
- logo_image = Image(
307
- position=Position.at_page_coordinates(0, 200, 400),
308
- format="image/png",
309
- width=80.0,
310
- height=80.0,
311
- data=image_bytes
312
- )
313
-
314
- # Add image to PDF
315
- client._add_image(logo_image)
316
-
317
- # Find images in document
318
- found_images = client._find_images()
319
-
320
- # Demonstrate image manipulation if images are found
321
- if found_images:
322
- first_image = found_images[0]
323
-
324
- # Get image properties via ObjectRef
325
- image_id = first_image.get_internal_id()
326
- image_type = first_image.get_type()
327
- image_position = first_image.get_position()
328
-
329
- # Move image to a new position
330
- new_image_position = Position.at_page_coordinates(0, 250, 450)
331
- client._move(first_image, new_image_position)
332
-
333
-
334
- # noinspection PyUnusedLocal
335
- def objectref_operations(client, paragraphs):
336
- """Test ObjectRef operations."""
337
- if paragraphs and len(paragraphs) > 1: # Use remaining paragraphs after deletion
338
- obj_ref = paragraphs[1]
339
- internal_id = obj_ref.get_internal_id()
340
- obj_type = obj_ref.get_type()
341
- position = obj_ref.get_position()
342
- obj_dict = obj_ref.to_dict()
343
-
344
- # Set new position
345
- new_ref_position = Position.at_page_coordinates(0, 75, 125)
346
- obj_ref.set_position(new_ref_position)
347
- updated_position = obj_ref.get_position()
348
-
349
-
350
- # noinspection PyUnusedLocal
351
- def enumerations(client):
352
- """Test enumeration demonstrations."""
353
- object_types = list(ObjectType)
354
- position_modes = list(PositionMode)
355
- shape_types = list(ShapeType)
356
-
357
-
358
- # noinspection PyUnusedLocal
359
- def error_handling(client):
360
- """Test error handling demonstration."""
361
- exception_caught = None
362
- try:
363
- # This might trigger a font not found error
364
- client.find_fonts("NonExistentFont", 12)
365
- except FontNotFoundException as e:
366
- exception_caught = e
367
- except Exception as e:
368
- exception_caught = e
369
-
370
-
371
- # noinspection PyUnusedLocal
372
- def pdf_operations(client):
373
- """Test PDF operations."""
374
- # Get PDF data
375
- pdf_data = client.get_pdf_file()
376
-
377
- # Save PDF (original functionality + comprehensive output)
378
- output_path = "comprehensive_output.pdf"
379
- client.save_pdf(output_path)
380
- client.save_pdf("output.pdf") # Keep original output file
381
-
382
- # Verify file exists
383
- file_exists = Path(output_path).exists()
384
- file_size = Path(output_path).stat().st_size if file_exists else 0
385
-
386
-
387
- def context_management():
388
- """Test context manager capabilities."""
389
- with open("jwt-token-mlahr-20250829-160417.txt", "r", encoding="utf-8") as f:
390
- token = f.read()
391
-
392
- # Context manager automatically handles session lifecycle
393
- with ClientV1(token=token, pdf_data="tests/fixtures/ObviouslyAwesome.pdf") as client:
394
- paragraphs = client.find_paragraphs()
395
-
396
- # Builder pattern works seamlessly inside context (original functionality preserved)
397
- paragraph = (client.paragraph_builder()
398
- .from_string("Context managed")
399
- .with_font(Font("Arial", 12))
400
- .with_position(Position.at_page_coordinates(0, 50, 10))
401
- .build())
402
-
403
- client._add_paragraph(paragraph)
404
-
405
- return len(paragraphs)
406
-
407
-
408
- def advanced_positioning():
409
- """Test advanced positioning features."""
410
- # Create positions using different methods
411
- positions = [
412
- Position.at_page(0),
413
- Position.at_page_coordinates(1, 100, 200),
414
- Position.at_page_coordinates(2, 150, 250)
415
- ]
416
-
417
- modified_positions = []
418
- for pos in positions:
419
- # Demonstrate position copying and modification
420
- copied_pos = pos.copy()
421
- copied_pos.move_x(25).move_y(35)
422
- modified_positions.append(copied_pos)
423
-
424
- return positions, modified_positions
425
-
426
-
427
- def main():
428
- """Main function demonstrating all API features."""
429
- with open("jwt-token-mlahr-20250829-160417.txt", "r", encoding="utf-8") as f:
430
- token = f.read()
431
-
432
- with ClientV1(token=token, pdf_data="tests/fixtures/ObviouslyAwesome.pdf", read_timeout=30.0) as client:
433
- # Execute all test operations
434
- paragraphs, text_lines = find_operations(client)
435
- page_management(client)
436
- position_system(client)
437
- font_management(client)
438
- register_font_operations(client)
439
- color_system(client)
440
- paragraph_operations(client)
441
- object_manipulation(client, paragraphs, text_lines)
442
- text_line_operations(client)
443
- form_operations(client)
444
- path_operations(client)
445
- image_operations(client)
446
- objectref_operations(client, paragraphs)
447
- enumerations(client)
448
- error_handling(client)
449
- pdf_operations(client)
450
-
451
-
452
- if __name__ == "__main__":
453
- main()
454
- context_management()
455
- advanced_positioning()