@ -5,6 +5,7 @@ import argparse
import asyncio
import datetime as dt
import uuid
from dataclasses import dataclass
from sqlalchemy import select
@ -20,42 +21,39 @@ from libs.oracle_client.exceptions import OracleNotFoundError
logger = get_logger ( __name__ )
async def fetch_exhibits ( run_id : str ) - > dict [ str , int ] :
settings = get_settings ( )
app_config = settings . get_app_config ( )
exhibit_types = app_config . get ( " pipeline " , { } ) . get ( " exhibit_types " , [ " EX-99.1 " ] )
@dataclass ( slots = True )
class _FetchedExhibit :
exhibit_type : str
checksum : str
cache_path : str
stats = { " seen " : 0 , " written " : 0 , " skipped " : 0 , " errors " : 0 }
batch_size = 100
@dataclass ( slots = True )
class _FetchResult :
doc_id : str
accession_no : str | None
fetched : list [ _FetchedExhibit ]
item_numbers : list [ str ] | None
errors : int
async with make_oracle_client ( ) as client :
svc = FilingsService ( client )
async with get_session ( ) as session :
job = JobRun (
job_run_id = uuid . UUID ( run_id ) ,
job_name = " filing_fetcher " ,
source_name = " oracle " ,
run_date = dt . date . today ( ) ,
status = " running " ,
async def _fetch_doc (
svc : FilingsService ,
doc : Document ,
exhibit_types : list [ str ] ,
) - > _FetchResult :
if not doc . accession_no :
return _FetchResult (
doc_id = str ( doc . document_id ) ,
accession_no = None ,
fetched = [ ] ,
item_numbers = None ,
errors = 0 ,
)
session . add ( job )
await session . flush ( )
await session . commit ( )
result = await session . execute (
select ( Document ) . where ( Document . parsed_status == " pending " )
)
docs = result . scalars ( ) . all ( )
stats [ " seen " ] = len ( docs )
fetched : list [ _FetchedExhibit ] = [ ]
errors = 0
for idx , doc in enumerate ( docs , 1 ) :
if not doc . accession_no :
stats [ " skipped " ] + = 1
continue
fetched_any = False
for exhibit_type in exhibit_types :
if exists_exhibit ( doc . accession_no , exhibit_type ) :
logger . info (
@ -63,7 +61,6 @@ async def fetch_exhibits(run_id: str) -> dict[str, int]:
accession_no = doc . accession_no ,
exhibit_type = exhibit_type ,
)
fetched_any = True
continue
try :
@ -71,37 +68,22 @@ async def fetch_exhibits(run_id: str) -> dict[str, int]:
svc . get_exhibit ( doc . accession_no , exhibit_type ) ,
timeout = 30.0 ,
)
checksum = write_exhibit (
doc . accession_no , exhibit_type , response . content
)
checksum = write_exhibit ( doc . accession_no , exhibit_type , response . content )
from libs . common . file_store import exhibit_path
cache_path = str ( exhibit_path ( doc . accession_no , exhibit_type ) )
existing_cache = await session . execute (
select ( ExhibitCache ) . where (
ExhibitCache . accession_no == doc . accession_no ,
ExhibitCache . exhibit_type == exhibit_type ,
)
)
if existing_cache . scalar_one_or_none ( ) is None :
cache_row = ExhibitCache (
accession_no = doc . accession_no ,
fetched . append (
_FetchedExhibit (
exhibit_type = exhibit_type ,
content_hash = checksum ,
cache_path = cache_path ,
checksum = checksum ,
cache_path = str ( exhibit_path ( doc . accession_no , exhibit_type ) ) ,
)
)
session . add ( cache_row )
fetched_any = True
stats [ " written " ] + = 1
logger . info (
" exhibit_fetched " ,
accession_no = doc . accession_no ,
exhibit_type = exhibit_type ,
)
except OracleNotFoundError :
logger . warning (
" exhibit_not_found " ,
@ -115,21 +97,20 @@ async def fetch_exhibits(run_id: str) -> dict[str, int]:
exhibit_type = exhibit_type ,
error = str ( exc ) ,
)
stats[ " errors" ] + = 1
errors + = 1
# Extract item_numbers from SGML header if not already set
item_numbers : list [ str ] | None = None
if not doc . item_numbers :
try :
item s = await asyncio . wait_for (
item _number s = await asyncio . wait_for (
svc . get_filing_items ( doc . accession_no ) ,
timeout = 15.0 ,
)
if items :
doc . item_numbers = items
if item_numbers :
logger . info (
" item_numbers_extracted " ,
accession_no = doc . accession_no ,
items = item s,
items = item _number s,
)
except Exception as exc :
logger . debug (
@ -138,16 +119,99 @@ async def fetch_exhibits(run_id: str) -> dict[str, int]:
error = str ( exc ) ,
)
# Always advance to ready_for_parse (exhibit may not exist)
return _FetchResult (
doc_id = str ( doc . document_id ) ,
accession_no = doc . accession_no ,
fetched = fetched ,
item_numbers = item_numbers ,
errors = errors ,
)
async def fetch_exhibits (
run_id : str ,
start_date : str | None = None ,
end_date : str | None = None ,
) - > dict [ str , int ] :
settings = get_settings ( )
app_config = settings . get_app_config ( )
exhibit_types = app_config . get ( " pipeline " , { } ) . get ( " exhibit_types " , [ " EX-99.1 " ] )
concurrency = max ( 1 , int ( app_config . get ( " pipeline " , { } ) . get ( " fetcher_concurrency " , 20 ) ) )
stats = { " seen " : 0 , " written " : 0 , " skipped " : 0 , " errors " : 0 }
batch_size = max ( 10 , int ( app_config . get ( " pipeline " , { } ) . get ( " fetcher_batch_size " , 25 ) ) )
async with make_oracle_client ( ) as client :
svc = FilingsService ( client )
async with get_session ( ) as session :
job = JobRun (
job_run_id = uuid . UUID ( run_id ) ,
job_name = " filing_fetcher " ,
source_name = " oracle " ,
run_date = dt . date . today ( ) ,
status = " running " ,
)
session . add ( job )
await session . flush ( )
await session . commit ( )
stmt = select ( Document ) . where ( Document . parsed_status == " pending " )
if start_date :
stmt = stmt . where ( Document . filing_date > = dt . date . fromisoformat ( start_date ) )
if end_date :
stmt = stmt . where ( Document . filing_date < = dt . date . fromisoformat ( end_date ) )
result = await session . execute ( stmt . order_by ( Document . filing_date , Document . document_id ) )
docs = result . scalars ( ) . all ( )
stats [ " seen " ] = len ( docs )
doc_map = { str ( doc . document_id ) : doc for doc in docs }
for batch_start in range ( 0 , len ( docs ) , batch_size ) :
batch_docs = docs [ batch_start : batch_start + batch_size ]
for task_start in range ( 0 , len ( batch_docs ) , concurrency ) :
task_docs = batch_docs [ task_start : task_start + concurrency ]
results = await asyncio . gather (
* [ _fetch_doc ( svc , doc , exhibit_types ) for doc in task_docs ]
)
for result_row in results :
doc = doc_map [ result_row . doc_id ]
if not result_row . accession_no :
stats [ " skipped " ] + = 1
continue
for fetched in result_row . fetched :
existing_cache = await session . execute (
select ( ExhibitCache ) . where (
ExhibitCache . accession_no == result_row . accession_no ,
ExhibitCache . exhibit_type == fetched . exhibit_type ,
)
)
if existing_cache . scalar_one_or_none ( ) is None :
session . add (
ExhibitCache (
accession_no = result_row . accession_no ,
exhibit_type = fetched . exhibit_type ,
content_hash = fetched . checksum ,
cache_path = fetched . cache_path ,
)
)
stats [ " written " ] + = 1
if result_row . item_numbers :
doc . item_numbers = result_row . item_numbers
doc . parsed_status = " ready_for_parse "
doc . updated_at_utc = dt . datetime . now ( tz = dt . UTC )
stats [ " errors " ] + = result_row . errors
# Commit in batches to preserve progress
if idx % batch_size == 0 :
processed = min ( batch_start + len ( batch_docs ) , len ( docs ) )
await session . commit ( )
logger . info (
" batch_committed " ,
processed = idx ,
processed = processed ,
total = stats [ " seen " ] ,
written = stats [ " written " ] ,
errors = stats [ " errors " ] ,
@ -167,13 +231,15 @@ async def fetch_exhibits(run_id: str) -> dict[str, int]:
def main ( ) - > None :
parser = argparse . ArgumentParser ( description = " Filing Fetcher " )
parser . add_argument ( " --run-id " , default = new_job_run_id ( ) )
parser . add_argument ( " --start-date " , default = None , metavar = " YYYY-MM-DD " )
parser . add_argument ( " --end-date " , default = None , metavar = " YYYY-MM-DD " )
args = parser . parse_args ( )
settings = get_settings ( )
configure_logging ( settings . log_level )
bind_job_run_id ( args . run_id )
asyncio . run ( fetch_exhibits ( args . run_id ))
asyncio . run ( fetch_exhibits ( args . run_id , start_date = args . start_date , end_date = args . end_date ))
if __name__ == " __main__ " :