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.
- pdfdancer_client_python-0.2.2/.github/workflows/ci.yml +40 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/.gitignore +2 -1
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/CLAUDE.md +8 -8
- {pdfdancer_client_python-0.1.1/src/pdfdancer_client_python.egg-info → pdfdancer_client_python-0.2.2}/PKG-INFO +31 -35
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/README.md +26 -29
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/pyproject.toml +8 -9
- pdfdancer_client_python-0.2.2/release.py +272 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/__init__.py +5 -3
- pdfdancer_client_python-0.2.2/src/pdfdancer/image_builder.py +30 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/models.py +67 -10
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/paragraph_builder.py +15 -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
- pdfdancer_client_python-0.2.2/src/pdfdancer/types.py +263 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2/src/pdfdancer_client_python.egg-info}/PKG-INFO +31 -35
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/SOURCES.txt +9 -7
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/test.py +56 -56
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/e2e/__init__.py +1 -1
- pdfdancer_client_python-0.2.2/tests/e2e/test_acroform.py +83 -0
- pdfdancer_client_python-0.2.2/tests/e2e/test_form_x_objects.py +36 -0
- pdfdancer_client_python-0.2.2/tests/e2e/test_image.py +91 -0
- pdfdancer_client_python-0.2.2/tests/e2e/test_line.py +83 -0
- pdfdancer_client_python-0.2.2/tests/e2e/test_page.py +33 -0
- pdfdancer_client_python-0.2.2/tests/e2e/test_paragraph.py +169 -0
- pdfdancer_client_python-0.2.2/tests/e2e/test_path.py +69 -0
- pdfdancer_client_python-0.2.2/tests/fixtures/DancingScript-Regular.ttf +0 -0
- pdfdancer_client_python-0.2.2/tests/fixtures/form-xobject-example.pdf +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/test_models.py +24 -48
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/test_openapi_compliance.py +4 -4
- pdfdancer_client_python-0.1.1/demo.py +0 -365
- pdfdancer_client_python-0.1.1/tests/e2e/test_form.py +0 -33
- pdfdancer_client_python-0.1.1/tests/e2e/test_image.py +0 -84
- pdfdancer_client_python-0.1.1/tests/e2e/test_line.py +0 -92
- pdfdancer_client_python-0.1.1/tests/e2e/test_page.py +0 -29
- pdfdancer_client_python-0.1.1/tests/e2e/test_paragraph.py +0 -172
- pdfdancer_client_python-0.1.1/tests/e2e/test_path.py +0 -56
- pdfdancer_client_python-0.1.1/tests/test_client_v1.py +0 -444
- pdfdancer_client_python-0.1.1/tests/test_error_extraction.py +0 -117
- pdfdancer_client_python-0.1.1/tests/test_exception_suppression.py +0 -53
- pdfdancer_client_python-0.1.1/tests/test_paragraph_builder.py +0 -324
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/docs/openapi.yml +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/requirements-dev.txt +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/requirements.txt +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/setup.cfg +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer/exceptions.py +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/dependency_links.txt +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/requires.txt +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/src/pdfdancer_client_python.egg-info/top_level.txt +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/__init__.py +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/JetBrainsMono-Regular.ttf +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/ObviouslyAwesome.pdf +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/basic-paths.pdf +0 -0
- {pdfdancer_client_python-0.1.1 → pdfdancer_client_python-0.2.2}/tests/fixtures/logo-80.png +0 -0
- {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/*
|
|
@@ -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.
|
|
42
|
-
images = client.
|
|
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.
|
|
46
|
-
client.
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Python client for PDFDancer API
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
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.
|
|
59
|
+
images = client.find_images(Position.at_page(0))
|
|
61
60
|
|
|
62
61
|
# Manipulation operations (mirrors Java client methods)
|
|
63
|
-
client.
|
|
64
|
-
client.
|
|
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.
|
|
70
|
+
.with_position(Position.at_page(0))
|
|
72
71
|
.build())
|
|
73
72
|
|
|
74
|
-
client.
|
|
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.
|
|
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.
|
|
110
|
+
objects = client._find(ObjectType.PARAGRAPH, position)
|
|
111
111
|
|
|
112
112
|
# Specific finders (Java: client.findParagraphs(), etc.)
|
|
113
|
-
paragraphs = client.
|
|
114
|
-
images = client.
|
|
115
|
-
forms = client.
|
|
116
|
-
paths = client.
|
|
117
|
-
text_lines = client.
|
|
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.
|
|
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.
|
|
128
|
-
result = client.
|
|
128
|
+
result = client._delete(object_ref)
|
|
129
|
+
result = client._delete_page(page_ref)
|
|
129
130
|
|
|
130
131
|
# Move (Java: client.move())
|
|
131
|
-
result = client.
|
|
132
|
+
result = client._move(object_ref, new_position)
|
|
132
133
|
|
|
133
134
|
# Add (Java: client.addImage(), client.addParagraph())
|
|
134
|
-
result = client.
|
|
135
|
-
result = client.
|
|
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.
|
|
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.
|
|
171
|
-
position = Position.
|
|
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.
|
|
175
|
-
y = position.
|
|
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.
|
|
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.
|
|
33
|
+
images = client.find_images(Position.at_page(0))
|
|
34
34
|
|
|
35
35
|
# Manipulation operations (mirrors Java client methods)
|
|
36
|
-
client.
|
|
37
|
-
client.
|
|
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.
|
|
44
|
+
.with_position(Position.at_page(0))
|
|
45
45
|
.build())
|
|
46
46
|
|
|
47
|
-
client.
|
|
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.
|
|
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.
|
|
84
|
+
objects = client._find(ObjectType.PARAGRAPH, position)
|
|
84
85
|
|
|
85
86
|
# Specific finders (Java: client.findParagraphs(), etc.)
|
|
86
|
-
paragraphs = client.
|
|
87
|
-
images = client.
|
|
88
|
-
forms = client.
|
|
89
|
-
paths = client.
|
|
90
|
-
text_lines = client.
|
|
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.
|
|
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.
|
|
101
|
-
result = client.
|
|
102
|
+
result = client._delete(object_ref)
|
|
103
|
+
result = client._delete_page(page_ref)
|
|
102
104
|
|
|
103
105
|
# Move (Java: client.move())
|
|
104
|
-
result = client.
|
|
106
|
+
result = client._move(object_ref, new_position)
|
|
105
107
|
|
|
106
108
|
# Add (Java: client.addImage(), client.addParagraph())
|
|
107
|
-
result = client.
|
|
108
|
-
result = client.
|
|
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.
|
|
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.
|
|
144
|
-
position = Position.
|
|
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.
|
|
148
|
-
y = position.
|
|
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.
|
|
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.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Python client for PDFDancer API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
11
|
-
{name = "
|
|
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://
|
|
41
|
-
Repository = "https://github.com/
|
|
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 = ['
|
|
47
|
+
target-version = ['py39']
|
|
49
48
|
|
|
50
49
|
[tool.mypy]
|
|
51
|
-
python_version = "3.
|
|
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()
|