webtools-cli 1.2.4__tar.gz → 1.2.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
5
5
  Author: Abhinav Adarsh
6
6
  License-Expression: MIT
@@ -30,7 +30,7 @@ Requires-Dist: mtranslate
30
30
  Requires-Dist: colorama
31
31
  Requires-Dist: playwright
32
32
  Requires-Dist: pycryptodome
33
- Requires-Dist: tenacity>=8.2.3
33
+ Requires-Dist: tenacity<9.0.0,>=8.2.3
34
34
  Requires-Dist: pyreadline3; platform_system == "Windows"
35
35
  Provides-Extra: playwright
36
36
  Requires-Dist: playwright; extra == "playwright"
@@ -54,7 +54,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
54
54
 
55
55
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
56
56
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
57
- - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
57
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage with **Seamless Mono-Installer** (one-click, conflict-free setup).
58
58
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
59
59
  - **💻 Terminal-First**: Designed for power users who live in the command line.
60
60
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -16,7 +16,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
16
16
 
17
17
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
18
18
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
19
- - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
19
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage with **Seamless Mono-Installer** (one-click, conflict-free setup).
20
20
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
21
21
  - **💻 Terminal-First**: Designed for power users who live in the command line.
22
22
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "webtools-cli"
7
- version = "1.2.4"
7
+ version = "1.2.6"
8
8
  description = "Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -37,7 +37,7 @@ dependencies = [
37
37
  "colorama",
38
38
  "playwright",
39
39
  "pycryptodome",
40
- "tenacity>=8.2.3",
40
+ "tenacity>=8.2.3,<9.0.0",
41
41
  "pyreadline3; platform_system == 'Windows'",
42
42
  ]
43
43
 
@@ -7,59 +7,24 @@ if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
7
7
  sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
8
8
  except Exception: pass
9
9
 
10
- # --- AUTO-INSTALLER FOR MISSING/BROKEN DEPENDENCIES ---
10
+ # --- AUTO-INSTALLER FOR PLAYWRIGHT BROWSERS ---
11
11
  def ensure_dependencies():
12
- """Checks and performs a forced clean install of dependencies at runtime"""
13
- # Dependencies that must be imported successfully
14
- # Format: module_name -> (pip_package_name, min_version_prefix, forced_upgrade)
15
- check_deps = {
16
- "mega": ("mega.py", None, False),
17
- "Crypto": ("pycryptodome", None, True), # Always ensure pycryptodome over pycrypto
18
- "tenacity": ("tenacity>=8.2.3", "8", True)
19
- }
20
-
21
- missing_or_broken = []
22
-
23
- # 1. Clean up "pycrypto" which causes SyntaxError on Python 3
24
- # If we catch a SyntaxError from 'from mega import Mega', it's almost certainly pycrypto
12
+ """Ensures Playwright browsers are installed if the library is present"""
25
13
  try:
26
- import Crypto.PublicKey.RSA
27
- except (SyntaxError, AttributeError, ImportError):
28
- # SyntaxError means it's the old pycrypto. We must kill it.
29
- missing_or_broken.append("pycryptodome")
30
-
31
- # 2. Check for missing or outdated modules
32
- for module_name, (pkg_name, min_v, forced) in check_deps.items():
33
- try:
34
- mod = __import__(module_name)
35
- if min_v:
36
- from importlib.metadata import version as get_v
37
- try:
38
- if int(get_v(module_name).split('.')[0]) < int(min_v):
39
- missing_or_broken.append(pkg_name)
40
- except:
41
- if forced: missing_or_broken.append(pkg_name)
42
- except:
43
- missing_or_broken.append(pkg_name)
44
-
45
- if missing_or_broken:
46
- print(f"\n[!] Critical dependency issues detected: {', '.join(missing_or_broken)}")
47
- print("[*] Running Forced Clean Repair (Python 3.11+ Stability Patch)...")
48
- try:
49
- # Step A: Uninstall known troublemakers
50
- subprocess.call([sys.executable, "-m", "pip", "uninstall", "-y", "pycrypto", "tenacity"],
51
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
52
-
53
- # Step B: Install modern core deps
54
- subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "pycryptodome", "tenacity>=8.2.3", "requests"])
55
-
56
- # Step C: Install mega.py WITHOUT dependencies to avoid downgrading tenacity
57
- subprocess.check_call([sys.executable, "-m", "pip", "install", "mega.py", "--no-deps"])
58
-
59
- print("[+] Environment repaired successfully! Please restart the app if it fails next.\n")
60
- except Exception as e:
61
- print(f"[-] Auto-repair failed: {e}")
62
- print("[!] Please run manually: pip uninstall pycrypto tenacity && pip install pycryptodome tenacity>=8.2.3 mega.py --no-deps")
14
+ from playwright.sync_api import sync_playwright
15
+ # Simple check for browser existence
16
+ with sync_playwright() as p:
17
+ try:
18
+ p.chromium.launch(headless=True).close()
19
+ except:
20
+ print("\n[*] Playwright found but browsers are missing. Auto-installing...")
21
+ subprocess.check_call([sys.executable, "-m", "playwright", "install", "chromium"])
22
+ except ImportError:
23
+ # pip install handled this, but just in case
24
+ pass
25
+ except Exception as e:
26
+ # Non-critical failure, don't block startup
27
+ pass
63
28
 
64
29
  ensure_dependencies()
65
30
 
@@ -82,174 +47,12 @@ from collections import Counter
82
47
  from flask import Flask, render_template_string, send_from_directory, request, jsonify, send_file, after_this_request
83
48
  from PIL import Image,ExifTags,ImageChops,ImageEnhance
84
49
  from io import BytesIO
85
- from mega import Mega
86
- try:
87
- from mega.errors import RequestError
88
- from mega.crypto import (
89
- base64_to_a32, decrypt_key, base64_url_decode, a32_to_str,
90
- encrypt_key, str_to_a32, mpi_to_int, base64_url_encode
91
- )
92
- from Crypto.PublicKey import RSA
93
- import binascii
94
- except ImportError:
95
- class RequestError(Exception): pass
96
-
97
- # --- MONKEY PATCH FOR MEGA.PY (Fixes TypeError in AES calls) ---
98
- try:
99
- from Crypto.Cipher import AES
100
- import mega.mega as mega_module
101
- _original_aes_new = AES.new
102
-
103
- def _patched_aes_new(key, *args, **kwargs):
104
- # Convert key to bytes if string
105
- if isinstance(key, str):
106
- key = key.encode('latin-1')
107
-
108
- # Convert positional arguments (like IV/mode) to bytes if strings
109
- new_args = list(args)
110
- for i in range(len(new_args)):
111
- if isinstance(new_args[i], str):
112
- new_args[i] = new_args[i].encode('latin-1')
113
-
114
- # Convert keyword arguments (IV, nonce, etc.) to bytes if strings
115
- for k in kwargs:
116
- if isinstance(kwargs[k], str):
117
- kwargs[k] = kwargs[k].encode('latin-1')
118
-
119
- return _original_aes_new(key, *new_args, **kwargs)
120
-
121
- # Apply patch globally
122
- AES.new = _patched_aes_new
123
- if hasattr(mega_module, 'AES'):
124
- mega_module.AES.new = _patched_aes_new
125
- except Exception:
126
- pass
127
50
 
128
- class RobustMega(Mega):
129
- """
130
- Patched version of Mega library to handle fragile API responses.
131
- The original library crashes if the API returns non-JSON or an empty string.
132
- """
133
- def _login_process(self, resp, password):
134
- """
135
- Overridden to fix 'Invalid RSA public exponent' error in newer pycryptodome.
136
- The original code passes 0 as the exponent, which is rejected.
137
- """
138
- encrypted_master_key = base64_to_a32(resp['k'])
139
- self.master_key = decrypt_key(encrypted_master_key, password)
140
- if 'tsid' in resp:
141
- tsid = base64_url_decode(resp['tsid'])
142
- key_encrypted = a32_to_str(
143
- encrypt_key(str_to_a32(tsid[:16]), self.master_key)
144
- )
145
- if key_encrypted == tsid[-16:]:
146
- self.sid = resp['tsid']
147
- elif 'csid' in resp:
148
- encrypted_rsa_private_key = base64_to_a32(resp['privk'])
149
- rsa_private_key = decrypt_key(
150
- encrypted_rsa_private_key, self.master_key
151
- )
152
-
153
- private_key_str = a32_to_str(rsa_private_key)
154
- self.rsa_private_key = [0, 0, 0, 0]
155
-
156
- # This loop parses the 4 components: p, q, d, u
157
- for i in range(4):
158
- l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
159
- self.rsa_private_key[i] = mpi_to_int(private_key_str[:l])
160
- private_key_str = private_key_str[l:]
161
-
162
- encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
163
-
164
- # The Fix: Calculate the real 'e' from p, q, d
165
- p = self.rsa_private_key[0]
166
- q = self.rsa_private_key[1]
167
- d = self.rsa_private_key[2]
168
- n = p * q
169
- phi = (p - 1) * (q - 1)
170
-
171
- try:
172
- # Calculate real e = d^-1 mod phi
173
- e = pow(d, -1, phi)
174
- rsa_decrypter = RSA.construct((n, e, d, p, q))
175
- except:
176
- # Fallback to 65537 if inverse fails (shouldn't happen)
177
- e = 65537
178
- rsa_decrypter = RSA.construct((n, e, d, p, q), consistency_check=False)
179
-
180
- # Handle potential library differences in how the key is accessed
181
- key_obj = rsa_decrypter.key if hasattr(rsa_decrypter, 'key') else rsa_decrypter
182
-
183
- # Decrypt sid integer
184
- sid_int = key_obj._decrypt(encrypted_sid)
185
-
186
- # Convert to hex then to bytes, ensuring we handle the 0 padding if any
187
- sid_hex = '%x' % sid_int
188
- if len(sid_hex) % 2:
189
- sid_hex = '0' + sid_hex
190
- sid_bytes = binascii.unhexlify(sid_hex)
191
-
192
- self.sid = base64_url_encode(sid_bytes[:43])
193
-
194
- def _api_request(self, data):
195
- params = {'id': self.sequence_num}
196
- self.sequence_num += 1
197
-
198
- if self.sid:
199
- params.update({'sid': self.sid})
200
-
201
- if not isinstance(data, list):
202
- data = [data]
203
-
204
- url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
205
- headers = {
206
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
207
- 'Accept': '*/*',
208
- 'Accept-Language': 'en-US,en;q=0.9',
209
- 'Origin': 'https://mega.nz',
210
- 'Referer': 'https://mega.nz/'
211
- }
212
-
213
- try:
214
- req = requests.post(
215
- url,
216
- params=params,
217
- data=json.dumps(data),
218
- headers=headers,
219
- timeout=self.timeout,
220
- )
221
-
222
- content = req.text.strip() if req.text else ""
223
-
224
- if not content:
225
- if req.status_code == 402:
226
- raise Exception("Mega.nz is temporarily blocking this connection (Status 402). This usually happens due to rate-limiting. Please try again later or use a different network.")
227
- raise Exception(f"Mega API returned empty response (Status {req.status_code})")
228
-
229
- try:
230
- json_resp = json.loads(content)
231
- except json.JSONDecodeError:
232
- # Try to parse as integer error code
233
- try:
234
- val = int(content)
235
- raise RequestError(val)
236
- except (ValueError, RequestError):
237
- if content.startswith("-") and content[1:].isdigit():
238
- raise RequestError(int(content))
239
- raise Exception(f"Mega API error: {content[:100]}")
240
-
241
- if isinstance(json_resp, int):
242
- if json_resp == -3:
243
- time.sleep(0.5)
244
- return self._api_request(data=data)
245
- raise RequestError(json_resp)
246
-
247
- return json_resp[0]
248
- except Exception:
249
- raise
51
+ # Internal Mega Client (Conflict-free and Python 3.11+ compatible)
52
+ from .install import Mega, RequestError
250
53
 
251
54
  # Global Mega session state
252
- mega_engine = RobustMega()
55
+ mega_engine = Mega()
253
56
  mega_sessions = {} # Mapping: email -> {'client': MegaClient, 'info': user_info, 'quota': quota}
254
57
  active_mega_email = None
255
58
 
@@ -0,0 +1,168 @@
1
+ """Installation and Utility Utilities for WebTools CLI"""
2
+ import os
3
+ import sys
4
+ import json
5
+ import random
6
+ import binascii
7
+ import requests
8
+ import subprocess
9
+ from Crypto.Cipher import AES
10
+ from Crypto.PublicKey import RSA
11
+ from Crypto.Util import Counter
12
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
13
+
14
+ # --- PLAYWRIGHT SETUP ---
15
+
16
+ def install_playwright_browsers():
17
+ """Install Playwright Chromium browser after package installation"""
18
+ try:
19
+ print("📦 Installing Playwright Chromium browser...")
20
+ subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
21
+ print("✓ Playwright Chromium installed successfully")
22
+ except subprocess.CalledProcessError:
23
+ print("⚠️ Failed to install Playwright browsers. Run manually: playwright install chromium")
24
+ except Exception as e:
25
+ print(f"⚠️ Playwright browser installation skipped: {e}")
26
+
27
+ # --- INTERNAL MEGA CLIENT (Conflict-free & Python 3.11+ compatible) ---
28
+
29
+ class RequestError(Exception):
30
+ """Exception for Mega API requests"""
31
+ pass
32
+
33
+ def base64_url_decode(data):
34
+ data += '=' * (4 - len(data) % 4)
35
+ return binascii.a2b_base64(data.replace('-', '+').replace('_', '/'))
36
+
37
+ def base64_url_encode(data):
38
+ return binascii.b2a_base64(data).decode('utf-8').strip().replace('+', '-').replace('/', '_').replace('=', '')
39
+
40
+ def a32_to_str(a):
41
+ return binascii.unhexlify(''.join(format(i, '08x') for i in a))
42
+
43
+ def str_to_a32(s):
44
+ if len(s) % 4:
45
+ s += b'\0' * (4 - len(s) % 4)
46
+ return [int(binascii.hexlify(s[i:i + 4]), 16) for i in range(0, len(s), 4)]
47
+
48
+ def base64_to_a32(s):
49
+ return str_to_a32(base64_url_decode(s))
50
+
51
+ def a32_to_base64(a):
52
+ return base64_url_encode(a32_to_str(a))
53
+
54
+ def aes_cbc_encrypt(data, key):
55
+ cipher = AES.new(a32_to_str(key), AES.MODE_CBC, b'\0' * 16)
56
+ return str_to_a32(cipher.encrypt(a32_to_str(data)))
57
+
58
+ def aes_cbc_decrypt(data, key):
59
+ cipher = AES.new(a32_to_str(key), AES.MODE_CBC, b'\0' * 16)
60
+ return str_to_a32(cipher.decrypt(a32_to_str(data)))
61
+
62
+ def decrypt_key(a, k):
63
+ res = []
64
+ for i in range(0, len(a), 4):
65
+ res += aes_cbc_decrypt(a[i:i + 4], k)
66
+ return res
67
+
68
+ def encrypt_key(a, k):
69
+ res = []
70
+ for i in range(0, len(a), 4):
71
+ res += aes_cbc_encrypt(a[i:i + 4], k)
72
+ return res
73
+
74
+ def prepare_key(a):
75
+ v = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]
76
+ for _ in range(0x10000):
77
+ for j in range(0, len(a), 4):
78
+ key = [0, 0, 0, 0]
79
+ for k in range(4):
80
+ if j + k < len(a):
81
+ key[k] = a[j + k]
82
+ v = aes_cbc_encrypt(v, key)
83
+ return v
84
+
85
+ def mpi_to_int(s):
86
+ return int(binascii.hexlify(s[2:]), 16)
87
+
88
+ class Mega:
89
+ def __init__(self):
90
+ self.sid = None
91
+ self.api_url = "https://g.api.mega.co.nz/cs"
92
+ self.sequence_num = random.randint(0, 0xFFFFFFFF)
93
+
94
+ def login(self, email, password):
95
+ password_aes = prepare_key(str_to_a32(password.encode('utf-8')))
96
+ user_hash = self._stringhash(email, password_aes)
97
+ resp = self._api_request({'a': 'us', 'user': email, 'uh': user_hash})
98
+
99
+ if isinstance(resp, int) and resp < 0:
100
+ raise RequestError(f"Login failed: {resp}")
101
+
102
+ encrypted_key = base64_to_a32(resp['k'])
103
+ self.master_key = decrypt_key(encrypted_key, password_aes)
104
+
105
+ if 'tsid' in resp:
106
+ self.sid = resp['tsid']
107
+ elif 'csid' in resp:
108
+ encrypted_rsa_private_key = base64_to_a32(resp['privk'])
109
+ rsa_private_key = decrypt_key(encrypted_rsa_private_key, self.master_key)
110
+ private_key_str = a32_to_str(rsa_private_key)
111
+ components = [0, 0, 0, 0]
112
+ for i in range(4):
113
+ l = int(((private_key_str[0]) * 256 + (private_key_str[1]) + 7) / 8) + 2
114
+ components[i] = mpi_to_int(private_key_str[:l])
115
+ private_key_str = private_key_str[l:]
116
+ encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
117
+ p, q, d, _ = components
118
+ n, phi = p * q, (p - 1) * (q - 1)
119
+ try: e = pow(d, -1, phi)
120
+ except: e = 65537
121
+ rsa_decrypter = RSA.construct((n, e, d, p, q))
122
+ sid_int = rsa_decrypter._decrypt(encrypted_sid)
123
+ sid_hex = '%x' % sid_int
124
+ if len(sid_hex) % 2: sid_hex = '0' + sid_hex
125
+ self.sid = base64_url_encode(binascii.unhexlify(sid_hex)[:43])
126
+ return self
127
+
128
+ def _stringhash(self, email, aes_key):
129
+ s = email.encode('utf-8')
130
+ h = [0, 0, 0, 0]
131
+ for i, b in enumerate(s): h[i % 4] ^= b
132
+ for _ in range(0x4000): h = aes_cbc_encrypt(h, aes_key)
133
+ return a32_to_base64([h[0], h[2]])
134
+
135
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10),
136
+ retry=retry_if_exception_type(requests.exceptions.RequestException))
137
+ def _api_request(self, data):
138
+ url = f"{self.api_url}?id={self.sequence_num}"
139
+ if self.sid: url += f"&sid={self.sid}"
140
+ self.sequence_num += 1
141
+ response = requests.post(url, json=[data], timeout=30)
142
+ response.raise_for_status()
143
+ result = response.json()
144
+ if isinstance(result, list):
145
+ res = result[0]
146
+ if isinstance(res, int) and res < 0: raise RequestError(f"API Error {res}")
147
+ return res
148
+ return result
149
+
150
+ def get_storage_space(self):
151
+ resp = self._api_request({'a': 'uq', 'strg': 1})
152
+ return {'total': resp['mstrg'], 'used': resp['cstrg']}
153
+
154
+ def upload(self, filename, dest_folder=None):
155
+ size = os.path.getsize(filename)
156
+ resp = self._api_request({'a': 'u', 's': size})
157
+ upload_url = resp['p']
158
+ with open(filename, 'rb') as f: data = f.read()
159
+ r = requests.post(upload_url, data=data, timeout=60)
160
+ upload_token = r.text
161
+ file_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)]
162
+ attribs = {'n': os.path.basename(filename)}
163
+ encoded_attribs = base64_url_encode(b'MEGA' + json.dumps(attribs).encode('utf-8'))
164
+ data = {'a': 'p', 't': dest_folder or 'ROOT', 'n': [{'h': upload_token, 't': 0, 'a': encoded_attribs, 'k': a32_to_base64(file_key[:4])}]}
165
+ return self._api_request(data)
166
+
167
+ if __name__ == "__main__":
168
+ install_playwright_browsers()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtools-cli
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Advanced Web Intelligence & Scraping Toolkit with CLI and Web UI
5
5
  Author: Abhinav Adarsh
6
6
  License-Expression: MIT
@@ -30,7 +30,7 @@ Requires-Dist: mtranslate
30
30
  Requires-Dist: colorama
31
31
  Requires-Dist: playwright
32
32
  Requires-Dist: pycryptodome
33
- Requires-Dist: tenacity>=8.2.3
33
+ Requires-Dist: tenacity<9.0.0,>=8.2.3
34
34
  Requires-Dist: pyreadline3; platform_system == "Windows"
35
35
  Provides-Extra: playwright
36
36
  Requires-Dist: playwright; extra == "playwright"
@@ -54,7 +54,7 @@ WebTools CLI is an advanced web intelligence suite for researchers, OSINT enthus
54
54
 
55
55
  - **🎯 Stealth & Speed**: Smart proxy rotation and Turbo-Fetch logic for evasion and performance.
56
56
  - **🧠 AI-Powered**: Automated content summarization, sentiment analysis, and readability scoring.
57
- - **☁️ Cloud-Native**: Integrated Mega.nz storage for seamless media backups and file management.
57
+ - **☁️ Cloud-Native**: Integrated Mega.nz storage with **Seamless Mono-Installer** (one-click, conflict-free setup).
58
58
  - **🔧 Security-Centric**: Built-in honeypot detection, threat leveling, and image forensic analysis.
59
59
  - **💻 Terminal-First**: Designed for power users who live in the command line.
60
60
  - **🛡️ Cross-Platform**: Works seamlessly on Windows, Linux, and macOS (with auto-download for Windows tunnels).
@@ -10,7 +10,7 @@ mtranslate
10
10
  colorama
11
11
  playwright
12
12
  pycryptodome
13
- tenacity>=8.2.3
13
+ tenacity<9.0.0,>=8.2.3
14
14
 
15
15
  [:platform_system == "Windows"]
16
16
  pyreadline3
@@ -1,17 +0,0 @@
1
- """Post-install script to setup Playwright browsers"""
2
- import subprocess
3
- import sys
4
-
5
- def install_playwright_browsers():
6
- """Install Playwright Chromium browser after package installation"""
7
- try:
8
- print("📦 Installing Playwright Chromium browser...")
9
- subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
10
- print("✓ Playwright Chromium installed successfully")
11
- except subprocess.CalledProcessError:
12
- print("⚠️ Failed to install Playwright browsers. Run manually: playwright install chromium")
13
- except Exception as e:
14
- print(f"⚠️ Playwright browser installation skipped: {e}")
15
-
16
- if __name__ == "__main__":
17
- install_playwright_browsers()
File without changes
File without changes