snowglobe 0.4.7__py3-none-any.whl → 0.4.9__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.
@@ -5,6 +5,7 @@ import importlib.util
5
5
  import json
6
6
  import logging
7
7
  import os
8
+ import sys
8
9
  import time
9
10
  import traceback
10
11
  from collections import defaultdict, deque
@@ -13,6 +14,7 @@ from functools import wraps
13
14
  from logging import getLogger
14
15
  from typing import Dict
15
16
  from urllib.parse import quote_plus
17
+ import uuid
16
18
 
17
19
  import httpx
18
20
  import uvicorn
@@ -20,6 +22,8 @@ from apscheduler import AsyncScheduler
20
22
  from apscheduler.triggers.interval import IntervalTrigger
21
23
  from fastapi import FastAPI, HTTPException, Request
22
24
 
25
+ from snowglobe.client.src.telemetry import trace_completion_fn, trace_risk_evaluation_fn
26
+
23
27
  from .cli_utils import info, shutdown_manager
24
28
  from .config import config, get_api_key_or_raise
25
29
  from .models import CompletionFunctionOutputs, CompletionRequest, RiskEvaluationRequest
@@ -126,16 +130,32 @@ async def process_application_heartbeat(app_id):
126
130
  try:
127
131
  prompt = "Hello from Snowglobe!"
128
132
  test_request = CompletionRequest(messages=[{"role": "user", "content": prompt}])
129
- completion_fn = apps.get(app_id, {}).get("completion_fn")
133
+ heartbeat_id = uuid.uuid4().hex
134
+ agent = apps.get(app_id, {})
135
+ agent_name = agent.get("name", "")
136
+ completion_fn = agent.get("completion_fn")
130
137
  if not completion_fn:
131
138
  LOGGER.warning(
132
139
  f"No completion function found for application {app_id}. Skipping heartbeat."
133
140
  )
134
141
  return
135
- if asyncio.iscoroutinefunction(completion_fn):
136
- response = await completion_fn(test_request)
137
- else:
138
- response = completion_fn(test_request)
142
+
143
+ @trace_completion_fn(
144
+ agent_name=agent_name,
145
+ conversation_id=heartbeat_id,
146
+ message_id=heartbeat_id,
147
+ session_id=heartbeat_id,
148
+ simulation_name=f"{agent_name} Heartbeat",
149
+ span_type="snowglobe/heartbeat",
150
+ )
151
+ async def run_completion_fn(completion_request: CompletionRequest):
152
+ if asyncio.iscoroutinefunction(completion_fn):
153
+ response = await completion_fn(completion_request)
154
+ else:
155
+ response = completion_fn(completion_request)
156
+ return response
157
+
158
+ response = await run_completion_fn(test_request)
139
159
  if not isinstance(response, CompletionFunctionOutputs):
140
160
  LOGGER.error(
141
161
  f"Completion function for application {app_id} did not return a valid response. Expected CompletionFunctionOutputs, got {type(response)}"
@@ -200,19 +220,31 @@ async def process_application_heartbeat(app_id):
200
220
  return connection_test_response.json()
201
221
 
202
222
 
203
- async def process_risk_evaluation(test, risk_name):
223
+ async def process_risk_evaluation(test, risk_name, simulation_name, agent_name):
204
224
  """finds correct risk and calls the risk evaluation function and creates a risk evaluation for the test"""
205
225
  start = time.time()
206
226
 
207
227
  messages = await fetch_messages(test=test)
208
228
 
209
- if asyncio.iscoroutinefunction(risks[risk_name]):
210
- risk_evaluation = await risks[risk_name](
211
- RiskEvaluationRequest(messages=messages)
212
- )
213
- else:
214
- risk_evaluation = risks[risk_name](RiskEvaluationRequest(messages=messages))
229
+ risk_eval_req = RiskEvaluationRequest(messages=messages)
230
+
231
+ @trace_risk_evaluation_fn(
232
+ agent_name=agent_name,
233
+ conversation_id=test["conversation_id"],
234
+ message_id=test["id"],
235
+ session_id=test["conversation_id"],
236
+ simulation_name=simulation_name,
237
+ span_type=f"snowglobe/risk-evaluation/{risk_name}",
238
+ risk_name=risk_name,
239
+ )
240
+ async def run_risk_evaluation_fn(risk_evaluation_request: RiskEvaluationRequest):
241
+ if asyncio.iscoroutinefunction(risks[risk_name]):
242
+ risk_evaluation = await risks[risk_name](risk_evaluation_request)
243
+ else:
244
+ risk_evaluation = risks[risk_name](risk_evaluation_request)
245
+ return risk_evaluation
215
246
 
247
+ risk_evaluation = await run_risk_evaluation_fn(risk_eval_req)
216
248
  LOGGER.debug(f"Risk evaluation output: {risk_evaluation}")
217
249
 
218
250
  # Extract fields from risk_evaluation object
@@ -248,16 +280,33 @@ async def process_risk_evaluation(test, risk_name):
248
280
  raise Exception("Error posting risk evaluation, task is not healthy")
249
281
 
250
282
 
251
- async def process_test(test, completion_fn, app_id):
283
+ async def process_test(test, completion_fn, app_id, simulation_name):
252
284
  """Processes a test by converting it to OpenAI style messages and calling the completion function"""
253
285
  start = time.time()
254
286
  # convert test to openai style messages
255
287
  messages = await fetch_messages(test=test)
256
288
 
257
- if asyncio.iscoroutinefunction(completion_fn):
258
- completionOutput = await completion_fn(CompletionRequest(messages=messages))
259
- else:
260
- completionOutput = completion_fn(CompletionRequest(messages=messages))
289
+ agent = apps.get(app_id, {})
290
+ agent_name = agent.get("name", "")
291
+
292
+ completion_req = CompletionRequest(messages=messages)
293
+
294
+ @trace_completion_fn(
295
+ agent_name=agent_name,
296
+ conversation_id=test["conversation_id"],
297
+ message_id=test["id"],
298
+ session_id=test["conversation_id"],
299
+ simulation_name=simulation_name,
300
+ span_type="snowglobe/completion",
301
+ )
302
+ async def run_completion_fn(completion_request: CompletionRequest):
303
+ if asyncio.iscoroutinefunction(completion_fn):
304
+ completionOutput = await completion_fn(completion_request)
305
+ else:
306
+ completionOutput = completion_fn(completion_request)
307
+ return completionOutput
308
+
309
+ completionOutput = await run_completion_fn(completion_req)
261
310
 
262
311
  LOGGER.debug(f"Completion output: {completionOutput}")
263
312
 
@@ -387,7 +436,11 @@ async def poll_for_completions():
387
436
  try:
388
437
  completion_request = await httpx.AsyncClient().post(
389
438
  f"{config.SNOWGLOBE_CLIENT_URL}/completion",
390
- json={"test": test, "app_id": app_id},
439
+ json={
440
+ "test": test,
441
+ "app_id": app_id,
442
+ "simulation_name": experiment["name"],
443
+ },
391
444
  timeout=30,
392
445
  )
393
446
  except (httpx.ConnectError, httpx.TimeoutException) as e:
@@ -542,6 +595,31 @@ async def poll_for_risk_evaluations():
542
595
  )
543
596
  continue
544
597
  experiment = experiment_request.json()
598
+
599
+ try:
600
+ app_request = await client.get(
601
+ f"{config.CONTROL_PLANE_URL}/api/applications/{experiment['app_id']}",
602
+ headers={"x-api-key": get_api_key_or_raise()},
603
+ )
604
+ except (httpx.ConnectError, httpx.TimeoutException) as e:
605
+ if shutdown_manager.is_shutdown_requested():
606
+ LOGGER.debug(f"HTTP error during shutdown (expected): {e}")
607
+ return
608
+ else:
609
+ LOGGER.error(
610
+ f"Connection error fetching application {experiment['app_id']}: {e}"
611
+ )
612
+ continue
613
+
614
+ app_name = experiment["app_id"]
615
+ if not app_request.is_success:
616
+ LOGGER.error(
617
+ f"Error fetching application {experiment['app_id']}: {app_request.text}"
618
+ )
619
+ else:
620
+ application = app_request.json()
621
+ app_name = application["name"]
622
+
545
623
  risk_eval_count = 0
546
624
 
547
625
  for risk_name in risks.keys():
@@ -607,7 +685,12 @@ async def poll_for_risk_evaluations():
607
685
  try:
608
686
  risk_eval_response = await httpx.AsyncClient().post(
609
687
  f"{config.SNOWGLOBE_CLIENT_URL}/risk-evaluation",
610
- json={"test": test, "risk_name": risk_name},
688
+ json={
689
+ "test": test,
690
+ "risk_name": risk_name,
691
+ "simulation_name": experiment["name"],
692
+ "agent_name": app_name,
693
+ },
611
694
  timeout=30,
612
695
  )
613
696
  except (
@@ -685,7 +768,17 @@ async def lifespan(app: FastAPI):
685
768
  "agent_wrapper", agent_file_path
686
769
  )
687
770
  agent_module = importlib.util.module_from_spec(spec)
688
- spec.loader.exec_module(agent_module)
771
+
772
+ # Add current directory to path
773
+ sys_path_backup = sys.path.copy()
774
+ current_dir = os.getcwd()
775
+ if current_dir not in sys.path:
776
+ sys.path.insert(0, current_dir)
777
+
778
+ try:
779
+ spec.loader.exec_module(agent_module)
780
+ finally:
781
+ sys.path = sys_path_backup
689
782
 
690
783
  if not hasattr(agent_module, "process_scenario"):
691
784
  LOGGER.warning(
@@ -699,6 +792,7 @@ async def lifespan(app: FastAPI):
699
792
  }
700
793
 
701
794
  except Exception as e:
795
+ traceback.print_exc()
702
796
  LOGGER.error(f"Error loading agent {filename}: {e}")
703
797
  continue
704
798
 
@@ -836,6 +930,7 @@ def create_client():
836
930
  completion_body = await request.json()
837
931
  test = completion_body.get("test")
838
932
  app_id = completion_body.get("app_id")
933
+ simulation_name = completion_body.get("simulation_name")
839
934
  # both are required non empty strings
840
935
  if not test or not app_id:
841
936
  raise HTTPException(
@@ -850,7 +945,7 @@ def create_client():
850
945
  completion_fn = apps.get(app_id, {}).get("completion_fn")
851
946
  LOGGER.debug(f"Received test: {test['id']}")
852
947
 
853
- await process_test(test, completion_fn, app_id)
948
+ await process_test(test, completion_fn, app_id, simulation_name)
854
949
  return {"status": "processed"}
855
950
 
856
951
  @app.post("/heartbeat")
@@ -890,10 +985,12 @@ def create_client():
890
985
  body = await request.json()
891
986
  test = body.get("test")
892
987
  risk_name = body.get("risk_name")
988
+ simulation_name = body.get("simulation_name")
989
+ agent_name = body.get("agent_name")
893
990
  LOGGER.debug(f"Received risk evaluation for test: {test['id']}")
894
991
 
895
992
  # For now, just simulate processing
896
- await process_risk_evaluation(test, risk_name)
993
+ await process_risk_evaluation(test, risk_name, simulation_name, agent_name)
897
994
  return {"status": "risk evaluation processed"}
898
995
 
899
996
  return app
@@ -6,6 +6,7 @@ import signal
6
6
  import sys
7
7
  import threading
8
8
  import time
9
+ import uuid
9
10
  import webbrowser
10
11
  from importlib.metadata import version
11
12
  from typing import Optional, Tuple
@@ -15,6 +16,8 @@ import uvicorn
15
16
  from fastapi import FastAPI, Request
16
17
  from fastapi.middleware.cors import CORSMiddleware
17
18
 
19
+ from snowglobe.client.src.telemetry import trace_completion_fn
20
+
18
21
  # Import start_client lazily inside the start command to avoid config initialization
19
22
  from .cli_utils import (
20
23
  check_auth_status,
@@ -198,7 +201,7 @@ def test(
198
201
  else:
199
202
  info("Check your implementation and try again.")
200
203
  docs_link(
201
- "Troubleshooting guide", "https://docs.snowglobe.so/troubleshooting"
204
+ "Troubleshooting guide", "https://snowglobe.so/docs/troubleshooting"
202
205
  )
203
206
  raise typer.Exit(1)
204
207
 
@@ -231,7 +234,7 @@ def init(
231
234
  if not is_auth:
232
235
  error("Authentication required to initialize agents")
233
236
  info("Please run 'snowglobe-connect auth' first to set up authentication")
234
- docs_link("Setup guide", "https://docs.snowglobe.so/setup")
237
+ docs_link("Setup guide", "https://snowglobe.so/docs/setup")
235
238
  raise typer.Exit(1)
236
239
 
237
240
  success("Authenticated successfully")
@@ -256,9 +259,7 @@ def init(
256
259
  raise typer.Exit(0)
257
260
  elif selected == "new":
258
261
  info("Creating new application not yet implemented in init command")
259
- info(
260
- "Please visit https://snowglobe.guardrails-ai.com/applications/create to create a new app"
261
- )
262
+ info("Please visit https://snowglobe.guardrailsai.com/app to create a new app")
262
263
  info("Then run this command again to select it")
263
264
  raise typer.Exit(0)
264
265
 
@@ -388,10 +389,24 @@ def test_agent_wrapper(filename: str, app_id: str, app_name: str) -> Tuple[bool,
388
389
  ]
389
390
  )
390
391
 
391
- if asyncio.iscoroutinefunction(process_scenario):
392
- response = asyncio.run(process_scenario(test_request))
393
- else:
394
- response = process_scenario(test_request)
392
+ test_id = uuid.uuid4()
393
+
394
+ @trace_completion_fn(
395
+ agent_name=app_name,
396
+ conversation_id=test_id,
397
+ message_id=test_id,
398
+ session_id=test_id,
399
+ simulation_name=f"{app_name} CLI Test",
400
+ span_type="snowglobe/cli-test",
401
+ )
402
+ async def run_process_scenario(completion_request: CompletionRequest):
403
+ if asyncio.iscoroutinefunction(process_scenario):
404
+ response = asyncio.run(process_scenario(completion_request))
405
+ else:
406
+ response = process_scenario(completion_request)
407
+ return response
408
+
409
+ response = asyncio.run(run_process_scenario(test_request))
395
410
 
396
411
  if hasattr(response, "response") and isinstance(response.response, str):
397
412
  if response.response == "Your response here":
@@ -402,6 +417,7 @@ def test_agent_wrapper(filename: str, app_id: str, app_name: str) -> Tuple[bool,
402
417
 
403
418
  except Exception as e:
404
419
  import traceback
420
+
405
421
  traceback.print_exc()
406
422
  return False, f"Error: {str(e)}"
407
423
 
@@ -434,7 +450,7 @@ def enhanced_error_handler(status_code: int, operation: str = "operation") -> No
434
450
  error("Authentication failed")
435
451
  info("Your API key may be invalid or expired")
436
452
  info("Run 'snowglobe-connect auth' to set up authentication")
437
- docs_link("Authentication help", "https://docs.snowglobe.so/auth")
453
+ docs_link("Authentication help", "https://snowglobe.so/docs/auth")
438
454
  elif status_code == 403:
439
455
  error("Access forbidden")
440
456
  info("You don't have permission for this operation")
@@ -606,6 +622,7 @@ def _create_auth_server(config_key: str, rc_path: str) -> FastAPI:
606
622
  return {"written": True}
607
623
  except Exception as e:
608
624
  import traceback
625
+
609
626
  traceback.print_exc()
610
627
  error(f"Failed to process key configuration: {e}")
611
628
  return {"error": "Failed to process key configuration request"}
@@ -624,7 +641,7 @@ def _show_auth_success_next_steps() -> None:
624
641
  console.print("3. Start the client:")
625
642
  console.print(" [bold green]snowglobe-connect start[/bold green]")
626
643
  console.print()
627
- docs_link("Getting started guide", "https://docs.snowglobe.so/getting-started")
644
+ docs_link("Getting started guide", "https://snowglobe.so/docs/getting-started")
628
645
 
629
646
 
630
647
  def _poll_for_api_key(rc_path: str, timeout: int = 300) -> bool:
@@ -792,6 +809,7 @@ def start(
792
809
  console.print()
793
810
  except Exception:
794
811
  import traceback
812
+
795
813
  traceback.print_exc()
796
814
  # Do not block startup if we cannot load agents mapping
797
815
  pass
@@ -0,0 +1,146 @@
1
+ import os
2
+ from importlib.metadata import version as importlib_version
3
+ from typing import Callable
4
+ from functools import wraps
5
+
6
+ from snowglobe.client.src.models import CompletionRequest, RiskEvaluationRequest
7
+
8
+ try:
9
+ import mlflow
10
+ import mlflow.tracing
11
+
12
+ mlflow.tracing.enable()
13
+ except ImportError:
14
+ mlflow = None
15
+
16
+ SNOWGLOBE_VERSION = importlib_version("snowglobe")
17
+
18
+
19
+ def trace_completion_fn(
20
+ *,
21
+ session_id: str,
22
+ conversation_id: str,
23
+ message_id: str,
24
+ simulation_name: str,
25
+ agent_name: str,
26
+ span_type: str,
27
+ ):
28
+ def trace_decorator(completion_fn: Callable):
29
+ disable_mlflow = os.getenv("SNOWGLOBE_DISABLE_MLFLOW_TRACING") or ""
30
+ if mlflow and disable_mlflow.lower() != "true":
31
+ mlflow_experiment_name = (
32
+ os.getenv("MLFLOW_EXPERIMENT_NAME") or simulation_name
33
+ )
34
+ mlflow.set_experiment(mlflow_experiment_name)
35
+
36
+ mlflow_active_model_id = os.getenv("MLFLOW_ACTIVE_MODEL_ID")
37
+ if mlflow_active_model_id:
38
+ mlflow.set_active_model(model_id=mlflow_active_model_id)
39
+ else:
40
+ mlflow.set_active_model(name=agent_name)
41
+
42
+ span_attributes = {
43
+ "snowglobe.version": SNOWGLOBE_VERSION,
44
+ "type": span_type,
45
+ "session_id": session_id,
46
+ "conversation_id": conversation_id,
47
+ "message_id": message_id,
48
+ "simulation_name": simulation_name,
49
+ "agent_name": agent_name,
50
+ }
51
+
52
+ @mlflow.trace(
53
+ name=span_type,
54
+ span_type=span_type,
55
+ attributes=span_attributes,
56
+ )
57
+ @wraps(completion_fn)
58
+ async def completion_fn_wrapper(test_request: CompletionRequest):
59
+ try:
60
+ mlflow.update_current_trace(
61
+ metadata={"mlflow.trace.session": session_id},
62
+ tags={
63
+ "session_id": session_id,
64
+ "conversation_id": conversation_id,
65
+ "message_id": message_id,
66
+ "simulation_name": simulation_name,
67
+ "agent_name": agent_name,
68
+ },
69
+ )
70
+ response = await completion_fn(test_request)
71
+ return response
72
+ except Exception as e:
73
+ raise e
74
+
75
+ return completion_fn_wrapper
76
+ else:
77
+ return completion_fn
78
+
79
+ return trace_decorator
80
+
81
+
82
+ def trace_risk_evaluation_fn(
83
+ *,
84
+ session_id: str,
85
+ conversation_id: str,
86
+ message_id: str,
87
+ simulation_name: str,
88
+ agent_name: str,
89
+ span_type: str,
90
+ risk_name,
91
+ ):
92
+ def trace_decorator(risk_evaluation_fn: Callable):
93
+ disable_mlflow = os.getenv("SNOWGLOBE_DISABLE_MLFLOW_TRACING") or ""
94
+ if mlflow and disable_mlflow.lower() != "true":
95
+ mlflow_experiment_name = (
96
+ os.getenv("MLFLOW_EXPERIMENT_NAME") or simulation_name
97
+ )
98
+ mlflow.set_experiment(mlflow_experiment_name)
99
+
100
+ mlflow_active_model_id = os.getenv("MLFLOW_ACTIVE_MODEL_ID")
101
+ if mlflow_active_model_id:
102
+ mlflow.set_active_model(model_id=mlflow_active_model_id)
103
+ else:
104
+ mlflow.set_active_model(name=agent_name)
105
+ span_attributes = {
106
+ "snowglobe.version": SNOWGLOBE_VERSION,
107
+ "type": span_type,
108
+ "session_id": session_id,
109
+ "conversation_id": conversation_id,
110
+ "message_id": message_id,
111
+ "simulation_name": simulation_name,
112
+ "agent_name": agent_name,
113
+ "risk_name": risk_name,
114
+ }
115
+
116
+ @mlflow.trace(
117
+ name=span_type,
118
+ span_type=span_type,
119
+ attributes=span_attributes,
120
+ )
121
+ @wraps(risk_evaluation_fn)
122
+ async def risk_evaluation_fn_wrapper(
123
+ risk_evaluation_request: RiskEvaluationRequest,
124
+ ):
125
+ try:
126
+ mlflow.update_current_trace(
127
+ metadata={"mlflow.trace.session": session_id},
128
+ tags={
129
+ "session_id": session_id,
130
+ "conversation_id": conversation_id,
131
+ "message_id": message_id,
132
+ "simulation_name": simulation_name,
133
+ "agent_name": agent_name,
134
+ "risk_name": risk_name,
135
+ },
136
+ )
137
+ response = await risk_evaluation_fn(risk_evaluation_request)
138
+ return response
139
+ except Exception as e:
140
+ raise e
141
+
142
+ return risk_evaluation_fn_wrapper
143
+ else:
144
+ return risk_evaluation_fn
145
+
146
+ return trace_decorator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowglobe
3
- Version: 0.4.7
3
+ Version: 0.4.9
4
4
  Summary: client server for usage with snowglobe experiments
5
5
  Author-email: Guardrails AI <contact@guardrailsai.com>
6
6
  License: MIT License
@@ -119,3 +119,17 @@ def process_scenario(request: CompletionRequest) -> CompletionFunctionOutputs:
119
119
  )
120
120
  return CompletionFunctionOutputs(response=response.choices[0].message.content)
121
121
  ```
122
+
123
+ ## Tracing with MLflow
124
+ The Snowglobe Connect SDK has MLflow tracing built in! Simply `pip install mlflow` and the sdk will take care of the rest. Read more about MLflow's tracing capability for GenAI Apps [here](https://mlflow.org/docs/latest/genai/tracing/app-instrumentation/).
125
+
126
+ ### Enhancing Snowglobe Connect SDK's Traces with Autologging
127
+ You can turn on mlflow autologging in your app to add additional context to the traces the Snowglobe Connect SDK captures. In you app's entry point simply call the appropriate autolog method for the LLM provider you're using. The below example shows how to enable this for LiteLLM:
128
+ ```py
129
+ import mlflow
130
+
131
+ mlflow.litellm.autolog()
132
+ ```
133
+
134
+ ### Disable Snowglobe Connect SDK's MLflow Tracing
135
+ If you already use MLflow and don't want the Snowglobe Connect SDK to capture additional traces, you can disable this feature by setting the `SNOWGLOBE_DISABLE_MLFLOW_TRACING` environment variable to `true`.
@@ -1,15 +1,16 @@
1
1
  snowglobe/client/__init__.py,sha256=kzp9wPUUYBXqDSKZbfmD4vrAQvrWSW5HOvtpFlEJWfs,353
2
- snowglobe/client/src/app.py,sha256=_rYhtopDeGy16HSxnqA4LR66-pmZeAJ1QNPd84N8HT8,38629
3
- snowglobe/client/src/cli.py,sha256=DIOI4E0ZeEljXDiEZb9rkbTWdHi3m4owWvyKtAC5Ro0,27999
2
+ snowglobe/client/src/app.py,sha256=CaDtbMn6ZXooQJuaPAHOXs2r9FkFqDxTrgqoW3Rl_2I,42686
3
+ snowglobe/client/src/cli.py,sha256=I3LVWJmyvUzOQlV0gv_AeMuEazfy8GsYdH6svo7cZOU,28544
4
4
  snowglobe/client/src/cli_utils.py,sha256=6C7J5gow8xveQYF4w6ewtQJKI7VvlLTx7FS_7gl7RwI,17227
5
5
  snowglobe/client/src/config.py,sha256=YRx_AQEZoHaAqk6guTxynIEGV_iJ3wNNGtMmaKsYMbc,10488
6
6
  snowglobe/client/src/models.py,sha256=BX310WrDN9Fd8v68me3XGL_ic1ulvjCrZyIT2ND1eUo,866
7
7
  snowglobe/client/src/project_manager.py,sha256=Ze-qs4dQI2kIV-PmtWZ1b67hMUfsnsMHus90aT8HOow,9970
8
8
  snowglobe/client/src/stats.py,sha256=IdaXroOZBmvLVa_p9pDE6hsxsc7-fBEDnLf8O6Ch0GA,1596
9
+ snowglobe/client/src/telemetry.py,sha256=N91Q37YfJaUYPa7BUAs_3x4LxjculwlETIKC5k1dbig,5045
9
10
  snowglobe/client/src/utils.py,sha256=hHOht0hc8fv3OuPTz2Tqs639CzSAF34JTZs5ifKV6YI,3708
10
- snowglobe-0.4.7.dist-info/licenses/LICENSE,sha256=S90V6iFU5ZeSg44JQYS1To3pa7ZEobrHc_t483qSKSI,1070
11
- snowglobe-0.4.7.dist-info/METADATA,sha256=v3zT_-OLfxZPOtUwBN3jLxUZxa3EnAJr5bXwpFVHz28,4467
12
- snowglobe-0.4.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- snowglobe-0.4.7.dist-info/entry_points.txt,sha256=mqx4mTwFPHttjctE2ceYTYWCCIG30Ji2C89aaCYgHcM,71
14
- snowglobe-0.4.7.dist-info/top_level.txt,sha256=PoyYihnCBjRyjeIT19yBcE47JTe7i1OwRXvJ4d5EohM,10
15
- snowglobe-0.4.7.dist-info/RECORD,,
11
+ snowglobe-0.4.9.dist-info/licenses/LICENSE,sha256=S90V6iFU5ZeSg44JQYS1To3pa7ZEobrHc_t483qSKSI,1070
12
+ snowglobe-0.4.9.dist-info/METADATA,sha256=NckGusSPGxCyboKGK79F-PiNaXstK6FOxmM0p0lOB5g,5406
13
+ snowglobe-0.4.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ snowglobe-0.4.9.dist-info/entry_points.txt,sha256=mqx4mTwFPHttjctE2ceYTYWCCIG30Ji2C89aaCYgHcM,71
15
+ snowglobe-0.4.9.dist-info/top_level.txt,sha256=PoyYihnCBjRyjeIT19yBcE47JTe7i1OwRXvJ4d5EohM,10
16
+ snowglobe-0.4.9.dist-info/RECORD,,