
    *js                       U d Z ddlmZ ddlZddlZddlmZmZmZm	Z	m
Z
  ej                  e      ZdZded<    eh d      Zd	Z ed
dh      Z ej(                         Zdag Zded<   d,dZd-dZd.dZd/dZd0dZd1dZd2dZd3dZd4dZ d5dZ!d6dZ"d6dZ#d6dZ$ddd	 	 	 d7dZ%ddl&m'Z( ddl&m)Z*  G d  d!      Z+ G d" d#      Z,d8d$Z-d9d%Z.d:d&Z/d;d'Z0ddd(	 	 	 d<d)Z1ddd*	 	 	 d=d+Z2y)>u  OpenAI 키 슬롯 브로커 — 1차 키가 죽으면 보조 키로 전환한다 (2026-07-30).

## 왜 있는가

2026-07-29 23:25 `billing_hard_limit_reached` 로 OpenAI 호출이 전부 400 이
되어 콘티 재시도 → 최종 샷 체인이 **전면 중단**됐다. 복구는 사용자가 임시
키를 손으로 넘겨 러너 프로세스 환경변수로만 싣는 수동 조치였다. 단일 키가
장시간 무인 실행의 단일 장애점이라 슬롯을 둘로 늘린다.

## 계약

- **전환 트리거는 키 수준 실패만이다.** billing hard limit / quota 소진 /
  401·403 인증. 일반 4xx·5xx·타임아웃·단순 rate limit(429) 은 **건드리지
  않는다** — 거기서 키를 바꾸면 멀쩡한 보조 키를 태우고 진짜 원인을 가린다.
- **조용한 전환 금지.** 전환은 WARNING 으로 남긴다(어느 슬롯이 왜 죽었고
  어디로 넘어갔는지). 슬롯이 다 떨어지면 전환하지 않고 원래 예외를 올린다 —
  빈 결과로 흘려보내지 않는다.
- **키 값은 어디에도 기록하지 않는다.** 로그에 남기는 것은 슬롯 이름뿐이다.
- 전환은 **프로세스 전역**이고 되돌아가지 않는다. 죽은 키로 매 호출마다
  다시 찔러 보면 그 자체가 비용이고 지연이다. 새 프로세스는 다시 1차부터.

## 쓰는 쪽

- LiteLLM Router: `active_key()` 로 deployment 를 만들고, 전환이 일어나면
  `register_switch_hook` 로 등록한 콜백이 Router 를 무효화한다.
- 직접 client: `openai_client()` 가 돌려주는 프록시를 그대로 쓰면 된다.
  `client.images.edit(...)` / `client.responses.create(...)` 처럼 기존
  호출 형태가 그대로 유지되고, 키 수준 실패일 때만 다음 슬롯으로 1회
  재시도한다.
    )annotationsN)AnyCallableListOptionalTuple))primaryopenai_api_key)	secondaryopenai_api_key_secondaryzTuple[Tuple[str, str], ...]SLOT_FIELDS>   invalid_api_keybilling_not_activeinsufficient_quotaaccount_deactivatedorganization_restrictedcredit_balance_exhaustedbilling_hard_limit_reached)r   zbilling hard limitr   zexceeded your current quotazno credits remainingzincorrect api keyr   zaccount is not activei  i  zList[Callable[[], None]]_switch_hooksc                    ddl } ddlm} g }t        D ]C  \  }}t	        t        ||d      xs d      j                         }|s1|j                  ||f       E t	        | j                  j                  dd      xs d      j                         }|r*||D ch c]  \  }}|	 c}}vr|j                  d|f       |S c c}}w )u  설정된 (슬롯 이름, 키) 목록 — 빈 슬롯은 빠진다.

    ★환경변수 ``OPENAI_API_KEY`` 는 **최후 슬롯**으로 편입한다 (2026-08-01).
    이전에는 존재 판정만 환경변수를 인정하고 실제 키 조회는 인정하지 않아
    **약속과 값이 갈라졌다** — ``has_openai_key()`` 는 True 인데
    ``active_key()`` 가 ``""`` 라서 preflight 만 통과하고 호출은 빈 키로 나갔다
    (직접 재현: ``OpenAIClient._api_key == ''``). 실효 키의 SOT 는 하나여야
    한다. 설정 슬롯이 우선이고, 같은 키면 슬롯을 늘리지 않는다(헛 전환 방지).
    r   N)settings OPENAI_API_KEYenv)
osapp.core.configr   r   strgetattrstripappendenvironget)	r   r   outnamefieldkeyenv_key_ks	            J/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/openai_keys.pyavailable_slotsr+   V   s     (!#C"e'(E2.4"5;;=JJc{# # "**..!126<"=CCEG7"5A1"55

E7#$J #6s   B>c                     t               } | sy t        5  t        t        t	        |       dz
        }d d d        |    d   S # 1 sw Y   xY w)N   r   r+   _lockmin_active_indexlenslotsidxs     r*   active_slotr6   o   sB    E	-Ua0 
:a= 
   AA
c                     t               } | syt        5  t        t        t	        |       dz
        }ddd       |    d   S # 1 sw Y   xY w)uI   현재 활성 슬롯의 키. 슬롯이 하나도 없으면 빈 문자열.r   r-   Nr.   r3   s     r*   
active_keyr9   x   sB    E	-Ua0 
:a= 
r7   c                     t               } | syt        5  t        t        t	        |       dz
        }ddd       |    d   | |   d   fS # 1 sw Y   xY w)uA  활성 슬롯과 그 키를 **한 값으로** 읽는다.

    [2026-08-01 A5] 슬롯과 키를 따로 읽으면 그 사이에 일어난 전환이 둘을
    어긋나게 한다 — "primary 슬롯에 보조 키가 박힌 클라이언트"가 캐시에
    들어가면 그 뒤로는 영구히 잘못된 짝을 쓴다.
    )Nr   r-   Nr   r.   r3   s     r*   active_slot_and_keyr;      sQ     E	-Ua0 
:a=%*Q-'' 
s   A		Ac                 (    t        t                     S N)r2   r+        r*   
slot_countr@      s     !!r?   c                 >    t         5  daddd       y# 1 sw Y   yxY w)uJ   활성 슬롯을 1차로 되돌린다(테스트·명시 재기동 전용).r   N)r/   r1   r>   r?   r*   resetrB      s     
 
s   c                t    t         5  | t        vrt        j                  |        ddd       y# 1 sw Y   yxY w)uY   슬롯 전환 직후 호출될 콜백 — 캐시된 클라이언트·Router 무효화용.N)r/   r   r    )fns    r*   register_switch_hookrE      s%    	]"  $ 
s   .7c                    t        | dd      }|y	 |j                         }t        |t              r|S dS # t        $ r Y yw xY w)uK   예외에 실린 응답 본문(JSON) — 없거나 JSON 이 아니면 None.responseN)r   json	Exception
isinstancedict)excresppayloads      r*   _response_payloadrO      sR    3
D)D|))+ !$/79T9  s   6 	AAc                   g dfd} |t        | dd              |t        | dd             t        | dd      t        |       fD ]m  }t        |t              s|j	                  d      }t        |t              r|ni |fD ]0  } ||j	                  d              ||j	                  d             2 o t              S )u,  예외에서 OpenAI 오류 식별자를 전부 뽑는다 — `code` 와 `type` 둘 다.

    ★둘 다 봐야 한다. 2026-07-31 실측에서 크레딧 소진 응답은
    code=`credit_balance_exhausted` / type=`insufficient_quota` 로 왔고,
    이 모듈이 알던 이름은 **type 쪽에만** 있었다. `code` 하나만 읽던 옛
    구현은 그래서 키 수준 실패를 놓쳤다.

    본문 모양도 두 가지다 — `{"error": {...}}` 로 감싼 것과 평면 dict
    (`{"message":…, "type":…, "code":…}`). 둘 다 받는다.
    c                Z    t        | t              r| r| vrj                  |        y y y y r=   )rJ   r   r    )vr#   s    r*   _addz_error_codes.<locals>._add   s*    a!JJqM )5!r?   codeNtypebodyerror)rR   r   returnNone)r   rO   rJ   rK   r"   tuple)rL   rS   srcerrdr#   s        @r*   _error_codesr^      s     C 	fd	#$fd	#$VT*,=c,BC#t$ggg$S$/3R#>Avv ?	 D :r?   c                *    t        |       }|r|d   S dS )u>   가장 구체적인 식별자 하나(없으면 빈 문자열).r   r   )r^   )rL   codess     r*   _error_codera      s    E58$"$r?   c                8    t        d t        |       D              S )Nc              3  ,   K   | ]  }|t         v   y wr=   )_KEY_LEVEL_CODES).0cs     r*   	<genexpr>z&_has_key_level_code.<locals>.<genexpr>   s     @.?q$$.?s   )anyr^   )rL   s    r*   _has_key_level_coderi      s    @l3.?@@@r?   c                    t        | dd      }t        |t              r|r|j                         dv S t	        |       j
                  xs d}|j                  d      ryt        |       S )u   이 예외가 OpenAI 호출에서 났는가.

    Gemini 쪽 401 로 OpenAI 키를 갈아 끼우는 헛수고를 막는 가드다.
    litellm 예외는 `llm_provider` 를 실어 주고, openai SDK 예외는 모듈
    경로로 알 수 있다.
    llm_providerN)openaiazureopenai_liker   rl   T)r   rJ   r   lowerrU   
__module__
startswithri   )rL   providermodules      r*   _is_openai_errorrt      sa     sND1H(C X~~#EEE#Y!!'RF"s##r?   c                   t        |       syt        |       ryt        | dd      }|t        t        | dd      dd      }t        |t              r	|t
        v ryt        |       g}t        | dd      }t        |t              r|j                  |       dj                  |      j                         t        fdt        D              S )	u  키를 바꾸면 풀릴 수 있는 실패인가.

    ★단순 rate limit(429)은 **아니다** — 그건 기존 재시도·backoff 의 몫이다.
    quota 소진이 429 로 오는 경우는 코드(`insufficient_quota` /
    `credit_balance_exhausted`)나 메시지 문구로 잡는다. 그 둘은 재시도해도
    풀리지 않으므로 backoff 에 맡기면 실행 전체가 그대로 죽는다.
    FTstatus_codeNrG   rV   
c              3  &   K   | ]  }|v  
 y wr=   r>   )re   mtexts     r*   rg   z'is_key_level_failure.<locals>.<genexpr>  s     5"4QqDy"4s   )rt   ri   r   rJ   int_KEY_LEVEL_STATUSr   r    joinro   rh   _KEY_LEVEL_MARKERS)rL   statuspartsraw_bodyrz   s       @r*   is_key_level_failurer      s     C 3S-.F~j$7M&#6->#> XJEsFD)H(C X99U!!#D5"4555r?   r   whereattempted_slotc          
     P   t        |       syt               }|D cg c]  \  }}|	 }}}t        5  t        t        t        dt        |      dz
              }|||v r|j                  |      }n|}||kD  r-t        j                  d||r||   nd|xs d       	 ddd       y|dz   t        |      k\  rFt        j                  d	t        |      |r||   d   nd|xs dt        |       dd
        	 ddd       y|dz   a|t           d   }	||   d   }
t        t              }ddd       t        j                  d
	t        |       xs t        |       dd |xs d|	       D ]
  }	  |         yc c}}w # 1 sw Y   VxY w# t         $ r t        j#                  d       Y @w xY w)uq  키 수준 실패면 다음 슬롯으로 전환한다. 재시도할 슬롯이 있으면 True.

    False 를 돌려주면 호출자는 **원래 예외를 그대로 올려야 한다** — 여기서
    조용히 삼키면 실패가 사라진 것처럼 보인다.

    ``attempted_slot`` (2026-08-01 A5, Codex 코드 리뷰 BLOCKING 5): **이 호출이
    실제로 실패한 슬롯**. 전에는 전역 활성 index 만 보고 판단해서, 같은 슬롯을
    잡은 두 호출이 나란히 실패하면 먼저 도착한 쪽이 0→1 로 옮기고 뒤이어
    도착한 쪽은 이미 1이 된 전역을 읽어 1→2 로 넘기려 했다. 슬롯이 2개면 그
    자리에서 "소진"으로 판정되어 **보조 키를 한 번도 못 써 본 요청이 그대로
    버려진다**(4스레드 재현: 3건 유실). 전환은 "지금 전역이 어디인가"가 아니라
    "이 호출이 어디서 실패했는가"를 기준으로 판단해야 한다.

    다른 호출이 이미 옮겨 놓았으면 **또 옮기지 않고** True 만 돌려준다 —
    옮길 필요가 없을 뿐 재시도할 슬롯은 있기 때문이다.
    Fr   r-   Nu   OpenAI 키 전환 생략 — %s 는 이미 %s 로 옮겨져 있다 (지점 %s). 이 호출은 옮기지 않고 재시도한다.u   없음?Tu^   OpenAI 키 슬롯 소진 — 남은 대체 키 없음 (슬롯 %d개, 현재 %s, 지점 %s): %s   uf   OpenAI 키 전환: %s → %s (사유=%s · 지점=%s). 이 프로세스는 이후 %s 슬롯을 쓴다.x   u   OpenAI 키 전환 훅 실패)r   r+   r/   r0   r1   maxr2   indexloggerinforW   r   listr   warningra   rI   	exception)rL   r   r   r4   nr(   namescur	attemptednxtprevhookshooks                r*   failover_onr   	  s   (  $E !541aQ5E!	-QE
Q!78%.E*AN3I I?KKOec
5<C
  
  q=CJ&LL!E
5E),Q/hc#htn	 / 
0 "AM"1%Y"]#7 
8 NN	cK$6C##s
 	=F 
 O "	H  	=;<	=s0   E2A*E8$AE8;(E8(F8FF%$F%)
GRAIN_SLOT)record_sendc                  ,    e Zd ZdZdZddZddZd	dZy)

_BoundPathuO   `client.images.edit` 같은 중첩 접근을 호출 시점까지 미뤄 둔다._owner_pathc                     || _         || _        y r=   r   )selfownerpaths      r*   __init__z_BoundPath.__init__V  s    
r?   c                J    t        | j                  | j                  |fz         S r=   )r   r   r   r   r$   s     r*   __getattr__z_BoundPath.__getattr__Z  s    $++tzzTG';<<r?   c                P    | j                   j                  | j                  ||      S r=   )r   _invoker   )r   argskwargss      r*   __call__z_BoundPath.__call__]  s    {{""4::tV<<r?   N)r   z'FailoverOpenAIClient'r   Tuple[str, ...])r$   r   rX   z'_BoundPath')r   r   r   r   rX   r   )__name__rp   __qualname____doc__	__slots__r   r   r   r>   r?   r*   r   r   Q  s    Y#I==r?   r   c                  0    e Zd ZdZddZddZd	dZd
dZy)FailoverOpenAIClientu  OpenAI 클라이언트 얼굴을 그대로 쓰되, 키 수준 실패에만 슬롯을 옮긴다.

    사용법은 기존과 같다 — `client.images.edit(...)`,
    `client.responses.create(...)`. 그 외 동작은 원본 클라이언트에 위임한다.
    c                R    || _         i | _        t        j                         | _        y r=   )_client_kwargs_cache	threadingLock_cache_lock)r   client_kwargss     r*   r   zFailoverOpenAIClient.__init__h  s     +$>>+r?   c                   ddl m} t               \  }}|xs d}| j                  5  | j                  j                  |      }|||fcddd       S  |dd|i| j                  }|| j                  |<   ||fcddd       S # 1 sw Y   yxY w)u  (슬롯 이름, 그 슬롯 키로 만든 클라이언트) — 반드시 짝으로 돌려준다.

        [A5] 호출자가 나중에 `active_slot()` 을 다시 읽으면, 그사이 다른
        스레드가 옮긴 슬롯을 자기가 쓴 슬롯으로 착각한다.
        r   )OpenAInoneNapi_keyr>   )rl   r   r;   r   r   r"   r   )r   r   	slot_namer&   slothitclients          r*   _rawzFailoverOpenAIClient._rawm  s     	",.	3"F++//$'CSy  ?C?4+>+>?F &DKK< s   !B&BB
c                   ddl m} dj                  |      }t        t	        dt                           D ]  }| j                         \  }}|D ]  }	t        ||	      }  ||       t        |r
|d   dk(  rdndt        d	| t        |j                  d
      xs d            }
	  ||i |}|
|
j                          |c S  t!        d| d      # t        $ r/}|
|
j                  | d       t        |||      s Y d }~d }~ww xY w)Nr   reserve_current_research_call.r-   sourceimagesimagellmzopenai_keys.modelr   )kindgranularityr   r   z raisedr   u3   OpenAI 호출 실패 — 모든 키 슬롯 소진 ())app.core.research_call_budgetr   r}   ranger   r@   r   r   _record_send_GRAIN_SLOTr   r"   okrI   failedr   RuntimeError)r   r   r   r   r   r   r(   r   targetpart_send_outrL   s                r*   r   zFailoverOpenAIClient._invoke  s    Os1jl+,A99;LD& .  *7 !!%$q'X*=g5'%eW-&**W-34	Et.v. $HHJO -R PQVPWWXYZZ  $LLE7'!23"3eDI J	s   &C	D%DDc                T    |j                  d      rt        |      t        | |f      S )Nr(   )rq   AttributeErrorr   r   s     r*   r   z FailoverOpenAIClient.__getattr__  s(    ??3 &&$((r?   N)r   r   )rX   zTuple[str, Any])r   r   r   rZ   r   rK   rX   r   )r$   r   rX   r   )r   rp   r   r   r   r   r   r   r>   r?   r*   r   r   a  s    ,
 $.[`)r?   r   c                 <    | j                  dd       t        di | S )u   failover 가 붙은 OpenAI 클라이언트.

    `OpenAI(...)` 를 직접 만들던 자리를 이걸로 바꾸면 그 지점이 자동으로
    2슬롯을 쓴다. `api_key` 는 브로커가 정하므로 넘기지 않는다.
    r   Nr>   )popr   )r   s    r*   openai_clientr     s"     i&0-00r?   c                X    	 ddl }|j                  |       d   dk(  S # t        $ r Y yw xY w)u  litellm 자신에게 provider 를 묻는다 — 이름 규칙을 우리가 흉내 내지 않는다.

    판정할 수 없으면(가짜 litellm 주입 등) **키를 끼우지 않는다** — 모르는 채로
    OpenAI 키를 남의 provider 에 넘기는 것이 더 나쁘다.
    r   Nr-   rl   F)litellmget_llm_providerrI   )r   r   s     r*   _is_openai_modelr     s7    ''.q1X== s    	))c                   ddl }	 ddlm}  |        ddlm} d|  d}t        |       s ||        |j                  dd| i|S d}t        t        d	t                           D ]P  }t               \  }}	|	s ||        |j                  dd| i|c S 	  ||        |j                  d| |	d
|c S  |J |# t        $ r Y w xY w# t        $ r}
|
}t        |
d|  d|      s Y d}
~
d}
~
ww xY w)u  Router 를 거치지 않는 **직접 litellm 호출**의 유일한 경로.

    [2026-08-01] production 10개 파일이 ``litellm.completion(...)`` 을 직접
    불렀고(13 호출) 전부 기본 모델이 OpenAI 인데 **``api_key=`` 를 하나도 넘기지
    않았다.** 그러면 litellm 이 ``os.environ["OPENAI_API_KEY"]`` 로 키를 정하는데,
    이 브로커는 그 환경변수를 한 번도 건드리지 않는다 — 즉 보조 슬롯으로
    전환돼도 그 호출들은 계속 죽은 1차 키를 쓴다.

    환경변수 동기화만으로는 부족하다. 그 경로들은 ``failover_on`` 을 부르지 않아
    **전환이 일어날 계기 자체가 없다.** 그래서 여기서 둘 다 한다 — ①활성 슬롯의
    키를 명시 전달하고 ②키 수준 실패면 다음 슬롯으로 재시도한다.

    ★이 함수는 ``llm_client`` 가 아니라 브로커에 있다. 소비자들은 테스트에서
    **가짜 litellm 모듈을 sys.modules 에 주입**해 격리하는데, Router 를 최상단에서
    import 하는 모듈을 끌어오면 그 격리가 깨진다(실측: ImportError 로 34건 실패).
    여기서는 함수 안에서 lazy import 하므로 그 계약이 그대로 유지된다.

    OpenAI 가 아닌 모델에는 아무것도 끼우지 않는다. 그 경우 ``failover_on`` 이
    False 를 돌려주므로 첫 실패에서 그대로 올라간다 — 호출자의 기존
    ``num_retries=0`` 계약이 유지된다.
    r   N)
_init_opikr   zopenai_keys.llm_completion[]r   r   r-   )r   r   zlitellm.completion[r   r>   )r   app.modules.llm.llm_clientr   rI   r   r   r   
completionr   r   r@   r;   r   )r   r   r   r   r   _wherelastr(   r   r&   rL   s              r*   llm_completionr     s3   , 9 L*5'3FE"%V4!w!!8888$(D3q*,'( ()	c *8%7%%<E<V<<
	 *8%7%%IE3I&II )( 
JK  <  	D0q9$ 	s)   B? C?	C
C	C6C11C6c                 L    t        t               xs dj                               S )u  OpenAI 키가 **하나라도** 있는가 — 존재 판단의 단일 권위.

    [2026-08-01] 이 판단이 19곳에 흩어져 있었다. 8곳은 환경변수만 읽었고
    11곳은 ``settings.openai_api_key``(=**1차 슬롯 필드**)로 게이트했다. 그래서
    보조 슬롯만 구성된 상태에서 브로커는 ``active_slot=secondary`` 를 정확히
    보는데도 소비자가 **그 앞에서** "키 없음"으로 막았다(직접 재현).

    ★환경변수는 ``available_slots`` 이 최후 슬롯으로 편입하므로 여기서 따로
    보지 않는다. 따로 보면 "있다"고 답한 뒤 ``active_key()`` 가 빈 값을 주는
    분열이 생긴다(Codex 재리뷰 BLOCKING-1 실측).
    r   )boolr9   r   r>   r?   r*   has_openai_keyr     s     #**,--r?   )r   	fixed_keyc                   |r | |      S d}t        t        dt                           D ]  }t               \  }}	  | |      c S  |J |# t        $ r}|}t        |||      s Y d}~Ad}~ww xY w)u  ``fn(api_key)`` 를 활성 슬롯 키로 부르고, 키 수준 실패면 다음 슬롯으로.

    [2026-08-01 Codex 재리뷰 BLOCKING-2] raw urllib 로 OpenAI 를 직접 부르는
    소비자 5곳이 활성 키를 **한 번 복사**했을 뿐 실패를 브로커에 알리지 않았다.
    ``OpenAIClient`` 는 심지어 **생성 시점**에 복사해 이후 전환이 닿지도 않았다.
    그 경로가 그 프로세스의 첫 OpenAI 호출이면 보조 키가 있어도 전환 계기가
    없다(직접 재현: Authorization 이 ``Bearer KEY-A`` 한 번뿐, 슬롯 primary 유지).

    ``fixed_key`` 가 주어지면 **그 키로 한 번만** 부른다 — 호출자가 명시한 키는
    고정 계약이므로 우리가 다른 키로 바꿔 부르지 않는다.

    ``fn`` 이 올리는 예외는 브로커가 OpenAI 호출임을 알아볼 수 있어야 한다
    (``llm_provider`` 표시). urllib ``HTTPError`` 는 provider 를 싣지 않아
    일반 판정기가 남의 provider 실패와 구분하지 못한다.
    Nr-   r   )r   r   r@   r;   rI   r   )rD   r   r   r   r(   r   r&   rL   s           r*   call_with_key_failoverr   '  s    " )}$(D3q*,'(')	c	c7N ) 
J  	Ds%E F	s   A	A0A++A0)r   rV   c                   	 d| _         t        |t              r|| _        |r/ddl}	 |j                  |      }t        |t              r|n|| _        | S # t        $ r d}Y (w xY w# t        $ r Y | S w xY w)u  raw HTTP 예외를 브로커가 읽을 수 있는 모양으로 표시해 돌려준다.

    urllib 은 ``llm_provider`` 도 ``status_code`` 도 싣지 않는다. 그대로 두면
    ``_is_openai_error`` 가 "OpenAI 호출인지 모름"으로 판정해 **문구 마커까지
    가 보지도 못한다.** 표시만 붙이면 기존 판정 계약(코드·상태·문구)이 그대로
    적용된다 — 판정 규칙을 여기서 다시 쓰지 않는다.
    rl   r   N)	rk   rJ   r{   rv   rH   loadsrI   rK   rV   )rL   r   rV   _jsonparseds        r*   mark_openai_failurer   H  s    #fc"$CO
 !T* ",FD!9vtCH J    Js3   $A$ A A$ A!A$  A!!A$ $	A10A1)rX   zList[Tuple[str, str]])rX   Optional[str])rX   r   )rX   zTuple[Optional[str], str])rX   r{   )rX   rY   )rD   zCallable[[], None]rX   rY   )rL   BaseExceptionrX   zOptional[dict])rL   r   rX   r   )rL   r   rX   r   )rL   r   rX   r   )rL   r   r   r   r   r   rX   r   )r   r   rX   r   )r   r   rX   r   )r   r   r   r   )rX   r   )rD   zCallable[[str], Any]r   r   r   r   rX   r   )rL   r   r   r   rV   r   rX   r   )3r   
__future__r   loggingr   typingr   r   r   r   r   	getLoggerr   r   r   __annotations__	frozensetrd   r~   r|   RLockr/   r1   r   r+   r6   r9   r;   r@   rB   rE   rO   r^   ra   ri   rt   r   r   app.core.send_ledgerr   r   r   r   r   r   r   r   r   r   r   r   r>   r?   r*   <module>r     sX  < #   7 7			8	$,(       sCj) 	*,' ,2("%	::%A$"6> 5704> ->9=>H ; <= = Q) Q)h1EP. FH6:&3?BB >B$&!+8r?   