sideload 0.1.0__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of sideload might be problematic. Click here for more details.
- sideload/cli.py +33 -9
- sideload/main.py +6 -6
- sideload/scripts/cleanup_pypi.py +171 -0
- {sideload-0.1.0.dist-info → sideload-1.0.0.dist-info}/METADATA +2 -1
- sideload-1.0.0.dist-info/RECORD +11 -0
- sideload-0.1.0.dist-info/RECORD +0 -10
- {sideload-0.1.0.dist-info → sideload-1.0.0.dist-info}/WHEEL +0 -0
- {sideload-0.1.0.dist-info → sideload-1.0.0.dist-info}/entry_points.txt +0 -0
sideload/cli.py
CHANGED
|
@@ -178,7 +178,7 @@ class SideloadClient:
|
|
|
178
178
|
return downloaded_files
|
|
179
179
|
|
|
180
180
|
def extract_and_reassemble(
|
|
181
|
-
self, wheel_files: List[Path], original_filename: str, output_path: Path
|
|
181
|
+
self, wheel_files: List[Path], package_names: List[str], original_filename: str, output_path: Path, debug: bool = False
|
|
182
182
|
):
|
|
183
183
|
"""Extract parts from wheel files and reassemble the original file"""
|
|
184
184
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
@@ -197,7 +197,7 @@ class SideloadClient:
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
# Extract each wheel file
|
|
200
|
-
for i, wheel_file in enumerate(wheel_files):
|
|
200
|
+
for i, (wheel_file, package_name) in enumerate(zip(wheel_files, package_names)):
|
|
201
201
|
progress.update(
|
|
202
202
|
extract_task,
|
|
203
203
|
description=f"📂 Extracting {wheel_file.name}...",
|
|
@@ -208,14 +208,33 @@ class SideloadClient:
|
|
|
208
208
|
import zipfile
|
|
209
209
|
|
|
210
210
|
with zipfile.ZipFile(wheel_file, "r") as zip_ref:
|
|
211
|
+
if debug:
|
|
212
|
+
console.print(f"\n[dim]Contents of {wheel_file.name}:[/dim]")
|
|
213
|
+
for name in sorted(zip_ref.namelist()):
|
|
214
|
+
console.print(f"[dim] {name}[/dim]")
|
|
211
215
|
zip_ref.extractall(temp_path)
|
|
212
216
|
|
|
213
|
-
# Find the part file
|
|
214
|
-
# Pattern: pkgname.data/data/share/pkgname/pkgname
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
# Find the part file using the exact package name
|
|
218
|
+
# Pattern: pkgname-version.data/data/share/pkgname/pkgname
|
|
219
|
+
# Need to find the .data directory that starts with package_name
|
|
220
|
+
data_dir = None
|
|
221
|
+
for d in temp_path.iterdir():
|
|
222
|
+
if d.is_dir() and d.name.startswith(f"{package_name}-") and d.name.endswith(".data"):
|
|
223
|
+
data_dir = d
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
if data_dir:
|
|
227
|
+
part_file_path = data_dir / "data" / "share" / package_name / package_name
|
|
228
|
+
if debug:
|
|
229
|
+
console.print(f"[dim]Looking for: {part_file_path}[/dim]")
|
|
230
|
+
console.print(f"[dim]Exists: {part_file_path.exists()}, Is file: {part_file_path.is_file() if part_file_path.exists() else 'N/A'}[/dim]")
|
|
231
|
+
if part_file_path.is_file():
|
|
232
|
+
part_files.append(part_file_path)
|
|
233
|
+
if debug:
|
|
234
|
+
console.print(f"[green]✓ Found part file: {part_file_path.name} (size: {part_file_path.stat().st_size:,} bytes)[/green]")
|
|
235
|
+
else:
|
|
236
|
+
if debug:
|
|
237
|
+
console.print(f"[yellow]⚠ Could not find .data directory for {package_name}[/yellow]")
|
|
219
238
|
|
|
220
239
|
progress.update(
|
|
221
240
|
extract_task,
|
|
@@ -317,6 +336,11 @@ Examples:
|
|
|
317
336
|
)
|
|
318
337
|
download_parser.add_argument("--collection", help="JSONBin collection ID")
|
|
319
338
|
download_parser.add_argument("--token", help="JSONBin API token")
|
|
339
|
+
download_parser.add_argument(
|
|
340
|
+
"--debug",
|
|
341
|
+
action="store_true",
|
|
342
|
+
help="Enable debug logging",
|
|
343
|
+
)
|
|
320
344
|
|
|
321
345
|
args = parser.parse_args()
|
|
322
346
|
|
|
@@ -393,7 +417,7 @@ Examples:
|
|
|
393
417
|
output_file = args.output / original_filename
|
|
394
418
|
|
|
395
419
|
client.extract_and_reassemble(
|
|
396
|
-
wheel_files, original_filename, output_file
|
|
420
|
+
wheel_files, package_names, original_filename, output_file, args.debug
|
|
397
421
|
)
|
|
398
422
|
|
|
399
423
|
# Success!
|
sideload/main.py
CHANGED
|
@@ -11,7 +11,7 @@ from sideload.jsonbin_connector import JSONBinConnector
|
|
|
11
11
|
|
|
12
12
|
JSONBIN_TOKEN = os.environ["JSONBIN_TOKEN"]
|
|
13
13
|
PYPI_TOKEN = os.environ["PYPI_TOKEN"]
|
|
14
|
-
MAX_PACKAGE_SIZE =
|
|
14
|
+
MAX_PACKAGE_SIZE = 2 * 1024 * 1024 # 95 MB
|
|
15
15
|
|
|
16
16
|
LAST_BINS: dict[str, str | None] = {}
|
|
17
17
|
|
|
@@ -91,7 +91,7 @@ def download_file(bin_id: str, url: str):
|
|
|
91
91
|
|
|
92
92
|
# Initialize variables to track progress.
|
|
93
93
|
downloaded = 0
|
|
94
|
-
chunk_size = 1024 *
|
|
94
|
+
chunk_size = 1024 * 1024 # Size of each chunk in bytes.
|
|
95
95
|
last_progress = 0
|
|
96
96
|
filename_root = filename.split(".")[0]
|
|
97
97
|
package_name = f"sideload_{filename_root}_bin_{bin_id}"
|
|
@@ -122,13 +122,13 @@ def download_file(bin_id: str, url: str):
|
|
|
122
122
|
try:
|
|
123
123
|
current_chunk_size = 0
|
|
124
124
|
for data in response.iter_content(chunk_size=chunk_size):
|
|
125
|
-
|
|
126
|
-
downloaded += len(data)
|
|
127
|
-
current_chunk_size += len(data)
|
|
128
|
-
if current_chunk_size >= MAX_PACKAGE_SIZE:
|
|
125
|
+
if current_chunk_size + len(data) > MAX_PACKAGE_SIZE:
|
|
129
126
|
current_part_fp.close()
|
|
130
127
|
current_part_fp = make_new_part()
|
|
131
128
|
current_chunk_size = 0
|
|
129
|
+
current_part_fp.write(data)
|
|
130
|
+
downloaded += len(data)
|
|
131
|
+
current_chunk_size += len(data)
|
|
132
132
|
if total_size < downloaded:
|
|
133
133
|
total_size = downloaded
|
|
134
134
|
progress = 99
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Admin script to delete all sideload-* packages from PyPI using browser automation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import asyncio
|
|
9
|
+
from playwright.async_api import async_playwright
|
|
10
|
+
|
|
11
|
+
PYPI_USER = os.environ.get("PYPI_USER")
|
|
12
|
+
PYPI_PASSWORD = os.environ.get("PYPI_PASSWORD")
|
|
13
|
+
|
|
14
|
+
if not PYPI_USER or not PYPI_PASSWORD:
|
|
15
|
+
print("❌ PYPI_USER and PYPI_PASSWORD environment variables must be set")
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def delete_project(page, package_name: str) -> bool:
|
|
20
|
+
"""Delete a project from PyPI using browser automation"""
|
|
21
|
+
try:
|
|
22
|
+
print(f"🗑️ Deleting {package_name}...")
|
|
23
|
+
|
|
24
|
+
# Go to project settings
|
|
25
|
+
settings_url = f'https://pypi.org/manage/project/{package_name}/settings/'
|
|
26
|
+
await page.goto(settings_url)
|
|
27
|
+
|
|
28
|
+
# Check if project exists (if we get 404, project doesn't exist)
|
|
29
|
+
title = await page.title()
|
|
30
|
+
if "404" in title or "Not Found" in title:
|
|
31
|
+
print(f" ⚠️ Project {package_name} not found")
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
# Scroll to the bottom of the page to find delete section
|
|
35
|
+
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
|
|
36
|
+
await asyncio.sleep(0.5)
|
|
37
|
+
|
|
38
|
+
# Find and check all "I understand..." checkboxes
|
|
39
|
+
checkboxes = await page.query_selector_all('input[type="checkbox"]')
|
|
40
|
+
for checkbox in checkboxes:
|
|
41
|
+
await checkbox.check()
|
|
42
|
+
|
|
43
|
+
# Find and type project name in confirmation field
|
|
44
|
+
confirm_input = await page.query_selector('input[name="confirm_project_name"]')
|
|
45
|
+
if not confirm_input:
|
|
46
|
+
print(f" ⚠️ Could not find confirmation input for {package_name}")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
await confirm_input.fill(package_name)
|
|
50
|
+
|
|
51
|
+
# Find and click the delete link that opens the modal
|
|
52
|
+
# This is the button with href="#project_name-modal"
|
|
53
|
+
delete_link = await page.query_selector('a.button--danger[data-delete-confirm-target="button"]')
|
|
54
|
+
if not delete_link:
|
|
55
|
+
print(f" ⚠️ Could not find delete link for {package_name}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# Wait a moment to ensure button is enabled
|
|
59
|
+
await asyncio.sleep(0.5)
|
|
60
|
+
|
|
61
|
+
# Click the link to open the modal
|
|
62
|
+
await delete_link.click()
|
|
63
|
+
await asyncio.sleep(0.5)
|
|
64
|
+
|
|
65
|
+
# Now find and click the actual delete button in the modal
|
|
66
|
+
modal_delete_button = await page.query_selector('button[type="submit"]')
|
|
67
|
+
if not modal_delete_button:
|
|
68
|
+
print(f" ⚠️ Could not find modal delete button for {package_name}")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
await modal_delete_button.click()
|
|
72
|
+
await page.wait_for_load_state("networkidle")
|
|
73
|
+
|
|
74
|
+
print(f" ✅ Deleted {package_name}")
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f" ❌ Error deleting {package_name}: {e}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def get_user_projects(page) -> list[str]:
|
|
83
|
+
"""Get all projects owned by the user that start with sideload-"""
|
|
84
|
+
try:
|
|
85
|
+
print("🔍 Fetching your PyPI projects...")
|
|
86
|
+
|
|
87
|
+
# Go to projects page
|
|
88
|
+
await page.goto('https://pypi.org/manage/projects/')
|
|
89
|
+
await page.wait_for_load_state("networkidle")
|
|
90
|
+
|
|
91
|
+
# Extract project names
|
|
92
|
+
projects = await page.evaluate('''() => {
|
|
93
|
+
const links = Array.from(document.querySelectorAll('a[href*="/manage/project/"]'));
|
|
94
|
+
return links.map(link => {
|
|
95
|
+
const match = link.href.match(/\\/manage\\/project\\/([^\\/]+)\\//);
|
|
96
|
+
return match ? match[1] : null;
|
|
97
|
+
}).filter(name => name && name.startsWith('sideload-'));
|
|
98
|
+
}''')
|
|
99
|
+
|
|
100
|
+
return projects
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"❌ Error fetching projects: {e}")
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def main():
|
|
108
|
+
print("🧹 PyPI Sideload Package Cleanup Tool")
|
|
109
|
+
print("=" * 50)
|
|
110
|
+
print()
|
|
111
|
+
|
|
112
|
+
async with async_playwright() as p:
|
|
113
|
+
# Launch browser
|
|
114
|
+
browser = await p.chromium.launch(headless=False)
|
|
115
|
+
context = await browser.new_context()
|
|
116
|
+
page = await context.new_page()
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Navigate to PyPI login page
|
|
120
|
+
print("🔐 Logging in to PyPI...")
|
|
121
|
+
print(" Opening login page...")
|
|
122
|
+
await page.goto('https://pypi.org/account/login/')
|
|
123
|
+
|
|
124
|
+
# Fill in login credentials
|
|
125
|
+
print(" Filling credentials...")
|
|
126
|
+
await page.fill('#username', PYPI_USER)
|
|
127
|
+
await page.fill('#password', PYPI_PASSWORD)
|
|
128
|
+
|
|
129
|
+
# Submit login form
|
|
130
|
+
await page.click('input[type="submit"]')
|
|
131
|
+
await asyncio.sleep(1)
|
|
132
|
+
|
|
133
|
+
# Wait for user to complete TOTP if required
|
|
134
|
+
current_url = page.url
|
|
135
|
+
if '/account/two-factor/' in current_url:
|
|
136
|
+
print(" ⚠️ TOTP required - please enter your 2FA code in the browser...")
|
|
137
|
+
while True:
|
|
138
|
+
current_url = page.url
|
|
139
|
+
# Wait until we're not on TOTP page anymore
|
|
140
|
+
if '/account/two-factor/' not in current_url:
|
|
141
|
+
break
|
|
142
|
+
await asyncio.sleep(1)
|
|
143
|
+
|
|
144
|
+
print("✅ Login completed!")
|
|
145
|
+
|
|
146
|
+
# Get all sideload projects
|
|
147
|
+
projects = await get_user_projects(page)
|
|
148
|
+
|
|
149
|
+
if not projects:
|
|
150
|
+
print("❌ No sideload-* projects found")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
print(f"\n📋 Found {len(projects)} sideload projects:")
|
|
154
|
+
for pkg in projects:
|
|
155
|
+
print(f" - {pkg}")
|
|
156
|
+
|
|
157
|
+
# Delete each project
|
|
158
|
+
print("\n🗑️ Deleting projects...\n")
|
|
159
|
+
deleted = 0
|
|
160
|
+
for pkg in projects:
|
|
161
|
+
if await delete_project(page, pkg):
|
|
162
|
+
deleted += 1
|
|
163
|
+
|
|
164
|
+
print(f"\n✅ Deleted {deleted}/{len(projects)} projects")
|
|
165
|
+
|
|
166
|
+
finally:
|
|
167
|
+
await browser.close()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
asyncio.run(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sideload
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Download large files via PyPI packages
|
|
5
5
|
Author: Sygmei
|
|
6
6
|
Author-email: Sygmei <3835355+Sygmei@users.noreply.github.com>
|
|
@@ -10,6 +10,7 @@ Requires-Dist: wheel>=0.45.1
|
|
|
10
10
|
Requires-Dist: rich>=13.0.0
|
|
11
11
|
Requires-Dist: httpx>=0.28.1
|
|
12
12
|
Requires-Dist: pip>=25.2
|
|
13
|
+
Requires-Dist: playwright>=1.55.0
|
|
13
14
|
Requires-Python: >=3.12
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sideload/__init__.py,sha256=Y3rHLtR7n0sjLXrn-BEcrbIHx-9uE1tPZkooavo7xcA,222
|
|
2
|
+
sideload/cli.py,sha256=V_pMrvBBLuFYFd1gM2AHYRUOUy8KO5lA9jkLnURP4ZU,16586
|
|
3
|
+
sideload/jsonbin.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
sideload/jsonbin_connector.py,sha256=HtR1Pwnpm5jfYcmnvcug9HaKxFpsyJBXYYlLuzWPiTE,1680
|
|
5
|
+
sideload/jsonbin_old.py,sha256=ve21WsV7Ay60moFfrR4lSM_JRZNqo4z5v69cNw0Iqo0,8411
|
|
6
|
+
sideload/main.py,sha256=aCI3DvjNeCyEVqmp12g4HdZOCuHhTwCPK1hIKrUMeBQ,7599
|
|
7
|
+
sideload/scripts/cleanup_pypi.py,sha256=CouuOhnbpSbrFc1KhlOeZAuajVVpGF9d4qwFpIKkEvY,5906
|
|
8
|
+
sideload-1.0.0.dist-info/WHEEL,sha256=-neZj6nU9KAMg2CnCY6T3w8J53nx1kFGw_9HfoSzM60,79
|
|
9
|
+
sideload-1.0.0.dist-info/entry_points.txt,sha256=7ULrIjaVhrxMhuddTeoPjeIrqmIvVc9cSU3lZU2_YqE,44
|
|
10
|
+
sideload-1.0.0.dist-info/METADATA,sha256=jBo_E7jtnSr0XUTEKmQTqdcE0tyvFtvqp0Rc1ZGEdUM,4281
|
|
11
|
+
sideload-1.0.0.dist-info/RECORD,,
|
sideload-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
sideload/__init__.py,sha256=Y3rHLtR7n0sjLXrn-BEcrbIHx-9uE1tPZkooavo7xcA,222
|
|
2
|
-
sideload/cli.py,sha256=2UQ_DgvB7S0MXmBhXwjx2T78-ZZzCUO4hN2YfCJ2hf8,15061
|
|
3
|
-
sideload/jsonbin.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
sideload/jsonbin_connector.py,sha256=HtR1Pwnpm5jfYcmnvcug9HaKxFpsyJBXYYlLuzWPiTE,1680
|
|
5
|
-
sideload/jsonbin_old.py,sha256=ve21WsV7Ay60moFfrR4lSM_JRZNqo4z5v69cNw0Iqo0,8411
|
|
6
|
-
sideload/main.py,sha256=5iAQQlHeCdCHxOWE9OPKHF9TNs0k45n9rnUbruleT8w,7589
|
|
7
|
-
sideload-0.1.0.dist-info/WHEEL,sha256=-neZj6nU9KAMg2CnCY6T3w8J53nx1kFGw_9HfoSzM60,79
|
|
8
|
-
sideload-0.1.0.dist-info/entry_points.txt,sha256=7ULrIjaVhrxMhuddTeoPjeIrqmIvVc9cSU3lZU2_YqE,44
|
|
9
|
-
sideload-0.1.0.dist-info/METADATA,sha256=Ouf83o2V29NcOs2pEPWVxmhgzUBP1D-SvgDt69-jZlY,4247
|
|
10
|
-
sideload-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|