
    },j4@                       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ZddlZddlmZ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ZdZdZ eddh      ZdZ dZ! ed       G d d             Z" G d de      Z#d4dZ$d5dZ%d6dZ&d7d8dZ'd9dZ(d:d;dZ)d<dZ*d<d Z+d=d!Z,d>d"Z-d?d#Z.d@d$Z/dAd%Z0dBd&Z1d:dCd'Z2dDd(Z3dDd)Z4dEd*Z5dFd+Z6dGd,Z7d:dHd-Z8dId.Z9dJd/Z:dKd0Z;dLd1Z<d7dMd2Z=e>d3k(  r e? e=             y)NzMWatch Apple's refurbished Mac page and notify ntfy about new target products.    )annotationsN)asdict	dataclass)
HTMLParser)Path)Iterablez*https://www.apple.com/shop/refurbished/macrefurbishedMaci,  z.refurbished_watch_state.jsonzqMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36)z13-inch macbook airz13 inch macbook air   512GB1TB`   @   T)frozenc                  Z    e Zd ZU ded<   ded<   dZded<   dZded<   dZded<   dZded	<   y)
ProductstrkeytitleN
str | Nonepriceurlramstorage)__name__
__module____qualname____annotations__r   r   r   r        M/Users/jameslopez/projects/FindRefurbishedAppleProduct/refurbished_watcher.pyr   r   %   s4    	HJE:CCGZr   r   c                  P     e Zd ZdZd fdZd	dZd
dZddZd fdZddZ	 xZ
S )ListingParserz:Small parser tuned for Apple's refurbished listing markup.c                    t         |   d       || _        g | _        g | _        d| _        g | _        d | _        d | _        d | _	        g | _
        d| _        y )NT)convert_charrefsF)super__init__base_urlproducts_stack_in_heading_heading_parts_current_href_pending_title_pending_url_price_parts_collect_price)selfr'   	__class__s     r    r&   zListingParser.__init__2   s`    $/ ')!# )+)-*.(,')#r   c                   | j                   j                  |       t        |      }|dk(  rG|j                  d      r6t        j
                  j                  | j                  |d   xs d      | _        |dv rd| _	        g | _
        |j                  dd      xs d}d|j                         v sd|j                         v rd| _        g | _        y y )	Nahref    h2h3Tclassr   current_price)r)   appenddictgeturllibparseurljoinr'   r,   r*   r+   lowerr0   r/   )r1   tagattrsattrclassess        r    handle_starttagzListingParser.handle_starttag?   s    3E{#:$((6*!'!5!5dmmT&\EWUW!XD,#D"$D((7B'-2gmmo%GMMO)K"&D "D *Lr   c                   |dv ra| j                   rUt        dj                  | j                              }t	        |      r|| _        | j                  | _        d| _         g | _        | j                  rU|dv rQt        dj                  | j                              }|r| j
                  r| j                  |       d| _        g | _
        |dk(  rd | _        | j                  r| j                  j                          y y )Nr7    F>   pdivspanr4   )r*   
clean_textjoinr+   is_target_titler-   r,   r.   r0   first_pricer/   _append_pendingr)   pop)r1   rC   r   r   s       r    handle_endtagzListingParser.handle_endtagO   s    ,4#3#3sxx(;(;<=Eu%&+#$($6$6!$D"$D3*>#>):): ;<E,,$$U+"'D "D#:!%D;;KKOO r   c                   | j                   r| j                  j                  |       | j                  r| j                  j                  |       | j
                  r-| j                  s t        |      }|r| j                  |       y y y y N)r*   r+   r<   r0   r/   r-   rP   rQ   )r1   datar   s      r    handle_datazListingParser.handle_datae   su    &&t,$$T*t':':%E$$U+  (;r   c                ^    t         |           | j                  r| j                  d        y y rU   )r%   closer-   rQ   )r1   r2   s    r    rY   zListingParser.closeq   s(      & r   c                    | j                   }| j                  }|sy t        |||      t        fd| j                  D              r(| j                  j                  t        |||             d | _         d | _        y )Nc              3  <   K   | ]  }|j                   k7    y wrU   r   ).0productr   s     r    	<genexpr>z0ListingParser._append_pending.<locals>.<genexpr>|   s     ?gw{{c!s   )r   r   r   r   )r-   r.   product_keyallr(   r<   r   )r1   r   r   r   r   s       @r    rQ   zListingParser._append_pendingv   sm    ##%,???MM  SUPS!TU" r   )r'   r   returnNone)rC   r   rD   zlist[tuple[str, str | None]]rb   rc   )rC   r   rb   rc   )rV   r   rb   rc   )rb   rc   )r   r   rb   rc   )r   r   r   __doc__r&   rG   rS   rW   rY   rQ   __classcell__)r2   s   @r    r"   r"   /   s$    D$# ,
,'
	!r   r"   c                    t        j                  |       j                  dd      j                  dd      } t        j                  dd|       j                         S )N   ‑-u   –z\s+rI   )htmlunescapereplaceresubstrip)values    r    rM   rM      sF    MM% ((37??#NE66&#u%++--r   c                    t        j                  dt        |             }|r!|j                  d      j	                  dd      S d S )Nz\$\s?\d[\d,]*(?:\.\d{2})?r   z$ $)rl   searchrM   grouprk   )ro   matchs     r    rP   rP      s:    II2Ju4EFE055;;q>!!$,?4?r   c                    | j                         j                  dd      }t        |       }t        |t	        |       |      S )Nrg   rh   )rB   rk   extract_storageis_target_productextract_ram)r   
normalizedr   s      r    rO   rO      s8    &&x5Je$GZU);WEEr   c                ^   | j                         j                  dd      dvryt        fdt        D              ryt	        |      }dv r&|	|t
        k  ry||j                         t        vryy|ydv r
|t        k\  ryd	v xr# t        j                  d
      d uxr	 |t        k\  S )Nrg   rh   refurbishedFc              3  &   K   | ]  }|v  
 y wrU   r   )r]   patternry   s     r    r_   z$is_target_product.<locals>.<genexpr>   s     
G0FW7j 0Fs   Tzmac miniz
mac studiozmacbook proz.\bapple\s+m5\b|\bm5(?:\s+pro|\s+max)?\s+chip\b)rB   rk   anyLEGACY_TARGET_PATTERNScapacity_gbMAC_MINI_MIN_RAM_GBupperMAC_MINI_STORAGE_OPTIONSMAC_STUDIO_MIN_RAM_GBrl   rr   M5_MACBOOK_PRO_MIN_RAM_GB)r   r   r   ram_gbry   s       @r    rw   rw      s    &&x5JJ&

G0F
GGFZ>V&997==?:R#R~z!f0E&E 	# 	0IIGT\``	0//r   c                x   |rt         j                  j                  |      }t        j                  d|j
                        }|r|j                  d      S |j
                  j                  d      xs t        |       j                         S dj                  d t        |       j                         |fD              S )Nz/product/([^/?#]+)   /|c              3  &   K   | ]	  }|s|  y wrU   r   )r]   parts     r    r_   zproduct_key.<locals>.<genexpr>   s     P%GT4D%Gs   )r?   r@   urlparserl   rr   pathrs   rstriprM   rB   rN   )r   r   r   parsedproduct_matchs        r    r`   r`      s    
&&s+		"7E &&q)){{!!#&C*U*;*A*A*CC88Pj&7&=&=&?%GPPPr   c                @   t         j                  j                  | t        ddd      }t         j                  j	                  ||      5 }|j
                  j                         xs d}|j                         j                  |d      cd d d        S # 1 sw Y   y xY w)	Nz?text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8zen-US,en;q=0.9)z
User-AgentAcceptzAccept-Language)headerstimeoututf-8rk   )errors)	r?   requestRequest
USER_AGENTurlopenr   get_content_charsetreaddecode)r   r   r   responsecharsets        r    
fetch_textr      s    nn$$$W/
 % G 
				9X""668CG}}%%gi%@ 
:	9	9s   ?BBc                    t        | |      }|r|S t        |      }|j                  |        |j                          |j                  S rU   )parse_embedded_tilesr"   feedrY   r(   )	page_htmlr'   r(   parsers       r    parse_listingr      s?    #Ix8H8$F
KK	
LLN??r   c                    g }t        |       D ]7  }t        ||      st        fd|D              s'|j                         9 |S )Nc              3  P   K   | ]  }|j                   j                   k7    y wrU   r\   )r]   existingr^   s     r    r_   z'parse_embedded_tiles.<locals>.<genexpr>   s     Px88<<7;;6xs   #&)iter_tile_objectsproduct_from_tilera   r<   )r   r'   r(   tiler^   s       @r    r   r      sF     H!),#D(3sPxPPOOG$ - Or   c              #     K   t        | d      D ]E  }	 t        j                  |      }t	        |t
              s*|D ]  }t	        |t              s|  G y # t        j                  $ r Y ^w xY ww)Ntiles)extract_json_arrays_for_keyjsonloadsJSONDecodeError
isinstancelistr=   )r   
array_textr   r   s       r    r   r      si     1)WE
	JJz*E eT"dD)J  F ## 		s2   A1AA1A1	A1A.+A1-A..A1c              #     K   d| d}d}	 | j                  ||      }|dk(  ry |t        d| d      z   }t        | |      }|| ||dz     |dz   }n|dz   }Nw)N"z":[r   z":r   )findlenfind_matching_json_end)textr   markerstartmarker_indexarray_start	array_ends          r    r   r      s     S\FE
yy/2"S1SE%55*4=	 {Y]33ME!OE s   AAc                   |t        |       k\  s| |   dvry | |   }|dk(  rdnd}d}d}d}t        |t        |             D ]E  }| |   }|r|rd}n|dk(  rd}n|d	k(  rd}|d	k(  rd}'||k(  r|d
z  }2||k(  s8|d
z  }|dk(  sC|c S  y )Nz[{[]}r   F\Tr   r   )r   range)	r   r   openercloserdepth	in_stringescapedindexchars	            r    r   r      s    D	T%[4%[FcMSsFEIGuc$i(E{!	3;IV^QJEV^QJEz# )$ r   c                   t        t        | j                  d      xs d            }i }| j                  d      }t        |t              r$t        |j                  d      t              r|d   }t        |j                  d            }t        |j                  d            }t        ||xs t        |      |      sy | j                  d      }|r)t        j                  j                  |t        |            nd }t        | j                  d      xs d      j                         }	|	xs t        |d |      }
d }| j                  d	      }t        |t              rk|j                  d
      }t        |t              rJ|j                  d      }|j                  d      }|rdt        |      d}n|rt        t        |            }t        |
|||||      S )Nr   r6   filters
dimensionstsMemorySizedimensionCapacityproductDetailsUrl
partNumberr   currentPrice
raw_amountamountrq   z,.2fr   r   r   r   r   r   )rM   r   r>   r   r=   normalize_capacityrw   rx   r?   r@   rA   rn   r`   floatrP   r   )r   r'   r   r   r   r   r   	url_valuer   part_numberr   r   
price_datar;   r   r   s                   r    r   r     s   s488G,234EJhhy!G'4 ZL0I4%P\*

Z^^N;
<C 0C!DEGUC$=;u+=wG,-I<E&,,

xY
84Cdhh|,2399;K

6UD#6CE'"J*d#"~6mT*&**<8J"&&x0FE*-d34#CK0s%u#3PWXXr   c                   | sy t        |       j                         j                         }t        j                  d|t        j
                        }|r2|j                  d       |j                  d      j                          S d S )N(\d+)\s?(GB|TB)flagsr      )r   rn   r   rl   	fullmatch
IGNORECASErs   )ro   r   rt   s      r    r   r   5  sk    u:##%DLL+TGE:?ekk!nekk!n22456ITIr   c                    | sy t        j                  d| j                         t         j                        }|sy t	        |j                  d            }|j                  d      j                         dk(  r|dz  S |S )Nr   r   r   r   TBi   )rl   r   rn   r   intrs   r   )ro   rt   r   s      r    r   r   =  sd    LL+U[[]"--PEQ F!KKN002d:6D=FFr   c           	         | j                   r| j                  r| j                  r| S | j                  s| S 	 t	        t        | j                  |            }t        |      }t        |      }| j                  xs t        |      }t        | j                  | j                   || j                  | j                   xs || j                  xs |      S # t        t        j                  j                  t        f$ r | cY S w xY w)Nr   r   )r   r   r   r   rM   r   OSErrorr?   errorURLErrorTimeoutErrorrx   rv   rP   r   r   r   )r^   r   bodyr   r   r   s         r    enrich_productr   G  s    {{w7==;;*W[['BC d
Cd#GMM.[.EKKmmKKKK3*7  V\\**L9 s    C *C=<C=c                    ddg}|D ]@  }t        j                  || t         j                        }|s,|j                  d       dc S  y )Nz&\b(\d+)\s?GB\s+(?:unified\s+)?memory\bz\b(\d+)\s?GB\s+RAM\br   r   GB)rl   rr   r   rs   )r   patternsr}   rt   s       r    rx   rx   ^  sO    1H 		'4r}}=kk!n%R((  r   c                    t        j                  d| t         j                        }|r|j                  d      j	                         S d S )Nz'\b(256GB|512GB|1TB|2TB|4TB|8TB)\s+SSD\br   r   )rl   rr   r   rs   r   )r   rt   s     r    rv   rv   j  s7    II@$bmm\E%*5;;q>!44r   c                l   | j                         s
t               S | j                  dd      5 }t        j                  |      }d d d        t        t              r"t        d |j                  dg       D              S t        |t              rt        d |D              S t               S # 1 sw Y   gxY w)Nrr   encodingc              3  2   K   | ]  }t        |        y wrU   r   r]   items     r    r_   zload_seen.<locals>.<genexpr>u  s     F)E3t9)E   	seen_keysc              3  2   K   | ]  }t        |        y wrU   r   r   s     r    r_   zload_seen.<locals>.<genexpr>w  s     13t9r   )	existssetopenr   loadr   r=   r>   r   )r   fhpayloads      r    	load_seenr  o  s    ;;=u	3	)R))B- 
*'4 F[")EFFF'4 11115L 
*	)s   B**B3c                   t        j                  dt        j                               t        |D ch c]  }|j                   c}      d}| j                  | j                  dz         }|j                  dd      5 }t        j                  ||d       |j                  d	       d d d        |j                  |        y c c}w # 1 sw Y    xY w)
Nz%Y-%m-%dT%H:%M:%SZ)
updated_atr   z.tmpwr   r   r   )indent
)timestrftimegmtimesortedr   with_suffixsuffixr  r   dumpwriterk   )r   r(   r^   r  tmp_pathr  s         r    	save_seenr  {  s    mm$8$++-HAWW[[ABG f 45H	sW	-		'2a(
 
. T B 
.	-s   C
>*CCc                    | j                  d      dz   t        j                  j                  |j	                  d            z   S )Nr   )r   r?   r@   quotern   )servertopics     r    ntfy_urlr    s3    ==#fll&8&8S9I&JJJr   c           
     L   t        |       j                  d      }t        j                  j	                  t        ||      |ddd| j                  xs t        dd      }t        j                  j                  ||      5 }|j                          d d d        y # 1 sw Y   y xY w)	Nr   POSTzNew refurbished Apple productcomputerztext/plain; charset=utf-8)TitleTagsClickzContent-Type)rV   methodr   r   )
format_productencoder?   r   r   r  r   DEFAULT_URLr   r   )r^   r  r  r   r   r   r   s          r    notify_ntfyr&    s    '"))'2Dnn$$4[[/K7	
	 % 
G 
				9X 
:	9	9s    BB#c                   | j                   g}| j                  r|j                  d| j                          | j                  r|j                  d| j                          | j                  r|j                  d| j                          | j
                  r|j                  | j
                         dj                  |      S )NzRAM: z	Storage: zPrice: r  )r   r   r<   r   r   r   rN   )r^   detailss     r    r#  r#    s    }}oG{{w{{m,-7??"345}}01{{w{{#99Wr   c                   t        | j                        }t        | j                  | j                        }t        || j                        }t        |      }|D cg c]  }|j                  |vs| }}|s2| j                  s&t        ||       t        dt        |       d       g S |D cg c]  }t        || j                         }}| j                  r&|D ]   }t        t        |             t                " nL|D ]G  }t        || j                   | j"                  | j                         t        d|j$                          I t        ||       |st        dt        |       d       |S c c}w c c}w )Nr   zInitialized state with z* matching products. No notifications sent.z
Notified: z"No new matching products. Checked z
 products.)r   
state_filer   r   r   r   r  r   notify_on_first_runr  printr   r   dry_runr#  r&  ntfy_serverr  r   )args
state_pathpager(   seenr^   new_productsenricheds           r    
check_oncer5    sA   doo&Jdhh5DT488,HZ D+3O8w{{$7NG8LO00*h''H6`ab	MYZ\'w=\HZ||G.)*G    G!1!14::t||TJw}}o./   j(#23x=/LMO) P [s   E40E4.E9c                    	 	 t        |        t        j                  | j                         ,# t        $ r(}t        d| t        j                         Y d }~Ld }~ww xY w)NzCheck failed: )file)r5  	Exceptionr,  sysstderrr  sleepinterval)r/  excs     r    run_foreverr>    sS    
	;t 	

4==!   	;N3%(szz::	;s   . 	AAAc           
     t   t        j                  t              }|j                  dt        j
                  j                  dt                     |j                  dt        j
                  j                  dt                     |j                  dt        j
                  j                  dd	             |j                  d
t        j
                  j                  dt                     |j                  dt        t        t        j
                  j                  dt                           |j                  dt        t        t        j
                  j                  dd                   |j                  ddd       |j                  ddd       |j                  ddd       |j                  |       S )N)descriptionz--urlAPPLE_REFURB_URL)defaultz--topic
NTFY_TOPICz--ntfy-serverNTFY_SERVERzhttps://ntfy.shz--state-file
STATE_FILEz
--intervalINTERVAL_SECONDS)typerB  z	--timeoutREQUEST_TIMEOUT30z--once
store_truezRun one check and exit.)actionhelpz	--dry-runz-Print new products instead of notifying ntfy.z--notify-on-first-runzASend notifications for products found before a state file exists.)argparseArgumentParserrd   add_argumentosenvironr>   r%  DEFAULT_TOPICDEFAULT_STATE_FILEr   DEFAULT_INTERVAL_SECONDSr   
parse_args)argvr   s     r    rU  rU    sN   $$9F
8JK)XY
	2::>>,+VW
O`1ab


|M_0`a
3BJJNNK]_w<x8yz
%rzz~~N_ae?f9gh
<UV
L?no
P  
 T""r   c                    t        | xs t        j                  dd        }|j                  rt	        |       yt        |       y)Nr   r   )rU  r9  rV  oncer5  r>  )rV  r/  s     r    mainrY    s<    d*chhqrl+Dyy4  	Dr   __main__)ro   r   rb   r   )ro   r   rb   r   )r   r   rb   boolrU   )r   r   r   r   r   r   rb   r[  )r   r   r   r   r   r   rb   r   )g      >@)r   r   r   r   rb   r   )r   r   r'   r   rb   list[Product])r   r   rb   zIterable[dict])r   r   r   r   rb   zIterable[str])r   r   r   r   rb   
int | None)r   r=   r'   r   rb   zProduct | None)ro   objectrb   r   )ro   r   rb   r]  )r^   r   r   r   rb   r   )r   r   rb   r   )r   r   rb   zset[str])r   r   r(   zIterable[Product]rb   rc   )r  r   r  r   rb   r   )
r^   r   r  r   r  r   r   r   rb   rc   )r^   r   rb   r   )r/  argparse.Namespacerb   r\  )r/  r_  rb   rc   )rV  z	list[str]rb   r_  )rV  zlist[str] | Nonerb   r   )@rd   
__future__r   rM  ri   r   rP  rl   r9  r  urllib.errorr?   urllib.parseurllib.requestdataclassesr   r   html.parserr   pathlibr   typingr   r%  rR  rT  rS  r   r   r   	frozensetr   r   r   r   r"   rM   rP   rO   rw   r`   r   r   r   r   r   r   r   r   r   r   rx   rv   r  r  r  r&  r#  r5  r>  rU  rY  r   
SystemExitr   r   r    <module>rj     sW   S "    	 	 
     ) "   ;  4 H  H  $gu%56    $  P!J P!f.
@
F:QA	$ :YBJG.	5
		K"
8"#$ z
TV
 r   