@@ -74,8 +74,11 @@ def __str__(self):
7474 def __getattr__ (self , attr ):
7575 """Generate methods for fetching resources"""
7676 p_image = re .compile (
77- r"^get_(?P<size>thumbnail|small|normal|large|xlarge)_image_url(?P<list>_list)?$"
77+ r"^get_"
78+ r"(?P<size>thumbnail|small|normal|large|xlarge)_image_url"
79+ r"(?P<list>_list)?$"
7880 )
81+
7982 get = attr .startswith ("get_" )
8083 url = attr .endswith ("_url" )
8184 text = attr .endswith ("_text" )
@@ -230,9 +233,15 @@ def get_errors(self):
230233
231234 return all_results
232235
233- def process (self ):
234- """Reprocess the document"""
235- self ._client .post (f"{ self .api_path } /{ self .id } /process/" )
236+ def process (self , ** kwargs ):
237+ """Process the document, used on upload and for reprocessing"""
238+ payload = {}
239+ if "force_ocr" in kwargs :
240+ payload ["force_ocr" ] = kwargs ["force_ocr" ]
241+ if "ocr_engine" in kwargs :
242+ payload ["ocr_engine" ] = kwargs ["ocr_engine" ]
243+
244+ self ._client .post (f"{ self .api_path } /{ self .id } /process/" , json = payload )
236245
237246
238247class DocumentClient (BaseAPIClient ):
@@ -310,6 +319,7 @@ def _format_upload_parameters(self, name, **kwargs):
310319 "title" ,
311320 "data" ,
312321 "force_ocr" ,
322+ "ocr_engine" ,
313323 "projects" ,
314324 "delayed_index" ,
315325 "revision_control" ,
@@ -333,21 +343,55 @@ def _format_upload_parameters(self, name, **kwargs):
333343
334344 return params
335345
346+ def _extract_ocr_options (self , kwargs ):
347+ """
348+ Extract and validate OCR options from kwargs.
349+
350+ Returns:
351+ force_ocr (bool)
352+ ocr_engine (str)
353+ """
354+ force_ocr = kwargs .pop ("force_ocr" , False )
355+ ocr_engine = kwargs .pop ("ocr_engine" , "tess4" )
356+
357+ if not isinstance (force_ocr , bool ):
358+ raise ValueError ("force_ocr must be a boolean" )
359+
360+ if ocr_engine and ocr_engine not in ("tess4" , "textract" ):
361+ raise ValueError (
362+ "ocr_engine must be either 'tess4' for tesseract or 'textract'"
363+ )
364+
365+ return force_ocr , ocr_engine
366+
336367 def _get_title (self , name ):
337368 """Get the default title for a document from its path"""
338369 return name .split (os .sep )[- 1 ].rsplit ("." , 1 )[0 ]
339370
340371 def _upload_url (self , file_url , ** kwargs ):
341372 """Upload a document from a publicly accessible URL"""
373+ # extract process-related args
374+ force_ocr , ocr_engine = self ._extract_ocr_options (kwargs )
375+
376+ # create the document
342377 params = self ._format_upload_parameters (file_url , ** kwargs )
343378 params ["file_url" ] = file_url
379+ if force_ocr :
380+ params ["force_ocr" ] = force_ocr
381+ params ["ocr_engine" ] = ocr_engine
344382 response = self .client .post ("documents/" , json = params )
345- return Document (self .client , response .json ())
383+ create_json = response .json ()
384+
385+ # wrap in Document object
386+ doc = Document (self .client , create_json )
387+
388+ return doc
346389
347390 def _upload_file (self , file_ , ** kwargs ):
348391 """Upload a document directly"""
349392 # create the document
350- force_ocr = kwargs .pop ("force_ocr" , False )
393+ force_ocr , ocr_engine = self ._extract_ocr_options (kwargs )
394+
351395 params = self ._format_upload_parameters (file_ .name , ** kwargs )
352396 response = self .client .post ("documents/" , json = params )
353397
@@ -357,12 +401,12 @@ def _upload_file(self, file_, **kwargs):
357401 response = requests_retry_session ().put (presigned_url , data = file_ .read ())
358402
359403 # begin processing the document
360- doc_id = create_json ["id" ]
361- response = self .client .post (
362- f"documents/{ doc_id } /process/" , json = {"force_ocr" : force_ocr }
363- )
404+ doc = Document (self .client , create_json )
364405
365- return Document (self .client , create_json )
406+ # begin processing
407+ doc .process (force_ocr = force_ocr , ocr_engine = ocr_engine )
408+
409+ return doc
366410
367411 def _collect_files (self , path , extensions ):
368412 """Find the paths to files with specified extensions under a directory"""
@@ -379,165 +423,98 @@ def _collect_files(self, path, extensions):
379423
380424 def upload_directory (self , path , handle_errors = False , extensions = ".pdf" , ** kwargs ):
381425 """Upload files with specified extensions in a directory"""
382- # pylint: disable=too-many-locals, too-many-branches
383-
384- # Do not set the same title for all documents
426+ # pylint:disable=too-many-locals
385427 kwargs .pop ("title" , None )
386428
387- # If extensions are specified as None, it will check for all supported
388- # filetypes.
389429 if extensions is None :
390430 extensions = SUPPORTED_EXTENSIONS
391-
392- # Convert single extension to a list if provided
393431 if extensions and not isinstance (extensions , list ):
394432 extensions = [extensions ]
395-
396- # Checks to see if the extensions are supported, raises an error if not.
397433 invalid_extensions = set (extensions ) - set (SUPPORTED_EXTENSIONS )
398434 if invalid_extensions :
399435 raise ValueError (
400436 f"Invalid extensions provided: { ', ' .join (invalid_extensions )} "
401437 )
402438
403- # Loop through the path and get all the files with matching extensions
404439 path_list = self ._collect_files (path , extensions )
405-
406440 logger .info (
407441 "Upload directory on %s: Found %d files to upload" , path , len (path_list )
408442 )
409443
410- # Upload all the files using the bulk API to reduce the number
411- # of API calls and improve performance
412444 obj_list = []
445+ force_ocr , ocr_engine = self ._extract_ocr_options (kwargs )
413446 params = self ._format_upload_parameters ("" , ** kwargs )
447+
414448 for i , file_paths in enumerate (grouper (path_list , BULK_LIMIT )):
415- # Grouper will put None's on the end of the last group
416449 file_paths = [p for p in file_paths if p is not None ]
417-
418450 logger .info ("Uploading group %d:\n %s" , i + 1 , "\n " .join (file_paths ))
419451
420- # Create the documents
421- logger .info ("Creating the documents..." )
422- try :
423- response = self .client .post (
424- "documents/" ,
425- json = [
426- merge_dicts (
427- params ,
428- {
429- "title" : self ._get_title (p ),
430- "original_extension" : os .path .splitext (
431- os .path .basename (p )
432- )[1 ]
433- .lower ()
434- .lstrip ("." ),
435- },
436- )
437- for p in file_paths
438- ],
439- )
440- except (APIError , RequestException ) as exc :
441- if handle_errors :
442- logger .info (
443- "Error creating the following documents: %s\n %s" ,
444- exc ,
445- "\n " .join (file_paths ),
446- )
447- continue
448- else :
449- raise
452+ create_json = self ._create_documents (file_paths , params , handle_errors )
453+ sorted_create_json = sorted (create_json , key = lambda j : j ["title" ])
454+ sorted_file_paths = sorted (file_paths , key = self ._get_title )
455+ obj_list .extend (sorted_create_json )
456+ presigned_urls = [j ["presigned_url" ] for j in sorted_create_json ]
450457
451- # Upload the files directly to storage
452- create_json = response .json ()
453- obj_list .extend (create_json )
454- presigned_urls = [j ["presigned_url" ] for j in create_json ]
455- for url , file_path in zip (presigned_urls , file_paths ):
456- logger .info ("Uploading %s to S3..." , file_path )
457- try :
458- with open (file_path , "rb" ) as file :
459- response = requests_retry_session ().put (url , data = file .read ())
460- self .client .raise_for_status (response )
461- except (APIError , RequestException ) as exc :
462- if handle_errors :
463- logger .info (
464- "Error uploading the following document: %s %s" ,
465- exc ,
466- file_path ,
467- )
468- continue
469- else :
470- raise
471-
472- # Begin processing the documents
473- logger .info ("Processing the documents..." )
474- doc_ids = [j ["id" ] for j in create_json ]
475- try :
476- response = self .client .post ("documents/process/" , json = {"ids" : doc_ids })
477- except (APIError , RequestException ) as exc :
478- if handle_errors :
479- logger .info (
480- "Error creating the following documents: %s\n %s" ,
481- exc ,
482- "\n " .join (file_paths ),
483- )
484- continue
485- else :
486- raise
458+ self ._upload_files_to_s3 (sorted_file_paths , presigned_urls , handle_errors )
459+ self ._process_documents (create_json , force_ocr , ocr_engine , handle_errors )
487460
488461 logger .info ("Upload directory complete" )
489-
490- # Pass back the list of documents
491462 return [Document (self .client , d ) for d in obj_list ]
492463
493- def upload_urls (self , url_list , handle_errors = False , ** kwargs ):
494- """Upload documents from a list of URLs"""
495-
496- # Do not set the same title for all documents
497- kwargs .pop ("title" , None )
498-
499- obj_list = []
500- params = self ._format_upload_parameters ("" , ** kwargs )
501- for i , url_group in enumerate (grouper (url_list , BULK_LIMIT )):
502- # Grouper will put None's on the end of the last group
503- url_group = [url for url in url_group if url is not None ]
504-
505- logger .info ("Uploading group %d: %s" , i + 1 , "\n " .join (url_group ))
506-
507- # Create the documents
508- logger .info ("Creating the documents..." )
509- try :
510- response = self .client .post (
511- "documents/" ,
512- json = [
513- merge_dicts (
514- params ,
515- {
516- "title" : self ._get_title (url ),
517- "file_url" : url ,
518- },
519- )
520- for url in url_group
521- ],
464+ def _create_documents (self , file_paths , params , handle_errors ):
465+ body = [
466+ merge_dicts (
467+ params ,
468+ {
469+ "title" : self ._get_title (p ),
470+ "original_extension" : os .path .splitext (os .path .basename (p ))[1 ]
471+ .lower ()
472+ .lstrip ("." ),
473+ },
474+ )
475+ for p in sorted (file_paths )
476+ ]
477+ try :
478+ response = self .client .post ("documents/" , json = body )
479+ except (APIError , RequestException ) as exc :
480+ if handle_errors :
481+ logger .info (
482+ "Error creating the following documents: %s\n %s" ,
483+ exc ,
484+ "\n " .join (file_paths ),
522485 )
486+ return []
487+ else :
488+ raise
489+ return response .json ()
490+
491+ def _upload_files_to_s3 (self , file_paths , presigned_urls , handle_errors ):
492+ for url , file_path in zip (presigned_urls , file_paths ):
493+ logger .info ("Uploading %s to S3..." , file_path )
494+ try :
495+ with open (file_path , "rb" ) as f :
496+ response = requests_retry_session ().put (url , data = f .read ())
497+ self .client .raise_for_status (response )
523498 except (APIError , RequestException ) as exc :
524499 if handle_errors :
525500 logger .info (
526- "Error creating the following documents: %s\n %s" ,
527- str (exc ),
528- "\n " .join (url_group ),
501+ "Error uploading the following document: %s %s" , exc , file_path
529502 )
530- continue
531503 else :
532504 raise
533505
534- create_json = response .json ()
535- obj_list .extend (create_json )
536-
537- logger .info ("Upload URLs complete" )
538-
539- # Pass back the list of documents
540- return [Document (self .client , d ) for d in obj_list ]
506+ def _process_documents (self , create_json , force_ocr , ocr_engine , handle_errors ):
507+ payload = [
508+ {"id" : j ["id" ], "force_ocr" : force_ocr , "ocr_engine" : ocr_engine }
509+ for j in create_json
510+ ]
511+ try :
512+ self .client .post ("documents/process/" , json = payload )
513+ except (APIError , RequestException ) as exc :
514+ if handle_errors :
515+ logger .info ("Error processing documents: %s" , exc )
516+ else :
517+ raise
541518
542519
543520class Mention :
0 commit comments