whatap-python 2.1.0__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
Binary file
whatap/build.py CHANGED
@@ -1,4 +1,4 @@
1
1
  app = 'Python'
2
2
  name = 'whatap-python'
3
- version = '2.1.0'
4
- release_date = '20260610'
3
+ version = '2.1.1'
4
+ release_date = '20260622'
@@ -3,68 +3,84 @@ from anthropic import APIError
3
3
 
4
4
  from whatap.llm.providers.interceptor import (
5
5
  before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
- capture_client,
6
+ capture_client, extract_response, _safe, _clear_httpc_pending,
7
7
  )
8
- from whatap.llm.providers.stream_accumulator import sync_stream, async_stream
8
+ from whatap.llm.providers.stream_accumulator import wrap_sync_stream, wrap_async_stream
9
9
  from whatap.llm.providers.anthropic.messages.messages_context import build_context
10
10
  from whatap.llm.providers.anthropic.messages.messages_extractor import finalize, AnthropicStream
11
11
 
12
12
 
13
13
  def intercept_create(fn, *args, **kwargs):
14
- """Anthropic Messages 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
15
- pack, ctx, features, stream = build_context(kwargs)
16
- capture_client(pack, ctx, args)
17
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
14
+ """Anthropic Messages 동기 호출을 인터셉트한다. 계측 실패는 사용자 호출로 전파되지 않는다."""
15
+ pack = active_key = None
16
+ try:
17
+ pack, ctx, features, stream = build_context(kwargs)
18
+ capture_client(pack, ctx, args)
19
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
20
+ before_call(pack, active_key)
21
+ except Exception:
22
+ if pack is not None and active_key is not None:
23
+ _safe(_ensure_end, pack, active_key)
24
+ return fn(*args, **kwargs)
18
25
 
19
- before_call(pack, active_key)
20
26
  _stream_returned = False
21
27
  try:
22
28
  try:
23
29
  response = fn(*args, **kwargs)
24
30
  except Exception as err:
25
- handle_error(pack, err, active_key, APIError)
31
+ _safe(handle_error, pack, err, active_key, APIError)
26
32
  raise
27
33
  finally:
28
- if ctx:
29
- ctx._llm_httpc_pending = False
34
+ _safe(_clear_httpc_pending, ctx)
30
35
 
31
- after_call(pack, ctx)
32
- if stream:
33
- _stream_returned = True
34
- return sync_stream(response, AnthropicStream(pack, active_key, features))
35
- finalize(response, pack, features)
36
- finalize_non_streaming(pack, active_key)
36
+ try:
37
+ after_call(pack, ctx)
38
+ if stream:
39
+ result, _stream_returned = wrap_sync_stream(response, AnthropicStream(pack, active_key, features))
40
+ return result
41
+ extract_response(response, finalize, pack, features)
42
+ finalize_non_streaming(pack, active_key)
43
+ except Exception:
44
+ pass
37
45
  return response
38
46
  finally:
39
47
  if not _stream_returned:
40
- _ensure_end(pack, active_key)
48
+ _safe(_ensure_end, pack, active_key)
41
49
 
42
50
 
43
51
  async def async_intercept_create(fn, *args, **kwargs):
44
- """Anthropic Messages 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
45
- pack, ctx, features, stream = build_context(kwargs)
46
- capture_client(pack, ctx, args)
47
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
52
+ """Anthropic Messages 비동기 호출을 인터셉트한다. 계측 실패는 사용자 호출로 전파되지 않는다."""
53
+ pack = active_key = None
54
+ try:
55
+ pack, ctx, features, stream = build_context(kwargs)
56
+ capture_client(pack, ctx, args)
57
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
58
+ before_call(pack, active_key)
59
+ except Exception:
60
+ if pack is not None and active_key is not None:
61
+ _safe(_ensure_end, pack, active_key)
62
+ return await fn(*args, **kwargs)
48
63
 
49
- before_call(pack, active_key)
50
64
  _stream_returned = False
51
65
  try:
52
66
  try:
53
67
  response = await fn(*args, **kwargs)
54
68
  except Exception as err:
55
- handle_error(pack, err, active_key, APIError)
69
+ _safe(handle_error, pack, err, active_key, APIError)
56
70
  raise
57
71
  finally:
58
- if ctx:
59
- ctx._llm_httpc_pending = False
72
+ _safe(_clear_httpc_pending, ctx)
60
73
 
61
- after_call(pack, ctx)
62
- if stream:
63
- _stream_returned = True
64
- return async_stream(response, AnthropicStream(pack, active_key, features))
65
- finalize(response, pack, features)
66
- finalize_non_streaming(pack, active_key)
74
+ try:
75
+ after_call(pack, ctx)
76
+ if stream:
77
+ result, _stream_returned = wrap_async_stream(response, AnthropicStream(pack, active_key, features))
78
+ return result
79
+ extract_response(response, finalize, pack, features)
80
+ finalize_non_streaming(pack, active_key)
81
+ except Exception:
82
+ pass
67
83
  return response
68
84
  finally:
69
85
  if not _stream_returned:
70
- _ensure_end(pack, active_key)
86
+ _safe(_ensure_end, pack, active_key)
@@ -84,6 +84,7 @@ class AnthropicStream(StreamAccumulator):
84
84
  self.block_type = getattr(block, 'type', None)
85
85
  self.block_name = getattr(block, 'name', None)
86
86
  if self.block_type == 'tool_use':
87
+ self.on_first_token()
87
88
  tag = (LlmFeature.COMPUTER_USE
88
89
  if self.block_name and 'computer' in self.block_name
89
90
  else LlmFeature.TOOL_USE)
@@ -92,7 +93,10 @@ class AnthropicStream(StreamAccumulator):
92
93
  elif t == 'content_block_delta':
93
94
  delta = getattr(event, 'delta', None)
94
95
  if self.block_type == 'thinking':
95
- self.reasoning += getattr(delta, 'thinking', '') or ''
96
+ thinking = getattr(delta, 'thinking', '') or ''
97
+ if thinking:
98
+ self.on_first_token()
99
+ self.reasoning += thinking
96
100
  elif self.block_type == 'text':
97
101
  text = getattr(delta, 'text', '') or ''
98
102
  if text:
@@ -6,6 +6,7 @@ API 호출 전후 처리 흐름:
6
6
  """
7
7
  import time
8
8
 
9
+ from whatap import logging
9
10
  from whatap.counter.tasks.llm_log_sink_task import dispatch_llm_pack
10
11
 
11
12
 
@@ -93,6 +94,25 @@ def _active_stat():
93
94
  return LlmStatTask.get_stat('ActiveStat')
94
95
 
95
96
 
97
+ def _safe(fn, *args, **kwargs):
98
+ """계측 보조 단계를 안전하게 실행한다 — 예외를 흡수(디버그 로깅)해 사용자 호출을 보호.
99
+
100
+ 계측은 어떤 경우에도 사용자 애플리케이션을 깨면 안 된다. 인터셉트 라이프사이클의
101
+ 모든 보조 호출(before/after/handle_error/_ensure_end 등)을 이걸로 감싼다.
102
+ """
103
+ try:
104
+ return fn(*args, **kwargs)
105
+ except Exception as e:
106
+ logging.debug('[LLM] instrumentation step skipped: %s' % e, extra={'id': 'LLM008'})
107
+ return None
108
+
109
+
110
+ def _clear_httpc_pending(ctx):
111
+ """fn() 직후 httpc pending 플래그 해제(있을 때만)."""
112
+ if ctx is not None:
113
+ ctx._llm_httpc_pending = False
114
+
115
+
96
116
  def before_call(pack, active_key):
97
117
  """API 호출 전: active 카운터 증가 + 시작 시간 기록 + 순차 인덱스 할당."""
98
118
  pack._active_ended = False
@@ -174,9 +194,39 @@ def finalize_non_streaming(pack, active_key):
174
194
  _ensure_end(pack, active_key)
175
195
 
176
196
 
197
+ def extract_response(response, finalize_fn, pack, *finalize_args):
198
+ """비스트리밍 응답을 계측한다. 어떤 예외도 사용자 호출로 전파시키지 않는다.
199
+
200
+ 계측은 사용자 애플리케이션을 절대 깨면 안 된다. finalize_fn 은 응답 구조
201
+ (``.choices`` / ``.content`` / ``.output`` 등) 를 단정하므로, 예상 밖 응답
202
+ (예: litellm/langchain 이 ``with_raw_response`` 로 받는 ``LegacyAPIResponse``)
203
+ 이 와도 여기서 흡수하고 계측만 생략한다.
204
+
205
+ 또한 응답이 ``parse()`` 를 가진 raw 래퍼면 parse() 로 실제 응답을 꺼내 계측한다.
206
+ parse() 결과는 캐시되어 호출측(litellm 등)의 후속 parse() 와 공유된다.
207
+ """
208
+ try:
209
+ target = response
210
+ parse = getattr(response, "parse", None)
211
+ if callable(parse):
212
+ try:
213
+ target = parse()
214
+ except Exception:
215
+ target = response
216
+ finalize_fn(target, pack, *finalize_args)
217
+ except Exception as e:
218
+ logging.debug('[LLM] response extract skipped: %s' % e, extra={'id': 'LLM005'})
219
+
220
+
177
221
  def _dispatch(pack):
178
- """로그싱크팩 전송 + 메트릭 stat 업데이트 통합 호출."""
179
- dispatch_llm_pack(pack)
180
- inst = _stat_task()
181
- if inst:
182
- inst.notify(pack)
222
+ """로그싱크팩 전송 + 메트릭 stat 업데이트 통합 호출. 송출 실패는 사용자에게 전파 안 함."""
223
+ try:
224
+ dispatch_llm_pack(pack)
225
+ except Exception as e:
226
+ logging.debug('[LLM] dispatch failed: %s' % e, extra={'id': 'LLM006'})
227
+ try:
228
+ inst = _stat_task()
229
+ if inst:
230
+ inst.notify(pack)
231
+ except Exception as e:
232
+ logging.debug('[LLM] stat notify failed: %s' % e, extra={'id': 'LLM007'})
@@ -3,80 +3,101 @@ from openai import OpenAIError
3
3
 
4
4
  from whatap.llm.providers.interceptor import (
5
5
  before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
- capture_client,
6
+ capture_client, extract_response, _safe, _clear_httpc_pending,
7
7
  )
8
- from whatap.llm.providers.stream_accumulator import sync_stream, async_stream
8
+ from whatap.llm.providers.stream_accumulator import wrap_sync_stream, wrap_async_stream
9
9
  from whatap.llm.providers.openai.chat.chat_context import build_context
10
10
  from whatap.llm.providers.openai.chat.chat_extractor import finalize, ChatStream
11
11
 
12
12
 
13
13
  def intercept_create(fn, *args, **kwargs):
14
- """OpenAI Chat Completions 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
15
- pack, ctx, features, stream = build_context(kwargs)
16
- capture_client(pack, ctx, args)
17
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
14
+ """OpenAI Chat Completions 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다.
18
15
 
19
- if stream:
20
- opts = dict(kwargs.get("stream_options") or {})
21
- if not opts.get("include_usage"):
22
- opts["include_usage"] = True
23
- kwargs["stream_options"] = opts
16
+ 계측 어느 단계가 실패해도 사용자 호출은 보호된다 — 계측 예외는 전파하지 않고
17
+ 사용자 fn 예외만 전파한다.
18
+ """
19
+ pack = active_key = None
20
+ try:
21
+ pack, ctx, features, stream = build_context(kwargs)
22
+ capture_client(pack, ctx, args)
23
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
24
+ if stream:
25
+ opts = dict(kwargs.get("stream_options") or {})
26
+ if not opts.get("include_usage"):
27
+ opts["include_usage"] = True
28
+ kwargs["stream_options"] = opts
29
+ before_call(pack, active_key)
30
+ except Exception:
31
+ if pack is not None and active_key is not None:
32
+ _safe(_ensure_end, pack, active_key)
33
+ return fn(*args, **kwargs)
24
34
 
25
- before_call(pack, active_key)
26
35
  _stream_returned = False
27
36
  try:
28
37
  try:
29
38
  response = fn(*args, **kwargs)
30
39
  except Exception as err:
31
- handle_error(pack, err, active_key, OpenAIError)
40
+ _safe(handle_error, pack, err, active_key, OpenAIError)
32
41
  raise
33
42
  finally:
34
- if ctx:
35
- ctx._llm_httpc_pending = False
43
+ _safe(_clear_httpc_pending, ctx)
36
44
 
37
- after_call(pack, ctx)
38
- if stream:
39
- _stream_returned = True
40
- return sync_stream(response, ChatStream(pack, active_key))
41
- finalize(response, pack, features)
42
- finalize_non_streaming(pack, active_key)
45
+ try:
46
+ after_call(pack, ctx)
47
+ if stream:
48
+ result, _stream_returned = wrap_sync_stream(response, ChatStream(pack, active_key))
49
+ return result
50
+ extract_response(response, finalize, pack, features)
51
+ finalize_non_streaming(pack, active_key)
52
+ except Exception:
53
+ pass
43
54
  return response
44
55
  finally:
45
56
  if not _stream_returned:
46
- _ensure_end(pack, active_key)
57
+ _safe(_ensure_end, pack, active_key)
47
58
 
48
59
 
49
60
  async def intercept_create_async(fn, *args, **kwargs):
50
- """OpenAI Chat Completions 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
51
- pack, ctx, features, stream = build_context(kwargs)
52
- capture_client(pack, ctx, args)
53
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
61
+ """OpenAI Chat Completions 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다.
54
62
 
55
- if stream:
56
- opts = dict(kwargs.get("stream_options") or {})
57
- if not opts.get("include_usage"):
58
- opts["include_usage"] = True
59
- kwargs["stream_options"] = opts
63
+ 계측 어느 단계가 실패해도 사용자 호출은 보호된다.
64
+ """
65
+ pack = active_key = None
66
+ try:
67
+ pack, ctx, features, stream = build_context(kwargs)
68
+ capture_client(pack, ctx, args)
69
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
70
+ if stream:
71
+ opts = dict(kwargs.get("stream_options") or {})
72
+ if not opts.get("include_usage"):
73
+ opts["include_usage"] = True
74
+ kwargs["stream_options"] = opts
75
+ before_call(pack, active_key)
76
+ except Exception:
77
+ if pack is not None and active_key is not None:
78
+ _safe(_ensure_end, pack, active_key)
79
+ return await fn(*args, **kwargs)
60
80
 
61
- before_call(pack, active_key)
62
81
  _stream_returned = False
63
82
  try:
64
83
  try:
65
84
  response = await fn(*args, **kwargs)
66
85
  except Exception as err:
67
- handle_error(pack, err, active_key, OpenAIError)
86
+ _safe(handle_error, pack, err, active_key, OpenAIError)
68
87
  raise
69
88
  finally:
70
- if ctx:
71
- ctx._llm_httpc_pending = False
89
+ _safe(_clear_httpc_pending, ctx)
72
90
 
73
- after_call(pack, ctx)
74
- if stream:
75
- _stream_returned = True
76
- return async_stream(response, ChatStream(pack, active_key))
77
- finalize(response, pack, features)
78
- finalize_non_streaming(pack, active_key)
91
+ try:
92
+ after_call(pack, ctx)
93
+ if stream:
94
+ result, _stream_returned = wrap_async_stream(response, ChatStream(pack, active_key))
95
+ return result
96
+ extract_response(response, finalize, pack, features)
97
+ finalize_non_streaming(pack, active_key)
98
+ except Exception:
99
+ pass
79
100
  return response
80
101
  finally:
81
102
  if not _stream_returned:
82
- _ensure_end(pack, active_key)
103
+ _safe(_ensure_end, pack, active_key)
@@ -89,9 +89,11 @@ class ChatStream(StreamAccumulator):
89
89
  getattr(delta, "reasoning_content", None) or
90
90
  getattr(delta, "reasoning", None) or "")
91
91
  if reasoning:
92
+ self.on_first_token()
92
93
  self.reasoning += reasoning
93
94
 
94
95
  if getattr(delta, "tool_calls", None):
96
+ self.on_first_token()
95
97
  self.has_tool = True
96
98
  for tc in delta.tool_calls:
97
99
  idx = tc.index
@@ -3,68 +3,84 @@ from openai import OpenAIError
3
3
 
4
4
  from whatap.llm.providers.interceptor import (
5
5
  before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
- capture_client,
6
+ capture_client, extract_response, _safe, _clear_httpc_pending,
7
7
  )
8
- from whatap.llm.providers.stream_accumulator import sync_stream, async_stream
8
+ from whatap.llm.providers.stream_accumulator import wrap_sync_stream, wrap_async_stream
9
9
  from whatap.llm.providers.openai.completions.completions_context import build_context
10
10
  from whatap.llm.providers.openai.completions.completions_extractor import finalize, CompletionsStream
11
11
 
12
12
 
13
13
  def intercept_completions(fn, *args, **kwargs):
14
- """Completions API 동기 인터셉트."""
15
- pack, ctx = build_context(kwargs)
16
- capture_client(pack, ctx, args)
17
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
14
+ """Completions API 동기 인터셉트. 계측 실패는 사용자 호출로 전파되지 않는다."""
15
+ pack = active_key = None
16
+ try:
17
+ pack, ctx = build_context(kwargs)
18
+ capture_client(pack, ctx, args)
19
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
20
+ before_call(pack, active_key)
21
+ except Exception:
22
+ if pack is not None and active_key is not None:
23
+ _safe(_ensure_end, pack, active_key)
24
+ return fn(*args, **kwargs)
18
25
 
19
- before_call(pack, active_key)
20
26
  _stream_returned = False
21
27
  try:
22
28
  try:
23
29
  response = fn(*args, **kwargs)
24
30
  except Exception as err:
25
- handle_error(pack, err, active_key, OpenAIError)
31
+ _safe(handle_error, pack, err, active_key, OpenAIError)
26
32
  raise
27
33
  finally:
28
- if ctx:
29
- ctx._llm_httpc_pending = False
34
+ _safe(_clear_httpc_pending, ctx)
30
35
 
31
- after_call(pack, ctx)
32
- if pack.stream:
33
- _stream_returned = True
34
- return sync_stream(response, CompletionsStream(pack, active_key))
35
- finalize(response, pack)
36
- finalize_non_streaming(pack, active_key)
36
+ try:
37
+ after_call(pack, ctx)
38
+ if pack.stream:
39
+ result, _stream_returned = wrap_sync_stream(response, CompletionsStream(pack, active_key))
40
+ return result
41
+ extract_response(response, finalize, pack)
42
+ finalize_non_streaming(pack, active_key)
43
+ except Exception:
44
+ pass
37
45
  return response
38
46
  finally:
39
47
  if not _stream_returned:
40
- _ensure_end(pack, active_key)
48
+ _safe(_ensure_end, pack, active_key)
41
49
 
42
50
 
43
51
  async def intercept_completions_async(fn, *args, **kwargs):
44
- """Completions API 비동기 인터셉트."""
45
- pack, ctx = build_context(kwargs)
46
- capture_client(pack, ctx, args)
47
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
52
+ """Completions API 비동기 인터셉트. 계측 실패는 사용자 호출로 전파되지 않는다."""
53
+ pack = active_key = None
54
+ try:
55
+ pack, ctx = build_context(kwargs)
56
+ capture_client(pack, ctx, args)
57
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
58
+ before_call(pack, active_key)
59
+ except Exception:
60
+ if pack is not None and active_key is not None:
61
+ _safe(_ensure_end, pack, active_key)
62
+ return await fn(*args, **kwargs)
48
63
 
49
- before_call(pack, active_key)
50
64
  _stream_returned = False
51
65
  try:
52
66
  try:
53
67
  response = await fn(*args, **kwargs)
54
68
  except Exception as err:
55
- handle_error(pack, err, active_key, OpenAIError)
69
+ _safe(handle_error, pack, err, active_key, OpenAIError)
56
70
  raise
57
71
  finally:
58
- if ctx:
59
- ctx._llm_httpc_pending = False
72
+ _safe(_clear_httpc_pending, ctx)
60
73
 
61
- after_call(pack, ctx)
62
- if pack.stream:
63
- _stream_returned = True
64
- return async_stream(response, CompletionsStream(pack, active_key))
65
- finalize(response, pack)
66
- finalize_non_streaming(pack, active_key)
74
+ try:
75
+ after_call(pack, ctx)
76
+ if pack.stream:
77
+ result, _stream_returned = wrap_async_stream(response, CompletionsStream(pack, active_key))
78
+ return result
79
+ extract_response(response, finalize, pack)
80
+ finalize_non_streaming(pack, active_key)
81
+ except Exception:
82
+ pass
67
83
  return response
68
84
  finally:
69
85
  if not _stream_returned:
70
- _ensure_end(pack, active_key)
86
+ _safe(_ensure_end, pack, active_key)
@@ -3,57 +3,73 @@ from openai import OpenAIError
3
3
 
4
4
  from whatap.llm.providers.interceptor import (
5
5
  before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
- capture_client,
6
+ capture_client, extract_response, _safe, _clear_httpc_pending,
7
7
  )
8
8
  from whatap.llm.providers.openai.embeddings.embeddings_context import build_context
9
9
  from whatap.llm.providers.openai.embeddings.embeddings_extractor import finalize
10
10
 
11
11
 
12
12
  def intercept_embeddings(fn, *args, **kwargs):
13
- """OpenAI Embeddings 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
14
- pack, ctx = build_context(kwargs)
15
- capture_client(pack, ctx, args)
16
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
13
+ """OpenAI Embeddings 동기 호출을 인터셉트한다. 계측 실패는 사용자 호출로 전파되지 않는다."""
14
+ pack = active_key = None
15
+ try:
16
+ pack, ctx = build_context(kwargs)
17
+ capture_client(pack, ctx, args)
18
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
19
+ before_call(pack, active_key)
20
+ except Exception:
21
+ if pack is not None and active_key is not None:
22
+ _safe(_ensure_end, pack, active_key)
23
+ return fn(*args, **kwargs)
17
24
 
18
- before_call(pack, active_key)
19
25
  try:
20
26
  try:
21
27
  response = fn(*args, **kwargs)
22
28
  except Exception as err:
23
- handle_error(pack, err, active_key, OpenAIError)
29
+ _safe(handle_error, pack, err, active_key, OpenAIError)
24
30
  raise
25
31
  finally:
26
- if ctx:
27
- ctx._llm_httpc_pending = False
32
+ _safe(_clear_httpc_pending, ctx)
28
33
 
29
- after_call(pack, ctx)
30
- finalize(response, pack, kwargs)
31
- finalize_non_streaming(pack, active_key)
34
+ try:
35
+ after_call(pack, ctx)
36
+ extract_response(response, finalize, pack, kwargs)
37
+ finalize_non_streaming(pack, active_key)
38
+ except Exception:
39
+ pass
32
40
  return response
33
41
  finally:
34
- _ensure_end(pack, active_key)
42
+ _safe(_ensure_end, pack, active_key)
35
43
 
36
44
 
37
45
  async def intercept_embeddings_async(fn, *args, **kwargs):
38
- """OpenAI Embeddings 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
39
- pack, ctx = build_context(kwargs)
40
- capture_client(pack, ctx, args)
41
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
46
+ """OpenAI Embeddings 비동기 호출을 인터셉트한다. 계측 실패는 사용자 호출로 전파되지 않는다."""
47
+ pack = active_key = None
48
+ try:
49
+ pack, ctx = build_context(kwargs)
50
+ capture_client(pack, ctx, args)
51
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
52
+ before_call(pack, active_key)
53
+ except Exception:
54
+ if pack is not None and active_key is not None:
55
+ _safe(_ensure_end, pack, active_key)
56
+ return await fn(*args, **kwargs)
42
57
 
43
- before_call(pack, active_key)
44
58
  try:
45
59
  try:
46
60
  response = await fn(*args, **kwargs)
47
61
  except Exception as err:
48
- handle_error(pack, err, active_key, OpenAIError)
62
+ _safe(handle_error, pack, err, active_key, OpenAIError)
49
63
  raise
50
64
  finally:
51
- if ctx:
52
- ctx._llm_httpc_pending = False
65
+ _safe(_clear_httpc_pending, ctx)
53
66
 
54
- after_call(pack, ctx)
55
- finalize(response, pack, kwargs)
56
- finalize_non_streaming(pack, active_key)
67
+ try:
68
+ after_call(pack, ctx)
69
+ extract_response(response, finalize, pack, kwargs)
70
+ finalize_non_streaming(pack, active_key)
71
+ except Exception:
72
+ pass
57
73
  return response
58
74
  finally:
59
- _ensure_end(pack, active_key)
75
+ _safe(_ensure_end, pack, active_key)
@@ -3,68 +3,90 @@ from openai import OpenAIError
3
3
 
4
4
  from whatap.llm.providers.interceptor import (
5
5
  before_call, handle_error, after_call, finalize_non_streaming, _ensure_end,
6
- capture_client,
6
+ capture_client, extract_response, _safe, _clear_httpc_pending,
7
7
  )
8
- from whatap.llm.providers.stream_accumulator import sync_stream, async_stream
8
+ from whatap.llm.providers.stream_accumulator import wrap_sync_stream, wrap_async_stream
9
9
  from whatap.llm.providers.openai.responses.responses_context import build_context
10
10
  from whatap.llm.providers.openai.responses.responses_extractor import finalize, ResponsesStream
11
11
 
12
12
 
13
13
  def intercept_responses_create(fn, *args, **kwargs):
14
- """OpenAI Responses 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
15
- pack, ctx, features, stream = build_context(kwargs)
16
- capture_client(pack, ctx, args)
17
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
14
+ """OpenAI Responses 동기 호출을 인터셉트하여 모니터링 데이터를 수집한다.
15
+
16
+ 계측 어느 단계가 실패해도 사용자 호출은 보호된다.
17
+ """
18
+ pack = active_key = None
19
+ try:
20
+ pack, ctx, features, stream = build_context(kwargs)
21
+ capture_client(pack, ctx, args)
22
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
23
+ before_call(pack, active_key)
24
+ except Exception:
25
+ if pack is not None and active_key is not None:
26
+ _safe(_ensure_end, pack, active_key)
27
+ return fn(*args, **kwargs)
18
28
 
19
- before_call(pack, active_key)
20
29
  _stream_returned = False
21
30
  try:
22
31
  try:
23
32
  response = fn(*args, **kwargs)
24
33
  except Exception as err:
25
- handle_error(pack, err, active_key, OpenAIError)
34
+ _safe(handle_error, pack, err, active_key, OpenAIError)
26
35
  raise
27
36
  finally:
28
- if ctx:
29
- ctx._llm_httpc_pending = False
37
+ _safe(_clear_httpc_pending, ctx)
30
38
 
31
- after_call(pack, ctx)
32
- if stream:
33
- _stream_returned = True
34
- return sync_stream(response, ResponsesStream(pack, active_key))
35
- finalize(response, pack, features)
36
- finalize_non_streaming(pack, active_key)
39
+ try:
40
+ after_call(pack, ctx)
41
+ if stream:
42
+ result, _stream_returned = wrap_sync_stream(response, ResponsesStream(pack, active_key))
43
+ return result
44
+ extract_response(response, finalize, pack, features)
45
+ finalize_non_streaming(pack, active_key)
46
+ except Exception:
47
+ pass
37
48
  return response
38
49
  finally:
39
50
  if not _stream_returned:
40
- _ensure_end(pack, active_key)
51
+ _safe(_ensure_end, pack, active_key)
41
52
 
42
53
 
43
54
  async def intercept_responses_create_async(fn, *args, **kwargs):
44
- """OpenAI Responses 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다."""
45
- pack, ctx, features, stream = build_context(kwargs)
46
- capture_client(pack, ctx, args)
47
- active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
55
+ """OpenAI Responses 비동기 호출을 인터셉트하여 모니터링 데이터를 수집한다.
56
+
57
+ 계측 어느 단계가 실패해도 사용자 호출은 보호된다.
58
+ """
59
+ pack = active_key = None
60
+ try:
61
+ pack, ctx, features, stream = build_context(kwargs)
62
+ capture_client(pack, ctx, args)
63
+ active_key = (pack.model, pack.operation_type, getattr(pack, "prompt_version", "v1"))
64
+ before_call(pack, active_key)
65
+ except Exception:
66
+ if pack is not None and active_key is not None:
67
+ _safe(_ensure_end, pack, active_key)
68
+ return await fn(*args, **kwargs)
48
69
 
49
- before_call(pack, active_key)
50
70
  _stream_returned = False
51
71
  try:
52
72
  try:
53
73
  response = await fn(*args, **kwargs)
54
74
  except Exception as err:
55
- handle_error(pack, err, active_key, OpenAIError)
75
+ _safe(handle_error, pack, err, active_key, OpenAIError)
56
76
  raise
57
77
  finally:
58
- if ctx:
59
- ctx._llm_httpc_pending = False
78
+ _safe(_clear_httpc_pending, ctx)
60
79
 
61
- after_call(pack, ctx)
62
- if stream:
63
- _stream_returned = True
64
- return async_stream(response, ResponsesStream(pack, active_key))
65
- finalize(response, pack, features)
66
- finalize_non_streaming(pack, active_key)
80
+ try:
81
+ after_call(pack, ctx)
82
+ if stream:
83
+ result, _stream_returned = wrap_async_stream(response, ResponsesStream(pack, active_key))
84
+ return result
85
+ extract_response(response, finalize, pack, features)
86
+ finalize_non_streaming(pack, active_key)
87
+ except Exception:
88
+ pass
67
89
  return response
68
90
  finally:
69
91
  if not _stream_returned:
70
- _ensure_end(pack, active_key)
92
+ _safe(_ensure_end, pack, active_key)
@@ -92,15 +92,18 @@ class ResponsesStream(StreamAccumulator):
92
92
  item = getattr(event, 'item', None)
93
93
  it = getattr(item, 'type', '') if item else ''
94
94
  if it == 'function_call':
95
+ self.on_first_token()
95
96
  self.output_features.add(LlmFeature.TOOL_USE)
96
97
  call_id = getattr(item, 'call_id', '') or getattr(item, 'id', '')
97
98
  if call_id and call_id not in self.tool_calls:
98
99
  self.tool_calls[call_id] = {"id": call_id, "function": getattr(item, 'name', ''), "arguments": ""}
99
100
  elif 'web_search' in it:
101
+ self.on_first_token()
100
102
  self.output_features.add(LlmFeature.WEBSEARCH)
101
103
  elif t == 'response.function_call_arguments.delta':
102
104
  call_id = getattr(event, 'call_id', '')
103
105
  if call_id in self.tool_calls:
106
+ self.on_first_token()
104
107
  self.tool_calls[call_id]["arguments"] += getattr(event, 'delta', '') or ''
105
108
  elif t == 'response.function_call_arguments.done':
106
109
  call_id = getattr(event, 'call_id', '')
@@ -5,6 +5,7 @@ on_chunk()로 청크 데이터를 누적하고, _apply()로 pack에 반영한다
5
5
  """
6
6
  import time
7
7
 
8
+ from whatap import logging
8
9
  from whatap.llm.providers.interceptor import _dispatch, _ensure_end
9
10
 
10
11
 
@@ -35,7 +36,11 @@ class StreamAccumulator(object):
35
36
  self.first_token_time = time.monotonic()
36
37
 
37
38
  def finalize(self):
38
- """스트림 종료 시: latency/ttft 계산 → _apply() → 전송 → active 카운터 감소."""
39
+ """스트림 종료 시: latency/ttft 계산 → _apply() → 전송 → active 카운터 감소.
40
+
41
+ 계측 실패(_apply/dispatch 예외)가 사용자 스트림으로 전파되면 안 되므로 흡수한다.
42
+ active 카운터 정리(_ensure_end)는 어떤 경우에도 보장한다.
43
+ """
39
44
  try:
40
45
  end_time = time.monotonic()
41
46
  self.pack.latency = round((end_time - self.pack._start_time) * 1000)
@@ -44,6 +49,8 @@ class StreamAccumulator(object):
44
49
  self._apply()
45
50
  self.pack.success = True
46
51
  _dispatch(self.pack)
52
+ except Exception as e:
53
+ logging.debug('[LLM] stream finalize skipped: %s' % e, extra={'id': 'LLM011'})
47
54
  finally:
48
55
  if self.active_key:
49
56
  _ensure_end(self.pack, self.active_key)
@@ -54,20 +61,79 @@ class StreamAccumulator(object):
54
61
 
55
62
 
56
63
  def sync_stream(response, acc):
57
- """동기 스트림 래퍼. 원본 응답을 그대로 yield하면서 청크를 누적한다."""
64
+ """동기 스트림 래퍼. 원본 응답을 그대로 yield하면서 청크를 누적한다.
65
+
66
+ 청크 누적(on_chunk) 실패는 흡수한다 — 계측이 사용자 스트림을 깨면 안 된다.
67
+ """
58
68
  try:
59
69
  for chunk in response:
60
- acc.on_chunk(chunk)
70
+ try:
71
+ acc.on_chunk(chunk)
72
+ except Exception as e:
73
+ logging.debug('[LLM] stream chunk skipped: %s' % e, extra={'id': 'LLM010'})
61
74
  yield chunk
62
75
  finally:
63
76
  acc.finalize()
64
77
 
65
78
 
66
79
  async def async_stream(response, acc):
67
- """비동기 스트림 래퍼. 원본 응답을 그대로 yield하면서 청크를 누적한다."""
80
+ """비동기 스트림 래퍼. 원본 응답을 그대로 yield하면서 청크를 누적한다.
81
+
82
+ 청크 누적(on_chunk) 실패는 흡수한다 — 계측이 사용자 스트림을 깨면 안 된다.
83
+ """
68
84
  try:
69
85
  async for chunk in response:
70
- acc.on_chunk(chunk)
86
+ try:
87
+ acc.on_chunk(chunk)
88
+ except Exception as e:
89
+ logging.debug('[LLM] stream chunk skipped: %s' % e, extra={'id': 'LLM010'})
71
90
  yield chunk
72
91
  finally:
73
92
  acc.finalize()
93
+
94
+
95
+ def _wrap_via_parse(response, acc, wrapper, iter_attr):
96
+ """with_raw_response 래퍼(LegacyAPIResponse)를 호환 처리한다.
97
+
98
+ litellm 은 스트리밍 시 ``client.chat.completions.with_raw_response.create(...)`` 로
99
+ 헤더를 뽑은 뒤 ``raw_response.parse()`` 로 실제 스트림을 꺼낸다. 이 경로의 응답은
100
+ 스트림 이터러블이 아니라 ``parse()`` 를 가진 래퍼이므로, 그대로 ``async_stream`` 으로
101
+ 감싸면 litellm 의 ``raw_response.parse()`` 가 'async_generator has no attribute parse'
102
+ 로 터진다. 여기서는 ``parse()`` 가 돌려줄 실제 스트림만 계측 래퍼로 감싸도록
103
+ ``parse`` 를 래핑하고 원본을 그대로 돌려준다.
104
+
105
+ 반환: (호출자가 돌려줄 객체, 계측 소유 여부). 감쌀 수 없으면 (response, False).
106
+ """
107
+ parse = getattr(response, "parse", None)
108
+ if not callable(parse):
109
+ return response, False
110
+
111
+ state = {}
112
+
113
+ def instrumented_parse(*args, **kwargs):
114
+ real = parse(*args, **kwargs)
115
+ if hasattr(real, iter_attr):
116
+ if "g" not in state:
117
+ state["g"] = wrapper(real, acc)
118
+ return state["g"]
119
+ return real
120
+
121
+ try:
122
+ response.parse = instrumented_parse
123
+ return response, True
124
+ except Exception:
125
+ return response, False
126
+
127
+
128
+ def wrap_sync_stream(response, acc):
129
+ """동기 스트리밍 응답을 계측 래퍼로 감싼다. 반환: (돌려줄 객체, 계측 소유 여부)."""
130
+ if hasattr(response, "__iter__"):
131
+ return sync_stream(response, acc), True
132
+ return _wrap_via_parse(response, acc, sync_stream, "__iter__")
133
+
134
+
135
+ def wrap_async_stream(response, acc):
136
+ """비동기 스트리밍 응답을 계측 래퍼로 감싼다. 반환: (돌려줄 객체, 계측 소유 여부)."""
137
+ if hasattr(response, "__aiter__"):
138
+ return async_stream(response, acc), True
139
+ return _wrap_via_parse(response, acc, async_stream, "__aiter__")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whatap-python
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Summary: Monitoring and Profiling Service
5
5
  Home-page: https://www.whatap.io
6
6
  Author: whatap
@@ -2,10 +2,10 @@ whatap/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  whatap/README.rst,sha256=M4dohffaOQQcSlo02OgWplEr9SlffyjV3v7w9vHf1RI,967
3
3
  whatap/__init__.py,sha256=tuUaSBg5PFasSmn3wUJASnPccQnSa0xOp0xq5BEdMLQ,32665
4
4
  whatap/__main__.py,sha256=9fgK-V2eh7rZQ0OKWjpYsWRCyhOx35zRK6XUvJXWuUo,64
5
- whatap/build.py,sha256=tD-Ae80QyXxkhtuyJZcLcSkZp_KnG4mTkFaw-ygyAW4,82
5
+ whatap/build.py,sha256=hKoNW8VIOOedHEmBLuaSxYWeJmPPBnrPCTza2KD885U,82
6
6
  whatap/whatap.conf,sha256=E3UACbCDQ3-3XgDJWovbAtEN6LVLa7BQuCA0NXtX3CY,457
7
- whatap/agent/darwin/amd64/whatap_python,sha256=kym_h4e-8h1DIrQ13nkP1wur9jn_mAgTEylZJl-dM-Y,14038408
8
- whatap/agent/darwin/arm64/whatap_python,sha256=ZMvRHqgpC5aiaNnKlXG_MrK0e6eM2KDbBBeXlSnE2co,13435730
7
+ whatap/agent/darwin/amd64/whatap_python,sha256=dLdQ2aVjuinboro4S8600b4sEA73RpFIHw72lvkSvRk,20802384
8
+ whatap/agent/darwin/arm64/whatap_python,sha256=ar2scLqop8rVxB_ITdOrSIMlyTThQ6rF1V6pf1acqag,19784050
9
9
  whatap/agent/linux/amd64/whatap_python,sha256=kt0etbV-7uxNXD3mzrAeVhM2oxwim1N0OtbpkJJB2vg,18905944
10
10
  whatap/agent/linux/arm64/whatap_python,sha256=7V7bcI9mELhE1vRPmSQcBBCoRTp2VbBXIKeq-ixTrm4,18780673
11
11
  whatap/agent/windows/whatap_python.exe,sha256=ZmZIlcLD8yecsTOlqR6SBnLVQvlbtN70Wv0bb83j8f4,30576618
@@ -58,31 +58,31 @@ whatap/llm/log_sink_packs/llm_tool_calls.py,sha256=CIYbIsjUQWBr4qetAfLNAVjIFnv-l
58
58
  whatap/llm/log_sink_packs/llm_tool_results.py,sha256=bbg71I_p6ICVsoqMyIJJ-dpvuhjaeAe7Go3JHYPuxB0,465
59
59
  whatap/llm/log_sink_packs/llm_tx_status.py,sha256=m-bSncZO7hD0sa5L7_xRxM9izY9f5k_WOr3Z6JcfEDI,3863
60
60
  whatap/llm/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
- whatap/llm/providers/interceptor.py,sha256=SyIWMQy-oaZqTJmlLGVod_dmmKy3ClFpvZKM3u3Plls,6771
62
- whatap/llm/providers/stream_accumulator.py,sha256=79P29Su3-j96D2LPsHu-F843Liitr68Ol3zWID9pq6I,2500
61
+ whatap/llm/providers/interceptor.py,sha256=e_zel79upmedpRYF8p-EEIlpDv-6SMsD_PSBxsXKfyM,8952
62
+ whatap/llm/providers/stream_accumulator.py,sha256=9AZ0yepp0q9y-p4QiWSRSpAW3jXI2tDoQtpfXo9AnWA,5353
63
63
  whatap/llm/providers/anthropic/__init__.py,sha256=OT75EbX5Wly5TrzQbwJsPXo1_wmDZTKm13LuLPwL90w,1595
64
64
  whatap/llm/providers/anthropic/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
- whatap/llm/providers/anthropic/messages/messages.py,sha256=KiI7KCM7-NFWaqkarin2raYlh9Xg7YICWiy-JAs1bbQ,2587
65
+ whatap/llm/providers/anthropic/messages/messages.py,sha256=GSSko_KjoJGpNdjP3sH0yq8K0ESmYNZTTA9qpu17PPM,3336
66
66
  whatap/llm/providers/anthropic/messages/messages_context.py,sha256=Hbl66Hgea514ygMEYUwIs5JbFQfpmpGx3IP-FHWTDoY,2736
67
- whatap/llm/providers/anthropic/messages/messages_extractor.py,sha256=NcpUZqq7VWteXRBgUiUNIU9h7TRj37fsdObVCQWPxPI,5438
67
+ whatap/llm/providers/anthropic/messages/messages_extractor.py,sha256=Iy5Zrl9D7OZJzND1CxSYiSoy2WWIQP3W2htBQa-fi-0,5587
68
68
  whatap/llm/providers/openai/__init__.py,sha256=XBU4X6FLhKVS-XhXiUfjXM-TZTJP-4Z155yyBlw-6fc,6408
69
69
  whatap/llm/providers/openai/content_parser.py,sha256=0b72UMfzVDnof66-cY6ELeCbqngUXsNhfO4BxRl9UAY,1484
70
70
  whatap/llm/providers/openai/chat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
- whatap/llm/providers/openai/chat/chat.py,sha256=4Z-D0EZm6Fojz4pNdz5p1X9Ja-XjsP_2kYMHNpPMReg,2949
71
+ whatap/llm/providers/openai/chat/chat.py,sha256=0zws4O84wXN1bHjghdU7r4icB9HMn2RS4-PFWfjPh9w,3939
72
72
  whatap/llm/providers/openai/chat/chat_context.py,sha256=5hdZq_pYXwAaJtX28LaG5sNp002HXPzMlwgL0jhYy3Q,2947
73
- whatap/llm/providers/openai/chat/chat_extractor.py,sha256=YH9R5HXotoB-7liXB3l9LrU29VkRtyscs7qfUi2rCdY,5067
73
+ whatap/llm/providers/openai/chat/chat_extractor.py,sha256=ubPoi6CKGvuNgkv40eVrzvLGIRMBFWUFu7nnpLb6T0g,5135
74
74
  whatap/llm/providers/openai/completions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- whatap/llm/providers/openai/completions/completions.py,sha256=YZYBZ1JVpZW3FZxl1Zq7WahorlMD3-V_shYUBA5Vn88,2420
75
+ whatap/llm/providers/openai/completions/completions.py,sha256=N5DO40p8VbKlr71bRg3ptRxI5rV6K7NjKUNYQA88oGM,3247
76
76
  whatap/llm/providers/openai/completions/completions_context.py,sha256=cYA2F2gjZUaLsOgbjkFQcDkE_nJIDr5UgayvxyGFixk,917
77
77
  whatap/llm/providers/openai/completions/completions_extractor.py,sha256=uCW05rou1zwQPuTifOmv-987nW_WoXZ4WKoJTmeD4TE,1934
78
78
  whatap/llm/providers/openai/embeddings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
- whatap/llm/providers/openai/embeddings/embeddings.py,sha256=GfK66PKJ7cxr5dFhTPot37041trqOjV_k-8iTfhDlZ0,2050
79
+ whatap/llm/providers/openai/embeddings/embeddings.py,sha256=8AJ2RlJYCOdbBuEuV2QVbAWbBqlZ2QLAXJEzruOv21U,2735
80
80
  whatap/llm/providers/openai/embeddings/embeddings_context.py,sha256=Knu3q9zI5Ph5rQUeALl-ZvaXRfvvbSoaOvCTAjdKb_g,793
81
81
  whatap/llm/providers/openai/embeddings/embeddings_extractor.py,sha256=Qzjt4SHmuLfSGECr8nt6hVj8R-SkeQfqkXY_xUz_VSg,983
82
82
  whatap/llm/providers/openai/responses/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
- whatap/llm/providers/openai/responses/responses.py,sha256=AkGdtD9ZLG-Gv8H4BL7xSIoSImc31bLZd-5TGS4dIUc,2585
83
+ whatap/llm/providers/openai/responses/responses.py,sha256=GKdwOuRpjT5ePxfMwgLL32odJdJ8UWSX6vusM_JL4m8,3452
84
84
  whatap/llm/providers/openai/responses/responses_context.py,sha256=8ZzqEbG4LFi8nhHggMkx7xoL3HNV8_HVShyqD7BSnmc,3308
85
- whatap/llm/providers/openai/responses/responses_extractor.py,sha256=sIU6g6DcYtfECouGmffVvQbynT8ef_qI3L-d_wcXvME,5386
85
+ whatap/llm/providers/openai/responses/responses_extractor.py,sha256=HinUvPiQplM_lRUiR694a5DJOmCA8RlPJztdvaayQ6k,5500
86
86
  whatap/llm/stats/__init__.py,sha256=uR_KiY5K_dwRyHX1u1961TY0hy94LJE0_hmueVgRfgo,1352
87
87
  whatap/llm/stats/active_stat.py,sha256=lPyfy_pBq1CM8ABXHU7bMmTXKu2CxXIAOhScfqOKC9s,2917
88
88
  whatap/llm/stats/answer_relevance_eval_stat.py,sha256=rel9_fdH5_90qQ4AZy01Qv8plorQsWmB-4A3XwJ3oFw,353
@@ -220,8 +220,8 @@ whatap/value/text_hash_value.py,sha256=WPsSWzkR17plw3sgGjhJK0heCQiy0XdDYhawvBvQl
220
220
  whatap/value/text_value.py,sha256=8CWF57POGgosr5zEnGc0BdY0pYF0Vh8EsifHaRgfEwU,1148
221
221
  whatap/value/value.py,sha256=puRYlGm7q0iX3TKOqe_e0NdgK9Zj2J1t7NzqaQ8g7bU,578
222
222
  whatap/value/value_enum.py,sha256=AoUU4HUWC1bPan8k8CRBE9ayl0rGvJJHdRCTsamkWxE,2577
223
- whatap_python-2.1.0.dist-info/METADATA,sha256=F5gjn7Ri6gu6Tf5nwZNyA9aMq2ontZHPiiOn95KhsvM,2244
224
- whatap_python-2.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
225
- whatap_python-2.1.0.dist-info/entry_points.txt,sha256=lAwpO3oqmPt5riREZO759dHWoM8dqv-vMpDWwDcMiJY,280
226
- whatap_python-2.1.0.dist-info/top_level.txt,sha256=8bRV-cNhEi4cSEIRQm5zDLRqkH1ki9YVrcfLIUVs5jg,7
227
- whatap_python-2.1.0.dist-info/RECORD,,
223
+ whatap_python-2.1.1.dist-info/METADATA,sha256=0arI4JATShXzF1jtoYJ6-5xybdFQ1PqZAErvzxlu_yA,2244
224
+ whatap_python-2.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
225
+ whatap_python-2.1.1.dist-info/entry_points.txt,sha256=lAwpO3oqmPt5riREZO759dHWoM8dqv-vMpDWwDcMiJY,280
226
+ whatap_python-2.1.1.dist-info/top_level.txt,sha256=8bRV-cNhEi4cSEIRQm5zDLRqkH1ki9YVrcfLIUVs5jg,7
227
+ whatap_python-2.1.1.dist-info/RECORD,,