+
    bi(                       R t ^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RI	t	 ^ RI
t
^ RIHt ^ RIHt ^ RIHtHtHtHtHt ^ RIHt ^ RIHt ^ RIHt ^ R	IHt ^ R
IH t  ^ RI!H"t" ^ RI#H$t$ ^ RI%H&t& ^ RI'H(t) ^ RI*H+t+ Rt,Rt-Rt.Rt/Rt0Rt1Rt2Rt3Rt40 Rmt5]! RR7       ! R R4      4       t60 Rmt7RR R llt8R R lt9R  R! lt:R" R# lt;R$ R% lt<R& R' lt=R( R) lt>R* R+ lt?R, R- lt@RR. R/ lltARR0 R1 lltBRR2 R3 lltCR4 R5 ltDR6 R7 ltER8R9R:R;R<R=/R> R? lltFR@ RA ltGRB RC ltHRD RE ltIRF RG ltJRH RI ltKRJ RK ltLRL RM ltMRRN RO lltNRP RQ ltORR RS ltPRT RU ltQRV RW ltRRX RY ltSRRZ R[ lltTR\ R] ltUR^ R_ ltVR` Ra ltWRb Rc ltXRd Re ltYRf Rg ltZRh Ri lt[Rj Rk lt\Rl Rm lt]RRn Ro llt^Rp Rq lt_Rr Rs lt`Rt Ru ltaRv Rw ltbRx Ry ltcRz R{ ltdR| R} lteR~ R ltf]gR8X  d   ]h! ]f! 4       4      hR#   ] d    Rt
^ RIt ELi ; i)a  Voucher_List_Scraper.py

USAGE
  pip install selenium
  # Ensure Firefox + geckodriver are installed.
  # If geckodriver is not on PATH, set:
  #   export GECKODRIVER_PATH=/full/path/to/geckodriver

  python3 Voucher_List_Scraper.py
  python3 Voucher_List_Scraper.py --headless

WHAT THIS DOES
  - Opens the Accounts list page and logs in (credentials hard-coded below).
  - Clicks id=accSearchBox, types "aa", waits 3 seconds, then presses Enter.
  - Waits a moment for the results to refresh.
  - Clicks the FIRST account row in the results table id='device-grid-list'.
  - Creates a folder in the same directory as this .py.
  - Writes a Markdown file inside that folder named after the clicked account text.
  - On the account page, finds id=ProgramListView; if the FIRST program row is active
    (checkbox checked), it records that program name and opens it.
  - Records "Program Summary" section (best effort) and captures all rows from the
    main DataTables product list (not just the first 10 displayed).

NOTES
  - The portal uses DataTables; to avoid stale element errors, this script snapshots
    tables with JavaScript (reads text + hrefs from the DOM) instead of reading
    WebElement.text repeatedly.
)annotationsN)	dataclass)Path)DictListOptionalSequenceTuple)urljoin)	webdriver)TimeoutException)By)Keys)ActionChains)Options)Service)expected_conditions)WebDriverWaitzFhttps://portal.redwingforbusiness.com/RWS_AccountsListPage?tab=accountzwalpine614@rwfb.comzSammiandCoco2!aaaccSearchBoxzdevice-grid-listProgramListViewALLT)frozenc                  ,    ] tR t^_t$ R]R&   R]R&   RtR# )LinkCellstrtexthref N)__name__
__module____qualname____firstlineno____annotations____static_attributes__r       voucher_scraper_core.pyr   r   _   s    
I
Ir%   r   c               $    V ^8  d   QhRRRRRR/# )   sr   max_lenintreturnr   )formats   "r&   __annotate__r.   l   s!      #  c r%   c                   T ;'       g    RP                  RR4      P                  RR4      P                  RR4      P                  4       p \        P                  ! RRV 4      p \        P                  ! RRV 4      p \        P                  ! R	RV 4      p V P	                  R
4      p V '       g   Rp V P                  4       \        9   d   V  R2p V RV # )zReturn a filesystem-safe single path component.

- Preserves spaces and '$' (so you can get names like '$150 Subsidy Safety Toe.md').
- Removes characters that are invalid on Windows and path separators on all OSes.
  
	\s+z[<>:"/\\|?*]_z[\x00-\x1f]z .itemN)replacestripresubrstripupper_WINDOWS_RESERVED)r)   r*   s   &&r&   _safe_fs_componentr?   l   s     
b$$,,T37??cJPPRA
vsAA 	Q'A 	~r1%A 	
Awwy%%cGXg;r%   c                    V ^8  d   QhRRRR/# r(   pathr   r,   boolr   )r-   s   "r&   r.   r.      s     
 
S 
T 
r%   c                2    \        V 4      pVP                  4       '       d   VP                  4       '       g   R# VP                  R4      ;_uu_ 4       pVP	                  ^4      R8H  uuRRR4       #   + '       g   i     R# ; i  \
         d     R# i ; i)zNReturn True if `path` looks like a real ELF binary (not a shell/snap wrapper).Frbs   ELFN)r   existsis_fileopenread	Exception)rB   pfs   &  r&   _is_elf_executablerM      sd    JxxzzVVD\\Q66!9
* \\\ s4   6B B A3(
B 3B	>B B BBc                    V ^8  d   QhRRRR/# rA   r   )r-   s   "r&   r.   r.      s        r%   c                     \        V 4      pVP                  4       ;'       d6    VP                  4       ;'       d    VP                  P	                  4       R 8H  #   \
         d     R# i ; i)z.exeF)r   rF   rG   suffixlowerrJ   )rB   rK   s   & r&   _is_windows_executablerR      sT    JxxzHHaiikHHahhnn.>&.HH s   !A A A A)(A)c                    V ^8  d   QhRRRR/# rA   r   )r-   s   "r&   r.   r.      s     $ $ $ $r%   c                X    \         P                  R 8X  d   \        V 4      # \        V 4      # )nt)osnamerR   rM   )rB   s   &r&   _is_usable_executablerX      s#    	ww$%d++d##r%   c                   V ^8  d   QhRR/# )r(   r,   Optional[str]r   )r-   s   "r&   r.   r.      s     5 5 5r%   c                    \         P                  P                  R4      p V '       dl   \        \	        V 4      P                  4       4      p \        V 4      '       d   V # \        \	        V 4      P                  R4      4      p\        V4      '       d   V# \         P                  R8X  dY   RR\        P                  ! R4      \        P                  ! R4      .pV F"  pV'       g   K  \        V4      '       g   K   Vu # 	  R# R	R
.pV F  p\        V4      '       g   K  Vu # 	  \        P                  ! R4      \        P                  ! R4      \        P                  ! R4      .pV F~  pV'       d   \        V4      '       d   Vu # V'       g   K*  \	        V4      P                  R8X  g   KF  \        \	        V4      P                  R4      4      p\        V4      '       g   K|  Vu # 	  R# )zFind a real Firefox executable for GeckoDriver.

On many Pi / Ubuntu setups, `firefox` on PATH is a snap wrapper script, which
GeckoDriver rejects. We prefer `firefox-bin` (real ELF) when available.
FIREFOX_BINARYzfirefox-binrU   z,C:\Program Files\Mozilla Firefox\firefox.exez2C:\Program Files (x86)\Mozilla Firefox\firefox.exezfirefox.exefirefoxNz1/snap/firefox/current/usr/lib/firefox/firefox-binz-/snap/firefox/current/usr/lib/firefox/firefoxzfirefox-esr)rV   environgetr   r   
expanduserrX   	with_namerM   rW   shutilwhichrR   )env_binsibwin_candidatescsnap_candidatespath_candidatess         r&   _locate_firefox_binaryrj      sy    jjnn-.Gd7m..01 ))N$w-))-89c""J	ww$;ALL'LL#	
  Aq+A..    	<7O a  H  	]#Y]#O
 #A&&H1a*d1g''67C!#&&
  r%   c                    V ^8  d   QhRRRR/# )r(   firefox_binrZ   r,   r   )r-   s   "r&   r.   r.      s     9 9] 9} 9r%   c                   \         P                  P                  R4      pV'       d6   \        \	        V4      P                  4       4      p\        V4      '       d   V# \         P                  R8X  d   V '       d7   \        \	        V 4      P                  R4      4      p\        V4      '       d   V# \        P                  ! R4      \        P                  ! R4      .pV F"  pV'       g   K  \        V4      '       g   K   Vu # 	  \        \	        \        4      P                  4       P                  R,          4      RR.pV F  p\        V4      '       g   K  Vu # 	  R# Rp\        V4      '       d   V# V '       d7   \        \	        V 4      P                  R4      4      p\        V4      '       d   V# \        P                  ! R4      pV'       d   \        V4      '       d   V# . R	OpV F  p\        V4      '       g   K  Vu # 	  R# )
z<Locate geckodriver with sane preferences for Pi/snap setups.GECKODRIVER_PATHrU   zgeckodriver.exegeckodriverzC:\tools\geckodriver.exez C:\WebDriver\bin\geckodriver.exeNz1/snap/firefox/current/usr/lib/firefox/geckodriver)z/usr/bin/geckodriverz/snap/bin/geckodriverz/usr/local/bin/geckodriver)rV   r^   r_   r   r   r`   rX   rW   ra   rR   rb   rc   __file__resolveparentrM   )rl   	env_geckore   ri   rg   common
snap_geckogeckos   &       r&   _locate_geckodriverrw      s    

12IY2245	 ++	ww$d;'112CDEC%c**
LL*+LL'
 !Aq+A.. ! X&&(//2CCD'/

 A%a((   EJ*%% ${#--m<=c""J LL'E#E**F
 a  H  r%   c                    V ^8  d   QhRRRR/# )r(   rl   rZ   r,   r   r   )r-   s   "r&   r.   r.     s     * *m * *r%   c                p   \        V 4      p\        \        \        4      P	                  4       P
                  R ,          4      pV'       d   \        RV 24        \        WR7      # \        R4        \        VR7      #   \         d    \        YR7      u # i ; i  \         d    \        TR7      u # i ; i)zgeckodriver.logz[driver] Using geckodriver: )executable_path
log_output)rz   log_pathzN[driver] WARNING: Could not locate geckodriver; letting Selenium try defaults.)r{   )r|   )	rw   r   r   rp   rq   rr   printr   	TypeError)rl   rv   r|   s   &  r&   _geckodriver_servicer     s    ,E4>))+225FFGH,UG45	E5FF 

Z[*(++  	E5DD	E  *))*s$   A> 2B >BBB54B5c                   V ^8  d   QhRR/# r(   r,   Noner   )r-   s   "r&   r.   r.   )  s     	 	 	r%   c                 d    \        \        4      P                  4       P                  p V R,          pVP	                  RRR7       \
        P                  P                  R\        V4      4       \
        P                  P                  R\        V R,          4      4       R#   \         d     R# i ; i)zIUse a project-local Selenium cache to avoid unwritable default locations.z.selenium-cacheTparentsexist_okSE_CACHE_PATHXDG_CACHE_HOMEz.cacheN)
r   rp   rq   rr   mkdirrV   r^   
setdefaultr   rJ   )base	cache_dirs     r&   _configure_selenium_runtime_envr   )  s    H~%%'..,,	t4


os9~>


.D8O0DE s   BB   B/.B/c                    V ^8  d   QhRRRR/# )r(   headlessrC   r,   zwebdriver.Firefoxr   )r-   s   "r&   r.   r.   5  s     ! !4 !$5 !r%   c                   \        4        \        4       pV '       d   VP                  R 4       \        4       pV'       d   \	        RV 24       W!n        M\	        R4       VP                  RR4       VP                  RR4        \        P                  ! \        V4      VR7      p TP                  RR4       T#   \         d    \        P                  R8X  dh   \	        R	4       \	        R
T'       d   RMR 24       \	        RT;'       g    R 24       \	        R\        T4      '       d   RMR 24       \	        R4       h i ; i  \         d     T# i ; i)
--headlessz[driver] Using firefox binary: zf[driver] WARNING: Could not locate a real Firefox binary; GeckoDriver may fail on snap-wrapper setups.zdom.webnotifications.enabledFzmedia.volume_scalez0.0)serviceoptionsrU   z[driver] Windows setup check:z[driver]   Firefox detected: yesnoz[driver]   Firefox path: z(not found)z![driver]   geckodriver detected: zT[driver]   Tip: install geckodriver.exe and put it on PATH, or set GECKODRIVER_PATH.i   i  )r   r   add_argumentrj   r}   binary_locationset_preferencer   Firefoxr   rJ   rV   rW   rw   set_window_size)r   optsrl   drivers   &   r&   build_driverr   5  sE   #%9D,' )*K/}=>*vw 	6>,e4
""+?+LVZ[tS) M  77d?121;%D1QRS-k.J.J]-KLM51+>>UDIK Lhi  Ms%   !B; 'E ;AE7EEEc                   V ^8  d   QhRR/# r(   timeoutr+   r   )r-   s   "r&   r.   r.   X  s     ] ]S ]r%   c                `    \        W4      P                  \        P                  ! W34      4      # N)r   untilECpresence_of_element_locatedr   byvaluer   s   &&&&r&   wait_presentr   X  s&    )//0N0NPR{0[\\r%   c                   V ^8  d   QhRR/# r   r   )r-   s   "r&   r.   r.   \  s     Y Ys Yr%   c                `    \        W4      P                  \        P                  ! W34      4      # r   )r   r   r   element_to_be_clickabler   s   &&&&r&   wait_clickabler   \  s%    )//0J0JB;0WXXr%   c                    V ^8  d   QhRRRR/# )r(   r   r+   r,   r   r   )r-   s   "r&   r.   r.   `  s      # t r%   c                   \         P                   ! 4       V,           p\         P                   ! 4       V8  d    V P                  \        P                  R4      pV'       g   R# \        ;QJ d    R V 4       F  '       g   K   RM	  RM! R V 4       4      '       g   R#  \         P                  ! R4       K  R#   \
         d     R# i ; i)z@Best-effort wait for DataTables processing overlay to disappear.zdiv.dataTables_processingNc              3  @   "   T F  qP                  4       x  K  	  R # 5ir   )is_displayed).0rK   s   & r&   	<genexpr>'datatables_wait_idle.<locals>.<genexpr>h  s     7A~~''s   TFg?)timefind_elementsr   CSS_SELECTORanyrJ   sleep)r   r   endprocss   &&  r&   datatables_wait_idler   `  s    
))+
C
))+
	((:UVE3773337777 8 	

3   		s#   'B9 "
B9 -B9 B9 9CCc                    V ^8  d   QhRRRR/# )r(   table_idr   r,   zDict[str, object]r   )r-   s   "r&   r.   r.   o  s     3 3 30A 3r%   c                (    RpV P                  W!4      # )zPReturn headers + row cell texts + first link (text+href) per row for a table id.as  
        const table = document.getElementById(arguments[0]);
        if (!table) return null;
        const headers = Array.from(table.querySelectorAll('thead th')).map(th => (th.innerText || '').trim());
        const rows = Array.from(table.querySelectorAll('tbody tr')).map(tr => {
            const cells = Array.from(tr.querySelectorAll('td')).map(td => (td.innerText || '').trim());
            const a = tr.querySelector('a[href]');
            const link = a ? {text: (a.innerText||'').trim(), href: a.getAttribute('href')||''} : null;
            return {cells, link};
        });
        return {headers, rows};
    )execute_scriptr   r   scripts   && r&   js_table_snapshotr   o  s    F   22r%   c                   V ^8  d   QhRR/# r   r   )r-   s   "r&   r.   r.     s     6 6T 6r%   c                   V P                  \        4       \        V \        P                  R ^4      p\        V \        P                  R^4      p V P                  RV4        VP                  4        VP                  \        4        VP                  4        VP                  \        4        V P                  \        P                  R4      P                  4        \        V \        P                  \        ^<4       R#   \         d     Li ; i  \         d     Li ; i  \         d     Li ; i  \         d#    TP                  \        P                  4        Li ; i)usernamepasswordz.arguments[0].scrollIntoView({block:'center'});LoginN)r_   	LOGIN_URLr   r   IDr   rJ   clear	send_keysUSERNAMEPASSWORDfind_elementclickr   ENTERACC_SEARCHBOX_ID)r   user_elpass_els   &  r&   loginr     s   
JJy 6255*b9G6255*b9GNPWX h h&BEE7+113
  0"5)    
    &$**%&sH   C> "D D  ..D1 >DDDD D.-D.1*EEpre_enter_sleep      @post_enter_sleep      ?final_sleep      ?c          
     ,    V ^8  d   QhRRRRRRRRRR/# )	r(   termr   r   floatr   r   r,   r   r   )r-   s   "r&   r.   r.     s<     5 5
5 	5
 5 5 
5r%   c                  T;'       g    RP                  4       P                  4       p\        RV R24       \        V \        P
                  \        ^<4      pVP                  4         VP                  4        TP                  T4       \        P                  ! T4        TP                  4         TP                  \        P                   4        TP                  \        P"                  4        \%        T 4      P                  \        P                   4      P'                  4         TP                  \        P(                  4       \        P                  ! T4       \+        T ^4       \        P                  ! T4       R#   \         dX     TP                  \        P                  R4       TP                  \        P                  4        ELW  \         d      ELfi ; ii ; i  \         d     ELDi ; i  \         d     EL6i ; i  \         d     EL(i ; i  \         d     ELi ; i  \         d     Li ; i)z6Type `term` into accSearchBox, wait, then press Enter.r0   zFiltering accounts: typing 'z' into accSearchBox...aN)r9   rQ   r}   r   r   r   r   r   r   rJ   r   r   CONTROL	BACKSPACEr   r   r   RETURNr   performTABr   )r   r   r   r   r   boxs   &&$$$ r&   filter_accounts_termr     s    JJB%%'D	(.D
EF
(8"
=CIIK		 MM$JJ		djj!dkk"V&&tzz2::<dhh 	JJ $JJ{K  	MM$,,,MM$..) 		  
      
  s   )F !G' 2G9 H 26H )H/ G$?GG G$G  G$'G65G69HHHHH,+H,/H=<H=c                   V ^8  d   QhRR/# r   r   )r-   s   "r&   r.   r.     s     5 5d 5r%   c                "    \        V \        4      # )z&Back-compat wrapper: uses SEARCH_TERM.)r   SEARCH_TERM)r   s   &r&   filter_accounts_simpler     s    44r%   c                   V ^8  d   QhRR/# )r(   r,   zTuple[LinkCell, Dict[str, str]]r   )r-   s   "r&   r.   r.     s     c c.M cr%   c                4   \        V \        P                  \        ^<4       R R lp\	        V ^<4      P                  V4       \        V \        4      pV'       d   VP                  R4      '       g   \        R4      hVP                  R4      ;'       g    .  Uu. uF  q3NK  	  ppVR,          ^ ,          pVP                  R4      pV'       d   VP                  R4      '       g   \        R4      hVP                  R	4      ;'       g    . p/ p\        V4       F.  w  rV	\        V4      8  g   K  Wy,          Y;'       g    R
V	 2&   K0  	  \        VP                  RR4      P                  4       VP                  RR4      P                  4       R7      V3# u upi )zHReturn the first account link + row mapping from accounts results table.c                   V ^8  d   QhRR/# )r(   r,   rC   r   )r-   s   "r&   r.   4get_first_account_from_results.<locals>.__annotate__  s      d r%   c                     V P                  \        P                  \        4      pVP	                  \        P
                  R 4      p\        V4      ^ 8  #   \         d     R# i ; i)ztbody tr td a[href]F)r   r   r   ACCOUNTS_TABLE_IDr   r   lenrJ   )dtblr   s   &  r&   _has_first_link7get_first_account_from_results.<locals>._has_first_link  sS    	..(9:C!!"//3HIAq6A: 		s   AA A#"A#rowsz)Accounts results table snapshot is empty.headerslinkr   z9Could not find a clickable account link in the first row.cellscol_r   r0   r   r   )r   r   r   r   r   r   r   r_   RuntimeError	enumerater   r   r9   )
r   r   snaphr   firstr   r   row_mapis
   &         r&   get_first_account_from_resultsr    sO     126 &"##O4V%67Dtxx''FGG&*hhy&9&?&?R&?A&?!&?GALOE99VDtxx''VWWyy)//RE G'"s5z>',xGOO4s$ # &"-335DHHVR<P<V<V<XY[bbb Bs   
Fc                    V ^8  d   QhRRRR/# )r(   	page_textr   r,   zDict[str, str]r   )r-   s   "r&   r.   r.     s     ;/ ;/3 ;/> ;/r%   c                  a T ;'       g    RP                  4        Uu. uF  qP                  4       NK  	  ppV Uu. uF  qP                  4       NK  	  ppRp\        V4       F  w  rVVR8X  g
   RV9   g   K  V^,           p M	  VR8X  d   / # 0 Rmp. o\	        V\        V4      4       F;  pW8,          V9   d    M-SP                  W(,          4       \        S4      ^P8  g   K;   M	  R V3R llp	/ p
V	! R4      V
R&   V	! R4      V
R&   V	! R	4      ;'       g	    V	! R
4      V
R&   V
P                  4        UUu/ uF  w  rV'       g   K  WbK  	  upp# u upi u upi u uppi )zParse Company Information from the Account Summary page text.

Parent Account is the first line under 'Company Information' (if present).
We pull:
  - parent_account (optional)
  - company_name
  - account_number
r0   zCOMPANY INFORMATIONc                    V ^8  d   QhRRRR/# )r(   labelr   r,   r   )r-   s   "r&   r.   9parse_company_information_from_text.<locals>.__annotate__&  s      C C r%   c                  < V P                  4       p\        S4       F  w  r#VP                  4       pWA8X  d`   \        V^,           \        S4      4       F?  pSV,          P	                  4       pV'       g   K$  VP                  4       R9   d   K;  Vu u # 	  VP                  VR,           4      '       g   K  V\        V 4      R P	                  4       u # 	  R# )   r2   Nr0   >   	ACCOUNT #COMPANY NAMEACCOUNT NUMBERPARENT ACCOUNT)r=   r   ranger   r9   
startswith)r	  lab_uidxrawruknxtsections   &      r&   _value_after9parse_company_information_from_text.<locals>._value_after&  s    !'*HCB{sQwG5A!!***,C yy{&gg J 6 }}US[))3u:;'--// + r%   r  parent_accountr  company_namer  r  account_number>   ACCOUNTSPROGRAMSATTACHMENTSCOMPANY ADDRESSTAX INFORMATIONCONTACT INFORMATIONCREDIT & BILLING INFORMATION#PAYMENT TERMS & INVOICE PREFERENCES)
splitlinesr9   r=   r   r  r   appenditems)r  lnlinesr=   startr  ustop_headersjr  outr  vr  s   &            @r&   #parse_company_information_from_textr3    sV    $-??">">"@A"@BXXZ"@EA"'(%BXXZ%E(E% %%)>!)CEE ! {		L G5#e*%8|#ux w<2 &   C()9:C&~6C()9:WWl;>WC YY[.[TQADAD[..e B(b /s   EE7EEc                   V ^8  d   QhRR/# r(   r,   r   r   )r-   s   "r&   r.   r.   >  s      # r%   c                     \        V \        P                  R^
4       V P                  \        P                  R4      pVP                  ;'       g    RP                  4       #   \         d     R# i ; i)zGet the account name from the Account Summary header span.

After clicking an account row, the portal shows the canonical account name in:
  <span id="AccountSummary:accountForm:aname">Aaron Equipment Company</span>
z AccountSummary:accountForm:anamer0   )r   r   r   r   r   r9   rJ   )r   els   & r&   "get_account_name_from_summary_spanr8  >  s\    VRUU$FK  (JK2$$&& s   AA" A" "A10A1c               $    V ^8  d   QhRRRRRR/# )r(   r   	List[str]r   zList[List[str]]r,   !Tuple[List[str], List[List[str]]]r   )r-   s   "r&   r.   r.   L  s"     ! ! !/ !No !r%   c           
        RR0p. p\        T ;'       g    . 4       FQ  w  rET;'       g    RP                  4       pV'       g   K)  VP                  4       V9   d   K@  VP                  V4       KS  	  V'       g   W3# V Uu. uF  q@V,          NK  	  pp. pT;'       g    .  F;  p	TP                  V Uu. uF  qD\	        V	4      8  d	   W,          MRNK  	  up4       K=  	  Wx3# u upi u upi )a  Drop backend/hidden columns that DataTables sometimes includes in the DOM.

The Red Wing portal product table often contains extra non-user columns such as:
  - Product Id
  - Filter Number

These may be invisible in the UI but appear in our scrape output.
z
product idzfilter numberr0   )r   r9   rQ   r)  r   )
r   r   
drop_nameskeep_idxr  r   hhnew_headersnew_rowsrs
   &&        r&   drop_hidden_product_columnsrC  L  s     0JH'--R(gg2__88:# ) }'/0x!1::xK0 "HZZRZxHx!c!f*!$"4xHI   	 1 Is    C#3"C(
c                   V ^8  d   QhRR/# )r(   r,   %List[Tuple[LinkCell, Dict[str, str]]]r   )r-   s   "r&   r.   r.   j  s     ( (-R (r%   c                   \        V \        P                  \        ^<4       \	        V \        4       \
        P                  ! R4       \        V ^4       \        V \        4       . p\        4       p\        R4       EF  p\        V \        4      pV'       d   VP                  R4      '       g    V# VP                  R4      ;'       g    .  Uu. uF  qUNK  	  ppVP                  R4      ;'       g    .  EF	  pVP                  R4      ;'       g    / pVP                  R4      ;'       g    RP                  4       p	V	'       d   W9   d   KX  VP                  V	4       \        VP                  R4      ;'       g    RP                  4       V	R	7      p
VP                  R
4      ;'       g    . p/ p\!        V4       F.  w  rV\#        V4      8  g   K  W,          Y;'       g    RV 2&   K0  	  VP%                  W34       EK  	  \'        V \        4      pV'       g    V# \
        P                  ! R4       \        V ^4       EK  	  V# u upi )zHReturn ALL account links + row mappings from the accounts results table.皙?i,  r   r   r   r   r0   r   r   r   r   ffffff?)r   r   r   r   set_datatable_length_allr   r   r   datatable_goto_firstsetr  r   r_   r9   addr   r   r   r)  datatable_click_next)r   r1  seenr6   r   r   r   rowr   r   	link_cellr   r  r  clickeds   &              r&   get_all_accounts_from_resultsrR  j  s    126 V%67JJsO$!2313C5D3Z ):;488F++0 J- +/((9*=*C*C*CE*CAa*CE88F#))r)C776?((bDHHV$**113D4<HHTN txx'7'='=2&D&D&FTRI"www/552E&(G!'*s5z>/4xGOO4s, + JJ	+, * 'v/@A J 	

4VR(3 6 J- Fs   
Ic                    V ^8  d   QhRRRR/# )r(   r   r   r,   List[Dict[str, object]]r   )r-   s   "r&   r.   r.     s      s 7N r%   c                z    Rp V P                  W!4      p\        T;'       g    . 4      #   \         d    . u # i ; i)z>Snapshot program rows: link text/href + checkbox active state.aF  
        const table = document.getElementById(arguments[0]);
        if (!table) return [];
        const rows = Array.from(table.querySelectorAll('tbody tr')).map(tr => {
            const a = tr.querySelector('a[href]');
            if (!a) return null;
            const cb = tr.querySelector('input[type="checkbox"]');
            const active = cb ? (cb.checked || cb.getAttribute('checked') !== null) : false;
            return {text: (a.innerText||'').trim(), href: a.getAttribute('href')||'', active: active};
        }).filter(x => x && x.href);
        return rows;
    )r   listrJ   )r   r   r   ress   &&  r&   js_program_list_snapshotrX    sB    F##F5CII2 	s   * * ::c                    V ^8  d   QhRRRR/# )r(   only_activerC   r,   rT  r   )r-   s   "r&   r.   r.     s     % %4 %D[ %r%   c           
     F    \        V \        P                  \        ^4       \        T \        4       \        P                  ! R4       \        T ^4       \        T \        4       . p\        4       p\        ^4       EF  p\        T \        4      pT F  pTP                  R4      ;'       g    RP                  4       pT'       d   Ys9   d   K<  TP                  T4       \!        TP                  R4      4      pT'       d   T'       g   Ky  TP#                  RTP                  R4      ;'       g    RP                  4       RTRT/4       K  	  \%        T \        4      p	T	'       g    T# \        P                  ! R4       \        T ^4       EK  	  T#   \         d    . u # i ; i)zmReturn program links from the 'List Of Account Related Programs' table.

Returns dicts: {text, href, active}
rG  r   r0   activer   rH  )r   r   r   PROGRAM_TABLE_IDr   rI  r   r   r   rJ  rK  r  rX  r_   r9   rL  rC   r)  rM  )
r   rZ  r1  rN  r6   r   rB  r   r\  rQ  s
   &&        r&   get_account_program_linksr^    sI   
VRUU$4b9
 V%56JJsO$!12#%C5D3Z'0@AAEE&M''R..0D4<HHTN!%%/*F6JJv!4!4" ; ; =vtXW]^_  'v/?@ J 	

4VR(! $ J=  	s    F F F c                   V ^8  d   QhRR/# r5  r   )r-   s   "r&   r.   r.     s      S r%   c                    Rp V P                  V4      pT;'       g    RP                  4       #   \         d     R# i ; i)z8Best-effort extraction of the 'Program Summary' section.a  
        function norm(s){return (s||'').replace(/\s+/g,' ').trim();}
        const headers = Array.from(document.querySelectorAll('h1,h2,h3,h4,legend,strong'));
        let h = headers.find(x => norm(x.innerText).toLowerCase() === 'program summary');
        if (!h) {
            // fallback: contains
            h = headers.find(x => norm(x.innerText).toLowerCase().includes('program summary'));
        }
        if (!h) return '';

        // Walk forward through siblings collecting text until the next header-like element.
        let out = [];
        let n = h.nextElementSibling;
        let guard = 0;
        while (n && guard < 200) {
            const tag = (n.tagName||'').toLowerCase();
            const txt = norm(n.innerText);
            if (['h1','h2','h3','h4','legend'].includes(tag)) break;
            if (txt) out.push(txt);
            n = n.nextElementSibling;
            guard++;
        }
        return out.join('\n\n');
    r0   )r   r9   rJ   )r   r   txts   &  r&   extract_program_summary_blockrb    sE    F0##F+		r  "" s   / / >>c                    V ^8  d   QhRRRR/# )r(   required_headerszSequence[str]r,   rZ   r   )r-   s   "r&   r.   r.     s      M m r%   c                    V Uu. uF   q"P                  4       P                  4       NK"  	  ppRp V P                  WC4      # u upi   \         d     R# i ; i)zfReturn a table id for the first table that contains all required headers (case-insensitive substring).a6  
        const required = arguments[0];
        const tables = Array.from(document.querySelectorAll('table'));
        for (const t of tables) {
            const ths = Array.from(t.querySelectorAll('thead th')).map(th => (th.innerText||'').trim().toLowerCase());
            if (!ths.length) continue;
            let ok = true;
            for (const r of required) {
                if (!ths.some(h => h.includes(r))) { ok = false; break; }
            }
            if (ok) {
                return t.id || null;
            }
        }
        return null;
    N)r9   rQ   r   rJ   )r   rd  rB  requiredr   s   &&   r&   find_table_by_headersrg    sY    +;<+;a	!+;H<F $$V66% =&  s   &AA AAc                    V ^8  d   QhRRRR/# r(   r   r   r,   r   r   )r-   s   "r&   r.   r.     s      s t r%   c                R    Rp V P                  W!4       R#   \         d     R# i ; i)zOTry to set the DataTable page length to 'All' (value -1) or the largest option.a	  
        const tableId = arguments[0];
        const sel = document.querySelector(`select[name='${tableId}_length'], select[name$='_length']`);
        if (!sel) return false;

        const opts = Array.from(sel.options);
        let chosen = opts.find(o => o.value === '-1') || opts.find(o => (o.text||'').toLowerCase().includes('all')) || opts[opts.length-1];
        if (!chosen) return false;
        sel.value = chosen.value;
        sel.dispatchEvent(new Event('change', {bubbles:true}));
        return true;
    N)r   rJ   r   s   && r&   rI  rI    s.    Ff/ s    &&c                    V ^8  d   QhRRRR/# )r(   r   r   r,   rC   r   )r-   s   "r&   r.   r.      s      3 4 r%   c                `    Rp \        V P                  W!4      4      #   \         d     R# i ; i)zAClick Next for a DataTable if available. Returns True if clicked.a  
        const tableId = arguments[0];
        const pag = document.getElementById(`${tableId}_paginate`);
        if (!pag) return false;
        const next = pag.querySelector('a.paginate_button.next');
        if (!next) return false;
        const cls = (next.className||'').toLowerCase();
        if (cls.includes('disabled') || next.getAttribute('aria-disabled') === 'true') return false;
        next.click();
        return true;
    F)rC   r   rJ   r   s   && r&   rM  rM     s6    
FF))&;<< s    --c                    V ^8  d   QhRRRR/# ri  r   )r-   s   "r&   r.   r.   2  s     ' '3 '4 'r%   c                    Rp \        V P                  W!4      4      pV'       d%   \        P                  ! R4       \	        V ^4       R# R#   \
         d     R# i ; i)z:Best-effort: jump DataTables pagination to the first page.a6  
        const tableId = arguments[0];
        const pag = document.getElementById(`${tableId}_paginate`);
        if (!pag) return false;

        const isDisabled = (a) => {
            if (!a) return true;
            const cls = (a.className||'').toLowerCase();
            if (cls.includes('disabled')) return true;
            if (a.getAttribute('aria-disabled') === 'true') return true;
            return false;
        };

        // Prefer "First" button if present
        const first = pag.querySelector('a.paginate_button.first');
        if (first && !isDisabled(first)) { first.click(); return true; }

        // Otherwise click the smallest numbered page button (usually page 1)
        const nums = Array.from(pag.querySelectorAll('a.paginate_button')).filter(a => {
            const cls = (a.className||'').toLowerCase();
            if (cls.includes('previous') || cls.includes('next') || cls.includes('first') || cls.includes('last')) return false;
            const txt = (a.innerText||'').trim();
            return /^\d+$/.test(txt);
        });
        if (nums.length > 0) {
            nums.sort((a,b) => parseInt(a.innerText.trim(),10) - parseInt(b.innerText.trim(),10));
            const btn = nums[0];
            if (!isDisabled(btn)) { btn.click(); return true; }
        }
        return false;
    g      ?N)rC   r   r   r   r   rJ   )r   r   r   rQ  s   &&  r&   rJ  rJ  2  sU    F>v,,V>?JJt ,   s   AA AAc               $    V ^8  d   QhRRRRRR/# )r(   r-  r   r   r,   r:  r   )r-   s   "r&   r.   r.   \  s!       #  r%   c                ,   T ;'       g    RP                  4       P                  4       p T;'       g    RP                  4       P                  4       pRp. pV F9  pV F0  pWE,           pW`8  d   K  Wa8  d   Vu u # VP                  V4       K2  	  K;  	  V# )zLReturn a list of two-letter search terms from start..end inclusive (aa..zz).r   zzabcdefghijklmnopqrstuvwxyz)r9   rQ   r)  )r-  r   letterstermsr   bts   &&     r&   iter_two_letter_termsrw  \  s    ]]d!!#))+E;;$



%
%
'C*GEAAywLLO   Lr%   c                    V ^8  d   QhRRRR/# r(   
state_pathr   r,   Dict[str, set]r   )r-   s   "r&   r.   r.   n  s      $ > r%   c                   R\        4       R\        4       /p V P                  4       '       d   \        P                  ! V P	                  RR7      ;'       g    R4      p\        R VP                  R4      ;'       g    .  4       4      VR&   \        R VP                  R4      ;'       g    .  4       4      VR&   V#   \         d     T# i ; i)zzLoad scraper resume state from disk.

Backward compatible with older state files that only stored `scanned_account_urls`.
scanned_account_urlscompleted_termsutf-8encoding{}c              3  8   "   T F  p\        V4      x  K  	  R # 5ir   r   r   xs   & r&   r   $load_scrape_state.<locals>.<genexpr>z  s     /iAg1AAg   c              3  p   "   T F,  p\        V4      P                  4       P                  4       x  K.  	  R # 5ir   r   r9   rQ   r  s   & r&   r   r  {  s(     *oLma3q6<<>+?+?+A+ALm   46)rK  rF   jsonloads	read_textr_   rJ   )rz  statedatas   &  r&   load_scrape_stater  n  s     	35E::j22G2DLLMD,//iJ`AaAgAgegAg/i,iE()'**oDHHUfLgLmLmkmLm*o'oE#$ L  Ls#   >B< %B< =-B< +B< <C
Cc                    V ^8  d   QhRRRR/# )r(   r  r   r,   r{  r   )r-   s   "r&   r.   r.     s     	 	c 	n 	r%   c           	        / p \         P                  ! T ;'       g    R 4      pR\        R VP	                  R4      ;'       g    .  4       4      R\        R VP	                  R4      ;'       g    .  4       4      /#   \         d    / p Lgi ; i)r  r}  c              3  8   "   T F  p\        V4      x  K  	  R # 5ir   r  r  s   & r&   r   /_load_scrape_state_from_text.<locals>.<genexpr>  s     #]5[qCFF5[r  r~  c              3  p   "   T F,  p\        V4      P                  4       P                  4       x  K.  	  R # 5ir   r  r  s   & r&   r   r    s(     c@a!s1v||~3355@ar  )r  r  rJ   rK  r_   )r  r  s   & r&   _load_scrape_state_from_textr    s    Dzz#++& 	#]TXX>T5U5[5[Y[5[#] ]3cIZ@[@a@a_a@acc   s   A< A< <BBc               $    V ^8  d   QhRRRRRR/# )r(   scannedrK  r~  r,   zDict[str, List[str]]r   )r-   s   "r&   r.   r.     s"      C # :N r%   c                N    R \        R V  4       4      R\        R V 4       4      /# )r}  c              3  J   "   T F  q'       g   K  \        V4      x  K  	  R # 5ir   r  r  s   & r&   r   !_state_payload.<locals>.<genexpr>  s     &Dw!!vs1vvws   	##r~  c              3     "   T F5  q'       g   K  \        V4      P                  4       P                  4       x  K7  	  R # 5ir   r  )r   rv  s   & r&   r   r    s,     !W/QUV"8#a&,,."6"6"8"8/s   	?/?)sorted)r  r~  s   &&r&   _state_payloadr    s,    &Dw&D D6!W/!WW r%   c                   V ^8  d   QhRR/# r   r   )r-   s   "r&   r.   r.     s     
 
 
r%   c                ^   \         e6   \         P                  ! V P                  4       \         P                  4       R # V P	                  ^ 4         \
        P                  ! V P                  4       \
        P                  ^4       R #   \         d    \        P                  ! R4        K\  i ; i)Ng?)fcntlflockfilenoLOCK_EXseekmsvcrtlockingLK_LOCKOSErrorr   r   fhs   &r&   _lock_file_exclusiver    sn    BIIK/GGAJ
	NN299;: 	JJt	s   4B !B,+B,c                   V ^8  d   QhRR/# r   r   )r-   s   "r&   r.   r.     s       r%   c                0   \         e6   \         P                  ! V P                  4       \         P                  4       R # V P	                  ^ 4        \
        P                  ! V P                  4       \
        P                  ^4       R #   \         d     R # i ; ir   )	r  r  r  LOCK_UNr  r  r  LK_UNLCKr  r  s   &r&   _unlock_filer    s_    BIIK/GGAJryy{FOOQ7 s   4B BBc                    V ^8  d   QhRRRR/# ry  r   )r-   s   "r&   r.   r.     s      $ > r%   c                R   V P                   P                  R R R7       V P                  RRR7      ;_uu_ 4       p\        V4        VP	                  ^ 4       \        VP                  4       4      \        V4       uuRRR4       #   \        T4       i ; i  + '       g   i     R# ; i)Tr   a+r  r  N)rr   r   rH   r  r  r  rI   r  )rz  r  s   & r&   _locked_read_scrape_stater    s~    D48		0	0BR 	GGAJ/	: 
1	0  
1	0	0s#   B)B/BBBB&	c               (    V ^8  d   QhRRRRRRRR/# )r(   rz  r   r  rK  r~  r,   r   r   )r-   s   "r&   r.   r.     s(      $  s t r%   c           
         V P                   P                  RRR7       V P                  RRR7      ;_uu_ 4       p\        V4        VP	                  ^ 4       \        VP                  4       4      p\        VR,          4      \        V4      ,          p\        VR,          4      \        V4      ,          p\        WV4      pVP	                  ^ 4       VP                  4        VP                  \        P                  ! V^RR7      4       VP                  4        \        P                  ! VP!                  4       4       \#        V4        R	R	R	4       R	#   \#        T4       i ; i  + '       g   i     R	# ; i  \$         d     R	# i ; i)
zMPersist resume state, merging with on-disk state (safe for parallel workers).Tr   r  r  r  r}  r~  )indent	sort_keysN)rr   r   rH   r  r  r  rI   rK  r  truncatewriter  dumpsflushrV   fsyncr  r  rJ   )rz  r  r~  r  currentmerged_scannedmerged_completedpayloads   &&&     r&   save_scrape_stater    s   t<__TG_44 $!
6rwwyA!$W-C%D!EG!T#&w/@'A#BSEY#Y (J
GAFG
%R  54 R  544  sG   8E7 E#C6E=E#	E7 E  E##E4	.E7 4E7 7FFc               $    V ^8  d   QhRRRRRR/# )r(   rz  r   account_urlr   r,   rC   r   )r-   s   "r&   r.   r.     s!      4 c d r%   c                T     \        V 4      pWR ,          9   #   \         d     R# i ; i)r}  F)r  rJ   )rz  r  r  s   && r&   is_account_scannedr    s2    )*5$:;;; s    ''c               $    V ^8  d   QhRRRRRR/# )r(   base_dirr   r  r   r,   r   )r-   s   "r&   r.   r.     s!     ) )$ )S )T )r%   c                    V R ,          pVP                  RRR7       \        P                  ! VP                  RRR7      4      P	                  4       pW# R2,          # )z.voucher_account_claimsTr   r  ignore)errorsz.lock)r   hashlibsha1encode	hexdigest)r  r  
claims_dirdigests   &&  r&   _account_claim_pathr    sW    55JTD1\\+,,WX,FGQQSF(%(((r%   c               (    V ^8  d   QhRRRRRRRR/# )	r(   r  r   r  r   stale_secondsr+   r,   Optional[Path]r   )r-   s   "r&   r.   r.     s)       3 s Zh r%   c           	     2   \        W4      p\        P                  ! 4       p\        ^4       F  p \        P                  ! \        V4      \        P                  \        P                  ,          \        P                  ,          R4      p\        P                  ! VRRR7      ;_uu_ 4       pVP                  R\        P                  ! 4        R24       VP                  R\        V4       R24       VP                  RV R24       R	R	R	4       Vu # 	  R	#   + '       g   i     L; i  \         dU     YCP                  4       P                  ,
          pY8  d   TP!                  R
R7        EKE  M  \"         d     Mi ; i  R	# \"         d      R	# i ; i)zKBest-effort account claim to reduce duplicate work across parallel workers.i  wr  r  zpid=r3   ztime=zurl=NT
missing_ok)r  r   r  rV   rH   r   O_CREATO_EXCLO_WRONLYfdopenr  getpidr+   FileExistsErrorstatst_mtimeunlinkrJ   )	r  r  r  
claim_pathnowr6   fdr  ages	   &&&      r&   try_claim_accountr    s;   $X;J
))+C1X	Z"**ryy*@2;;*NPUVB2sW554		}B/05S
"-.4}B/0 6  & ! 65
  	OO-666&%%%6 '   		s[   A:D)*AD	D)D&!D))F58E21F2F =F?F  FFFFc                    V ^8  d   QhRRRR/# )r(   r  r  r,   r   r   )r-   s   "r&   r.   r.     s      n  r%   c                d    V '       g   R #  V P                  RR7       R #   \         d     R # i ; i)NTr  )r  rJ   )r  s   &r&   release_account_claimr    s0    T* s     //c                    V ^8  d   QhRRRR/# )r(   r)   r   r,   r   )r-   s   "r&   r.   r.     s     : :# :# :r%   c                ~    \         P                  ! R RT ;'       g    RP                  4       4      P                  4       # )r5   r2   r0   )r:   r;   r9   rQ   )r)   s   &r&   
_norm_namer    s+    66&#R017799r%   c                    V ^8  d   QhRRRR/# )r(   accountsrE  r,   rC   r   )r-   s   "r&   r.   r.     s     0 0-R 0W[ 0r%   c                    \        V 4      ^	8w  d   R# V  UUu0 uF  w  r\        VP                  4      kK  	  pppV\        8H  # u uppi )	   F)r   r  r   SPECIAL_SLOW_RETRY_ACCOUNTS)r  r   r6   namess   &   r&   should_retry_term_in_slow_moder    sA    
8}2:;(wtZ		"(E;/// <s    Ac          
     ,    V ^8  d   QhRRRRRRRRRR	/# )
r(   r   r   worker_countr+   worker_index	all_termsr:  r,   rC   r   )r-   s   "r&   r.   r.     s1     0 0# 0S 0 0Xa 0fj 0r%   c                r    V^8:  d   R#  VP                  V 4      pYA,          T8H  #   \         d     R# i ; i)r  TF)index
ValueError)r   r  r  r  poss   &&&& r&   term_assigned_to_workerr    sD    qood# <//  s   ' 66c                    V ^8  d   QhRRRR/# )r(   r   r   r,   r;  r   )r-   s   "r&   r.   r.     s       9Z r%   c                   . p. p\        W4       \        P                  ! R4       \        V ^4       \	        4       p\        R4       EF  p\        W4      pV'       g    W#3# VP                  R4      ;'       g    .  Uu. uF  qwNK  	  ppVP                  R4      ;'       g    .  Fo  pVP                  R4      ;'       g    . p	RP                  V	R,          4      p
W9   d   K>  VP                  V
4       TP                  V	 Uu. uF  qNK  	  up4       Kq  	  \        W4      pV'       g    W#3# \        P                  ! R4       \        V ^4       EK  	  W#3# u upi u upi )	zSExtract all rows from a DataTables table by snapshotting each page and paging Next.r   i  r   r   r   |:N   NrG  )rI  r   r   r   rK  r  r   r_   joinrL  r)  rM  )r   r   r   r   rN  r6   r   r   rB  r   sigrg   rQ  s   &&           r&   extract_all_datatable_rowsr    s@   GD V.JJsO$5D3Z 2" =!  $xx	288b8:818:((6"((b(AEE'N((bE((59%C{HHSMKKE*EqE*+ ) 'v8 = 	

3VR(% ( =! ; +s   ?
E
Ec                   V ^8  d   QhRR/# )r(   r,   zOptional[LinkCell]r   )r-   s   "r&   r.   r.   9  s      -? r%   c                    \        V \        P                  \        ^4       Rp T P                  T\        4      pT'       g   R# \        TP                  R4      ;'       g    RP                  4       TP                  R4      ;'       g    RP                  4       R7      #   \         d     R# i ; i  \         d     R# i ; i)zGIf the first program row is active (checkbox checked), return its link.Na&  
        const table = document.getElementById(arguments[0]);
        if (!table) return null;
        const firstRow = table.querySelector('tbody tr');
        if (!firstRow) return null;
        const cb = firstRow.querySelector('input[type="checkbox"]');
        const active = cb ? (cb.checked || cb.getAttribute('checked') !== null) : false;
        if (!active) return null;
        const a = firstRow.querySelector('a[href]');
        if (!a) return null;
        return {text: (a.innerText||'').trim(), href: a.getAttribute('href')||''};
    r   r0   r   r   )
r   r   r   r]  r   r   r   r_   r9   rJ   )r   r   rW  s   &  r&   get_first_active_program_linkr	  9  s    VRUU$4b9F##F,<=cggfo33::<CGGFODYDYWYC`C`Cbcc'  (  s4    B  B2 B2 "'B2 
B2  B/.B/2C Cc               (    V ^8  d   QhRRRRRRRR/# )r(   output_folderr   md_namer   contentr,   r   )r-   s   "r&   r.   r.   U  s(      $  s t r%   c                ~    V P                  R R R7       \        V4      pW R2,          pVP                  VRR7       V# )Tr   z.mdr  r  )r   r?   
write_text)r  r  r  	base_namemd_paths   &&&  r&   write_markdownr  U  sF    t4"7+I3//Gw1Nr%   c                   V ^8  d   QhRR/# )r(   r,   r+   r   )r-   s   "r&   r.   r.   _  s     m mc mr%   c                 <   \         P                  ! 4       p V P                  R RRR7       V P                  R\        ^RR7       V P                  R\        ^ RR7       V P	                  4       pVP
                  ^8  d   V P                  R	4       VP                  ^ 8  g   VP                  VP
                  8  d   V P                  R
4       \        \        4      P                  4       P                  pV\        ,          pVR,          p\        V4      pVR,          pVR,          p\        VP                  R7      p \!        V4       \#        \$        R4      p	\'        V	4       U
Uu. uF*  w  rWP
                  ,          VP                  8X  g   K(  VNK,  	  pp
p\)        RV	^ ,           RV	Rg,           R\+        V	4       R24       \)        RVP                  ^,            RVP
                   R\+        V4       R24       \)        R\+        V4       R\+        V4       R24       V'       d   \)        RV^ ,           24       M\)        R4       V E	F  p\-        V4      pWnR,          ,          pW~R,          ,          pW9   d   K5  \)        Ri4       \)        RV 24       \)        Rh4       Rp \/        W4       VP4                  p\;        V4      p\=        V4      '       dK   \)        R!4       R"p \/        WR#R$R%R&7       VP4                  p\6        P8                  ! R'4       \;        V4      pV'       g0   \)        R(V R)24       VP?                  V4       \A        WFV4       EK  \)        R*V R+\+        V4       24       V'       d   \)        R,4       V'       d   R-MR pV'       d   R-MR.pV'       d   R/MR0pV'       d   R1MR2p\'        V^R37       EF"  w  pw  pp\C        VVPD                  4      pVV9   g   \G        VV4      '       d;   VP?                  V4       \)        R4V R\+        V4       R5VPH                   24       Ks  \K        VV4      pV'       g*   \)        R4V R\+        V4       R6VPH                   24       K   \)        R4V R\+        V4       R7VPH                   24       VP3                  R84      ;'       g5    VP3                  R94      ;'       g    VP3                  R:4      ;'       g    R;pVP3                  V4       \6        P8                  ! V4       VPM                  \N        PP                  R<4      PH                  p\S        V4      p\U        V4      pT;'       g&    VP3                  R=4      ;'       g    VPH                  pVP3                  R>4      ;'       g    Tp VP3                  R?4      ;'       g    R;p!V '       d   V R@V  2MT p"V!'       d   V" R@V! 2p"V\W        V"^RA7      ,          p#V#PY                  R"R"RB7       \[        VR"RC7      p$V$ U%u. uF'  p%\]        V%P3                  RD4      4      '       g   K%  V%NK)  	  p&p%\)        RE\+        V&4       24       V&'       EgN   . p'V'P_                  RFV 24       V '       d   V'P_                  RGV  24       V!'       d   V'P_                  RHV! 24       V'P_                  RIV 24       V'P_                  RJV 24       V'P_                  RK4       VPa                  4        F  w  p(p)V'P_                  RLV( RMV) 24       K  	  V'P_                  RN4       V'P_                  RO4       V'P_                  V4       V'P_                  RP4       RPc                  V'4      R,           p*\e        V#RQV*4       VP?                  V4       \A        WFV4        \g        V4       EK  V& EF  p%V%P3                  RR4      ;'       g    R;Pi                  4       ;'       g    RSp+V%P3                  RT4      ;'       g    R;Pi                  4       p,\C        VV,4      p-\)        RUV+ 24       VP3                  V-4       \6        P8                  ! V4       \k        V4      p.V.'       g+   VPM                  \N        PP                  R<4      PH                  p.. p/. p0\m        VRVRW.4      p1V1'       d    \o        VV14      w  p/p0\q        V/V04      w  p/p0MH\m        VRV.4      ;'       g    \m        VRX.4      p1V1'       d   \o        VV14      w  p/p0\q        V/V04      w  p/p0. p'V'P_                  RFV+ 24       V'P_                  RYV 24       V '       d   V'P_                  RZV  24       V!'       d   V'P_                  RHV! 24       V'P_                  RIV 24       V'P_                  R[V- 24       V'P_                  RJV 24       V'P_                  R\4       V'P_                  RO4       V'P_                  V.4       V'P_                  RP4       V'P_                  R]\+        V04       R^24       V/'       Ed"   V0'       Ed   V'P_                  R_4       V'P_                  R`Pc                  Ra V/ 4       4      4       V0 F  p2V2 U3u. uF/  p3T3;'       g    R;Ps                  RRb4      Ps                  R`Rc4      NK1  	  p4p3\+        V44      \+        V/4      8  d+   V4R;.\+        V/4      \+        V44      ,
          ,          ,          p4\+        V44      \+        V/4      8  d   V4Rd\+        V/4       p4V'P_                  R`Pc                  V44      4       K  	  V'P_                  RP4       MV'P_                  Re4       RPc                  V'4      R,           p*\e        V#V+V*4       VP3                  V4       \6        P8                  ! V4       EK  	  VP?                  V4       \A        WFV4       VP3                  V4       \6        P8                  ! V4       \g        V4       EK%  	  VP?                  V4       \A        WFV4       E	K  	  \)        Rf4        VPu                  4        ^ # u upp
i   \0         dT     TP3                  TP4                  4       \6        P8                  ! R 4       M  \0         d     Mi ; i\/        Y4        E	Li ; i  \0         dN     TP3                  T4       \6        P8                  ! R'4       M  \0         d     Mi ; i\/        YR#R$R%R&7        E	Li ; iu up%i u up3i   \g        T4       i ; i  TPu                  4        i ; i)jr   
store_truezRun Firefox headless)actionhelpz--worker-countz)Total number of parallel workers/browsers)typedefaultr  z--worker-indexzThis worker index (0-based)z--worker-count must be >= 1z5--worker-index must be between 0 and --worker-count-1zvoucher_scanned_accounts.jsonr}  r~  )r   rq  zSearch terms: z .. z	  (total )zWorker /z: z assigned terms.zResume (local snapshot): z completed terms, z scanned accounts.zFirst assigned term: z!No terms assigned to this worker.r3   zSEARCH TERM: Fgffffff?zTDetected known 9-account partial-load pattern. Retrying same term with slower waits.Tg      @g333333?r   )r   r   r   g       @zNo accounts returned for term 'z'.zAccounts found for 'z': zSlow mode active for this term.g?g?r   r   gffffff?g333333?)r-  [z] SKIP (already scanned): z$] SKIP (claimed by another worker): z] Account: z	Account #zAccount#col_2r0   bodyr  r  r  r6   )r*   r   )rZ  r\  z  Active programs: z# z
- **Account #**: z- **Parent Account**: z- **Account URL**: z- **Search Term**: z$
## Account Row (from results table)z- **z**: z
## Account Page Textz```textz```zAccount Summaryr   Programr   z    - StylezProduct NameProductz
- **Account Name**: z- **Account #**: z- **Program URL**: z
## Program Summaryu    
## Product List (all rows) — z rowsz```csv,c              3  D   "   T F  qP                  R R4      x  K  	  R# 5i)r"  ;N)r8   )r   r   s   & r&   r   main.<locals>.<genexpr>+  s     1_!))C2E2Es    r2   r$  Nz._No product table found (or it had no rows)._
z
All terms completed.r  zP================================================================================zQ
================================================================================);argparseArgumentParserr   r+   
parse_argsr  errorr  r   rp   rq   rr   OUTPUT_ROOT_DIRNAMEr  r   r   r   rw  r   r   r}   r   r  r   rJ   r_   current_urlr   r   rR  r  rL  r  r
   r   r  r   r  r   r   TAG_NAMEr3  r8  r?   r   r^  rC   r)  r*  r  r  r  r9   rb  rg  r  rC  r8   quit)5apargsr  vouchers_rootrz  scrape_statescanned_accountsr~  r   rt  r  rv  worker_termsr   current_stateterm_slow_modeaccounts_list_urlr  account_open_sleepprogram_open_sleepaccount_return_sleeplist_return_sleepr  account_linkr  r  r  acct_numaccount_page_textcompany_info	span_namer  r  r  folder_label
out_folderprogramsrK   active_programsr,  r  r2  
md_contentprogram_nameprogram_hrefprogram_urlprogram_summary_textproduct_headersproduct_rowsprod_table_idrB  rg   cleaneds5                                                        r&   mainrM  _  s   		 	 	"BOOL<ROSOO$3@kOlOO$3@]O^==?D1
./1 1 1T5F5F F
HIH~%%'..H22M;;J$Z0L#$:;"#45O4==1FZf%k48&/&6g&6da1?P?P;PUYUfUf:f&6guQxjU2YKyUANOd''!+,Ad.?.?-@< !!13	
 	)#o*>)??QRUVfRgQhhz{|),q/):;<56 D5jAM.D EE->??O&/"M$()(O"N3$V2 !' 2 24V<H-h77< "&s(s]`nqr %+$6$6!

38@7vR@A##D)!*P(c#h-AB78(6C(6C*83c '5309(!0L0L,,lG%&79J9JK"226HU`6a6a$((5AcU!CM?2L\M^M^L_`a.xE
!AcU!CM?2VWcWhWhVijkz6AcU!CM?+l>O>O=PQR&{{;7pp7;;z;RppV]VaVabiVjppnpHJJ{+JJ12(.(;(;BKK(P(U(U%#FGX#YL B6 JI#,#e#e0@0@0P#e#eT`TeTeL%1%5%56F%G%S%S8N%1%5%56F%G%M%M2NIWl^1^4D#E`l_mL%*6q8H'I!.1CLZ]1^!^J$$TD$A8TRH2:&T(Qd155?>Sqq(O&T/O0D/EFG*?+-r,%89)!LL+>~>N)OP)!LL+A.AQ)RS':;-%HI':4&%AB%LM$+MMODAq!LL4s$qc):; %4 %=>Y/%67U+%)YYu%5%<
&z3DjQ(,,[9)*X P **5M -()f(;(;'B'B'D'Q'Q	()f(;(;'B'B'D&-k<&H|n56

;/

#56/LV/T,3393F3Fr{{TZ3[3`3`0578:(=fwP^F_(`(<VW]_l<m9O\<WXgiu<v9O\,A&7),T  -C  -CXmnt  xA  wB  YCM,@Z[acp@q =@[\kmy@z = "r,%89'=l^%LM)!LL+<^<L)MN)!LL+A.AQ)RS':;-%HI':;-%HI':4&%AB%;<Y/%9:U+'H\IZH[[`%ab*?||!LL2!LL1_1_)_`%1cd*ecd^_AGG+<+<T3+G+O+OPSUX+Ycd*e#&w<#o2F#F$+ts?7KcRYl7Z/[$[G#&w<#o2F#F.56LO8L.MG %SXXg-> ? &2 "LL/!LL)Z[%)YYu%5%<
&z<L

;/

#78} -@ %((5%jOTJJ01JJ01)*5M 1MP %jOLI !L 	&' 	m h4  3JJv112JJsO  $V23& ! s

#45

3$ (s]`nqrrsz 'UZ +f, **5 	s  <)t	 %%p-p-Dt	 (p335t	 )r84t	 -A#t	 t	 t	 )t	 5Ct	 7>s96s9s9(A2s9s94$s9s94
s9?s9?s9"s/7s/=?s9=s9Ds9t	 ,s9
s9"s9=Cs9s9/As9?s9Cs9As9 s4.'s4Es9=t	 -t	 3r?1q10r1q?<r>q??rt	 rt	 s, 'ss,ss,ss,(t	 +s,,t	 /
s99tt	 	t__main__>	   town of la platatown of indian headbolling afb commissarycharles county utilitiesbattle creek constructioncharles county public works!charles county emergency services!maryland transportation authority-smithsonian - african american history museum>   AUXCONNULPRNCOM1COM2COM3COM4COM5COM6COM7COM8COM9LPT1LPT2LPT3LPT4LPT5LPT6LPT7LPT8LPT9)   )   )F)r   rq  )i`T  )i__doc__
__future__r   r&  r  r  rV   r:   rb   r   r  ImportErrorr  dataclassesr   pathlibr   typingr   r   r   r   r	   urllib.parser
   seleniumr   selenium.common.exceptionsr   selenium.webdriver.common.byr   selenium.webdriver.common.keysr   'selenium.webdriver.common.action_chainsr   "selenium.webdriver.firefox.optionsr   "selenium.webdriver.firefox.servicer   selenium.webdriver.supportr   r   selenium.webdriver.support.uir   r   r   r   r   r   r   r]  r*  ONLY_ACTIVE_PROGRAMSr  r   r>   r?   rM   rR   rX   rj   rw   r   r   r   r   r   r   r   r   r   r   r  r3  r8  rC  rR  rX  r^  rb  rg  rI  rM  rJ  rw  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r	  r  rM  r   
SystemExitr   r%   r&   <module>r     s  : #    	 	   "  8 8    7 + / @ 6 6 @ 7 U	  ! & $    
  $  
 8
$5p9x*$	!F]Y3"6@5 !	5
 "5 5p5
cD;/~!<(V*%PB2($'T$&	
.)6:00D8m` z
TV
 }(  Es   G6 6HH