
    Odj                        S r SSKJr  SSKJrJr  SSKrSrSrSr	Sr
S	rS
rSrSrSrSr " S S\5      r " S S\5      r " S S5      r " S S\5      r " S S\5      rg)u  Клиенты Instagram и Threads: свои посты и их метрики.

Оба API — из семейства Meta Graph, но живут на разных хостах, под разными
токенами и отдают insights в разной форме. Общее — пагинация курсорами,
формат ошибок и политика повторов — вынесено в MetaClient.
    )annotations)CallableIteratorNz!https://graph.instagram.com/v23.0zhttps://graph.threads.net/v1.0)viewsreachlikescommentssavedsharestotal_interactions)followsprofile_visits)r   r   repliesrepostsquotesr   zVid,caption,media_type,media_product_type,permalink,timestamp,like_count,comments_countz4id,text,media_type,permalink,timestamp,is_quote_post         c                      \ rS rSrSrSrg)MetaAPIError!   uM   Запрос к Meta не удался и повторы не помогли. N__name__
__module____qualname____firstlineno____doc____static_attributes__r       /home/rasp/meta-puller/api.pyr   r   !   s    Wr    r   c                      \ rS rSrSrSrg)TokenExpiredError%   uo   Код 190: токен протух. Лечится переавторизацией, а не повтором.r   Nr   r   r    r!   r#   r#   %   s    yr    r#   c                      \ rS rSrSrSrS\S4 SS jjrSS jrSS jr	\
SS j5       rSS	 jrSS
 jr\
SS j5       rSrg)
MetaClient)   u   Общая механика: запрос с повторами, пагинация, чистка токена из ошибок. Nc                    Xl         Ub  UO[        S5      R                  U l        X0l        U=(       d    [
        R                  " 5       U l        g )Ntime)_token
__import__sleep_sleep_max_retriesrequestsSession_session)selftokenr-   max_retriessessions        r!   __init__MetaClient.__init__.   s;    $0ej6H6N6N'58#3#3#5r    c                `    U R                   (       a  UR                  U R                   S5      $ U$ )up   Токен не должен попасть ни в лог, ни в алерт, ни в текст ошибки.z***)r+   replace)r3   texts     r!   _maskMetaClient._mask5   s"    37;;t||DKK/HDHr    c           
        SR                  U R                  UR                  S5      5      n[        X R                  S9nSn[        U R                  5       GH0  n U R                  R                  X4SS9nU R                  U5      nUR                  S5      =(       d    0 n	U	R                  S5      S	:X  a*  [        U R                  U	R                  S
S5      5      5      eUR                  S:  a
  SU;  a  Us  $ SR                  UR                  U R                  U	R                  S
S5      5      5      nUR                  S:  a  UR                  S:w  a    O5X`R                  S-
  :  d  GM  U R!                  ["        SU-  -  5        GM3     [%        SR                  U R                  U5      U5      5      e! [        R                   a$  n
U R                  [        U
5      5      n S n
A
NS n
A
ff = f)Nz{}/{}/)access_tokenr(   <   )paramstimeouterrorcode   messageu   токен протухi  zHTTP {}: {}u   без описанияi  i     r   u	   {} — {})formatbase_urllstripdictr+   ranger/   r2   get_payloadr#   r<   status_coder0   RequestExceptionstrr.   BACKOFF_BASE_SECONDSr   )r3   pathrB   urlquery
last_errorattemptresppayloadrD   excs              r!   rN   MetaClient.get9   s   nnT]]DKK,<=V++6
T../G}}((B(G ---G,299V$++DJJ		)-FG-I J J##c)gW.D"N*11$$djj9F_1`&ac
##c)d.>.>#.E**Q..0ALAB% 0& ;--djjozJKK! ,, 2!ZZC1
2s   F55G-	G((G-c                     U R                  5       n[        U[        5      (       a  U$ SU0$ ! [         a	    SSS00s $ f = f)NrD   rG   u   не-JSON ответdata)json
ValueError
isinstancerL   )rY   rZ   s     r!   rO   MetaClient._payloadR   sU    	BiikG %Wd33wJ&'9JJ  	Bi)?@AA	Bs   - A A c              #  n  #    U[         S.n U R                  X5      nUR                  S/ 5       H  nUR                  SS5      U:  a    gUv   M!     UR                  S5      =(       d    0 R                  S0 5      R                  S5      nU(       a  UR                  S5      (       d  g[        XGS	9nM  7f)
u  Листает страницы, пока посты новее `since`.

Останавливаться на первой странице нельзя: одна тред-серия способна
вытолкнуть 30-дневное окно за пределы 25 постов.
)fieldslimitr^   	timestampr(   Npagingcursorsafter)ri   )	PAGE_SIZErN   rL   )r3   rT   rd   sincerB   rZ   itemri   s           r!   	_paginateMetaClient._paginateZ   s      #Y7hht,GFB/88K,u4
 0 [[*0b55iDHHQEF 3 3&.F s   B3B5c                    U R                  SR                  U5      SSR                  U5      05      nUR                  S/ 5       Vs0 s H  oDS   U R                  U5      _M     sn$ s  snf )Nz{}/insightsmetric,r^   name)rN   rI   join_metric_value)r3   post_idmetricsrZ   ms        r!   	_insightsMetaClient._insightsl   se    ((=//8$chhw&78::A++fb:QR:QQ&	4--a00:QRRRs   A(c                    SU ;   a  U S   =(       d    0 R                  S5      $ U R                  S5      =(       d    0 /nUS   R                  S5      $ )uW   Instagram кладёт число в values[0].value, Threads — в total_value.value.total_valuevaluevaluesr   rN   )rp   r}   s     r!   rt   MetaClient._metric_valueq   sO     F"=)/R44W==H%-"ay}}W%%r    )r/   r2   r.   r+   )r4   rR   r-   zCallable[[float], None]r5   int)r;   rR   returnrR   )rT   rR   rB   rL   r   rL   r   rL   )rT   rR   rd   rR   rk   rR   r   Iterator[dict])ru   rR   r   rL   )rp   rL   )r   r   r   r   r   rJ   MAX_RETRIESr7   r<   rN   staticmethodrO   rm   rx   rt   r   r   r    r!   r&   r&   )   sc     LHDH$/6!6IL2 K K/$S
 & &r    r&   c                  :    \ rS rSr\rSS jrSS jrS	S
S jjrSr	g)InstagramClientz   c                    U R                  SSS05      nUR                  S5      UR                  S5      UR                  S5      S.$ )Nmerd   z'id,username,followers_count,media_countusernamefollowers_countmedia_countr   	followersr   r~   )r3   r^   s     r!   accountInstagramClient.account}   sG    xxx)RST HHZ0!XX&78#xx68 	8r    c              #  L  #    U R                  S[        U5       H  nUS   UR                  S5      =(       d    SUR                  S5      UR                  S5      UR                  S5      UR                  S5      UR                  S	5      UR                  S
5      S.v   M     g 7f)Nzme/mediaidcaptionr(   
media_typemedia_product_type	permalinkrf   
like_countcomments_count)ru   r   r   r   r   rf   r   r	   )rm   IG_MEDIA_FIELDSrN   r3   rk   rl   s      r!   postsInstagramClient.posts   s     NN:FD:88I.4""hh|4&*hh/C&D!XXk2!XXk2,/ HH%56	 	 Gs   B"B$Nc                r    [        [        5      nUS:X  a  U[        [        5      -  nU R                  X5      $ )NFEED)listIG_METRICS_COMMONIG_METRICS_FEED_ONLYrx   )r3   ru   product_typerv   s       r!   insightsInstagramClient.insights   s4    ()6!t011G~~g//r    r   r   rk   rR   r   r   Nru   rR   r   rR   r   rL   )
r   r   r   r   IG_BASErJ   r   r   r   r   r   r    r!   r   r   z   s    H80 0r    r   c                  :    \ rS rSr\rSS jrSS jrS	S
S jjrSr	g)ThreadsClient   c                   ^  T R                  SSS05      nT R                  SSS05      n[        U 4S jUR                  S/ 5       5       S 5      nUR                  S	5      US S
.$ )Nr   rd   zid,usernamezme/threads_insightsrp   r   c              3  t   >#    U  H-  nUR                  S 5      S:X  d  M  TR                  U5      v   M/     g7f)rr   r   N)rN   rt   ).0rw   r3   s     r!   	<genexpr>(ThreadsClient.account.<locals>.<genexpr>   s9      4,Dqf!22 #T"",Ds   88r^   r   r   )rN   next)r3   r^   r   r   s   `   r!   r   ThreadsClient.account   sq    xxx78881H>O3PQ4HLL,D 459;	 !HHZ0&#% 	%r    c              #  0  #    U R                  S[        U5       Hw  nUS   UR                  S5      =(       d    SUR                  S5      [        [	        UR                  S5      5      5      UR                  S5      UR                  S5      S	.v   My     g 7f)
Nz
me/threadsr   r;   r(   r   is_quote_postr   rf   )ru   r;   r   r   r   rf   )rm   TH_MEDIA_FIELDSrN   r   boolr   s      r!   r   ThreadsClient.posts   sy     NN<%HD:(.B"hh|4!$T$((?*C%D!E!XXk2!XXk2  Is   BBNc                .    U R                  U[        5      $ r   )rx   
TH_METRICS)r3   ru   r   s      r!   r   ThreadsClient.insights   s     ~~gz22r    r   r   r   r   r   )
r   r   r   r   TH_BASErJ   r   r   r   r   r   r    r!   r   r      s    H	%	3 3r    r   )r   
__future__r   typingr   r   r0   r   r   r   r   r   r   r   rj   r   rS   RuntimeErrorr   r#   r&   r   r   r   r    r!   <module>r      s    # % 
-
*
5 4 I
9H	 X< Xz zN& N&b0j 0:3J 3r    