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 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 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)
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 = 95 * 1024 * 1024 # 95 MB
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 * 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.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,,
@@ -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,,