o
    
jRL                     @   s  d 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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Zddl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$ ddl%m&Z&m'Z' e(e)Z*eG dd dZ+e, Z-de+fddZ.dd Z/d#ddZ0d#ddZ1de!de!fddZ2G dd de3Z4G dd dZ5zddl6m7Z7 e7j8e5_9e5j8e7_8W n e:y   e*;d  Y nw ee<e=e>e&ef Z?ee? Z@ee? ZAeeAe@f ZBG d!d" d"eZCdS )$u  Server-Sent Events response for Starlette / FastAPI.

Intentional divergence from ``starlette.responses.StreamingResponse``
--------------------------------------------------------------------

``EventSourceResponse`` is modelled on Starlette's ``StreamingResponse`` and
re-syncs most of its behaviour (WebSocket denial, ``collapse_excgroups()``
around the task group, ``memoryview`` chunk handling). The following points
are deliberate divergences — DO NOT "fix" them without reading the rationale:

1. ASGI ``spec_version >= 2.4`` fast path is NOT adopted.
   Upstream short-circuits to ``await stream_response(send)`` and converts
   ``OSError`` into ``ClientDisconnect``, skipping ``listen_for_disconnect``.
   We keep ``_listen_for_disconnect`` running because it
     (a) invokes ``client_close_handler_callable`` on disconnect,
     (b) flips ``self.active = False`` so ``_ping`` and the cooperative
         shutdown grace loop exit promptly.
   Adopting the upstream fast path would regress both features.

2. ``_wrap_websocket_denial_send`` is inlined in this module rather than
   inherited from ``starlette.responses.Response``. The helper landed on
   Starlette ``main`` after our minimum pin (``starlette>=0.41.3``); inline
   until the floor moves past the release that contains it.

3. ``collapse_excgroups()`` is vendored in ``sse_starlette._utils`` rather
   than imported from ``starlette._utils`` (private module).
    N)	dataclassfield)datetimetimezone)
AnyAsyncIterable	AwaitableCallable	CoroutineIteratorMappingOptionalSetUnion)BackgroundTask)iterate_in_threadpool)MutableHeaders)Response)ReceiveScopeSendMessage)collapse_excgroups)ServerSentEventensure_bytesc                   @   s6   e Zd ZU dZeedZeej	 e
d< dZee
d< dS )_ShutdownStatezPer-thread state for shutdown coordination.

    Issue #152 fix: Uses threading.local() instead of ContextVar to ensure
    one watcher per thread rather than one per async context.
    )default_factoryeventsFwatcher_startedN)__name__
__module____qualname____doc__r   setr   r   anyioEvent__annotations__r   bool r(   r(   D/home/kuhnn/.local/lib/python3.10/site-packages/sse_starlette/sse.pyr   >   s   
 r   returnc                  C   s$   t tdd} | du rt } | t_| S )z4Get or create shutdown state for the current thread.shutdown_stateN)getattr_thread_stater   r+   )stater(   r(   r)   _get_shutdown_stateN   s
   r/   c                  C   sN   zt t j} t| dr| j}t|dr|W S W dS W dS  ty&   Y dS w )am  
    Try to get uvicorn Server instance via signal handler introspection.

    When uvicorn registers signal handlers, they're bound methods on the Server instance.
    We can retrieve the Server from the handler's __self__ attribute.

    Returns None if:
    - Not running under uvicorn
    - Signal handler isn't a bound method
    - Any introspection fails
    __self__should_exitN)signal	getsignalSIGTERMhasattrr0   	Exception)handlerserverr(   r(   r)   _get_uvicorn_serverW   s   

r9   c                     st   t  } t }z.	 tjrntjr|dur|jrdt_n	tdI dH  q	t| jD ]}|	  q)W d| _
dS d| _
w )ag  
    Poll for shutdown and broadcast to all events in this context.

    One watcher runs per thread (event loop). Checks two shutdown sources:
    1. AppStatus.should_exit - set when our monkey-patch works
    2. uvicorn Server.should_exit - via signal handler introspection (Issue #132 fix)

    When either becomes True, signals all registered events.
    TNg      ?F)r/   r9   	AppStatusr1   enable_automatic_graceful_drainr$   sleeplistr   r#   r   )r.   uvicorn_servereventr(   r(   r)   _shutdown_watchern   s(   

r@   c                  C   sL   t  } | js$d| _zt }|t  W dS  ty#   d| _Y dS w dS )zDEnsure the shutdown watcher is running for this thread (event loop).TFN)r/   r   asyncioget_running_loopcreate_taskr@   RuntimeError)r.   loopr(   r(   r)   $_ensure_watcher_started_on_this_loop   s   rF   sendc                    s   dt ddf fdd}|S )a;  Mirror of ``starlette.responses.Response._wrap_websocket_denial_send``.

    Divergence #2 (see module docstring): inlined because the helper landed
    on Starlette ``main`` (commit 9ee9519) after our minimum pin
    ``starlette>=0.41.3``. Drop this once the floor moves past the release
    that contains it.
    messager*   Nc                    s8   | d }|dv ri | dd| i}  | I d H  d S )Ntype>   http.response.bodyhttp.response.startz
websocket.r(   )rH   message_typerG   r(   r)   wrapped   s
   z,_wrap_websocket_denial_send.<locals>.wrapped)r   )rG   rN   r(   rM   r)   _wrap_websocket_denial_send   s   	rO   c                   @   s   e Zd ZdS )SendTimeoutErrorN)r   r    r!   r(   r(   r(   r)   rP      s    rP   c                   @   sN   e Zd ZU dZdZdZdZee e	d< e
dd Ze
dd	 Ze
d
d ZdS )r:   z\Helper to capture a shutdown signal from Uvicorn so we can gracefully terminate SSE streams.FTNoriginal_handlerc                   C   
   dt _dS )aJ  
        Prevent automatic SSE stream termination on server shutdown.

        WARNING: When disabled, you MUST set AppStatus.should_exit = True
        at some point during shutdown, or streams will never close and the
        server will hang indefinitely (or until uvicorn's graceful shutdown
        timeout expires).
        FNr:   r;   r(   r(   r(   r)    disable_automatic_graceful_drain   s   

z*AppStatus.disable_automatic_graceful_drainc                   C   rR   )a  
        Re-enable automatic SSE stream termination on server shutdown.

        This restores the default behavior where SIGTERM triggers immediate
        stream draining. Call this to undo a previous call to
        disable_automatic_graceful_drain().
        TNrS   r(   r(   r(   r)   $enable_automatic_graceful_drain_mode   s   
	z.AppStatus.enable_automatic_graceful_drain_modec                  O   s.   t jrdt _t jd urt j| i | d S d S )NT)r:   r;   r1   rQ   )argskwargsr(   r(   r)   handle_exit   s
   
zAppStatus.handle_exit)r   r    r!   r"   r1   r;   rQ   r   r	   r&   staticmethodrT   rU   rX   r(   r(   r(   r)   r:      s   
 


r:   )ServerzHUvicorn not installed. Graceful shutdown on server termination disabled.c                   @   sz  e Zd ZdZdZdZ												d0ded	ed
ee	e
e
f  de
dee dee dee
 deeg ef  deeg ed f  dee deeeged f  deej deddfddZedeeef fddZejdeeef ddfddZd1deddfdd Zd!eddfd"d#Zd$eddfd%d&Zed2d'd(Z d2d)d*Z!d!eddfd+d,Z"d-e#d$ed!eddfd.d/Z$dS )3EventSourceResponseag  Streaming response implementing the SSE (Server-Sent Events) specification.

    Args:
        content: Async iterable or sync iterator yielding SSE event data.
        status_code: HTTP status code. Default: 200.
        headers: Additional HTTP headers.
        media_type: Response media type. Default: "text/event-stream".
        background: Background task to run after response completes.
        ping: Ping interval in seconds (0 to disable). Default: 15.
        sep: Line separator for SSE messages ("\r\n", "\r", or "\n").
        ping_message_factory: Callable returning custom ping ServerSentEvent.
        data_sender_callable: Async callable for push-based data sending.
        send_timeout: Timeout in seconds for individual send operations.
        client_close_handler_callable: Async callback on client disconnect.
        shutdown_event: Optional ``anyio.Event`` set by the library when server
            shutdown is detected. Generators can watch this event to send farewell
            messages and exit cooperatively instead of receiving CancelledError.
        shutdown_grace_period: Seconds to wait after setting ``shutdown_event``
            before force-cancelling the generator. Must be >= 0. Should be less
            than your ASGI server's graceful shutdown timeout. Default: 0
            (immediate cancel, identical to pre-v3.3.0 behavior).
       
   Ntext/event-streamr   contentstatus_codeheaders
media_type
backgroundpingsepping_message_factorydata_sender_callable)NNNsend_timeoutclient_close_handler_callableshutdown_eventshutdown_grace_periodr*   c                 C   s   |dvrt d| |p| j| _t|tr|| _nt|| _|| _|d u r)| jn|| _|| _	|	| _
|
| _t }|d urA|| |dd d|d< d|d< | | |d u r[| jn|| _|| _|| _|d	k rlt d
|| _|| _d| _t | _d S )N)Nr]   
z'sep must be one of: \r\n, \r, \n, got: zCache-Controlzno-storez
keep-alive
ConnectionnozX-Accel-Bufferingr   z"shutdown_grace_period must be >= 0T)
ValueErrorDEFAULT_SEPARATORrf   
isinstancer   body_iteratorr   ra   rc   rd   rh   ri   r   update
setdefaultinit_headersDEFAULT_PING_INTERVALping_intervalrg   rj   _shutdown_event_shutdown_grace_periodactiver$   Lock
_send_lock)selfr`   ra   rb   rc   rd   re   rf   rg   rh   ri   rj   rk   rl   _headersr(   r(   r)   __init__  s6   



zEventSourceResponse.__init__c                 C   s   | j S N)_ping_intervalr   r(   r(   r)   ry   O  s   z!EventSourceResponse.ping_intervalvaluec                 C   s0   t |ttfstd|dk rtd|| _d S )Nzping interval must be intr   z$ping interval must be greater than 0)rs   intfloat	TypeErrorrq   r   )r   r   r(   r(   r)   ry   S  s
   
Fforcec                 C   s   t d)Nz-Compression is not supported for SSE streams.)NotImplementedError)r   r   r(   r(   r)   enable_compression[  s   z&EventSourceResponse.enable_compressionrG   c              	      s  |d| j | jdI dH  | j2 zL3 dH W }t|| j}td| t| j	}|d|ddI dH  W d   n1 s?w   Y  |r]|j
r]t| jdd}|durZ| I dH  t q6 | j4 I dH  d	| _|dd
d	dI dH  W d  I dH  dS 1 I dH sw   Y  dS )zHSend out SSE data to the client as it becomes available in the iterator.rK   )rI   statusrb   Nz	chunk: %srJ   TrI   body	more_bodyacloseF    )ra   raw_headersrt   r   rf   loggerdebugr$   move_on_afterri   cancel_calledr,   rP   r~   r|   )r   rG   datachunkcancel_scoper   r(   r(   r)   _stream_response^  s4   


.z$EventSourceResponse._stream_responsereceivec                    sX   | j r*| I dH }|d dkr%d| _ td | jr#| |I dH  dS | j sdS dS )uq  Watch for a disconnect message from the client.

        Divergence #1 (see module docstring): kept unconditionally instead of
        adopting Starlette's ASGI 2.4 ``OSError → ClientDisconnect`` fast path,
        because this loop drives ``client_close_handler_callable`` and flips
        ``self.active = False`` for ``_ping`` and the shutdown grace loop.
        NrI   zhttp.disconnectFz+Got event: http.disconnect. Stop streaming.)r|   r   r   rj   )r   r   rH   r(   r(   r)   _listen_for_disconnectz  s   
z*EventSourceResponse._listen_for_disconnectc               	      st   t jrdS t  t } t }| j| zt jr#W | j| dS |	 I dH  W | j| dS | j| w )z0Wait for shutdown signal via the shared watcher.N)
r:   r1   rF   r/   r$   r%   r   adddiscardwait)r.   r?   r(   r(   r)   _listen_for_exit_signal  s   z+EventSourceResponse._listen_for_exit_signalc                    s   |   I dH  | jr| j  | jdkrDt| j | jr2tdI dH  | jsW d   dS W d   dS 1 s=w   Y  dS dS )a~  Wait for shutdown signal, then optionally give generator a grace period.

        Issue #167: When a shutdown_event is provided, the library sets it before
        returning, giving the generator a chance to send farewell events and exit
        cooperatively. The shutdown_grace_period controls how long to wait before
        force-cancelling via task group cancellation.
        Nr   g?)r   rz   r#   r{   r$   r   r|   r<   r   r(   r(   r)   "_listen_for_exit_signal_with_grace  s   

"z6EventSourceResponse._listen_for_exit_signal_with_gracec              	      s   | j r^t| jI dH  | jr|  ntdttj	 | j
d}t|| j
}td| | j4 I dH  | j rD|d|ddI dH  W d  I dH  n1 I dH sTw   Y  | j sdS dS )zPeriodically send ping messages to keep the connection alive on proxies.
        - frequenccy ca every 15 seconds.
        - Alternatively one can send periodically a comment line (one starting with a ':' character)
        Nzping - )commentrf   zping: %srJ   Tr   )r|   r$   r<   r   rg   r   r   nowr   utcrf   r   r   r   r~   )r   rG   sse_ping
ping_bytesr(   r(   r)   _ping  s,   

(zEventSourceResponse._pingscopec              
      s  |d dkrt t f t 4 I dH Kdtg td f ffdd}|fdd |fd	d |j jrMj | fd
d W d  I dH  n1 I dH shw   Y  W d   n1 sww   Y  j	dur	 I dH  dS dS )a  Entrypoint for Starlette's ASGI contract. We spin up tasks:
        - _stream_response to push events
        - _ping to keep the connection alive
        - _listen_for_exit_signal to respond to server shutdown
        - _listen_for_disconnect to respond to client disconnect
        rI   	websocketNcoroc                    s   |  I d H   j   d S r   )r   cancel)r   )
task_groupr(   r)   cancel_on_finish  s   z6EventSourceResponse.__call__.<locals>.cancel_on_finishc                      
     S r   )r   r(   r   rG   r(   r)   <lambda>     
 z.EventSourceResponse.__call__.<locals>.<lambda>c                      r   r   )r   r(   r   r(   r)   r     r   c                      s
     S r   )r   r(   )r   r   r(   r)   r     r   )
rO   r   r$   create_task_groupr	   r   
start_soonr   rh   rd   )r   r   r   rG   r   r(   )r   r   rG   r   r)   __call__  s0   
(
zEventSourceResponse.__call__)r^   Nr_   NNNNNNNNr   )Fr*   N)%r   r    r!   r"   rx   rr   ContentStreamr   r   r   strr   r	   r   r
   r   r   r   r$   r%   r   propertyr   ry   setterr'   r   r   r   r   r   rY   r   r   r   r   r   r(   r(   r(   r)   r[      sz    	

I
r[   r   )Dr"   rA   loggingr2   	threadingdataclassesr   r   r   r   typingr   r   r   r	   r
   r   r   r   r   r   r$   starlette.backgroundr   starlette.concurrencyr   starlette.datastructuresr   starlette.responsesr   starlette.typesr   r   r   r   sse_starlette._utilsr   sse_starlette.eventr   r   	getLoggerr   r   r   localr-   r/   r9   r@   rF   rO   TimeoutErrorrP   r:   uvicorn.mainrZ   rX   rQ   ImportErrorr   r   bytesdictContentSyncContentStreamAsyncContentStreamr   r[   r(   r(   r(   r)   <module>   sR    0
	

$&