sideload 0.1.0__py3-none-any.whl → 1.1.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 CHANGED
@@ -178,13 +178,26 @@ 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, work_dir: Path = None
182
182
  ):
183
183
  """Extract parts from wheel files and reassemble the original file"""
184
- with tempfile.TemporaryDirectory() as temp_dir:
185
- temp_path = Path(temp_dir)
184
+ # Use provided work directory or create a temporary one
185
+ use_temp = work_dir is None
186
+ if use_temp:
187
+ temp_dir_obj = tempfile.TemporaryDirectory()
188
+ temp_path = Path(temp_dir_obj.name)
189
+ else:
190
+ temp_dir_obj = None
191
+ temp_path = work_dir
192
+ temp_path.mkdir(parents=True, exist_ok=True)
193
+
194
+ try:
186
195
  part_files = []
187
196
 
197
+ if not use_temp:
198
+ console.print(f"[yellow]⚠️ Using work directory: {temp_path}[/yellow]")
199
+ console.print(f"[yellow] Files will be kept after extraction for debugging[/yellow]")
200
+
188
201
  with Progress(
189
202
  SpinnerColumn(),
190
203
  TextColumn("[progress.description]{task.description}"),
@@ -197,7 +210,7 @@ class SideloadClient:
197
210
  )
198
211
 
199
212
  # Extract each wheel file
200
- for i, wheel_file in enumerate(wheel_files):
213
+ for i, (wheel_file, package_name) in enumerate(zip(wheel_files, package_names)):
201
214
  progress.update(
202
215
  extract_task,
203
216
  description=f"📂 Extracting {wheel_file.name}...",
@@ -208,14 +221,33 @@ class SideloadClient:
208
221
  import zipfile
209
222
 
210
223
  with zipfile.ZipFile(wheel_file, "r") as zip_ref:
224
+ if debug:
225
+ console.print(f"\n[dim]Contents of {wheel_file.name}:[/dim]")
226
+ for name in sorted(zip_ref.namelist()):
227
+ console.print(f"[dim] {name}[/dim]")
211
228
  zip_ref.extractall(temp_path)
212
229
 
213
- # Find the part file in the wheel's data directory structure
214
- # Pattern: pkgname.data/data/share/pkgname/pkgname
215
- data_dirs = list(temp_path.glob("*.data/data/share/*/*"))
216
- for part_file in data_dirs:
217
- if part_file.is_file():
218
- part_files.append(part_file)
230
+ # Find the part file using the exact package name
231
+ # Pattern: pkgname-version.data/data/share/pkgname/pkgname
232
+ # Need to find the .data directory that starts with package_name
233
+ data_dir = None
234
+ for d in temp_path.iterdir():
235
+ if d.is_dir() and d.name.startswith(f"{package_name}-") and d.name.endswith(".data"):
236
+ data_dir = d
237
+ break
238
+
239
+ if data_dir:
240
+ part_file_path = data_dir / "data" / "share" / package_name / package_name
241
+ if debug:
242
+ console.print(f"[dim]Looking for: {part_file_path}[/dim]")
243
+ 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]")
244
+ if part_file_path.is_file():
245
+ part_files.append(part_file_path)
246
+ if debug:
247
+ console.print(f"[green]✓ Found part file: {part_file_path.name} (size: {part_file_path.stat().st_size:,} bytes)[/green]")
248
+ else:
249
+ if debug:
250
+ console.print(f"[yellow]⚠ Could not find .data directory for {package_name}[/yellow]")
219
251
 
220
252
  progress.update(
221
253
  extract_task,
@@ -263,6 +295,10 @@ class SideloadClient:
263
295
  description="✅ Reassembly complete!",
264
296
  completed=len(part_files),
265
297
  )
298
+ finally:
299
+ # Clean up temporary directory if we created one
300
+ if use_temp and temp_dir_obj:
301
+ temp_dir_obj.cleanup()
266
302
 
267
303
 
268
304
  def display_header():
@@ -317,6 +353,16 @@ Examples:
317
353
  )
318
354
  download_parser.add_argument("--collection", help="JSONBin collection ID")
319
355
  download_parser.add_argument("--token", help="JSONBin API token")
356
+ download_parser.add_argument(
357
+ "--debug",
358
+ action="store_true",
359
+ help="Enable debug logging",
360
+ )
361
+ download_parser.add_argument(
362
+ "--work-dir",
363
+ type=Path,
364
+ help="Working directory for extraction (for debugging, defaults to temp directory)",
365
+ )
320
366
 
321
367
  args = parser.parse_args()
322
368
 
@@ -393,7 +439,7 @@ Examples:
393
439
  output_file = args.output / original_filename
394
440
 
395
441
  client.extract_and_reassemble(
396
- wheel_files, original_filename, output_file
442
+ wheel_files, package_names, original_filename, output_file, args.debug, args.work_dir
397
443
  )
398
444
 
399
445
  # Success!
sideload/main.py CHANGED
@@ -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 * 1000 # Size of each chunk in bytes.
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
- current_part_fp.write(data)
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: 0.1.0
3
+ Version: 1.1.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=zGcpAJ_j4ZrqWln3lQfZ2kO8TD_AW2PjAfVo3BvRIwM,17465
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=EiZguc4-ug8RM6vXkZuXW_ps8jFWoAfeKBBNy7FX-F0,7600
7
+ sideload/scripts/cleanup_pypi.py,sha256=CouuOhnbpSbrFc1KhlOeZAuajVVpGF9d4qwFpIKkEvY,5906
8
+ sideload-1.1.0.dist-info/WHEEL,sha256=-neZj6nU9KAMg2CnCY6T3w8J53nx1kFGw_9HfoSzM60,79
9
+ sideload-1.1.0.dist-info/entry_points.txt,sha256=7ULrIjaVhrxMhuddTeoPjeIrqmIvVc9cSU3lZU2_YqE,44
10
+ sideload-1.1.0.dist-info/METADATA,sha256=AGrmSEXpybApWqfbA3uv9MHaGY2wUXo_poyPHWVRvd8,4281
11
+ sideload-1.1.0.dist-info/RECORD,,
@@ -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,,