2424import pathlib
2525import logging
2626from typing import Tuple
27+ import copy
2728
2829from .client import ApiClient
2930
@@ -37,60 +38,91 @@ def __init__(self, message="GROBID server is not available"):
3738
3839
3940class 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