WhyCrash 1.0.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.
- whycrash-1.0.0/PKG-INFO +140 -0
- whycrash-1.0.0/README.md +119 -0
- whycrash-1.0.0/WhyCrash/__init__.py +243 -0
- whycrash-1.0.0/WhyCrash.egg-info/PKG-INFO +140 -0
- whycrash-1.0.0/WhyCrash.egg-info/SOURCES.txt +8 -0
- whycrash-1.0.0/WhyCrash.egg-info/dependency_links.txt +1 -0
- whycrash-1.0.0/WhyCrash.egg-info/requires.txt +3 -0
- whycrash-1.0.0/WhyCrash.egg-info/top_level.txt +1 -0
- whycrash-1.0.0/setup.cfg +4 -0
- whycrash-1.0.0/setup.py +25 -0
whycrash-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: WhyCrash
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A highly automatic AI error handler and code fixer using OpenRouter and Minimax.
|
|
5
|
+
Home-page: https://github.com/yourusername/WhyCrash
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: rich
|
|
13
|
+
Requires-Dist: questionary
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# π WhyCrash
|
|
23
|
+
**WhyCrash** is a fully automatic AI assistant for error handling in Python. When your code crashes, WhyCrash intercepts the error, analyzes it using neural networks (OpenRouter + Minimax), gathers context from your local project files, and provides the cause along with an **AUTOMATIC CODE FIX**.
|
|
24
|
+
|
|
25
|
+
Did your code crash? The AI will explain why and automatically replace the broken file with the fixed one (if you allow it).
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
## β¨ Main Features
|
|
31
|
+
- π§ **Smart Traceback Analysis**: Understands not just the line with the error but also gathers imported local project files.
|
|
32
|
+
- π οΈ **Auto-Fixing**: Proposes a ready-made fix and can rewrite the target Python files itself.
|
|
33
|
+
- π― **Precise Control**: You decide where to catch errors: in the entire project, in a single function, or in a specific block of code.
|
|
34
|
+
- π¨ **Beautiful Interface**: Uses the `rich` library for nice windows and terminal formatting.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## π¦ Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install WhyCrash
|
|
42
|
+
```
|
|
43
|
+
> *(Requires `requests`, `rich`, and `questionary` β they will install automatically)*
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## π οΈ How to Use
|
|
48
|
+
|
|
49
|
+
You have 4 ways to control which errors WhyCrash should catch. Choose the one that fits best!
|
|
50
|
+
|
|
51
|
+
### 1. Global Intercept (Easiest)
|
|
52
|
+
If you want **any** unhandled error in your program to be analyzed by the AI:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import WhyCrash
|
|
56
|
+
|
|
57
|
+
# Enable error catching for the whole script
|
|
58
|
+
WhyCrash.debug()
|
|
59
|
+
|
|
60
|
+
# If the code crashes below, WhyCrash comes to the rescue!
|
|
61
|
+
print(1 / 0)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Dynamic Toggle (start & end)
|
|
65
|
+
If you have a large block of code and want to turn on smart analysis right before it, and turn it off right after:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import WhyCrash
|
|
69
|
+
|
|
70
|
+
# ... normal code without WhyCrash ...
|
|
71
|
+
|
|
72
|
+
WhyCrash.start_debug() # Turn on the interceptor
|
|
73
|
+
|
|
74
|
+
a = "text"
|
|
75
|
+
b = int(a) # <-- This error will go to the AI!
|
|
76
|
+
|
|
77
|
+
WhyCrash.end_debug() # Turn off the interceptor (returns to standard behavior)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Decorator for Specific Functions `@catch_errors`
|
|
81
|
+
If you are only concerned about the reliability of a specific function, you can wrap it in a decorator. If the function crashes, WhyCrash will trigger, while system errors outside of it remain untouched.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from WhyCrash import catch_errors
|
|
85
|
+
|
|
86
|
+
@catch_errors
|
|
87
|
+
def my_danger_function():
|
|
88
|
+
# If it breaks here β WhyCrash will trigger
|
|
89
|
+
file = open("no_exist.txt", "r")
|
|
90
|
+
|
|
91
|
+
def normal_function():
|
|
92
|
+
# And if it breaks here β standard Python traceback
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
my_danger_function()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4. Context Manager `with catch_block()`
|
|
99
|
+
For the most precise control, if you expect a failure in literally 2 specific lines of code:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from WhyCrash import catch_block
|
|
103
|
+
|
|
104
|
+
print("Starting work...")
|
|
105
|
+
text = "100"
|
|
106
|
+
|
|
107
|
+
with catch_block():
|
|
108
|
+
# Only code inside this block is monitored
|
|
109
|
+
number = int(text)
|
|
110
|
+
result = number / 0 # This will trigger an error sent to WhyCrash!
|
|
111
|
+
|
|
112
|
+
print("This code will not execute if there was an error above.")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## π How to Ignore Error Catching?
|
|
118
|
+
WhyCrash only analyzes **unhandled** exceptions. If you want an error in your code **not** to reach WhyCrash and the script to keep running, simply use a standard `try...except` block:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import WhyCrash
|
|
122
|
+
WhyCrash.debug()
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
int("letter")
|
|
126
|
+
except ValueError:
|
|
127
|
+
print("Error caught, it won't reach WhyCrash. Moving on!")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## βοΈ Under the Hood
|
|
131
|
+
- **OpenRouter & Minimax** β Responsible for code analysis, "Reasoning," and generating fix files.
|
|
132
|
+
- **Traceback Walking** β The script automatically follows the error chain, finds all your `.py` files involved, reads them, and sends them to the AI as context.
|
|
133
|
+
- **Rich** β Beautiful console UI (colors, panels, Markdown formatting).
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
Made with β€οΈ to save developers' nerves!
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
π **Languages:** [Π ΡΡΡΠΊΠΈΠΉ](docs/README_ru.md) | [Deutsch](docs/README_de.md)
|
whycrash-1.0.0/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# π WhyCrash
|
|
2
|
+
**WhyCrash** is a fully automatic AI assistant for error handling in Python. When your code crashes, WhyCrash intercepts the error, analyzes it using neural networks (OpenRouter + Minimax), gathers context from your local project files, and provides the cause along with an **AUTOMATIC CODE FIX**.
|
|
3
|
+
|
|
4
|
+
Did your code crash? The AI will explain why and automatically replace the broken file with the fixed one (if you allow it).
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## β¨ Main Features
|
|
10
|
+
- π§ **Smart Traceback Analysis**: Understands not just the line with the error but also gathers imported local project files.
|
|
11
|
+
- π οΈ **Auto-Fixing**: Proposes a ready-made fix and can rewrite the target Python files itself.
|
|
12
|
+
- π― **Precise Control**: You decide where to catch errors: in the entire project, in a single function, or in a specific block of code.
|
|
13
|
+
- π¨ **Beautiful Interface**: Uses the `rich` library for nice windows and terminal formatting.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## π¦ Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install WhyCrash
|
|
21
|
+
```
|
|
22
|
+
> *(Requires `requests`, `rich`, and `questionary` β they will install automatically)*
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## π οΈ How to Use
|
|
27
|
+
|
|
28
|
+
You have 4 ways to control which errors WhyCrash should catch. Choose the one that fits best!
|
|
29
|
+
|
|
30
|
+
### 1. Global Intercept (Easiest)
|
|
31
|
+
If you want **any** unhandled error in your program to be analyzed by the AI:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import WhyCrash
|
|
35
|
+
|
|
36
|
+
# Enable error catching for the whole script
|
|
37
|
+
WhyCrash.debug()
|
|
38
|
+
|
|
39
|
+
# If the code crashes below, WhyCrash comes to the rescue!
|
|
40
|
+
print(1 / 0)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Dynamic Toggle (start & end)
|
|
44
|
+
If you have a large block of code and want to turn on smart analysis right before it, and turn it off right after:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import WhyCrash
|
|
48
|
+
|
|
49
|
+
# ... normal code without WhyCrash ...
|
|
50
|
+
|
|
51
|
+
WhyCrash.start_debug() # Turn on the interceptor
|
|
52
|
+
|
|
53
|
+
a = "text"
|
|
54
|
+
b = int(a) # <-- This error will go to the AI!
|
|
55
|
+
|
|
56
|
+
WhyCrash.end_debug() # Turn off the interceptor (returns to standard behavior)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Decorator for Specific Functions `@catch_errors`
|
|
60
|
+
If you are only concerned about the reliability of a specific function, you can wrap it in a decorator. If the function crashes, WhyCrash will trigger, while system errors outside of it remain untouched.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from WhyCrash import catch_errors
|
|
64
|
+
|
|
65
|
+
@catch_errors
|
|
66
|
+
def my_danger_function():
|
|
67
|
+
# If it breaks here β WhyCrash will trigger
|
|
68
|
+
file = open("no_exist.txt", "r")
|
|
69
|
+
|
|
70
|
+
def normal_function():
|
|
71
|
+
# And if it breaks here β standard Python traceback
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
my_danger_function()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. Context Manager `with catch_block()`
|
|
78
|
+
For the most precise control, if you expect a failure in literally 2 specific lines of code:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from WhyCrash import catch_block
|
|
82
|
+
|
|
83
|
+
print("Starting work...")
|
|
84
|
+
text = "100"
|
|
85
|
+
|
|
86
|
+
with catch_block():
|
|
87
|
+
# Only code inside this block is monitored
|
|
88
|
+
number = int(text)
|
|
89
|
+
result = number / 0 # This will trigger an error sent to WhyCrash!
|
|
90
|
+
|
|
91
|
+
print("This code will not execute if there was an error above.")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## π How to Ignore Error Catching?
|
|
97
|
+
WhyCrash only analyzes **unhandled** exceptions. If you want an error in your code **not** to reach WhyCrash and the script to keep running, simply use a standard `try...except` block:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import WhyCrash
|
|
101
|
+
WhyCrash.debug()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
int("letter")
|
|
105
|
+
except ValueError:
|
|
106
|
+
print("Error caught, it won't reach WhyCrash. Moving on!")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## βοΈ Under the Hood
|
|
110
|
+
- **OpenRouter & Minimax** β Responsible for code analysis, "Reasoning," and generating fix files.
|
|
111
|
+
- **Traceback Walking** β The script automatically follows the error chain, finds all your `.py` files involved, reads them, and sends them to the AI as context.
|
|
112
|
+
- **Rich** β Beautiful console UI (colors, panels, Markdown formatting).
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
Made with β€οΈ to save developers' nerves!
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
π **Languages:** [Π ΡΡΡΠΊΠΈΠΉ](docs/README_ru.md) | [Deutsch](docs/README_de.md)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
def debug():
|
|
7
|
+
"""ΠΡΡ ΡΡΠ½ΠΊΡΠΈΡ Π½ΡΠΆΠ½ΠΎ Π²ΡΠ·Π²Π°ΡΡ Π² Π½Π°ΡΠ°Π»Π΅ ΠΊΠΎΠ΄Π°: WhyCrash.debug() (ΠΠΠΠΠΠΠ¬ΠΠ«Π ΠΠΠ ΠΠ₯ΠΠΠ’)"""
|
|
8
|
+
start_debug()
|
|
9
|
+
|
|
10
|
+
def start_debug():
|
|
11
|
+
"""ΠΠΊΠ»ΡΡΠ°Π΅Ρ AI-Π°Π½Π°Π»ΠΈΠ· ΠΎΡΠΈΠ±ΠΎΠΊ Π΄Π»Ρ Π²ΡΠ΅Π³ΠΎ ΠΏΠΎΡΠ»Π΅Π΄ΡΡΡΠ΅Π³ΠΎ ΠΊΠΎΠ΄Π°"""
|
|
12
|
+
if not hasattr(sys, 'ps1'):
|
|
13
|
+
sys.excepthook = _ai_excepthook
|
|
14
|
+
|
|
15
|
+
def end_debug():
|
|
16
|
+
"""ΠΡΠΊΠ»ΡΡΠ°Π΅Ρ AI-Π°Π½Π°Π»ΠΈΠ· ΠΎΡΠΈΠ±ΠΎΠΊ (Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ ΡΡΠ°Π½Π΄Π°ΡΡΠ½ΠΎΠ΅ ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ Python)"""
|
|
17
|
+
if sys.excepthook == _ai_excepthook:
|
|
18
|
+
sys.excepthook = sys.__excepthook__
|
|
19
|
+
|
|
20
|
+
import contextlib
|
|
21
|
+
import functools
|
|
22
|
+
|
|
23
|
+
@contextlib.contextmanager
|
|
24
|
+
def catch_block():
|
|
25
|
+
"""ΠΠΎΠ½ΡΠ΅ΠΊΡΡΠ½ΡΠΉ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ Π΄Π»Ρ ΠΏΠ΅ΡΠ΅Ρ
Π²Π°ΡΠ° ΠΎΡΠΈΠ±ΠΎΠΊ ΡΠΎΠ»ΡΠΊΠΎ Π² ΠΎΠΏΡΠ΅Π΄Π΅Π»Π΅Π½Π½ΠΎΠΌ Π±Π»ΠΎΠΊΠ΅ ΠΊΠΎΠ΄Π°"""
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
except Exception:
|
|
29
|
+
_ai_excepthook(*sys.exc_info())
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
def catch_errors(func):
|
|
33
|
+
"""ΠΠ΅ΠΊΠΎΡΠ°ΡΠΎΡ Π΄Π»Ρ ΠΏΠ΅ΡΠ΅Ρ
Π²Π°ΡΠ° ΠΎΡΠΈΠ±ΠΎΠΊ ΡΠΎΠ»ΡΠΊΠΎ Π² ΠΊΠΎΠ½ΠΊΡΠ΅ΡΠ½ΠΎΠΉ ΡΡΠ½ΠΊΡΠΈΠΈ"""
|
|
34
|
+
@functools.wraps(func)
|
|
35
|
+
def wrapper(*args, **kwargs):
|
|
36
|
+
try:
|
|
37
|
+
return func(*args, **kwargs)
|
|
38
|
+
except Exception:
|
|
39
|
+
_ai_excepthook(*sys.exc_info())
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
return wrapper
|
|
42
|
+
|
|
43
|
+
def _ai_excepthook(exc_type, exc_value, exc_traceback):
|
|
44
|
+
try:
|
|
45
|
+
import requests
|
|
46
|
+
import json
|
|
47
|
+
except ImportError:
|
|
48
|
+
print("ΠΠ»Ρ ΡΠ°Π±ΠΎΡΡ WhyCrash Π½Π΅ΠΎΠ±Ρ
ΠΎΠ΄ΠΈΠΌΠΎ ΡΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΠΏΠ°ΠΊΠ΅Ρ 'requests': pip install requests")
|
|
49
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from rich.console import Console
|
|
54
|
+
from rich.markdown import Markdown
|
|
55
|
+
from rich.panel import Panel
|
|
56
|
+
RICH = True
|
|
57
|
+
console = Console()
|
|
58
|
+
except ImportError:
|
|
59
|
+
RICH = False
|
|
60
|
+
|
|
61
|
+
RED = '\033[91m'
|
|
62
|
+
GREEN = '\033[92m'
|
|
63
|
+
YELLOW = '\033[93m'
|
|
64
|
+
CYAN = '\033[96m'
|
|
65
|
+
RESET = '\033[0m'
|
|
66
|
+
|
|
67
|
+
if RICH:
|
|
68
|
+
console.print(Panel("Oops! ΠΡΠΎΠΈΠ·ΠΎΡΠ»Π° ΠΎΡΠΈΠ±ΠΊΠ°. WhyCrash ΡΠΎΠ±ΠΈΡΠ°Π΅Ρ ΠΊΠΎΠ½ΡΠ΅ΠΊΡΡ ΠΈ Π°Π½Π°Π»ΠΈΠ·ΠΈΡΡΠ΅Ρ ΠΏΡΠΎΠ±Π»Π΅ΠΌΡ...", border_style="bold red", expand=False))
|
|
69
|
+
else:
|
|
70
|
+
print(f"\n{RED}Oops! ΠΡΠΎΠΈΠ·ΠΎΡΠ»Π° ΠΎΡΠΈΠ±ΠΊΠ°. WhyCrash ΡΠΎΠ±ΠΈΡΠ°Π΅Ρ ΠΊΠΎΠ½ΡΠ΅ΠΊΡΡ ΠΈ Π°Π½Π°Π»ΠΈΠ·ΠΈΡΡΠ΅Ρ ΠΏΡΠΎΠ±Π»Π΅ΠΌΡ...{RESET}\n")
|
|
71
|
+
|
|
72
|
+
tb_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
|
|
73
|
+
tb_text = "".join(tb_lines)
|
|
74
|
+
|
|
75
|
+
local_files = {}
|
|
76
|
+
deepest_file = None
|
|
77
|
+
deepest_line = 0
|
|
78
|
+
|
|
79
|
+
tb = exc_traceback
|
|
80
|
+
while tb:
|
|
81
|
+
filename = tb.tb_frame.f_code.co_filename
|
|
82
|
+
lineno = tb.tb_lineno
|
|
83
|
+
deepest_file = filename
|
|
84
|
+
deepest_line = lineno
|
|
85
|
+
|
|
86
|
+
if isinstance(filename, str) and os.path.exists(filename):
|
|
87
|
+
if 'site-packages' not in filename and 'lib\\python' not in filename.lower() and 'lib/python' not in filename.lower():
|
|
88
|
+
if filename not in local_files:
|
|
89
|
+
try:
|
|
90
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
|
91
|
+
local_files[filename] = f.read()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
tb = tb.tb_next
|
|
95
|
+
|
|
96
|
+
context_str = ""
|
|
97
|
+
for fpath, code in local_files.items():
|
|
98
|
+
context_str += f"### Π€Π°ΠΉΠ»: {fpath} ###\n```python\n{code}\n```\n\n"
|
|
99
|
+
|
|
100
|
+
first_prompt = f"""ΠΡΠΎΠΈΠ·ΠΎΡΠ»Π° ΠΎΡΠΈΠ±ΠΊΠ° Π² Python ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ.
|
|
101
|
+
ΠΡΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅ Π²ΡΠ·Π²Π°Π½ΠΎ Π² ΡΠ°ΠΉΠ»Π΅ '{deepest_file}' Π½Π° ΡΡΡΠΎΠΊΠ΅ {deepest_line}.
|
|
102
|
+
|
|
103
|
+
Traceback:
|
|
104
|
+
{tb_text}
|
|
105
|
+
|
|
106
|
+
ΠΡΡ
ΠΎΠ΄Π½ΡΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ½ΡΠ΅ΠΊΡΡΠ½ΡΡ
ΡΠ°ΠΉΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ ΡΠ΅, ΡΡΠΎ ΠΎΡΠ½ΠΎΡΡΡΡΡ ΠΊ ΠΏΡΠΎΠ΅ΠΊΡΡ):
|
|
107
|
+
{context_str}
|
|
108
|
+
|
|
109
|
+
ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ°, ΠΏΡΠΎΠ°Π½Π°Π»ΠΈΠ·ΠΈΡΡΠΉ ΡΡΡ ΠΎΡΠΈΠ±ΠΊΡ ΠΈ Π΄Π΅ΡΠ°Π»ΡΠ½ΠΎ ΠΎΠ±ΡΡΡΠ½ΠΈ Π½Π° ΡΡΡΡΠΊΠΎΠΌ ΡΠ·ΡΠΊΠ΅, ΠΏΠΎΡΠ΅ΠΌΡ ΠΎΠ½Π° ΠΏΡΠΎΠΈΠ·ΠΎΡΠ»Π°.
|
|
110
|
+
ΠΠΎΠΊΠ° ΡΡΠΎ ΠΠ ΠΏΠΈΡΠΈ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½Π½ΡΠΉ ΠΊΠΎΠ΄, ΡΠΎΠ»ΡΠΊΠΎ ΠΏΡΠΎΠ°Π½Π°Π»ΠΈΠ·ΠΈΡΡΠΉ ΠΈ ΠΎΠ±ΡΡΡΠ½ΠΈ ΠΏΡΠΈΡΠΈΠ½Ρ ΠΏΡΠΎΠ±Π»Π΅ΠΌΡ."""
|
|
111
|
+
|
|
112
|
+
API_KEY = "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"
|
|
113
|
+
|
|
114
|
+
# ========= ΠΠΠ ΠΠ«Π ΠΠΠΠ ΠΠ‘ Π OPENROUTER =========
|
|
115
|
+
messages = [{"role": "user", "content": first_prompt}]
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = requests.post(
|
|
119
|
+
url="https://openrouter.ai/api/v1/chat/completions",
|
|
120
|
+
headers={
|
|
121
|
+
"Authorization": f"Bearer {API_KEY}",
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
},
|
|
124
|
+
data=json.dumps({
|
|
125
|
+
"model": "minimax/minimax-m2.5",
|
|
126
|
+
"messages": messages,
|
|
127
|
+
"reasoning": {"enabled": True}
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
resp_json = response.json()
|
|
132
|
+
assistant_message = resp_json['choices'][0]['message']
|
|
133
|
+
|
|
134
|
+
reasoning = assistant_message.get('reasoning_details') or ""
|
|
135
|
+
content = assistant_message.get('content') or ""
|
|
136
|
+
|
|
137
|
+
if RICH:
|
|
138
|
+
if reasoning:
|
|
139
|
+
console.print(Panel(Markdown(f"**ΠΡΡΠ»ΠΈ (Reasoning):**\n\n{reasoning}"), title="AI ΠΠ±Π΄ΡΠΌΡΠ²Π°Π΅Ρ", border_style="cyan"))
|
|
140
|
+
console.print(Panel(Markdown(content), title="ΠΠ½Π°Π»ΠΈΠ· ΠΎΡΠΈΠ±ΠΊΠΈ", border_style="yellow"))
|
|
141
|
+
else:
|
|
142
|
+
print(f"{CYAN}============== ΠΠ½Π°Π½ΠΈΡ ΠΈ ΠΠ½Π°Π»ΠΈΠ· (Minimax) =============={RESET}\n")
|
|
143
|
+
if reasoning:
|
|
144
|
+
print(f"{CYAN}--- Π Π°Π·ΠΌΡΡΠ»Π΅Π½ΠΈΡ ---{RESET}\n{reasoning}\n")
|
|
145
|
+
print(f"{YELLOW}--- ΠΠ±ΡΡΡΠ½Π΅Π½ΠΈΠ΅ ---{RESET}\n{content}\n")
|
|
146
|
+
print(f"{CYAN}======================================================={RESET}\n")
|
|
147
|
+
|
|
148
|
+
# ΠΡΠΎΡΠΈΠΌ Ρ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ ΠΏΠΎΠ΄ΡΠ²Π΅ΡΠΆΠ΄Π΅Π½ΠΈΠ΅
|
|
149
|
+
try:
|
|
150
|
+
import questionary
|
|
151
|
+
answer = questionary.select(
|
|
152
|
+
"Π₯ΠΎΡΠΈΡΠ΅, ΡΡΠΎΠ±Ρ WhyCrash ΠΈΡΠΏΡΠ°Π²ΠΈΠ» ΡΡΡ ΠΎΡΠΈΠ±ΠΊΡ?",
|
|
153
|
+
choices=["ΠΠ°", "ΠΠ΅Ρ"]
|
|
154
|
+
).ask()
|
|
155
|
+
if answer != "ΠΠ°":
|
|
156
|
+
print(f"{YELLOW}ΠΡΠΌΠ΅Π½Π΅Π½ΠΎ. ΠΡΡ
ΠΎΠ΄ΠΈΠΌ...{RESET}")
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
except ImportError:
|
|
159
|
+
answer = input(f"{GREEN}Π₯ΠΎΡΠΈΡΠ΅, ΡΡΠΎΠ±Ρ WhyCrash ΠΈΡΠΏΡΠ°Π²ΠΈΠ» ΡΡΡ ΠΎΡΠΈΠ±ΠΊΡ? (y/n, enter=yes): {RESET}")
|
|
160
|
+
if answer.strip().lower() not in ('', 'y', 'yes', 'Π΄Π°', 'Π΄'):
|
|
161
|
+
print(f"{YELLOW}ΠΡΠΌΠ΅Π½Π΅Π½ΠΎ. ΠΡΡ
ΠΎΠ΄ΠΈΠΌ...{RESET}")
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
|
|
164
|
+
# ========= ΠΠ’ΠΠ ΠΠ ΠΠΠΠ ΠΠ‘ Π OPENROUTER (ΠΠ ΠΠΠΠΠΠΠΠ Π ΠΠΠΠ«Π¨ΠΠΠΠΠ―) =========
|
|
165
|
+
if RICH:
|
|
166
|
+
console.print(f"[bold green]ΠΠ΅Π½Π΅ΡΠΈΡΡΠ΅ΠΌ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅...[/bold green]")
|
|
167
|
+
else:
|
|
168
|
+
print(f"\n{GREEN}ΠΠ΅Π½Π΅ΡΠΈΡΡΠ΅ΠΌ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅...{RESET}")
|
|
169
|
+
|
|
170
|
+
messages.append({
|
|
171
|
+
"role": "assistant",
|
|
172
|
+
"content": content,
|
|
173
|
+
"reasoning_details": reasoning
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
second_prompt = """ΠΠ°ΠΏΠΈΡΠΈ ΠΠΠΠΠ«Π ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½Π½ΡΠΉ ΠΊΠΎΠ΄ Π΄Π»Ρ ΡΠ°ΠΉΠ»Π°, Π² ΠΊΠΎΡΠΎΡΠΎΠΌ Π½ΡΠΆΠ½ΠΎ ΡΠ΄Π΅Π»Π°ΡΡ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ.
|
|
177
|
+
ΠΠΠΠΠ: ΠΡΠ²Π΅Π΄ΠΈ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½Π½ΡΠΉ ΠΊΠΎΠ΄ Π²Π½ΡΡΡΠΈ ΠΎΠ΄Π½ΠΎΠ³ΠΎ Π±Π»ΠΎΠΊΠ° ```python ... ```.
|
|
178
|
+
ΠΠ΅ΠΏΠΎΡΡΠ΅Π΄ΡΡΠ²Π΅Π½Π½ΠΎ ΠΏΠ΅ΡΠ΅Π΄ Π±Π»ΠΎΠΊΠΎΠΌ ΠΊΠΎΠ΄Π° Π½Π°ΠΏΠΈΡΠΈ ΠΏΡΡΡΠΎΠΉ ΠΊΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠΉ ΠΈΠ»ΠΈ ΡΡΡΠΎΠΊΡ, ΡΠΊΠ°Π·ΡΠ²Π°ΡΡΡΡ ΠΊΠ°ΠΊΠΎΠΉ ΡΠ°ΠΉΠ» ΡΡ ΠΈΡΠΏΡΠ°Π²Π»ΡΠ΅ΡΡ, Π² ΡΠ°ΠΊΠΎΠΌ ΡΠΎΡΠ½ΠΎΠΌ ΡΠΎΡΠΌΠ°ΡΠ΅:
|
|
179
|
+
FILE_TO_FIX: <ΠΏΠΎΠ»Π½ΡΠΉ_ΠΏΡΡΡ_ΠΊ_ΡΠ°ΠΉΠ»Ρ>
|
|
180
|
+
ΠΡΠ²ΠΎΠ΄ΠΈ Π²Π΅ΡΡ ΡΠ°ΠΉΠ» ΡΠ΅Π»ΠΈΠΊΠΎΠΌ, ΡΡΠΎΠ±Ρ Ρ ΠΌΠΎΠ³ ΠΏΠΎΠ»Π½ΠΎΡΡΡΡ Π·Π°ΠΌΠ΅Π½ΠΈΡΡ ΡΡΠ°ΡΡΠΉ ΡΠ°ΠΉΠ»."""
|
|
181
|
+
|
|
182
|
+
messages.append({"role": "user", "content": second_prompt})
|
|
183
|
+
|
|
184
|
+
response2 = requests.post(
|
|
185
|
+
url="https://openrouter.ai/api/v1/chat/completions",
|
|
186
|
+
headers={
|
|
187
|
+
"Authorization": f"Bearer {API_KEY}",
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
},
|
|
190
|
+
data=json.dumps({
|
|
191
|
+
"model": "minimax/minimax-m2.5",
|
|
192
|
+
"messages": messages,
|
|
193
|
+
"reasoning": {"enabled": True}
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
response2.raise_for_status()
|
|
197
|
+
resp_json2 = response2.json()
|
|
198
|
+
assistant_message2 = resp_json2['choices'][0]['message']
|
|
199
|
+
|
|
200
|
+
content2 = assistant_message2.get('content') or ""
|
|
201
|
+
|
|
202
|
+
# ΠΠ°ΡΡΠΈΠΌ ΠΎΡΠ²Π΅Ρ
|
|
203
|
+
file_to_fix = deepest_file
|
|
204
|
+
match_file = re.search(r"FILE_TO_FIX:\s*(.*)", content2)
|
|
205
|
+
if match_file:
|
|
206
|
+
file_to_fix = match_file.group(1).strip()
|
|
207
|
+
|
|
208
|
+
parts = content2.split("```python")
|
|
209
|
+
if len(parts) > 1:
|
|
210
|
+
last_block = parts[-1].split("```")[0]
|
|
211
|
+
fixed_code = last_block.strip()
|
|
212
|
+
|
|
213
|
+
if os.path.exists(file_to_fix):
|
|
214
|
+
try:
|
|
215
|
+
with open(file_to_fix, 'w', encoding='utf-8') as f:
|
|
216
|
+
f.write(fixed_code + '\n')
|
|
217
|
+
if RICH:
|
|
218
|
+
console.print(f"[bold green][+] Π€Π°ΠΉΠ» '{file_to_fix}' ΡΡΠΏΠ΅ΡΠ½ΠΎ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½! ΠΠ°ΠΏΡΡΡΠΈΡΠ΅ ΡΠΊΡΠΈΠΏΡ Π·Π°Π½ΠΎΠ²ΠΎ.[/bold green]")
|
|
219
|
+
else:
|
|
220
|
+
print(f"{GREEN}\n[+] Π€Π°ΠΉΠ» '{file_to_fix}' ΡΡΠΏΠ΅ΡΠ½ΠΎ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½! ΠΠ°ΠΏΡΡΡΠΈΡΠ΅ ΡΠΊΡΠΈΠΏΡ Π·Π°Π½ΠΎΠ²ΠΎ.{RESET}")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
if RICH:
|
|
223
|
+
console.print(f"[bold red][-] ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°ΠΏΠΈΡΠ°ΡΡ ΡΠ°ΠΉΠ»: {e}[/bold red]")
|
|
224
|
+
else:
|
|
225
|
+
print(f"{RED}\n[-] ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°ΠΏΠΈΡΠ°ΡΡ ΡΠ°ΠΉΠ»: {e}{RESET}")
|
|
226
|
+
else:
|
|
227
|
+
if RICH:
|
|
228
|
+
console.print(f"[bold red]ΠΠ΅ Π½Π°ΠΉΠ΄Π΅Π½ ΡΠ°ΠΉΠ» Π΄Π»Ρ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ: {file_to_fix}[/bold red]")
|
|
229
|
+
else:
|
|
230
|
+
print(f"\n{RED}ΠΠ΅ Π½Π°ΠΉΠ΄Π΅Π½ ΡΠ°ΠΉΠ» Π΄Π»Ρ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ: {file_to_fix}{RESET}")
|
|
231
|
+
else:
|
|
232
|
+
if RICH:
|
|
233
|
+
console.print(f"[bold yellow]ΠΠΎΠ΄ Π΄Π»Ρ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ Π² ΠΎΡΠ²Π΅ΡΠ΅.[/bold yellow]")
|
|
234
|
+
else:
|
|
235
|
+
print(f"\n{YELLOW}ΠΠΎΠ΄ Π΄Π»Ρ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ Π² ΠΎΡΠ²Π΅ΡΠ΅.{RESET}")
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
if RICH:
|
|
239
|
+
console.print(f"[bold red]ΠΠ¨ΠΠΠΠ WhyCrash API: {e}[/bold red]")
|
|
240
|
+
else:
|
|
241
|
+
print(f"{RED}ΠΠ¨ΠΠΠΠ WhyCrash API: {e}{RESET}")
|
|
242
|
+
print("\nΠΡΠΈΠ³ΠΈΠ½Π°Π»ΡΠ½ΡΠΉ Traceback:")
|
|
243
|
+
print(tb_text)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: WhyCrash
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A highly automatic AI error handler and code fixer using OpenRouter and Minimax.
|
|
5
|
+
Home-page: https://github.com/yourusername/WhyCrash
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: rich
|
|
13
|
+
Requires-Dist: questionary
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# π WhyCrash
|
|
23
|
+
**WhyCrash** is a fully automatic AI assistant for error handling in Python. When your code crashes, WhyCrash intercepts the error, analyzes it using neural networks (OpenRouter + Minimax), gathers context from your local project files, and provides the cause along with an **AUTOMATIC CODE FIX**.
|
|
24
|
+
|
|
25
|
+
Did your code crash? The AI will explain why and automatically replace the broken file with the fixed one (if you allow it).
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
## β¨ Main Features
|
|
31
|
+
- π§ **Smart Traceback Analysis**: Understands not just the line with the error but also gathers imported local project files.
|
|
32
|
+
- π οΈ **Auto-Fixing**: Proposes a ready-made fix and can rewrite the target Python files itself.
|
|
33
|
+
- π― **Precise Control**: You decide where to catch errors: in the entire project, in a single function, or in a specific block of code.
|
|
34
|
+
- π¨ **Beautiful Interface**: Uses the `rich` library for nice windows and terminal formatting.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## π¦ Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install WhyCrash
|
|
42
|
+
```
|
|
43
|
+
> *(Requires `requests`, `rich`, and `questionary` β they will install automatically)*
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## π οΈ How to Use
|
|
48
|
+
|
|
49
|
+
You have 4 ways to control which errors WhyCrash should catch. Choose the one that fits best!
|
|
50
|
+
|
|
51
|
+
### 1. Global Intercept (Easiest)
|
|
52
|
+
If you want **any** unhandled error in your program to be analyzed by the AI:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import WhyCrash
|
|
56
|
+
|
|
57
|
+
# Enable error catching for the whole script
|
|
58
|
+
WhyCrash.debug()
|
|
59
|
+
|
|
60
|
+
# If the code crashes below, WhyCrash comes to the rescue!
|
|
61
|
+
print(1 / 0)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Dynamic Toggle (start & end)
|
|
65
|
+
If you have a large block of code and want to turn on smart analysis right before it, and turn it off right after:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import WhyCrash
|
|
69
|
+
|
|
70
|
+
# ... normal code without WhyCrash ...
|
|
71
|
+
|
|
72
|
+
WhyCrash.start_debug() # Turn on the interceptor
|
|
73
|
+
|
|
74
|
+
a = "text"
|
|
75
|
+
b = int(a) # <-- This error will go to the AI!
|
|
76
|
+
|
|
77
|
+
WhyCrash.end_debug() # Turn off the interceptor (returns to standard behavior)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Decorator for Specific Functions `@catch_errors`
|
|
81
|
+
If you are only concerned about the reliability of a specific function, you can wrap it in a decorator. If the function crashes, WhyCrash will trigger, while system errors outside of it remain untouched.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from WhyCrash import catch_errors
|
|
85
|
+
|
|
86
|
+
@catch_errors
|
|
87
|
+
def my_danger_function():
|
|
88
|
+
# If it breaks here β WhyCrash will trigger
|
|
89
|
+
file = open("no_exist.txt", "r")
|
|
90
|
+
|
|
91
|
+
def normal_function():
|
|
92
|
+
# And if it breaks here β standard Python traceback
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
my_danger_function()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4. Context Manager `with catch_block()`
|
|
99
|
+
For the most precise control, if you expect a failure in literally 2 specific lines of code:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from WhyCrash import catch_block
|
|
103
|
+
|
|
104
|
+
print("Starting work...")
|
|
105
|
+
text = "100"
|
|
106
|
+
|
|
107
|
+
with catch_block():
|
|
108
|
+
# Only code inside this block is monitored
|
|
109
|
+
number = int(text)
|
|
110
|
+
result = number / 0 # This will trigger an error sent to WhyCrash!
|
|
111
|
+
|
|
112
|
+
print("This code will not execute if there was an error above.")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## π How to Ignore Error Catching?
|
|
118
|
+
WhyCrash only analyzes **unhandled** exceptions. If you want an error in your code **not** to reach WhyCrash and the script to keep running, simply use a standard `try...except` block:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import WhyCrash
|
|
122
|
+
WhyCrash.debug()
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
int("letter")
|
|
126
|
+
except ValueError:
|
|
127
|
+
print("Error caught, it won't reach WhyCrash. Moving on!")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## βοΈ Under the Hood
|
|
131
|
+
- **OpenRouter & Minimax** β Responsible for code analysis, "Reasoning," and generating fix files.
|
|
132
|
+
- **Traceback Walking** β The script automatically follows the error chain, finds all your `.py` files involved, reads them, and sends them to the AI as context.
|
|
133
|
+
- **Rich** β Beautiful console UI (colors, panels, Markdown formatting).
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
Made with β€οΈ to save developers' nerves!
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
π **Languages:** [Π ΡΡΡΠΊΠΈΠΉ](docs/README_ru.md) | [Deutsch](docs/README_de.md)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
WhyCrash
|
whycrash-1.0.0/setup.cfg
ADDED
whycrash-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name='WhyCrash',
|
|
8
|
+
version='1.0.0',
|
|
9
|
+
packages=find_packages(),
|
|
10
|
+
install_requires=[
|
|
11
|
+
'requests',
|
|
12
|
+
'rich',
|
|
13
|
+
'questionary',
|
|
14
|
+
],
|
|
15
|
+
description='A highly automatic AI error handler and code fixer using OpenRouter and Minimax.',
|
|
16
|
+
long_description=long_description,
|
|
17
|
+
long_description_content_type='text/markdown',
|
|
18
|
+
url='https://github.com/yourusername/WhyCrash', # Update this when you have a github repo
|
|
19
|
+
classifiers=[
|
|
20
|
+
'Programming Language :: Python :: 3',
|
|
21
|
+
'License :: OSI Approved :: MIT License',
|
|
22
|
+
'Operating System :: OS Independent',
|
|
23
|
+
],
|
|
24
|
+
python_requires='>=3.8',
|
|
25
|
+
)
|