trainly 0.1.0__tar.gz

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.
trainly-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Trainly
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.
22
+
@@ -0,0 +1,8 @@
1
+ include README.md
2
+ include LICENSE
3
+ include requirements.txt
4
+ recursive-include trainly *.py
5
+ recursive-include examples *.py
6
+ recursive-exclude * __pycache__
7
+ recursive-exclude * *.py[co]
8
+
trainly-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,649 @@
1
+ Metadata-Version: 2.4
2
+ Name: trainly
3
+ Version: 0.1.0
4
+ Summary: Dead simple RAG integration for Python applications
5
+ Author-email: Trainly Team <support@trainly.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://trainly.com
8
+ Project-URL: Documentation, https://trainly.com/docs/python-sdk
9
+ Project-URL: Repository, https://github.com/trainly/python-sdk
10
+ Project-URL: Bug Reports, https://github.com/trainly/python-sdk/issues
11
+ Keywords: trainly,rag,ai,llm,knowledge-base,embeddings,vector-search
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: requests>=2.25.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
29
+ Requires-Dist: black>=22.0.0; extra == "dev"
30
+ Requires-Dist: flake8>=5.0.0; extra == "dev"
31
+ Requires-Dist: mypy>=0.990; extra == "dev"
32
+ Requires-Dist: types-requests>=2.25.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Trainly Python SDK
36
+
37
+ **Dead simple RAG integration for Python applications with V1 OAuth Authentication**
38
+
39
+ Go from `pip install` to working AI in under 5 minutes. Now supports direct OAuth integration with **permanent user subchats** and complete privacy protection.
40
+
41
+ [![PyPI version](https://badge.fury.io/py/trainly.svg)](https://badge.fury.io/py/trainly)
42
+ [![Python versions](https://img.shields.io/pypi/pyversions/trainly.svg)](https://pypi.org/project/trainly/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
+
45
+ ## 🚀 Quick Start
46
+
47
+ ### Installation
48
+
49
+ ```bash
50
+ pip install trainly
51
+ ```
52
+
53
+ ### Basic Usage
54
+
55
+ ```python
56
+ from trainly import TrainlyClient
57
+
58
+ # Initialize the client
59
+ trainly = TrainlyClient(
60
+ api_key="tk_your_api_key_here",
61
+ chat_id="chat_abc123"
62
+ )
63
+
64
+ # Ask a question
65
+ response = trainly.query(
66
+ question="What are the main findings?"
67
+ )
68
+
69
+ print("Answer:", response.answer)
70
+ print("Citations:", len(response.context))
71
+
72
+ # Access context details
73
+ for i, chunk in enumerate(response.context):
74
+ print(f"Citation [{i}]: {chunk.chunk_text[:100]}... (score: {chunk.score})")
75
+ ```
76
+
77
+ ## 📖 Table of Contents
78
+
79
+ - [Installation](#installation)
80
+ - [Basic Usage](#basic-usage)
81
+ - [Type Hints](#type-hints)
82
+ - [Environment Variables](#environment-variables)
83
+ - [V1 OAuth Authentication](#v1-oauth-authentication)
84
+ - [Core Features](#core-features)
85
+ - [Query](#query)
86
+ - [Streaming Responses](#streaming-responses)
87
+ - [File Upload](#file-upload)
88
+ - [List Files](#list-files)
89
+ - [Delete Files](#delete-files)
90
+ - [Custom Scopes](#custom-scopes)
91
+ - [Error Handling](#error-handling)
92
+ - [Configuration Options](#configuration-options)
93
+ - [Examples](#examples)
94
+ - [API Reference](#api-reference)
95
+
96
+ ## 🎯 Type Hints
97
+
98
+ The Python SDK includes full type hints for better IDE support:
99
+
100
+ ```python
101
+ from trainly import TrainlyClient, QueryResponse, ChunkScore
102
+ from typing import List
103
+
104
+ trainly = TrainlyClient(
105
+ api_key="tk_your_api_key",
106
+ chat_id="chat_abc123"
107
+ )
108
+
109
+ # Fully typed response
110
+ response: QueryResponse = trainly.query(
111
+ question="What is the conclusion?",
112
+ model="gpt-4o",
113
+ temperature=0.5,
114
+ max_tokens=2000
115
+ )
116
+
117
+ # Access typed fields
118
+ answer: str = response.answer
119
+ context: List[ChunkScore] = response.context
120
+ if response.usage:
121
+ tokens: int = response.usage.total_tokens
122
+ ```
123
+
124
+ ## 🔐 Environment Variables
125
+
126
+ For better security, use environment variables for your credentials:
127
+
128
+ **.env**
129
+ ```bash
130
+ TRAINLY_API_KEY=tk_your_api_key_here
131
+ TRAINLY_CHAT_ID=chat_abc123
132
+ ```
133
+
134
+ **Python**
135
+ ```python
136
+ from trainly import TrainlyClient
137
+
138
+ # Automatically loads from environment variables
139
+ trainly = TrainlyClient()
140
+
141
+ response = trainly.query("What are the key findings?")
142
+ print(response.answer)
143
+ ```
144
+
145
+ ## 🆕 V1 OAuth Authentication
146
+
147
+ For user-facing applications with OAuth:
148
+
149
+ ```python
150
+ from trainly import TrainlyV1Client
151
+
152
+ # User authenticates with their OAuth provider
153
+ user_token = get_user_oauth_token() # Your OAuth implementation
154
+
155
+ # Initialize V1 client with user's token
156
+ trainly = TrainlyV1Client(
157
+ user_token=user_token,
158
+ app_id="app_your_app_id"
159
+ )
160
+
161
+ # Query user's private data
162
+ response = trainly.query(
163
+ messages=[
164
+ {"role": "user", "content": "What is in my documents?"}
165
+ ]
166
+ )
167
+
168
+ print(response.answer)
169
+ ```
170
+
171
+ ### V1 Benefits
172
+
173
+ - ✅ **Permanent User Data**: Same user = same private subchat forever
174
+ - ✅ **Complete Privacy**: Developer never sees user files or queries
175
+ - ✅ **Any OAuth Provider**: Clerk, Auth0, Cognito, Firebase, custom OIDC
176
+ - ✅ **Zero Migration**: Works with your existing OAuth setup
177
+ - ✅ **Simple Integration**: Just provide `app_id` and user's OAuth token
178
+
179
+ ## 🎨 Core Features
180
+
181
+ ### Query
182
+
183
+ Ask questions about your knowledge base:
184
+
185
+ ```python
186
+ from trainly import TrainlyClient
187
+
188
+ trainly = TrainlyClient(
189
+ api_key="tk_your_api_key",
190
+ chat_id="chat_abc123"
191
+ )
192
+
193
+ # Simple query
194
+ response = trainly.query("What are the main conclusions?")
195
+ print(response.answer)
196
+
197
+ # Query with custom parameters
198
+ response = trainly.query(
199
+ question="Explain the methodology in detail",
200
+ model="gpt-4o",
201
+ temperature=0.3,
202
+ max_tokens=2000,
203
+ include_context=True
204
+ )
205
+
206
+ # Access context chunks
207
+ for chunk in response.context:
208
+ print(f"Source: {chunk.source}")
209
+ print(f"Score: {chunk.score}")
210
+ print(f"Text: {chunk.chunk_text[:200]}...")
211
+ print("---")
212
+ ```
213
+
214
+ ### Streaming Responses
215
+
216
+ Stream responses in real-time:
217
+
218
+ ```python
219
+ from trainly import TrainlyClient
220
+
221
+ trainly = TrainlyClient(
222
+ api_key="tk_your_api_key",
223
+ chat_id="chat_abc123"
224
+ )
225
+
226
+ # Stream response chunks
227
+ for chunk in trainly.query_stream("Explain the methodology in detail"):
228
+ if chunk.is_content:
229
+ print(chunk.data, end="", flush=True)
230
+ elif chunk.is_context:
231
+ print("\n\nContext chunks received:", len(chunk.data))
232
+ elif chunk.is_end:
233
+ print("\n\nStream complete!")
234
+ ```
235
+
236
+ ### File Upload
237
+
238
+ Upload files to your knowledge base:
239
+
240
+ ```python
241
+ from trainly import TrainlyClient
242
+
243
+ trainly = TrainlyClient(
244
+ api_key="tk_your_api_key",
245
+ chat_id="chat_abc123"
246
+ )
247
+
248
+ # Upload a file
249
+ result = trainly.upload_file("./research_paper.pdf")
250
+ print(f"Uploaded: {result.filename}")
251
+ print(f"File ID: {result.file_id}")
252
+ print(f"Size: {result.size_bytes} bytes")
253
+
254
+ # Upload with custom scopes
255
+ result = trainly.upload_file(
256
+ "./document.pdf",
257
+ scope_values={
258
+ "project_id": "proj_123",
259
+ "category": "research"
260
+ }
261
+ )
262
+ ```
263
+
264
+ ### List Files
265
+
266
+ Get all files in your knowledge base:
267
+
268
+ ```python
269
+ from trainly import TrainlyClient
270
+
271
+ trainly = TrainlyClient(
272
+ api_key="tk_your_api_key",
273
+ chat_id="chat_abc123"
274
+ )
275
+
276
+ # List all files
277
+ files = trainly.list_files()
278
+ print(f"Total files: {files.total_files}")
279
+ print(f"Total size: {files.total_size_bytes} bytes")
280
+
281
+ for file in files.files:
282
+ print(f"- {file.filename}")
283
+ print(f" ID: {file.file_id}")
284
+ print(f" Size: {file.size_bytes} bytes")
285
+ print(f" Chunks: {file.chunk_count}")
286
+ print(f" Uploaded: {file.upload_datetime}")
287
+ ```
288
+
289
+ ### Delete Files
290
+
291
+ Remove files from your knowledge base:
292
+
293
+ ```python
294
+ from trainly import TrainlyClient
295
+
296
+ trainly = TrainlyClient(
297
+ api_key="tk_your_api_key",
298
+ chat_id="chat_abc123"
299
+ )
300
+
301
+ # Delete a specific file
302
+ result = trainly.delete_file("v1_user_xyz_document.pdf_1609459200")
303
+ print(f"Deleted: {result.filename}")
304
+ print(f"Chunks deleted: {result.chunks_deleted}")
305
+ print(f"Space freed: {result.size_bytes_freed} bytes")
306
+ ```
307
+
308
+ ## 🏷️ Custom Scopes
309
+
310
+ Tag your documents with custom attributes for powerful filtering:
311
+
312
+ ```python
313
+ from trainly import TrainlyClient
314
+
315
+ trainly = TrainlyClient(
316
+ api_key="tk_your_api_key",
317
+ chat_id="chat_abc123"
318
+ )
319
+
320
+ # Upload with scopes
321
+ trainly.upload_file(
322
+ "./project_report.pdf",
323
+ scope_values={
324
+ "playlist_id": "xyz123",
325
+ "workspace_id": "acme_corp",
326
+ "project": "alpha"
327
+ }
328
+ )
329
+
330
+ # Query with scope filters - only searches matching documents
331
+ response = trainly.query(
332
+ question="What are the key features?",
333
+ scope_filters={"playlist_id": "xyz123"}
334
+ )
335
+ # ☝️ Only searches documents with playlist_id="xyz123"
336
+
337
+ # Query with multiple filters
338
+ response = trainly.query(
339
+ question="Show me updates",
340
+ scope_filters={
341
+ "workspace_id": "acme_corp",
342
+ "project": "alpha"
343
+ }
344
+ )
345
+ # ☝️ Only searches documents matching ALL specified scopes
346
+
347
+ # Query everything (no filters)
348
+ response = trainly.query("What do I have?")
349
+ # ☝️ Searches ALL documents
350
+ ```
351
+
352
+ **Use Cases:**
353
+
354
+ - 🎵 **Playlist Apps**: Filter by `playlist_id` to query specific playlists
355
+ - 🏢 **Multi-Tenant SaaS**: Filter by `tenant_id` or `workspace_id`
356
+ - 📁 **Project Management**: Filter by `project_id` or `team_id`
357
+ - 👥 **User Segmentation**: Filter by `user_tier`, `department`, etc.
358
+
359
+ ## ⚠️ Error Handling
360
+
361
+ Always wrap API calls in try-except blocks:
362
+
363
+ ```python
364
+ from trainly import TrainlyClient, TrainlyError
365
+
366
+ trainly = TrainlyClient(
367
+ api_key="tk_your_api_key",
368
+ chat_id="chat_abc123"
369
+ )
370
+
371
+ try:
372
+ response = trainly.query("What is the conclusion?")
373
+ print(response.answer)
374
+ except TrainlyError as e:
375
+ if e.status_code == 429:
376
+ print("Rate limit exceeded. Please wait and retry.")
377
+ elif e.status_code == 401:
378
+ print("Invalid API key")
379
+ elif e.status_code == 400:
380
+ print(f"Bad request: {e}")
381
+ else:
382
+ print(f"Error: {e}")
383
+ ```
384
+
385
+ ## ⚙️ Configuration Options
386
+
387
+ ### TrainlyClient
388
+
389
+ ```python
390
+ from trainly import TrainlyClient
391
+
392
+ trainly = TrainlyClient(
393
+ api_key="tk_your_api_key", # Required (or set TRAINLY_API_KEY)
394
+ chat_id="chat_abc123", # Required (or set TRAINLY_CHAT_ID)
395
+ base_url="https://api.trainly.com", # Optional: Custom API URL
396
+ timeout=30, # Optional: Request timeout (seconds)
397
+ max_retries=3, # Optional: Max retry attempts
398
+ )
399
+ ```
400
+
401
+ ### TrainlyV1Client (OAuth)
402
+
403
+ ```python
404
+ from trainly import TrainlyV1Client
405
+
406
+ trainly = TrainlyV1Client(
407
+ user_token="user_oauth_token", # Required: User's OAuth token
408
+ app_id="app_your_app_id", # Required: Your app ID
409
+ base_url="https://api.trainly.com", # Optional: Custom API URL
410
+ timeout=30, # Optional: Request timeout (seconds)
411
+ )
412
+ ```
413
+
414
+ ## 🔍 Examples
415
+
416
+ ### Complete File Management
417
+
418
+ ```python
419
+ from trainly import TrainlyClient, TrainlyError
420
+
421
+ def manage_files():
422
+ trainly = TrainlyClient(
423
+ api_key="tk_your_api_key",
424
+ chat_id="chat_abc123"
425
+ )
426
+
427
+ # Upload multiple files
428
+ files_to_upload = [
429
+ "./doc1.pdf",
430
+ "./doc2.txt",
431
+ "./doc3.docx"
432
+ ]
433
+
434
+ for file_path in files_to_upload:
435
+ try:
436
+ result = trainly.upload_file(file_path)
437
+ print(f"✅ Uploaded: {result.filename}")
438
+ except TrainlyError as e:
439
+ print(f"❌ Failed to upload {file_path}: {e}")
440
+
441
+ # List all files
442
+ files = trainly.list_files()
443
+ print(f"\n📁 Total files: {files.total_files}")
444
+ print(f"💾 Total storage: {files.total_size_bytes / 1024 / 1024:.2f} MB")
445
+
446
+ for file in files.files:
447
+ print(f"\n- {file.filename}")
448
+ print(f" Size: {file.size_bytes / 1024:.2f} KB")
449
+ print(f" Chunks: {file.chunk_count}")
450
+
451
+ # Query the knowledge base
452
+ response = trainly.query("What are the key findings across all documents?")
453
+ print(f"\n🤖 AI Response:\n{response.answer}")
454
+
455
+ # Delete old files
456
+ if files.files:
457
+ oldest_file = files.files[0]
458
+ confirm = input(f"\nDelete {oldest_file.filename}? (y/n): ")
459
+ if confirm.lower() == 'y':
460
+ result = trainly.delete_file(oldest_file.file_id)
461
+ print(f"🗑️ Deleted: {result.filename}")
462
+ print(f"💾 Freed: {result.size_bytes_freed / 1024:.2f} KB")
463
+
464
+ if __name__ == "__main__":
465
+ manage_files()
466
+ ```
467
+
468
+ ### Context Manager Usage
469
+
470
+ ```python
471
+ from trainly import TrainlyClient
472
+
473
+ # Use as context manager for automatic cleanup
474
+ with TrainlyClient(api_key="tk_your_api_key", chat_id="chat_abc123") as trainly:
475
+ response = trainly.query("What are the main points?")
476
+ print(response.answer)
477
+
478
+ files = trainly.list_files()
479
+ print(f"Total files: {files.total_files}")
480
+ # Session automatically closed
481
+ ```
482
+
483
+ ### V1 OAuth Integration (Flask Example)
484
+
485
+ ```python
486
+ from flask import Flask, request, jsonify
487
+ from trainly import TrainlyV1Client, TrainlyError
488
+
489
+ app = Flask(__name__)
490
+
491
+ @app.route("/api/query", methods=["POST"])
492
+ def query_user_data():
493
+ # Get user's OAuth token from request
494
+ auth_header = request.headers.get("Authorization")
495
+ if not auth_header or not auth_header.startswith("Bearer "):
496
+ return jsonify({"error": "Missing or invalid authorization"}), 401
497
+
498
+ user_token = auth_header.split(" ")[1]
499
+
500
+ try:
501
+ # Initialize V1 client with user's token
502
+ trainly = TrainlyV1Client(
503
+ user_token=user_token,
504
+ app_id="app_your_app_id"
505
+ )
506
+
507
+ # Get question from request
508
+ data = request.get_json()
509
+ question = data.get("question")
510
+
511
+ if not question:
512
+ return jsonify({"error": "Missing question"}), 400
513
+
514
+ # Query user's private knowledge base
515
+ response = trainly.query(
516
+ messages=[{"role": "user", "content": question}]
517
+ )
518
+
519
+ return jsonify({
520
+ "answer": response.answer,
521
+ "context_count": len(response.context),
522
+ "model": response.model
523
+ })
524
+
525
+ except TrainlyError as e:
526
+ return jsonify({"error": str(e)}), e.status_code or 500
527
+ finally:
528
+ if 'trainly' in locals():
529
+ trainly.close()
530
+
531
+ if __name__ == "__main__":
532
+ app.run(debug=True)
533
+ ```
534
+
535
+ ## 📚 API Reference
536
+
537
+ ### TrainlyClient
538
+
539
+ #### `__init__(api_key, chat_id, base_url, timeout, max_retries)`
540
+ Initialize the client with API credentials.
541
+
542
+ #### `query(question, model, temperature, max_tokens, include_context, scope_filters) -> QueryResponse`
543
+ Query the knowledge base with a question.
544
+
545
+ #### `query_stream(question, model, temperature, max_tokens, scope_filters) -> Iterator[StreamChunk]`
546
+ Stream responses in real-time.
547
+
548
+ #### `upload_file(file_path, scope_values) -> UploadResult`
549
+ Upload a file to the knowledge base.
550
+
551
+ #### `list_files() -> FileListResult`
552
+ List all files in the knowledge base.
553
+
554
+ #### `delete_file(file_id) -> FileDeleteResult`
555
+ Delete a file from the knowledge base.
556
+
557
+ ### TrainlyV1Client
558
+
559
+ #### `__init__(user_token, app_id, base_url, timeout)`
560
+ Initialize the V1 client with OAuth token.
561
+
562
+ #### `query(messages, model, temperature, max_tokens, scope_filters) -> QueryResponse`
563
+ Query the user's private knowledge base.
564
+
565
+ #### `upload_file(file_path, scope_values) -> UploadResult`
566
+ Upload a file to the user's knowledge base.
567
+
568
+ #### `upload_text(text, content_name, scope_values) -> UploadResult`
569
+ Upload text content to the user's knowledge base.
570
+
571
+ #### `list_files() -> FileListResult`
572
+ List all files in the user's knowledge base.
573
+
574
+ #### `delete_file(file_id) -> FileDeleteResult`
575
+ Delete a file from the user's knowledge base.
576
+
577
+ #### `bulk_upload_files(file_paths, scope_values) -> BulkUploadResult`
578
+ Upload multiple files at once (up to 10 files).
579
+
580
+ ### Response Models
581
+
582
+ All response models are dataclasses with full type hints:
583
+
584
+ - `QueryResponse`: Answer and context from a query
585
+ - `ChunkScore`: A chunk of text with relevance score
586
+ - `Usage`: Token usage information
587
+ - `UploadResult`: Result of a file upload
588
+ - `FileInfo`: Information about a file
589
+ - `FileListResult`: List of files with metadata
590
+ - `FileDeleteResult`: Result of file deletion
591
+ - `BulkUploadResult`: Result of bulk upload operation
592
+ - `StreamChunk`: A chunk from a streaming response
593
+
594
+ ### Exceptions
595
+
596
+ - `TrainlyError`: Base exception for all Trainly SDK errors
597
+ - `status_code`: HTTP status code (if applicable)
598
+ - `details`: Additional error details
599
+
600
+ ## 🛠️ Development
601
+
602
+ ### Install in Development Mode
603
+
604
+ ```bash
605
+ git clone https://github.com/trainly/python-sdk.git
606
+ cd python-sdk
607
+ pip install -e ".[dev]"
608
+ ```
609
+
610
+ ### Run Tests
611
+
612
+ ```bash
613
+ pytest
614
+ pytest --cov=trainly # With coverage
615
+ ```
616
+
617
+ ### Format Code
618
+
619
+ ```bash
620
+ black trainly examples tests
621
+ ```
622
+
623
+ ### Type Checking
624
+
625
+ ```bash
626
+ mypy trainly
627
+ ```
628
+
629
+ ## 📝 License
630
+
631
+ MIT - see [LICENSE](LICENSE) file for details.
632
+
633
+ ## 🤝 Contributing
634
+
635
+ Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
636
+
637
+ ## 🆘 Support
638
+
639
+ - 📖 [Documentation](https://trainly.com/docs/python-sdk)
640
+ - 💬 [Discord Community](https://discord.gg/trainly)
641
+ - 📧 [Email Support](mailto:support@trainly.com)
642
+ - 🐛 [Report Issues](https://github.com/trainly/python-sdk/issues)
643
+
644
+ ---
645
+
646
+ **Made with ❤️ by the Trainly team**
647
+
648
+ *The simplest way to add AI to your Python app*
649
+