
    j(                        d Z ddlmZ ddlZddlmZmZmZ  G d de      Z	 G d d      Z
 ej                         Zdd	Zdd
ZddZddZddZddZddZ	 	 	 	 	 	 	 	 	 	 ddZddZddZddZg dZddZy) us  W20E5 — runtime image call budget primitive.

A thread-safe counter installed for the duration of a background run that
acts as the authoritative cap at every gpt-image-2 / Gemini image / fal
provider call site. ``reserve()`` raises ``ImageCallBudgetExceeded`` BEFORE
the network request is allowed, so cap=0 / exhausted runs never reach the
provider.

The primitive is stdlib-only; no ``app.*`` imports and no provider SDK
imports (verified by an AST test). Call sites import
``reserve_current_call`` which is a no-op when no budget is installed on
the current thread, keeping read-only paths and existing tests unaffected.
    )annotationsN)AnyDictOptionalc                  $     e Zd ZdZd fdZ xZS )ImageCallBudgetExceededzRaised by ``ImageCallBudget.reserve`` when the cap is reached.

    The exception is raised *before* the provider/network call so the
    caller can abort without touching the SDK.
    c                   t        |      | _        t        |      | _        t        |      | _        t
        |   d| j                   d| j                   d| j                         y )Nu#   image_call_budget exceeded — cap=z, used=z, denied source=)intcapusedstrsourcesuper__init__)selfr   r   r   	__class__s       P/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/image_call_budget.pyr   z ImageCallBudgetExceeded.__init__   sZ    s8I	&k1$((7499+ N![[O-	
    )r   r
   r   r
   r   r   returnNone)__name__
__module____qualname____doc__r   __classcell__)r   s   @r   r   r      s    
 
r   r   c                  ,    e Zd ZdZdZddZddZd	dZy)
ImageCallBudgetzCounter with a hard cap on image-provider calls.

    Counts are managed under a re-entrant lock; ``reserve`` and ``snapshot``
    are safe to call from multiple threads against the same instance.
    )_cap_used_denied_lockc                    t        |      }|dk  rt        d| d      || _        d| _        d| _        t        j                         | _        y )Nr   zcap must be >= 0 (got ))r
   
ValueErrorr   r   r    	threadingRLockr!   )r   r   cap_ints      r   r   zImageCallBudget.__init__.   sJ    c(Q;5gYa@AA	
__&
r   c                  | j                   5  | j                  | j                  k\  r7| xj                  dz  c_        t	        | j                  | j                  |      | xj                  dz  c_        ddd       y# 1 sw Y   yxY w)a9  Reserve one unit of the budget for a provider call.

        Must be called *before* the actual provider/network call. Raises
        :class:`ImageCallBudgetExceeded` and increments the denied counter
        when the cap has already been reached; the used counter is left
        unchanged in that case.
           )r   r   r   N)r!   r   r   r    r   )r   r   s     r   reservezImageCallBudget.reserve7   s^     ZZzzTYY&!-		

6  JJ!OJ ZZs   A&A<<Bc                    | j                   5  | j                  | j                  | j                  | j                  | j                  z
  dcddd       S # 1 sw Y   yxY w)z6Return a JSON-serialisable copy of the current counts.)r   r   denied	remainingN)r!   r   r   r    )r   s    r   snapshotzImageCallBudget.snapshotG   s=    ZZyy

,,!YY3	 ZZs   <AAN)r   r
   r   r   r   r   r   r   )r   zDict[str, int])r   r   r   r   	__slots__r   r*   r.    r   r   r   r   %   s     6I' r   r   c                    | t         _        y)zInstall ``budget`` as the current-thread budget.

    The previously installed budget (if any) is replaced. Callers should
    pair this with :func:`uninstall_budget` in a try/finally.
    N_localbudgetr5   s    r   install_budgetr7   ]   s     FMr   c                     dt         _        y)z@Clear the current-thread budget (no error if nothing installed).Nr3   r1   r   r   uninstall_budgetr9   f   s	    FMr   c                 $    t        t        dd      S )z8Return the budget installed on this thread, or ``None``.r5   Ngetattrr4   r1   r   r   get_current_budgetr=   k   s    68T**r   c                    | t         _        y)u  이 스레드의 **정지 확인**을 건다.

    `check` 는 인자 없이 불리고, 멈춰야 하면 예외를 올린다 (보통
    `CancellationToken.raise_if_cancelled`). None 이면 확인 없음.

    ★왜 예산과 나란히 두나 — 유료 이미지 호출은 전부 `reserve_current_call`
     을 지난다. 「돈을 쓰기 직전」이 정확히 여기다. 취소 확인을 각 호출부에
     흩어 놓으면 반드시 빠지는 곳이 생긴다.
    Nr4   
stop_check)checks    r   install_stop_checkrB   p   s     Fr   c                     d t         _        y Nr?   r1   r   r   uninstall_stop_checkrE   }   s
    Fr   c                 $    t        t        dd       S )Nr@   r;   r1   r   r   get_current_stop_checkrG      s    6<..r   c                h    t               }| |        t               }|y|j                  |        y)u  Reserve one image-call unit against the current-thread budget.

    No-op when no budget is installed — keeps non-budgeted code paths
    (legacy unit tests, dry-runs, plan-only runs) unaffected.

    Raises :class:`ImageCallBudgetExceeded` when the installed budget is
    exhausted; callers should let the exception propagate so that
    ``run_steps_batch`` can record it and abort the batch.
    N)r   )rG   r=   r*   )r   r@   r5   s      r   reserve_current_callrI      s5     ()J!F~
NN&N!r   c                    |  ||i |S t               }t        |        	  ||i ||t                S t        |       S # |t                w t        |       w xY w)u   Run ``fn(*args, **kwargs)`` with ``budget`` installed on this thread.

    ``budget=None`` is a no-op for installation (preserves the current
    install). The previous thread-local budget — if any — is restored in a
    ``finally`` block so exceptions cannot leak install state.
    )r=   r7   r9   )r5   fnargskwargspreviouss        r   run_with_budgetrO      sg     ~4"6""!#H6%4"6"8$ 8$s   A Ac                    |  ||i |S t               }t        |        	  ||i |t        |       S # t        |       w xY w)u^   `stop_check` 를 이 스레드에 걸고 `fn` 을 돈다. 끝나면 원래대로 되돌린다.)rG   rB   )r@   rK   rL   rM   rN   s        r   run_with_stop_checkrQ      sL    4"6""%'Hz"%4"6"8$8$s	   4 Ac                ,     t               d fd}|S )uH  정지 확인을 **다른 스레드로 실어 나른다.**

    `install_stop_check` 는 스레드마다 따로라, pool worker 는 아무것도 못 본다.
    예산에 `bind_current_budget` 가 있는 것과 같은 이유로 이것이 필요하다 —
    없으면 팬아웃 안에서 정지가 통째로 안 들린다.
    c                 $    t        g| i |S rD   )rQ   )rL   rM   capturedrK   s     r   _wrappedz)bind_current_stop_check.<locals>._wrapped   s    "8RA$A&AAr   rL   r   rM   r   r   r   )rG   )rK   rU   rT   s   ` @r   bind_current_stop_checkrW      s     &'HB Or   c                D     t               t               d fd}|S )u<  Return a callable that re-installs *this thread's* current budget
    **and stop check** when invoked on another thread.

    ★정지 확인을 예산과 **같이** 실어 나른다. 둘 다 스레드 지역이고 둘 다
     `reserve_current_call` 에서 쓰이므로, 하나만 나르면 팬아웃 안에서 정지가
     통째로 안 들린다. 기존 호출부를 안 고쳐도 되도록 이 함수가 둘을 다 잡는다.

    Intended use::

        from concurrent.futures import ThreadPoolExecutor
        with ThreadPoolExecutor(max_workers=4) as pool:
            pool.submit(bind_current_budget(_worker), arg1, arg2)

    The budget is captured at ``bind_current_budget`` call time, not at
    callable invocation time — so the parent may safely clear its own
    install before the worker actually runs.
    c                 0    t        t        g| i |S rD   )rQ   rO   )rL   rM   rT   captured_stoprK   s     r   rU   z%bind_current_budget.<locals>._wrapped   s)    "?Hb
;?
CI
 	
r   rV   )r=   rG   )rK   rU   rT   rZ   s   ` @@r   bind_current_budgetr[      s!    $ "#H*,M

 Or   )r   r   r7   r9   r=   rI   rO   r[   rB   rE   rG   rQ   rW   c                 <    t               } | y | j                         S rD   )r=   r.   r6   s    r   current_snapshotr]     s    !F~??r   )r5   r   r   r   )r   r   )r   Optional[ImageCallBudget])rA   Optional[Any]r   r   )r   r_   r/   )
r5   r^   rK   r   rL   r   rM   r   r   r   )
r@   r_   rK   r   rL   r   rM   r   r   r   )rK   r   r   r   )r   zOptional[Dict[str, Any]])r   
__future__r   r%   typingr   r   r   RuntimeErrorr   r   localr4   r7   r9   r=   rB   rE   rG   rI   rO   rQ   rW   r[   __all__r]   r1   r   r   <module>re      s    #  & &
l 
"* *j 
	
+

/"H%%%% % 	%
 	%2	%:&r   