slide-narrator 5.5.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,558 @@
1
+ Metadata-Version: 2.4
2
+ Name: slide-narrator
3
+ Version: 5.5.0
4
+ Summary: Thread and file storage components for conversational AI - the companion to Tyler AI framework
5
+ Project-URL: Homepage, https://github.com/adamwdraper/slide
6
+ Project-URL: Repository, https://github.com/adamwdraper/slide
7
+ Project-URL: Bug Tracker, https://github.com/adamwdraper/slide/issues
8
+ Author: adamwdraper
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: aiosqlite>=0.21.0
21
+ Requires-Dist: alembic>=1.14.1
22
+ Requires-Dist: asyncpg>=0.30.0
23
+ Requires-Dist: click>=8.1.8
24
+ Requires-Dist: filetype>=1.2.0
25
+ Requires-Dist: greenlet>=3.2.3
26
+ Requires-Dist: pydantic>=2.10.4
27
+ Requires-Dist: pypdf>=5.3.0
28
+ Requires-Dist: sqlalchemy>=2.0.36
29
+ Requires-Dist: typing-extensions>=4.12.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: coverage>=7.6.10; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.25.2; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
34
+ Requires-Dist: pytest>=8.3.4; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # The Narrator
38
+
39
+ Thread and file storage components for conversational AI - the storage foundation for the Slide ecosystem.
40
+
41
+ ## Overview
42
+
43
+ The Narrator provides robust, production-ready storage solutions for conversational AI applications, serving as the storage layer for Tyler and other Slide components. It includes:
44
+
45
+ - **ThreadStore**: Persistent storage for conversation threads with support for both in-memory and SQL backends
46
+ - **FileStore**: Secure file storage with automatic processing for various file types
47
+ - **Models**: Pydantic models for threads, messages, and attachments
48
+ - **CLI Tools**: Command-line interface for database management and setup
49
+
50
+ ## Features
51
+
52
+ ### ThreadStore
53
+ - **Multiple Backends**: In-memory (development), SQLite (local), PostgreSQL (production)
54
+ - **Async/Await Support**: Built for modern Python async applications
55
+ - **Message Filtering**: Automatic handling of system vs. user messages
56
+ - **Platform Integration**: Support for external platform references (Slack, Discord, etc.)
57
+ - **Connection Pooling**: Production-ready database connection management
58
+
59
+ ### FileStore
60
+ - **Secure Storage**: Automatic file validation and type checking
61
+ - **Multiple Formats**: Support for documents, images, audio, and more
62
+ - **Content Processing**: Automatic text extraction from PDFs, image analysis
63
+ - **Storage Limits**: Configurable file size and total storage limits
64
+ - **Sharded Storage**: Efficient file organization to prevent directory bloat
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ # Using uv (recommended)
70
+ uv add slide-narrator
71
+
72
+ # Using pip (fallback)
73
+ pip install slide-narrator
74
+ ```
75
+
76
+ ## Setup
77
+
78
+ ### Docker Setup (Recommended for Development)
79
+
80
+ For local development with PostgreSQL, Narrator includes built-in Docker commands:
81
+
82
+ ```bash
83
+ # One-command setup: starts PostgreSQL and initializes tables
84
+ uv run narrator docker-setup
85
+
86
+ # This will:
87
+ # 1. Start a PostgreSQL container
88
+ # 2. Wait for it to be ready
89
+ # 3. Initialize the database tables
90
+ # 4. Show you the connection string
91
+
92
+ # The database will be available at:
93
+ # postgresql+asyncpg://narrator:narrator_dev@localhost:5432/narrator
94
+ ```
95
+
96
+ To manage the Docker container:
97
+
98
+ ```bash
99
+ # Stop container (preserves data)
100
+ uv run narrator docker-stop
101
+
102
+ # Stop and remove all data
103
+ uv run narrator docker-stop --remove-volumes
104
+
105
+ # Start container again
106
+ uv run narrator docker-start
107
+
108
+ # Check database status
109
+ uv run narrator status
110
+ ```
111
+
112
+ For custom configurations, the Docker commands respect environment variables:
113
+
114
+ ```bash
115
+ # Use a different port
116
+ uv run narrator docker-setup --port 5433
117
+
118
+ # Or set environment variables (matching docker-compose.yml)
119
+ export NARRATOR_DB_NAME=mydb
120
+ export NARRATOR_DB_USER=myuser
121
+ export NARRATOR_DB_PASSWORD=mypassword
122
+ export NARRATOR_DB_PORT=5433
123
+
124
+ # Then run docker-setup
125
+ uv run narrator docker-setup
126
+
127
+ # This will create:
128
+ # postgresql+asyncpg://myuser:mypassword@localhost:5433/mydb
129
+ ```
130
+
131
+ ### Database Setup
132
+
133
+ For production use with PostgreSQL or SQLite persistence, you'll need to initialize the database tables:
134
+
135
+ ```bash
136
+ # Initialize database tables (PostgreSQL)
137
+ uv run narrator init --database-url "postgresql+asyncpg://user:password@localhost/dbname"
138
+
139
+ # Initialize database tables (SQLite)
140
+ uv run narrator init --database-url "sqlite+aiosqlite:///path/to/your/database.db"
141
+
142
+ # Check database status
143
+ uv run narrator status --database-url "postgresql+asyncpg://user:password@localhost/dbname"
144
+ ```
145
+
146
+ You can also use environment variables instead of passing the database URL:
147
+
148
+ ```bash
149
+ # Set environment variable
150
+ export NARRATOR_DATABASE_URL="postgresql+asyncpg://user:password@localhost/dbname"
151
+
152
+ # Then run without --database-url flag
153
+ uv run narrator init
154
+ uv run narrator status
155
+ ```
156
+
157
+ ### Environment Variables
158
+
159
+ Configure the narrator using environment variables:
160
+
161
+ ```bash
162
+ # Database settings
163
+ NARRATOR_DATABASE_URL="postgresql+asyncpg://user:password@localhost/dbname"
164
+ NARRATOR_DB_POOL_SIZE=5 # Connection pool size
165
+ NARRATOR_DB_MAX_OVERFLOW=10 # Max additional connections
166
+ NARRATOR_DB_POOL_TIMEOUT=30 # Connection timeout (seconds)
167
+ NARRATOR_DB_POOL_RECYCLE=300 # Connection recycle time (seconds)
168
+ NARRATOR_DB_ECHO=false # Enable SQL logging
169
+
170
+ # File storage settings
171
+ NARRATOR_FILE_STORAGE_PATH=/path/to/files # Storage directory
172
+ NARRATOR_MAX_FILE_SIZE=52428800 # 50MB max file size
173
+ NARRATOR_MAX_STORAGE_SIZE=5368709120 # 5GB max total storage
174
+ NARRATOR_ALLOWED_MIME_TYPES=image/jpeg,application/pdf # Allowed file types
175
+
176
+ # Logging
177
+ NARRATOR_LOG_LEVEL=INFO # Log level
178
+ ```
179
+
180
+ ## Quick Start
181
+
182
+ ### Basic Thread Storage
183
+
184
+ ```python
185
+ import asyncio
186
+ from narrator import ThreadStore, Thread, Message
187
+
188
+ async def main():
189
+ # Create an in-memory store for development
190
+ store = await ThreadStore.create()
191
+
192
+ # Create a thread
193
+ thread = Thread(title="My Conversation")
194
+
195
+ # Add messages
196
+ thread.add_message(Message(role="user", content="Hello!"))
197
+ thread.add_message(Message(role="assistant", content="Hi there!"))
198
+
199
+ # Save the thread
200
+ await store.save(thread)
201
+
202
+ # Retrieve the thread
203
+ retrieved = await store.get(thread.id)
204
+ print(f"Thread: {retrieved.title}")
205
+ print(f"Messages: {len(retrieved.messages)}")
206
+
207
+ asyncio.run(main())
208
+ ```
209
+
210
+ ### File Storage
211
+
212
+ ```python
213
+ import asyncio
214
+ from narrator import FileStore
215
+
216
+ async def main():
217
+ # Create a file store
218
+ store = await FileStore.create()
219
+
220
+ # Save a file
221
+ content = b"Hello, world!"
222
+ metadata = await store.save(content, "hello.txt", "text/plain")
223
+
224
+ print(f"File ID: {metadata['id']}")
225
+ print(f"Storage path: {metadata['storage_path']}")
226
+
227
+ # Retrieve the file
228
+ retrieved_content = await store.get(metadata['id'])
229
+ print(f"Content: {retrieved_content.decode()}")
230
+
231
+ asyncio.run(main())
232
+ ```
233
+
234
+ ### Database Storage
235
+
236
+ ```python
237
+ import asyncio
238
+ from narrator import ThreadStore
239
+
240
+ async def main():
241
+ # Use SQLite for persistent storage
242
+ store = await ThreadStore.create("sqlite+aiosqlite:///conversations.db")
243
+
244
+ # Use PostgreSQL for production
245
+ # store = await ThreadStore.create("postgresql+asyncpg://user:pass@localhost/dbname")
246
+
247
+ # The API is the same regardless of backend
248
+ thread = Thread(title="Persistent Conversation")
249
+ await store.save(thread)
250
+
251
+ asyncio.run(main())
252
+ ```
253
+
254
+ ## Configuration
255
+
256
+ ### Database Configuration
257
+
258
+ The Narrator supports multiple database backends:
259
+
260
+ #### Memory storage (Default)
261
+ ```python
262
+ from narrator import ThreadStore
263
+
264
+ # Use factory pattern for immediate connection validation
265
+ store = await ThreadStore.create() # Uses memory backend
266
+
267
+ # Thread operations are immediate
268
+ thread = Thread()
269
+ await store.save(thread)
270
+ ```
271
+
272
+ Key characteristics:
273
+ - Fastest possible performance (direct dictionary access)
274
+ - No persistence (data is lost when program exits)
275
+ - No setup required (works out of the box)
276
+ - Perfect for scripts and one-off conversations
277
+ - Great for testing and development
278
+
279
+ #### PostgreSQL storage
280
+ ```python
281
+ from narrator import ThreadStore
282
+
283
+ # Use factory pattern for immediate connection validation
284
+ db_url = "postgresql+asyncpg://user:pass@localhost/dbname"
285
+ try:
286
+ store = await ThreadStore.create(db_url)
287
+ print("Connected to database successfully")
288
+ except Exception as e:
289
+ print(f"Database connection failed: {e}")
290
+ # Handle connection failure appropriately
291
+
292
+ # Must save threads and changes to persist
293
+ thread = Thread()
294
+ await store.save(thread) # Required
295
+ thread.add_message(message)
296
+ await store.save(thread) # Save changes
297
+
298
+ # Always use thread.id with database storage
299
+ result = await store.get(thread.id)
300
+ ```
301
+
302
+ Key characteristics:
303
+ - Async operations for non-blocking I/O
304
+ - Persistent storage (data survives program restarts)
305
+ - Cross-session support (can access threads from different processes)
306
+ - Production-ready
307
+ - Automatic schema management through SQLAlchemy
308
+ - Connection validation at startup with factory pattern
309
+
310
+ #### SQLite storage
311
+ ```python
312
+ from narrator import ThreadStore
313
+
314
+ # Use factory pattern for immediate connection validation
315
+ db_url = "sqlite+aiosqlite:///path/to/db.sqlite"
316
+ store = await ThreadStore.create(db_url)
317
+
318
+ # Or use in-memory SQLite database
319
+ store = await ThreadStore.create("sqlite+aiosqlite://") # In-memory SQLite
320
+ ```
321
+
322
+ ### File Storage Configuration
323
+
324
+ ```python
325
+ from narrator import FileStore
326
+
327
+ # Create a FileStore instance with factory pattern
328
+ file_store = await FileStore.create(
329
+ base_path="/path/to/files", # Optional custom path
330
+ max_file_size=100 * 1024 * 1024, # 100MB (optional)
331
+ max_storage_size=10 * 1024 * 1024 * 1024 # 10GB (optional)
332
+ )
333
+
334
+ # Or use default settings from environment variables
335
+ file_store = await FileStore.create()
336
+ ```
337
+
338
+ ## Advanced Usage
339
+
340
+ ### Using ThreadStore and FileStore Together
341
+
342
+ ```python
343
+ import asyncio
344
+ from narrator import ThreadStore, FileStore, Thread, Message
345
+
346
+ async def main():
347
+ # Create stores
348
+ thread_store = await ThreadStore.create("sqlite+aiosqlite:///main.db")
349
+ file_store = await FileStore.create("/path/to/files")
350
+
351
+ # Create a thread with file attachment
352
+ thread = Thread(title="Document Discussion")
353
+
354
+ # Create a message with an attachment
355
+ message = Message(role="user", content="Here's a document")
356
+
357
+ # Add file content
358
+ pdf_content = b"..." # Your PDF content
359
+ message.add_attachment(pdf_content, filename="document.pdf")
360
+
361
+ thread.add_message(message)
362
+
363
+ # Save thread (attachments are processed automatically)
364
+ await thread_store.save(thread)
365
+
366
+ print(f"Thread saved with ID: {thread.id}")
367
+
368
+ asyncio.run(main())
369
+ ```
370
+
371
+ ### Message Attachments
372
+
373
+ Messages can include file attachments that are automatically processed:
374
+
375
+ ```python
376
+ import asyncio
377
+ from narrator import Thread, Message, Attachment, FileStore
378
+
379
+ async def main():
380
+ file_store = await FileStore.create()
381
+
382
+ # Create a message with an attachment
383
+ message = Message(role="user", content="Here's a document")
384
+
385
+ # Add file content
386
+ pdf_content = b"..." # Your PDF content
387
+ attachment = Attachment(filename="document.pdf", content=pdf_content)
388
+ message.add_attachment(attachment)
389
+
390
+ # Process and store the attachment
391
+ await attachment.process_and_store(file_store)
392
+
393
+ # The attachment now has extracted text and metadata
394
+ print(f"Status: {attachment.status}")
395
+ print(f"File ID: {attachment.file_id}")
396
+ if attachment.attributes:
397
+ print(f"Extracted text: {attachment.attributes.get('text', 'N/A')[:100]}...")
398
+
399
+ asyncio.run(main())
400
+ ```
401
+
402
+ ### Platform Integration
403
+
404
+ Threads can be linked to external platforms:
405
+
406
+ ```python
407
+ import asyncio
408
+ from narrator import Thread, ThreadStore
409
+
410
+ async def main():
411
+ store = await ThreadStore.create()
412
+
413
+ # Create a thread linked to Slack
414
+ thread = Thread(
415
+ title="Support Ticket #123",
416
+ platforms={
417
+ "slack": {
418
+ "channel": "C1234567",
419
+ "thread_ts": "1234567890.123"
420
+ }
421
+ }
422
+ )
423
+
424
+ await store.save(thread)
425
+
426
+ # Find threads by platform
427
+ slack_threads = await store.find_by_platform("slack", {"channel": "C1234567"})
428
+ print(f"Found {len(slack_threads)} Slack threads in channel")
429
+
430
+ asyncio.run(main())
431
+ ```
432
+
433
+ ## Database CLI
434
+
435
+ The Narrator includes a CLI tool for database management:
436
+
437
+ ```bash
438
+ # Initialize database tables
439
+ uv run narrator init --database-url "postgresql+asyncpg://user:pass@localhost/dbname"
440
+
441
+ # Initialize using environment variable
442
+ export NARRATOR_DATABASE_URL="postgresql+asyncpg://user:pass@localhost/dbname"
443
+ uv run narrator init
444
+
445
+ # Check database status
446
+ uv run narrator status --database-url "postgresql+asyncpg://user:pass@localhost/dbname"
447
+
448
+ # Check status using environment variable
449
+ uv run narrator status
450
+ ```
451
+
452
+ Available commands:
453
+ - `uv run narrator init` - Initialize database tables
454
+ - `uv run narrator status` - Check database connection and basic statistics
455
+
456
+ ## Key Design Principles
457
+
458
+ 1. **Factory Pattern**: Use `await ThreadStore.create()` and `await FileStore.create()` for proper initialization and connection validation
459
+ 2. **Backend Agnostic**: Same API whether using in-memory, SQLite, or PostgreSQL storage
460
+ 3. **Production Ready**: Built-in connection pooling, error handling, and health checks
461
+ 4. **Tyler Integration**: Seamlessly integrates with Tyler agents for conversation persistence
462
+ 5. **Platform Support**: Native support for external platforms like Slack, Discord, and custom integrations
463
+
464
+ ## API Reference
465
+
466
+ ### ThreadStore
467
+
468
+ #### Methods
469
+
470
+ - `await ThreadStore.create(database_url=None)`: Factory method to create and initialize a store
471
+ - `await store.save(thread)`: Save a thread to storage
472
+ - `await store.get(thread_id)`: Retrieve a thread by ID
473
+ - `await store.delete(thread_id)`: Delete a thread
474
+ - `await store.list(limit=100, offset=0)`: List threads with pagination
475
+ - `await store.find_by_attributes(attributes)`: Find threads by custom attributes
476
+ - `await store.find_by_platform(platform_name, properties)`: Find threads by platform
477
+ - `await store.list_recent(limit=None)`: List recent threads
478
+
479
+ ### FileStore
480
+
481
+ #### Methods
482
+
483
+ - `await FileStore.create(base_path=None, ...)`: Factory method to create and validate a store
484
+ - `await store.save(content, filename, mime_type=None)`: Save file content
485
+ - `await store.get(file_id, storage_path=None)`: Retrieve file content
486
+ - `await store.delete(file_id, storage_path=None)`: Delete a file
487
+ - `await store.get_storage_size()`: Get total storage size
488
+ - `await store.check_health()`: Check storage health
489
+
490
+ ### Models
491
+
492
+ #### Thread
493
+ - `id`: Unique thread identifier
494
+ - `title`: Thread title
495
+ - `messages`: List of messages
496
+ - `created_at`: Creation timestamp
497
+ - `updated_at`: Last update timestamp
498
+ - `attributes`: Custom attributes dictionary
499
+ - `platforms`: Platform-specific metadata
500
+
501
+ #### Message
502
+ - `id`: Unique message identifier
503
+ - `role`: Message role (user, assistant, system, tool)
504
+ - `content`: Message content
505
+ - `attachments`: List of file attachments
506
+ - `timestamp`: Message timestamp
507
+ - `metrics`: Performance metrics
508
+
509
+ #### Attachment
510
+ - `filename`: Original filename
511
+ - `mime_type`: File MIME type
512
+ - `file_id`: Storage file ID
513
+ - `storage_path`: Path in storage
514
+ - `status`: Processing status (pending, stored, failed)
515
+ - `attributes`: Processed content and metadata
516
+
517
+ ## Development
518
+
519
+ ### Running Tests
520
+
521
+ To run the test suite locally:
522
+
523
+ ```bash
524
+ # Install development dependencies
525
+ uv sync --extra dev
526
+
527
+ # Run tests with coverage
528
+ uv run pytest tests/ --cov=narrator --cov-report=term-missing --cov-branch --cov-report=term --no-cov-on-fail -v
529
+
530
+ # Run tests without coverage (faster)
531
+ uv run pytest tests/ -v
532
+ ```
533
+
534
+ ### Test Requirements
535
+
536
+ The test suite requires:
537
+ - Python 3.13+
538
+ - pytest with async support
539
+ - Test coverage reporting
540
+
541
+ ## Contributing
542
+
543
+ 1. Fork the repository
544
+ 2. Create a feature branch
545
+ 3. Make your changes
546
+ 4. Add tests
547
+ 5. Run the test suite to ensure everything works
548
+ 6. Submit a pull request
549
+
550
+ ## License
551
+
552
+ MIT License - see LICENSE file for details.
553
+
554
+ ## Support
555
+
556
+ For issues and questions:
557
+ - GitHub Issues: [Repository Issues](https://github.com/adamwdraper/the-narrator/issues)
558
+ - Documentation: [API Reference](https://github.com/adamwdraper/the-narrator#api-reference)
@@ -0,0 +1,20 @@
1
+ narrator/__init__.py,sha256=Yzh2UFHX_grpGr_RvFqWpUTevr06FyP_R_Z9IHpF9jI,403
2
+ narrator/database/__init__.py,sha256=UngOnFqImCeJiMZlMasm72mC4-UnJDDvfu1MNQLkRA8,189
3
+ narrator/database/cli.py,sha256=QvET17X5kLZ7GiOTw0b80-u4FuI-tOTu4SjAqCBkiSs,8355
4
+ narrator/database/models.py,sha256=OyiufON_ZLNM7PUxXrUoFu1uu62SolHABwpedVnSsfk,2659
5
+ narrator/database/storage_backend.py,sha256=NZ0SKvEL6qKgdJrxuBw5qs6k-9mdWXWE0Ad36epIn2U,27598
6
+ narrator/database/thread_store.py,sha256=vMIPDdwuSpTyPogEUmxGcILxM_r1wxoQBUOn8XJpdqM,11301
7
+ narrator/database/migrations/__init__.py,sha256=IqoSL8eCcbcOtn96u2_TTrNG0KN1Jn1yreDZEO4RsnM,173
8
+ narrator/models/__init__.py,sha256=J8Rsv2lmfGR5QmUjoAPEFTSQt5TGtyrBynnp17HdZnU,179
9
+ narrator/models/attachment.py,sha256=_DIe2F5Vd75UtKmWO7x9d69dzAxUWCOXotvzR-nGVuU,17940
10
+ narrator/models/message.py,sha256=QVItUV75subrmVoEWFy_25khHP7fwbVaKZI8Rh5IPTI,21629
11
+ narrator/models/thread.py,sha256=MMZfJPMMw1zdD_1Jp4NqEAvnTVpHjaFSY82eV4921PQ,19326
12
+ narrator/storage/__init__.py,sha256=K4cxGITSQoQiw32QOWZsCBm11fwDTbsyzHGeAqcL6yY,101
13
+ narrator/storage/file_store.py,sha256=H2F08e_DYjO-L782s1DgSctJ_2x-7EjrTFxLxV3oWuk,20062
14
+ narrator/utils/__init__.py,sha256=P4BhLvBJbBvb8qha2tTZPlYbjCRXth_K97f4vNc77UI,109
15
+ narrator/utils/logging.py,sha256=Hm6D4VX03e28UCkNS1pCOXnYQKHQ2nz_PvZX8h-wLgg,1807
16
+ slide_narrator-5.5.0.dist-info/METADATA,sha256=tcd_3zqNCGJ6EIgmcZVnvyRI3ROKzn7fcKFuOw-2N9E,16494
17
+ slide_narrator-5.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
+ slide_narrator-5.5.0.dist-info/entry_points.txt,sha256=5Oa53AERvPVdrEvsdWbY85xfzAGayOqq_P4KEmf1khA,56
19
+ slide_narrator-5.5.0.dist-info/licenses/LICENSE,sha256=g6cGasroU9sqSOjThWg14w0BMlwZhgmOQQVTiu036ks,1068
20
+ slide_narrator-5.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ narrator = narrator.database.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Adam Draper
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.