grobid-client-python 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- grobid_client/grobid_client.py +107 -83
- {grobid_client_python-0.2.0.dist-info → grobid_client_python-0.2.2.dist-info}/METADATA +10 -6
- {grobid_client_python-0.2.0.dist-info → grobid_client_python-0.2.2.dist-info}/RECORD +6 -6
- {grobid_client_python-0.2.0.dist-info → grobid_client_python-0.2.2.dist-info}/WHEEL +1 -1
- {grobid_client_python-0.2.0.dist-info → grobid_client_python-0.2.2.dist-info}/entry_points.txt +0 -0
- {grobid_client_python-0.2.0.dist-info → grobid_client_python-0.2.2.dist-info}/top_level.txt +0 -0
grobid_client/grobid_client.py
CHANGED
|
@@ -34,7 +34,7 @@ import shutil
|
|
|
34
34
|
import tarfile
|
|
35
35
|
import tempfile
|
|
36
36
|
import zipfile
|
|
37
|
-
from typing import Any, BinaryIO, Callable, Optional, Tuple, Union
|
|
37
|
+
from typing import Any, BinaryIO, Callable, List, Optional, Tuple, Union
|
|
38
38
|
import copy
|
|
39
39
|
|
|
40
40
|
from .format.TEI2LossyJSON import TEI2LossyJSONConverter
|
|
@@ -449,10 +449,15 @@ class GrobidClient(ApiClient):
|
|
|
449
449
|
input_file_path = pathlib.Path(input_file)
|
|
450
450
|
|
|
451
451
|
if output is not None:
|
|
452
|
-
#
|
|
452
|
+
# Mirror the input tree under the output directory: the path of the
|
|
453
|
+
# file relative to input_path is kept (only the extension changes),
|
|
454
|
+
# so sub/paper.pdf -> <output>/sub/paper.grobid.tei.xml. Keeping the
|
|
455
|
+
# subdirectories also prevents two inputs that share a basename
|
|
456
|
+
# from mapping to the same output file.
|
|
453
457
|
input_path_abs = pathlib.Path(input_path).resolve()
|
|
454
458
|
input_file_rel = input_file_path.resolve().relative_to(input_path_abs)
|
|
455
|
-
filename = pathlib.Path(output) /
|
|
459
|
+
filename = pathlib.Path(output) / input_file_rel.with_name(
|
|
460
|
+
f"{input_file_rel.stem}.grobid.tei.xml")
|
|
456
461
|
else:
|
|
457
462
|
# Use the same directory as the input file
|
|
458
463
|
filename = input_file_path.parent / f"{input_file_path.stem}.grobid.tei.xml"
|
|
@@ -629,9 +634,14 @@ class GrobidClient(ApiClient):
|
|
|
629
634
|
|
|
630
635
|
# Partition into archives (streamed), remote loose files (s3) and local
|
|
631
636
|
# filesystem files (directories are expanded to their eligible files).
|
|
637
|
+
# Local files are grouped with the base directory their output paths
|
|
638
|
+
# are computed from: a directory given as input is the base of the
|
|
639
|
+
# files found inside it (so the output mirrors its tree), while loose
|
|
640
|
+
# files (single paths, glob matches) share a common ancestor.
|
|
632
641
|
archive_paths = []
|
|
633
642
|
remote_files = []
|
|
634
|
-
|
|
643
|
+
fs_groups: List[Tuple[str, list]] = []
|
|
644
|
+
loose_files = []
|
|
635
645
|
for path in matched_paths:
|
|
636
646
|
if self._is_s3(path):
|
|
637
647
|
if self._looks_like_archive(path):
|
|
@@ -643,12 +653,20 @@ class GrobidClient(ApiClient):
|
|
|
643
653
|
elif self._is_archive(path):
|
|
644
654
|
archive_paths.append(path)
|
|
645
655
|
elif os.path.isdir(path):
|
|
646
|
-
|
|
656
|
+
dir_files = self._collect_directory_files(path, service)
|
|
657
|
+
if dir_files:
|
|
658
|
+
fs_groups.append((path, dir_files))
|
|
647
659
|
elif os.path.isfile(path) and self._is_eligible_input(os.path.basename(path), service):
|
|
648
|
-
|
|
660
|
+
loose_files.append(path)
|
|
661
|
+
elif not os.path.exists(path):
|
|
662
|
+
self.logger.warning(f"Input path does not exist: {path}")
|
|
649
663
|
else:
|
|
650
664
|
self.logger.debug(f"Skipping input (not an eligible file/dir/archive): {path}")
|
|
651
665
|
|
|
666
|
+
if loose_files:
|
|
667
|
+
fs_groups.append((self._common_base(loose_files), loose_files))
|
|
668
|
+
fs_files = [f for _, files in fs_groups for f in files]
|
|
669
|
+
|
|
652
670
|
if not fs_files and not archive_paths and not remote_files:
|
|
653
671
|
self.logger.warning(f"No eligible files found in input(s): {inputs}")
|
|
654
672
|
return
|
|
@@ -667,16 +685,17 @@ class GrobidClient(ApiClient):
|
|
|
667
685
|
# Local files gathered from directories and/or loose glob matches
|
|
668
686
|
if fs_files:
|
|
669
687
|
print(f"Found {len(fs_files)} local file(s) to process")
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
688
|
+
for base_dir, group_files in fs_groups:
|
|
689
|
+
bp, be, bs = self._run_file_batches(
|
|
690
|
+
service, group_files, base_dir, output, n,
|
|
691
|
+
generate_ids, consolidate_header, consolidate_citations,
|
|
692
|
+
include_raw_citations, include_raw_affiliations, tei_coordinates,
|
|
693
|
+
segment_sentences, force, verbose, flavor, json_output, markdown_output,
|
|
694
|
+
skip_errors=skip_errors
|
|
695
|
+
)
|
|
696
|
+
processed_files_count += bp
|
|
697
|
+
errors_files_count += be
|
|
698
|
+
skipped_files_count += bs
|
|
680
699
|
total_files += len(fs_files)
|
|
681
700
|
|
|
682
701
|
# Loose remote (s3) files: streamed into memory one chunk at a time
|
|
@@ -927,6 +946,10 @@ class GrobidClient(ApiClient):
|
|
|
927
946
|
|
|
928
947
|
def _is_eligible_input(self, filename: str, service: str) -> bool:
|
|
929
948
|
"""Return True if a file name is a valid input for the given service."""
|
|
949
|
+
# macOS AppleDouble metadata entries (._paper.pdf), which tar and zip
|
|
950
|
+
# add next to the real files, carry the extension but not the document
|
|
951
|
+
if filename.startswith("._"):
|
|
952
|
+
return False
|
|
930
953
|
if filename.endswith(".pdf") or filename.endswith(".PDF"):
|
|
931
954
|
return True
|
|
932
955
|
if service == 'processCitationList' and (
|
|
@@ -1090,8 +1113,9 @@ class GrobidClient(ApiClient):
|
|
|
1090
1113
|
the chunk size and nothing but the results ever touches the disk. (The
|
|
1091
1114
|
exception is ``processCitationList``, whose ``.txt`` inputs are read
|
|
1092
1115
|
from a path and therefore still go through a temporary directory.)
|
|
1093
|
-
Output files follow the same
|
|
1094
|
-
processing
|
|
1116
|
+
Output files follow the same naming convention as directory
|
|
1117
|
+
processing: the entry's path inside the archive is mirrored under
|
|
1118
|
+
``output`` (``nested/y.pdf`` -> ``<output>/nested/y.grobid.tei.xml``).
|
|
1095
1119
|
"""
|
|
1096
1120
|
start_time = time.time()
|
|
1097
1121
|
self._warn_on_consolidation_timeout(consolidate_citations)
|
|
@@ -1529,75 +1553,75 @@ class GrobidClient(ApiClient):
|
|
|
1529
1553
|
|
|
1530
1554
|
results.append(r)
|
|
1531
1555
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1556
|
+
for r in concurrent.futures.as_completed(results):
|
|
1557
|
+
input_file, status, text = r.result()
|
|
1558
|
+
filename = self._output_file_name(input_file, input_path, output)
|
|
1535
1559
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1560
|
+
if status != 200 or text is None:
|
|
1561
|
+
self.logger.error(f"Processing of {input_file} failed with error {status}: {text}")
|
|
1562
|
+
error_count += 1
|
|
1563
|
+
# writing error file with suffixed error code
|
|
1564
|
+
error_filename = self._error_file_name(filename, status)
|
|
1565
|
+
try:
|
|
1566
|
+
self._write_atomic(error_filename, text if text is not None else "")
|
|
1567
|
+
self.logger.info(f"Error details written to {error_filename}")
|
|
1568
|
+
# A previous run may have failed with a different status code;
|
|
1569
|
+
# keep only the marker of this run so the error state of the
|
|
1570
|
+
# document stays unambiguous (and skippable).
|
|
1571
|
+
self._remove_error_files(filename, keep=error_filename)
|
|
1572
|
+
except OSError as e:
|
|
1573
|
+
self.logger.error(f"Failed to write error file {filename}: {str(e)}")
|
|
1574
|
+
else:
|
|
1575
|
+
processed_count += 1
|
|
1576
|
+
# writing TEI file
|
|
1577
|
+
try:
|
|
1578
|
+
self._write_atomic(filename, text)
|
|
1579
|
+
self.logger.debug(f"Successfully wrote TEI file: {filename}")
|
|
1556
1580
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1581
|
+
# The document no longer is in error: drop the marker left by
|
|
1582
|
+
# a previous failed run so it is not skipped by --skip-errors.
|
|
1583
|
+
self._remove_error_files(filename)
|
|
1560
1584
|
|
|
1561
1585
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1586
|
+
# Convert to JSON if requested
|
|
1587
|
+
if json_output:
|
|
1588
|
+
try:
|
|
1589
|
+
converter = TEI2LossyJSONConverter()
|
|
1590
|
+
json_data = converter.convert_tei_file(filename, stream=False)
|
|
1591
|
+
|
|
1592
|
+
if json_data:
|
|
1593
|
+
json_filename = filename.replace('.grobid.tei.xml', '.json')
|
|
1594
|
+
# Always write JSON file when TEI is written (respects --force behavior)
|
|
1595
|
+
json_filename_expanded = os.path.expanduser(json_filename)
|
|
1596
|
+
self._write_atomic(
|
|
1597
|
+
json_filename_expanded,
|
|
1598
|
+
json.dumps(json_data, indent=2, ensure_ascii=False))
|
|
1599
|
+
self.logger.debug(f"Successfully wrote JSON file: {json_filename_expanded}")
|
|
1600
|
+
else:
|
|
1601
|
+
self.logger.warning(f"Failed to convert TEI to JSON for {filename}")
|
|
1602
|
+
except Exception as e:
|
|
1603
|
+
self.logger.error(f"Failed to convert TEI to JSON for {filename}: {str(e)}")
|
|
1604
|
+
|
|
1605
|
+
# Convert to Markdown if requested
|
|
1606
|
+
if markdown_output:
|
|
1607
|
+
try:
|
|
1608
|
+
from .format.TEI2Markdown import TEI2MarkdownConverter
|
|
1609
|
+
converter = TEI2MarkdownConverter()
|
|
1610
|
+
markdown_data = converter.convert_tei_file(filename)
|
|
1611
|
+
|
|
1612
|
+
if markdown_data is not None:
|
|
1613
|
+
markdown_filename = filename.replace('.grobid.tei.xml', '.md')
|
|
1614
|
+
# Always write Markdown file when TEI is written (respects --force behavior)
|
|
1615
|
+
markdown_filename_expanded = os.path.expanduser(markdown_filename)
|
|
1616
|
+
self._write_atomic(markdown_filename_expanded, markdown_data)
|
|
1617
|
+
self.logger.debug(f"Successfully wrote Markdown file: {markdown_filename_expanded}")
|
|
1618
|
+
else:
|
|
1619
|
+
self.logger.warning(f"Failed to convert TEI to Markdown for {filename}")
|
|
1620
|
+
except Exception as e:
|
|
1621
|
+
self.logger.error(f"Failed to convert TEI to Markdown for {filename}: {str(e)}")
|
|
1622
|
+
|
|
1623
|
+
except OSError as e:
|
|
1624
|
+
self.logger.error(f"Failed to write TEI XML file {filename}: {str(e)}")
|
|
1601
1625
|
|
|
1602
1626
|
# Calculate batch statistics
|
|
1603
1627
|
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.
|
|
3
|
+
Version: 0.2.2
|
|
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>
|
|
@@ -411,6 +411,9 @@ grobid_client --input "~/data/**/*.pdf" --output ~/results processFulltextDocu
|
|
|
411
411
|
|
|
412
412
|
> [!NOTE]
|
|
413
413
|
> `--input` accepts a directory, a single file, an **archive**, or a **glob pattern**:
|
|
414
|
+
> - **Directories** are processed recursively and the output mirrors the input tree: `~/pdfs/2023/a.pdf` →
|
|
415
|
+
> `~/results/2023/a.grobid.tei.xml` (subdirectories of `--output` are created as needed, and the JSON/Markdown
|
|
416
|
+
> outputs and error files sit next to each TEI file). Archives keep their internal paths the same way.
|
|
414
417
|
> - **Archives** (`.zip`, `.tar`, `.tar.gz`/`.tgz`, `.tar.bz2`/`.tbz2`) are streamed: eligible entries are read into
|
|
415
418
|
> memory in chunks of `queue_size` and sent to GROBID straight from there — the archive is never fully decompressed and
|
|
416
419
|
> nothing but the results in `--output` ever touches the disk. If `--output` is omitted, results go to a directory named
|
|
@@ -636,18 +639,19 @@ settings.
|
|
|
636
639
|
|
|
637
640
|
### Choosing a queue size
|
|
638
641
|
|
|
639
|
-
`queue_size` controls how many files are grouped into one processing chunk. It is a memory
|
|
642
|
+
`queue_size` controls how many files are grouped into one processing chunk. It is a memory knob, not a
|
|
640
643
|
concurrency one: parallelism toward the GROBID server is set by `-n`, and each chunk is processed `n` files at a time.
|
|
641
644
|
When `queue_size` is not set, the client picks a sensible default per input type (see the table above); set it
|
|
642
645
|
explicitly only if you need to override that. A few guidelines:
|
|
643
646
|
|
|
644
647
|
- **Never set it below `n`.** Effective parallelism is `min(n, queue_size)`: a queue smaller than the thread pool
|
|
645
648
|
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
|
|
647
|
-
|
|
649
|
+
- **Results are written as they complete.** Each output file lands on disk as soon as its document has been
|
|
650
|
+
processed, whatever the queue size, so an interrupted run only loses the documents that were in flight
|
|
648
651
|
(already-written results are skipped on re-run unless `--force` is used).
|
|
649
|
-
- **Local PDF directories:** the queue holds only file paths
|
|
650
|
-
1000.
|
|
652
|
+
- **Local PDF directories:** the queue holds only file paths (each file is opened when it is sent), so large values
|
|
653
|
+
are essentially free — the default is 1000. At the end of each chunk the pool waits for its slowest document, so a
|
|
654
|
+
large queue keeps the workers busy.
|
|
651
655
|
- **Archives (zip/tar) and S3:** each chunk is read or downloaded *into memory* before processing starts, so peak RAM
|
|
652
656
|
grows with `queue_size × average file size`. Keep it moderate — the 1.2 × `n` default is a safe floor; going up to
|
|
653
657
|
a few multiples of `n` (e.g. 2–5 ×) trades memory for slightly better throughput around chunk boundaries.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
grobid_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
grobid_client/client.py,sha256=kAmRDQECMt6ck7AytnDAvMz8-KnM4QCIEp6rfFHqdzI,6925
|
|
3
|
-
grobid_client/grobid_client.py,sha256=
|
|
3
|
+
grobid_client/grobid_client.py,sha256=slqvpBW-3m-p1JQMQtfDJiZTrEEeLQ4-TyC3Ns_CRQc,94970
|
|
4
4
|
grobid_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
grobid_client/format/TEI2LossyJSON.py,sha256=5KtHaQbP2leMEiFLi3yV70SPSDqKvCq-Zf3jmHTSEj8,50337
|
|
6
6
|
grobid_client/format/TEI2LossyJSON_cli.py,sha256=TPsrE913MJBlTsVrd4Oc16AF3tXqwi_IzTt8UYcpZnU,3848
|
|
@@ -9,8 +9,8 @@ grobid_client/format/TEI2Markdown_cli.py,sha256=SZ5zMT0Lo7QG0DXypPyuA5TuJrgieVJs
|
|
|
9
9
|
grobid_client/format/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
grobid_client/format/__main__.py,sha256=jklj0sd9_2QbZm7MWwnZsdH1XtepKTJX2hbZBCMvK78,1848
|
|
11
11
|
grobid_client/format/validate_json_refs.py,sha256=iJimV0d9iPYqpw9b54C-c39AmtAKuyfMEWLS2F-QwRQ,14893
|
|
12
|
-
grobid_client_python-0.2.
|
|
13
|
-
grobid_client_python-0.2.
|
|
14
|
-
grobid_client_python-0.2.
|
|
15
|
-
grobid_client_python-0.2.
|
|
16
|
-
grobid_client_python-0.2.
|
|
12
|
+
grobid_client_python-0.2.2.dist-info/METADATA,sha256=X1FrPOqH42bSaFRAjPXQ1Qgg529YjpbR-YtQQes__Ss,43992
|
|
13
|
+
grobid_client_python-0.2.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
14
|
+
grobid_client_python-0.2.2.dist-info/entry_points.txt,sha256=-WwPi8D8eXEbDB6QvitvHjxSGBeyLCyFvAoZcaDGswQ,67
|
|
15
|
+
grobid_client_python-0.2.2.dist-info/top_level.txt,sha256=HnjgWxdJNKTLIN86XkgRAfVeEK4C71fc9X4fe7qdMvA,14
|
|
16
|
+
grobid_client_python-0.2.2.dist-info/RECORD,,
|
{grobid_client_python-0.2.0.dist-info → grobid_client_python-0.2.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|