snowglobe 0.4.0__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.
@@ -0,0 +1,290 @@
1
+ """
2
+ Project and agent management for the new clean file structure
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Tuple
11
+
12
+ SNOWGLOBE_DIR = ".snowglobe"
13
+ AGENTS_FILE = "agents.json"
14
+ CONFIG_FILE = "config.json"
15
+
16
+
17
+ class ProjectManager:
18
+ """Manages snowglobe project structure and agent mappings"""
19
+
20
+ def __init__(self, project_root: str = None):
21
+ self.project_root = Path(project_root or os.getcwd())
22
+ self.snowglobe_dir = self.project_root / SNOWGLOBE_DIR
23
+ self.agents_file = self.snowglobe_dir / AGENTS_FILE
24
+ self.config_file = self.snowglobe_dir / CONFIG_FILE
25
+
26
+ def ensure_project_structure(self) -> None:
27
+ """Create .snowglobe directory if it doesn't exist"""
28
+ self.snowglobe_dir.mkdir(exist_ok=True)
29
+
30
+ # Create agents.json if it doesn't exist
31
+ if not self.agents_file.exists():
32
+ with open(self.agents_file, "w") as f:
33
+ json.dump({}, f, indent=2)
34
+
35
+ def load_agents_mapping(self) -> Dict[str, Dict[str, Any]]:
36
+ """Load agent UUID mappings from .snowglobe/agents.json"""
37
+ if not self.agents_file.exists():
38
+ return {}
39
+
40
+ try:
41
+ with open(self.agents_file, "r") as f:
42
+ return json.load(f)
43
+ except (json.JSONDecodeError, IOError):
44
+ # Return empty dict if file is corrupted
45
+ return {}
46
+
47
+ def save_agents_mapping(self, mapping: Dict[str, Dict[str, Any]]) -> None:
48
+ """Save agent UUID mappings to .snowglobe/agents.json"""
49
+ # Only ensure directory exists, not the full structure to avoid recursion
50
+ self.snowglobe_dir.mkdir(exist_ok=True)
51
+
52
+ with open(self.agents_file, "w") as f:
53
+ json.dump(mapping, f, indent=2, sort_keys=True)
54
+
55
+ def add_agent_mapping(self, filename: str, uuid: str, name: str) -> None:
56
+ """Add a new agent mapping"""
57
+ mapping = self.load_agents_mapping()
58
+
59
+ mapping[filename] = {
60
+ "uuid": uuid,
61
+ "name": name,
62
+ "created": datetime.utcnow().isoformat() + "Z",
63
+ }
64
+
65
+ self.save_agents_mapping(mapping)
66
+
67
+ def get_agent_by_filename(self, filename: str) -> Optional[Dict[str, Any]]:
68
+ """Get agent info by filename"""
69
+ mapping = self.load_agents_mapping()
70
+ return mapping.get(filename)
71
+
72
+ def get_agent_by_uuid(self, uuid: str) -> Optional[Tuple[str, Dict[str, Any]]]:
73
+ """Get agent filename and info by UUID"""
74
+ mapping = self.load_agents_mapping()
75
+
76
+ for filename, info in mapping.items():
77
+ if info.get("uuid") == uuid:
78
+ return filename, info
79
+
80
+ return None
81
+
82
+ def list_agents(self) -> List[Tuple[str, Dict[str, Any]]]:
83
+ """List all agents in the project"""
84
+ mapping = self.load_agents_mapping()
85
+ agents = []
86
+
87
+ for filename, info in mapping.items():
88
+ # Check if the file actually exists
89
+ file_path = self.project_root / filename
90
+ if file_path.exists():
91
+ agents.append((filename, info))
92
+
93
+ return agents
94
+
95
+ def remove_agent_mapping(self, filename: str) -> bool:
96
+ """Remove an agent mapping"""
97
+ mapping = self.load_agents_mapping()
98
+
99
+ if filename in mapping:
100
+ del mapping[filename]
101
+ self.save_agents_mapping(mapping)
102
+ return True
103
+
104
+ return False
105
+
106
+ def sanitize_filename(self, name: str, default: str = "agent_wrapper") -> str:
107
+ """Convert agent name to safe filename"""
108
+ if not name or not name.strip():
109
+ return f"{default}.py"
110
+
111
+ # Replace spaces and special chars with underscores
112
+ safe_name = re.sub(r"[^\w\s-]", "", name)
113
+ safe_name = re.sub(r"[-\s]+", "_", safe_name)
114
+ safe_name = safe_name.lower().strip("_")
115
+
116
+ # Ensure it's not empty and has .py extension
117
+ if not safe_name:
118
+ safe_name = default
119
+
120
+ return f"{safe_name}.py"
121
+
122
+ def find_available_filename(self, preferred_name: str) -> str:
123
+ """Find an available filename, adding numbers if needed"""
124
+ base_name = preferred_name
125
+ if base_name.endswith(".py"):
126
+ base_name = base_name[:-3]
127
+
128
+ counter = 1
129
+ filename = f"{base_name}.py"
130
+
131
+ while (self.project_root / filename).exists():
132
+ filename = f"{base_name}_{counter}.py"
133
+ counter += 1
134
+
135
+ return filename
136
+
137
+ def load_config(self) -> Dict[str, Any]:
138
+ """Load configuration from .snowglobe/config.json"""
139
+ if not self.config_file.exists():
140
+ return {}
141
+
142
+ try:
143
+ with open(self.config_file, "r") as f:
144
+ return json.load(f)
145
+ except (json.JSONDecodeError, IOError):
146
+ return {}
147
+
148
+ def save_config(self, config: Dict[str, Any]) -> None:
149
+ """Save configuration to .snowglobe/config.json"""
150
+ self.snowglobe_dir.mkdir(exist_ok=True)
151
+
152
+ with open(self.config_file, "w") as f:
153
+ json.dump(config, f, indent=2, sort_keys=True)
154
+
155
+ def get_api_key(self) -> Optional[str]:
156
+ """Get API key from config or environment"""
157
+ # Check environment first
158
+ api_key = os.getenv("SNOWGLOBE_API_KEY") or os.getenv("GUARDRAILS_API_KEY")
159
+ if api_key:
160
+ return api_key
161
+
162
+ # Check .snowglobe/config.json
163
+ config = self.load_config()
164
+ api_key = config.get("api_key")
165
+ if api_key:
166
+ return api_key
167
+
168
+ # Check legacy .snowgloberc in current directory
169
+ rc_path = self.project_root / ".snowgloberc"
170
+ if rc_path.exists():
171
+ try:
172
+ with open(rc_path, "r") as f:
173
+ for line in f:
174
+ if line.startswith("SNOWGLOBE_API_KEY="):
175
+ return line.strip().split("=", 1)[1]
176
+ except IOError:
177
+ pass
178
+
179
+ return None
180
+
181
+ def set_api_key(self, api_key: str) -> None:
182
+ """Set API key in config"""
183
+ config = self.load_config()
184
+ config["api_key"] = api_key
185
+ self.save_config(config)
186
+
187
+ def get_control_plane_url(self) -> str:
188
+ """Get control plane URL from config or environment"""
189
+ # Check environment first
190
+ url = os.getenv("CONTROL_PLANE_URL")
191
+ if url:
192
+ return url
193
+
194
+ # Check .snowglobe/config.json
195
+ config = self.load_config()
196
+ url = config.get("control_plane_url")
197
+ if url:
198
+ return url
199
+
200
+ # Check legacy .snowgloberc in current directory
201
+ rc_path = self.project_root / ".snowgloberc"
202
+ if rc_path.exists():
203
+ try:
204
+ with open(rc_path, "r") as f:
205
+ for line in f:
206
+ if line.startswith("CONTROL_PLANE_URL="):
207
+ return line.strip().split("=", 1)[1]
208
+ except IOError:
209
+ pass
210
+
211
+ return "https://api.snowglobe.guardrailsai.com"
212
+
213
+ def set_control_plane_url(self, url: str) -> None:
214
+ """Set control plane URL in config"""
215
+ config = self.load_config()
216
+ config["control_plane_url"] = url
217
+ self.save_config(config)
218
+
219
+ def migrate_legacy_config(self) -> bool:
220
+ """Migrate .snowgloberc to .snowglobe/config.json"""
221
+ rc_path = self.project_root / ".snowgloberc"
222
+ if not rc_path.exists():
223
+ return False
224
+
225
+ config = self.load_config()
226
+ migrated = False
227
+
228
+ try:
229
+ with open(rc_path, "r") as f:
230
+ for line in f:
231
+ line = line.strip()
232
+ if line.startswith("SNOWGLOBE_API_KEY="):
233
+ api_key = line.split("=", 1)[1]
234
+ if api_key and not config.get("api_key"):
235
+ config["api_key"] = api_key
236
+ migrated = True
237
+ elif line.startswith("CONTROL_PLANE_URL="):
238
+ url = line.split("=", 1)[1]
239
+ if url and not config.get("control_plane_url"):
240
+ config["control_plane_url"] = url
241
+ migrated = True
242
+
243
+ if migrated:
244
+ self.save_config(config)
245
+ # Optionally remove the old file
246
+ # rc_path.unlink() # Uncomment to delete legacy file
247
+
248
+ return migrated
249
+ except IOError:
250
+ return False
251
+
252
+ def validate_project(self) -> Tuple[bool, List[str]]:
253
+ """Validate project structure and return issues"""
254
+ issues = []
255
+
256
+ # Check if .snowglobe directory exists
257
+ if not self.snowglobe_dir.exists():
258
+ issues.append(
259
+ "No .snowglobe directory found. Run 'snowglobe-connect init' to set up."
260
+ )
261
+ return False, issues
262
+
263
+ # Check if agents.json exists and is valid
264
+ if not self.agents_file.exists():
265
+ issues.append("No agents.json file found in .snowglobe/")
266
+ else:
267
+ try:
268
+ mapping = self.load_agents_mapping()
269
+
270
+ # Check for orphaned files
271
+ for filename in mapping.keys():
272
+ file_path = self.project_root / filename
273
+ if not file_path.exists():
274
+ issues.append(f"Agent file missing: {filename}")
275
+
276
+ # Check for unmapped agent files
277
+ for py_file in self.project_root.glob("*_wrapper.py"):
278
+ if py_file.name not in mapping:
279
+ issues.append(f"Unmapped agent file found: {py_file.name}")
280
+
281
+ except Exception as e:
282
+ issues.append(f"Invalid agents.json file: {e}")
283
+
284
+ is_valid = len(issues) == 0
285
+ return is_valid, issues
286
+
287
+
288
+ def get_project_manager(project_root: str = None) -> ProjectManager:
289
+ """Get a ProjectManager instance"""
290
+ return ProjectManager(project_root)
@@ -0,0 +1,53 @@
1
+ """
2
+ Shared statistics tracking for Snowglobe client.
3
+ This module avoids circular imports by providing a neutral location
4
+ for stats that both app.py and cli.py need to access.
5
+ """
6
+
7
+ import datetime
8
+
9
+ # Tracking state
10
+ ui_stats = {
11
+ "last_activity_time": None,
12
+ "start_time": None,
13
+ "total_messages": 0,
14
+ "experiment_totals": {}, # experiment_name -> total_count
15
+ }
16
+
17
+
18
+ def initialize_stats():
19
+ """Initialize stats tracking when server starts"""
20
+ ui_stats["start_time"] = datetime.datetime.now()
21
+
22
+
23
+ def track_batch_completion(experiment_name: str, count: int):
24
+ """Track completed batch of scenarios"""
25
+ ui_stats["total_messages"] += count
26
+ if experiment_name not in ui_stats["experiment_totals"]:
27
+ ui_stats["experiment_totals"][experiment_name] = 0
28
+ ui_stats["experiment_totals"][experiment_name] += count
29
+ ui_stats["last_activity_time"] = datetime.datetime.now()
30
+
31
+
32
+ def get_shutdown_stats():
33
+ """Get stats for graceful shutdown summary"""
34
+ if not ui_stats["start_time"]:
35
+ return None
36
+
37
+ uptime = datetime.datetime.now() - ui_stats["start_time"]
38
+ hours = int(uptime.total_seconds() // 3600)
39
+ minutes = int((uptime.total_seconds() % 3600) // 60)
40
+ seconds = int(uptime.total_seconds() % 60)
41
+
42
+ if hours > 0:
43
+ uptime_str = f"{hours}h {minutes}m {seconds}s"
44
+ elif minutes > 0:
45
+ uptime_str = f"{minutes}m {seconds}s"
46
+ else:
47
+ uptime_str = f"{seconds}s"
48
+
49
+ return {
50
+ "total_messages": ui_stats["total_messages"],
51
+ "experiment_totals": ui_stats["experiment_totals"],
52
+ "uptime": uptime_str,
53
+ }
@@ -0,0 +1,117 @@
1
+ from logging import getLogger
2
+
3
+ import httpx
4
+
5
+ from .config import config
6
+ from .models import SnowglobeData, SnowglobeMessage
7
+
8
+ LOGGER = getLogger(__name__)
9
+
10
+
11
+ async def fetch_experiments(app_id: str = None) -> list[dict]:
12
+ """
13
+ Fetch experiments from the Snowglobe server.
14
+
15
+ Returns:
16
+ list[dict]: A list of experiments.
17
+ """
18
+ async with httpx.AsyncClient() as client:
19
+ experiments_url = f"{config.CONTROL_PLANE_URL}/api/experiments?&evaluated=false"
20
+ if app_id:
21
+ experiments_url += f"&appId={config.APPLICATION_ID}"
22
+ experiments_response = await client.get(
23
+ experiments_url,
24
+ headers={"x-api-key": config.API_KEY},
25
+ )
26
+
27
+ if not experiments_response.status_code == 200:
28
+ try:
29
+ message = experiments_response.json().get("message")
30
+ except Exception:
31
+ message = experiments_response.text
32
+ LOGGER.error(f"Error fetching experiments: {message}")
33
+ raise Exception(
34
+ f"{experiments_response.status_code} - {message or 'Unknown error'}"
35
+ )
36
+ experiments = experiments_response.json()
37
+ return experiments
38
+
39
+
40
+ async def fetch_messages(*, test) -> list[SnowglobeMessage]:
41
+ """
42
+ Fetch messages from the Snowglobe server for a given test.
43
+
44
+ Args:
45
+ test (str): The test identifier.
46
+
47
+ Returns:
48
+ list[SnowglobeMessage]: A list of messages associated with the test.
49
+ """
50
+ # init messages
51
+ messages = [
52
+ SnowglobeMessage(
53
+ role="user",
54
+ content=test["prompt"],
55
+ snowglobe_data=SnowglobeData(
56
+ conversation_id=test["conversation_id"],
57
+ test_id=test["id"],
58
+ ),
59
+ ),
60
+ ]
61
+ # if full turn append response
62
+ if "response" in test and test["response"]:
63
+ messages.append(
64
+ SnowglobeMessage(
65
+ role="assistant",
66
+ content=test["response"],
67
+ snowglobe_data=SnowglobeData(
68
+ conversation_id=test["conversation_id"],
69
+ test_id=test["id"],
70
+ ),
71
+ )
72
+ )
73
+
74
+ # build rest of messages
75
+ parent_id = test.get("parent_test_id")
76
+ async with httpx.AsyncClient() as client:
77
+ while parent_id:
78
+ # get parent test
79
+ parent_test_response = await client.get(
80
+ f"{config.CONTROL_PLANE_URL}/api/experiments/{test['experiment_id']}/tests/{parent_id}",
81
+ headers={"x-api-key": config.API_KEY},
82
+ )
83
+
84
+ if not parent_test_response.status_code == 200:
85
+ raise Exception(
86
+ f"Error fetching parent test {parent_id}: {parent_test_response.text}"
87
+ )
88
+
89
+ parent_test = parent_test_response.json()
90
+
91
+ parent_id = parent_test.get("parent_test_id")
92
+
93
+ messages.insert(
94
+ 0,
95
+ SnowglobeMessage(
96
+ role="assistant",
97
+ content=parent_test["response"],
98
+ snowglobe_data=SnowglobeData(
99
+ conversation_id=parent_test["conversation_id"],
100
+ test_id=parent_test["id"],
101
+ ),
102
+ ),
103
+ )
104
+
105
+ messages.insert(
106
+ 0,
107
+ SnowglobeMessage(
108
+ role="user",
109
+ content=parent_test["prompt"],
110
+ snowglobe_data=SnowglobeData(
111
+ conversation_id=parent_test["conversation_id"],
112
+ test_id=parent_test["id"],
113
+ ),
114
+ ),
115
+ )
116
+
117
+ return messages
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: snowglobe
3
+ Version: 0.4.0
4
+ Summary: client server for usage with snowglobe experiments
5
+ Author-email: Guardrails AI <contact@guardrailsai.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 Guardrails AI
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: typer>=0.9.0
32
+ Requires-Dist: fastapi>=0.115.6
33
+ Requires-Dist: uvicorn[standard]>=0.23.0
34
+ Requires-Dist: requests>=2.31.0
35
+ Requires-Dist: pydantic>=2.11.5
36
+ Requires-Dist: APScheduler==4.0.0a6
37
+ Requires-Dist: httpx==0.28.1
38
+ Requires-Dist: rich>=13.0.0
39
+ Provides-Extra: dev
40
+ Requires-Dist: ruff<0.2.0,>=0.1.0; extra == "dev"
41
+ Requires-Dist: mypy<2.0.0,>=1.5.0; extra == "dev"
42
+ Requires-Dist: pre-commit>=4.1.0; extra == "dev"
43
+ Requires-Dist: coverage>=7.6.12; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # Snowlgobe Connect SDK
47
+
48
+ The Snowglobe Connect SDK helps you connect your AI agents to Snowglobe. It sends simulated user messages to your LLM-based application during experiments. Your application should process these messages and return a response, enabling simulated conversations and custom code based risk assessment.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ # Paste Guardrails Client or extract from guardrailsrc
54
+ export GUARDRAILS_TOKEN=$(cat ~/.guardrailsrc| awk -F 'token=' '{print $2}' | awk '{print $1}' | tr -d '\n')
55
+ ```
56
+
57
+ ```
58
+ # Install client
59
+ pip install -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
60
+ --extra-index-url="https://pypi.org/simple" snowglobe-connect
61
+ ```
62
+
63
+ If using uv, set the `--prerelease=allow` flag
64
+ ```
65
+ pip install -U --index-url="https://__token__:$GUARDRAILS_TOKEN@pypi.guardrailsai.com/simple" \
66
+ --extra-index-url="https://pypi.org/simple" --prerelease=allow snowglobe-connect
67
+ ```
68
+
69
+
70
+ ## `snowglobe-connect` commands
71
+
72
+ ```bash
73
+ snowglobe-connect auth # Sets up your API key
74
+ snowglobe-connect init # Initializes a new agent connection and creates an agent wrapper file
75
+ snowglobe-connect test # Tests your agent connection
76
+ snowglobe-connect start # Starts the process of processing simulated user messages
77
+ snowglobe-connect --help
78
+ ```
79
+
80
+ When using one of our specific preview environments in .snowgloberc one can override our server's URL with:
81
+
82
+ ```bash
83
+ CONTROL_PLANE_URL=
84
+ ```
85
+
86
+ ## Sample custom llm usage in agent wrapper file
87
+
88
+ Each agent wrapper file resides in the root directory of your project, and is named after the agent (e.g. `My Agent Name` becomes `my_agent_name.py`).
89
+
90
+ ```python
91
+ from snowglobe.client import CompletionRequest, CompletionFunctionOutputs
92
+ from openai import OpenAI
93
+ import os
94
+
95
+ client = OpenAI(api_key=os.getenv("SNOWGLOBE_API_KEY"))
96
+
97
+ def process_scenario(request: CompletionRequest) -> CompletionFunctionOutputs:
98
+ """
99
+ Process a scenario request from Snowglobe.
100
+
101
+ This function is called by the Snowglobe client to process requests. It should return a
102
+ CompletionFunctionOutputs object with the response content.
103
+
104
+ Example CompletionRequest:
105
+ CompletionRequest(
106
+ messages=[
107
+ SnowglobeMessage(role="user", content="Hello, how are you?", snowglobe_data=None),
108
+ ]
109
+ )
110
+
111
+ Example CompletionFunctionOutputs:
112
+ CompletionFunctionOutputs(response="This is a string response from your application")
113
+
114
+ Args:
115
+ request (CompletionRequest): The request object containing the messages.
116
+
117
+ Returns:
118
+ CompletionFunctionOutputs: The response object with the generated content.
119
+ """
120
+
121
+ # Process the request using the messages. Example:
122
+ messages = request.to_openai_messages()
123
+ response = client.chat.completions.create(
124
+ model="gpt-4o-mini",
125
+ messages=messages
126
+ )
127
+ return CompletionFunctionOutputs(response=response.choices[0].message.content)
128
+ ```
@@ -0,0 +1,15 @@
1
+ snowglobe/client/__init__.py,sha256=kzp9wPUUYBXqDSKZbfmD4vrAQvrWSW5HOvtpFlEJWfs,353
2
+ snowglobe/client/src/app.py,sha256=PZCTwF3n_2Bi825dOflGeN8GSjuf5ebKLSxV7S90TxA,30289
3
+ snowglobe/client/src/cli.py,sha256=f8AKB0mAcMK-63aZczs4LxnB33RqoxKPEnv2w1lgu1c,25159
4
+ snowglobe/client/src/cli_utils.py,sha256=PRWpbrALfTc3fpAXl32pfyZWkLYi_3N03csTQR1wnjc,11911
5
+ snowglobe/client/src/config.py,sha256=HAJD7RkO6IB_mFkmGLpy9Ma3sB0KpZE7zmGsa9K__iE,9284
6
+ snowglobe/client/src/models.py,sha256=BX310WrDN9Fd8v68me3XGL_ic1ulvjCrZyIT2ND1eUo,866
7
+ snowglobe/client/src/project_manager.py,sha256=Ze-qs4dQI2kIV-PmtWZ1b67hMUfsnsMHus90aT8HOow,9970
8
+ snowglobe/client/src/stats.py,sha256=IdaXroOZBmvLVa_p9pDE6hsxsc7-fBEDnLf8O6Ch0GA,1596
9
+ snowglobe/client/src/utils.py,sha256=U0nQjTjO28OghG7lV6BuI_2MkcsOhu31Nz00nCJU4sM,3670
10
+ snowglobe-0.4.0.dist-info/licenses/LICENSE,sha256=S90V6iFU5ZeSg44JQYS1To3pa7ZEobrHc_t483qSKSI,1070
11
+ snowglobe-0.4.0.dist-info/METADATA,sha256=OEuNnqFkNHhu3z0cgORX17kyKGOhoAvo7IxUfkopMTM,4921
12
+ snowglobe-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ snowglobe-0.4.0.dist-info/entry_points.txt,sha256=mqx4mTwFPHttjctE2ceYTYWCCIG30Ji2C89aaCYgHcM,71
14
+ snowglobe-0.4.0.dist-info/top_level.txt,sha256=PoyYihnCBjRyjeIT19yBcE47JTe7i1OwRXvJ4d5EohM,10
15
+ snowglobe-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ snowglobe-connect = snowglobe.client.src.cli:cli_app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Guardrails AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ snowglobe