snowglobe 0.4.8__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.
@@ -14,6 +14,7 @@ from functools import wraps
14
14
  from logging import getLogger
15
15
  from typing import Dict
16
16
  from urllib.parse import quote_plus
17
+ import uuid
17
18
 
18
19
  import httpx
19
20
  import uvicorn
@@ -21,6 +22,8 @@ from apscheduler import AsyncScheduler
21
22
  from apscheduler.triggers.interval import IntervalTrigger
22
23
  from fastapi import FastAPI, HTTPException, Request
23
24
 
25
+ from snowglobe.client.src.telemetry import trace_completion_fn, trace_risk_evaluation_fn
26
+
24
27
  from .cli_utils import info, shutdown_manager
25
28
  from .config import config, get_api_key_or_raise
26
29
  from .models import CompletionFunctionOutputs, CompletionRequest, RiskEvaluationRequest
@@ -127,16 +130,32 @@ async def process_application_heartbeat(app_id):
127
130
  try:
128
131
  prompt = "Hello from Snowglobe!"
129
132
  test_request = CompletionRequest(messages=[{"role": "user", "content": prompt}])
130
- 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")
131
137
  if not completion_fn:
132
138
  LOGGER.warning(
133
139
  f"No completion function found for application {app_id}. Skipping heartbeat."
134
140
  )
135
141
  return
136
- if asyncio.iscoroutinefunction(completion_fn):
137
- response = await completion_fn(test_request)
138
- else:
139
- 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)
140
159
  if not isinstance(response, CompletionFunctionOutputs):
141
160
  LOGGER.error(
142
161
  f"Completion function for application {app_id} did not return a valid response. Expected CompletionFunctionOutputs, got {type(response)}"
@@ -201,19 +220,31 @@ async def process_application_heartbeat(app_id):
201
220
  return connection_test_response.json()
202
221
 
203
222
 
204
- async def process_risk_evaluation(test, risk_name):
223
+ async def process_risk_evaluation(test, risk_name, simulation_name, agent_name):
205
224
  """finds correct risk and calls the risk evaluation function and creates a risk evaluation for the test"""
206
225
  start = time.time()
207
226
 
208
227
  messages = await fetch_messages(test=test)
209
228
 
210
- if asyncio.iscoroutinefunction(risks[risk_name]):
211
- risk_evaluation = await risks[risk_name](
212
- RiskEvaluationRequest(messages=messages)
213
- )
214
- else:
215
- risk_evaluation = risks[risk_name](RiskEvaluationRequest(messages=messages))
229
+ risk_eval_req = RiskEvaluationRequest(messages=messages)
216
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
246
+
247
+ risk_evaluation = await run_risk_evaluation_fn(risk_eval_req)
217
248
  LOGGER.debug(f"Risk evaluation output: {risk_evaluation}")
218
249
 
219
250
  # Extract fields from risk_evaluation object
@@ -249,16 +280,33 @@ async def process_risk_evaluation(test, risk_name):
249
280
  raise Exception("Error posting risk evaluation, task is not healthy")
250
281
 
251
282
 
252
- async def process_test(test, completion_fn, app_id):
283
+ async def process_test(test, completion_fn, app_id, simulation_name):
253
284
  """Processes a test by converting it to OpenAI style messages and calling the completion function"""
254
285
  start = time.time()
255
286
  # convert test to openai style messages
256
287
  messages = await fetch_messages(test=test)
257
288
 
258
- if asyncio.iscoroutinefunction(completion_fn):
259
- completionOutput = await completion_fn(CompletionRequest(messages=messages))
260
- else:
261
- 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)
262
310
 
263
311
  LOGGER.debug(f"Completion output: {completionOutput}")
264
312
 
@@ -388,7 +436,11 @@ async def poll_for_completions():
388
436
  try:
389
437
  completion_request = await httpx.AsyncClient().post(
390
438
  f"{config.SNOWGLOBE_CLIENT_URL}/completion",
391
- json={"test": test, "app_id": app_id},
439
+ json={
440
+ "test": test,
441
+ "app_id": app_id,
442
+ "simulation_name": experiment["name"],
443
+ },
392
444
  timeout=30,
393
445
  )
394
446
  except (httpx.ConnectError, httpx.TimeoutException) as e:
@@ -543,6 +595,31 @@ async def poll_for_risk_evaluations():
543
595
  )
544
596
  continue
545
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
+
546
623
  risk_eval_count = 0
547
624
 
548
625
  for risk_name in risks.keys():
@@ -608,7 +685,12 @@ async def poll_for_risk_evaluations():
608
685
  try:
609
686
  risk_eval_response = await httpx.AsyncClient().post(
610
687
  f"{config.SNOWGLOBE_CLIENT_URL}/risk-evaluation",
611
- 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
+ },
612
694
  timeout=30,
613
695
  )
614
696
  except (
@@ -686,7 +768,7 @@ async def lifespan(app: FastAPI):
686
768
  "agent_wrapper", agent_file_path
687
769
  )
688
770
  agent_module = importlib.util.module_from_spec(spec)
689
-
771
+
690
772
  # Add current directory to path
691
773
  sys_path_backup = sys.path.copy()
692
774
  current_dir = os.getcwd()
@@ -848,6 +930,7 @@ def create_client():
848
930
  completion_body = await request.json()
849
931
  test = completion_body.get("test")
850
932
  app_id = completion_body.get("app_id")
933
+ simulation_name = completion_body.get("simulation_name")
851
934
  # both are required non empty strings
852
935
  if not test or not app_id:
853
936
  raise HTTPException(
@@ -862,7 +945,7 @@ def create_client():
862
945
  completion_fn = apps.get(app_id, {}).get("completion_fn")
863
946
  LOGGER.debug(f"Received test: {test['id']}")
864
947
 
865
- await process_test(test, completion_fn, app_id)
948
+ await process_test(test, completion_fn, app_id, simulation_name)
866
949
  return {"status": "processed"}
867
950
 
868
951
  @app.post("/heartbeat")
@@ -902,10 +985,12 @@ def create_client():
902
985
  body = await request.json()
903
986
  test = body.get("test")
904
987
  risk_name = body.get("risk_name")
988
+ simulation_name = body.get("simulation_name")
989
+ agent_name = body.get("agent_name")
905
990
  LOGGER.debug(f"Received risk evaluation for test: {test['id']}")
906
991
 
907
992
  # For now, just simulate processing
908
- await process_risk_evaluation(test, risk_name)
993
+ await process_risk_evaluation(test, risk_name, simulation_name, agent_name)
909
994
  return {"status": "risk evaluation processed"}
910
995
 
911
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.8
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=Ij7nB88VeGtz4iEKc9iwCQnG-AdsYaIDscwcU7SzDXg,39112
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.8.dist-info/licenses/LICENSE,sha256=S90V6iFU5ZeSg44JQYS1To3pa7ZEobrHc_t483qSKSI,1070
11
- snowglobe-0.4.8.dist-info/METADATA,sha256=158Z9QgzGaU4HIBqe020xSQMI7evAz0bZdlpl29yPZI,4467
12
- snowglobe-0.4.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- snowglobe-0.4.8.dist-info/entry_points.txt,sha256=mqx4mTwFPHttjctE2ceYTYWCCIG30Ji2C89aaCYgHcM,71
14
- snowglobe-0.4.8.dist-info/top_level.txt,sha256=PoyYihnCBjRyjeIT19yBcE47JTe7i1OwRXvJ4d5EohM,10
15
- snowglobe-0.4.8.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,,