Ë
    «@ÌjB#  ã            	       óà  — d Z ddlZddlmZ ddlmZmZmZmZ ddl	m
Z
mZ ddlmZ ddlmZ ddlmZ dd	lmZmZmZmZ dd
lmZmZmZmZmZ  ej<                  d«      Z e«       Z dedee!   de!fd„Z"de!de#e!   fd„Z$de!de#e!   fd„Z%de!de!de!fd„Z&de!de'fd„Z(de!de!dee)   fd„Z*de!de!dejV                  fd„Z, G d„ de
«      Z-de!de#e!   de)fd„Z.e j_                  d«       ed«      fdedee!   fd „«       Z0e jc                  d!«       ed«      fd"e-dedee!   fd#„«       Z2e jc                  d$«       ed«      fd"e-dedee!   fd%„«       Z3e jc                  d&«       ed«      fd"e-dedee!   fd'„«       Z4e jc                  d(«       ed«      fd"e-dedee!   fd)„«       Z5e jc                  d*«       ed«      fd"e-dedee!   fd+„«       Z6e j_                  d,«       ed«      fdedee!   fd-„«       Z7y).u  
shoot_api.py â€” API of the photographer's phone app (/shoot).

Auth is the school's private link (`?t=TOKEN`, sent as the `X-Shoot-Token` header): it is regenerated from the
admin when a phone is lost. The phone only ever sends counts; it never sees a photo or a family.

  GET  /api/shoot/info    school, classes, photographers, max photos per child, the live counts
  POST /api/shoot/class   {photographer, qr | class_name}   start a class (closes the previous one)
  POST /api/shoot/child   {photographer, count}              the next child had `count` photos
  POST /api/shoot/skip    {photographer, count}              `count` photos to ignore (test shots)
  POST /api/shoot/undo    {photographer}                     cancel the last child / skip of this class
  POST /api/shoot/end     {photographer}                     close the class (pause, change of room)
  GET  /api/shoot/status  live counts, problems and the last events

The design rationale (counts and a fence, never clocks) is in services/shoot_service.py.
é    N)ÚOptional)Ú	APIRouterÚHeaderÚHTTPExceptionÚRequest)Ú	BaseModelÚField)Úget_db)Úenforce)Ú	client_ip)Úaudit_serviceÚphotographer_serviceÚschool_serviceÚshoot_service)Ú
KIND_CHILDÚ
KIND_CLASSÚKIND_ENDÚ	KIND_SKIPÚ	KIND_UNDOzmeeconic.shootÚrequestÚtokenÚreturnc                 óÜ   — t        dt        | «      › �dd¬«       t        j                  |xs d«      }|st	        dd¬«      ‚t        j                  |«      }|r|d	   st	        dd
¬«      ‚|S )Nzshoot:iÜ  i,  )Úmax_attemptsÚwindow_secondsÚ i‘  zInvalid or revoked link.©ÚdetailÚactivezSchool disabled.)Úrate_limit_enforcer   r   Úschool_for_tokenr   r   Ú
get_school)r   r   Ú	school_idÚschools       ú$/var/www/photos/backend/shoot_api.pyÚ_schoolr&   !   so   € ä˜¤	¨'Ó 2Ð3Ð4À4ÐX[Õ\Ü×.Ñ.¨uª{¸Ó;€IÙÜ˜CÐ(BÔCÐCÜ×&Ñ& yÓ1€FÙ˜ Ò)Ü˜CÐ(:Ô;Ð;ØÐó    r#   c                 ó¶   — ddl m} t        «       5 }|j                  d| f«      j	                  «       }d d d «       t        d„ D «       |¬«      S # 1 sw Y   ŒxY w)Nr   )Úclass_sort_keyz1SELECT name FROM school_classes WHERE school_id=?c              3   ó&   K  — | ]	  }|d    –— Œ y­w)ÚnameN© )Ú.0Úrs     r%   ú	<genexpr>z_classes.<locals>.<genexpr>1   s   è ø€ Ò+ �1�V•9Ñ+ùs   ‚)Úkey)Úadminr)   r
   ÚexecuteÚfetchallÚsorted)r#   r)   ÚconnÚrowss       r%   Ú_classesr7   -   sV   € Ý$Ü	‹ð j�TØ�|‰|ÐOÐR[ÐQ]Ó^×gÑgÓiˆ÷jäÑ+ dÔ+°Ô@Ð@÷jð jús   ‘"AÁAc                 ó~   — t        j                  | «      D �cg c]  }|d   ‘Œ	 c}xs t         j                  gS c c}w )Nr+   )r   Úlist_photographersÚIMPLICIT_PHOTOGRAPHER)r#   Úps     r%   Ú_photographersr<   4   s8   € Ü3×FÑFÀyÓQÖR˜!ˆAˆf‹IÒRò 8Ü ×6Ñ6Ð7ð8ùÒRs   ˜:r+   c                 ó:   — |t        | «      vrt        dd¬«      ‚|S )Né�  zUnknown photographer.r   )r<   r   )r#   r+   s     r%   Ú_photographerr?   9   s"   € Ø”> )Ó,Ñ,Ü˜CÐ(?Ô@Ð@Ø€Kr'   c                 ó¤   — 	 t        dt        t        j                  | «      j	                  d«      xs d«      «      S # t
        t        f$ r Y yw xY w)Né   Úshoot_max_per_childé   )ÚmaxÚintr   Úget_settingsÚgetÚ	TypeErrorÚ
ValueError)r#   s    r%   Ú_max_per_childrJ   ?   sM   € ðÜ�1”cœ.×5Ñ5°iÓ@×DÑDÐEZÓ[Ò`Ð_`ÓaÓbÐbøÜ”zÐ"ò Ùðús   ‚:= ½AÁAÚsidÚphc                 óB  — t        j                  | |«      }t        j                  |«      }|r|d   j                  s|d   nd}|€yt        j                  | |«      d   }t        j
                  ||«      }t        |j                  «      }t        j                  | |«      D �cg c]  }||vsŒ||vsŒ|‘Œ }	}t        j                  | |t        dd|	i«       |j                  |j                  t        |	«      |j                  |j                  t        |	«      k(  dœS c c}w )z_Close the open run with its fence: exactly the files in the folder that belong to it right now.éÿÿÿÿNÚ	processedÚfiles)Ú
class_nameÚdeclaredÚreceivedÚchildrenÚok)r   Ú
events_forÚ
build_runsÚclosedÚ	get_stateÚclaimed_namesÚsetÚbeforeÚ
pool_namesÚrecord_eventr   rQ   rR   ÚlenrT   )
rK   rL   ÚeventsÚrunsÚrunrO   Úclaimedr\   ÚnÚowns
             r%   Ú_close_open_runrf   F   s  € ä×%Ñ% c¨2Ó.€FÜ×#Ñ# FÓ+€DÙ 4¨¡8§?¢?ˆ$ˆrŠ(¸€CØ
€{ØÜ×'Ñ'¨¨RÓ0°Ñ=€IÜ×)Ñ)¨$°	Ó:€GÜ�—‘‹_€FÜ#×.Ñ.¨s°BÓ7Ö
`�¸1ÀGÒ;KÐPQÐY_ÒP_Š1Ð
`€CÐ
`Ü×Ñ˜s B¬°$¸À#¸ÔGØŸ.™.°c·l±lÔPSÐTWÓPXØŸ™¨C¯L©L¼CÀ»HÑ,DñFð Fùò as   Â#	DÂ-DÂ2Dc                 óv   — t        j                  t        j                  | |«      «      }|€t        dd¬«      ‚|S )Né™  z,No class is active: scan the class QR first.r   )r   Úopen_runrV   r   )rK   rL   rb   s      r%   Ú_require_openrj   V   s7   € Ü
×
 Ñ
 ¤×!9Ñ!9¸#¸rÓ!BÓ
C€CØ
€{Ü˜CÐ(VÔWÐWØ€Jr'   c                   ó®   — e Zd ZU  edd¬«      Zeed<    edd¬«      Zee   ed<    edd	¬«      Z	ee   ed
<    edde
j                  ¬«      Zee   ed<   y)Ú	ShootBodyrA   é@   )Ú
min_lengthÚ
max_lengthÚphotographerNéÈ   )Údefaultro   Úqré(   rQ   )rr   ÚgeÚleÚcount)Ú__name__Ú
__module__Ú__qualname__r	   rp   ÚstrÚ__annotations__rs   r   rQ   r   ÚMAX_SKIPrw   rE   r,   r'   r%   rl   rl   ]   sZ   … Ù¨°rÔ:€L�#Ó:Ù d°sÔ;€Bˆ�‰Ó;Ù %¨d¸rÔ B€J�˜‘ÓBÙ ¨°!¸×8NÑ8NÔO€Eˆ8�C‰=ÔOr'   rl   Únamesc                 óV   — |D �ci c]  }|t        j                  | |«      “Œ c}S c c}w )N)r   Úlive_counts)rK   r~   rd   s      r%   Ú_liver�   d   s(   € Ø:?Ö@°QˆAŒ}×(Ñ(¨¨aÓ0Ñ0Ò@Ð@ùÒ@s   …&z/api/shoot/infoÚx_shoot_tokenc           
   ƒ   óø   K  — t        | |«      }t        j                  |«      }t        |«      }||j	                  d«      xs ||j	                  d«      dk(  t        |«      |t        |«      t        ||«      dœS ­w)NÚschool_nameÚ
shoot_modeÚtrue)r#   r„   ÚenabledÚclassesÚphotographersÚmax_per_childÚlive)r&   r   rF   r<   rG   r7   rJ   r�   )r   r‚   rK   Úsettingsr~   s        r%   Ú
shoot_infor�   h   sw   è ø€ ä
�'˜=Ó
)€CÜ×*Ñ*¨3Ó/€HÜ˜3Ó€EàØ—|‘| MÓ2Ò9°cØ—<‘< Ó-°Ñ7Ü˜C“=ØÜ'¨Ó,Ü�c˜5Ó!ñð ùs   ‚A8A:z/api/shoot/classÚbodyc              ƒ   ó�  K  — t        ||«      }t        || j                  «      }| j                  rEt	        j
                  | j                  «      }|st        dd¬«      ‚|\  }}||k7  rPt        dd¬«      ‚| j                  r*dj                  | j                  j                  «       «      }nt        dd¬«      ‚|t        |«      vrt        dd|› �¬«      ‚t        ||«      }t	        j                  ||«      }	t	        j                  |	«      }
t	        j                  |
t	        j                  ||«      d   «      }t	        j                   ||«      D �cg c]	  }||vsŒ|‘Œ }}t	        j"                  ||t$        |d	|i«      }t'        j(                  d
|t+        |«      |||t-        |«      dœ¬«       |||t-        |«      dœS c c}w ­w)Nr>   zThis QR is not a class QR.r   z%This class belongs to another school.ú z'A class QR or a class name is required.zUnknown class: rO   r\   zshoot.class)rQ   rp   Ústrays)Úuser_idÚ
ip_addressr#   Údetails)ÚeventrQ   Úpreviousr‘   )r&   r?   rp   rs   r   Úparse_class_qrr   rQ   ÚjoinÚsplitr7   rf   rV   rW   rZ   rY   r]   r^   r   r   Ú	log_eventr   r_   )rŽ   r   r‚   rK   rL   ÚparsedÚ	qr_schoolrQ   r–   r`   ra   rc   rd   r‘   Úevs                  r%   Úshoot_classrž   x   s¢  è ø€ ä
�'˜=Ó
)€CÜ	�s˜D×-Ñ-Ó	.€BØ‡w‚wÜ×-Ñ-¨d¯g©gÓ6ˆÙÜ Ð,HÔIÐIØ &Ñˆ	�:Ø˜ÒÜ Ð,SÔTÐTØ	�ŠØ—X‘X˜dŸo™o×3Ñ3Ó5Ó6‰
ä˜CÐ(QÔRÐRØœ #›Ñ&Ü˜C¨/¸*¸Ð(FÔGÐGä˜s BÓ'€HÜ×%Ñ% c¨2Ó.€FÜ×#Ñ# FÓ+€DÜ×)Ñ)¨$´×0GÑ0GÈÈRÓ0PÐQ\Ñ0]Ó^€GÜ&×1Ñ1°#°rÓ:ÖO�A¸aÀwÒ>NŠaÐO€FÐOÜ	×	#Ñ	# C¨¬Z¸ÀhÐPVÐEWÓ	X€BÜ×Ñ˜M°2Ä)ÈGÓBTÐ`cØ3=ÈrÔ]`ÐagÓ]hÑ$iõkà z¸xÔSVÐW]ÓS^Ñ_Ð_ùò	 Pùs   ‚EGÅ	GÅGÅ A&Gz/api/shoot/childc              ƒ   ób  K  — t        ||«      }t        || j                  «      }t        ||«       t	        |«      }| j
                  �| j
                  |kD  rt        dd|› d�¬«      ‚t        j                  ||t        t        | j
                  «      «       dt        j                  ||«      iS ­w)Nr>   zA child has at most z photos: tap once per child.r   r‹   )r&   r?   rp   rj   rJ   rw   r   r   r^   r   r{   r€   )rŽ   r   r‚   rK   rL   Úlimits         r%   Úshoot_childr¡   •   s™   è ø€ ä
�'˜=Ó
)€CÜ	�s˜D×-Ñ-Ó	.€BÜ�#�rÔÜ˜3Ó€EØ‡z�zÐ˜TŸZ™Z¨%Ò/ä˜CÐ*>¸u¸gÐEaÐ(bÔcÐcÜ×Ñ˜s B¬
´C¸¿
¹
³OÔDØ”M×-Ñ-¨c°2Ó6Ð7Ð7ùs   ‚B-B/z/api/shoot/skipc           	   ƒ   óü   K  — t        ||«      }t        || j                  «      }t        ||«       t	        j
                  ||t        t        | j                  xs d«      «       dt	        j                  ||«      iS ­w)NrA   r‹   )
r&   r?   rp   rj   r   r^   r   r{   rw   r€   )rŽ   r   r‚   rK   rL   s        r%   Ú
shoot_skipr£   ¢   sg   è ø€ ä
�'˜=Ó
)€CÜ	�s˜D×-Ñ-Ó	.€BÜ�#�rÔÜ×Ñ˜s B¬	´3°t·z±z²ÀQÓ3GÔHØ”M×-Ñ-¨c°2Ó6Ð7Ð7ùs   ‚A:A<z/api/shoot/undoc              ƒ   óþ   K  — t        ||«      }t        || j                  «      }t        ||«      }|j                  st        dd¬«      ‚t        j                  ||t        «       dt        j                  ||«      iS ­w)Nrh   zNothing to undo in this class.r   r‹   )
r&   r?   rp   rj   Úitemsr   r   r^   r   r€   )rŽ   r   r‚   rK   rL   rb   s         r%   Ú
shoot_undor¦   «   so   è ø€ ä
�'˜=Ó
)€CÜ	�s˜D×-Ñ-Ó	.€BÜ
˜˜RÓ
 €CØ�9Š9Ü˜CÐ(HÔIÐIÜ×Ñ˜s B¬	Ô2Ø”M×-Ñ-¨c°2Ó6Ð7Ð7ùs   ‚A;A=z/api/shoot/endc              ƒ   ón   K  — t        ||«      }t        || j                  «      }t        ||«      }d|iS ­w)NrX   )r&   r?   rp   rf   )rŽ   r   r‚   rK   rL   rX   s         r%   Ú	shoot_endr¨   ¶   s;   è ø€ ä
�'˜=Ó
)€CÜ	�s˜D×-Ñ-Ó	.€BÜ˜S "Ó%€FØ�fÐÐùs   ‚35z/api/shoot/statusc              ƒ   óT  K  — t        | |«      }ddlm}  ||«      }t        |«      }t	        j
                  |«      t        ||«      |j                  dd«      |j                  dd«      |D �ci c],  }|t	        j                  ||«      j                  dg «      dd  “Œ. c}dœS c c}w ­w)Nr   )Úget_organizer_statusÚpending_countÚunassigned_countÚblockedéûÿÿÿ)r`   r‹   ÚpendingÚ
unassignedr­   )	r&   Úservices.organizer_servicerª   r<   r   Úrecent_eventsr�   rG   rY   )r   r‚   rK   rª   Úorgr~   rd   s          r%   Úshoot_statusr´   ¾   s£   è ø€ ä
�'˜=Ó
)€CÝ?Ù
˜sÓ
#€CÜ˜3Ó€Eä×-Ñ-¨cÓ2Ü�c˜5Ó!Ø—7‘7˜?¨AÓ.Ø—g‘gÐ0°!Ó4ØX]Ö^ÐST�A”}×.Ñ.¨s°AÓ6×:Ñ:¸9ÀbÓIÈ"È#ÐNÑNÒ^ñð ùò
 _ùs   ‚A+B(Á-1B#Â
B()8Ú__doc__ÚloggingÚtypingr   Úfastapir   r   r   r   Úpydanticr   r	   Úcore.dbr
   Úcore.rate_limitr   r    Úcore.securityr   Úservicesr   r   r   r   Úservices.shoot_servicer   r   r   r   r   Ú	getLoggerÚlogÚrouterr{   r&   Úlistr7   r<   r?   rE   rJ   Údictrf   ÚRunrj   rl   r�   rG   r�   Úpostrž   r¡   r£   r¦   r¨   r´   r,   r'   r%   ú<module>rÆ      sï  ðñó  Ý ç =Ó =ß %å Ý 9Ý #ß WÓ Wß YÕ Yà€g×ÑÐ(Ó)€Ù	‹€ð	�Wð 	 X¨c¡]ð 	°só 	ðA˜ð A  S¡	ó Að8˜cð 8 d¨3¡ió 8ð
˜Sð ¨ð °ó ð˜cð  có ðF˜ð F #ð F¨(°4©.ó Fð �sð  ð ¨×(9Ñ(9ó ôP�	ô PðAˆsð A˜4 ™9ð A¨ó Að ‡�ÐÓÙFLÈTÃlñ ˜gð °h¸s±mò ó ðð ‡�ÐÓ ÙX^Ð_cÓXdñ `˜Ið `°ð `ÈÐQTÉò `ó !ð`ð8 ‡�ÐÓ ÙX^Ð_cÓXdñ 	8˜Ið 	8°ð 	8ÈÐQTÉò 	8ó !ð	8ð ‡�ÐÓÙW]Ð^bÓWcñ 8˜9ð 8¨wð 8ÀxÐPSÁ}ò 8ó  ð8ð ‡�ÐÓÙW]Ð^bÓWcñ 8˜9ð 8¨wð 8ÀxÐPSÁ}ò 8ó  ð8ð ‡�ÐÓÙV\Ð]aÓVbñ ˜)ð ¨gð ÀhÈsÁmò ó ðð ‡�ÐÓ ÙHNÈtËñ  ð ¸À¹ò ó !ñr'   