railtracks-cli 1.1.22__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,473 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ railtracks - A Python development server with JSON API
5
+ Usage: railtracks [command]
6
+
7
+ Commands:
8
+ init Initialize railtracks environment (setup directories, download UI)
9
+ viz Start the railtracks development server
10
+ migrate Verify and migrate the structure of .railtracks/ directory
11
+
12
+ - Checks to see if there is a .railtracks directory
13
+ - If not, it creates one (and adds it to the .gitignore)
14
+ - If there is a build directory, it runs the build command
15
+ - If there is a .railtracks directory, it starts the server
16
+
17
+ For testing purposes, you can add `alias railtracks="python railtracks.py"` to your .bashrc or .zshrc
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import shutil
23
+ import socket
24
+ import sys
25
+ import tempfile
26
+ import threading
27
+ import time
28
+ import urllib.request
29
+ import webbrowser
30
+ import zipfile
31
+ from pathlib import Path
32
+ from urllib.parse import unquote
33
+
34
+ import uvicorn
35
+ from fastapi import FastAPI
36
+ from fastapi.responses import FileResponse, JSONResponse
37
+
38
+ __version__ = "1.1.22"
39
+
40
+ # TODO: Once we are releasing to PyPi change this to the release asset instead
41
+ latest_ui_url = "https://railtownazureb2c.blob.core.windows.net/cdn/rc-viz/latest.zip"
42
+
43
+ cli_name = "railtracks"
44
+ cli_directory = ".railtracks"
45
+ DEFAULT_PORT = 3030
46
+
47
+ # FastAPI app instance
48
+ app = FastAPI()
49
+
50
+
51
+ def get_script_directory():
52
+ """Get the directory where this script is located"""
53
+ return Path(__file__).parent.absolute()
54
+
55
+
56
+ def print_status(message):
57
+ print(f"[{cli_name}] {message}")
58
+
59
+
60
+ def print_success(message):
61
+ print(f"[{cli_name}] {message}")
62
+
63
+
64
+ def print_warning(message):
65
+ print(f"[{cli_name}] {message}")
66
+
67
+
68
+ def print_error(message):
69
+ print(f"[{cli_name}] {message}")
70
+
71
+
72
+ def is_port_in_use(port):
73
+ """Check if a port is already in use"""
74
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
75
+ try:
76
+ sock.bind(("localhost", port))
77
+ return False # Port is available
78
+ except OSError:
79
+ return True # Port is in use
80
+
81
+
82
+ def create_railtracks_dir():
83
+ """Create .railtracks directory if it doesn't exist and add to .gitignore"""
84
+ railtracks_dir = Path(cli_directory)
85
+ if not railtracks_dir.exists():
86
+ print_status(f"Creating {cli_directory} directory...")
87
+ railtracks_dir.mkdir(exist_ok=True)
88
+ print_success(f"Created {cli_directory} directory")
89
+
90
+ # Check if cli_directory is in .gitignore
91
+ gitignore_path = Path(".gitignore")
92
+ if gitignore_path.exists():
93
+ with open(gitignore_path) as f:
94
+ gitignore_content = f.read()
95
+
96
+ if cli_directory not in gitignore_content:
97
+ print_status(f"Adding {cli_directory} to .gitignore...")
98
+ with open(gitignore_path, "a") as f:
99
+ f.write(f"\n{cli_directory}\n")
100
+ print_success(f"Added {cli_directory} to .gitignore")
101
+ else:
102
+ print_status("Creating .gitignore file...")
103
+ with open(gitignore_path, "w") as f:
104
+ f.write(f"{cli_directory}\n")
105
+ print_success(f"Created .gitignore with {cli_directory}")
106
+
107
+
108
+ def download_and_extract_ui():
109
+ """Download the latest frontend UI and extract it to .railtracks/ui"""
110
+ ui_url = latest_ui_url
111
+ ui_dir = Path(f"{cli_directory}/ui")
112
+
113
+ print_status("Downloading latest frontend UI...")
114
+
115
+ try:
116
+ # Create temporary file for download
117
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_file:
118
+ temp_zip_path = temp_file.name
119
+
120
+ # Download the zip file
121
+ print_status(f"Downloading from: {ui_url}")
122
+ urllib.request.urlretrieve(ui_url, temp_zip_path)
123
+
124
+ # Create ui directory if it doesn't exist
125
+ ui_dir.mkdir(parents=True, exist_ok=True)
126
+
127
+ # Extract the zip file
128
+ print_status("Extracting UI files...")
129
+ with zipfile.ZipFile(temp_zip_path, "r") as zip_ref:
130
+ zip_ref.extractall(ui_dir)
131
+
132
+ # Clean up temporary file
133
+ os.unlink(temp_zip_path)
134
+
135
+ print_success("Frontend UI downloaded and extracted successfully")
136
+ print_status(f"UI files available in: {ui_dir}")
137
+
138
+ except urllib.error.URLError as e:
139
+ print_error(f"Failed to download UI: {e}")
140
+ print_error("Please check your internet connection and try again")
141
+ sys.exit(1)
142
+ except zipfile.BadZipFile as e:
143
+ print_error(f"Failed to extract UI zip file: {e}")
144
+ print_error("The downloaded file may be corrupted")
145
+ sys.exit(1)
146
+ except Exception as e:
147
+ print_error(f"Unexpected error during UI download/extraction: {e}")
148
+ sys.exit(1)
149
+
150
+
151
+ def init_railtracks():
152
+ """Initialize the railtracks environment"""
153
+ print_status("Initializing railtracks environment...")
154
+
155
+ # Setup directories
156
+ create_railtracks_dir()
157
+
158
+ # Download and extract UI
159
+ download_and_extract_ui()
160
+
161
+ print_success("railtracks initialization completed!")
162
+ print_status("You can now run 'railtracks viz' to start the server")
163
+
164
+
165
+ def migrate_railtracks():
166
+ """Migrate and verify the structure of .railtracks directory"""
167
+ print_status("Verifying .railtracks directory structure...")
168
+
169
+ # Get the .railtracks directory path
170
+ railtracks_dir = Path(cli_directory)
171
+
172
+ # Verify/create .railtracks directory
173
+ if not railtracks_dir.exists():
174
+ print_status(f"Creating {cli_directory} directory...")
175
+ railtracks_dir.mkdir(exist_ok=True)
176
+ print_success(f"Created {cli_directory} directory")
177
+
178
+ # Verify/create .railtracks/data directory
179
+ data_dir = railtracks_dir / "data"
180
+ if not data_dir.exists():
181
+ print_status("Creating .railtracks/data directory...")
182
+ data_dir.mkdir(parents=True, exist_ok=True)
183
+ print_success("Created .railtracks/data directory")
184
+
185
+ # Verify/create .railtracks/data/evaluations directory
186
+ evaluations_dir = data_dir / "evaluations"
187
+ if not evaluations_dir.exists():
188
+ print_status("Creating .railtracks/data/evaluations directory...")
189
+ evaluations_dir.mkdir(parents=True, exist_ok=True)
190
+ print_success("Created .railtracks/data/evaluations directory")
191
+
192
+ # Verify/create .railtracks/data/sessions directory
193
+ sessions_dir = data_dir / "sessions"
194
+ if not sessions_dir.exists():
195
+ print_status("Creating .railtracks/data/sessions directory...")
196
+ sessions_dir.mkdir(parents=True, exist_ok=True)
197
+ print_success("Created .railtracks/data/sessions directory")
198
+
199
+ # Find all JSON files in .railtracks root only (not recursive, not in subdirectories)
200
+ json_files = list(railtracks_dir.glob("*.json"))
201
+
202
+ if json_files:
203
+ print_status(
204
+ f"Found {len(json_files)} JSON file(s) in .railtracks root to migrate..."
205
+ )
206
+ for json_file in json_files:
207
+ destination = sessions_dir / json_file.name
208
+ shutil.move(str(json_file), str(destination))
209
+ print_success(f"Migrated {json_file.name} to .railtracks/data/sessions/")
210
+ print_success(
211
+ f"Migration completed: {len(json_files)} file(s) moved to .railtracks/data/sessions/"
212
+ )
213
+ else:
214
+ print_status("No JSON files found in .railtracks root to migrate")
215
+
216
+ print_success("Directory structure verification and migration completed!")
217
+
218
+
219
+ # FastAPI endpoints
220
+
221
+
222
+ def get_railtracks_dir():
223
+ """Get the .railtracks directory path"""
224
+ return Path(cli_directory)
225
+
226
+
227
+ def get_data_dir(subdir):
228
+ """Get a data subdirectory path (e.g., evaluations, sessions)"""
229
+ return get_railtracks_dir() / "data" / subdir
230
+
231
+
232
+ @app.get("/api/evaluations")
233
+ async def get_evaluations():
234
+ """Get all evaluation JSON files from .railtracks/data/evaluations/"""
235
+ evaluations_dir = get_data_dir("evaluations")
236
+ evaluations = []
237
+
238
+ if evaluations_dir.exists():
239
+ for file_path in evaluations_dir.glob("*.json"):
240
+ try:
241
+ with open(file_path, encoding="utf-8") as f:
242
+ content = json.load(f)
243
+ evaluations.append(content)
244
+ except (json.JSONDecodeError, IOError) as e:
245
+ print_error(f"Error reading evaluation file {file_path.name}: {e}")
246
+
247
+ return JSONResponse(content=evaluations)
248
+
249
+
250
+ @app.get("/api/sessions")
251
+ async def get_sessions():
252
+ """Get all session JSON files from .railtracks/data/sessions/"""
253
+ sessions_dir = get_data_dir("sessions")
254
+ sessions = []
255
+
256
+ if sessions_dir.exists():
257
+ for file_path in sessions_dir.glob("*.json"):
258
+ try:
259
+ with open(file_path, encoding="utf-8") as f:
260
+ content = json.load(f)
261
+ sessions.append(content)
262
+ except (json.JSONDecodeError, IOError) as e:
263
+ print_error(f"Error reading session file {file_path.name}: {e}")
264
+
265
+ return JSONResponse(content=sessions)
266
+
267
+
268
+ @app.get("/api/files")
269
+ async def get_files():
270
+ """
271
+ DEPRECATED: This endpoint is deprecated and kept for old visualizer compatibility.
272
+ List JSON files in .railtracks directory
273
+ """
274
+ railtracks_dir = get_railtracks_dir()
275
+ json_files = []
276
+
277
+ try:
278
+ if railtracks_dir.exists():
279
+ for file_path in railtracks_dir.glob("*.json"):
280
+ json_files.append(
281
+ {
282
+ "name": file_path.name,
283
+ "size": file_path.stat().st_size,
284
+ "modified": file_path.stat().st_mtime,
285
+ }
286
+ )
287
+
288
+ response = JSONResponse(content=json_files)
289
+ response.headers["Deprecated"] = "true"
290
+ return response
291
+ except Exception as e:
292
+ print_error(f"Error handling /api/files: {e}")
293
+ return JSONResponse(content={"error": "Internal Server Error"}, status_code=500)
294
+
295
+
296
+ @app.get("/api/json/{filename:path}")
297
+ async def get_json_file(filename: str):
298
+ """
299
+ DEPRECATED: This endpoint is deprecated and kept for old visualizer compatibility.
300
+ Load specific JSON file from .railtracks directory
301
+ """
302
+ railtracks_dir = get_railtracks_dir()
303
+
304
+ try:
305
+ # URL decode the filename to handle spaces and special characters
306
+ filename = unquote(filename)
307
+ if not filename.endswith(".json"):
308
+ filename += ".json"
309
+
310
+ file_path = railtracks_dir / filename
311
+
312
+ if not file_path.exists():
313
+ return JSONResponse(
314
+ content={"error": f"File {filename} not found"}, status_code=404
315
+ )
316
+
317
+ # Read and parse JSON file
318
+ with open(file_path, encoding="utf-8") as f:
319
+ content = f.read()
320
+ # Validate JSON
321
+ json_data = json.loads(content)
322
+
323
+ response = JSONResponse(content=json_data)
324
+ response.headers["Deprecated"] = "true"
325
+ return response
326
+
327
+ except json.JSONDecodeError as e:
328
+ print_error(f"Invalid JSON in {filename}: {e}")
329
+ return JSONResponse(content={"error": f"Invalid JSON: {e}"}, status_code=400)
330
+ except Exception as e:
331
+ print_error(f"Error handling /api/json/{filename}: {e}")
332
+ return JSONResponse(content={"error": "Internal Server Error"}, status_code=500)
333
+
334
+
335
+ @app.post("/api/refresh")
336
+ async def refresh():
337
+ """
338
+ DEPRECATED: This endpoint is deprecated and kept for old visualizer compatibility.
339
+ Trigger frontend refresh
340
+ """
341
+ print_status("Frontend refresh triggered")
342
+ response = JSONResponse(content={"status": "refresh_triggered"})
343
+ response.headers["Deprecated"] = "true"
344
+ return response
345
+
346
+
347
+ @app.get("/{full_path:path}")
348
+ async def serve_ui_or_404(full_path: str):
349
+ """Serve UI files with SPA routing fallback (catch-all route)"""
350
+ # Skip API routes
351
+ if full_path.startswith("api/"):
352
+ return JSONResponse(content={"error": "Not Found"}, status_code=404)
353
+
354
+ ui_dir = Path(f"{cli_directory}/ui")
355
+ ui_file = ui_dir / full_path
356
+ if ui_file.exists() and ui_file.is_file():
357
+ return FileResponse(str(ui_file))
358
+ # Fallback to index.html for SPA routing
359
+ index_file = ui_dir / "index.html"
360
+ if index_file.exists():
361
+ return FileResponse(str(index_file))
362
+ return JSONResponse(content={"error": "File not found"}, status_code=404)
363
+
364
+
365
+ class RailtracksServer:
366
+ """Main server class"""
367
+
368
+ def __init__(self, port=DEFAULT_PORT):
369
+ self.port = port
370
+ self.running = False
371
+ self.config = None
372
+
373
+ def start(self):
374
+ """Start the FastAPI server"""
375
+ self.running = True
376
+
377
+ # Print server info
378
+ print_success(f"🚀 railtracks server running at http://localhost:{self.port}")
379
+ print_status(f"📁 Serving files from: {cli_directory}/ui/")
380
+ print_status("📋 API endpoints:")
381
+ print_status(" GET /api/evaluations - Get all evaluation JSON files")
382
+ print_status(" GET /api/sessions - Get all session JSON files")
383
+ print_status(" GET /api/files - List JSON files (deprecated)")
384
+ print_status(" GET /api/json/{filename} - Load JSON file (deprecated)")
385
+ print_status(" POST /api/refresh - Trigger frontend refresh (deprecated)")
386
+ print_status("Press Ctrl+C to stop the server")
387
+
388
+ # Open browser after a short delay to ensure server is ready
389
+ def open_browser():
390
+ time.sleep(1) # Give server a moment to fully start
391
+ url = f"http://localhost:{self.port}"
392
+ print_status(f"Opening browser to {url}")
393
+ try:
394
+ webbrowser.open(url)
395
+ except Exception as e:
396
+ print_warning(f"Could not open browser automatically: {e}")
397
+ print_status(f"Please manually open: {url}")
398
+
399
+ browser_thread = threading.Thread(target=open_browser)
400
+ browser_thread.daemon = True
401
+ browser_thread.start()
402
+
403
+ # Start uvicorn server
404
+ try:
405
+ config = uvicorn.Config(
406
+ app,
407
+ host="localhost",
408
+ port=self.port,
409
+ log_level="info",
410
+ access_log=False, # We handle our own logging
411
+ )
412
+ server = uvicorn.Server(config)
413
+ self.config = config
414
+ server.run()
415
+ except KeyboardInterrupt:
416
+ self.stop()
417
+
418
+ def stop(self):
419
+ """Stop the server and cleanup"""
420
+ if self.running:
421
+ print_status("Shutting down railtracks...")
422
+ self.running = False
423
+
424
+ print_success("railtracks stopped.")
425
+
426
+
427
+ def main():
428
+ """Main function"""
429
+ if len(sys.argv) < 2:
430
+ print(f"Usage: {cli_name} [command]")
431
+ print("")
432
+ print("Commands:")
433
+ print(
434
+ f" init Initialize {cli_name} environment (setup directories, download portable UI)"
435
+ )
436
+ print(f" viz Start the {cli_name} development server")
437
+ print(f" migrate Verify and migrate the structure of .{cli_name}/ directory")
438
+ print("")
439
+ print("Examples:")
440
+ print(f" {cli_name} init # Initialize development environment")
441
+ print(f" {cli_name} viz # Start visualizer web app")
442
+ print(
443
+ f" {cli_name} migrate # Verify and migrate .{cli_name}/ directory structure"
444
+ )
445
+ sys.exit(1)
446
+
447
+ command = sys.argv[1]
448
+
449
+ if command == "init":
450
+ init_railtracks()
451
+ elif command == "viz":
452
+ # Check if port is already in use
453
+ if is_port_in_use(DEFAULT_PORT):
454
+ print_error(f"Port {DEFAULT_PORT} is already in use!")
455
+ print_status("Please stop the existing server.")
456
+ sys.exit(1)
457
+
458
+ # Setup directories
459
+ create_railtracks_dir()
460
+
461
+ # Start server
462
+ server = RailtracksServer()
463
+ server.start()
464
+ elif command == "migrate":
465
+ migrate_railtracks()
466
+ else:
467
+ print(f"Unknown command: {command}")
468
+ print("Available commands: init, viz, migrate")
469
+ sys.exit(1)
470
+
471
+
472
+ if __name__ == "__main__":
473
+ main()
@@ -0,0 +1,6 @@
1
+ """Entry point for railtracks CLI when run as a module"""
2
+
3
+ from railtracks_cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: railtracks-cli
3
+ Version: 1.1.22
4
+ Summary: railtracks - A Python development server with JSON API
5
+ Author-email: Logan Underwood <logan@railtown.ai>, Levi Varsanyi <levi@railtown.ai>, Jaime Bueza <jaime@railtown.ai>, Amir Refaee <amir@railtown.ai>, Aryan Ballani <aryan@railtown.ai>, Tristan Brown <tristan@railtown.ai>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Natural Language :: English
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
+ Classifier: Environment :: Console
22
+ Classifier: Typing :: Typed
23
+ License-File: LICENSE
24
+ Requires-Dist: railtracks
25
+ Requires-Dist: fastapi
26
+ Requires-Dist: uvicorn[standard]
27
+ Project-URL: Release Notes, https://github.com/RailtownAI/railtracks/releases
28
+ Project-URL: documentation, https://railtownai.github.io/railtracks/
29
+ Project-URL: home, https://railtracks.org
30
+ Project-URL: repository, https://github.com/RailtownAI/railtracks
31
+
32
+ # Railtracks CLI
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/railtracks-cli)](https://github.com/RailtownAI/railtracks/releases)
35
+ [![Python Versions](https://img.shields.io/pypi/pyversions/railtracks-cli?logo=python&)](https://pypi.org/project/railtracks/)
36
+ [![License](https://img.shields.io/pypi/l/railtracks-cli)](https://opensource.org/licenses/MIT)
37
+ [![PyPI - Downloads](https://img.shields.io/pepy/dt/railtracks-cli)](https://pypistats.org/packages/railtracks-cli)
38
+ [![Docs](https://img.shields.io/badge/docs-latest-00BFFF.svg?logo=)](https://railtownai.github.io/railtracks/)
39
+ [![GitHub stars](https://img.shields.io/github/stars/RailtownAI/railtracks.svg?style=social&label=Star)](https://github.com/RailtownAI/railtracks)
40
+ [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/h5ZcahDc)
41
+
42
+ A simple CLI to help developers visualize and debug their agents.
43
+
44
+ ## What is Railtracks CLI?
45
+
46
+ Railtracks CLI is a development tool that provides:
47
+
48
+ - **Local Development Server**: A web-based visualizer for your railtracks projects
49
+ - **JSON API**: RESTful endpoints to interact with your project data
50
+ - **Modern UI**: A downloadable frontend interface for project visualization
51
+
52
+ ## Quick Start
53
+
54
+ ### 1. Installation
55
+
56
+ ```bash
57
+ pip install railtracks-cli
58
+ ```
59
+
60
+ ### 2. Initialize Your Project
61
+
62
+ First, initialize the railtracks environment in your project directory:
63
+
64
+ ```bash
65
+ railtracks init
66
+ ```
67
+
68
+ This command will:
69
+
70
+ - Create a `.railtracks` directory in your project
71
+ - Add `.railtracks` to your `.gitignore` file
72
+ - Download and extract the latest frontend UI
73
+
74
+ ### 3. Start the Development Server
75
+
76
+ ```bash
77
+ railtracks viz
78
+ ```
79
+
80
+ This starts the development server at `http://localhost:3030` with:
81
+
82
+ - API endpoints for data access
83
+ - Portable Web-based visualizer interface that can be opened in any web environment (web, mobile, vs extension, chrome extension, etc)
84
+
85
+ ## Project Structure
86
+
87
+ After initialization, your project will have this structure:
88
+
89
+ ```
90
+ your-project/
91
+ ├── .railtracks/ # Railtracks working directory
92
+ │ ├── ui/ # Frontend interface files
93
+ │ └── *.json # Your project JSON files
94
+ ├── .gitignore # Updated to exclude .railtracks
95
+ └── your-source-files/ # Your actual project files
96
+ ```
97
+
@@ -0,0 +1,7 @@
1
+ railtracks_cli/__init__.py,sha256=UzP2XxVnz5m2tbtPiTrS8-VK3IJEda9M1uAVGhwNpms,15810
2
+ railtracks_cli/__main__.py,sha256=1LnxwDolMoaJ1kuSNMNgsm5DDSOWy6Q8RTh8P-FKGIU,130
3
+ railtracks_cli-1.1.22.dist-info/entry_points.txt,sha256=mc-9r0VsRzZe4DvKJqBTKcAq58SPphRKi-X2ZLun8hE,50
4
+ railtracks_cli-1.1.22.dist-info/licenses/LICENSE,sha256=Y5ir_N69ZJf1coSB9f1xW7CP17TP5gI-YwUSg7xWewM,1078
5
+ railtracks_cli-1.1.22.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
6
+ railtracks_cli-1.1.22.dist-info/METADATA,sha256=cwc8LaErS3ZC1hAaXh7fJ72QFKW5STdPTL-UWnCCEOA,3829
7
+ railtracks_cli-1.1.22.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ railtracks=railtracks_cli:main
3
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Railtown 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
13
+ all 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
21
+ THE SOFTWARE.