grobid-client-python 0.1.5__tar.gz → 0.2.1__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 (76) hide show
  1. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/.github/workflows/ci-build.yml +5 -1
  2. grobid_client_python-0.2.1/MANIFEST.in +2 -0
  3. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/PKG-INFO +148 -12
  4. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/Readme.md +144 -11
  5. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/config.json +0 -1
  6. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/client.py +44 -21
  7. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/format/TEI2LossyJSON.py +129 -90
  8. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/format/TEI2LossyJSON_cli.py +4 -2
  9. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/format/TEI2Markdown.py +19 -17
  10. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/format/TEI2Markdown_cli.py +4 -2
  11. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/format/__main__.py +3 -1
  12. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/format/validate_json_refs.py +3 -2
  13. grobid_client_python-0.2.1/grobid_client/grobid_client.py +2209 -0
  14. grobid_client_python-0.2.1/grobid_client/py.typed +0 -0
  15. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/PKG-INFO +148 -12
  16. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/SOURCES.txt +2 -0
  17. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/requires.txt +4 -0
  18. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/scm_file_list.json +41 -39
  19. grobid_client_python-0.2.1/grobid_client_python.egg-info/scm_version.json +8 -0
  20. grobid_client_python-0.2.1/pyproject.toml +70 -0
  21. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/conftest.py +1 -1
  22. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/test_config.json +1 -1
  23. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/test_conversions.py +1 -1
  24. grobid_client_python-0.2.1/tests/test_grobid_client.py +1711 -0
  25. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/test_integration.py +4 -4
  26. grobid_client_python-0.2.1/tests/test_s3.py +180 -0
  27. grobid_client_python-0.1.5/MANIFEST.in +0 -1
  28. grobid_client_python-0.1.5/grobid_client/grobid_client.py +0 -1034
  29. grobid_client_python-0.1.5/grobid_client_python.egg-info/scm_version.json +0 -8
  30. grobid_client_python-0.1.5/pyproject.toml +0 -41
  31. grobid_client_python-0.1.5/tests/test_grobid_client.py +0 -673
  32. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/.github/dependabot.yml +0 -0
  33. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/.github/workflows/ci-release.yml +0 -0
  34. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/.gitignore +0 -0
  35. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/LICENSE +0 -0
  36. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/example.py +0 -0
  37. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/__init__.py +0 -0
  38. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client/format/__init__.py +0 -0
  39. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/dependency_links.txt +0 -0
  40. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/entry_points.txt +0 -0
  41. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/top_level.txt +0 -0
  42. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/pytest.ini +0 -0
  43. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/requirements.txt +0 -0
  44. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/20180928112135.png +0 -0
  45. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/20180928112138.csv +0 -0
  46. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_pdf/0046d83a-edd6-4631-b57c-755cdcce8b7f.pdf +0 -0
  47. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_pdf/0046d83a-edd6-4631-b57c-755cdcce8b7f.tei.xml +0 -0
  48. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_pdf/mjb3wlzxcb2mc-migowebupload-1766042162782.grobid.tei.xml +0 -0
  49. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-1.txt +0 -0
  50. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-2.txt +0 -0
  51. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-3.txt +0 -0
  52. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-4.txt +0 -0
  53. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-5.txt +0 -0
  54. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-6.txt +0 -0
  55. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-7.txt +0 -0
  56. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/setup.cfg +0 -0
  57. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/__init__.py +0 -0
  58. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/0046d83a-edd6-4631-b57c-755cdcce8b7f.tei.xml +0 -0
  59. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/__init__.py +0 -0
  60. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/article_withdrawn.grobid.tei.xml +0 -0
  61. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41477-023-01501-1.grobid.tei.xml +0 -0
  62. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41477-023-01501-1.json +0 -0
  63. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41586-023-05895-y.grobid.tei.xml +0 -0
  64. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41586-023-05895-y.json +0 -0
  65. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41598-023-32039-z.grobid.tei.xml +0 -0
  66. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41598-023-32039-z.json +0 -0
  67. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1371_journal.pone.0218311.grobid.tei.xml +0 -0
  68. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1371_journal.pone.0218311.json +0 -0
  69. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.7554_elife.78558.grobid.tei.xml +0 -0
  70. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.7554_elife.78558.json +0 -0
  71. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/2021.naacl-main.224.grobid.tei.xml +0 -0
  72. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/repeated_text/ijms-24-05988.grobid.tei.xml +0 -0
  73. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/repeated_text/ijms-24-05988.json +0 -0
  74. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/resources/repeated_text/ijms-24-05988.md +0 -0
  75. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/run_tests.py +0 -0
  76. {grobid_client_python-0.1.5 → grobid_client_python-0.2.1}/tests/test_client.py +0 -0
@@ -30,7 +30,7 @@ jobs:
30
30
  - name: Install dependencies
31
31
  run: |
32
32
  python -m pip install --upgrade pip
33
- pip install --upgrade flake8 pytest pycodestyle pytest-cov pytest-mock
33
+ pip install --upgrade flake8 pytest pycodestyle pytest-cov pytest-mock "mypy==2.3.*"
34
34
  if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
35
35
  - name: Lint with flake8
36
36
  run: |
@@ -38,6 +38,10 @@ jobs:
38
38
  flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
39
39
  # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
40
40
  flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
41
+ - name: Type check with mypy
42
+ if: matrix.python-version == '3.11'
43
+ run: |
44
+ mypy grobid_client
41
45
  - name: Test with pytest
42
46
  run: |
43
47
  pytest tests/ -v --cov=grobid_client --cov-report=xml --cov-report=term-missing
@@ -0,0 +1,2 @@
1
+ include Readme.md
2
+ include grobid_client/py.typed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grobid-client-python
3
- Version: 0.1.5
3
+ Version: 0.2.1
4
4
  Summary: Simple python client for GROBID REST services
5
5
  Author-email: Patrice Lopez <patrice.lopez@science-miner.com>
6
6
  Maintainer-email: Patrice Lopez <patrice.lopez@science-miner.com>, Luca Foppiano <lucanoro@duck.com>
@@ -216,6 +216,9 @@ Requires-Dist: requests
216
216
  Requires-Dist: dateparser
217
217
  Requires-Dist: beautifulsoup4
218
218
  Requires-Dist: lxml
219
+ Provides-Extra: s3
220
+ Requires-Dist: smart_open[s3]>=6.0; extra == "s3"
221
+ Requires-Dist: boto3; extra == "s3"
219
222
 
220
223
  # GROBID Client Python
221
224
 
@@ -252,6 +255,10 @@ concurrent processing capabilities for PDF documents, reference strings, and pat
252
255
  - **Sentence Segmentation**: Layout-aware sentence segmentation capabilities
253
256
  - **JSON Output**: Convert TEI XML output to structured JSON format with CORD-19-like structure
254
257
  - **Markdown Output**: Convert TEI XML output to clean Markdown format with structured sections
258
+ - **Type Hints**: Ships inline type annotations and a `py.typed` marker (PEP 561) for static type checking
259
+ - **Archive Streaming**: Process files directly from `.zip`/`.tar`/`.tar.gz` archives without fully decompressing them
260
+ - **S3 Streaming**: Read PDFs and zips straight from `s3://` (range-streamed, no full download) with the optional `[s3]` extra
261
+ - **In-Memory Documents**: Send PDFs held as bytes straight to GROBID, without writing them to disk first
255
262
 
256
263
  ## 📋 Prerequisites
257
264
 
@@ -276,6 +283,9 @@ Choose one of the following installation methods:
276
283
 
277
284
  ```bash
278
285
  pip install grobid-client-python
286
+
287
+ # to stream inputs directly from S3 (s3:// URIs), install the optional 's3' extra:
288
+ pip install "grobid-client-python[s3]"
279
289
  ```
280
290
 
281
291
  ### Development Version
@@ -339,15 +349,16 @@ grobid_client [OPTIONS] SERVICE
339
349
 
340
350
  #### Common Options
341
351
 
342
- | Option | Description | Default |
343
- |-------------|--------------------------|-------------------------|
344
- | `--input` | Input directory path | Required |
345
- | `--output` | Output directory path | Same as input |
346
- | `--server` | GROBID server URL | `http://localhost:8070` |
347
- | `--n` | Concurrency level | 10 |
348
- | `--config` | Config file path | Optional |
349
- | `--force` | Overwrite existing files | False |
350
- | `--verbose` | Enable verbose logging | False |
352
+ | Option | Description | Default |
353
+ |-----------------|---------------------------------------------------|-------------------------|
354
+ | `--input` | Input directory path | Required |
355
+ | `--output` | Output directory path | Same as input |
356
+ | `--server` | GROBID server URL | `http://localhost:8070` |
357
+ | `--n` | Concurrency level | 10 |
358
+ | `--config` | Config file path | Optional |
359
+ | `--force` | Overwrite existing files | False |
360
+ | `--skip_errors` | Also skip documents that failed in a previous run | False |
361
+ | `--verbose` | Enable verbose logging | False |
351
362
 
352
363
  #### Processing Options
353
364
 
@@ -385,8 +396,49 @@ grobid_client --server https://grobid.example.com --input ~/citations.txt proces
385
396
 
386
397
  # Force reprocessing with sentence segmentation and JSON output
387
398
  grobid_client --input ~/docs --force --segment_sentences --json processFulltextDocument
399
+
400
+ # Resume an interrupted run without retrying the documents that already failed
401
+ grobid_client --input ~/docs --output ~/results --skip_errors processFulltextDocument
402
+
403
+ # Process PDFs directly from a zip or tar.gz archive (streamed, not fully decompressed)
404
+ grobid_client --input ~/papers.zip --output ~/results processFulltextDocument
405
+ grobid_client --input ~/papers.tar.gz --output ~/results processFulltextDocument
406
+
407
+ # --input also accepts glob patterns (quote them so the shell does not expand them)
408
+ grobid_client --input "~/papers/*.zip" --output ~/results processFulltextDocument # many archives
409
+ grobid_client --input "~/data/**/*.pdf" --output ~/results processFulltextDocument # PDFs in subdirectories
388
410
  ```
389
411
 
412
+ > [!NOTE]
413
+ > `--input` accepts a directory, a single file, an **archive**, or a **glob pattern**:
414
+ > - **Archives** (`.zip`, `.tar`, `.tar.gz`/`.tgz`, `.tar.bz2`/`.tbz2`) are streamed: eligible entries are read into
415
+ > memory in chunks of `queue_size` and sent to GROBID straight from there — the archive is never fully decompressed and
416
+ > nothing but the results in `--output` ever touches the disk. If `--output` is omitted, results go to a directory named
417
+ > after the archive (e.g. `papers.zip` → `papers/`).
418
+ > - **Glob patterns** (`paper.zip`, `paper*.zip`, `**/paper*.zip`, `**/*.pdf`, …) are expanded with `**` recursion; each
419
+ > match is handled by type (archive → streamed, directory → recursed, file → processed). Quote the pattern so your shell
420
+ > passes it through to the client unexpanded.
421
+ > - **S3** (requires `pip install "grobid-client-python[s3]"`): pass an `s3://` object, prefix or glob. A remote zip is
422
+ > **range-streamed** (only its central directory and the entries are fetched — never the whole object); loose remote
423
+ > PDFs are fetched a batch at a time, directly into memory without being written to a local file first. Credentials use
424
+ > the standard AWS chain (env vars / `~/.aws` / IAM role).
425
+ > ```bash
426
+ > grobid_client --input "s3://my-bucket/papers/2021.zip" --output ~/out processFulltextDocument # one remote zip
427
+ > grobid_client --input "s3://my-bucket/pdfs/*.pdf" --output ~/out processFulltextDocument # loose PDFs
428
+ > grobid_client --input "s3://my-bucket/zips/" --output ~/out processFulltextDocument # every object under a prefix
429
+ > ```
430
+ >
431
+ > A **manifest of paths** (local, glob or `s3://`, one per line, `#` comments allowed) can be processed together via
432
+ > `--input-list paths.txt` (combinable with `--input`).
433
+
434
+ > [!NOTE]
435
+ > **Skipping already handled documents.** By default a re-run skips a document only when its TEI output already exists,
436
+ > so documents that failed are sent to GROBID again. Since a failed document generally fails again unless something
437
+ > changed, `--skip_errors` also skips the documents for which a previous run left an error file
438
+ > (`<name>_<status>.txt`, e.g. `paper_500.txt`) next to the expected TEI output. Drop the flag (or use `--force`) to
439
+ > retry them. Error files are kept in sync automatically: the marker is deleted once the document is processed
440
+ > successfully, and replaced when the same document fails again with a different status code.
441
+
390
442
  ### Python Library
391
443
 
392
444
  #### Basic Usage
@@ -443,6 +495,15 @@ client.process(
443
495
  markdown_output=True
444
496
  )
445
497
 
498
+ # Re-run without retrying the documents that failed before
499
+ client.process(
500
+ service="processFulltextDocument",
501
+ input_path="/path/to/pdfs",
502
+ output_path="/path/to/output",
503
+ force=False,
504
+ skip_errors=True
505
+ )
506
+
446
507
  ```python
447
508
  # Process citation lists
448
509
  client.process(
@@ -452,6 +513,63 @@ client.process(
452
513
  )
453
514
  ```
454
515
 
516
+ #### Processing a PDF from memory
517
+
518
+ A PDF that is already in memory - downloaded from an API, read from a database or an object store - can be sent
519
+ directly, without writing it to a temporary file first. `process_pdf` takes either a path or the document itself, as
520
+ `bytes` or as any binary stream, and returns the TEI as a string:
521
+
522
+ ```python
523
+ import io
524
+ import requests
525
+
526
+ pdf = io.BytesIO(requests.get("https://example.org/paper.pdf").content)
527
+ pdf.name = "paper.pdf" # optional, see below
528
+
529
+ name, status, tei = client.process_pdf(
530
+ service="processFulltextDocument",
531
+ pdf_file=pdf,
532
+ consolidate_header=True,
533
+ tei_coordinates=True
534
+ )
535
+
536
+ if status == 200:
537
+ print(tei)
538
+ ```
539
+
540
+ There is no flag to say where the document comes from: the object itself says it. A document also carries its own name,
541
+ taken from the `name` attribute that `open()` sets on files and that can be set on anything else, `io.BytesIO` included.
542
+ The name identifies the document in the request sent to GROBID, in the logs, and as the first element of the result, so
543
+ documents processed this way stay distinguishable. Bytes passed on their own have nothing to be named after and fall
544
+ back to `document.pdf`.
545
+
546
+ Several documents can be sent concurrently with `process_documents`, which runs them through the same thread pool the
547
+ file-based processing uses:
548
+
549
+ ```python
550
+ results = client.process_documents(
551
+ service="processFulltextDocument",
552
+ documents=[pdf1, pdf2, "/path/to/paper3.pdf"],
553
+ n=10 # documents sent concurrently
554
+ )
555
+
556
+ for name, status, tei in results:
557
+ ...
558
+ ```
559
+
560
+ Documents that do not name themselves are named `document-1.pdf`, `document-2.pdf`, ... after their position. Results
561
+ come back **in the order the documents were given**, not in completion order, so they can be zipped back onto whatever
562
+ the caller has them keyed by. A document that fails does not stop the others: its own entry carries the error status.
563
+
564
+ Before an in-memory run starts (this includes archive and `s3://` streaming), the client asks the server's `/api/health`
565
+ how many engines it actually has and logs a warning when the requested concurrency `n` exceeds them - the surplus
566
+ requests would only queue on the server or bounce as 503 - and an info message when engines would sit idle. The check is
567
+ advisory: a server without the endpoint (older GROBID) never blocks the run.
568
+
569
+ > [!NOTE]
570
+ > Both return the TEI instead of writing it to disk, so the caller decides what to do with it. Use `process()` for the
571
+ > directory-oriented processing with resume and JSON/Markdown conversion.
572
+
455
573
  ### Standalone Conversion Tools
456
574
 
457
575
  The library includes standalone scripts to convert TEI XML files to other formats without using the main client or server.
@@ -488,7 +606,6 @@ settings.
488
606
  ```json
489
607
  {
490
608
  "grobid_server": "http://localhost:8070",
491
- "batch_size": 1000,
492
609
  "sleep_time": 5,
493
610
  "timeout": 60,
494
611
  "coordinates": [
@@ -507,7 +624,7 @@ settings.
507
624
  | Parameter | Description | Default |
508
625
  |-----------------|------------------------------------------------------------------------------------------------------------------|-------------------------|
509
626
  | `grobid_server` | GROBID server URL | `http://localhost:8070` |
510
- | `batch_size` | Thread pool size. **Tune carefully: a large batch size will result in the data being written less frequently** | 1000 |
627
+ | `queue_size` | Number of files queued per processing chunk. See [Choosing a queue size](#choosing-a-queue-size). | 1000 for local directories, 1.2 × `n` for archives and S3 |
511
628
  | `sleep_time` | Wait time when server is busy (seconds) | 5 |
512
629
  | `timeout` | Client-side timeout (seconds) | 180 |
513
630
  | `coordinates` | XML elements for coordinate extraction | See above |
@@ -517,6 +634,25 @@ settings.
517
634
  > Since version 0.0.12, the config file is optional. The client will use default localhost settings if no configuration
518
635
  > is provided.
519
636
 
637
+ ### Choosing a queue size
638
+
639
+ `queue_size` controls how many files are grouped into one processing chunk. It is a memory knob, not a
640
+ concurrency one: parallelism toward the GROBID server is set by `-n`, and each chunk is processed `n` files at a time.
641
+ When `queue_size` is not set, the client picks a sensible default per input type (see the table above); set it
642
+ explicitly only if you need to override that. A few guidelines:
643
+
644
+ - **Never set it below `n`.** Effective parallelism is `min(n, queue_size)`: a queue smaller than the thread pool
645
+ leaves workers idle. A bit of headroom above `n` (the default streaming value is 1.2 × `n`) keeps the pool busy.
646
+ - **Results are written as they complete.** Each output file lands on disk as soon as its document has been
647
+ processed, whatever the queue size, so an interrupted run only loses the documents that were in flight
648
+ (already-written results are skipped on re-run unless `--force` is used).
649
+ - **Local PDF directories:** the queue holds only file paths (each file is opened when it is sent), so large values
650
+ are essentially free — the default is 1000. At the end of each chunk the pool waits for its slowest document, so a
651
+ large queue keeps the workers busy.
652
+ - **Archives (zip/tar) and S3:** each chunk is read or downloaded *into memory* before processing starts, so peak RAM
653
+ grows with `queue_size × average file size`. Keep it moderate — the 1.2 × `n` default is a safe floor; going up to
654
+ a few multiples of `n` (e.g. 2–5 ×) trades memory for slightly better throughput around chunk boundaries.
655
+
520
656
  > [!WARNING]
521
657
  > **Citation consolidation and the `timeout` setting.** When `--consolidate_citations` (or `consolidate_citations=True`)
522
658
  > is enabled, GROBID queries external services (e.g. CrossRef) to enrich the extracted references. This is considerably
@@ -33,6 +33,10 @@ concurrent processing capabilities for PDF documents, reference strings, and pat
33
33
  - **Sentence Segmentation**: Layout-aware sentence segmentation capabilities
34
34
  - **JSON Output**: Convert TEI XML output to structured JSON format with CORD-19-like structure
35
35
  - **Markdown Output**: Convert TEI XML output to clean Markdown format with structured sections
36
+ - **Type Hints**: Ships inline type annotations and a `py.typed` marker (PEP 561) for static type checking
37
+ - **Archive Streaming**: Process files directly from `.zip`/`.tar`/`.tar.gz` archives without fully decompressing them
38
+ - **S3 Streaming**: Read PDFs and zips straight from `s3://` (range-streamed, no full download) with the optional `[s3]` extra
39
+ - **In-Memory Documents**: Send PDFs held as bytes straight to GROBID, without writing them to disk first
36
40
 
37
41
  ## 📋 Prerequisites
38
42
 
@@ -57,6 +61,9 @@ Choose one of the following installation methods:
57
61
 
58
62
  ```bash
59
63
  pip install grobid-client-python
64
+
65
+ # to stream inputs directly from S3 (s3:// URIs), install the optional 's3' extra:
66
+ pip install "grobid-client-python[s3]"
60
67
  ```
61
68
 
62
69
  ### Development Version
@@ -120,15 +127,16 @@ grobid_client [OPTIONS] SERVICE
120
127
 
121
128
  #### Common Options
122
129
 
123
- | Option | Description | Default |
124
- |-------------|--------------------------|-------------------------|
125
- | `--input` | Input directory path | Required |
126
- | `--output` | Output directory path | Same as input |
127
- | `--server` | GROBID server URL | `http://localhost:8070` |
128
- | `--n` | Concurrency level | 10 |
129
- | `--config` | Config file path | Optional |
130
- | `--force` | Overwrite existing files | False |
131
- | `--verbose` | Enable verbose logging | False |
130
+ | Option | Description | Default |
131
+ |-----------------|---------------------------------------------------|-------------------------|
132
+ | `--input` | Input directory path | Required |
133
+ | `--output` | Output directory path | Same as input |
134
+ | `--server` | GROBID server URL | `http://localhost:8070` |
135
+ | `--n` | Concurrency level | 10 |
136
+ | `--config` | Config file path | Optional |
137
+ | `--force` | Overwrite existing files | False |
138
+ | `--skip_errors` | Also skip documents that failed in a previous run | False |
139
+ | `--verbose` | Enable verbose logging | False |
132
140
 
133
141
  #### Processing Options
134
142
 
@@ -166,8 +174,49 @@ grobid_client --server https://grobid.example.com --input ~/citations.txt proces
166
174
 
167
175
  # Force reprocessing with sentence segmentation and JSON output
168
176
  grobid_client --input ~/docs --force --segment_sentences --json processFulltextDocument
177
+
178
+ # Resume an interrupted run without retrying the documents that already failed
179
+ grobid_client --input ~/docs --output ~/results --skip_errors processFulltextDocument
180
+
181
+ # Process PDFs directly from a zip or tar.gz archive (streamed, not fully decompressed)
182
+ grobid_client --input ~/papers.zip --output ~/results processFulltextDocument
183
+ grobid_client --input ~/papers.tar.gz --output ~/results processFulltextDocument
184
+
185
+ # --input also accepts glob patterns (quote them so the shell does not expand them)
186
+ grobid_client --input "~/papers/*.zip" --output ~/results processFulltextDocument # many archives
187
+ grobid_client --input "~/data/**/*.pdf" --output ~/results processFulltextDocument # PDFs in subdirectories
169
188
  ```
170
189
 
190
+ > [!NOTE]
191
+ > `--input` accepts a directory, a single file, an **archive**, or a **glob pattern**:
192
+ > - **Archives** (`.zip`, `.tar`, `.tar.gz`/`.tgz`, `.tar.bz2`/`.tbz2`) are streamed: eligible entries are read into
193
+ > memory in chunks of `queue_size` and sent to GROBID straight from there — the archive is never fully decompressed and
194
+ > nothing but the results in `--output` ever touches the disk. If `--output` is omitted, results go to a directory named
195
+ > after the archive (e.g. `papers.zip` → `papers/`).
196
+ > - **Glob patterns** (`paper.zip`, `paper*.zip`, `**/paper*.zip`, `**/*.pdf`, …) are expanded with `**` recursion; each
197
+ > match is handled by type (archive → streamed, directory → recursed, file → processed). Quote the pattern so your shell
198
+ > passes it through to the client unexpanded.
199
+ > - **S3** (requires `pip install "grobid-client-python[s3]"`): pass an `s3://` object, prefix or glob. A remote zip is
200
+ > **range-streamed** (only its central directory and the entries are fetched — never the whole object); loose remote
201
+ > PDFs are fetched a batch at a time, directly into memory without being written to a local file first. Credentials use
202
+ > the standard AWS chain (env vars / `~/.aws` / IAM role).
203
+ > ```bash
204
+ > grobid_client --input "s3://my-bucket/papers/2021.zip" --output ~/out processFulltextDocument # one remote zip
205
+ > grobid_client --input "s3://my-bucket/pdfs/*.pdf" --output ~/out processFulltextDocument # loose PDFs
206
+ > grobid_client --input "s3://my-bucket/zips/" --output ~/out processFulltextDocument # every object under a prefix
207
+ > ```
208
+ >
209
+ > A **manifest of paths** (local, glob or `s3://`, one per line, `#` comments allowed) can be processed together via
210
+ > `--input-list paths.txt` (combinable with `--input`).
211
+
212
+ > [!NOTE]
213
+ > **Skipping already handled documents.** By default a re-run skips a document only when its TEI output already exists,
214
+ > so documents that failed are sent to GROBID again. Since a failed document generally fails again unless something
215
+ > changed, `--skip_errors` also skips the documents for which a previous run left an error file
216
+ > (`<name>_<status>.txt`, e.g. `paper_500.txt`) next to the expected TEI output. Drop the flag (or use `--force`) to
217
+ > retry them. Error files are kept in sync automatically: the marker is deleted once the document is processed
218
+ > successfully, and replaced when the same document fails again with a different status code.
219
+
171
220
  ### Python Library
172
221
 
173
222
  #### Basic Usage
@@ -224,6 +273,15 @@ client.process(
224
273
  markdown_output=True
225
274
  )
226
275
 
276
+ # Re-run without retrying the documents that failed before
277
+ client.process(
278
+ service="processFulltextDocument",
279
+ input_path="/path/to/pdfs",
280
+ output_path="/path/to/output",
281
+ force=False,
282
+ skip_errors=True
283
+ )
284
+
227
285
  ```python
228
286
  # Process citation lists
229
287
  client.process(
@@ -233,6 +291,63 @@ client.process(
233
291
  )
234
292
  ```
235
293
 
294
+ #### Processing a PDF from memory
295
+
296
+ A PDF that is already in memory - downloaded from an API, read from a database or an object store - can be sent
297
+ directly, without writing it to a temporary file first. `process_pdf` takes either a path or the document itself, as
298
+ `bytes` or as any binary stream, and returns the TEI as a string:
299
+
300
+ ```python
301
+ import io
302
+ import requests
303
+
304
+ pdf = io.BytesIO(requests.get("https://example.org/paper.pdf").content)
305
+ pdf.name = "paper.pdf" # optional, see below
306
+
307
+ name, status, tei = client.process_pdf(
308
+ service="processFulltextDocument",
309
+ pdf_file=pdf,
310
+ consolidate_header=True,
311
+ tei_coordinates=True
312
+ )
313
+
314
+ if status == 200:
315
+ print(tei)
316
+ ```
317
+
318
+ There is no flag to say where the document comes from: the object itself says it. A document also carries its own name,
319
+ taken from the `name` attribute that `open()` sets on files and that can be set on anything else, `io.BytesIO` included.
320
+ The name identifies the document in the request sent to GROBID, in the logs, and as the first element of the result, so
321
+ documents processed this way stay distinguishable. Bytes passed on their own have nothing to be named after and fall
322
+ back to `document.pdf`.
323
+
324
+ Several documents can be sent concurrently with `process_documents`, which runs them through the same thread pool the
325
+ file-based processing uses:
326
+
327
+ ```python
328
+ results = client.process_documents(
329
+ service="processFulltextDocument",
330
+ documents=[pdf1, pdf2, "/path/to/paper3.pdf"],
331
+ n=10 # documents sent concurrently
332
+ )
333
+
334
+ for name, status, tei in results:
335
+ ...
336
+ ```
337
+
338
+ Documents that do not name themselves are named `document-1.pdf`, `document-2.pdf`, ... after their position. Results
339
+ come back **in the order the documents were given**, not in completion order, so they can be zipped back onto whatever
340
+ the caller has them keyed by. A document that fails does not stop the others: its own entry carries the error status.
341
+
342
+ Before an in-memory run starts (this includes archive and `s3://` streaming), the client asks the server's `/api/health`
343
+ how many engines it actually has and logs a warning when the requested concurrency `n` exceeds them - the surplus
344
+ requests would only queue on the server or bounce as 503 - and an info message when engines would sit idle. The check is
345
+ advisory: a server without the endpoint (older GROBID) never blocks the run.
346
+
347
+ > [!NOTE]
348
+ > Both return the TEI instead of writing it to disk, so the caller decides what to do with it. Use `process()` for the
349
+ > directory-oriented processing with resume and JSON/Markdown conversion.
350
+
236
351
  ### Standalone Conversion Tools
237
352
 
238
353
  The library includes standalone scripts to convert TEI XML files to other formats without using the main client or server.
@@ -269,7 +384,6 @@ settings.
269
384
  ```json
270
385
  {
271
386
  "grobid_server": "http://localhost:8070",
272
- "batch_size": 1000,
273
387
  "sleep_time": 5,
274
388
  "timeout": 60,
275
389
  "coordinates": [
@@ -288,7 +402,7 @@ settings.
288
402
  | Parameter | Description | Default |
289
403
  |-----------------|------------------------------------------------------------------------------------------------------------------|-------------------------|
290
404
  | `grobid_server` | GROBID server URL | `http://localhost:8070` |
291
- | `batch_size` | Thread pool size. **Tune carefully: a large batch size will result in the data being written less frequently** | 1000 |
405
+ | `queue_size` | Number of files queued per processing chunk. See [Choosing a queue size](#choosing-a-queue-size). | 1000 for local directories, 1.2 × `n` for archives and S3 |
292
406
  | `sleep_time` | Wait time when server is busy (seconds) | 5 |
293
407
  | `timeout` | Client-side timeout (seconds) | 180 |
294
408
  | `coordinates` | XML elements for coordinate extraction | See above |
@@ -298,6 +412,25 @@ settings.
298
412
  > Since version 0.0.12, the config file is optional. The client will use default localhost settings if no configuration
299
413
  > is provided.
300
414
 
415
+ ### Choosing a queue size
416
+
417
+ `queue_size` controls how many files are grouped into one processing chunk. It is a memory knob, not a
418
+ concurrency one: parallelism toward the GROBID server is set by `-n`, and each chunk is processed `n` files at a time.
419
+ When `queue_size` is not set, the client picks a sensible default per input type (see the table above); set it
420
+ explicitly only if you need to override that. A few guidelines:
421
+
422
+ - **Never set it below `n`.** Effective parallelism is `min(n, queue_size)`: a queue smaller than the thread pool
423
+ leaves workers idle. A bit of headroom above `n` (the default streaming value is 1.2 × `n`) keeps the pool busy.
424
+ - **Results are written as they complete.** Each output file lands on disk as soon as its document has been
425
+ processed, whatever the queue size, so an interrupted run only loses the documents that were in flight
426
+ (already-written results are skipped on re-run unless `--force` is used).
427
+ - **Local PDF directories:** the queue holds only file paths (each file is opened when it is sent), so large values
428
+ are essentially free — the default is 1000. At the end of each chunk the pool waits for its slowest document, so a
429
+ large queue keeps the workers busy.
430
+ - **Archives (zip/tar) and S3:** each chunk is read or downloaded *into memory* before processing starts, so peak RAM
431
+ grows with `queue_size × average file size`. Keep it moderate — the 1.2 × `n` default is a safe floor; going up to
432
+ a few multiples of `n` (e.g. 2–5 ×) trades memory for slightly better throughput around chunk boundaries.
433
+
301
434
  > [!WARNING]
302
435
  > **Citation consolidation and the `timeout` setting.** When `--consolidate_citations` (or `consolidate_citations=True`)
303
436
  > is enabled, GROBID queries external services (e.g. CrossRef) to enrich the extracted references. This is considerably
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "grobid_server": "http://localhost:8070",
3
- "batch_size": 1000,
4
3
  "timeout": 180,
5
4
  "sleep_time": 5,
6
5
  "coordinates": [
@@ -1,6 +1,10 @@
1
1
  """ Generic API Client """
2
+ from __future__ import annotations
3
+
2
4
  from copy import deepcopy
3
5
  import json
6
+ from typing import Any, Optional, Tuple
7
+
4
8
  import requests
5
9
 
6
10
  try:
@@ -16,12 +20,17 @@ class ApiClient(object):
16
20
  service methods, i.e. ``get``, ``post``, ``put`` and ``delete``.
17
21
  """
18
22
 
19
- accept_type = "application/xml"
20
- api_base = None
23
+ accept_type: str = "application/xml"
24
+ api_base: Optional[str] = None
21
25
 
22
26
  def __init__(
23
- self, base_url, username=None, api_key=None, status_endpoint=None, timeout=60
24
- ):
27
+ self,
28
+ base_url: str,
29
+ username: Optional[str] = None,
30
+ api_key: Optional[str] = None,
31
+ status_endpoint: Optional[str] = None,
32
+ timeout: int = 60,
33
+ ) -> None:
25
34
  """Initialise client.
26
35
 
27
36
  Args:
@@ -37,7 +46,7 @@ class ApiClient(object):
37
46
  self.timeout = timeout
38
47
 
39
48
  @staticmethod
40
- def encode(request, data):
49
+ def encode(request: Any, data: Optional[dict]) -> Any:
41
50
  """Add request content data to request body, set Content-type header.
42
51
 
43
52
  Should be overridden by subclasses if not using JSON encoding.
@@ -58,7 +67,7 @@ class ApiClient(object):
58
67
  return request
59
68
 
60
69
  @staticmethod
61
- def decode(response):
70
+ def decode(response: Any) -> Any:
62
71
  """Decode the returned data in the response.
63
72
 
64
73
  Should be overridden by subclasses if something else than JSON is
@@ -73,9 +82,9 @@ class ApiClient(object):
73
82
  try:
74
83
  return response.json()
75
84
  except ValueError as e:
76
- return e.message
85
+ return e.message # type: ignore[attr-defined] # pre-existing (Python 2 style)
77
86
 
78
- def get_credentials(self):
87
+ def get_credentials(self) -> dict:
79
88
  """Returns parameters to be added to authenticate the request.
80
89
 
81
90
  This lives on its own to make it easier to re-implement it if needed.
@@ -87,14 +96,14 @@ class ApiClient(object):
87
96
 
88
97
  def call_api(
89
98
  self,
90
- method,
91
- url,
92
- headers=None,
93
- params=None,
94
- data=None,
95
- files=None,
96
- timeout=None,
97
- ):
99
+ method: str,
100
+ url: str,
101
+ headers: Optional[dict] = None,
102
+ params: Optional[dict] = None,
103
+ data: Optional[dict] = None,
104
+ files: Optional[dict] = None,
105
+ timeout: Optional[int] = None,
106
+ ) -> Tuple[requests.Response, int]:
98
107
  """Call API.
99
108
 
100
109
  This returns object containing data, with error details if applicable.
@@ -130,7 +139,7 @@ class ApiClient(object):
130
139
 
131
140
  return r, r.status_code
132
141
 
133
- def get(self, url, params=None, **kwargs):
142
+ def get(self, url: str, params: Optional[dict] = None, **kwargs: Any) -> Tuple[requests.Response, int]:
134
143
  """Call the API with a GET request.
135
144
 
136
145
  Args:
@@ -142,7 +151,7 @@ class ApiClient(object):
142
151
  """
143
152
  return self.call_api("GET", url, params=params, **kwargs)
144
153
 
145
- def delete(self, url, params=None, **kwargs):
154
+ def delete(self, url: str, params: Optional[dict] = None, **kwargs: Any) -> Tuple[requests.Response, int]:
146
155
  """Call the API with a DELETE request.
147
156
 
148
157
  Args:
@@ -154,7 +163,14 @@ class ApiClient(object):
154
163
  """
155
164
  return self.call_api("DELETE", url, params=params, **kwargs)
156
165
 
157
- def put(self, url, params=None, data=None, files=None, **kwargs):
166
+ def put(
167
+ self,
168
+ url: str,
169
+ params: Optional[dict] = None,
170
+ data: Optional[dict] = None,
171
+ files: Optional[dict] = None,
172
+ **kwargs: Any,
173
+ ) -> Tuple[requests.Response, int]:
158
174
  """Call the API with a PUT request.
159
175
 
160
176
  Args:
@@ -170,7 +186,14 @@ class ApiClient(object):
170
186
  "PUT", url, params=params, data=data, files=files, **kwargs
171
187
  )
172
188
 
173
- def post(self, url, params=None, data=None, files=None, **kwargs):
189
+ def post(
190
+ self,
191
+ url: str,
192
+ params: Optional[dict] = None,
193
+ data: Optional[dict] = None,
194
+ files: Optional[dict] = None,
195
+ **kwargs: Any,
196
+ ) -> Tuple[requests.Response, int]:
174
197
  """Call the API with a POST request.
175
198
 
176
199
  Args:
@@ -186,7 +209,7 @@ class ApiClient(object):
186
209
  method="POST", url=url, params=params, data=data, files=files, **kwargs
187
210
  )
188
211
 
189
- def service_status(self, **kwargs):
212
+ def service_status(self, **kwargs: Any) -> Tuple[requests.Response, int]:
190
213
  """Call the API to get the status of the service.
191
214
 
192
215
  Returns: