grobid-client-python 0.0.12__py3-none-any.whl → 0.0.13__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.
@@ -15,7 +15,6 @@ which is not implemented for the moment.
15
15
 
16
16
  """
17
17
  import os
18
- import io
19
18
  import json
20
19
  import argparse
21
20
  import time
@@ -23,58 +22,234 @@ import concurrent.futures
23
22
  import ntpath
24
23
  import requests
25
24
  import pathlib
25
+ import logging
26
+ from typing import Tuple
26
27
 
27
28
  from .client import ApiClient
28
29
 
29
30
 
30
31
  class ServerUnavailableException(Exception):
31
- pass
32
+ """Exception raised when GROBID server is not available or not responding."""
33
+
34
+ def __init__(self, message="GROBID server is not available"):
35
+ super().__init__(message)
36
+ self.message = message
37
+
32
38
 
33
39
  class GrobidClient(ApiClient):
34
40
 
35
- def __init__(self, grobid_server='localhost',
36
- batch_size=1000,
37
- coordinates=["persName", "figure", "ref", "biblStruct", "formula", "s", "note", "title"],
38
- sleep_time=5,
39
- timeout=60,
40
- config_path=None,
41
- check_server=True):
41
+ def __init__(
42
+ self,
43
+ grobid_server='http://localhost:8070',
44
+ batch_size=1000,
45
+ coordinates=None,
46
+ sleep_time=5,
47
+ timeout=60,
48
+ config_path=None,
49
+ check_server=True
50
+ ):
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
+
42
68
  self.config = {
43
69
  'grobid_server': grobid_server,
44
70
  'batch_size': batch_size,
45
71
  'coordinates': coordinates,
46
72
  'sleep_time': sleep_time,
47
- 'timeout': timeout
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
+ }
48
82
  }
83
+
84
+ # Load config first (which may override logging settings)
49
85
  if config_path:
50
86
  self._load_config(config_path)
87
+
88
+ # Configure logging based on config
89
+ self._configure_logging()
90
+
51
91
  if check_server:
52
92
  self._test_server_connection()
53
93
 
94
+ def _configure_logging(self):
95
+ """Configure logging based on the configuration settings."""
96
+ # Get logging config with defaults
97
+ log_config = self.config.get('logging', {})
98
+
99
+ # Parse log level
100
+ log_level_str = log_config.get('level', 'INFO').upper()
101
+ log_level = getattr(logging, log_level_str, logging.INFO)
102
+
103
+ # Parse log format
104
+ log_format = log_config.get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
105
+
106
+ # Create formatter
107
+ formatter = logging.Formatter(log_format)
108
+
109
+ # Configure the logger
110
+ self.logger = logging.getLogger(__name__)
111
+ self.logger.setLevel(log_level)
112
+
113
+ # Clear any existing handlers to avoid duplicates
114
+ for handler in self.logger.handlers[:]:
115
+ self.logger.removeHandler(handler)
116
+
117
+ # Console handler
118
+ if log_config.get('console', True):
119
+ console_handler = logging.StreamHandler()
120
+ console_handler.setLevel(log_level)
121
+ console_handler.setFormatter(formatter)
122
+ self.logger.addHandler(console_handler)
123
+
124
+ # File handler
125
+ log_file = log_config.get('file')
126
+ if log_file:
127
+ try:
128
+ # Parse file size (support formats like "10MB", "1GB", etc.)
129
+ max_bytes = self._parse_file_size(log_config.get('max_file_size', '10MB'))
130
+ backup_count = log_config.get('backup_count', 3)
131
+
132
+ from logging.handlers import RotatingFileHandler
133
+ file_handler = RotatingFileHandler(
134
+ log_file,
135
+ maxBytes=max_bytes,
136
+ backupCount=backup_count
137
+ )
138
+ file_handler.setLevel(log_level)
139
+ file_handler.setFormatter(formatter)
140
+ self.logger.addHandler(file_handler)
141
+
142
+ self.logger.debug(
143
+ f"File logging configured: {log_file} (max size: {max_bytes}, backups: {backup_count})")
144
+ except Exception as e:
145
+ # Fallback to basic file handler if rotating handler fails
146
+ try:
147
+ file_handler = logging.FileHandler(log_file)
148
+ file_handler.setLevel(log_level)
149
+ file_handler.setFormatter(formatter)
150
+ self.logger.addHandler(file_handler)
151
+ self.logger.warning(f"Using basic file handler due to error with rotating handler: {e}")
152
+ except Exception as file_error:
153
+ self.logger.warning(f"Could not configure file logging: {file_error}")
154
+
155
+ self.logger.info(
156
+ f"Logging configured - Level: {log_level_str}, Console: {log_config.get('console', True)}, File: {log_file or 'disabled'}")
157
+
158
+ def _parse_file_size(self, size_str):
159
+ """Parse file size string like '10MB', '1GB' to bytes."""
160
+ size_str = str(size_str).upper().strip()
161
+
162
+ # Extract number and unit
163
+ import re
164
+ match = re.match(r'(\d+(?:\.\d+)?)\s*([KMGT]?B?)', size_str)
165
+ if not match:
166
+ return 10 * 1024 * 1024 # Default 10MB
167
+
168
+ number = float(match.group(1))
169
+ unit = match.group(2)
170
+
171
+ # Convert to bytes
172
+ multipliers = {
173
+ '': 1,
174
+ 'B': 1,
175
+ 'KB': 1024,
176
+ 'MB': 1024 ** 2,
177
+ 'GB': 1024 ** 3,
178
+ 'TB': 1024 ** 4
179
+ }
180
+
181
+ return int(number * multipliers.get(unit, 1))
182
+
54
183
  def _load_config(self, path="./config.json"):
55
184
  """
56
- Load the json configuration
185
+ Load and merge configuration from a JSON file with default values.
186
+ If the file doesn't exist, keep the default values.
187
+
188
+ Args:
189
+ path (str): Path to the JSON configuration file
190
+
191
+ Raises:
192
+ FileNotFoundError: If the config file is not found
193
+ json.JSONDecodeError: If the config file contains invalid JSON
194
+ Exception: For other file reading errors
57
195
  """
58
- config_json = open(path).read()
59
- self.config = json.loads(config_json)
196
+ # Create a temporary logger for configuration loading since main logger isn't configured yet
197
+ temp_logger = logging.getLogger(f"{__name__}.config_loader")
198
+ if not temp_logger.handlers:
199
+ temp_handler = logging.StreamHandler()
200
+ temp_handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
201
+ temp_logger.addHandler(temp_handler)
202
+ temp_logger.setLevel(logging.INFO)
60
203
 
61
- def _test_server_connection(self):
62
- """Test if the server is up and running."""
204
+ try:
205
+ temp_logger.info(f"Loading configuration file from {path}")
206
+ with open(path, 'r') as config_file:
207
+ config_json = config_file.read()
208
+ # Update the default config with values from the file
209
+ file_config = json.loads(config_json)
210
+ self.config.update(file_config)
211
+ temp_logger.info("Configuration file loaded successfully")
212
+ except FileNotFoundError as e:
213
+ # If config file doesn't exist, keep using default values
214
+ error_msg = f"The specified config file {path} was not found. Check the path or leave it blank to use the default configuration."
215
+ temp_logger.error(error_msg)
216
+ raise FileNotFoundError(error_msg) from e
217
+ except json.JSONDecodeError as e:
218
+ # If config exists, but it's invalid, we raise an exception
219
+ error_msg = f"Could not parse config file at {path}: {str(e)}"
220
+ temp_logger.error(error_msg)
221
+ raise json.JSONDecodeError(error_msg, e.doc, e.pos) from e
222
+ except Exception as e:
223
+ error_msg = f"Error reading config file at {path}: {str(e)}"
224
+ temp_logger.error(error_msg)
225
+ raise Exception(error_msg) from e
226
+
227
+ def _test_server_connection(self) -> Tuple[bool, int]:
228
+ """Test if the server is up and running.
229
+
230
+ Returns:
231
+ tuple: (is_available, status_code)
232
+
233
+ Raises:
234
+ ServerUnavailableException: If server is not reachable
235
+ """
63
236
  the_url = self.get_server_url("isalive")
64
237
  try:
65
- r = requests.get(the_url)
66
- except:
67
- print("GROBID server does not appear up and running, the connection to the server failed")
68
- raise ServerUnavailableException
238
+ r = requests.get(the_url, timeout=10)
239
+ status = r.status_code
69
240
 
70
- status = r.status_code
241
+ if status != 200:
242
+ error_msg = f"GROBID server {self.config['grobid_server']} does not appear up and running (status: {status})"
243
+ self.logger.error(error_msg)
244
+ return False, status
245
+ else:
246
+ self.logger.info(f"GROBID server {self.config['grobid_server']} is up and running")
247
+ return True, status
71
248
 
72
- if status != 200:
73
- print("GROBID server does not appear up and running " + str(status))
74
- return False, status
75
- else:
76
- print("GROBID server is up and running")
77
- return True, status
249
+ except requests.exceptions.RequestException as e:
250
+ error_msg = f"GROBID server {self.config['grobid_server']} does not appear up and running, connection failed: {str(e)}"
251
+ self.logger.error(error_msg)
252
+ raise ServerUnavailableException(error_msg) from e
78
253
 
79
254
  def _output_file_name(self, input_file, input_path, output):
80
255
  # we use ntpath here to be sure it will work on Windows too
@@ -92,7 +267,7 @@ class GrobidClient(ApiClient):
92
267
 
93
268
  return filename
94
269
 
95
- def ping(self) -> (bool, int):
270
+ def ping(self) -> Tuple[bool, int]:
96
271
  """
97
272
  Check the Grobid service. Returns True if the service is up.
98
273
  In addition, returns also the status code.
@@ -100,61 +275,85 @@ class GrobidClient(ApiClient):
100
275
  return self._test_server_connection()
101
276
 
102
277
  def process(
103
- self,
104
- service,
105
- input_path,
106
- output=None,
107
- n=10,
108
- generateIDs=False,
109
- consolidate_header=True,
110
- consolidate_citations=False,
111
- include_raw_citations=False,
112
- include_raw_affiliations=False,
113
- tei_coordinates=False,
114
- segment_sentences=False,
115
- force=True,
116
- verbose=False,
117
- flavor=None
278
+ self,
279
+ service,
280
+ input_path,
281
+ output=None,
282
+ n=10,
283
+ generateIDs=False,
284
+ consolidate_header=True,
285
+ consolidate_citations=False,
286
+ include_raw_citations=False,
287
+ include_raw_affiliations=False,
288
+ tei_coordinates=False,
289
+ segment_sentences=False,
290
+ force=True,
291
+ verbose=False,
292
+ flavor=None
118
293
  ):
119
294
  batch_size_pdf = self.config["batch_size"]
120
- input_files = []
121
295
 
296
+ # First pass: count all eligible files
297
+ all_input_files = []
122
298
  for (dirpath, dirnames, filenames) in os.walk(input_path):
123
299
  for filename in filenames:
124
300
  if filename.endswith(".pdf") or filename.endswith(".PDF") or \
125
- (service == 'processCitationList' and (filename.endswith(".txt") or filename.endswith(".TXT"))) or \
126
- (service == 'processCitationPatentST36' and (filename.endswith(".xml") or filename.endswith(".XML"))):
127
- if verbose:
128
- try:
129
- print(filename)
130
- except Exception:
131
- # may happen on linux see https://stackoverflow.com/questions/27366479/python-3-os-walk-file-paths-unicodeencodeerror-utf-8-codec-cant-encode-s
132
- pass
133
- input_files.append(os.sep.join([dirpath, filename]))
134
-
135
- if len(input_files) == batch_size_pdf:
136
- self.process_batch(
137
- service,
138
- input_files,
139
- input_path,
140
- output,
141
- n,
142
- generateIDs,
143
- consolidate_header,
144
- consolidate_citations,
145
- include_raw_citations,
146
- include_raw_affiliations,
147
- tei_coordinates,
148
- segment_sentences,
149
- force,
150
- verbose,
151
- flavor
152
- )
153
- input_files = []
301
+ (service == 'processCitationList' and (
302
+ filename.endswith(".txt") or filename.endswith(".TXT"))) or \
303
+ (service == 'processCitationPatentST36' and (
304
+ filename.endswith(".xml") or filename.endswith(".XML"))):
305
+ full_path = os.sep.join([dirpath, filename])
306
+ all_input_files.append(full_path)
307
+
308
+ # Log total files found
309
+ total_files = len(all_input_files)
310
+ if total_files == 0:
311
+ self.logger.warning(f"No eligible files found in {input_path}")
312
+ return
313
+
314
+ self.logger.info(f"Found {total_files} file(s) to process")
315
+
316
+ # Counter for actually processed files
317
+ processed_files_count = 0
318
+ input_files = []
319
+
320
+ for input_file in all_input_files:
321
+ # Extract just the filename for verbose logging
322
+ filename = os.path.basename(input_file)
323
+
324
+ if verbose:
325
+ try:
326
+ self.logger.info(f"Found file: {filename}")
327
+ except UnicodeEncodeError:
328
+ # may happen on linux see https://stackoverflow.com/questions/27366479/python-3-os-walk-file-paths-unicodeencodeerror-utf-8-codec-cant-encode-s
329
+ self.logger.warning(f"Could not log filename due to encoding issues")
330
+
331
+ input_files.append(input_file)
332
+
333
+ if len(input_files) == batch_size_pdf:
334
+ batch_processed = self.process_batch(
335
+ service,
336
+ input_files,
337
+ input_path,
338
+ output,
339
+ n,
340
+ generateIDs,
341
+ consolidate_header,
342
+ consolidate_citations,
343
+ include_raw_citations,
344
+ include_raw_affiliations,
345
+ tei_coordinates,
346
+ segment_sentences,
347
+ force,
348
+ verbose,
349
+ flavor
350
+ )
351
+ processed_files_count += batch_processed
352
+ input_files = []
154
353
 
155
354
  # last batch
156
355
  if len(input_files) > 0:
157
- self.process_batch(
356
+ batch_processed = self.process_batch(
158
357
  service,
159
358
  input_files,
160
359
  input_path,
@@ -170,37 +369,44 @@ class GrobidClient(ApiClient):
170
369
  force,
171
370
  verbose,
172
371
  )
372
+ processed_files_count += batch_processed
373
+
374
+ # Log final statistics
375
+ self.logger.info(f"Processing completed: {processed_files_count} out of {total_files} files processed")
173
376
 
174
377
  def process_batch(
175
- self,
176
- service,
177
- input_files,
178
- input_path,
179
- output,
180
- n,
181
- generateIDs,
182
- consolidate_header,
183
- consolidate_citations,
184
- include_raw_citations,
185
- include_raw_affiliations,
186
- tei_coordinates,
187
- segment_sentences,
188
- force,
189
- verbose=False,
190
- flavor=None
378
+ self,
379
+ service,
380
+ input_files,
381
+ input_path,
382
+ output,
383
+ n,
384
+ generateIDs,
385
+ consolidate_header,
386
+ consolidate_citations,
387
+ include_raw_citations,
388
+ include_raw_affiliations,
389
+ tei_coordinates,
390
+ segment_sentences,
391
+ force,
392
+ verbose=False,
393
+ flavor=None
191
394
  ):
192
395
  if verbose:
193
- print(len(input_files), "files to process in current batch")
396
+ self.logger.info(f"{len(input_files)} files to process in current batch")
397
+
398
+ processed_count = 0
194
399
 
195
400
  # we use ThreadPoolExecutor and not ProcessPoolExecutor because it is an I/O intensive process
196
401
  with concurrent.futures.ThreadPoolExecutor(max_workers=n) as executor:
197
- #with concurrent.futures.ProcessPoolExecutor(max_workers=n) as executor:
402
+ # with concurrent.futures.ProcessPoolExecutor(max_workers=n) as executor:
198
403
  results = []
199
404
  for input_file in input_files:
200
405
  # check if TEI file is already produced
201
406
  filename = self._output_file_name(input_file, input_path, output)
202
407
  if not force and os.path.isfile(filename):
203
- print(filename, "already exist, skipping... (use --force to reprocess pdf input files)")
408
+ self.logger.info(
409
+ f"{filename} already exists, skipping... (use --force to reprocess pdf input files)")
204
410
  continue
205
411
 
206
412
  selected_process = self.process_pdf
@@ -208,8 +414,8 @@ class GrobidClient(ApiClient):
208
414
  selected_process = self.process_txt
209
415
 
210
416
  if verbose:
211
- print(f"Adding {input_file} to the queue.")
212
-
417
+ self.logger.info(f"Adding {input_file} to the queue")
418
+
213
419
  r = executor.submit(
214
420
  selected_process,
215
421
  service,
@@ -230,44 +436,55 @@ class GrobidClient(ApiClient):
230
436
  for r in concurrent.futures.as_completed(results):
231
437
  input_file, status, text = r.result()
232
438
  filename = self._output_file_name(input_file, input_path, output)
439
+ processed_count += 1
233
440
 
234
441
  if status != 200 or text is None:
235
- print("Processing of", input_file, "failed with error", str(status), ",", text)
442
+ self.logger.error(f"Processing of {input_file} failed with error {status}: {text}")
236
443
  # writing error file with suffixed error code
237
444
  try:
238
445
  pathlib.Path(os.path.dirname(filename)).mkdir(parents=True, exist_ok=True)
239
- with open(filename.replace(".grobid.tei.xml", "_"+str(status)+".txt"), 'w', encoding='utf8') as tei_file:
446
+ error_filename = filename.replace(".grobid.tei.xml", f"_{status}.txt")
447
+ with open(error_filename, 'w', encoding='utf8') as error_file:
240
448
  if text is not None:
241
- tei_file.write(text)
449
+ error_file.write(text)
242
450
  else:
243
- tei_file.write("")
244
- except OSError:
245
- print("Writing resulting TEI XML file", filename, "failed")
451
+ error_file.write("")
452
+ self.logger.info(f"Error details written to {error_filename}")
453
+ except OSError as e:
454
+ self.logger.error(f"Failed to write error file {filename}: {str(e)}")
246
455
  else:
247
456
  # writing TEI file
248
457
  try:
249
458
  pathlib.Path(os.path.dirname(filename)).mkdir(parents=True, exist_ok=True)
250
- with open(filename,'w',encoding='utf8') as tei_file:
459
+ with open(filename, 'w', encoding='utf8') as tei_file:
251
460
  tei_file.write(text)
252
- except OSError:
253
- print("Writing resulting TEI XML file", filename, "failed")
461
+ self.logger.debug(f"Successfully wrote TEI file: {filename}")
462
+ except OSError as e:
463
+ self.logger.error(f"Failed to write TEI XML file {filename}: {str(e)}")
464
+
465
+ return processed_count
254
466
 
255
467
  def process_pdf(
256
- self,
257
- service,
258
- pdf_file,
259
- generateIDs,
260
- consolidate_header,
261
- consolidate_citations,
262
- include_raw_citations,
263
- include_raw_affiliations,
264
- tei_coordinates,
265
- segment_sentences,
266
- flavor=None,
267
- start=-1,
268
- end=-1
468
+ self,
469
+ service,
470
+ pdf_file,
471
+ generateIDs,
472
+ consolidate_header,
473
+ consolidate_citations,
474
+ include_raw_citations,
475
+ include_raw_affiliations,
476
+ tei_coordinates,
477
+ segment_sentences,
478
+ flavor=None,
479
+ start=-1,
480
+ end=-1
269
481
  ):
270
- pdf_handle = open(pdf_file, "rb")
482
+ try:
483
+ 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
+
271
488
  files = {
272
489
  "input": (
273
490
  pdf_file,
@@ -276,7 +493,7 @@ class GrobidClient(ApiClient):
276
493
  {"Expires": "0"},
277
494
  )
278
495
  }
279
-
496
+
280
497
  the_url = self.get_server_url(service)
281
498
 
282
499
  # set the GROBID parameters
@@ -304,10 +521,12 @@ class GrobidClient(ApiClient):
304
521
 
305
522
  try:
306
523
  res, status = self.post(
307
- url=the_url, files=files, data=the_data, headers={"Accept": "text/plain"}, timeout=self.config['timeout']
524
+ url=the_url, files=files, data=the_data, headers={"Accept": "text/plain"},
525
+ timeout=self.config['timeout']
308
526
  )
309
527
 
310
528
  if status == 503:
529
+ self.logger.warning(f"Server busy (503), retrying {pdf_file} after {self.config['sleep_time']} seconds")
311
530
  time.sleep(self.config["sleep_time"])
312
531
  return self.process_pdf(
313
532
  service,
@@ -319,13 +538,22 @@ class GrobidClient(ApiClient):
319
538
  include_raw_affiliations,
320
539
  tei_coordinates,
321
540
  segment_sentences,
541
+ flavor,
322
542
  start,
323
- end,
324
- flavor
543
+ end
325
544
  )
326
- except requests.exceptions.ReadTimeout:
545
+ except requests.exceptions.ReadTimeout as e:
546
+ self.logger.error(f"Request timeout for {pdf_file}: {str(e)}")
547
+ pdf_handle.close()
548
+ return (pdf_file, 408, f"Request timeout: {str(e)}")
549
+ except requests.exceptions.RequestException as e:
550
+ self.logger.error(f"Request failed for {pdf_file}: {str(e)}")
327
551
  pdf_handle.close()
328
- return (pdf_file, 408, None)
552
+ return (pdf_file, 500, f"Request failed: {str(e)}")
553
+ 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)}")
329
557
 
330
558
  pdf_handle.close()
331
559
  return (pdf_file, status, res.text)
@@ -334,21 +562,27 @@ class GrobidClient(ApiClient):
334
562
  return self.config['grobid_server'] + "/api/" + service
335
563
 
336
564
  def process_txt(
337
- self,
338
- service,
339
- txt_file,
340
- generateIDs,
341
- consolidate_header,
342
- consolidate_citations,
343
- include_raw_citations,
344
- include_raw_affiliations,
345
- tei_coordinates,
346
- segment_sentences
565
+ self,
566
+ service,
567
+ txt_file,
568
+ generateIDs,
569
+ consolidate_header,
570
+ consolidate_citations,
571
+ include_raw_citations,
572
+ include_raw_affiliations,
573
+ tei_coordinates,
574
+ segment_sentences
347
575
  ):
348
576
  # create request based on file content
349
- references = None
350
- with open(txt_file) as f:
351
- references = [line.rstrip() for line in f]
577
+ try:
578
+ with open(txt_file, 'r', encoding='utf-8') as f:
579
+ references = [line.rstrip() for line in f]
580
+ except IOError as e:
581
+ self.logger.error(f"Failed to read text file {txt_file}: {str(e)}")
582
+ return (txt_file, 500, f"Failed to read file: {str(e)}")
583
+ except UnicodeDecodeError as e:
584
+ self.logger.error(f"Unicode decode error reading {txt_file}: {str(e)}")
585
+ return (txt_file, 500, f"Unicode decode error: {str(e)}")
352
586
 
353
587
  the_url = self.get_server_url(service)
354
588
 
@@ -359,29 +593,46 @@ class GrobidClient(ApiClient):
359
593
  if include_raw_citations:
360
594
  the_data["includeRawCitations"] = "1"
361
595
  the_data["citations"] = references
362
- res, status = self.post(
363
- url=the_url, data=the_data, headers={"Accept": "application/xml"}
364
- )
365
596
 
366
- if status == 503:
367
- time.sleep(self.config["sleep_time"])
368
- return self.process_txt(
369
- service,
370
- txt_file,
371
- generateIDs,
372
- consolidate_header,
373
- consolidate_citations,
374
- include_raw_citations,
375
- include_raw_affiliations,
376
- tei_coordinates,
377
- segment_sentences
597
+ try:
598
+ res, status = self.post(
599
+ url=the_url, data=the_data, headers={"Accept": "application/xml"}
378
600
  )
379
601
 
602
+ 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(
606
+ service,
607
+ txt_file,
608
+ generateIDs,
609
+ consolidate_header,
610
+ consolidate_citations,
611
+ include_raw_citations,
612
+ include_raw_affiliations,
613
+ tei_coordinates,
614
+ segment_sentences
615
+ )
616
+ 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)}")
619
+ 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)}")
622
+
380
623
  return (txt_file, status, res.text)
381
624
 
625
+
382
626
  def main():
627
+ # Basic logging setup for initialization only
628
+ # The actual logging configuration will be done by GrobidClient based on config.json
629
+ temp_logger = logging.getLogger(__name__)
630
+ temp_handler = logging.StreamHandler()
631
+ temp_handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
632
+ temp_logger.addHandler(temp_handler)
633
+ temp_logger.setLevel(logging.INFO)
634
+
383
635
  valid_services = [
384
- "processFulltextDocumentBlank",
385
636
  "processFulltextDocument",
386
637
  "processHeaderDocument",
387
638
  "processReferences",
@@ -393,10 +644,13 @@ def main():
393
644
  parser = argparse.ArgumentParser(description="Client for GROBID services")
394
645
  parser.add_argument(
395
646
  "service",
396
- help="one of " + str(valid_services),
647
+ choices=valid_services,
648
+ help="Grobid service to be called.",
397
649
  )
398
650
  parser.add_argument(
399
- "--input", default=None, help="path to the directory containing files to process: PDF or .txt (for processCitationList only, one reference per line), or .xml for patents in ST36"
651
+ "--input",
652
+ default=None,
653
+ help="path to the directory containing files to process: PDF or .txt (for processCitationList only, one reference per line), or .xml for patents in ST36"
400
654
  )
401
655
  parser.add_argument(
402
656
  "--output",
@@ -405,10 +659,14 @@ def main():
405
659
  )
406
660
  parser.add_argument(
407
661
  "--config",
408
- default="./config.json",
409
- help="path to the config file, default is ./config.json",
662
+ default=None,
663
+ help="path to the config file (optional)",
664
+ )
665
+ parser.add_argument(
666
+ "--n",
667
+ default=10,
668
+ help="concurrency for service usage"
410
669
  )
411
- parser.add_argument("--n", default=10, help="concurrency for service usage")
412
670
  parser.add_argument(
413
671
  "--generateIDs",
414
672
  action="store_true",
@@ -468,22 +726,35 @@ def main():
468
726
  output_path = args.output
469
727
  flavor = args.flavor
470
728
 
729
+ # Initialize n with default value
730
+ n = 10
471
731
  if args.n is not None:
472
732
  try:
473
733
  n = int(args.n)
474
734
  except ValueError:
475
- print("Invalid concurrency parameter n:", n, ", n = 10 will be used by default")
476
- pass
735
+ temp_logger.warning(f"Invalid concurrency parameter n: {args.n}. Using default value n = 10")
736
+
737
+ # Initialize GrobidClient which will configure logging based on config.json
738
+ try:
739
+ client = GrobidClient(config_path=config_path)
740
+ # Now use the client's logger for all subsequent logging
741
+ logger = client.logger
742
+ except ServerUnavailableException as e:
743
+ temp_logger.error(f"Server unavailable: {str(e)}")
744
+ exit(1)
745
+ except Exception as e:
746
+ temp_logger.error(f"Failed to initialize GrobidClient: {str(e)}")
747
+ exit(1)
477
748
 
478
749
  # if output path does not exist, we create it
479
750
  if output_path is not None and not os.path.isdir(output_path):
480
751
  try:
481
- print("output directory does not exist but will be created:", output_path)
752
+ logger.info(f"Output directory does not exist but will be created: {output_path}")
482
753
  os.makedirs(output_path)
483
- except OSError:
484
- print("Creation of the directory", output_path, "failed")
485
- else:
486
- print("Successfully created the directory", output_path)
754
+ logger.info(f"Successfully created the directory {output_path}")
755
+ except OSError as e:
756
+ logger.error(f"Creation of the directory {output_path} failed: {str(e)}")
757
+ exit(1)
487
758
 
488
759
  service = args.service
489
760
  generateIDs = args.generateIDs
@@ -496,36 +767,36 @@ def main():
496
767
  segment_sentences = args.segmentSentences
497
768
  verbose = args.verbose
498
769
 
499
- if service is None or not service in valid_services:
500
- print("Missing or invalid service, must be one of", valid_services)
501
- exit(1)
502
-
503
- try:
504
- client = GrobidClient(config_path=config_path)
505
- except ServerUnavailableException:
770
+ if service is None or service not in valid_services:
771
+ logger.error(f"Missing or invalid service '{service}', must be one of {valid_services}")
506
772
  exit(1)
507
773
 
508
774
  start_time = time.time()
509
775
 
510
- client.process(
511
- service,
512
- input_path,
513
- output=output_path,
514
- n=n,
515
- generateIDs=generateIDs,
516
- consolidate_header=consolidate_header,
517
- consolidate_citations=consolidate_citations,
518
- include_raw_citations=include_raw_citations,
519
- include_raw_affiliations=include_raw_affiliations,
520
- tei_coordinates=tei_coordinates,
521
- segment_sentences=segment_sentences,
522
- force=force,
523
- verbose=verbose,
524
- flavor=flavor
525
- )
776
+ try:
777
+ client.process(
778
+ service,
779
+ input_path,
780
+ output=output_path,
781
+ n=n,
782
+ generateIDs=generateIDs,
783
+ consolidate_header=consolidate_header,
784
+ consolidate_citations=consolidate_citations,
785
+ include_raw_citations=include_raw_citations,
786
+ include_raw_affiliations=include_raw_affiliations,
787
+ tei_coordinates=tei_coordinates,
788
+ segment_sentences=segment_sentences,
789
+ force=force,
790
+ verbose=verbose,
791
+ flavor=flavor
792
+ )
793
+ except Exception as e:
794
+ logger.error(f"Processing failed: {str(e)}")
795
+ exit(1)
526
796
 
527
797
  runtime = round(time.time() - start_time, 3)
528
- print("runtime: %s seconds " % (runtime))
798
+ logger.info(f"Processing completed in {runtime} seconds")
799
+
529
800
 
530
801
  if __name__ == "__main__":
531
802
  main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grobid-client-python
3
- Version: 0.0.12
3
+ Version: 0.0.13
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>
@@ -218,15 +218,12 @@ Requires-Dist: requests
218
218
  [![SWH](https://archive.softwareheritage.org/badge/origin/https://github.com/kermitt2/grobid_client_python/)](https://archive.softwareheritage.org/browse/origin/https://github.com/kermitt2/grobid_client_python/)
219
219
  [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
220
220
 
221
- # Simple python client for GROBID REST services
221
+ # Simple Python client for GROBID REST services
222
222
 
223
223
  This Python client can be used to process in an efficient concurrent manner a set of PDF in a given directory by the [GROBID](https://github.com/kermitt2/grobid) service. It includes a command line for processing PDF on a file system and write results in a given output directory and a library for import in other python scripts. The client can also process similarly a list of files with reference strings (one per line) and patents in XML ST36 formats.
224
224
 
225
- ## Before you start
226
-
227
- Please be aware that, at the moment, [grobid does not support Windows](https://grobid.readthedocs.io/en/latest/Troubleshooting/#windows-related-issues).
228
- If you are a Windows user, don't worry. You can still [run grobid
229
- via Docker](https://grobid.readthedocs.io/en/latest/Grobid-docker/).
225
+ > [!IMPORTANT]
226
+ > Please be aware that, at the moment, Grobid supports windows only through Docker containers. See [here](https://grobid.readthedocs.io/en/latest/Grobid-docker/).
230
227
 
231
228
  ## Build and run
232
229
 
@@ -314,11 +311,12 @@ optional arguments:
314
311
  with additional <s> elements
315
312
  --verbose print information about processed files in the console
316
313
  --flavor specifcy the flavor to be used. Current accepted
317
- values: `article/light`, `article/light-ref`, `sdo/ietf`,
318
- however better check [here](https://grobid.readthedocs.io/en/latest/Grobid-specialized-processes/#flavors).
314
+ values: `article/light`, `article/light-ref`, `sdo/ietf`.
315
+ ```
319
316
 
317
+ > [!TIP]
318
+ > Before using flavors, check [here](https://grobid.readthedocs.io/en/latest/Grobid-specialized-processes/#flavors).
320
319
 
321
- ```
322
320
 
323
321
  Examples:
324
322
 
@@ -367,6 +365,9 @@ See also `example.py`.
367
365
 
368
366
  ## Configuration of the client
369
367
 
368
+ > [!TIP]
369
+ > from version 0.0.12 the `config.json` will be optional, by default the client will connect to the local server.
370
+
370
371
  There are a few parameters that can be set with the `config.json` file.
371
372
 
372
373
  - `grobid_server` indicates the URL of the GROBID server to be used by the client.
@@ -0,0 +1,8 @@
1
+ grobid_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ grobid_client/client.py,sha256=crvUiVnnkn6tFwO4lKQg2cTxAHZxCBAmeNSVtuL4ZA0,6009
3
+ grobid_client/grobid_client.py,sha256=1ZHOhlPEgPDExK8RCYpZTq4M0kfMRVgrKEEpCCqUlZg,29533
4
+ grobid_client_python-0.0.13.dist-info/METADATA,sha256=F1h_3LtqdZRhttiwmenPLeS4BBp8Ttk7MYG6HAOBXf0,24213
5
+ grobid_client_python-0.0.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ grobid_client_python-0.0.13.dist-info/entry_points.txt,sha256=-WwPi8D8eXEbDB6QvitvHjxSGBeyLCyFvAoZcaDGswQ,67
7
+ grobid_client_python-0.0.13.dist-info/top_level.txt,sha256=HnjgWxdJNKTLIN86XkgRAfVeEK4C71fc9X4fe7qdMvA,14
8
+ grobid_client_python-0.0.13.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- grobid_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- grobid_client/client.py,sha256=crvUiVnnkn6tFwO4lKQg2cTxAHZxCBAmeNSVtuL4ZA0,6009
3
- grobid_client/grobid_client.py,sha256=bt5gdmUEkhKVojI0y5yO9bW1BLT1Mf8EZ2ZKZmjfkII,18071
4
- grobid_client_python-0.0.12.dist-info/METADATA,sha256=XTZ0S_vrfhuN6EboeInLPGzzqT7RjwLsVRb6thNGJvs,24226
5
- grobid_client_python-0.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- grobid_client_python-0.0.12.dist-info/entry_points.txt,sha256=-WwPi8D8eXEbDB6QvitvHjxSGBeyLCyFvAoZcaDGswQ,67
7
- grobid_client_python-0.0.12.dist-info/top_level.txt,sha256=HnjgWxdJNKTLIN86XkgRAfVeEK4C71fc9X4fe7qdMvA,14
8
- grobid_client_python-0.0.12.dist-info/RECORD,,