U
    i                     @   s   d dl Z d dlZd dlZd dlZd dlZd dlZd dlmZ d dlmZm	Z	m
Z
mZmZ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mZ d dlmZmZmZ d dlmZmZ d d	l m!Z!m"Z" d d
l#m$Z$m%Z%m&Z& ede'Z(dZ)dddhZ*e+dddZ,e-ddddZ.ej/e-dddZ0ej/e1ddddZ2e+dddZ3dd Z4e-d d!d"Z5e-e+d#d$d%Z6e-dd&d'Z7e-dd(d)Z8e-e-d*d+d,Z9ej:d-d.d/Z;d0d1 Z<d2d3 Z=e-d d4d5Z>e(j?d6d7d8d9d7 Z@e(j?d:d;d8d<d; ZAe(j?d=d>d8d?d> ZBe(j?d@dAd8dBdA ZCe(j?dCdDd8dEdD ZDe(j?dFdGdHgdIdJdKdI ZEe(j?dLdHgdMdJdNdM ZFe(jGdOdPd8dQdP ZHe(jIdRdSd8e-dTdUdSZJe(jIdVdWd8e-dTdXdWZKe(jIdYdZd8d[dZ ZLe(j?d\dHgd]dJd^d] ZMe(j?d_dGgd`dJe-dadbd`ZNe(j?dcdGgdddJe-dadeddZOe(j?dfdHgdgdJe-dadhdgZPe(j?didHgdjdJe-dadkdjZQe(j?dldGgdmdJe-dadndmZRe(j?dodGgdpdJe-dadqdpZSe(jGdrdsd8e-dadtdsZTe(jGdudvd8e-dTdwdvZUe(jGdxdyd8e-dTdzdyZVdS ){    N)datetime)
	Blueprintcurrent_apprender_templaterequestredirecturl_forflash	send_filejsonifysession)bulk_db)require_login_access_token)
save_draft
load_draft)safe_filenamevideo_duration_seconds_ffprobebuild_public_media_url)creator_can_post_nowparse_publish_at_iso_to_ts)
load_token
save_token)query_creator_infoupload_video_direct_postfetch_post_statusZuiZadmin_authenticatedcompletefailed	cancelled)returnc                   C   s   t dd k	S )Ntiktok_token)r   get r!   r!   //var/www/html/luxverbi-app/app/blueprints/ui.py_is_connected%   s    r#   )aliasr   c                 C   sR   | pd  }|sdS z$t|s6td}|r6t|| W n tk
rL   Y nX dS )u   
    Defensa extra para el caso en que ya existan sesiones antiguas donde
    oauth_alias estaba seteado pero token_store no lo tenía aún.
     Nr   )stripr   r   r    r   	Exception)r$   atokr!   r!   r"   _ensure_alias_token_persisted)   s    
r*   )conn
table_namec              	   C   sf   |   }|d| d g }| D ]:}z||d  W q& tk
r^   ||d  Y q&X q&|S )NzPRAGMA table_info()name   )cursorexecutefetchallappendr'   )r+   r,   curcolsrowr!   r!   r"   _table_columns;   s    r7   )r+   jobr   c                    sh   t | d  fdd| D }d| }ddgt| }| d| d| dt|  d S )	N	bulk_jobsc                    s"   i | ]\}}|t  kr||qS r!   )set.0kvr5   r!   r"   
<dictcomp>I   s       z$_insert_bulk_job.<locals>.<dictcomp>, ?INSERT INTO bulk_jobs (
) VALUES (r-   )r7   itemsjoinkeyslenr1   tuplevalues)r+   r8   Zfilteredcolnamesplaceholdersr!   r?   r"   _insert_bulk_jobG   s
    
rM   c                   C   s   t tdkS )NT)r   r    ADMIN_SESSION_KEYr!   r!   r!   r"   _is_admin_authenticatedO   s    rO   c                  C   s,   t  r
d S tjrtjntj} ttd| dS )Nui.admin_login)next)rO   r   query_string	full_pathpathr   r   next_urlr!   r!   r"   _admin_guardS   s    rW   )stored_filenamec                 C   s@   | pd  }|r(d|ks(d|ks(d|kr,d S tjtjd |S )Nr%   /\z..
UPLOAD_DIR)r&   osrT   rF   r   config)rX   filenamer!   r!   r"   _job_file_pathZ   s    r_   )rX   r   c                 C   s   t | }t|otj|S )N)r_   boolr\   rT   isfile)rX   rT   r!   r!   r"   _job_has_filea   s    rb   c                 C   sD   | dkrdS zt t| dW S  tk
r>   t|  Y S X d S )NNr%   -z%Y-%m-%d %H:%M:%S)r   fromtimestampintstrftimer'   str)tsr!   r!   r"   
_format_tsf   s    rj   c                 C   s|   | dkrdS ddddg}t | }|d }|D ]$}|dk sD||d	 krH qR|d
 }q,|dkrj|dd| S t| d| S )Nrc   rd   BZKBMBGBr   i   g      @z.1f )floatrf   )sizeunitsvalueunitr!   r!   r"   _format_byteso   s    
ru   )statusr   c                 C   sD   | pd   }|dkrdS |dkr(dS |dkr4dS |dkr@dS d	S )
Nr%   r   okr   errr   warn   
submitting	submittedinfo)r&   lower)rv   str!   r!   r"   _status_tone|   s    r   )r6   c                 C   s   t | }|dpd  }|tk|d< t||d< t|dpDd|d< t|d|d< t|d	|d
< t|d|d< t|d|d< |d rt|dpd}zt	j
||d< W q tk
r   d |d< Y qX nd |d< t|d |d< |S )Nrv   r%   is_terminalZstatus_tonerX   has_filecreated_at_tsZcreated_at_labelpublish_at_tsZpublish_at_labelnext_attempt_tsZnext_attempt_labelupdated_at_tsZupdated_at_labelZfile_size_bytesZfile_size_label)dictr    r&   r~   TERMINAL_JOB_STATUSESr   rb   rj   r_   r\   rT   getsizeOSErrorru   )r6   r8   rv   	file_pathr!   r!   r"   _job_for_admin   s$    r   c                  C   sn   t tjd } tj| _|  }|d | }| 	  g }|D ](}t
|}|d r^|d s^q@|| q@|S )NBULK_DB_PATHz
        SELECT * FROM bulk_jobs
        ORDER BY
          CASE
            WHEN status IN ('scheduled', 'submitting', 'submitted') THEN 0
            ELSE 1
          END,
          publish_at_ts ASC,
          updated_at_ts DESC
        r   r   )r   r   r]   sqlite3Rowrow_factoryr0   r1   r2   closer   r3   )r+   r4   rowsjobsr6   r8   r!   r!   r"   _load_admin_jobs   s    r   c           	      C   s   t jd }i }| D ].}|dp"d }|s.q||g | qg }t|D ]r}| s^qP|	 }||j
g }||j
|jt|jtt|jt|tdd |D tdd |D d qP|jdd	 d
 |S )Nr[   rX   r%   c                 s   s   | ]}|d  rdndV  qdS )r   r   r/   Nr!   r<   jr!   r!   r"   	<genexpr>   s     z)_load_upload_inventory.<locals>.<genexpr>c                 s   s   | ]}|d  rdndV  qdS )r   r/   r   Nr!   r   r!   r!   r"   r      s     )rX   Z
size_bytesZ
size_labelZmodified_at_labellinked_jobs_countactive_jobs_countZterminal_jobs_countc                 S   s    | d dk| d  | d   fS )Nr   r   r   rX   )r~   )itemr!   r!   r"   <lambda>   s    

z(_load_upload_inventory.<locals>.<lambda>)key)r   r]   r    r&   
setdefaultr3   r\   scandiris_filestatr.   st_sizeru   rj   rf   st_mtimerH   sumsort)	r   
upload_dirZ	job_linksr8   rX   filesentryr   Zlinked_jobsr!   r!   r"   _load_upload_inventory   s6    
r   c                 C   s   t tjd }tj|_| }|d| f | }|	  d}d}|D ]>}t
|d pXd}|d pfd  tkr~||7 }qH||7 }qH||fS )Nr   z
        SELECT status, COUNT(*) AS total
        FROM bulk_jobs
        WHERE stored_filename=?
        GROUP BY status
        r   totalrv   r%   )r   r   r]   r   r   r   r0   r1   r2   r   rf   r&   r~   r   )rX   r+   r4   r   ZactiveZterminalr6   r   r!   r!   r"   _count_jobs_for_file   s"    	

r   rY   landing)endpointc                   C   s   t dt dS )Nzlanding.htmlZis_connectedr   r#   r!   r!   r!   r"   r     s    /appapp_homec                   C   s   t dt dS )Nz
index.htmlr   r   r!   r!   r!   r"   r     s    z/contactcontactc                   C   s   t dt dS )Nzcontact.htmlr   r   r!   r!   r!   r"   r     s    z/tostosc                   C   s   t dS )Nztos.htmlr   r!   r!   r!   r"   r     s    z/privacyprivacyc                   C   s   t dS )Nzprivacy.htmlr   r!   r!   r!   r"   r     s    z/admin/loginGETPOSTadmin_login)methodsr   c                  C   s   t  rttdS tjdp,tjdp,d } tjdkrtjdpJd}t	t
jdp\d}|rt||rdtt< dt_tdd	 | d
rt| S ttdS tdd td| dS )Nui.admin_jobsrQ   r%   r   passwordADMIN_PASSWORDTzAcceso admin concedido.successrY   u   Contraseña admin incorrecta.errorzadmin_login.htmlrU   )rO   r   r   r   argsr    formr&   methodrh   r   r]   secretscompare_digestr   rN   modifiedr	   
startswithr   )rV   providedexpectedr!   r!   r"   r     s     



z/admin/logoutadmin_logoutc                   C   s(   t td  dt _tdd ttdS )NTu   Sesión admin cerrada.r   rP   )r   poprN   r   r	   r   r   r!   r!   r!   r"   r   5  s    
z/admin
admin_jobsc                  C   sN   t  } | r| S t }dd |D }dd |D }t|}td|||t|dS )Nc                 S   s   g | ]}|d  s|qS r   r!   r<   r8   r!   r!   r"   
<listcomp>D  s      zadmin_jobs.<locals>.<listcomp>c                 S   s   g | ]}|d  r|qS r   r!   r   r!   r!   r"   r   E  s      zadmin_jobs.html)in_progress_jobsfinished_jobsupload_filesZ
total_jobs)rW   r   r   r   rH   )guardr   r   r   r   r!   r!   r"   r   =  s    z/admin/jobs/<job_id>/canceladmin_cancel_jobjob_idc                 C   s  t  }|r|S ttjd }tj|_| }|d| f |	 }|sd|
  tdd ttdS |d pnd  }|tkr|
  tdd	 ttdS tt }|d
||| f |  |
  |jdkrtdd	 ttdS |dkrtdd	 n
tdd ttdS )Nr   "SELECT * FROM bulk_jobs WHERE id=?El trabajo indicado no existe.r   r   rv   r%   z!Ese trabajo ya estaba finalizado.warningz
        UPDATE bulk_jobs
        SET status='cancelled',
            next_attempt_ts=?,
            last_status='CANCELLED_BY_ADMIN',
            updated_at_ts=?
        WHERE id=? AND status NOT IN ('complete', 'failed', 'cancelled')
        r/   uG   No se pudo cancelar el trabajo porque su estado cambió mientras tanto.rz   ul   Trabajo cancelado localmente. Si TikTok ya había aceptado el envío, esta acción no puede retirarlo allí.z Trabajo cancelado correctamente.r   )rW   r   r   r]   r   r   r   r0   r1   fetchoner   r	   r   r   r&   r~   r   rf   timecommitrowcount)r   r   r+   r4   r6   rv   now_tsr!   r!   r"   r   Q  sD    





z/admin/jobs/<job_id>/relaunchadmin_relaunch_jobc                 C   s  t  }|r|S ttjd }tj|_| }|d| f |	 }|sd|
  tdd ttdS |d pnd  }|tkr|
  tdd ttdS |d	 pd}t|}|rtj|s|
  td
d ttdS t|d pd p|}t j}	|	 d| }
t|
}zt|| W nL tk
rt } z,|
  td| d ttd W Y S d }~X Y nX tt }|	|||ddd |d |d |d |d |d |d |d |d |d |d |d |
|dd d d |d}zt|| |  W nr t k
rn } zR|
  zt!| W n tk
r<   Y nX td| d ttd W Y S d }~X Y nX |
  td|	 d ttdS )Nr   r   r   r   r   rv   r%   z-Solo se pueden relanzar trabajos finalizados.rX   z=No se puede relanzar porque el archivo original ya no existe.original_filename_z6No se pudo duplicar el archivo para el relanzamiento: 	scheduledr   access_tokenaccount_aliastitleprivacy_levelallow_comment
allow_duetallow_stitchcommercial_togglebrand_organic_togglebrand_content_toggleis_aigcidr   r   r   rv   attemptsZ
last_errorr   r   r   r   r   r   r   r   r   r   r   rX   r   	video_url
publish_idZlast_statusZlast_fail_reasonr   z#No se pudo crear el nuevo trabajo: zTrabajo relanzado. Nuevo job: r   )"rW   r   r   r]   r   r   r   r0   r1   r   r   r	   r   r   r&   r~   r   r_   r\   rT   ra   r   uuiduuid4hexshutilcopy2r   rf   r   rM   r   r'   remove)r   r   r+   r4   r6   rv   Zsource_filenamesource_pathr   Z
new_job_idZnew_stored_filenameZnew_file_pathexcr   r8   r!   r!   r"   r     s    



"
"z/admin/files/deleteadmin_delete_filec               
   C   s
  t  } | r| S tjdpd }t|}|sDtdd ttdS t	|\}}t
j|srtdd ttdS zt
| W nB tk
r } z$td| d ttd W Y S d }~X Y nX |d	krtd
| dd n|d	krtdd n
tdd ttdS )NrX   r%   u   Nombre de archivo inválido.r   r   z&El archivo ya no existe en el sistema.r   zNo se pudo borrar el archivo: r   u   Archivo borrado. Ojo: había uE    trabajo(s) en curso usando ese vídeo y ahora quedarán sin fichero.uV   Archivo borrado. Los trabajos finalizados asociados dejarán de mostrarse en el panel.r   zArchivo borrado del sistema.)rW   r   r   r    r&   r_   r	   r   r   r   r\   rT   ra   r   r   )r   rX   r   Zactive_jobsZterminal_jobsr   r!   r!   r"   r     s4    

"

z/upload_localupload_localc                  C   s   t  } | stdd tdS dtjkr8tdd tdS tjd }|jsZtdd tdS tjd }t|j}t	
 j}| d| }tj||}|| t||||d d d d	d d d
	 ttd|dS )N!Please connect with TikTok first.r   r   ZvideozNo video file provided.zNo file selected.r[   r   Zlocal_uploaded)	r   rX   r   r   last_init_responseposting_summaryrv   r   bulk_job_idui.post_to_tiktokdraft_id)r   r	   r   r   r   r^   r   r]   r   r   r   r   r\   rT   rF   saver   r   )r   filer   Zoriginal_fnr   rX   Z	save_pathr!   r!   r"   r     s>    








z/static_preview/<draft_id>static_previewr   c                 C   sJ   t  }|sdS t| }|sdS |d}|r8tj|s<dS t|dddS )N)zNot authenticated  )z	Not found  r   z	video/mp4F)mimetypeas_attachment)r   r   r    r\   rT   existsr
   )r   r   draftrT   r!   r!   r"   r  0  s    
z/post/<draft_id>post_to_tiktokc                 C   s2  t  }|stdd tdS t| }|s:tdd tdS zt|}W n> tk
r } z td| d td W Y S d }~X Y nX t|\}}|d}t|d }d}	d }
t	|t
r|d	kr|d	kr||krd
}	d||f }
d }|d}|rt|tjd tjd tjd d}td| |||	|
|||d	S )Nr   r   r   Draft not found.8Cannot post right now. Please try again later. Details: max_video_post_duration_secr   Tr   Fz7Video is %.1fs but max allowed is %ss for this creator.rX   PUBLIC_BASE_URLMEDIA_SIGNING_SECRETMEDIA_TOKEN_TTL_SECONDSrX   Zpublic_base_urlZsigning_secretZttl_secondszpost_to_tiktok.html)r   r	  creator_infoduration_okduration_msgpull_urlcan_post_nowcannot_post_reason)r   r	   r   r   r   r'   r   r    r   
isinstancerf   r   r   r]   r   )r   r   r	  r  er  r  max_durdurr  r  r  rX   r!   r!   r"   r
  A  sR    


"
z/post/<draft_id>/publishpublishc                 C   sV  t  }|stdd tdS t| }|s:tdd tdS zt|}W nF tk
r } z(td| d ttd| d W Y S d }~X Y nX tj	dd	krtd
d ttj
ptdS t|\}}|st|pdd ttd| dS tj	dpd }|s tdd ttj
ptdS tj	dp0d }tj	ddk}	tj	ddk}
tj	ddk}|r|dkrtdd ttd| dS |	dpg }||krtdd ttd| dS tj	ddk}tj	ddk}tj	ddk}|	ddkrd}|	ddkrd}|	d dkr0d}| }| }| }d}d}|	r|
sv|svtd!d ttd| dS |
}|}|r|dkrtdd ttd| dS |	d"}t|d# }t|tr
|d$kr
|d$kr
||kr
td%||f d ttd| dS |	d&}|s4td'd ttd| dS tjd( }tj||}tj|sttd)d ttd| dS t|tjd* tjd+ tjd, d-}zrt||||||||dd.|d/}|	d0pi 	d1}t| |||||||||	|
|d2d3|d4 ttd5| dW S  tk
rP } z(td6| d ttd| d W Y S d }~X Y nX d S )7Nr   r   r   r  r  r   r   music_confirm1uE   You must agree to TikTok’s Music Usage Confirmation before posting.zui.app_homezJThis TikTok account cannot publish at this moment. Please try again later.r   r%   z1Please select a Privacy status before publishing.r   r   on
your_brandbranded_content	SELF_ONLYzIBranded content cannot be posted with privacy set to Only me (SELF_ONLY).privacy_level_options'You must select a valid privacy option.r   r   r   comment_disabledTFduet_disabledstitch_disabled]If commercial content disclosure is on, you must select Your brand, Branded content, or both.r  r   r   1Video duration %.1fs exceeds allowed maximum %ss.rX   2Internal error: missing stored_filename for draft.r[   Video file not found on server.r  r  r  r  ZPULL_FROM_URL)r   Zcaptionr   disable_commentdisable_duetdisable_stitchr   r   r   moder   datar   )r   r   r   r   r   r   r   r!  Zposted)r   r   r   rv   r   zui.status_pagezError posting to TikTok: )r   r	   r   r   r   r'   r   r   r   r    referrerr   r&   r   r  rf   r   r]   r\   rT   rF   r  r   r   r   )r   r   r	  r  r  r  r  r   r   r   r   r!  optionsr   r   r   r,  r-  r.  r   r   r  r  rX   r   
local_pathr   Z	init_respr   r!   r!   r"   r  x  s    

&






*




z/post/<draft_id>/scheduleschedule_postc           "         s  t  }|stdd tdS t| }|s:tdd tdS tdpFd }|sbd}|td< dt_t| t	j
d	pxd }|std
d ttd| dS zt|}W nF tk
r } z(td| d ttd| d W Y S d }~X Y nX t	j
ddkrtdd ttd| dS t	j
dp,d }|sRtdd ttd| dS t	j
dpbd }t	j
ddk}	t	j
ddk}
t	j
ddk}|	r|
s|stdd ttd| dS t|	o|
}t|	o|}|r
|dkr
tdd ttd| dS t	j
ddk}t	j
ddk}t	j
ddk}|d}|rXtj|srtdd ttd| dS |d }|d!pd ptj|}|std"d ttd| dS z
t|}|d#pg }|r||krtd$d ttd| dW S |d%dkrd&}|d'dkr.d&}|d(dkrBd&}|d)}t|}t|tr|d*kr|d*kr||krtd+||f d ttd| dW S t|\}}|st|pd,d- d. W n4 tk
r } ztd/| d. W 5 d }~X Y nX tt }t j}||||d0d*d |||||r6d1nd*|rBd1nd*|rNd1nd*|	rZd1nd*|rfd1nd*|rrd1nd*d1||dd d d |d2}ttjd3 }tj |_!|" }|#d4 d5d6 |$ D }t%|  fd7d8|& D }d9'|( } d9'd:gt)| }!|#d;|  d<|! d=t*|+  |,  |-  t.| |d0|||||||	|
|||d>
d? td@| dA ttdB|dCS )DNr   r   r   r  Zoauth_aliasr%   defaultT
publish_atz#Please select a schedule date/time.r   r   z<Invalid publish_at (must include timezone offset). Details: r  r  uH   You must agree to TikTok’s Music Usage Confirmation before scheduling.r   z1Please select a Privacy status before scheduling.r   r   r  r   r!  r(  r"  z;Branded content cannot be scheduled with privacy SELF_ONLY.r   r   r   r   r+  rX   r   r*  r#  r$  r%  Fr&  r'  r  r   r)  z2This TikTok account cannot publish at this moment.zD Your job was scheduled anyway; the worker will retry automatically.r   zKCould not validate creator settings right now (scheduled anyway). Details: r   r/   r   r   zPRAGMA table_info(bulk_jobs)c                 S   s   g | ]}|d  qS )r.   r!   )r<   rr!   r!   r"   r     s     z!schedule_post.<locals>.<listcomp>c                    s   i | ]\}}| kr||qS r!   r!   r;   Zcolsetr!   r"   r@     s       z!schedule_post.<locals>.<dictcomp>rA   rB   rC   rD   r-   )
r   r   r   r   r   r   r   r!  r6  r   )rv   r   r   zScheduled job created: r   zui.bulk_job_pager   )/r   r	   r   r   r   r    r&   r   r*   r   r   r   r   r'   r`   r\   rT   r  basenamer   r   r  rf   r   r   r   r   r   r   r   r]   r   r   r   r0   r1   r2   r:   rE   rF   rG   rH   rI   rJ   r   r   r   )"r   r   r	  r   r6  r   r  r   r   r   r   r!  r   r   r   r   r   r   rX   r   r  r2  r  r  r  r  r   r   r8   r+   r4   r5   rK   rL   r!   r8  r"   r4    s   


&






"


*"

"z/status/<draft_id>status_pagec                 C   sH   t  }|stdd tdS t| }|s:tdd tdS td| |dS )Nr   r   r   r  zstatus.html)r   r	  )r   r	   r   r   r   )r   r   r	  r!   r!   r"   r:    s    

z/api/status/<draft_id>
api_statusc              
   C   s   t  }|stddddfS t| }|s:tddddfS |d}|sZtdd	d
dfS zt||}td|ddfW S  tk
r } ztdt|ddf W Y S d }~X Y nX d S )NFnot_authenticatedrw   r   r  draft_not_foundr  r   TZno_publish_id_yet)rw   state   )rw   r0  )r   r   r   r    r   r'   rh   )r   r   r	  r   r0  r  r!   r!   r"   r;    s    

z/api/media_url/<draft_id>api_media_urlc                 C   s   t  }|stddddfS t| }|s:tddddfS |d}|sZtdddd	fS tjd
 }tj||}tj	|stddddfS t
|tjd tjd tjd d}td|dS )NFr<  r=  r  r>  r  rX   Zmissing_stored_filenamei  r[   Zfile_not_foundr  r  r  r  T)rw   url)r   r   r   r    r   r]   r\   rT   rF   r  r   )r   r   r	  rX   r   r3  rB  r!   r!   r"   rA    s(    

z/bulk/job/<job_id>bulk_job_pagec                 C   s(   t  }|stdd tdS td| dS )Nr   r   r   zbulk_job.htmlr   )r   r	   r   r   )r   r   r!   r!   r"   rC    s
    
z/api/bulk/job/<job_id>api_bulk_jobc                 C   s   t  }|stddddfS ttjd }tj|_| }|	d| f |
 }|  |sntddddfS td	t|d
dfS )NFr<  r=  r  r   r   Z	not_foundr  T)rw   r8   r@  )r   r   r   r   r]   r   r   r   r0   r1   r   r   r   )r   r   r+   r4   r6   r!   r!   r"   rD    s    )Wr\   r   r   r   r   r   r   flaskr   r   r   r   r   r   r	   r
   r   r   db.bulkr   Zservices.authr   Zservices.draftsr   r   Zservices.mediar   r   r   Zservices.creatorr   r   token_storer   r   Ztiktok_clientr   r   r   __name__bprN   r   r`   r#   rh   r*   
Connectionr7   r   rM   rO   rW   r_   rb   rj   ru   r   r   r   r   r   r   router   r   r   r   r   r   r   r    r   postr   r   r   r   r  r
  r  r4  r:  r;  rA  rC  rD  r!   r!   r!   r"   <module>   s   0

	&







4W
'
)6  :