pdfdancer-client-python 0.1.1__tar.gz → 0.2.2__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 (53) hide show
  1. pdfdancer_client_python-0.2.2/.github/workflows/ci.yml +40 -0
  2. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/.gitignore +2 -1
  3. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/CLAUDE.md +8 -8
  4. {pdfdancer_client_python-0.1.1/src/pdfdancer_client_python.egg-info → pdfdancer_client_python-0.2.2}/PKG-INFO +31 -35
  5. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/README.md +26 -29
  6. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/pyproject.toml +8 -9
  7. pdfdancer_client_python-0.2.2/release.py +272 -0
  8. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/__init__.py +5 -3
  9. pdfdancer_client_python-0.2.2/src/pdfdancer/image_builder.py +30 -0
  10. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/models.py +67 -10
  11. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/paragraph_builder.py +15 -12
  12. pdfdancer_client_python-0.1.1/src/pdfdancer/client_v1.py → pdfdancer_client_python-0.2.2/src/pdfdancer/pdfdancer_v1.py +237 -56
  13. pdfdancer_client_python-0.2.2/src/pdfdancer/types.py +263 -0
  14. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2/src/pdfdancer_client_python.egg-info}/PKG-INFO +31 -35
  15. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/SOURCES.txt +9 -7
  16. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/test.py +56 -56
  17. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/e2e/__init__.py +1 -1
  18. pdfdancer_client_python-0.2.2/tests/e2e/test_acroform.py +83 -0
  19. pdfdancer_client_python-0.2.2/tests/e2e/test_form_x_objects.py +36 -0
  20. pdfdancer_client_python-0.2.2/tests/e2e/test_image.py +91 -0
  21. pdfdancer_client_python-0.2.2/tests/e2e/test_line.py +83 -0
  22. pdfdancer_client_python-0.2.2/tests/e2e/test_page.py +33 -0
  23. pdfdancer_client_python-0.2.2/tests/e2e/test_paragraph.py +169 -0
  24. pdfdancer_client_python-0.2.2/tests/e2e/test_path.py +69 -0
  25. pdfdancer_client_python-0.2.2/tests/fixtures/DancingScript-Regular.ttf +0 -0
  26. pdfdancer_client_python-0.2.2/tests/fixtures/form-xobject-example.pdf +0 -0
  27. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/test_models.py +24 -48
  28. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/test_openapi_compliance.py +4 -4
  29. pdfdancer_client_python-0.1.1/demo.py +0 -365
  30. pdfdancer_client_python-0.1.1/tests/e2e/test_form.py +0 -33
  31. pdfdancer_client_python-0.1.1/tests/e2e/test_image.py +0 -84
  32. pdfdancer_client_python-0.1.1/tests/e2e/test_line.py +0 -92
  33. pdfdancer_client_python-0.1.1/tests/e2e/test_page.py +0 -29
  34. pdfdancer_client_python-0.1.1/tests/e2e/test_paragraph.py +0 -172
  35. pdfdancer_client_python-0.1.1/tests/e2e/test_path.py +0 -56
  36. pdfdancer_client_python-0.1.1/tests/test_client_v1.py +0 -444
  37. pdfdancer_client_python-0.1.1/tests/test_error_extraction.py +0 -117
  38. pdfdancer_client_python-0.1.1/tests/test_exception_suppression.py +0 -53
  39. pdfdancer_client_python-0.1.1/tests/test_paragraph_builder.py +0 -324
  40. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/docs/openapi.yml +0 -0
  41. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/requirements-dev.txt +0 -0
  42. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/requirements.txt +0 -0
  43. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/setup.cfg +0 -0
  44. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/exceptions.py +0 -0
  45. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/dependency_links.txt +0 -0
  46. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/requires.txt +0 -0
  47. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/top_level.txt +0 -0
  48. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/__init__.py +0 -0
  49. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/JetBrainsMono-Regular.ttf +0 -0
  50. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/ObviouslyAwesome.pdf +0 -0
  51. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/basic-paths.pdf +0 -0
  52. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/logo-80.png +0 -0
  53. {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/mixed-form-types.pdf +0 -0
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, staging, develop, development, dev ]
6
+ pull_request:
7
+ branches: [ main, staging, develop, development, dev ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ['3.9', '3.10', '3.11', '3.12']
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v4
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Create virtual environment
25
+ run: python -m venv venv
26
+
27
+ - name: Install dependencies
28
+ run: |
29
+ venv/bin/pip install --upgrade pip
30
+ venv/bin/pip install -e .
31
+ venv/bin/pip install -r requirements-dev.txt
32
+
33
+ - name: Run tests
34
+ run: venv/bin/python -m pytest tests/ -v --ignore=tests/e2e/
35
+
36
+ - name: Build distribution packages
37
+ run: venv/bin/python -m build
38
+
39
+ - name: Validate packages
40
+ run: venv/bin/python -m twine check dist/*
@@ -1,7 +1,8 @@
1
1
  output.pdf
2
2
  .idea
3
3
  comprehensive_output.pdf
4
- jwt-token-mlahr-20250829-160417.txt
4
+ jwt-token*.txt
5
5
  dist/
6
6
  src/pdfdancer_python.egg-info
7
7
  .aider*
8
+ src/pdfdancer_client_python.egg-info
@@ -38,19 +38,19 @@ The client is a pure manual implementation that closely mirrors the Java client:
38
38
  client = ClientV1(token="jwt-token", pdf_data="document.pdf")
39
39
 
40
40
  # Find operations (mirrors Java findParagraphs, findImages, etc.)
41
- paragraphs = client.find_paragraphs(position)
42
- images = client.find_images(position)
41
+ paragraphs = client._find_paragraphs(position)
42
+ images = client._find_images(position)
43
43
 
44
44
  # Manipulation operations (mirrors Java delete, move, etc.)
45
- client.delete(paragraphs[0])
46
- client.move(images[0], new_position)
45
+ client._delete(paragraphs[0])
46
+ client._move(images[0], new_position)
47
47
 
48
48
  # Builder pattern (mirrors Java ParagraphBuilder)
49
49
  paragraph = (client.paragraph_builder()
50
- .from_string("Text content")
51
- .with_font(Font("Arial", 12))
52
- .with_position(Position.on_page_coordinates(0, 100, 200))
53
- .build())
50
+ .from_string("Text content")
51
+ .with_font(Font("Arial", 12))
52
+ .with_position(Position.at_page_coordinates(0, 100, 200))
53
+ .build())
54
54
 
55
55
  # Context manager support (Python enhancement)
56
56
  with ClientV1(token="jwt-token", pdf_data=pdf_file) as client:
@@ -1,19 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.1.1
3
+ Version: 0.2.2
4
4
  Summary: Python client for PDFDancer API
5
- Author-email: TFC <info@example.com>
5
+ Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/tfc/pdfdancer-python
8
- Project-URL: Repository, https://github.com/tfc/pdfdancer-python.git
7
+ Project-URL: Homepage, https://www.pdfdancer.com/
8
+ Project-URL: Repository, https://github.com/MenschMachine/pdfdancer-client-python
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
12
  Classifier: Programming Language :: Python :: 3.9
15
13
  Classifier: Programming Language :: Python :: 3.10
16
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
17
16
  Description-Content-Type: text/markdown
18
17
  Requires-Dist: requests>=2.25.0
19
18
  Requires-Dist: pydantic>=1.8.0
@@ -57,21 +56,21 @@ client = ClientV1(token="your-jwt-token", pdf_data="document.pdf")
57
56
 
58
57
  # Find operations (mirrors Java client methods)
59
58
  paragraphs = client.find_paragraphs(None)
60
- images = client.find_images(Position.from_page_index(0))
59
+ images = client.find_images(Position.at_page(0))
61
60
 
62
61
  # Manipulation operations (mirrors Java client methods)
63
- client.delete(paragraphs[0])
64
- client.move(images[0], Position.on_page_coordinates(0, 100, 200))
62
+ client._delete(paragraphs[0])
63
+ client._move(images[0], Position.at_page_coordinates(0, 100, 200))
65
64
 
66
65
  # Builder pattern (mirrors Java ParagraphBuilder)
67
66
  paragraph = (client.paragraph_builder()
68
67
  .from_string("Hello World")
69
68
  .with_font(Font("Arial", 12))
70
69
  .with_color(Color(255, 0, 0))
71
- .with_position(Position.from_page_index(0))
70
+ .with_position(Position.at_page(0))
72
71
  .build())
73
72
 
74
- client.add_paragraph(paragraph)
73
+ client._add_paragraph(paragraph)
75
74
 
76
75
  # Save result (mirrors Java savePDF)
77
76
  client.save_pdf("output.pdf")
@@ -85,7 +84,7 @@ from pdfdancer import ClientV1
85
84
  # Automatic resource management
86
85
  with ClientV1(token="jwt-token", pdf_data="input.pdf") as client:
87
86
  paragraphs = client.find_paragraphs(None)
88
- client.delete(paragraphs[0])
87
+ client._delete(paragraphs[0])
89
88
  client.save_pdf("output.pdf")
90
89
  # Session automatically cleaned up
91
90
  ```
@@ -105,37 +104,39 @@ client = ClientV1(token="jwt-token", pdf_data=pdf_file, base_url="https://api.se
105
104
  ```
106
105
 
107
106
  ### Find Operations
107
+
108
108
  ```python
109
109
  # Generic find (Java: client.find())
110
- objects = client.find(ObjectType.PARAGRAPH, position)
110
+ objects = client._find(ObjectType.PARAGRAPH, position)
111
111
 
112
112
  # Specific finders (Java: client.findParagraphs(), etc.)
113
- paragraphs = client.find_paragraphs(position)
114
- images = client.find_images(position)
115
- forms = client.find_forms(position)
116
- paths = client.find_paths(position)
117
- text_lines = client.find_text_lines(position)
113
+ paragraphs = client._find_paragraphs(position)
114
+ images = client._find_images(position)
115
+ forms = client._find_form_x_objects(position)
116
+ paths = client._find_paths(position)
117
+ text_lines = client._find_text_lines(position)
118
118
 
119
119
  # Page operations (Java: client.getPages(), client.getPage())
120
120
  pages = client.get_pages()
121
- page = client.get_page(1) # 1-based indexing
121
+ page = client._get_page(1) # 1-based indexing
122
122
  ```
123
123
 
124
124
  ### Manipulation Operations
125
+
125
126
  ```python
126
127
  # Delete (Java: client.delete(), client.deletePage())
127
- result = client.delete(object_ref)
128
- result = client.delete_page(page_ref)
128
+ result = client._delete(object_ref)
129
+ result = client._delete_page(page_ref)
129
130
 
130
131
  # Move (Java: client.move())
131
- result = client.move(object_ref, new_position)
132
+ result = client._move(object_ref, new_position)
132
133
 
133
134
  # Add (Java: client.addImage(), client.addParagraph())
134
- result = client.add_image(image, position)
135
- result = client.add_paragraph(paragraph)
135
+ result = client._add_image(image, position)
136
+ result = client._add_paragraph(paragraph)
136
137
 
137
138
  # Modify (Java: client.modifyParagraph(), client.modifyTextLine())
138
- result = client.modify_paragraph(ref, new_paragraph)
139
+ result = client._modify_paragraph(ref, new_paragraph)
139
140
  result = client.modify_text_line(ref, "new text")
140
141
  ```
141
142
 
@@ -167,12 +168,12 @@ paragraph = (builder
167
168
  from pdfdancer import Position
168
169
 
169
170
  # Factory methods (Java: Position.fromPageNumber(), Position.onPageCoordinates())
170
- position = Position.from_page_index(0)
171
- position = Position.on_page_coordinates(0, 100, 200)
171
+ position = Position.at_page(0)
172
+ position = Position.at_page_coordinates(0, 100, 200)
172
173
 
173
174
  # Coordinate access (Java: position.getX(), position.getY())
174
- x = position.get_x()
175
- y = position.get_y()
175
+ x = position.x()
176
+ y = position.y()
176
177
 
177
178
  # Movement (Java: position.moveX(), position.moveY())
178
179
  position.move_x(50.0)
@@ -237,7 +238,7 @@ font = Font(name="Arial", size=12.0)
237
238
  color = Color(r=255, g=128, b=0)
238
239
 
239
240
  # Position with bounding rectangle (Java: Position, BoundingRect)
240
- position = Position.on_page_coordinates(page=0, x=100.0, y=200.0)
241
+ position = Position.at_page_coordinates(page=0, x=100.0, y=200.0)
241
242
  ```
242
243
 
243
244
  ## Development
@@ -261,11 +262,6 @@ python -m pytest tests/test_paragraph_builder.py -v
261
262
  python -m pytest tests/test_models.py -v
262
263
  ```
263
264
 
264
- ### Run Demo
265
- ```bash
266
- python demo.py
267
- ```
268
-
269
265
  ### Build Package
270
266
  ```bash
271
267
  python -m build
@@ -30,21 +30,21 @@ client = ClientV1(token="your-jwt-token", pdf_data="document.pdf")
30
30
 
31
31
  # Find operations (mirrors Java client methods)
32
32
  paragraphs = client.find_paragraphs(None)
33
- images = client.find_images(Position.from_page_index(0))
33
+ images = client.find_images(Position.at_page(0))
34
34
 
35
35
  # Manipulation operations (mirrors Java client methods)
36
- client.delete(paragraphs[0])
37
- client.move(images[0], Position.on_page_coordinates(0, 100, 200))
36
+ client._delete(paragraphs[0])
37
+ client._move(images[0], Position.at_page_coordinates(0, 100, 200))
38
38
 
39
39
  # Builder pattern (mirrors Java ParagraphBuilder)
40
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))
44
- .with_position(Position.from_page_index(0))
44
+ .with_position(Position.at_page(0))
45
45
  .build())
46
46
 
47
- client.add_paragraph(paragraph)
47
+ client._add_paragraph(paragraph)
48
48
 
49
49
  # Save result (mirrors Java savePDF)
50
50
  client.save_pdf("output.pdf")
@@ -58,7 +58,7 @@ from pdfdancer import ClientV1
58
58
  # Automatic resource management
59
59
  with ClientV1(token="jwt-token", pdf_data="input.pdf") as client:
60
60
  paragraphs = client.find_paragraphs(None)
61
- client.delete(paragraphs[0])
61
+ client._delete(paragraphs[0])
62
62
  client.save_pdf("output.pdf")
63
63
  # Session automatically cleaned up
64
64
  ```
@@ -78,37 +78,39 @@ client = ClientV1(token="jwt-token", pdf_data=pdf_file, base_url="https://api.se
78
78
  ```
79
79
 
80
80
  ### Find Operations
81
+
81
82
  ```python
82
83
  # Generic find (Java: client.find())
83
- objects = client.find(ObjectType.PARAGRAPH, position)
84
+ objects = client._find(ObjectType.PARAGRAPH, position)
84
85
 
85
86
  # Specific finders (Java: client.findParagraphs(), etc.)
86
- paragraphs = client.find_paragraphs(position)
87
- images = client.find_images(position)
88
- forms = client.find_forms(position)
89
- paths = client.find_paths(position)
90
- text_lines = client.find_text_lines(position)
87
+ paragraphs = client._find_paragraphs(position)
88
+ images = client._find_images(position)
89
+ forms = client._find_form_x_objects(position)
90
+ paths = client._find_paths(position)
91
+ text_lines = client._find_text_lines(position)
91
92
 
92
93
  # Page operations (Java: client.getPages(), client.getPage())
93
94
  pages = client.get_pages()
94
- page = client.get_page(1) # 1-based indexing
95
+ page = client._get_page(1) # 1-based indexing
95
96
  ```
96
97
 
97
98
  ### Manipulation Operations
99
+
98
100
  ```python
99
101
  # Delete (Java: client.delete(), client.deletePage())
100
- result = client.delete(object_ref)
101
- result = client.delete_page(page_ref)
102
+ result = client._delete(object_ref)
103
+ result = client._delete_page(page_ref)
102
104
 
103
105
  # Move (Java: client.move())
104
- result = client.move(object_ref, new_position)
106
+ result = client._move(object_ref, new_position)
105
107
 
106
108
  # Add (Java: client.addImage(), client.addParagraph())
107
- result = client.add_image(image, position)
108
- result = client.add_paragraph(paragraph)
109
+ result = client._add_image(image, position)
110
+ result = client._add_paragraph(paragraph)
109
111
 
110
112
  # Modify (Java: client.modifyParagraph(), client.modifyTextLine())
111
- result = client.modify_paragraph(ref, new_paragraph)
113
+ result = client._modify_paragraph(ref, new_paragraph)
112
114
  result = client.modify_text_line(ref, "new text")
113
115
  ```
114
116
 
@@ -140,12 +142,12 @@ paragraph = (builder
140
142
  from pdfdancer import Position
141
143
 
142
144
  # Factory methods (Java: Position.fromPageNumber(), Position.onPageCoordinates())
143
- position = Position.from_page_index(0)
144
- position = Position.on_page_coordinates(0, 100, 200)
145
+ position = Position.at_page(0)
146
+ position = Position.at_page_coordinates(0, 100, 200)
145
147
 
146
148
  # Coordinate access (Java: position.getX(), position.getY())
147
- x = position.get_x()
148
- y = position.get_y()
149
+ x = position.x()
150
+ y = position.y()
149
151
 
150
152
  # Movement (Java: position.moveX(), position.moveY())
151
153
  position.move_x(50.0)
@@ -210,7 +212,7 @@ font = Font(name="Arial", size=12.0)
210
212
  color = Color(r=255, g=128, b=0)
211
213
 
212
214
  # Position with bounding rectangle (Java: Position, BoundingRect)
213
- position = Position.on_page_coordinates(page=0, x=100.0, y=200.0)
215
+ position = Position.at_page_coordinates(page=0, x=100.0, y=200.0)
214
216
  ```
215
217
 
216
218
  ## Development
@@ -234,11 +236,6 @@ python -m pytest tests/test_paragraph_builder.py -v
234
236
  python -m pytest tests/test_models.py -v
235
237
  ```
236
238
 
237
- ### Run Demo
238
- ```bash
239
- python demo.py
240
- ```
241
-
242
239
  ### Build Package
243
240
  ```bash
244
241
  python -m build
@@ -4,22 +4,21 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pdfdancer-client-python"
7
- version = "0.1.1"
7
+ version = "0.2.2"
8
8
  description = "Python client for PDFDancer API"
9
9
  readme = "README.md"
10
10
  authors = [
11
- {name = "TFC", email = "info@example.com"}
11
+ { name = "The Famous Cat Ltd.", email = "hi@thefamouscat.com" }
12
12
  ]
13
- license = {text = "MIT"}
13
+ license = { text = "MIT" }
14
14
  classifiers = [
15
15
  "Development Status :: 4 - Beta",
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: MIT License",
18
- "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.8",
20
18
  "Programming Language :: Python :: 3.9",
21
19
  "Programming Language :: Python :: 3.10",
22
20
  "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
23
22
  ]
24
23
  dependencies = [
25
24
  "requests>=2.25.0",
@@ -37,16 +36,16 @@ dev = [
37
36
  ]
38
37
 
39
38
  [project.urls]
40
- Homepage = "https://github.com/tfc/pdfdancer-python"
41
- Repository = "https://github.com/tfc/pdfdancer-python.git"
39
+ Homepage = "https://www.pdfdancer.com/"
40
+ Repository = "https://github.com/MenschMachine/pdfdancer-client-python"
42
41
 
43
42
  [tool.setuptools.packages.find]
44
43
  where = ["src"]
45
44
 
46
45
  [tool.black]
47
46
  line-length = 88
48
- target-version = ['py38']
47
+ target-version = ['py39']
49
48
 
50
49
  [tool.mypy]
51
- python_version = "3.8"
50
+ python_version = "3.9"
52
51
  strict = true
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PDFDancer Python Client Release Tool
4
+
5
+ A tool to bump version and upload to PyPI.
6
+ """
7
+
8
+ import argparse
9
+ import glob
10
+ import re
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import List
15
+
16
+
17
+ class ReleaseError(Exception):
18
+ """Base exception for release operations."""
19
+ pass
20
+
21
+
22
+ class VersionBumper:
23
+ """Handles version bumping in pyproject.toml."""
24
+
25
+ def __init__(self, pyproject_path: Path = Path("pyproject.toml")):
26
+ self.pyproject_path = pyproject_path
27
+ if not self.pyproject_path.exists():
28
+ raise ReleaseError(f"pyproject.toml not found at {pyproject_path}")
29
+
30
+ def get_current_version(self) -> str:
31
+ """Get the current version from pyproject.toml."""
32
+ content = self.pyproject_path.read_text()
33
+ match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
34
+ if not match:
35
+ raise ReleaseError("Version not found in pyproject.toml")
36
+ return match.group(1)
37
+
38
+ def set_version(self, new_version: str) -> None:
39
+ """Set a new version in pyproject.toml."""
40
+ content = self.pyproject_path.read_text()
41
+ new_content = re.sub(
42
+ r'^version\s*=\s*"[^"]+"',
43
+ f'version = "{new_version}"',
44
+ content,
45
+ flags=re.MULTILINE
46
+ )
47
+ if content == new_content:
48
+ raise ReleaseError("Failed to update version in pyproject.toml")
49
+ self.pyproject_path.write_text(new_content)
50
+
51
+ def bump_version(self, bump_type: str) -> str:
52
+ """Bump version by type (major, minor, patch)."""
53
+ current = self.get_current_version()
54
+ parts = current.split(".")
55
+
56
+ if len(parts) != 3:
57
+ raise ReleaseError(f"Invalid version format: {current}")
58
+
59
+ try:
60
+ major, minor, patch = map(int, parts)
61
+ except ValueError:
62
+ raise ReleaseError(f"Invalid version format: {current}")
63
+
64
+ if bump_type == "major":
65
+ major += 1
66
+ minor = 0
67
+ patch = 0
68
+ elif bump_type == "minor":
69
+ minor += 1
70
+ patch = 0
71
+ elif bump_type == "patch":
72
+ patch += 1
73
+ else:
74
+ raise ReleaseError(f"Invalid bump type: {bump_type}")
75
+
76
+ new_version = f"{major}.{minor}.{patch}"
77
+ self.set_version(new_version)
78
+ return new_version
79
+
80
+
81
+ class PyPIUploader:
82
+ """Handles PyPI upload operations."""
83
+
84
+ def __init__(self, venv_path: Path = Path("venv")):
85
+ self.venv_path = venv_path
86
+ self.python_exe = self._get_python_executable()
87
+
88
+ def _get_python_executable(self) -> Path:
89
+ """Get the Python executable from the virtual environment."""
90
+ if sys.platform == "win32":
91
+ python_exe = self.venv_path / "Scripts" / "python.exe"
92
+ else:
93
+ python_exe = self.venv_path / "bin" / "python"
94
+
95
+ if not python_exe.exists():
96
+ raise ReleaseError(f"Python executable not found at {python_exe}")
97
+ return python_exe
98
+
99
+ def run_command(self, cmd: List[str], check: bool = True) -> subprocess.CompletedProcess:
100
+ """Run a command and return the result."""
101
+ print(f"Running: {' '.join(cmd)}")
102
+ result = subprocess.run(cmd, capture_output=True, text=True)
103
+
104
+ if check and result.returncode != 0:
105
+ print(f"Command failed with exit code {result.returncode}")
106
+ print(f"STDOUT: {result.stdout}")
107
+ print(f"STDERR: {result.stderr}")
108
+ raise ReleaseError(f"Command failed: {' '.join(cmd)}")
109
+
110
+ return result
111
+
112
+ def clean_dist(self) -> None:
113
+ """Clean the dist directory."""
114
+ dist_path = Path("dist")
115
+ if dist_path.exists():
116
+ import shutil
117
+ shutil.rmtree(dist_path)
118
+ print("Cleaned dist directory")
119
+
120
+ def build_package(self) -> None:
121
+ """Build the package."""
122
+ self.run_command([str(self.python_exe), "-m", "build"])
123
+ print("Package built successfully")
124
+
125
+ def check_package(self) -> None:
126
+ """Check the built package."""
127
+ self.run_command([str(self.python_exe), "-m", "twine", "check", "dist/*"])
128
+ print("Package validation passed")
129
+
130
+ def upload_to_pypi(self, test: bool = False) -> None:
131
+ """Upload to PyPI or test PyPI."""
132
+ cmd = [str(self.python_exe), "-m", "twine", "upload"]
133
+ if test:
134
+ cmd.extend(["--repository", "testpypi"])
135
+ cmd.append("dist/*")
136
+
137
+ self.run_command(cmd)
138
+ repo_name = "Test PyPI" if test else "PyPI"
139
+ print(f"Package uploaded to {repo_name} successfully")
140
+
141
+ def run_tests(self, include_e2e: bool = False) -> None:
142
+ """Run the test suite."""
143
+ if include_e2e:
144
+ test_path = "tests/"
145
+ else:
146
+ # Collect all test files except those in e2e/
147
+ test_files = [
148
+ f for f in glob.glob("tests/**/*.py", recursive=True)
149
+ if "e2e" not in f
150
+ ]
151
+ test_path = " ".join(test_files)
152
+
153
+ self.run_command([str(self.python_exe), "-m", "pytest"] + test_path.split() + ["-v"])
154
+ print("All tests passed")
155
+
156
+
157
+ def main():
158
+ """Main entry point."""
159
+ parser = argparse.ArgumentParser(description="PDFDancer Python Client Release Tool")
160
+ parser.add_argument(
161
+ "action",
162
+ choices=["bump", "upload", "release"],
163
+ help="Action to perform: bump (version only), upload (build+upload), release (bump+test+build+upload)"
164
+ )
165
+ parser.add_argument(
166
+ "--bump-type",
167
+ choices=["major", "minor", "patch"],
168
+ default="patch",
169
+ help="Type of version bump (default: patch)"
170
+ )
171
+ parser.add_argument(
172
+ "--version",
173
+ help="Specific version to set (overrides --bump-type)"
174
+ )
175
+ parser.add_argument(
176
+ "--test",
177
+ action="store_true",
178
+ help="Upload to test PyPI instead of production PyPI"
179
+ )
180
+ parser.add_argument(
181
+ "--skip-tests",
182
+ action="store_true",
183
+ help="Skip running tests before release"
184
+ )
185
+ parser.add_argument(
186
+ "--include-e2e",
187
+ action="store_true",
188
+ help="Include E2E tests (requires PDFDancer server and token)"
189
+ )
190
+ parser.add_argument(
191
+ "--dry-run",
192
+ action="store_true",
193
+ help="Show what would be done without actually doing it"
194
+ )
195
+
196
+ args = parser.parse_args()
197
+
198
+ try:
199
+ version_bumper = VersionBumper()
200
+ uploader = PyPIUploader()
201
+
202
+ if args.dry_run:
203
+ print("DRY RUN MODE - No changes will be made")
204
+
205
+ if args.action in ["bump", "release"]:
206
+ current_version = version_bumper.get_current_version()
207
+ print(f"Current version: {current_version}")
208
+
209
+ if args.version:
210
+ new_version = args.version
211
+ if not args.dry_run:
212
+ version_bumper.set_version(new_version)
213
+ else:
214
+ if not args.dry_run:
215
+ new_version = version_bumper.bump_version(args.bump_type)
216
+ else:
217
+ # Calculate what the new version would be for dry run
218
+ parts = current_version.split(".")
219
+ major, minor, patch = map(int, parts)
220
+ if args.bump_type == "major":
221
+ major += 1
222
+ minor = 0
223
+ patch = 0
224
+ elif args.bump_type == "minor":
225
+ minor += 1
226
+ patch = 0
227
+ elif args.bump_type == "patch":
228
+ patch += 1
229
+ new_version = f"{major}.{minor}.{patch}"
230
+
231
+ print(f"New version: {new_version}")
232
+
233
+ if args.action in ["upload", "release"]:
234
+ if args.action == "release" and not args.skip_tests:
235
+ if not args.dry_run:
236
+ print("Running tests...")
237
+ uploader.run_tests(include_e2e=args.include_e2e)
238
+ else:
239
+ test_type = "all tests (including E2E)" if args.include_e2e else "unit tests only"
240
+ print(f"Would run {test_type}")
241
+
242
+ if not args.dry_run:
243
+ print("Cleaning dist directory...")
244
+ uploader.clean_dist()
245
+
246
+ print("Building package...")
247
+ uploader.build_package()
248
+
249
+ print("Checking package...")
250
+ uploader.check_package()
251
+
252
+ print("Uploading to PyPI...")
253
+ uploader.upload_to_pypi(test=args.test)
254
+ else:
255
+ print("Would clean dist directory")
256
+ print("Would build package")
257
+ print("Would check package")
258
+ repo_name = "Test PyPI" if args.test else "PyPI"
259
+ print(f"Would upload to {repo_name}")
260
+
261
+ print("Release process completed successfully!")
262
+
263
+ except ReleaseError as e:
264
+ print(f"Error: {e}", file=sys.stderr)
265
+ sys.exit(1)
266
+ except KeyboardInterrupt:
267
+ print("\nOperation cancelled by user", file=sys.stderr)
268
+ sys.exit(1)
269
+
270
+
271
+ if __name__ == "__main__":
272
+ main()