pdftagger 0.1.1__tar.gz → 0.2.0__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.
- {pdftagger-0.1.1 → pdftagger-0.2.0}/PKG-INFO +30 -3
- {pdftagger-0.1.1 → pdftagger-0.2.0}/README.md +29 -2
- pdftagger-0.2.0/RELEASING.md +29 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/examples/quickstart.py +6 -1
- {pdftagger-0.1.1 → pdftagger-0.2.0}/src/pdftagger/__init__.py +17 -1
- pdftagger-0.2.0/src/pdftagger/_version.py +1 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/src/pdftagger/client.py +28 -3
- {pdftagger-0.1.1 → pdftagger-0.2.0}/src/pdftagger/models.py +82 -0
- pdftagger-0.2.0/tests/fixtures/job_report.json +2877 -0
- pdftagger-0.2.0/tests/test_report.py +111 -0
- pdftagger-0.1.1/RELEASING.md +0 -15
- pdftagger-0.1.1/src/pdftagger/_version.py +0 -1
- {pdftagger-0.1.1 → pdftagger-0.2.0}/.github/workflows/publish.yml +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/.github/workflows/test.yml +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/.gitignore +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/LICENSE +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/examples/webhook_flask.py +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/pyproject.toml +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/src/pdftagger/exceptions.py +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/src/pdftagger/py.typed +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/src/pdftagger/webhooks.py +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/tests/_sign_reference.mjs +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/tests/test_batches.py +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/tests/test_client.py +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/tests/test_webhook_cross_lang.py +0 -0
- {pdftagger-0.1.1 → pdftagger-0.2.0}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pdftagger
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Official Python SDK for the PDF Tagger API — auto-tag PDFs for PDF/UA accessibility.
|
|
5
5
|
Project-URL: Homepage, https://pdf-tagger.com
|
|
6
6
|
Project-URL: Documentation, https://pdf-tagger.com/api/reference
|
|
@@ -56,6 +56,7 @@ Description-Content-Type: text/markdown
|
|
|
56
56
|
Official Python SDK for the [PDF Tagger](https://pdf-tagger.com) API. Upload a PDF,
|
|
57
57
|
submit a tagging job, and download a **PDF/UA-tagged, accessible** PDF — with correct
|
|
58
58
|
reading order, headings, tables, and alt text. Tag one PDF, or up to 100 at once as a batch.
|
|
59
|
+
Each job comes with a PDF/UA-1 report: the result for every accessibility requirement in PDF/UA-1.
|
|
59
60
|
|
|
60
61
|
```bash
|
|
61
62
|
pip install pdftagger
|
|
@@ -99,6 +100,31 @@ job = client.tag_pdf("report.pdf", wait=False) # returns immediately
|
|
|
99
100
|
job = client.wait_for_job(job) # ...poll later
|
|
100
101
|
```
|
|
101
102
|
|
|
103
|
+
## The PDF/UA-1 report
|
|
104
|
+
|
|
105
|
+
Every completed job has a report: the result and evidence for every accessibility requirement in
|
|
106
|
+
PDF/UA-1, and a report PDF you can keep.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
report = client.get_job_report(job)
|
|
110
|
+
print(report.summary) # {'handled': 169, 'failed': 0, 'needs_review': 0, 'for_information': 0, 'not_applicable': 51}
|
|
111
|
+
|
|
112
|
+
for req in report.failed: # also report.needs_review
|
|
113
|
+
print(req.reference, req.name, req.evidence)
|
|
114
|
+
|
|
115
|
+
client.download_report(job, "report.accessibility-report.pdf")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each `Requirement` has a `result`:
|
|
119
|
+
|
|
120
|
+
| `result` | Meaning |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `handled` | It passed, or PDF Tagger met or fixed it. |
|
|
123
|
+
| `failed` | A validator found it failing in the file. |
|
|
124
|
+
| `needs_review` | A person must confirm it. |
|
|
125
|
+
| `for_information` | A limit from the source file that you cannot act on. |
|
|
126
|
+
| `not_applicable` | The document has no content of this kind. |
|
|
127
|
+
|
|
102
128
|
## Many PDFs at once: batches
|
|
103
129
|
|
|
104
130
|
Tag up to 100 PDFs as one batch. `tag_pdfs()` uploads every file, submits them in one
|
|
@@ -169,6 +195,7 @@ client.upload_file(upload, "report.pdf")
|
|
|
169
195
|
job = client.submit_job(key=upload.key, ocr=True)
|
|
170
196
|
job = client.get_job(job.id) # processing / completed / failed
|
|
171
197
|
result = client.get_job_result(job.id) # short-lived download URL
|
|
198
|
+
report = client.get_job_report(job.id) # the PDF/UA-1 report
|
|
172
199
|
```
|
|
173
200
|
|
|
174
201
|
## Errors
|
|
@@ -180,8 +207,8 @@ All errors derive from `PdfTaggerError`.
|
|
|
180
207
|
| `AuthenticationError` | 401 — key missing, invalid, or revoked |
|
|
181
208
|
| `BadRequestError` | 400 — invalid request (for a batch, `.errors` lists each bad file) |
|
|
182
209
|
| `InsufficientCreditsError` | 402 — not enough credits (a batch charges nothing) |
|
|
183
|
-
| `NotFoundError` | 404 — no such job/resource for this account |
|
|
184
|
-
| `ConflictError` | 409 — idempotency conflict, or result not ready |
|
|
210
|
+
| `NotFoundError` | 404 — no such job/resource for this account, or the job has no report |
|
|
211
|
+
| `ConflictError` | 409 — idempotency conflict, or result or report not ready |
|
|
185
212
|
| `RateLimitError` | 429 — rate limited (`.retry_after` seconds) |
|
|
186
213
|
| `JobFailed` | the job finished with a `failed` status (credits are refunded) |
|
|
187
214
|
| `JobTimeout` | the job, or the batch, did not finish within the poll timeout |
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Official Python SDK for the [PDF Tagger](https://pdf-tagger.com) API. Upload a PDF,
|
|
4
4
|
submit a tagging job, and download a **PDF/UA-tagged, accessible** PDF — with correct
|
|
5
5
|
reading order, headings, tables, and alt text. Tag one PDF, or up to 100 at once as a batch.
|
|
6
|
+
Each job comes with a PDF/UA-1 report: the result for every accessibility requirement in PDF/UA-1.
|
|
6
7
|
|
|
7
8
|
```bash
|
|
8
9
|
pip install pdftagger
|
|
@@ -46,6 +47,31 @@ job = client.tag_pdf("report.pdf", wait=False) # returns immediately
|
|
|
46
47
|
job = client.wait_for_job(job) # ...poll later
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
## The PDF/UA-1 report
|
|
51
|
+
|
|
52
|
+
Every completed job has a report: the result and evidence for every accessibility requirement in
|
|
53
|
+
PDF/UA-1, and a report PDF you can keep.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
report = client.get_job_report(job)
|
|
57
|
+
print(report.summary) # {'handled': 169, 'failed': 0, 'needs_review': 0, 'for_information': 0, 'not_applicable': 51}
|
|
58
|
+
|
|
59
|
+
for req in report.failed: # also report.needs_review
|
|
60
|
+
print(req.reference, req.name, req.evidence)
|
|
61
|
+
|
|
62
|
+
client.download_report(job, "report.accessibility-report.pdf")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Each `Requirement` has a `result`:
|
|
66
|
+
|
|
67
|
+
| `result` | Meaning |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `handled` | It passed, or PDF Tagger met or fixed it. |
|
|
70
|
+
| `failed` | A validator found it failing in the file. |
|
|
71
|
+
| `needs_review` | A person must confirm it. |
|
|
72
|
+
| `for_information` | A limit from the source file that you cannot act on. |
|
|
73
|
+
| `not_applicable` | The document has no content of this kind. |
|
|
74
|
+
|
|
49
75
|
## Many PDFs at once: batches
|
|
50
76
|
|
|
51
77
|
Tag up to 100 PDFs as one batch. `tag_pdfs()` uploads every file, submits them in one
|
|
@@ -116,6 +142,7 @@ client.upload_file(upload, "report.pdf")
|
|
|
116
142
|
job = client.submit_job(key=upload.key, ocr=True)
|
|
117
143
|
job = client.get_job(job.id) # processing / completed / failed
|
|
118
144
|
result = client.get_job_result(job.id) # short-lived download URL
|
|
145
|
+
report = client.get_job_report(job.id) # the PDF/UA-1 report
|
|
119
146
|
```
|
|
120
147
|
|
|
121
148
|
## Errors
|
|
@@ -127,8 +154,8 @@ All errors derive from `PdfTaggerError`.
|
|
|
127
154
|
| `AuthenticationError` | 401 — key missing, invalid, or revoked |
|
|
128
155
|
| `BadRequestError` | 400 — invalid request (for a batch, `.errors` lists each bad file) |
|
|
129
156
|
| `InsufficientCreditsError` | 402 — not enough credits (a batch charges nothing) |
|
|
130
|
-
| `NotFoundError` | 404 — no such job/resource for this account |
|
|
131
|
-
| `ConflictError` | 409 — idempotency conflict, or result not ready |
|
|
157
|
+
| `NotFoundError` | 404 — no such job/resource for this account, or the job has no report |
|
|
158
|
+
| `ConflictError` | 409 — idempotency conflict, or result or report not ready |
|
|
132
159
|
| `RateLimitError` | 429 — rate limited (`.retry_after` seconds) |
|
|
133
160
|
| `JobFailed` | the job finished with a `failed` status (credits are refunded) |
|
|
134
161
|
| `JobTimeout` | the job, or the batch, did not finish within the poll timeout |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
PyPI publishing is automatic: publishing a GitHub release runs `.github/workflows/publish.yml`,
|
|
4
|
+
which builds the package and uploads it with PyPI trusted publishing (no token).
|
|
5
|
+
|
|
6
|
+
1. Set the new version in `src/pdftagger/_version.py` and merge it to `main`.
|
|
7
|
+
2. On GitHub, create a release with the tag `v<version>` (for example `v0.1.0`) and publish it.
|
|
8
|
+
3. Check the "Publish to PyPI" workflow run, then `pip install pdftagger==<version>`.
|
|
9
|
+
|
|
10
|
+
One-time setup (already done once it has been published):
|
|
11
|
+
|
|
12
|
+
- On PyPI, add a trusted publisher for the project `pdftagger`: owner `PDF-Tagger`,
|
|
13
|
+
repository `pdftagger-python`, workflow `publish.yml`, environment `pypi`. Before the first
|
|
14
|
+
release this is a "pending publisher" (Account settings → Publishing).
|
|
15
|
+
- On GitHub, create the environment `pypi` (Settings → Environments).
|
|
16
|
+
|
|
17
|
+
## The PyPI provenance banner
|
|
18
|
+
|
|
19
|
+
PyPI compares each release with the latest earlier release that has attestations and was made within 14 days. It
|
|
20
|
+
shows a "Change of provenance" warning when the publishing repository or workflow changed. 0.1.1 has that warning
|
|
21
|
+
because 0.1.0 came from `rachel-ackeret/pdftagger-python` and 0.1.1 from `PDF-Tagger/pdftagger-python`. Every later
|
|
22
|
+
release from this repository and `publish.yml` has no warning, so the project page is clean from 0.2.0 on. Keep the
|
|
23
|
+
repository and the workflow file name the same; a change brings the warning back for one release. Do not delete
|
|
24
|
+
0.1.0 to clear 0.1.1's own page: deletion is permanent and burns the version number.
|
|
25
|
+
|
|
26
|
+
## Releases that need an API change
|
|
27
|
+
|
|
28
|
+
Publish a release only after every API endpoint it calls is live on api.pdf-tagger.com. 0.2.0 calls
|
|
29
|
+
`GET /v1/jobs/{id}/report`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tag a PDF
|
|
1
|
+
"""Tag a PDF, save the accessible result and print its PDF/UA-1 report.
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
4
|
export PDFTAGGER_API_KEY=sk_live_...
|
|
@@ -38,6 +38,11 @@ def main() -> int:
|
|
|
38
38
|
|
|
39
39
|
client.download_result(job, out)
|
|
40
40
|
print(f"Tagged PDF saved to {out} (job {job.id}, {job.credits_used} credits)")
|
|
41
|
+
|
|
42
|
+
report = client.get_job_report(job)
|
|
43
|
+
print(f"{report.standard}: {report.summary}")
|
|
44
|
+
for req in report.failed + report.needs_review:
|
|
45
|
+
print(f" {req.result}: {req.reference} {req.name}")
|
|
41
46
|
return 0
|
|
42
47
|
|
|
43
48
|
|
|
@@ -6,6 +6,8 @@ Auto-tag PDFs for PDF/UA accessibility from your own code:
|
|
|
6
6
|
>>> client = PdfTaggerClient(api_key="sk_live_...")
|
|
7
7
|
>>> job = client.tag_pdf("report.pdf", ocr=True)
|
|
8
8
|
>>> client.download_result(job, "report.tagged.pdf")
|
|
9
|
+
>>> report = client.get_job_report(job) # the PDF/UA-1 report
|
|
10
|
+
>>> report.summary
|
|
9
11
|
|
|
10
12
|
Many PDFs at once, as one batch:
|
|
11
13
|
|
|
@@ -30,7 +32,19 @@ from .exceptions import (
|
|
|
30
32
|
RateLimitError,
|
|
31
33
|
WebhookVerificationError,
|
|
32
34
|
)
|
|
33
|
-
from .models import
|
|
35
|
+
from .models import (
|
|
36
|
+
Account,
|
|
37
|
+
Batch,
|
|
38
|
+
BatchCounts,
|
|
39
|
+
BatchEvent,
|
|
40
|
+
BatchItem,
|
|
41
|
+
Job,
|
|
42
|
+
JobReport,
|
|
43
|
+
JobResult,
|
|
44
|
+
Requirement,
|
|
45
|
+
Upload,
|
|
46
|
+
WebhookEvent,
|
|
47
|
+
)
|
|
34
48
|
from .webhooks import SIGNATURE_HEADER, verify_signature
|
|
35
49
|
|
|
36
50
|
__all__ = [
|
|
@@ -42,6 +56,8 @@ __all__ = [
|
|
|
42
56
|
"Upload",
|
|
43
57
|
"Job",
|
|
44
58
|
"JobResult",
|
|
59
|
+
"JobReport",
|
|
60
|
+
"Requirement",
|
|
45
61
|
"Batch",
|
|
46
62
|
"BatchItem",
|
|
47
63
|
"BatchCounts",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -20,7 +20,7 @@ from .exceptions import (
|
|
|
20
20
|
NotFoundError,
|
|
21
21
|
RateLimitError,
|
|
22
22
|
)
|
|
23
|
-
from .models import Account, Batch, BatchItem, BatchItemInput, Job, JobResult, Upload
|
|
23
|
+
from .models import Account, Batch, BatchItem, BatchItemInput, Job, JobReport, JobResult, Upload
|
|
24
24
|
|
|
25
25
|
DEFAULT_BASE_URL = "https://api.pdf-tagger.com"
|
|
26
26
|
|
|
@@ -236,6 +236,14 @@ class PdfTaggerClient:
|
|
|
236
236
|
"""Get a short-lived download URL for a completed job."""
|
|
237
237
|
return JobResult.from_dict(self._request("GET", f"/v1/jobs/{_job_id(job)}/result"))
|
|
238
238
|
|
|
239
|
+
def get_job_report(self, job: Union[str, Job]) -> JobReport:
|
|
240
|
+
"""Get a completed job's PDF/UA-1 report: the result for every accessibility requirement.
|
|
241
|
+
|
|
242
|
+
Raises :class:`ConflictError` while the job is not completed, and :class:`NotFoundError`
|
|
243
|
+
when the job has no PDF/UA-1 report.
|
|
244
|
+
"""
|
|
245
|
+
return JobReport.from_dict(self._request("GET", f"/v1/jobs/{_job_id(job)}/report"))
|
|
246
|
+
|
|
239
247
|
def submit_batch(
|
|
240
248
|
self,
|
|
241
249
|
items: Sequence[BatchItemInput],
|
|
@@ -440,9 +448,26 @@ class PdfTaggerClient:
|
|
|
440
448
|
Returns the written :class:`~pathlib.Path` when ``dest`` is given, otherwise the
|
|
441
449
|
PDF bytes.
|
|
442
450
|
"""
|
|
443
|
-
|
|
451
|
+
return self._download(self.get_job_result(job).result_url, dest)
|
|
452
|
+
|
|
453
|
+
def download_report(
|
|
454
|
+
self,
|
|
455
|
+
job: Union[str, Job],
|
|
456
|
+
dest: Optional[Union[str, Path]] = None,
|
|
457
|
+
) -> Union[bytes, Path]:
|
|
458
|
+
"""Download a completed job's PDF/UA-1 report PDF.
|
|
459
|
+
|
|
460
|
+
Returns the written :class:`~pathlib.Path` when ``dest`` is given, otherwise the
|
|
461
|
+
PDF bytes. Raises :class:`NotFoundError` when the report PDF is no longer stored.
|
|
462
|
+
"""
|
|
463
|
+
report = self.get_job_report(job)
|
|
464
|
+
if not report.report_pdf_url:
|
|
465
|
+
raise NotFoundError("The report PDF for this job is no longer stored.", status_code=404)
|
|
466
|
+
return self._download(report.report_pdf_url, dest)
|
|
467
|
+
|
|
468
|
+
def _download(self, url: str, dest: Optional[Union[str, Path]]) -> Union[bytes, Path]:
|
|
444
469
|
try:
|
|
445
|
-
response = self._http.get(
|
|
470
|
+
response = self._http.get(url)
|
|
446
471
|
except httpx.HTTPError as exc:
|
|
447
472
|
raise APIError(f"Download failed: {exc}") from exc
|
|
448
473
|
if not response.is_success:
|
|
@@ -125,6 +125,88 @@ class JobResult:
|
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
@dataclass
|
|
129
|
+
class Requirement:
|
|
130
|
+
"""One accessibility requirement in a job's PDF/UA-1 report, with its result on this document.
|
|
131
|
+
|
|
132
|
+
``result`` is one of ``handled`` (passed, or PDF Tagger met or fixed it), ``failed`` (a validator
|
|
133
|
+
found it failing in the file), ``needs_review`` (a person must confirm it), ``for_information`` (a
|
|
134
|
+
limit from the source file you cannot act on) or ``not_applicable`` (the document has no content of
|
|
135
|
+
this kind). ``evidence`` is the sentence behind the result, or ``None`` for a plain pass.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
id: str
|
|
139
|
+
reference: str
|
|
140
|
+
name: str
|
|
141
|
+
result: str
|
|
142
|
+
level: Optional[str] = None
|
|
143
|
+
section_id: Optional[str] = None
|
|
144
|
+
section_title: Optional[str] = None
|
|
145
|
+
category: Optional[str] = None
|
|
146
|
+
evidence: Optional[str] = None
|
|
147
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "Requirement":
|
|
151
|
+
section = data.get("section") or {}
|
|
152
|
+
return cls(
|
|
153
|
+
id=data["id"],
|
|
154
|
+
reference=data.get("reference", ""),
|
|
155
|
+
name=data.get("name", ""),
|
|
156
|
+
result=data["result"],
|
|
157
|
+
level=data.get("level"),
|
|
158
|
+
section_id=section.get("id"),
|
|
159
|
+
section_title=section.get("title"),
|
|
160
|
+
category=data.get("category"),
|
|
161
|
+
evidence=data.get("evidence"),
|
|
162
|
+
raw=dict(data),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class JobReport:
|
|
168
|
+
"""A job's PDF/UA-1 report (``GET /v1/jobs/{id}/report``).
|
|
169
|
+
|
|
170
|
+
``requirements`` lists every accessibility requirement in PDF/UA-1, in the standard's order.
|
|
171
|
+
``summary`` counts the requirements by result. ``report_pdf_url`` is a short-lived link to the
|
|
172
|
+
report PDF, or ``None`` when the file is no longer stored.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
job_id: str
|
|
176
|
+
standard: str
|
|
177
|
+
total_requirements: int
|
|
178
|
+
summary: Dict[str, int]
|
|
179
|
+
requirements: List[Requirement]
|
|
180
|
+
report_pdf_url: Optional[str] = None
|
|
181
|
+
expires_in: Optional[int] = None
|
|
182
|
+
request_id: Optional[str] = None
|
|
183
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "JobReport":
|
|
187
|
+
return cls(
|
|
188
|
+
job_id=data["job_id"],
|
|
189
|
+
standard=data.get("standard", ""),
|
|
190
|
+
total_requirements=int(data.get("total_requirements", 0)),
|
|
191
|
+
summary=dict(data.get("summary") or {}),
|
|
192
|
+
requirements=[Requirement.from_dict(r) for r in data.get("requirements") or []],
|
|
193
|
+
report_pdf_url=data.get("report_pdf_url"),
|
|
194
|
+
expires_in=data.get("expires_in"),
|
|
195
|
+
request_id=data.get("request_id"),
|
|
196
|
+
raw=dict(data),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def failed(self) -> List[Requirement]:
|
|
201
|
+
"""The requirements a validator found failing in the file."""
|
|
202
|
+
return [r for r in self.requirements if r.result == "failed"]
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def needs_review(self) -> List[Requirement]:
|
|
206
|
+
"""The requirements a person must confirm."""
|
|
207
|
+
return [r for r in self.requirements if r.result == "needs_review"]
|
|
208
|
+
|
|
209
|
+
|
|
128
210
|
@dataclass
|
|
129
211
|
class BatchItem:
|
|
130
212
|
"""One file in a batch for :meth:`~pdftagger.client.PdfTaggerClient.submit_batch`.
|