
    i                    H   d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z		 ddl
Z
ddlmZ ddlmZ ddlmZmZmZmZmZ ddlmZ ddlmZ dd	lmZ dd
lmZ ddlm Z  ddl!m"Z" ddl#m$Z$ ddl%m&Z& ddl'm(Z) ddl*m+Z+ dZ,dZ-dZ.dZ/dZ0dZ1dZ2dZ3h dZ4 ed       G d d             Z5h dZ6dRdSdZ7dTdZ8dTd Z9dTd!Z:dUd"Z;dVd#Z<dWd$Z=dXd%Z>dYd&Z?dZd[d'Z@dZd[d(ZAdZd\d)ZBd]d*ZCdXd+ZDd,d-d.d/	 	 	 	 	 	 	 	 	 d^d0ZEdXd1ZFd_d2ZGd`d3ZHdad4ZIdbd5ZJdcd6ZKddd7ZLdedfd8ZMdad9ZNdgd:ZOdhd;ZPdid<ZQdhd=ZRdjdkd>ZSdld?ZTdmd@ZUdndAZVdXdBZWdXdCZXdldDZYdodEZZdpdFZ[dqdGZ\drdsdHZ]dtdIZ^dudJZ_dvdKZ`dwdLZadxdMZbdydNZcdzdOZdd{dPZeefdQk(  r eg ee             y# e$ r
 dZ
ddlZY vw xY w)|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ProgramListViewT>	   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)frozenc                  "    e Zd ZU ded<   ded<   y)LinkCellstrtexthrefN)__name__
__module____qualname____annotations__     +Voucher_List_Folder/voucher_scraper_core.pyr#   r#   ^   s    
I
Ir,   r#   >   AUXCONNULPRNCOM1COM2COM3COM4COM5COM6COM7COM8COM9LPT1LPT2LPT3LPT4LPT5LPT6LPT7LPT8LPT9c                ~   | xs dj                  dd      j                  dd      j                  dd      j                         } t        j                  dd|       } t        j                  dd|       } t        j                  d	d|       } | j	                  d
      } | sd} | j                         t        v r|  d} | d| S )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)smax_lens     r-   _safe_fs_componentrV   k   s     
b$$,,T37??cJPPRA
vsAA 	Q'A 	~r1%A 	
Awwy%%cGXg;r,   c                    	 t        |       }|j                         r|j                         sy|j                  d      5 }|j	                  d      dk(  cddd       S # 1 sw Y   yxY w# t
        $ r Y yw xY w)zNReturn True if `path` looks like a real ELF binary (not a shell/snap wrapper).Frb   s   ELFN)r   existsis_fileopenread	Exception)pathpfs      r-   _is_elf_executablerb      sg    JxxzVVD\ 	+Q66!9
*	+ 	+ 	+ s3   +A) A) A	A) A&"A) &A) )	A54A5c                    	 t        |       }|j                         xr/ |j                         xr |j                  j	                         dk(  S # t
        $ r Y yw xY w)Nz.exeF)r   rZ   r[   suffixlowerr^   )r_   r`   s     r-   _is_windows_executablerf      sN    JxxzHaiikHahhnn.>&.HH s   AA 	AAc                T    t         j                  dk(  rt        |       S t        |       S )Nnt)osnamerf   rb   )r_   s    r-   _is_usable_executablerk      s#    	ww$%d++d##r,   c                 2   t         j                  j                  d      } | r_t        t	        |       j                               } t        |       r| S t        t	        |       j                  d            }t        |      r|S t         j                  dk(  rFddt        j                  d      t        j                  d      g}|D ]  }|st        |      s|c S  yd	d
g}|D ]  }t        |      s|c S  t        j                  d      t        j                  d      t        j                  d      g}|D ]a  }|rt        |      r|c S |st	        |      j                  dk(  s0t        t	        |      j                  d            }t        |      s_|c S  y)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-binrh   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)ri   environgetr$   r   
expanduserrk   	with_namerb   rj   shutilwhichrf   )env_binsibwin_candidatescsnap_candidatespath_candidatess         r-   _locate_firefox_binaryr{      sr    jjnn-.Gd7m..01 )N$w-))-89c"J	ww$;ALL'LL#	
   	A+A.	  	<7O  a H 	]#Y]#O
  #A&Ha*d1g''67C!#&
 r,   c                L   t         j                  j                  d      }|r/t        t	        |      j                               }t        |      r|S t         j                  dk(  r| r0t        t	        |       j                  d            }t        |      r|S t        j                  d      t        j                  d      g}|D ]  }|st        |      s|c S  t        t	        t              j                         j                  dz        ddg}|D ]  }t        |      s|c S  yd}t        |      r|S | r0t        t	        |       j                  d            }t        |      r|S t        j                  d      }|rt        |      r|S g d	}|D ]  }t        |      s|c S  y)
z<Locate geckodriver with sane preferences for Pi/snap setups.GECKODRIVER_PATHrh   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)ri   ro   rp   r$   r   rq   rk   rj   rr   rf   rs   rt   __file__resolveparentrb   )firefox_bin	env_geckorv   rz   rx   common
snap_geckogeckos           r-   _locate_geckodriverr      s    

12IY2245	 +	ww$d;'112CDEC%c*
LL*+LL'
 ! 	A+A.	 X&&(//2CCD'/

  	A%a(	  EJ*% ${#--m<=c"J LL'E#E*F
  a H r,   c                X   t        |       }t        t        t              j	                         j
                  dz        }|rt        d|        	 t        ||      S t        d       	 t        |      S # t        $ r t        ||      cY S w xY w# t        $ r t        |      cY S w xY w)Nzgeckodriver.logz[driver] Using geckodriver: )executable_path
log_output)r   log_pathzN[driver] WARNING: Could not locate geckodriver; letting Selenium try defaults.)r   )r   )	r   r$   r   r   r   r   printr   	TypeError)r   r   r   s      r-   _geckodriver_servicer     s    ,E4>))+225FFGH,UG45	E5XFF 

Z[*(++  	E58DD	E  *))*s$   A5 )B 5BBB)(B)c                 L   	 t        t              j                         j                  } | dz  }|j	                  dd       t
        j                  j                  dt        |             t
        j                  j                  dt        | dz               y# t        $ r Y yw xY w)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   r   r   r   mkdirri   ro   
setdefaultr$   r^   )base	cache_dirs     r-   _configure_selenium_runtime_envr   (  s    H~%%'..,,	t4


os9~>


.D8O0DE s   BB 	B#"B#c                j   t                t               }| r|j                  d       t               }|rt	        d|        ||_        nt	        d       |j                  dd       |j                  dd       	 t        j                  t        |      |      }	 |j                  dd       |S # t        $ rj t        j                  d	k(  rUt	        d
       t	        d|rdnd        t	        d|xs d        t	        dt        |      rdnd        t	        d        w xY w# t        $ r Y |S w xY w)N
--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optionsrh   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_argumentr{   r   binary_locationset_preferencer   Firefoxr   r^   ri   rj   r   set_window_size)headlessoptsr   drivers       r-   build_driverr   4  sA   #%9D,' )*K/}=>*vw 	6>,e4
""+?+LVZ[tS) M  77d?121;%D1QRS-k.J]-KLM51+>UDIK Lhi  Ms   : B/ D% /A3D"%	D21D2c                b    t        | |      j                  t        j                  ||f            S N)r   untilECpresence_of_element_locatedr   byvaluetimeouts       r-   wait_presentr   W  s+    )//0N0NPRTY{0[\\r,   c                b    t        | |      j                  t        j                  ||f            S r   )r   r   r   element_to_be_clickabler   s       r-   wait_clickabler   [  s*    )//0J0JBPU;0WXXr,   c                J   t        j                          |z   }t        j                          |k  rf	 | j                  t        j                  d      }|syt	        d |D              sy	 t        j                  d       t        j                          |k  reyy# t
        $ r Y yw xY w)z@Best-effort wait for DataTables processing overlay to disappear.zdiv.dataTables_processingNc              3  <   K   | ]  }|j                           y wr   )is_displayed).0r`   s     r-   	<genexpr>z'datatables_wait_idle.<locals>.<genexpr>g  s     7Aq~~'7s   g?)timefind_elementsr   CSS_SELECTORanyr^   sleep)r   r   endprocss       r-   datatables_wait_idler   _  s    
))+
C
))+
	((:UVE777 8 	

3 ))+
  		s   "B B 	B"!B"c                *    d}| j                  ||      S )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   table_idscripts      r-   js_table_snapshotr   n  s    F   22r,   c                   | j                  t               t        | t        j                  dd      }t        | t        j                  dd      }	 | j                  d|       	 |j                          |j                  t               	 |j                          |j                  t               	 | j                  t        j                  d      j                          t        | t        j                  t        d       y # t        $ r Y w xY w# t        $ r Y w xY w# t        $ r Y w xY w# t        $ r" |j                  t        j                         Y xw xY w)Nusername   passwordz.arguments[0].scrollIntoView({block:'center'});Login<   )rp   	LOGIN_URLr   r   IDr   r^   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 ..D* =	D	D		DD	D'&D'*(EE      @      ?      ?pre_enter_sleeppost_enter_sleepfinal_sleepc               L   |xs dj                         j                         }t        d| d       t        | t        j
                  t        d      }|j                          	 |j                          |j                  |       t        j                  |       	 |j                          	 |j                  t        j                          	 |j                  t        j"                         	 t%        |       j                  t        j                         j'                          	 |j                  t        j(                         t        j                  |       t+        | d       t        j                  |       y# t        $ rT 	 |j                  t        j                  d       |j                  t        j                         n# t        $ r Y nw xY wY aw xY w# t        $ r Y :w xY w# t        $ r Y *w xY w# t        $ r Y w xY w# t        $ r Y w xY w# t        $ r Y w xY w)z6Type `term` into accSearchBox, wait, then press Enter.rE   zFiltering accounts: typing 'z' into accSearchBox...r   a   N)rN   re   r   r   r   r   r   r   r   r^   r   r   CONTROL	BACKSPACEr   r   r   RETURNr   performTABr   )r   termr   r   r   boxs         r-   filter_accounts_termr     s    JB%%'D	(.D
EF
(8"
=CIIK		 MM$JJ		djj!dkk"V&&tzz2::<dhh 	JJ $JJ{K  	MM$,,,MM$..) 		  
      
  s   #E8 G +G( G8 +6H "H 8	G?GG	GGGGG	G%$G%(	G54G58	HH	HH	H#"H#c                "    t        | t              S )z&Back-compat wrapper: uses SEARCH_TERM.)r   SEARCH_TERM)r   s    r-   filter_accounts_simpler     s    44r,   c                   t        | t        j                  t        d       dd}t	        | d      j                  |       t        | t              }|r|j                  d      st        d      |j                  d      xs g D cg c]  }| }}|d   d   }|j                  d      }|r|j                  d      st        d	      |j                  d
      xs g }i }t        |      D ]#  \  }	}|	t        |      k  s||	   ||xs d|	 <   % t        |j                  dd      j                         |j                  dd      j                               |fS c c}w )zHReturn the first account link + row mapping from accounts results table.r   c                    	 | j                  t        j                  t              }|j	                  t        j
                  d      }t        |      dkD  S # t        $ r Y yw xY w)Nztbody tr td a[href]r   F)r   r   r   ACCOUNTS_TABLE_IDr   r   lenr^   )dtblr   s      r-   _has_first_linkz7get_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.headersr   linkr&   z9Could not find a clickable account link in the first row.cellscol_r%   rE   r%   r&   )returnbool)r   r   r   r   r   r   r   rp   RuntimeError	enumerater   r#   rN   )
r   r   snaphr   firstr   r   row_mapis
             r-   get_first_account_from_resultsr    sF     126 &"##O4V%67Dtxx'FGG&*hhy&9&?RA!AGALOE99VDtxx'VWWyy)/RE G'" 01s5z>',QxGAO4s$0 &"-335DHHVR<P<V<V<XY[bbb Bs   	Ec                b   | xs dj                         D cg c]  }|j                          }}|D cg c]  }|j                          }}d}t        |      D ]  \  }}|dk(  sd|v s|dz   } n |dk(  ri S h d}g t	        |t        |            D ]/  }||   |v r n&j                  ||          t              dk\  s/ n dfd}	i }
 |	d      |
d	<    |	d
      |
d<    |	d      xs  |	d      |
d<   |
j                         D ci c]  \  }}|s	|| c}}S c c}w c c}w c c}}w )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
    rE   zCOMPANY INFORMATION   >   COMPANY ADDRESSTAX INFORMATIONCONTACT INFORMATIONCREDIT & BILLING INFORMATION#PAYMENT TERMS & INVOICE PREFERENCESACCOUNTSPROGRAMSATTACHMENTSP   c                p   | j                         }t              D ]  \  }}|j                         }||k(  rKt        |dz   t                    D ]0  }|   j	                         }|s|j                         dv r,|c c S  |j                  |dz         s{|t        |       d  j	                         c S  y)Nr
  >   	ACCOUNT #COMPANY NAMEACCOUNT NUMBERPARENT ACCOUNTrG   rE   )rR   r  ranger   rN   
startswith)labellab_uidxrawruknxtsections          r-   _value_afterz9parse_company_information_from_text.<locals>._value_after%  s    !'* 	0HCBU{sQwG5 A!!***,C yy{&gg J }}US[)3u:;'--//	0 r,   r  parent_accountr  company_namer  r  account_number)r  r$   r   r$   )
splitlinesrN   rR   r  r  r   appenditems)	page_textlnlinesrR   startr  ustop_headersjr#  outr   vr"  s                @r-   #parse_company_information_from_textr3    sZ    $-?">">"@ABRXXZAEA"'(BRXXZ(E(E%  1%%)>!)CEE {		L G5#e*% 8|#uQx w<2  C()9:C&~6C()9:Wl;>WC YY[.TQAAqD..e B(b /s   D!D&
D+D+c                    	 t        | t        j                  dd       | j                  t        j                  d      }|j                  xs dj                         S # t        $ r Y yw xY w)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:aname
   rE   )r   r   r   r   r%   rN   r^   )r   els     r-   "get_account_name_from_summary_spanr7  =  sZ    VRUU$FK  (JK2$$&& s   AA 	A('A(c           
     |   ddh}g }t        | xs g       D ]@  \  }}|xs dj                         }|s|j                         |v r0|j                  |       B |s| |fS |D cg c]  }| |   	 }}g }|xs g D ]4  }	|j                  |D cg c]  }|t	        |	      k  r|	|   nd c}       6 ||fS c c}w c c}w )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 numberrE   )r  rN   re   r(  r   )
r   r   
drop_nameskeep_idxr  r  hhnew_headersnew_rowsrs
             r-   drop_hidden_product_columnsr?  K  s     0JH'-R( 1g2__88:# }'/0!71:0K0 "HZR JxH!!c!f*!A$"4HIJ  	 1 Is   #B4B9
c                   t        | t        j                  t        d       t	        | t               t        j                  d       t        | d       t        | t               g }t               }t        d      D ]t  }t        | t              }|r|j                  d      s |S |j                  d      xs g D cg c]  }| }}|j                  d      xs g D ]  }|j                  d      xs i }|j                  d      xs d	j                         }	|	r|	|v rB|j                  |	       t        |j                  d
      xs d	j                         |	      }
|j                  d      xs g }i }t!        |      D ]#  \  }}|t#        |      k  s||   ||xs d| <   % |j%                  |
|f        t'        | t              }|s |S t        j                  d       t        | d       w |S c c}w )zHReturn ALL account links + row mappings from the accounts results table.r   皙?r   i,  r   r   r   r&   rE   r%   r   r   r   ffffff?)r   r   r   r   set_datatable_length_allr   r   r   datatable_goto_firstsetr  r   rp   rN   addr#   r  r   r(  datatable_click_next)r   r1  seenrK   r  r  r   rowr   r&   	link_cellr   r  r  clickeds                  r-   get_all_accounts_from_resultsrL  i  s    126 V%67JJsO$!2313C5D3Z ) ):;488F+0 J- +/((9*=*CEAaEE88F#)r 	-C776?(bDHHV$*113D44<HHTN txx'7'=2&D&D&FTRI"www/52E&(G!'* 81s5z>/4QxGAO4s,8 JJ	7+,	- 'v/@A J 	

4VR(3)6 J- Fs   :	G3c                l    d}	 | j                  ||      }t        |xs g       S # t        $ r g cY S w xY w)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   listr^   )r   r   r   ress       r-   js_program_list_snapshotrP    sB    F##FH5CI2 	s    % 33c                   	 t        | t        j                  t        d       t        | t               t        j                  d       t        | d       t        | t               g }t               }t        d      D ]  }t        | t              }|D ]  }|j                  d      xs dj                         }|r||v r-|j                  |       t!        |j                  d            }|r|s]|j#                  |j                  d      xs dj                         ||d	        t%        | t              }	|	s |S t        j                  d
       t        | d        |S # t        $ r g cY S w xY w)zuReturn program links from the 'List Of Account Related Programs' table.

    Returns dicts: {text, href, active}
    r   rA  r      r&   rE   activer%   )r%   r&   rS  rB  )r   r   r   PROGRAM_TABLE_IDr   rC  r   r   r   rD  rE  r  rP  rp   rN   rF  r   r(  rG  )
r   only_activer1  rH  rK   r   r>  r&   rS  rK  s
             r-   get_account_program_linksrV    sK   
VRUU$4b9
 V%56JJsO$!12#%C5D3Z )'0@A 	`AEE&M'R..0D44<HHTN!%%/*F6JJv!4" ; ; =tW]^_	` 'v/?@ J 	

4VR(!)$ J=  	s    E E,+E,c                p    d}	 | j                  |      }|xs dj                         S # t        $ r Y yw xY w)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');
    rE   )r   rN   r^   )r   r   txts      r-   extract_program_summary_blockrY    sC    F0##F+	r  "" s   $) 	55c                    |D cg c]   }|j                         j                         " }}d}	 | j                  ||      S c c}w # t        $ r Y yw xY w)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)rN   re   r   r^   )r   required_headersr>  requiredr   s        r-   find_table_by_headersr]    sZ    +;<a	!<H<F $$VX66% =&  s   %AA 	AAc                L    d}	 | j                  ||       y# t        $ r Y yw xY w)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   r^   r   s      r-   rC  rC    s0    Ffh/ s    	##c                \    d}	 t        | j                  ||            S # t        $ r Y yw xY w)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)r   r   r^   r   s      r-   rG  rG    s8    
FF))&(;<< s    	++c                    d}	 t        | j                  ||            }|r"t        j                  d       t	        | d       yy# t
        $ r Y yw xY w)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)r   r   r   r   r   r^   )r   r   r   rK  s       r-   rD  rD  1  sW    F>v,,VX>?JJt ,   s   >A 	AAc                    | xs dj                         j                         } |xs dj                         j                         }d}g }|D ]0  }|D ])  }||z   }|| k  r||kD  r|c c S |j                  |       + 2 |S )zLReturn a list of two-letter search terms from start..end inclusive (aa..zz).r   zzabcdefghijklmnopqrstuvwxyz)rN   re   r(  )r-  r   letterstermsr   bts          r-   iter_two_letter_termsri  [  s    ]d!!#))+E;$



%
%
'C*GE  	AAA5y3wLLO	 Lr,   c                d   t               t               d}	 | j                         ryt        j                  | j	                  d      xs d      }t        d |j                  d      xs g D              |d<   t        d |j                  d      xs g D              |d<   |S # t        $ r Y |S w xY w)	zLoad 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  2   K   | ]  }t        |        y wr   r$   r   xs     r-   r   z$load_scrape_state.<locals>.<genexpr>y  s     /i1A/i   rl  c              3  j   K   | ]+  }t        |      j                         j                          - y wr   r$   rN   re   rt  s     r-   r   z$load_scrape_state.<locals>.<genexpr>z  s#     *oa3q6<<>+?+?+A*o   13rm  )rE  rZ   jsonloads	read_textrp   r^   )
state_pathstatedatas      r-   load_scrape_stater  m  s     !$5E::j22G2DLMD,//iJ`AaAgeg/i,iE()'**oDHHUfLgLmkm*o'oE#$ L  Ls   B	B" "	B/.B/c                    i }	 t        j                  | xs d      }t        d |j	                  d      xs g D              t        d |j	                  d      xs g D              dS # t        $ r i }Y Xw xY w)Nrq  c              3  2   K   | ]  }t        |        y wr   rs  rt  s     r-   r   z/_load_scrape_state_from_text.<locals>.<genexpr>  s     #]qCF#]rv  rl  c              3  j   K   | ]+  }t        |      j                         j                          - y wr   rx  rt  s     r-   r   z/_load_scrape_state_from_text.<locals>.<genexpr>  s#     c!s1v||~335cry  rm  rk  )rz  r{  r^   rE  rp   )r  r  s     r-   _load_scrape_state_from_textr    s|    Dzz#+& !$#]TXX>T5U5[Y[#] ]cIZ@[@a_acc   s   A( (A65A6c                L    t        d | D              t        d |D              dS )Nc              3  8   K   | ]  }|st        |        y wr   rs  rt  s     r-   r   z!_state_payload.<locals>.<genexpr>  s     &D!!s1v&Ds   c              3  p   K   | ].  }|st        |      j                         j                          0 y wr   rx  )r   rh  s     r-   r   z!_state_payload.<locals>.<genexpr>  s&     !WQUV#a&,,."6"6"8!Ws   6,6rk  )sorted)scannedrm  s     r-   _state_payloadr    s(     &&Dw&D D!!W/!WW r,   c                L   t         3t        j                  | j                         t         j                         y | j	                  d       	 	 t        j                  | j                         t
        j                  d       y # t        $ r t        j                  d       Y nw xY wZ)Nr   r
  g?)fcntlflockfilenoLOCK_EXseekmsvcrtlockingLK_LOCKOSErrorr   r   fhs    r-   _lock_file_exclusiver    ss    BIIK/GGAJ
	NN299;: 	JJt		 s   3B B"!B"c                   t         3t        j                  | j                         t         j                         y | j	                  d       	 t        j                  | j                         t
        j                  d       y # t        $ r Y y w xY w)Nr   r
  )	r  r  r  LOCK_UNr  r  r  LK_UNLCKr  r  s    r-   _unlock_filer    s_    BIIK/GGAJryy{FOOQ7 s   3B   	BBc                0   | j                   j                  dd       | j                  dd      5 }t        |       	 |j	                  d       t        |j                               t        |       cd d d        S # t        |       w xY w# 1 sw Y   y xY w)NTr   a+rn  ro  r   )r   r   r\   r  r  r  r]   r  )r}  r  s     r-   _locked_read_scrape_stater    s    D48		0 BR 	GGAJ/	:   s"   B)A<'B<B		BBc                   	 | j                   j                  dd       | j                  dd      5 }t        |       	 |j	                  d       t        |j                               }t        |d         t        |      z  }t        |d         t        |      z  }t        ||      }|j	                  d       |j                          |j                  t        j                  |d	d
             |j                          t        j                  |j!                                t#        |       	 ddd       y# t#        |       w xY w# 1 sw Y   yxY w# t$        $ r Y yw xY w)zMPersist resume state, merging with on-disk state (safe for parallel workers).Tr   r  rn  ro  r   rl  rm     )indent	sort_keysN)r   r   r\   r  r  r  r]   rE  r  truncatewriterz  dumpsflushri   fsyncr  r  r^   )r}  r  rm  r  currentmerged_scannedmerged_completedpayloads           r-   save_scrape_stater    s$   t<__TG_4 	! $!
6rwwyA!$W-C%D!EG!T#&w/@'A#BSEY#Y (9IJ
GAFG
%R 	! 	! R 	! 	!  sF   0E E	C%D9$E	0E 9EE		EE E 	E! E!c                F    	 t        |       }||d   v S # t        $ r Y yw xY w)Nrl  F)r  r^   )r}  account_urlr~  s      r-   is_account_scannedr    s4    )*5e$:;;; s    	  c                    | dz  }|j                  dd       t        j                  |j                  dd            j	                         }|| dz  S )Nz.voucher_account_claimsTr   rn  ignore)errorsz.lock)r   hashlibsha1encode	hexdigest)base_dirr  
claims_dirdigests       r-   _account_claim_pathr    sY    55JTD1\\+,,WX,FGQQSF6(%(((r,   c                   t        | |      }t        j                         }t        d      D ]  }	 t        j                  t        |      t        j                  t        j                  z  t        j                  z  d      }t        j                  |dd      5 }|j                  dt        j                          d       |j                  dt        |       d       |j                  d	| d       d
d
d
       |c S  y
# 1 sw Y   xY w# t        $ rN 	 ||j                         j                  z
  }||kD  r|j!                  d       Y "	 Y  y
# t"        $ r Y Y  y
w xY wt"        $ r Y  y
w xY w)zKBest-effort account claim to reduce duplicate work across parallel workers.r  i  wrn  ro  zpid=rH   ztime=zurl=NT
missing_ok)r  r   r  ri   r\   r$   O_CREATO_EXCLO_WRONLYfdopenr  getpidintFileExistsErrorstatst_mtimeunlinkr^   )	r  r  stale_seconds
claim_pathnowrK   fdr  ages	            r-   try_claim_accountr    sQ   $X{;J
))+C1X 	Z"**ryy*@2;;*NPUVB2sW5 14		}B/05S
"-.4}B/01 & !1 1
  	JOO-666&%%%6 '
     		sI   A(DAD3	DD
	D	E/4E	E E/E  E/.E/c                N    | sy 	 | j                  d       y # t        $ r Y y w xY w)NTr  )r  r^   )r  s    r-   release_account_claimr    s0    T* s    	$$c                p    t        j                  dd| xs dj                               j                         S )NrJ   rG   rE   )rO   rP   rN   re   )rT   s    r-   
_norm_namer     s)    66&#R017799r,   c                    t        |       dk7  ry| D ch c]  \  }}t        |j                         }}}|t        k(  S c c}}w )N	   F)r   r  r%   SPECIAL_SLOW_RETRY_ACCOUNTS)accountsr   rK   namess       r-   should_retry_term_in_slow_moder    sB    
8}2:;wtQZ		";E;/// <s   A c                `    |dk  ry	 |j                  |       }||z  |k(  S # t        $ r Y yw xY w)Nr
  TF)index
ValueError)r   worker_countworker_index	all_termsposs        r-   term_assigned_to_workerr    sF    qood# ,<//  s   ! 	--c                v   g }g }t        | |       t        j                  d       t        | d       t	               }t        d      D ]  }t        | |      }|s ||fS |j                  d      xs g D cg c]  }| }}|j                  d      xs g D ]`  }|j                  d      xs g }	dj                  |	dd	       }
|
|v r1|j                  |
       |j                  |	D cg c]  }| c}       b t        | |      }|s ||fS t        j                  d
       t        | d        ||fS c c}w c c}w )zSExtract all rows from a DataTables table by snapshotting each page and paging Next.r   r   i  r   r   r   |NrY   rA  )rC  r   r   r   rE  r  r   rp   joinrF  r(  rG  )r   r   r   r   rH  rK   r  r  r>  r   sigrx   rK  s                r-   extract_all_datatable_rowsr    sD   GD VX.JJsO$5D3Z ) 2" D=!  $xx	28b:1::((6"(b 	,AEE'N(bE((5!9%Cd{HHSMKKE*q*+	, 'vx8 D= 	

3VR(%)( D=! ; +s   4	D1%	D6c                V   	 t        | t        j                  t        d       d}	 | j                  |t              }|syt        |j                  d      xs dj                         |j                  d      xs dj                               S # t        $ r Y yw xY w# t        $ r Y yw xY w)zGIf the first program row is active (checkbox checked), return its link.r   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%   rE   r&   r   )
r   r   r   rT  r   r   r#   rp   rN   r^   )r   r   rO  s      r-   get_first_active_program_linkr  8  s    VRUU$4b9F##F,<=cggfo3::<CGGFODYWYC`C`Cbcc'  (  s(    B B AB 	BB	B('B(c                x    | j                  dd       t        |      }| | dz  }|j                  |d       |S )NTr   z.mdrn  ro  )r   rV   
write_text)output_foldermd_namecontent	base_namemd_paths        r-   write_markdownr  T  sH    t4"7+I3//Gw1Nr,   c                    t        j                         } | j                  ddd       | j                  dt        dd       | j                  d	t        d
d       | j	                         }|j
                  dk  r| j                  d       |j                  d
k  s|j                  |j
                  k\  r| j                  d       t        t              j                         j                  }|dz  }t        |      }|d   }|d   }t        |j                        }	 t        |       t!        t"        d      }t%        |      D 	
cg c]$  \  }	}
|	|j
                  z  |j                  k(  s#|
& }}	}
t'        d|d
    d|d    dt)        |       d       t'        d|j                  dz    d|j
                   dt)        |       d       t'        dt)        |       dt)        |       d       |rt'        d|d
           nt'        d        |D ]  }t+        |      }||d   z  }||d   z  }||v r$t'        d!       t'        d"|        t'        d#       d$}	 t-        ||       |j2                  }t9        |      }t;        |      rJt'        d&       d'}	 t-        ||d(d)d*+       |j2                  }t5        j6                  d,       t9        |      }|s.t'        d-| d.       |j=                  |       t?        |||       t'        d/| d0t)        |              |rt'        d1       |rd2nd%}|rd2nd3}|rd4nd5}|rd6nd7}t%        |d8      D ]O  \  }\  }}tA        ||jB                        }||v stE        ||      r9|j=                  |       t'        d9| dt)        |       d:|jF                          itI        ||      }|s(t'        d9| dt)        |       d;|jF                          	 t'        d9| dt)        |       d<|jF                          |j1                  d=      xs( |j1                  d>      xs |j1                  d?      xs d@}|j1                  |       t5        j6                  |       |jK                  tL        jN                  dA      jF                  }tQ        |      }tS        |      }|xs |j1                  dB      xs |jF                  }|j1                  dC      xs |}|j1                  dD      xs d@} |r| dE| n| }!| r|! dE|  }!|tU        |!dFG      z  }"|"jW                  d'd'H       tY        |d'I      }#|#D $cg c]  }$t[        |$j1                  dJ            s|$! }%}$t'        dKt)        |%              |%s;g }&|&j]                  dL|        |r|&j]                  dM|        | r|&j]                  dN|         |&j]                  dO|        |&j]                  dP|        |&j]                  dQ       |j_                         D ]  \  }'}(|&j]                  dR|' dS|(         |&j]                  dT       |&j]                  dU       |&j]                  |       |&j]                  dV       dWja                  |&      dWz   })tc        |"dX|)       |j=                  |       t?        |||       	 te        |       y|%D ]  }$|$j1                  dY      xs d@jg                         xs dZ}*|$j1                  d[      xs d@jg                         }+tA        ||+      },t'        d\|*        |j1                  |,       t5        j6                  |       ti        |      }-|-s*|jK                  tL        jN                  dA      jF                  }-g }.g }/tk        |d]d^g      }0|0rtm        ||0      \  }.}/to        |.|/      \  }.}/n<tk        |d]g      xs tk        |d_g      }0|0rtm        ||0      \  }.}/to        |.|/      \  }.}/g }&|&j]                  dL|*        |&j]                  d`|        |r|&j]                  da|        | r|&j]                  dN|         |&j]                  dO|        |&j]                  db|,        |&j]                  dP|        |&j]                  dc       |&j]                  dU       |&j]                  |-       |&j]                  dV       |&j]                  ddt)        |/       de       |.r |/r|&j]                  df       |&j]                  dgja                  dh |.D                     |/D ]  }1|1D 2cg c](  }2|2xs d@jq                  dWdi      jq                  dgdj      * }3}2t)        |3      t)        |.      k  r|3d@gt)        |.      t)        |3      z
  z  z  }3t)        |3      t)        |.      kD  r|3d t)        |.       }3|&j]                  dgja                  |3              |&j]                  dV       n|&j]                  dk       dWja                  |&      dWz   })tc        |"|*|)       |j1                  |       t5        j6                  |        |j=                  |       t?        |||       |j1                  |       t5        j6                  |       te        |       R |j=                  |       t?        |||        t'        dl       	 |js                          y
c c}
}	w # t.        $ rQ 	 |j1                  |j2                         t5        j6                  d%       n# t.        $ r Y nw xY wt-        ||       Y w xY w# t.        $ rK 	 |j1                  |       t5        j6                  d,       n# t.        $ r Y nw xY wt-        ||d(d)d*+       Y w xY wc c}$w c c}2w # te        |       w xY w# |js                          w xY w)mNr   
store_truezRun Firefox headless)actionhelpz--worker-countr
  z)Total number of parallel workers/browsers)typedefaultr  z--worker-indexr   zThis worker index (0-based)z--worker-count must be >= 1z5--worker-index must be between 0 and --worker-count-1zvoucher_scanned_accounts.jsonrl  rm  )r   rc  zSearch terms: z .. r	  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.zQ
================================================================================zSEARCH TERM: zP================================================================================Fgffffff?zTDetected known 9-account partial-load pattern. Retrying same term with slower waits.Tg      @g333333?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_2rE   bodyr%  r&  r$  rK   rR  )rU   r   )rU  rS  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```rH   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  @   K   | ]  }|j                  d d        yw)r  ;N)rM   )r   r  s     r-   r   zmain.<locals>.<genexpr>)  s     1_!!))C2E1_s   rG   r   z._No product table found (or it had no rows)._
z
All terms completed.):argparseArgumentParserr   r  
parse_argsr  errorr  r   r   r   r   r  r   r   r   ri  r   r  r   r   r  r   r^   rp   current_urlr   r   rL  r  rF  r  r   r&   r  r%   r  r   r   TAG_NAMEr3  r7  rV   r   rV  r   r(  r)  r  r  r  rN   rY  r]  r  r?  rM   quit)4apargsr  r}  scrape_statescanned_accountsrm  r   rf  r  rh  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programsr`   active_programsr,  r   r2  
md_contentprogram_nameprogram_hrefprogram_urlprogram_summary_textproduct_headersproduct_rowsprod_table_idr>  rx   cleaneds4                                                       r-   mainr&  ^  s   		 	 	"BOOL<ROSOO$3@kOlOO$3@]O^==?D1
./1 1 1T5F5F F
HIH~%%'..H;;J$Z0L#$:;"#45O4==1FZf%k48&/&6gda1t?P?P;PUYUfUf:fgguQxjU2YKyUANOd''!+,Ad.?.?-@< !!13	
 	)#o*>)??QRUVfRgQhhz{|),q/):;<56  D	MD5jAM.D EE}->??O&/"M$()(O"N3$VT2 !' 2 24V<H-h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(!0L F6,,lG%&79J9JK"226HU`6a$((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p7;;z;RpV]VaVabiVjpnpHJJ{+JJ12(.(;(;BKK(P(U(U%#FGX#YL B6 JI#,#e0@0@0P#eT`TeTeL%1%5%56F%G%S8N%1%5%56F%G%M2NIWl^1^4D#E`l_mL%*6q8H'I!),>|UX,Y!YJ$$TD$A8TRH2:&TQd155?>Sq&TO&T/O0D/EFG*+-r,%89)!LL+>~>N)OP)!LL+A.AQ)RS':;-%HI':4&%AB%LM$+MMO <DAq!LL4s$qc):;< %=>Y/%67U+%)YYu%5%<
&z3DjQ(,,[9)*6FX P **5M - >9()f(;'B'B'D'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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*e^_AG+<+<T3+G+O+OPSUX+Y*e*e#&w<#o2F#F$+ts?7KcRYl7Z/[$[G#&w<#o2F#F.56LO8L.MG %SXXg-> ?@ "LL/!LL)Z[%)YYu%5%<
&z<L

;/

#78}>9@ %((5%j2BOTJJ01JJ01)*5MF6P %j*:OLID	ML 	&' 	m h4  3JJv112JJsO  $VT23& ! s

#45

3$ (s]`nqrrsz 'UZ +f, **5 	s  )n3 $k*k.C+n3 k%&/n3 m&En3 .En#n,n0E	n#:n3 I0n#7-n$D/n#:n3 n3 %	l?/0l l? 	l,)l?+l,,l?;n3 >l??n3 	n&m32n3	m?<n>m??nn3 nn3 
n##n00n3 3o__main__)   )rT   r$   rU   r  r   r$   )r_   r$   r   r   )r   Optional[str])r   r)  r   r)  )r   r)  r   r   )r   None)r   r   r   zwebdriver.Firefox)r   )r   r  )r   r  r   r*  )r   r$   r   zDict[str, object])
r   r$   r   floatr   r+  r   r+  r   r*  )r   zTuple[LinkCell, Dict[str, str]])r*  r$   r   zDict[str, str])r   r$   )r   	List[str]r   zList[List[str]]r   !Tuple[List[str], List[List[str]]])r   %List[Tuple[LinkCell, Dict[str, str]]])r   r$   r   List[Dict[str, object]])F)rU  r   r   r/  )r[  zSequence[str]r   r)  )r   r$   r   r*  )r   r$   r   r   )r   rc  )r-  r$   r   r$   r   r,  )r}  r   r   Dict[str, set])r  r$   r   r0  )r  rE  rm  rE  r   zDict[str, List[str]])r}  r   r  rE  rm  rE  r   r*  )r}  r   r  r$   r   r   )r  r   r  r$   r   r   )i`T  )r  r   r  r$   r  r  r   Optional[Path])r  r1  r   r*  )rT   r$   r   r$   )r  r.  r   r   )
r   r$   r  r  r  r  r  r,  r   r   )r   r$   r   r-  )r   zOptional[LinkCell])r  r   r  r$   r  r$   r   r   )r   r  )h__doc__
__future__r   r  r  rz  ri   rO   rs   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   rT  ONLY_ACTIVE_PROGRAMSr  r#   rS   rV   rb   rf   rk   r{   r   r   r   r   r   r   r   r   r   r   r   r  r3  r7  r?  rL  rP  rV  rY  r]  rC  rG  rD  ri  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r&  r'   
SystemExitr+   r,   r-   <module>rD     s&  : #    	 	   "  8 8    7 + / @ 6 6 @ 7 U	  ! & $   
  $  
 8
$5p9x*$	!F]Y3"6H !!5
5 	5
 5 5 
5p5
cD;/~!<(V*%PB2($'T$&	
.)6:00D8l^ z
TV
 y(  Es   F F! F!