o
    ưik                     @   sP  d dl Z d dlZd dlZd dlm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Zd dlZd dlmZ d dlmZ d dlmZ erRd dl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$ d dl%m&Z& d dl'm(Z(m)Z)m*Z*m+Z+m,Z, d dl-m.Z.m/Z/ d dlm0Z0m1Z1 d dl2m3Z3m4Z4m5Z5m6Z6 G dd de#Z7dS )    N)asynccontextmanager)datetime)
TYPE_CHECKINGAnyAsyncGeneratorDictListLiteralOptionalTupleUnioncast)
get_secret)verbose_proxy_logger)GenericGuardrailAPIInputs)Logging)uuid)	DualCache)BlockedPiiEntityErrorGuardrailRaisedException)CustomGuardraillog_guardrail_information)UserAPIKeyAuth)GuardrailEventHooksLitellmParams	PiiActionPiiEntityTypePresidioPerRequestConfig)PresidioAnalyzeRequestPresidioAnalyzeResponseItem)GuardrailStatusStreamingChoices)EmbeddingResponseImageResponseModelResponseModelResponseStreamc                       s^  e Zd ZdZdZ												dVdedee dee dee dee ded	ee d
ee dee	e
eef ef  dee dee	e
eef ef  deee
eef   f fddZ		dWdee dee fddZedeejdf fddZdXddZdd ZdefddZdedee dedefdd Zdedee dede
ee e	f fd!d"Z	dYded#eded$e	eef dee	 defd%d&Z d#e
ee e	f de
ee e	f fd'd(Z!d#e
ee e	f fd)d*Z"dededee dedef
d+d,Z#d-e$d.e%d/ed0efd1d2Z&d3ed4ed0ede'eef fd5d6Z(d3ed4ed0ede'eef fd7d8Z)d/ed-e$d9e
e*e+e,f fd:d;Z-e.ded<e	eef defd=d>Z/d9e*ded?e0d@ de*fdAdBZ1d9e
e*e+e,f defdCdDZ2d-e$d9ededee3df fdEdFZ4d/edee fdGdHZ5dIdJ Z6e7	dYdKdLdedMe0dN dOedP ddLf
dQdRZ8dSe9ddf fdTdUZ:  Z;S )Z_OPTIONAL_PresidioPIIMaskingNFmock_testingmock_redacted_textpresidio_analyzer_api_basepresidio_anonymizer_api_baseoutput_parse_piiapply_to_outputpresidio_ad_hoc_recognizerslogging_onlypii_entities_configpresidio_languagepresidio_score_thresholdspresidio_entities_deny_listc              
      st  |du rd| _ tj |d< t jdi | d| _i | _|| _|p!d| _|| _|	p)i | _	|p.i | _
|p3g | _|
p8d| _d | _t | _t | _i | _|du rPd S |}|d urzt|d}t|| _W d    n1 smw   Y  W n= ty   td|  tjy } ztdt| d	| d }~w ty } ztd
t| d	| d }~ww | j||d d S )NTZ
event_hookZpresidioFenrzFile not found. file_path=zError decoding JSON file: z, file_path=zAn error occurred: )r)   r*    )r.   r   super__init__guardrail_provider
pii_tokensr(   r+   r,   r/   r1   r2   r0   _http_sessionasyncioLock_session_lock	threading	get_ident_main_thread_id_loop_sessionsopenjsonloadad_hoc_recognizersFileNotFoundError	ExceptionJSONDecodeErrorstrvalidate_environment)selfr'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r1   r2   kwargsrE   filee	__class__r5   h/home/app/Keep/.python/lib/python3.10/site-packages/litellm/proxy/guardrails/guardrail_hooks/presidio.pyr7   H   s^   





z%_OPTIONAL_PresidioPIIMasking.__init__c                 C   s   |pt dd | _|pt dd | _| jd u rtd| jds'|  jd7  _| jds9| jds9d| j | _| jd u rBtd| jdsO|  jd7  _| jdsc| jdsed| j | _d S d S d S )NZPRESIDIO_ANALYZER_API_BASEZPRESIDIO_ANONYMIZER_API_BASEz5Missing `PRESIDIO_ANALYZER_API_BASE` from environment/zhttp://zhttps://z7Missing `PRESIDIO_ANONYMIZER_API_BASE` from environment)r   r)   litellmr*   rG   endswith
startswith)rK   r)   r*   r5   r5   rQ   rJ      s:   





z1_OPTIONAL_PresidioPIIMasking.validate_environmentreturnc              	   C  s   t  }t | jkr>| j4 I dH  | jdu s| jjr"t	 | _| jV  W d  I dH  dS 1 I dH s7w   Y  dS || j
vsI| j
| jrPt	 | j
|< | j
| V  dS )ah  
        Async context manager for yielding an HTTP session.

        Logic:
        1. If running in the main thread (where the object was initialized/destined to live normally),
           use the shared `self._http_session` (protected by a lock).
        2. If running in a background thread (e.g. logging hook), use a cached session for that loop.
        N)r;   get_running_loopr>   r?   r@   r=   r:   closedaiohttpClientSessionrA   )rK   current_loopr5   r5   rQ   _get_session_iterator   s   

.
	
z2_OPTIONAL_PresidioPIIMasking._get_session_iteratorc                    s\   | j dur| j js| j  I dH  d| _ | j D ]}|js&| I dH  q| j  dS )zClose all cached HTTP sessions.N)r:   rX   closerA   valuesclear)rK   sessionr5   r5   rQ   _close_http_session   s   z0_OPTIONAL_PresidioPIIMasking._close_http_sessionc                 C   s   dS )zFCleanup: we try to close, but doing async cleanup in __del__ is risky.Nr5   rK   r5   r5   rQ   __del__   s   z$_OPTIONAL_PresidioPIIMasking.__del__c                 C   s"   | j sdS tdd | j  D S )zYReturn True if pii_entities_config has any BLOCK action (fail-closed on analyzer errors).Fc                 s   s    | ]}|t jkV  qd S N)r   BLOCK).0actionr5   r5   rQ   	<genexpr>   s    

zA_OPTIONAL_PresidioPIIMasking._has_block_action.<locals>.<genexpr>)r/   anyr^   rb   r5   r5   rQ   _has_block_action   s
   z._OPTIONAL_PresidioPIIMasking._has_block_actiontextpresidio_configrequest_datac                 C   st   t || jd}| jdur| j|d< | jrt| j |d< |r'|jr'|j|d< tt|}|	| j
|d tt |S )z
        Construct the payload for the Presidio analyze request

        API Ref: https://microsoft.github.io/presidio/api-docs/api-docs.html#tag/Analyzer/paths/~1analyze/post
        )rk   languageNrE   entitiesrn   )rm   )r   r0   rE   r/   listkeysrn   r   dictupdateZ)get_guardrail_dynamic_request_body_params)rK   rk   rl   rm   analyze_payloadZcasted_analyze_payloadr5   r5   rQ   %_get_presidio_analyze_request_payload   s   






zB_OPTIONAL_PresidioPIIMasking._get_presidio_analyze_request_payloadc                    s  z|rt | dkrtd g W S  jdur jW S   4 I dH } j d} j|||d}td|| dtdt	t
 f fd	d
}|j||ddid4 I dH }|jdkr| I dH }	|d|j d|	dd  W  d  I dH  W  d  I dH  W S t|d|jdd}
d|
vr| I dH }	|d|
 d|	dd  dW  d  I dH  W  d  I dH  W S | I dH }td| W d  I dH  n1 I dH sw   Y  t|trDd|v r|d|d W  d  I dH  W S td zt
d#i |gW W  d  I dH  W S  tyC } z|d| W  Y d}~W  d  I dH  W S d}~ww t|tsh|dt|j dt|dd  W  d  I dH  W S g }|D ]B}t|tstdt|jt|dd   qlz|t
d#i | W ql ty } ztd!|| W Y d}~qld}~ww |W  d  I dH  W S 1 I dH sw   Y  W dS  ty     ty } ztd"t|j |d}~ww )$zV
        Send text to the Presidio analyzer endpoint and get analysis results
        r   z9Skipping Presidio analysis for empty/whitespace-only textNZanalyzerk   rl   rm   z&Making request to: %s with payload: %sreasonrV   c                    s@   t  jp
 jp
 j}|rt jd|  ddtd|  g S )NzbPresidio analyzer returned invalid response; cannot verify PII when PII protection is configured: F)guardrail_namemessageZ should_wrap_with_default_messagez*Presidio analyzer %s, returning empty list)boolr/   r+   r,   r   rx   r   warning)rw   Zshould_fail_closedrb   r5   rQ   _fail_on_invalid_responseD  s   
zL_OPTIONAL_PresidioPIIMasking.analyze_text.<locals>._fail_on_invalid_responseAcceptapplication/jsonrC   headers  zHTTP z from Presidio analyzer:    content_typeContent-Type z5expected application/json Content-Type but received '
'; body: ''analyze_results: %serrorzerror: zGPresidio returned dict (not list), attempting to process as single itemzfailed to parse dict response: zunexpected type z$ (expected list or dict), response: zASkipping invalid Presidio result item (expected dict, got %s): %sd   z4Failed to parse Presidio result item: %s (error: %s)zPresidio PII analysis failed: r5   )lenstripr   debugr(   r\   r)   ru   rI   r   r   poststatusrk   getattrr   getrC   
isinstancerr   rG   rp   type__name__r{   appendr   )rK   rk   rl   rm   r`   Zanalyze_urlrt   r|   response
error_bodyr   analyze_resultsrN   Zfinal_resultsitemr5   rb   rQ   analyze_text  s   	

2=(
INN Z6mz)_OPTIONAL_PresidioPIIMasking.analyze_textr   masked_entity_countc              
      s  z,t |trt|dkr|W S |  4 I dH }| j d}td| ||d}|j||ddid4 I dH R}	|	jd	krU|		 I dH }
t
d
|	j d|
dd  t|	d|	jdd}d|vrz|		 I dH }
t
d| d|
dd  d|	 I dH }W d  I dH  n1 I dH sw   Y  W d  I dH  n1 I dH sw   Y  |}|dur+td| |d D ]k}|d }|d }|d }|d dkr|du r|du rtd i }d|vri |d< |d }| dtt dd  }||| ||< |d| | ||d  }|dd}|dur'||dd  ||< q|W S t
d! t
yU } zt|}d"|v sEd#|v rF t
d$t|j |d}~ww )%z`
        Send analysis results to the Presidio anonymizer endpoint to get redacted text
        r   NZ	anonymizezMaking request to: %s)rk   Zanalyzer_resultsr}   r~   r   r   z"Presidio anonymizer returned HTTP z: r   r   r   r   z4Presidio anonymizer returned non-JSON Content-Type 'r   r   zredacted_text: %sitemsstartendrk   operatorreplaceTu   Presidio anonymize_text called without request_data — PII tokens cannot be stored per-request. This may indicate a missing caller update.r9   _   entity_type   z*Invalid anonymizer response: received NonezInvalid anonymizer responsezPresidio anonymizer returnedz#Presidio PII anonymization failed: )r   rp   r   r\   r*   r   r   r   r   rk   rG   r   r   r   rC   r{   rI   r   uuid4r   r   )rK   rk   r   r+   r   rm   r`   Zanonymize_urlZanonymize_payloadr   r   r   redacted_textnew_textr   r   r   replacementr9   r   rN   Z	error_strr5   r5   rQ   anonymize_text  s   
*(#

	

z+_OPTIONAL_PresidioPIIMasking.anonymize_textc           	      C   s   | j s| js|S t|ts|S g }dd | jD }|D ]L}|d}t|dur-t|d|n|}|r7||v r7q| j rb|d}d}|durK| j |}|du rU| j d}|durb|du sa||k rbq|| q|S )z
        Drop detections that fall below configured per-entity score thresholds
        or match an entity type in the deny list.
        c                 S   s   g | ]
}t |d t|qS )value)r   rI   )rf   xr5   r5   rQ   
<listcomp>"  s    zP_OPTIONAL_PresidioPIIMasking.filter_analyze_results_by_score.<locals>.<listcomp>r   Nr   scoreALL)r1   r2   r   rp   r   rI   r   r   )	rK   r   Zfiltered_resultsZdeny_list_stringsr   r   Zstr_entity_typer   	thresholdr5   r5   rQ   filter_analyze_results_by_score  s:   


z<_OPTIONAL_PresidioPIIMasking.filter_analyze_results_by_scorec                 C   s`   | j du rdS t|trdS |D ]}|d}|r-|| j v r-| j | tjkr-t|| jdqdS )zE
        Raise an exception if blocked entities are detected
        Nr   )r   rx   )r/   r   r   r   r   re   r   rx   )rK   r   resultr   r5   r5   rQ   ,raise_exception_if_blocked_entities_detectedA  s   



zI_OPTIONAL_PresidioPIIMasking.raise_exception_if_blocked_entities_detectedc                    s  t  }d}d}i }d}	zz| jdur| j}
n]| j|||dI dH }td| | j|d}| j|d | j|||||dI dH }|W W i }|dkrXt	|t
rWdd	 |D }n|	}| j| j|||| t   t  |  |d
 S |
d W W i }|dkrt	|t
rdd	 |D }n|	}| j| j|||| t   t  |  |d
 S  ty } zd}t|}	|d}~ww i }|dkrt	|t
rdd	 |D }n|	}| j| j|||| t   t  |  |d
 w )zY
        Calls Presidio Analyze + Anonymize endpoints for PII Analysis + Masking
        Nsuccessr   rv   r   )r   )rk   r   r+   r   rm   c                 S   s   g | ]}t |qS r5   )rr   )rf   r   r5   r5   rQ   r     s    z:_OPTIONAL_PresidioPIIMasking.check_pii.<locals>.<listcomp>)r8   guardrail_json_responserm   Zguardrail_status
start_timeend_timedurationr   rk   Zguardrail_failed_to_respond)r   nowr(   r   r   r   r   r   r   r   r   Z:add_standard_logging_guardrail_information_to_request_datar8   	timestamptotal_secondsrG   rI   )rK   rk   r+   rl   rm   r   r   r   r   Zexception_strr   Zanonymized_textr   rN   r5   r5   rQ   	check_pii]  s   





	



z&_OPTIONAL_PresidioPIIMasking.check_piiuser_api_key_dictcachedata	call_typec              
      s  z| dd}td| | |}| dd}|du r |W S g }g }	t|D ]X\}
}| dd}|du r7q(t|trQ|| j|| j	||d |	|
df q(t|t
rt|D ]%\}}| dd}|du riqZ|| j|| j	||d |	|
t|f qZq(tj| I dH }t|D ]H\}}|	| }tt|d }
ttt |d	 }||
  dd}|du rqt|tr|du r|||
 d< qt|t
r|dur|||
 d | d< qtd
|d   ||d< |W S  ty } z|d}~ww )a  
        - Check if request turned off pii
            - Check if user allowed to turn off pii (key permissions -> 'allow_pii_controls')

        - Take the request data
        - Call /analyze -> get the results
        - Call /anonymize w/ the analyze results -> get the redacted text

        For multiple messages in /chat/completions, we'll need to call them in parallel.
        content_safetyNzcontent_safety: %smessagescontentrk   r+   rl   rm   rk   r   r   ,Presidio PII Masking: Redacted pii message: )r   r   r   'get_presidio_settings_from_request_data	enumerater   rI   r   r   r+   rp   intr;   gatherr   r
   rG   )rK   r   r   r   r   r   rl   r   taskstask_mappingsmsg_idxmr   content_idxctext_str	responsestask_idxr4   mappingcontent_idx_optionalrN   r5   r5   rQ   async_pre_call_hook  s   


z0_OPTIONAL_PresidioPIIMasking.async_pre_call_hookrL   r   c           	         s   ddl m}  fdd}z&t }|dd}||}| W  d    W S 1 s.w   Y  W d S  tyA   |  Y S w )Nr   )ThreadPoolExecutorc               	      sR   t  } zt |  | j dW |   t d S |   t d w )z9Run the coroutine in a new event loop within this thread.)rL   r   r   N)r;   new_event_loopset_event_looprun_until_completeasync_logging_hookr]   )Znew_loopr   rL   r   rK   r5   rQ   run_in_new_loop  s   
zB_OPTIONAL_PresidioPIIMasking.logging_hook.<locals>.run_in_new_loopr   )max_workers)concurrent.futuresr   r;   rW   submitr   RuntimeError)	rK   rL   r   r   r   r   r   executorfuturer5   r   rQ   logging_hook  s   
(
z)_OPTIONAL_PresidioPIIMasking.logging_hookc              
      s  |dks	|dkr| dd}g }g }|du r||fS | |}t|D ]V\}}	|	 dd}
|
du r3q$t|
trL|| j|
d||d ||df q$t|
trzt|
D ]$\}}| dd}|du rdqU|| j|d||d ||t|f qUq$t	j
| I dH }t|D ]H\}}|| }tt|d	 }ttt |d
 }||  dd}
|
du rqt|
tr|du r||| d< qt|
tr|dur||| d | d< qtd|  ||d< ||fS )zK
        Masks the input before logging to langfuse, datadog, etc.
        
completionZacompletionr   Nr   Fr   rk   r   r   r   )r   r   r   r   rI   r   r   rp   r   r;   r   r   r
   r   r   )rK   rL   r   r   r   r   r   rl   r   r   r   r   r   r   r   r   r4   r   r   r5   r5   rQ   r     sz   


z/_OPTIONAL_PresidioPIIMasking.async_logging_hookr   c                    s   t d| j dt|  | jdu r| j||dI dH S | jdu r*tjdu r*|S t|trBt|j	d t
sB| j||dd	I dH  |S )
ze
        Output parse the response object to replace the masked tokens with user sent values
        z(PII Masking Args: self.output_parse_pii=z; type of response=T)r   rm   NFr   unmaskr   rm   mode)r   r   r+   r   r,   _mask_output_responserS   r   r$   choicesr!   _process_response_for_pii)rK   r   r   r   r5   r5   rQ   async_post_call_success_hooki  s&   	

z9_OPTIONAL_PresidioPIIMasking.async_post_call_success_hookr9   c                 C   s   |  D ]F\}}|| v r| ||} qtdt|d }ttdt| t| t| D ]}| |d }||rIt||krI| d| | }  nq,q| S )a  
        Replace PII tokens in *text* with their original values.

        Includes a fallback for tokens that were truncated by ``max_tokens``:
        if the *end* of ``text`` matches the *beginning* of a token and the
        overlap is long enough, the truncated suffix is replaced with the
        original value.  The minimum overlap length is
        ``min(20, len(token) // 2)`` to reduce the risk of false positives
        when multiple tokens share a common prefix.
              r   N)r   r   minr   rangemaxrU   )rk   r9   tokenZoriginal_textZmin_overlapisubr5   r5   rQ   _unmask_pii_text  s   $z-_OPTIONAL_PresidioPIIMasking._unmask_pii_textr   )maskr   c                    s  |r	| di ni }|s|dkrtd | |pi }|jD ]}t|dd}|du r-q t|dd}t|trV|dkrD| |||_	nK|dkrU| j
|d||d	I dH |_	n9t|tr|D ]1}	t|	tseq]|	 d
}
|
du roq]|dkr|| |
||	d
< q]|dkr| j
|
d||d	I dH |	d
< q]t|dd}|r|D ]4}t|dd}|rt|dr|j}t|tr|dkr| |||_q|dkr| j
|d||d	I dH |_qt|dd}|rt|dr|j}t|tr|dkr| |||_q |dkr| j
|d||d	I dH |_q |S )zt
        Helper to recursively process a ModelResponse for PII.
        Handles all choices and tool calls.
        r9   r   u9   No pii_tokens found in request_data — nothing to unmaskry   Nr   r   Fr   rk   
tool_callsfunction	argumentsfunction_call)r   r   r   r   r   r   r   rI   r   r   r   rp   rr   hasattrr   )rK   r   rm   r   r9   rl   choicery   r   r   Z
text_valuer   Z	tool_callr   argsr   r5   r5   rQ   r     s   







z6_OPTIONAL_PresidioPIIMasking._process_response_for_piic                    s8   t |ts|S t |tr|S | j||ddI dH  |S )zL
        Apply Presidio masking on model responses (non-streaming).
        r   r   N)r   r$   r%   r   )rK   r   rm   r5   r5   rQ   r     s   

z2_OPTIONAL_PresidioPIIMasking._mask_output_responsec              
   C  sn  ddl m} ddlm} ddlm} | jrg }zG|2 z3 dH W }t|tr+|	| q6 |s2W dS |||
dd}	t|	|sK|D ]}|V  qBW dS | j|	|dd	I dH  ||	}
|
V  W dS  ty } ztd
t|  |D ]}|V  qsW Y d}~dS d}~ww |r|
di ni }|s|rtd | jr|s|2 z	3 dH W }|V  q6 dS g }za|2 z3 dH W }t|tr|	| q6 |sW dS |||
dd}	t|	|s|D ]}|V  qW dS t|	dds|r|d }t|dd}|rt|	d| | j|	|dd	I dH  ||	}
|
V  W dS  ty6 } ztdt|  |D ]}|V  q$W Y d}~dS d}~ww )zU
        Process streaming response chunks to unmask PII tokens when needed.
        r   )#convert_model_response_to_streaming)stream_chunk_builder)r$   Nr   )chunksr   r   r   z$Error masking streaming PII output: r9   z7No pii_tokens in request_data for streaming unmask pathusager   z#Error in PII streaming processing: )Z)litellm.llms.base_llm.base_model_iteratorr   Zlitellm.mainr   litellm.types.utilsr$   r,   r   r%   r   r   r   rG   r   r   rI   r   r+   r   setattr)rK   r   r   rm   r   r   r$   Z
all_chunkschunkZassembled_model_responseZmock_response_streamrN   r9   Zremaining_chunks
last_chunkZlast_chunk_usager5   r5   rQ   'async_post_call_streaming_iterator_hook  s   	










zD_OPTIONAL_PresidioPIIMasking.async_post_call_streaming_iterator_hookc                 C   sD   d|v r | dd }|d u rd S | d}|r tdi |}|S d S )NmetadataZguardrail_configr5   )r   r   )rK   r   	_metadataZ_guardrail_configZ_presidio_configr5   r5   rQ   r     s   
zD_OPTIONAL_PresidioPIIMasking.get_presidio_settings_from_request_datac                 C   s:   zt | tjrt| W d S W d S  ty   Y d S w rd   )r   r   rS   Zset_verboseprintrG   )rK   Zprint_statementr5   r5   rQ   print_verbose  s   
z*_OPTIONAL_PresidioPIIMasking.print_verboseinputsr   
input_type)requestr   logging_objLiteLLMLoggingObjc           	         sP   | dg }g }|D ]}| j|| jd|pi dI dH }|| q||d< |S )a  
        UI will call this function to check:
            1. If the connection to the guardrail is working
            2. When Testing the guardrail with some text, this function will be called with the input text and returns a text after applying the guardrail
        textsNr   )r   r   r+   r   )	rK   r
  rm   r  r  r  Z	new_textsrk   Zmodified_textr5   r5   rQ   apply_guardrail  s   z,_OPTIONAL_PresidioPIIMasking.apply_guardraillitellm_paramsc                    s>   t  | |jr|j| _|jr|j| _|jr|j| _dS dS )z@
        Update the guardrails litellm params in memory
        N)r6   update_in_memory_litellm_paramsr/   r1   r2   )rK   r  rO   r5   rQ   r    s   z<_OPTIONAL_PresidioPIIMasking.update_in_memory_litellm_params)FNNNFFNNNNNN)NN)rV   Nrd   )<r   
__module____qualname__Zuser_api_key_cacherE   rz   r
   rr   rI   r   r   r   r   floatr   r7   rJ   r   r   rY   rZ   r\   ra   rc   rj   r   r   ru   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r$   r"   r#   r   staticmethodr   r	   r   r   r%   r  r   r	  r   r  r   r  __classcell__r5   r5   rO   rQ   r&   C   sz   	
O
*

&
 

m
-

K
V

 

K
 
[


r
r&   )8r;   rC   r>   
contextlibr   r   typingr   r   r   r   r   r	   r
   r   r   r   rY   rS   r   Zlitellm._loggingr   r  r   Z*litellm.litellm_core_utils.litellm_loggingr   r  Zlitellm._uuidr   Zlitellm.caching.cachingr   Zlitellm.exceptionsr   r   Z%litellm.integrations.custom_guardrailr   r   Zlitellm.proxy._typesr   Zlitellm.types.guardrailsr   r   r   r   r   Z7litellm.types.proxy.guardrails.guardrail_hooks.presidior   r   r    r!   Zlitellm.utilsr"   r#   r$   r%   r&   r5   r5   r5   rQ   <module>   s.   
0