grobid-client-python 0.2.0__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 (72) hide show
  1. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/PKG-INFO +7 -6
  2. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/Readme.md +6 -5
  3. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/grobid_client.py +71 -65
  4. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/PKG-INFO +7 -6
  5. grobid_client_python-0.2.1/grobid_client_python.egg-info/scm_version.json +8 -0
  6. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/pyproject.toml +1 -1
  7. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/test_grobid_client.py +37 -0
  8. grobid_client_python-0.2.0/grobid_client_python.egg-info/scm_version.json +0 -8
  9. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/.github/dependabot.yml +0 -0
  10. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/.github/workflows/ci-build.yml +0 -0
  11. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/.github/workflows/ci-release.yml +0 -0
  12. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/.gitignore +0 -0
  13. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/LICENSE +0 -0
  14. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/MANIFEST.in +0 -0
  15. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/config.json +0 -0
  16. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/example.py +0 -0
  17. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/__init__.py +0 -0
  18. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/client.py +0 -0
  19. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/format/TEI2LossyJSON.py +0 -0
  20. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/format/TEI2LossyJSON_cli.py +0 -0
  21. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/format/TEI2Markdown.py +0 -0
  22. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/format/TEI2Markdown_cli.py +0 -0
  23. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/format/__init__.py +0 -0
  24. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/format/__main__.py +0 -0
  25. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/format/validate_json_refs.py +0 -0
  26. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client/py.typed +0 -0
  27. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/SOURCES.txt +0 -0
  28. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/dependency_links.txt +0 -0
  29. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/entry_points.txt +0 -0
  30. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/requires.txt +0 -0
  31. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/scm_file_list.json +0 -0
  32. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/grobid_client_python.egg-info/top_level.txt +0 -0
  33. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/pytest.ini +0 -0
  34. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/requirements.txt +0 -0
  35. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/20180928112135.png +0 -0
  36. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/20180928112138.csv +0 -0
  37. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_pdf/0046d83a-edd6-4631-b57c-755cdcce8b7f.pdf +0 -0
  38. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_pdf/0046d83a-edd6-4631-b57c-755cdcce8b7f.tei.xml +0 -0
  39. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_pdf/mjb3wlzxcb2mc-migowebupload-1766042162782.grobid.tei.xml +0 -0
  40. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-1.txt +0 -0
  41. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-2.txt +0 -0
  42. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-3.txt +0 -0
  43. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-4.txt +0 -0
  44. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-5.txt +0 -0
  45. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-6.txt +0 -0
  46. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/resources/test_txt/reference-strings-7.txt +0 -0
  47. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/setup.cfg +0 -0
  48. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/__init__.py +0 -0
  49. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/conftest.py +0 -0
  50. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/0046d83a-edd6-4631-b57c-755cdcce8b7f.tei.xml +0 -0
  51. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/__init__.py +0 -0
  52. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/article_withdrawn.grobid.tei.xml +0 -0
  53. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41477-023-01501-1.grobid.tei.xml +0 -0
  54. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41477-023-01501-1.json +0 -0
  55. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41586-023-05895-y.grobid.tei.xml +0 -0
  56. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41586-023-05895-y.json +0 -0
  57. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41598-023-32039-z.grobid.tei.xml +0 -0
  58. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1038_s41598-023-32039-z.json +0 -0
  59. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1371_journal.pone.0218311.grobid.tei.xml +0 -0
  60. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.1371_journal.pone.0218311.json +0 -0
  61. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.7554_elife.78558.grobid.tei.xml +0 -0
  62. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/10.7554_elife.78558.json +0 -0
  63. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/refs_offsets/2021.naacl-main.224.grobid.tei.xml +0 -0
  64. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/repeated_text/ijms-24-05988.grobid.tei.xml +0 -0
  65. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/repeated_text/ijms-24-05988.json +0 -0
  66. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/resources/repeated_text/ijms-24-05988.md +0 -0
  67. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/run_tests.py +0 -0
  68. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/test_client.py +0 -0
  69. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/test_config.json +0 -0
  70. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/test_conversions.py +0 -0
  71. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/test_integration.py +0 -0
  72. {grobid_client_python-0.2.0 → grobid_client_python-0.2.1}/tests/test_s3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grobid-client-python
3
- Version: 0.2.0
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>
@@ -636,18 +636,19 @@ settings.
636
636
 
637
637
  ### Choosing a queue size
638
638
 
639
- `queue_size` controls how many files are grouped into one processing chunk. It is a memory/durability knob, not a
639
+ `queue_size` controls how many files are grouped into one processing chunk. It is a memory knob, not a
640
640
  concurrency one: parallelism toward the GROBID server is set by `-n`, and each chunk is processed `n` files at a time.
641
641
  When `queue_size` is not set, the client picks a sensible default per input type (see the table above); set it
642
642
  explicitly only if you need to override that. A few guidelines:
643
643
 
644
644
  - **Never set it below `n`.** Effective parallelism is `min(n, queue_size)`: a queue smaller than the thread pool
645
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 per chunk.** Output files land on disk only once a whole chunk has been processed, so a
647
- larger queue means results are written less frequently and an interrupted run loses at most one chunk of work
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
648
  (already-written results are skipped on re-run unless `--force` is used).
649
- - **Local PDF directories:** the queue holds only file paths, so large values are essentially free — the default is
650
- 1000. Lower it if you want results flushed to disk more often on long runs.
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.
651
652
  - **Archives (zip/tar) and S3:** each chunk is read or downloaded *into memory* before processing starts, so peak RAM
652
653
  grows with `queue_size × average file size`. Keep it moderate — the 1.2 × `n` default is a safe floor; going up to
653
654
  a few multiples of `n` (e.g. 2–5 ×) trades memory for slightly better throughput around chunk boundaries.
@@ -414,18 +414,19 @@ settings.
414
414
 
415
415
  ### Choosing a queue size
416
416
 
417
- `queue_size` controls how many files are grouped into one processing chunk. It is a memory/durability knob, not a
417
+ `queue_size` controls how many files are grouped into one processing chunk. It is a memory knob, not a
418
418
  concurrency one: parallelism toward the GROBID server is set by `-n`, and each chunk is processed `n` files at a time.
419
419
  When `queue_size` is not set, the client picks a sensible default per input type (see the table above); set it
420
420
  explicitly only if you need to override that. A few guidelines:
421
421
 
422
422
  - **Never set it below `n`.** Effective parallelism is `min(n, queue_size)`: a queue smaller than the thread pool
423
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 per chunk.** Output files land on disk only once a whole chunk has been processed, so a
425
- larger queue means results are written less frequently and an interrupted run loses at most one chunk of work
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
426
  (already-written results are skipped on re-run unless `--force` is used).
427
- - **Local PDF directories:** the queue holds only file paths, so large values are essentially free — the default is
428
- 1000. Lower it if you want results flushed to disk more often on long runs.
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.
429
430
  - **Archives (zip/tar) and S3:** each chunk is read or downloaded *into memory* before processing starts, so peak RAM
430
431
  grows with `queue_size × average file size`. Keep it moderate — the 1.2 × `n` default is a safe floor; going up to
431
432
  a few multiples of `n` (e.g. 2–5 ×) trades memory for slightly better throughput around chunk boundaries.
@@ -646,6 +646,8 @@ class GrobidClient(ApiClient):
646
646
  fs_files.extend(self._collect_directory_files(path, service))
647
647
  elif os.path.isfile(path) and self._is_eligible_input(os.path.basename(path), service):
648
648
  fs_files.append(path)
649
+ elif not os.path.exists(path):
650
+ self.logger.warning(f"Input path does not exist: {path}")
649
651
  else:
650
652
  self.logger.debug(f"Skipping input (not an eligible file/dir/archive): {path}")
651
653
 
@@ -927,6 +929,10 @@ class GrobidClient(ApiClient):
927
929
 
928
930
  def _is_eligible_input(self, filename: str, service: str) -> bool:
929
931
  """Return True if a file name is a valid input for the given service."""
932
+ # macOS AppleDouble metadata entries (._paper.pdf), which tar and zip
933
+ # add next to the real files, carry the extension but not the document
934
+ if filename.startswith("._"):
935
+ return False
930
936
  if filename.endswith(".pdf") or filename.endswith(".PDF"):
931
937
  return True
932
938
  if service == 'processCitationList' and (
@@ -1529,75 +1535,75 @@ class GrobidClient(ApiClient):
1529
1535
 
1530
1536
  results.append(r)
1531
1537
 
1532
- for r in concurrent.futures.as_completed(results):
1533
- input_file, status, text = r.result()
1534
- filename = self._output_file_name(input_file, input_path, output)
1538
+ for r in concurrent.futures.as_completed(results):
1539
+ input_file, status, text = r.result()
1540
+ filename = self._output_file_name(input_file, input_path, output)
1535
1541
 
1536
- if status != 200 or text is None:
1537
- self.logger.error(f"Processing of {input_file} failed with error {status}: {text}")
1538
- error_count += 1
1539
- # writing error file with suffixed error code
1540
- error_filename = self._error_file_name(filename, status)
1541
- try:
1542
- self._write_atomic(error_filename, text if text is not None else "")
1543
- self.logger.info(f"Error details written to {error_filename}")
1544
- # A previous run may have failed with a different status code;
1545
- # keep only the marker of this run so the error state of the
1546
- # document stays unambiguous (and skippable).
1547
- self._remove_error_files(filename, keep=error_filename)
1548
- except OSError as e:
1549
- self.logger.error(f"Failed to write error file {filename}: {str(e)}")
1550
- else:
1551
- processed_count += 1
1552
- # writing TEI file
1553
- try:
1554
- self._write_atomic(filename, text)
1555
- self.logger.debug(f"Successfully wrote TEI file: {filename}")
1542
+ if status != 200 or text is None:
1543
+ self.logger.error(f"Processing of {input_file} failed with error {status}: {text}")
1544
+ error_count += 1
1545
+ # writing error file with suffixed error code
1546
+ error_filename = self._error_file_name(filename, status)
1547
+ try:
1548
+ self._write_atomic(error_filename, text if text is not None else "")
1549
+ self.logger.info(f"Error details written to {error_filename}")
1550
+ # A previous run may have failed with a different status code;
1551
+ # keep only the marker of this run so the error state of the
1552
+ # document stays unambiguous (and skippable).
1553
+ self._remove_error_files(filename, keep=error_filename)
1554
+ except OSError as e:
1555
+ self.logger.error(f"Failed to write error file {filename}: {str(e)}")
1556
+ else:
1557
+ processed_count += 1
1558
+ # writing TEI file
1559
+ try:
1560
+ self._write_atomic(filename, text)
1561
+ self.logger.debug(f"Successfully wrote TEI file: {filename}")
1556
1562
 
1557
- # The document no longer is in error: drop the marker left by
1558
- # a previous failed run so it is not skipped by --skip-errors.
1559
- self._remove_error_files(filename)
1563
+ # The document no longer is in error: drop the marker left by
1564
+ # a previous failed run so it is not skipped by --skip-errors.
1565
+ self._remove_error_files(filename)
1560
1566
 
1561
1567
 
1562
- # Convert to JSON if requested
1563
- if json_output:
1564
- try:
1565
- converter = TEI2LossyJSONConverter()
1566
- json_data = converter.convert_tei_file(filename, stream=False)
1567
-
1568
- if json_data:
1569
- json_filename = filename.replace('.grobid.tei.xml', '.json')
1570
- # Always write JSON file when TEI is written (respects --force behavior)
1571
- json_filename_expanded = os.path.expanduser(json_filename)
1572
- self._write_atomic(
1573
- json_filename_expanded,
1574
- json.dumps(json_data, indent=2, ensure_ascii=False))
1575
- self.logger.debug(f"Successfully wrote JSON file: {json_filename_expanded}")
1576
- else:
1577
- self.logger.warning(f"Failed to convert TEI to JSON for {filename}")
1578
- except Exception as e:
1579
- self.logger.error(f"Failed to convert TEI to JSON for {filename}: {str(e)}")
1580
-
1581
- # Convert to Markdown if requested
1582
- if markdown_output:
1583
- try:
1584
- from .format.TEI2Markdown import TEI2MarkdownConverter
1585
- converter = TEI2MarkdownConverter()
1586
- markdown_data = converter.convert_tei_file(filename)
1587
-
1588
- if markdown_data is not None:
1589
- markdown_filename = filename.replace('.grobid.tei.xml', '.md')
1590
- # Always write Markdown file when TEI is written (respects --force behavior)
1591
- markdown_filename_expanded = os.path.expanduser(markdown_filename)
1592
- self._write_atomic(markdown_filename_expanded, markdown_data)
1593
- self.logger.debug(f"Successfully wrote Markdown file: {markdown_filename_expanded}")
1594
- else:
1595
- self.logger.warning(f"Failed to convert TEI to Markdown for {filename}")
1596
- except Exception as e:
1597
- self.logger.error(f"Failed to convert TEI to Markdown for {filename}: {str(e)}")
1598
-
1599
- except OSError as e:
1600
- self.logger.error(f"Failed to write TEI XML file {filename}: {str(e)}")
1568
+ # Convert to JSON if requested
1569
+ if json_output:
1570
+ try:
1571
+ converter = TEI2LossyJSONConverter()
1572
+ json_data = converter.convert_tei_file(filename, stream=False)
1573
+
1574
+ if json_data:
1575
+ json_filename = filename.replace('.grobid.tei.xml', '.json')
1576
+ # Always write JSON file when TEI is written (respects --force behavior)
1577
+ json_filename_expanded = os.path.expanduser(json_filename)
1578
+ self._write_atomic(
1579
+ json_filename_expanded,
1580
+ json.dumps(json_data, indent=2, ensure_ascii=False))
1581
+ self.logger.debug(f"Successfully wrote JSON file: {json_filename_expanded}")
1582
+ else:
1583
+ self.logger.warning(f"Failed to convert TEI to JSON for {filename}")
1584
+ except Exception as e:
1585
+ self.logger.error(f"Failed to convert TEI to JSON for {filename}: {str(e)}")
1586
+
1587
+ # Convert to Markdown if requested
1588
+ if markdown_output:
1589
+ try:
1590
+ from .format.TEI2Markdown import TEI2MarkdownConverter
1591
+ converter = TEI2MarkdownConverter()
1592
+ markdown_data = converter.convert_tei_file(filename)
1593
+
1594
+ if markdown_data is not None:
1595
+ markdown_filename = filename.replace('.grobid.tei.xml', '.md')
1596
+ # Always write Markdown file when TEI is written (respects --force behavior)
1597
+ markdown_filename_expanded = os.path.expanduser(markdown_filename)
1598
+ self._write_atomic(markdown_filename_expanded, markdown_data)
1599
+ self.logger.debug(f"Successfully wrote Markdown file: {markdown_filename_expanded}")
1600
+ else:
1601
+ self.logger.warning(f"Failed to convert TEI to Markdown for {filename}")
1602
+ except Exception as e:
1603
+ self.logger.error(f"Failed to convert TEI to Markdown for {filename}: {str(e)}")
1604
+
1605
+ except OSError as e:
1606
+ self.logger.error(f"Failed to write TEI XML file {filename}: {str(e)}")
1601
1607
 
1602
1608
  # Calculate batch statistics
1603
1609
  batch_runtime = time.time() - batch_start_time
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grobid-client-python
3
- Version: 0.2.0
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>
@@ -636,18 +636,19 @@ settings.
636
636
 
637
637
  ### Choosing a queue size
638
638
 
639
- `queue_size` controls how many files are grouped into one processing chunk. It is a memory/durability knob, not a
639
+ `queue_size` controls how many files are grouped into one processing chunk. It is a memory knob, not a
640
640
  concurrency one: parallelism toward the GROBID server is set by `-n`, and each chunk is processed `n` files at a time.
641
641
  When `queue_size` is not set, the client picks a sensible default per input type (see the table above); set it
642
642
  explicitly only if you need to override that. A few guidelines:
643
643
 
644
644
  - **Never set it below `n`.** Effective parallelism is `min(n, queue_size)`: a queue smaller than the thread pool
645
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 per chunk.** Output files land on disk only once a whole chunk has been processed, so a
647
- larger queue means results are written less frequently and an interrupted run loses at most one chunk of work
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
648
  (already-written results are skipped on re-run unless `--force` is used).
649
- - **Local PDF directories:** the queue holds only file paths, so large values are essentially free — the default is
650
- 1000. Lower it if you want results flushed to disk more often on long runs.
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.
651
652
  - **Archives (zip/tar) and S3:** each chunk is read or downloaded *into memory* before processing starts, so peak RAM
652
653
  grows with `queue_size × average file size`. Keep it moderate — the 1.2 × `n` default is a safe floor; going up to
653
654
  a few multiples of `n` (e.g. 2–5 ×) trades memory for slightly better throughput around chunk boundaries.
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.1",
3
+ "distance": 0,
4
+ "node": "g39488b4dd319d720c68264c1cda19a1568bdcb76",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-16"
8
+ }
@@ -3,7 +3,7 @@ requires = ["setuptools", "setuptools-scm"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [tool.bumpversion]
6
- current_version = "0.2.0"
6
+ current_version = "0.2.1"
7
7
  commit = "true"
8
8
  tag = "true"
9
9
  tag_name = "v{new_version}"
@@ -1378,6 +1378,43 @@ class TestSkipErrors:
1378
1378
  assert (processed, errors, skipped) == (0, 0, 1)
1379
1379
 
1380
1380
 
1381
+ class TestIncrementalWrites:
1382
+ """Results are written as each document finishes, not once the whole queue is done."""
1383
+
1384
+ def test_finished_document_is_written_while_others_are_in_flight(self):
1385
+ with patch('grobid_client.grobid_client.GrobidClient._test_server_connection'):
1386
+ with patch('grobid_client.grobid_client.GrobidClient._configure_logging'):
1387
+ client = GrobidClient(check_server=False)
1388
+ client.logger = Mock()
1389
+
1390
+ with tempfile.TemporaryDirectory() as d:
1391
+ inp = os.path.join(d, 'in')
1392
+ out = os.path.join(d, 'out')
1393
+ os.makedirs(inp)
1394
+ for name in ('a.pdf', 'b.pdf'):
1395
+ with open(os.path.join(inp, name), 'wb') as f:
1396
+ f.write(b'%PDF')
1397
+ a_output = os.path.join(out, 'a.grobid.tei.xml')
1398
+ seen_while_in_flight = []
1399
+
1400
+ def fake_post(url, files=None, data=None, headers=None, timeout=None):
1401
+ if os.path.basename(files['input'][0]) == 'b.pdf':
1402
+ # Hold b until a's result reaches the disk (or give up)
1403
+ deadline = time.time() + 5
1404
+ while not os.path.isfile(a_output) and time.time() < deadline:
1405
+ time.sleep(0.01)
1406
+ seen_while_in_flight.append(os.path.isfile(a_output))
1407
+ resp = Mock()
1408
+ resp.text = '<TEI>ok</TEI>'
1409
+ return (resp, 200)
1410
+
1411
+ with patch.object(GrobidClient, 'post', side_effect=fake_post):
1412
+ client.process('processFulltextDocument', inp, output=out, n=2)
1413
+
1414
+ assert seen_while_in_flight == [True]
1415
+ assert os.path.isfile(os.path.join(out, 'b.grobid.tei.xml'))
1416
+
1417
+
1381
1418
  class TestInMemoryDocuments:
1382
1419
  """Processing a PDF that is already in memory, without writing it to disk.
1383
1420
 
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.2.0",
3
- "distance": 0,
4
- "node": "gfd1858e863d62f7a4ee0d1a2d8dc4f96d1e407c9",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-22"
8
- }