
    bi(                    \   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
n# e$ r	 dZ
ddlZY nw xY w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Z3dZ4h dZ5 ed           G d d                      Z6h dZ7ddd%Z8dd(Z9dd)Z:dd*Z;dd,Z<dd.Z=dd0Z>dd2Z?dd5Z@ddd8ZAddd9ZBddd:ZCdd=ZDdd>ZEd?d@dAdBddHZFddIZGddKZHddNZIddOZJddUZKddWZLddYZMddd\ZNdd]ZOdd`ZPddaZQddbZRddcZSdddgZTddkZUddmZVddrZWddsZXddtZYdduZZddvZ[ddxZ\ddzZ]ddd~Z^ddZ_ddZ`ddZaddZbddZcddZdddZeddZfegdk    r eh ef                      dS )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>	   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<   dS )LinkCellstrtexthrefN)__name__
__module____qualname____annotations__     voucher_scraper_core.pyr$   r$   _   s"         IIIIIIIIr-   r$   >   AUXCONNULPRNCOM1COM2COM3COM4COM5COM6COM7COM8COM9LPT1LPT2LPT3LPT4LPT5LPT6LPT7LPT8LPT9   sr%   max_lenintreturnc                   | pd                     dd                               dd                               dd                                          } t          j        dd|           } t          j        dd|           } t          j        d	d|           } |                     d
          } | sd} |                                 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)rF   rG   s     r.   _safe_fs_componentrZ   l   s     
b$$$,,T377??cJJPPRRA
vsAA 	Q''A 	~r1%%A 	
A wwyy%%%GGGXgX;r-   pathboolc                (   	 t          |           }|                                r|                                sdS |                    d          5 }|                    d          dk    cddd           S # 1 swxY w Y   dS # t
          $ r Y dS 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)r[   pfs      r.   _is_elf_executablerg      s    JJxxzz 	 	5VVD\\ 	+Q66!99
*	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+   uus:   7B B A6)B 6A::B =A:>B 
BBc                    	 t          |           }|                                o0|                                o|j                                        dk    S # t
          $ r Y dS w xY w)Nz.exeF)r   r`   ra   suffixlowerrd   )r[   re   s     r.   _is_windows_executablerk      se    JJxxzzHaiikkHahnn.>.>&.HH   uus   AA 
A$#A$c                ^    t           j        dk    rt          |           S t          |           S )Nnt)osnamerk   rg   )r[   s    r.   _is_usable_executablerp      s)    	w$%d+++d###r-   Optional[str]c                    t           j                            d          } | rt          t	          |                                                     } t          |           r| S t          t	          |                               d                    }t          |          r|S t           j	        dk    rFddt          j        d          t          j        d          g}|D ]}|rt          |          r|c S dS d	d
g}|D ]}t          |          r|c S t          j        d          t          j        d          t          j        d          g}|D ]s}|rt          |          r|c S |rZt	          |          j	        dk    rBt          t	          |                              d                    }t          |          r|c S tdS )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-binrm   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)rn   environgetr%   r   
expanduserrp   	with_namerg   ro   shutilwhichrk   )env_binsibwin_candidatescsnap_candidatespath_candidatess         r.   _locate_firefox_binaryr      s    jnn-..G d7mm..0011 )) 	N$w--))-8899c"" 	J	w$;AL''L##	
   	 	A +A.. t 	<7O   a   	HHH	
 	]##Y]##O
    	#A&& 	HHH 	a**d1gg''6677C!#&& 


4r-   firefox_binc                   t           j                            d          }|r?t          t	          |                                                    }t          |          r|S t           j        dk    r| r@t          t	          |                               d                    }t          |          r|S t          j        d          t          j        d          g}|D ]}|rt          |          r|c S t          t	          t                                                    j        dz            ddg}|D ]}t          |          r|c S dS d}t          |          r|S | r@t          t	          |                               d                    }t          |          r|S t          j        d          }|rt          |          r|S g d	}|D ]}t          |          r|c S dS )
z<Locate geckodriver with sane preferences for Pi/snap setups.GECKODRIVER_PATHrm   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)rn   ru   rv   r%   r   rw   rp   ro   rx   rk   ry   rz   __file__resolveparentrg   )r   	env_geckor|   r   r~   common
snap_geckogeckos           r.   _locate_geckodriverr      s2    
122I Y224455	 ++ 		w$ 	d;''112CDDEEC%c** 
L*++L''
 ! 	 	A +A.. X&&((/2CCDD'/

  	 	A%a(( t EJ*%%   ${##--m<<==c"" 	J L''E #E**   F
   a   	HHH	 4r-   r   c                   t          |           }t          t          t                                                    j        dz            }|rEt          d|            	 t          ||          S # t          $ r t          ||          cY S w xY wt          d           	 t          |          S # 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>>))++25FFGGH E4U44555	E5XFFFF 	E 	E 	E58DDDDDD	E 

Z[[[*(++++ * * *))))))*s$    A1 1BB"B2 2CCNonec                 r   	 t          t                                                    j        } | dz  }|                    dd           t
          j                            dt          |                     t
          j                            dt          | dz                       dS # t          $ r Y dS 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   mkdirrn   ru   
setdefaultr%   rd   )base	cache_dirs     r.   _configure_selenium_runtime_envr   )  s    H~~%%''.,,	t444

os9~~>>>

.D8O0D0DEEEEE   s   B$B( (
B65B6headlesswebdriver.Firefoxc                   t                       t                      }| r|                    d           t                      }|rt	          d|            ||_        nt	          d           |                    dd           |                    dd           	 t          j        t          |          |          }n# t          $ r} t          j        d	k    rkt	          d
           t	          d|rdnd            t	          d|pd            t	          dt          |          rdnd            t	          d            w xY w	 |                    dd           n# t          $ r Y nw xY w|S )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optionsrm   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   rd   rn   ro   r   set_window_size)r   optsr   drivers       r.   build_driverr   5  s   #%%%99D (,''' )**K x===>>>*vwww 	6>>>,e444
"+?+L+LVZ[[[   7d??1222R;2P%%DRRSSSLk.J]LLMMM K1+>>HUUDK K L L LhiiitS))))   Ms   #B> >BE	E   
E-,E-   timeoutc                p    t          | |                              t          j        ||f                    S N)r   untilECpresence_of_element_locatedr   byvaluer   s       r.   wait_presentr   X  s1    ))//0NPRTY{0[0[\\\r-   c                p    t          | |                              t          j        ||f                    S r   )r   r   r   element_to_be_clickabler   s       r.   wait_clickabler   \  s0    ))//0JBPU;0W0WXXXr-   c                ^   t          j                     |z   }t          j                     |k     r	 |                     t          j        d          }|sdS t	          d |D                       sdS n# t
          $ r Y dS w xY wt          j        d           t          j                     |k     }dS dS )z@Best-effort wait for DataTables processing overlay to disappear.zdiv.dataTables_processingNc              3  >   K   | ]}|                                 V  d S r   )is_displayed.0re   s     r.   	<genexpr>z'datatables_wait_idle.<locals>.<genexpr>h  s,      77Aq~~''777777r-   g?)timefind_elementsr   CSS_SELECTORanyrd   sleep)r   r   endprocss       r.   datatables_wait_idler   `  s    
)++
C
)++

	((:UVVE 7777777  	 	 	FF	
3 )++





s   "A/ A/ /
A=<A=table_idDict[str, object]c                2    d}|                      ||          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   r   scripts      r.   js_table_snapshotr   o  s!    F   222r-   c                   |                      t                     t          | t          j        dd          }t          | t          j        dd          }	 |                     d|           n# t          $ r Y nw xY w	 |                                 n# t          $ r Y nw xY w|                    t                     	 |                                 n# t          $ r Y nw xY w|                    t                     	 |                     t          j        d                                           n/# t          $ r" |                    t          j                   Y nw xY wt          | t          j        t          d           d S )Nusernamer   passwordz.arguments[0].scrollIntoView({block:'center'});Login<   )rv   	LOGIN_URLr   r   IDr   rd   clear	send_keysUSERNAMEPASSWORDfind_elementclickr   ENTERACC_SEARCHBOX_ID)r   user_elpass_els      r.   loginr     s   
JJy 625*b99G625*b99GNPWXXXX      h   h&BE7++113333 & & &$*%%%%%&  0"55555sH   A+ +
A87A8<B 
BB<C 
CC<2D/ /)EE      @      ?      ?pre_enter_sleeppost_enter_sleepfinal_sleeptermr   floatr   r   c                  |pd                                                                 }t          d| d           t          | t          j        t          d          }|                                 	 |                                 na# t          $ rT 	 |
                    t          j        d           |
                    t          j                   n# t          $ r Y nw xY wY nw xY w|
                    |           t          j        |           	 |                                 n# t          $ r Y nw xY w	 |
                    t          j                   n# t          $ r Y nw xY w	 |
                    t          j                   n# t          $ r Y nw xY w	 t%          |           
                    t          j                                                   n# t          $ r Y nw xY w	 |
                    t          j                   n# t          $ r Y nw xY wt          j        |           t+          | d           t          j        |           dS )z6Type `term` into accSearchBox, wait, then press Enter.rK   zFiltering accounts: typing 'z' into accSearchBox...r   a   N)rT   rj   r   r   r   r   r   r   r   rd   r   r   CONTROL	BACKSPACEr   r   r   RETURNr   performTABr   )r   r   r   r   r   boxs         r.   filter_accounts_termr     s|    JB%%''D	
E
E
E
EFFF
(8"
=
=CIIKKK		   	MM$,,,,MM$.)))) 	 	 	D	 MM$J		   dj!!!!   dk""""   V&&tz22::<<<<   dh    	J   $$$J{s   2B 
C%?CC%
CC%CC%$C%D' '
D43D48E 
E%$E%)F	 	
FF>G 
G&%G&*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44r-   Tuple[LinkCell, Dict[str, str]]c                <   t          | t          j        t          d           dd}t	          | d                              |           t          | t                    }|r|                    d          st          d          d |                    d          pg D             }|d         d	         }|                    d
          }|r|                    d          st          d          |                    d          pg }i }t          |          D ](\  }}	|t          |          k     r||         ||	pd| <   )t          |                    dd                                          |                    dd                                                    |fS )zHReturn the first account link + row mapping from accounts results table.r   rI   r\   c                    	 |                      t          j        t                    }|                    t          j        d          }t          |          dk    S # t          $ r Y dS w xY w)Nztbody tr td a[href]r   F)r   r   r   ACCOUNTS_TABLE_IDr   r   lenrd   )dtblr   s      r.   _has_first_linkz7get_first_account_from_results.<locals>._has_first_link  sg    	..(9::C!!"/3HIIAq66A: 	 	 	55	s   AA 
A('A(rowsz)Accounts results table snapshot is empty.c                    g | ]}|S r,   r,   r   hs     r.   
<listcomp>z2get_first_account_from_results.<locals>.<listcomp>  s    AAA!AAAr-   headersr   linkr'   z9Could not find a clickable account link in the first row.cellscol_r&   rK   r&   r'   )rI   r\   )r   r   r   r   r   r   r   rv   RuntimeError	enumerater   r$   rT   )
r   r  snapr	  firstr
  r  row_mapir  s
             r.   get_first_account_from_resultsr    s     12666    &"##O444V%677D Htxx'' HFGGGAAdhhy&9&9&?RAAAGLOE99VD Xtxx'' XVWWWyy))/RE G'"" 0 01s5zz>>',QxGAO$&"--3355DHHVR<P<P<V<V<X<XYYY[bbbr-   	page_textDict[str, str]c                6  
 d | pd                                 D             }d |D             }d}t          |          D ]\  }}|dk    sd|v r|dz   } n|dk    ri S h d}g 
t          |t          |                    D ]>}||         |v r n1
                    ||                    t          
          dk    r n?d
fd}i }	 |d          |	d<    |d          |	d<    |d          p
 |d          |	d<   d |	                                D             S )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
    c                6    g | ]}|                                 S r,   )rT   r   lns     r.   r  z7parse_company_information_from_text.<locals>.<listcomp>  s     AAABRXXZZAAAr-   rK   c                6    g | ]}|                                 S r,   )rX   r  s     r.   r  z7parse_company_information_from_text.<locals>.<listcomp>	  s     (((BRXXZZ(((r-   zCOMPANY INFORMATION   >   COMPANY ADDRESSTAX INFORMATIONCONTACT INFORMATIONCREDIT & BILLING INFORMATION#PAYMENT TERMS & INVOICE PREFERENCESACCOUNTSPROGRAMSATTACHMENTSP   labelr%   rI   c                   |                                  }t                    D ]\  }}|                                 }||k    r\t          |dz   t                              D ];}|                                         }|s|                                 dv r6|c c S |                    |dz             r+|t          |           d                                          c S dS )Nr  >   	ACCOUNT #COMPANY NAMEACCOUNT NUMBERPARENT ACCOUNTrM   rK   )rX   r  ranger   rT   
startswith)r'  lab_uidxrawruknxtsections          r.   _value_afterz9parse_company_information_from_text.<locals>._value_after&  s    !'** 	0 	0HCBU{{sQwG55  A!!***,,C ! yy{{&ggg JJJJJ}}US[)) 03u::;;'--/////0rr-   r,  parent_accountr*  company_namer+  r)  account_numberc                    i | ]
\  }}|||S r,   r,   )r   r3  vs      r.   
<dictcomp>z7parse_company_information_from_text.<locals>.<dictcomp>:  s#    ...TQA.Aq...r-   )r'  r%   rI   r%   )
splitlinesr  r-  r   appenditems)r  linesrX   startr  ustop_headersjr6  outr5  s             @r.   #parse_company_information_from_textrF    s    BA9?">">"@"@AAAE((%(((EE%    1%%%)>!)C)CEEE *D {{		 	 	L G5#e**%%  8|##EuQx   w<<2E        C(L)9::C&,~66C(L)9::Wll;>W>WC..SYY[[....r-   c                    	 t          | t          j        dd           |                     t          j        d          }|j        pd                                S # t          $ r Y dS 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
   rK   )r   r   r   r   r&   rT   rd   )r   els     r.   "get_account_name_from_summary_spanrJ  >  sp    VRU$FKKK  (JKK2$$&&&   rrs   AA 
A'&A'r	  	List[str]r  List[List[str]]!Tuple[List[str], List[List[str]]]c                N   	 ddh}g }t           pg           D ]J\  }}|pd                                }|s|                                |v r5|                    |           K|s |fS  fd|D             }g }|pg D ]#	|                    	fd|D                        $||fS )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 numberrK   c                     g | ]
}|         S r,   r,   )r   r  r	  s     r.   r  z/drop_hidden_product_columns.<locals>.<listcomp>c  s    000!71:000r-   c                J    g | ]}|t                    k     r|         nd  S )rK   )r   )r   r  rs     r.   r  z/drop_hidden_product_columns.<locals>.<listcomp>f  s/    HHH!!c!ff**!A$$"HHHr-   )r  rT   rj   r>  )
r	  r  
drop_nameskeep_idxr  r  hhnew_headersnew_rowsrQ  s
   `        @r.   drop_hidden_product_columnsrW  L  s     0JH'-R((  1g2__ 	88::## }0000x000K "HZR J JHHHHxHHHIIII  r-   %List[Tuple[LinkCell, Dict[str, str]]]c                r   t          | t          j        t          d           t	          | t                     t          j        d           t          | d           t          | t                     g }t                      }t          d          D ]}t          | t                    }|r|                    d          s nvd |                    d          pg D             }|                    d          pg D ]}|                    d          pi }|                    d	          pd
                                }|r||v rI|                    |           t          |                    d          pd
                                |          }	|                    d          pg }
i }t!          |          D ](\  }}|t#          |
          k     r|
|         ||pd| <   )|                    |	|f           t'          | t                    }|s n&t          j        d           t          | d           |S )zHReturn ALL account links + row mappings from the accounts results table.r   皙?r   i,  r  c                    g | ]}|S r,   r,   r  s     r.   r  z1get_all_accounts_from_results.<locals>.<listcomp>|  s    EEEAaEEEr-   r	  r
  r'   rK   r&   r  r  r  ffffff?)r   r   r   r   set_datatable_length_allr   r   r   datatable_goto_firstsetr-  r   rv   rT   addr$   r  r   r>  datatable_click_next)r   rE  seenrQ   r  r	  rowr
  r'   	link_cellr  r  r  r  clickeds                  r.   get_all_accounts_from_resultsrf  j  s5    12666 V%6777JsOOO$$$!233313C55D3ZZ ) ) ):;; 	488F++ 	EEE$((9*=*=*CEEE88F##)r 	- 	-C776??(bDHHV$$*1133D 44<<HHTNNN txx'7'7'=2&D&D&F&FTRRRI"www//52E&(G!'** 8 81s5zz>>/4QxGAO,JJ	7+,,,,&v/@AA 	E
4VR((((Jr-   List[Dict[str, object]]c                z    d}	 |                      ||          }t          |p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   listrd   )r   r   r   ress       r.   js_program_list_snapshotrk    sW    F##FH55CI2   			s   &+ ::Fonly_activec                Z   	 t          | t          j        t          d           n# t          $ r g cY S w xY wt          | t                     t          j        d           t          | d           t          | t                     g }t                      }t          d          D ]}t          | t                    }|D ]}|                    d          pd                                }|r||v r2|                    |           t!          |                    d                    }|r|sn|                    |                    d          pd                                ||d	           t%          | t                    }	|	s n&t          j        d
           t          | d           |S )zuReturn program links from the 'List Of Account Related Programs' table.

    Returns dicts: {text, href, active}
    r   rZ  r      r'   rK   activer&   )r&   r'   ro  r\  )r   r   r   PROGRAM_TABLE_IDr   r]  r   r   r   r^  r_  r-  rk  rv   rT   r`  r\   r>  ra  )
r   rl  rE  rb  rQ   r  rQ  r'   ro  re  s
             r.   get_account_program_linksrq    s   
VRU$4b9999   			 V%5666JsOOO$$$!1222#%C55D3ZZ ) )'0@AA 	` 	`AEE&MM'R..00D 44<<HHTNNN!%%//**F 6 JJv!4" ; ; = =tW]^^____&v/?@@ 	E
4VR((((Js   !$ 33c                    d}	 |                      |          }|pd                                S # t          $ r Y dS 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');
    rK   )r   rT   rd   )r   r   txts      r.   extract_program_summary_blockrt    sY    F0##F++	r  """   rrs   */ 
==required_headersSequence[str]c                n    d |D             }d}	 |                      ||          S # t          $ r Y dS w xY w)zfReturn a table id for the first table that contains all required headers (case-insensitive substring).c                Z    g | ](}|                                                                 )S r,   )rT   rj   )r   rQ  s     r.   r  z)find_table_by_headers.<locals>.<listcomp>  s*    <<<a		!!<<<r-   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r   rd   )r   ru  requiredr   s       r.   find_table_by_headersr{    sZ    <<+;<<<HF $$VX666   tts   & 
44c                Z    d}	 |                      ||           dS # t          $ r Y dS 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;
    Nry  r   s      r.   r]  r]    sL    Ffh/////   s    
**c                p    d}	 t          |                     ||                    S # t          $ r Y dS 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   rd   r   s      r.   ra  ra     sN    
FF))&(;;<<<   uus   "' 
55c                    d}	 t          |                     ||                    }|r&t          j        d           t	          | d           dS dS # t
          $ r Y dS 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   rd   )r   r   r   re  s       r.   r^  r^  2  s    F>v,,VX>>?? 	-Jt ,,,,,	- 	-    s   A	A 
AAzzrA  r   c                   | pd                                                                 } |pd                                                                 }d}g }|D ]4}|D ]/}||z   }|| k     r||k    r|c c S |                    |           05|S )zLReturn a list of two-letter search terms from start..end inclusive (aa..zz).r   r  abcdefghijklmnopqrstuvwxyz)rT   rj   r>  )rA  r   letterstermsr   bts          r.   iter_two_letter_termsr  \  s    ]d!!##))++E;$




%
%
'
'C*GE   	 	AAA5yy3wwLLOOOO	 Lr-   
state_pathr   Dict[str, set]c                   t                      t                      d}	 |                                 rt          j        |                     d          pd          }t          d |                    d          pg D                       |d<   t          d |                    d          pg D                       |d<   n# t          $ r Y nw xY w|S )	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  4   K   | ]}t          |          V  d S r   r%   r   xs     r.   r   z$load_scrape_state.<locals>.<genexpr>z  s(      /i/i1A/i/i/i/i/i/ir-   r  c              3  |   K   | ]7}t          |                                                                          V  8d S r   r%   rT   rj   r  s     r.   r   z$load_scrape_state.<locals>.<genexpr>{  s<      *o*oa3q66<<>>+?+?+A+A*o*o*o*o*o*or-   r  )r_  r`   jsonloads	read_textrv   rd   )r  statedatas      r.   load_scrape_stater  n  s     !$55 E 	p:j22G2DDLMMD,//i/iJ`AaAaAgeg/i/i/i,i,iE()'**o*oDHHUfLgLgLmkm*o*o*o'o'oE#$   Ls   B C   
CCr1  c                   i }	 t          j        | pd          }n# t          $ r i }Y nw xY wt          d |                    d          pg D                       t          d |                    d          pg D                       dS )Nr  c              3  4   K   | ]}t          |          V  d S r   r  r  s     r.   r   z/_load_scrape_state_from_text.<locals>.<genexpr>  s(      #]#]qCFF#]#]#]#]#]#]r-   r  c              3  |   K   | ]7}t          |                                                                          V  8d S r   r  r  s     r.   r   z/_load_scrape_state_from_text.<locals>.<genexpr>  s<      cc!s1vv||~~3355ccccccr-   r  r  )r  r  rd   r_  rv   )r1  r  s     r.   _load_scrape_state_from_textr    s    Dz#+&&    !$#]#]TXX>T5U5U5[Y[#]#]#] ] ]ccIZ@[@[@a_accccc  s    **scannedr_  r  Dict[str, List[str]]c                h    t          d | D                       t          d |D                       dS )Nc              3  8   K   | ]}|t          |          V  d S r   r  r  s     r.   r   z!_state_payload.<locals>.<genexpr>  s-      &D&D!!&Ds1vv&D&D&D&D&D&Dr-   c              3     K   | ]9}|t          |                                                                          V  :d S r   r  )r   r  s     r.   r   z!_state_payload.<locals>.<genexpr>  sB      !W!WQUV!W#a&&,,.."6"6"8"8!W!W!W!W!W!Wr-   r  )sorted)r  r  s     r.   _state_payloadr    sE     &&D&Dw&D&D&D D D!!W!W/!W!W!WWW  r-   c                V   t           3t          j        |                                 t           j                   d S |                     d           	 	 t          j        |                                 t
          j        d           d S # t          $ r t          j
        d           Y nw xY wZ)Nr   Tr  g?)fcntlflockfilenoLOCK_EXseekmsvcrtlockingLK_LOCKOSErrorr   r   fhs    r.   _lock_file_exclusiver    s    BIIKK///GGAJJJ	N299;;:::F 	 	 	Jt		s   2B B'&B'c                ,   t           3t          j        |                                 t           j                   d S |                     d           	 t          j        |                                 t
          j        d           d S # t          $ r Y d S w xY w)Nr   r  )	r  r  r  LOCK_UNr  r  r  LK_UNLCKr  r  s    r.   _unlock_filer    s    BIIKK///GGAJJJryy{{FOQ77777   s   2B 
BBc                n   | j                             dd           |                     dd          5 }t          |           	 |                    d           t          |                                          t          |           cd d d            S # t          |           w xY w# 1 swxY w Y   d S )NTr   a+r  r  r   )r   r   rb   r  r  r  rc   r  )r  r  s     r.   _locked_read_scrape_stater    s    D4888		0	0 BR   	GGAJJJ/		::                 s)   B*5B:B*B''B**B.1B.c                f   	 | j                             dd           |                     dd          5 }t          |           	 |                    d           t          |                                          }t          |d                   t          |          z  }t          |d                   t          |          z  }t          ||          }|                    d           |	                                 |
                    t          j        |d	d
                     |                                 t          j        |                                           t#          |           n# t#          |           w xY w	 ddd           dS # 1 swxY w Y   dS # t$          $ r Y dS w xY w)zMPersist resume state, merging with on-disk state (safe for parallel workers).Tr   r  r  r  r   r  r     )indent	sort_keysN)r   r   rb   r  r  r  rc   r_  r  truncatewriter  dumpsflushrn   fsyncr  r  rd   )r  r  r  r  currentmerged_scannedmerged_completedpayloads           r.   save_scrape_stater    s   t<<<__TG_44 	! $$$!


6rwwyyAA!$W-C%D!E!EG!T#&w/@'A#B#BSEYEY#Y (9IJJ


GAFFFGGG


%%%R    R     	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!    sM   3F" FDE3#F3FFF" FF" FF" "
F0/F0account_urlc                X    	 t          |           }||d         v S # t          $ r Y dS w xY w)Nr  F)r  rd   )r  r  r  s      r.   is_account_scannedr    sG    )*55e$:;;;   uus    
))base_dirc                    | dz  }|                     dd           t          j        |                    dd                                                    }|| dz  S )Nz.voucher_account_claimsTr   r  ignore)errorsz.lock)r   hashlibsha1encode	hexdigest)r  r  
claims_dirdigests       r.   _account_claim_pathr    sh    55JTD111\+,,WX,FFGGQQSSF6(((((r-   `T  stale_secondsOptional[Path]c                   t          | |          }t          j                    }t          d          D ]Q}	 t          j        t          |          t          j        t          j        z  t          j        z  d          }t          j	        |dd          5 }|
                    dt          j                     d           |
                    dt          |           d           |
                    d	| d           d
d
d
           n# 1 swxY w Y   |c S # t          $ rR 	 ||                                j        z
  }||k    r|                    d           Y -n# t"          $ r Y nw xY wY  d
S t"          $ r Y  d
S w xY wd
S )zKBest-effort account claim to reduce duplicate work across parallel workers.r  i  wr  r  zpid=rN   ztime=zurl=NT
missing_ok)r  r   r-  rn   rb   r%   O_CREATO_EXCLO_WRONLYfdopenr  getpidrH   FileExistsErrorstatst_mtimeunlinkrd   )	r  r  r  
claim_pathnowrQ   fdr  ages	            r.   try_claim_accountr    s   $X{;;J
)++C1XX  	Z"*ry*@2;*NPUVVB2sW555 1/	///000-S---...////0001 1 1 1 1 1 1 1 1 1 1 1 1 1 1  	 	 	JOO--66&&%%%666H '    444 	 	 	444	4sa   ADA*D?DD	DD	D
F%8E! F!
E.+F-E..F5	FFr  c                ^    | sd S 	 |                      d           d S # t          $ r Y d S w xY w)NTr  )r  rd   )r  s    r.   release_account_claimr    sS     T*****   s    
,,c                z    t          j        dd| pd                                                                          S )NrP   rM   rK   )rU   rV   rT   rj   )rF   s    r.   
_norm_namer    s0    6&#R001177999r-   accountsc                Z    t          |           dk    rdS d | D             }|t          k    S )N	   Fc                <    h | ]\  }}t          |j                  S r,   )r  r&   )r   r
  rQ   s      r.   	<setcomp>z1should_retry_term_in_slow_mode.<locals>.<setcomp>  s&    ;;;wtQZ	"";;;r-   )r   SPECIAL_SLOW_RETRY_ACCOUNTS)r  namess     r.   should_retry_term_in_slow_moder    s7    
8}}u;;(;;;E///r-   worker_countworker_index	all_termsc                t    |dk    rdS 	 |                     |           }n# t          $ r Y dS w xY w||z  |k    S )Nr  TF)index
ValueError)r   r  r  r  poss        r.   term_assigned_to_workerr    s\    qtood##   uu,<//s     
..c                   g }g }t          | |           t          j        d           t          | d           t	                      }t          d          D ]}t          | |          }|s nd |                    d          pg D             }|                    d          pg D ]o}|                    d          pg }d                    |d	d
                   }	|	|v r;|	                    |	           |
                    d |D                        pt          | |          }
|
s n%t          j        d           t          | d           ||fS )zSExtract all rows from a DataTables table by snapshotting each page and paging Next.r   r   i  c                    g | ]}|S r,   r,   r  s     r.   r  z.extract_all_datatable_rows.<locals>.<listcomp>&  s    :::1:::r-   r	  r  r  |Nr_   c                    g | ]}|S r,   r,   r   r~   s     r.   r  z.extract_all_datatable_rows.<locals>.<listcomp>-  s    ***q***r-   rZ  )r]  r   r   r   r_  r-  r   rv   joinr`  r>  ra  )r   r   r	  r  rb  rQ   r  rQ  r  sigre  s              r.   extract_all_datatable_rowsr	    sm   GD VX...JsOOO$$$55D3ZZ ) ) 22 	E::txx	228b:::((6""(b 	, 	,AEE'NN(bE((5!9%%Cd{{HHSMMMKK**E***++++ 'vx88 	E
3VR((((D=r-   Optional[LinkCell]c                   	 t          | t          j        t          d           n# t          $ r Y dS w xY wd}	 |                     |t                    }|sdS t          |                    d          pd                                |                    d          pd                                          S # t          $ r Y dS 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&   rK   r'   r  )
r   r   r   rp  r   r   r$   rv   rT   rd   )r   r   rj  s      r.   get_first_active_program_linkr  9  s    VRU$4b9999   ttF##F,<== 	4cggfoo3::<<CGGFOODYWYC`C`CbCbcccc   tts$   !$ 
22B6 AB6 6
CCoutput_foldermd_namecontentc                    |                      dd           t          |          }| | dz  }|                    |d           |S )NTr   z.mdr  r  )r   rZ   
write_text)r  r  r  	base_namemd_paths        r.   write_markdownr  U  sY    t444"7++I////Gw111Nr-   c            
     d  1 t          j                    } |                     ddd           |                     dt          dd           |                     d	t          d
d           |                                 11j        dk     r|                     d           1j        d
k     s1j        1j        k    r|                     d           t          t                    
                                j        }|t          z  }|dz  }t          |          }|d         }|d         }t          1j                  }	 t!          |           t#          t$          d          }1fdt'          |          D             }	t)          d|d
          d|d          dt+          |           d           t)          d1j        dz    d1j         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/          ||
           n`# t0          $ rS 	 |                    |j                   t7          j        d&           n# t0          $ r Y nw xY wt/          ||
           Y nw xY w|j        }t;          |          }t=          |          rt)          d'           d(}	 t/          ||
d)d*d+,           n_# t0          $ rR 	 |                    |           t7          j        d-           n# t0          $ r Y nw xY wt/          ||
d)d*d+,           Y nw xY w|j        }t7          j        d-           t;          |          }|s;t)          d.|
 d/           |                    |
           tA          |||           t)          d0|
 d1t+          |                      |rt)          d2           |rd3nd&}|rd3nd4}|rd5nd6}|rd7nd8}t'          |d9          D ]N\  }\  }}tC          ||j"                  }||v stG          ||          r@|                    |           t)          d:| dt+          |           d;|j$                    rtK          ||          }|s+t)          d:| dt+          |           d<|j$                    	 t)          d:| dt+          |           d=|j$                    |                    d>          p+|                    d?          p|                    d@          pdA}|                    |           t7          j        |           |&                    tN          j(        dB          j$        }tS          |          }tU          |          }|p|                    dC          p|j$        }|                    dD          p|}|                    dE          pdA}|r| dF| n| }|r| dF| }|tW          |dGH          z  } | ,                    d(d(I           t[          |d(J          }!dK |!D             }"t)          dLt+          |"                      |"s}g }#|#.                    dM|            |r|#.                    dN|            |r|#.                    dO|            |#.                    dP|            |#.                    dQ|
            |#.                    dR           |/                                D ] \  }$}%|#.                    dS|$ dT|%            !|#.                    dU           |#.                    dV           |#.                    |           |#.                    dW           dX0                    |#          dXz   }&tc          | dY|&           |                    |           tA          |||           	 te          |           |"D ]	}'|'                    dZ          pdA3                                pd[}(|'                    d\          pdA3                                })tC          ||)          }*t)          d]|(            |                    |*           t7          j        |           ti          |          }+|+s%|&                    tN          j(        dB          j$        }+g },g }-tk          |d^d_g          }.|.r'tm          ||.          \  },}-to          |,|-          \  },}-nJtk          |d^g          ptk          |d`g          }.|.r&tm          ||.          \  },}-to          |,|-          \  },}-g }#|#.                    dM|(            |#.                    da|            |r|#.                    db|            |r|#.                    dO|            |#.                    dP|            |#.                    dc|*            |#.                    dQ|
            |#.                    dd           |#.                    dV           |#.                    |+           |#.                    dW           |#.                    det+          |-           df           |,r|-r|#.                    dg           |#.                    dh0                    di |,D                                  |-D ]}/dj |/D             }0t+          |0          t+          |,          k     r&|0dAgt+          |,          t+          |0          z
  z  z  }0t+          |0          t+          |,          k    r|0d t+          |,                   }0|#.                    dh0                    |0                     |#.                    dW           n|#.                    dk           dX0                    |#          dXz   }&tc          | |(|&           |                    |           t7          j        |           |                    |           tA          |||           |                    |           t7          j        |           te          |           <# te          |           w xY w|                    |
           tA          |||           
t)          dl           	 |8                                 d
S # |8                                 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.jsonr  r  )r   r  c                @    g | ]\  }}|j         z  j        k    |S r,   )r  r  )r   r  r  argss      r.   r  zmain.<locals>.<listcomp>v  s3    gggda1t?P;PUYUf:f:f:f:f:fr-   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?)rA  [z] SKIP (already scanned): z$] SKIP (claimed by another worker): z] Account: z	Account #zAccount#col_2rK   bodyr8  r9  r7  rQ   rn  )rG   r   )rl  c                V    g | ]&}t          |                    d                     $|'S )ro  )r\   rv   r   s     r.   r  zmain.<locals>.<listcomp>  s/    &T&T&TQd155??>S>S&Tq&T&T&Tr-   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```rN   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  B   K   | ]}|                     d d          V  dS )r&  ;NrS   r  s     r.   r   zmain.<locals>.<genexpr>+  s0      1_1_!!))C2E2E1_1_1_1_1_1_r-   c                f    g | ].}|pd                      dd                               dd          /S )rK   rN   rM   r&  r(  r)  r  s     r.   r  zmain.<locals>.<listcomp>-  s?    *e*e*e^_AG+<+<T3+G+G+O+OPSUX+Y+Y*e*e*er-   z._No product table found (or it had no rows)._
z
All terms completed.)9argparseArgumentParserr   rH   
parse_argsr  errorr  r   r   r   r   OUTPUT_ROOT_DIRNAMEr  r   r   r   r  r   r  r   r   r  r   rd   rv   current_urlr   r   rf  r  r`  r  r   r'   r  r&   r  r   r   TAG_NAMErF  rJ  rZ   r   rq  r>  r?  r  r  r  rT   rt  r{  r	  rW  quit)2apr  vouchers_rootr  scrape_statescanned_accountsr  r   r  worker_termsr   current_stateterm_slow_modeaccounts_list_urlr  account_open_sleepprogram_open_sleepaccount_return_sleeplist_return_sleepr0  account_linkr  r  r  acct_numaccount_page_textcompany_info	span_namer8  r9  r7  folder_label
out_folderprogramsactive_programsr@  r3  r;  
md_contentre   program_nameprogram_hrefprogram_urlprogram_summary_textproduct_headersproduct_rowsprod_table_idrQ  cleanedr  s2                                                    @r.   mainrQ  _  s   		 	"	"BOOL<ROSSSOO$3@kOlllOO$3@]O^^^==??D1
.///1 1T5F F F
HIIIH~~%%''.H22M;;J$Z00L#$:;"#45O4=111FZf%k488ggggi&6&6gggNuQxNNU2YNNUNNNOOO3d'!+ 3 3d.? 3 3<  3 3 3	
 	
 	
 	{#o*>*>{{RUVfRgRg{{{||| 	7;,q/;;<<<<5666  D	M D	MD5jAAM.D EE}->??O&&/"""($(()))(OOO"N3$VT2222 3 3 3JJv1222JsOOOO    D$VT222223 !' 24V<<H-h77 A<   "&s(s]`nqrrrrr  s s s

#4555
3$   (s]`nqrrrrrrs %+$6!
38@@ @@@@AAA##D)))!*.>PPPAAA#h--AABBB 97888(6!?C(6!?C*8#A33c '5 >309(!0L0L0L F6 F6,,lG%&79JKK"2226HU`6a6a2$((555`c``CMM``\M^``aaa.xEE
! jcjjCMMjjWcWhjjkkkz6QcQQCMMQQl>OQQRRR&{{;77p7;;z;R;RpV]VaVabiVjVjpnpHJJ{+++J1222(.(;(;BK(P(P(U%#FGX#Y#YL B6 J JI#,#e0@0@0P0P#eT`TeL%1%5%56F%G%G%S8N%1%5%56F%G%G%M2NIW#nl#E#E^#E#E#E`l]nL% J*6'I'I'I'I!.1CLZ]1^1^1^!^J$$TD$AAA8TRRRH&T&T(&T&T&TOFO0D0DFFGGG* !+-%8,%8%8999) Q!LL)O~)O)OPPP) T!LL)R.)R)RSSS%H;%H%HIII%A4%A%ABBB%LMMM$+MMOO < <DAq!LL):):):q):):;;;;%=>>>Y///%6777U+++%)YYu%5%5%<
&z3DjQQQ(,,[999)*6FXXX P **5555M - >9 >9()f(;'B'B'D'D'Q	()f(;'B'B'D'D&-k<&H&H5|55666

;///
#5666/LV/T/T,3 a393F3Fr{TZ3[3[3`0578:(=fwP^F_(`(`( {<VW]_l<m<m9O\<WXgiu<v<v9O\\,A&7),T,T  -CXmnt  xA  wB  YC  YCM, {@Z[acp@q@q =@[\kmy@z@z = "%8,%8%8999%Ll%L%LMMM) O!LL)M^)M)MNNN) T!LL)R.)R)RSSS%H;%H%HIII%H;%H%HIII%A4%A%ABBB%;<<<Y///%9:::U+++%a\IZIZ%a%a%abbb* \| \!LL222!LL1_1_1_1_1_)_)_```%1 @ @*e*ecd*e*e*e#&w<<#o2F2F#F#F$+ts?7K7KcRYll7Z/[$[G#&w<<#o2F2F#F#F.56LO8L8L6L.MG %SXXg->-> ? ? ? ?!LL////!LL)Z[[[%)YYu%5%5%<
&z<LLL

;///
#78888$((555%j2BOTTTJJ0111J0111)*5555)*5555%%%j*:OLLLL&''' 	s   <Et J-,t -
L
8.K'&L
'
K41L
3K44L
t 	L

9t Mt 
N5$)NN5
NN5NN52t 4N55E5t +Lr79t 
Qr7&t 7s:t t/__main__)rE   )rF   r%   rG   rH   rI   r%   )r[   r%   rI   r\   )rI   rq   )r   rq   rI   rq   )r   rq   rI   r   )rI   r   )r   r\   rI   r   )r   )r   rH   )r   rH   rI   r   )r   r%   rI   r   )
r   r%   r   r   r   r   r   r   rI   r   )rI   r   )r  r%   rI   r  )rI   r%   )r	  rK  r  rL  rI   rM  )rI   rX  )r   r%   rI   rg  )F)rl  r\   rI   rg  )ru  rv  rI   rq   )r   r%   rI   r   )r   r%   rI   r\   )r   r  )rA  r%   r   r%   rI   rK  )r  r   rI   r  )r1  r%   rI   r  )r  r_  r  r_  rI   r  )r  r   r  r_  r  r_  rI   r   )r  r   r  r%   rI   r\   )r  r   r  r%   rI   r   )r  )r  r   r  r%   r  rH   rI   r  )r  r  rI   r   )rF   r%   rI   r%   )r  rX  rI   r\   )
r   r%   r  rH   r  rH   r  rK  rI   r\   )r   r%   rI   rM  )rI   r
  )r  r   r  r%   r  r%   rI   r   )rI   rH   )i__doc__
__future__r   r+  r  r  rn   rU   ry   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   rp  r/  ONLY_ACTIVE_PROGRAMSr  r$   rY   rZ   rg   rk   rp   r   r   r   r   r   r   r   r   r   r   r   r   r  rF  rJ  rW  rf  rk  rq  rt  r{  r]  ra  r^  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r	  r  r  rQ  r(   
SystemExitr,   r-   r.   <module>re     sp   : # " " " " "    				 				  LLLL   EMMMMM " ! ! ! ! !       8 8 8 8 8 8 8 8 8 8 8 8 8 8                   7 7 7 7 7 7 + + + + + + / / / / / / @ @ @ @ @ @ 6 6 6 6 6 6 6 6 6 6 6 6 @ @ @ @ @ @ 7 7 7 7 7 7 U	  ! & $    
 
 
  $       
       8
 
 
 
   $ $ $ $5 5 5 5p9 9 9 9x* * * *$	 	 	 	! ! ! !F] ] ] ] ]Y Y Y Y Y    3 3 3 3"6 6 6 6H !!5 5 5 5 5 5p5 5 5 5
c c c cD;/ ;/ ;/ ;/~   ! ! ! !<( ( ( (V   *% % % % %P   B   2   (   $' ' ' 'T    $   &	 	 	 	   
 
 
 
         .   ) ) ) )    6   : : : :0 0 0 00 0 0 0   D   8   m m m m` z
*TTVV

 s   + 99