Skip to content

Commit c5c449e

Browse files
authored
Add --server to override any baseurl (#90)
1 parent fb544b0 commit c5c449e

5 files changed

Lines changed: 167 additions & 102 deletions

File tree

Readme.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ usage: grobid_client [-h] [--input INPUT] [--output OUTPUT] [--config CONFIG]
5555
[--n N] [--generateIDs] [--consolidate_header]
5656
[--consolidate_citations] [--include_raw_citations]
5757
[--include_raw_affiliations] [--force] [--teiCoordinates]
58-
[--verbose] [--flavor FLAVOR]
58+
[--verbose] [--flavor FLAVOR] [--server SERVER]
5959
service
6060
6161
Client for GROBID services
@@ -75,6 +75,7 @@ optional arguments:
7575
(optional)
7676
--config CONFIG path to the config file, default is ./config.json
7777
--n N concurrency for service usage
78+
--server SERVER GROBID server URL (default: http://localhost:8070)
7879
--generateIDs generate random xml:id to textual XML elements of the
7980
result files
8081
--consolidate_header call GROBID with consolidation of the metadata
@@ -132,6 +133,15 @@ The following command example will process all the PDF files present in the inpu
132133
> grobid_client --input ~/tmp/in2 --output ~/tmp/out --teiCoordinates --segmentSentences processFulltextDocument
133134
```
134135

136+
To use a different GROBID server (e.g., a hosted service), use the `--server` argument:
137+
138+
```console
139+
> grobid_client --server https://lfoppiano-grobid.hf.space --input ~/tmp/in2 --output ~/tmp/out processFulltextDocument
140+
```
141+
142+
> [!NOTE]
143+
> The `--server` argument will override the server URL specified in the config file. If both are provided, the CLI argument takes precedence.
144+
135145
The file `example.py` gives an example of usage as a library, from a another python script.
136146

137147
## Using the client in your python
@@ -141,16 +151,24 @@ Import and call the client as follow:
141151
```python
142152
from grobid_client.grobid_client import GrobidClient
143153

154+
# Using default localhost server
144155
client = GrobidClient(config_path="./config.json")
145156
client.process("processFulltextDocument", "/mnt/data/covid/pdfs", n=20)
157+
158+
# Using a custom server
159+
client = GrobidClient(grobid_server="https://lfoppiano-grobid.hf.space", config_path="./config.json")
160+
client.process("processFulltextDocument", "/mnt/data/covid/pdfs", n=20)
146161
```
147162

148163
See also `example.py`.
149164

150165
## Configuration of the client
151166

152167
> [!TIP]
153-
> from version 0.0.12 the `config.json` will be optional, by default the client will connect to the local server (`http://localhost:8070`).
168+
> from version 0.0.12 the `config.json` will be optional, by default the client will connect to the local server (`http://localhost:8070`).
169+
170+
> [!NOTE]
171+
> When using the CLI, the `--server` argument will override the `grobid_server` value in the config file. This allows you to use a config file for most settings while easily switching servers via command line.
154172
155173
There are a few parameters that can be set with the `config.json` file.
156174

example.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
from grobid_client.grobid_client import GrobidClient
22

33
if __name__ == "__main__":
4+
# Example 1: Using config file values (no constructor parameters)
45
client = GrobidClient(config_path="./config.json")
56
client.process("processFulltextDocument", "./resources/test_pdf", output="./resources/test_out/", consolidate_citations=True, tei_coordinates=True, force=True)
7+
8+
# Example 2: Overriding config file with explicit server parameter
9+
# client = GrobidClient(grobid_server="https://lfoppiano-grobid.hf.space", config_path="./config.json")
10+
# client.process("processFulltextDocument", "./resources/test_pdf", output="./resources/test_out/", consolidate_citations=True, tei_coordinates=True, force=True)
11+
12+
# Example 3: Using default values (no config file, no parameters)
13+
# client = GrobidClient()
14+
# client.process("processFulltextDocument", "./resources/test_pdf", output="./resources/test_out/", consolidate_citations=True, tei_coordinates=True, force=True)

grobid_client/grobid_client.py

Lines changed: 132 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import pathlib
2525
import logging
2626
from typing import Tuple
27+
import copy
2728

2829
from .client import ApiClient
2930

@@ -37,60 +38,91 @@ def __init__(self, message="GROBID server is not available"):
3738

3839

3940
class GrobidClient(ApiClient):
41+
# Default configuration values
42+
DEFAULT_CONFIG = {
43+
'grobid_server': 'http://localhost:8070',
44+
'batch_size': 1000,
45+
'sleep_time': 5,
46+
'timeout': 180,
47+
'coordinates': [
48+
"title",
49+
"persName",
50+
"affiliation",
51+
"orgName",
52+
"formula",
53+
"figure",
54+
"ref",
55+
"biblStruct",
56+
"head",
57+
"p",
58+
"s",
59+
"note"
60+
],
61+
'logging': {
62+
'level': 'INFO',
63+
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
64+
'console': True,
65+
'file': None, # Disabled by default
66+
'max_file_size': '10MB',
67+
'backup_count': 3
68+
}
69+
}
4070

4171
def __init__(
4272
self,
43-
grobid_server='http://localhost:8070',
44-
batch_size=1000,
73+
grobid_server=None,
74+
batch_size=None,
4575
coordinates=None,
46-
sleep_time=5,
47-
timeout=180,
76+
sleep_time=None,
77+
timeout=None,
4878
config_path=None,
4979
check_server=True
5080
):
51-
# Set default coordinates if None provided
52-
if coordinates is None:
53-
coordinates = [
54-
"title",
55-
"persName",
56-
"affiliation",
57-
"orgName",
58-
"formula",
59-
"figure",
60-
"ref",
61-
"biblStruct",
62-
"head",
63-
"p",
64-
"s",
65-
"note"
66-
]
67-
68-
self.config = {
81+
# Initialize config with defaults
82+
self.config = copy.deepcopy(self.DEFAULT_CONFIG)
83+
84+
# Load config file (which may override current values)
85+
if config_path:
86+
self._load_config(config_path)
87+
88+
# Constructor parameters take precedence over config file values
89+
# This ensures CLI arguments override config file values
90+
self._set_config_params({
6991
'grobid_server': grobid_server,
7092
'batch_size': batch_size,
7193
'coordinates': coordinates,
7294
'sleep_time': sleep_time,
73-
'timeout': timeout,
74-
'logging': {
75-
'level': 'INFO',
76-
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
77-
'console': True,
78-
'file': None, # Disabled by default
79-
'max_file_size': '10MB',
80-
'backup_count': 3
81-
}
82-
}
83-
84-
# Load config first (which may override logging settings)
85-
if config_path:
86-
self._load_config(config_path)
95+
'timeout': timeout
96+
})
8797

8898
# Configure logging based on config
8999
self._configure_logging()
90100

91101
if check_server:
92102
self._test_server_connection()
93103

104+
def _set_config_params(self, params):
105+
"""Set configuration parameters, only if they are not None."""
106+
for key, value in params.items():
107+
if value is not None:
108+
self.config[key] = value
109+
110+
def _handle_server_busy_retry(self, file_path, retry_func, *args, **kwargs):
111+
"""Handle server busy (503) retry logic."""
112+
self.logger.warning(f"Server busy (503), retrying {file_path} after {self.config['sleep_time']} seconds")
113+
time.sleep(self.config["sleep_time"])
114+
return retry_func(*args, **kwargs)
115+
116+
def _handle_request_error(self, file_path, error, error_type="Request"):
117+
"""Handle request errors with consistent logging and return format."""
118+
self.logger.error(f"{error_type} failed for {file_path}: {str(error)}")
119+
return (file_path, 500, f"{error_type} failed: {str(error)}")
120+
121+
def _handle_unexpected_error(self, file_path, error):
122+
"""Handle unexpected errors with consistent logging and return format."""
123+
self.logger.error(f"Unexpected error processing {file_path}: {str(error)}")
124+
return (file_path, 500, f"Unexpected error: {str(error)}")
125+
94126
def _configure_logging(self):
95127
"""Configure logging based on the configuration settings."""
96128
# Get logging config with defaults
@@ -479,56 +511,53 @@ def process_pdf(
479511
start=-1,
480512
end=-1
481513
):
514+
pdf_handle = None
482515
try:
483516
pdf_handle = open(pdf_file, "rb")
484-
except IOError as e:
485-
self.logger.error(f"Failed to open PDF file {pdf_file}: {str(e)}")
486-
return (pdf_file, 500, f"Failed to open file: {str(e)}")
487-
488-
files = {
489-
"input": (
490-
pdf_file,
491-
pdf_handle,
492-
"application/pdf",
493-
{"Expires": "0"},
494-
)
495-
}
496-
497-
the_url = self.get_server_url(service)
517+
518+
files = {
519+
"input": (
520+
pdf_file,
521+
pdf_handle,
522+
"application/pdf",
523+
{"Expires": "0"},
524+
)
525+
}
498526

499-
# set the GROBID parameters
500-
the_data = {}
501-
if generateIDs:
502-
the_data["generateIDs"] = "1"
503-
if consolidate_header:
504-
the_data["consolidateHeader"] = "1"
505-
if consolidate_citations:
506-
the_data["consolidateCitations"] = "1"
507-
if include_raw_citations:
508-
the_data["includeRawCitations"] = "1"
509-
if include_raw_affiliations:
510-
the_data["includeRawAffiliations"] = "1"
511-
if tei_coordinates:
512-
the_data["teiCoordinates"] = self.config["coordinates"]
513-
if segment_sentences:
514-
the_data["segmentSentences"] = "1"
515-
if flavor:
516-
the_data["flavor"] = flavor
517-
if start and start > 0:
518-
the_data["start"] = str(start)
519-
if end and end > 0:
520-
the_data["end"] = str(end)
527+
the_url = self.get_server_url(service)
528+
529+
# set the GROBID parameters
530+
the_data = {}
531+
if generateIDs:
532+
the_data["generateIDs"] = "1"
533+
if consolidate_header:
534+
the_data["consolidateHeader"] = "1"
535+
if consolidate_citations:
536+
the_data["consolidateCitations"] = "1"
537+
if include_raw_citations:
538+
the_data["includeRawCitations"] = "1"
539+
if include_raw_affiliations:
540+
the_data["includeRawAffiliations"] = "1"
541+
if tei_coordinates:
542+
the_data["teiCoordinates"] = self.config["coordinates"]
543+
if segment_sentences:
544+
the_data["segmentSentences"] = "1"
545+
if flavor:
546+
the_data["flavor"] = flavor
547+
if start and start > 0:
548+
the_data["start"] = str(start)
549+
if end and end > 0:
550+
the_data["end"] = str(end)
521551

522-
try:
523552
res, status = self.post(
524553
url=the_url, files=files, data=the_data, headers={"Accept": "text/plain"},
525554
timeout=self.config['timeout']
526555
)
527556

528557
if status == 503:
529-
self.logger.warning(f"Server busy (503), retrying {pdf_file} after {self.config['sleep_time']} seconds")
530-
time.sleep(self.config["sleep_time"])
531-
return self.process_pdf(
558+
return self._handle_server_busy_retry(
559+
pdf_file,
560+
self.process_pdf,
532561
service,
533562
pdf_file,
534563
generateIDs,
@@ -542,21 +571,22 @@ def process_pdf(
542571
start,
543572
end
544573
)
574+
575+
return (pdf_file, status, res.text)
576+
577+
except IOError as e:
578+
self.logger.error(f"Failed to open PDF file {pdf_file}: {str(e)}")
579+
return (pdf_file, 400, f"Failed to open file: {str(e)}")
545580
except requests.exceptions.ReadTimeout as e:
546581
self.logger.error(f"Request timeout for {pdf_file}: {str(e)}")
547-
pdf_handle.close()
548582
return (pdf_file, 408, f"Request timeout: {str(e)}")
549583
except requests.exceptions.RequestException as e:
550-
self.logger.error(f"Request failed for {pdf_file}: {str(e)}")
551-
pdf_handle.close()
552-
return (pdf_file, 500, f"Request failed: {str(e)}")
584+
return self._handle_request_error(pdf_file, e)
553585
except Exception as e:
554-
self.logger.error(f"Unexpected error processing {pdf_file}: {str(e)}")
555-
pdf_handle.close()
556-
return (pdf_file, 500, f"Unexpected error: {str(e)}")
557-
558-
pdf_handle.close()
559-
return (pdf_file, status, res.text)
586+
return self._handle_unexpected_error(pdf_file, e)
587+
finally:
588+
if pdf_handle:
589+
pdf_handle.close()
560590

561591
def get_server_url(self, service):
562592
return self.config['grobid_server'] + "/api/" + service
@@ -600,9 +630,9 @@ def process_txt(
600630
)
601631

602632
if status == 503:
603-
self.logger.warning(f"Server busy (503), retrying {txt_file} after {self.config['sleep_time']} seconds")
604-
time.sleep(self.config["sleep_time"])
605-
return self.process_txt(
633+
return self._handle_server_busy_retry(
634+
txt_file,
635+
self.process_txt,
606636
service,
607637
txt_file,
608638
generateIDs,
@@ -614,11 +644,9 @@ def process_txt(
614644
segment_sentences
615645
)
616646
except requests.exceptions.RequestException as e:
617-
self.logger.error(f"Request failed for {txt_file}: {str(e)}")
618-
return (txt_file, 500, f"Request failed: {str(e)}")
647+
return self._handle_request_error(txt_file, e)
619648
except Exception as e:
620-
self.logger.error(f"Unexpected error processing {txt_file}: {str(e)}")
621-
return (txt_file, 500, f"Unexpected error: {str(e)}")
649+
return self._handle_unexpected_error(txt_file, e)
622650

623651
return (txt_file, status, res.text)
624652

@@ -718,6 +746,11 @@ def main():
718746
default=None,
719747
help="Define the flavor to be used for the fulltext extraction",
720748
)
749+
parser.add_argument(
750+
"--server",
751+
default=None,
752+
help="GROBID server URL override of the config file. If config not provided, default is http://localhost:8070",
753+
)
721754

722755
args = parser.parse_args()
723756

@@ -736,7 +769,12 @@ def main():
736769

737770
# Initialize GrobidClient which will configure logging based on config.json
738771
try:
739-
client = GrobidClient(config_path=config_path)
772+
# Only pass grobid_server if it was explicitly provided (not the default)
773+
client_kwargs = {'config_path': config_path}
774+
if args.server is not None: # Only override if user specified a different server
775+
client_kwargs['grobid_server'] = args.server
776+
777+
client = GrobidClient(**client_kwargs)
740778
# Now use the client's logger for all subsequent logging
741779
logger = client.logger
742780
except ServerUnavailableException as e:

0 commit comments

Comments
 (0)