1414import requests
1515from requests .exceptions import RequestException
1616
17- import argparse
18- import re
19- import os
17+ _PYPI_URL_PROD = 'https://pypi.org/'
18+ _PYPI_URL_TEST = 'https://test.pypi.org/'
2019
2120def valid_hostname (hostname ):
2221 """Validate hostname format"""
@@ -41,7 +40,9 @@ def non_empty_string(value):
4140 epilog = "Environment variables required (unless --dry): PYPI_CLEANUP_PASSWORD, PYPI_CLEANUP_OTP"
4241)
4342parser .add_argument ("--dry" , action = "store_true" , help = "Show what would be deleted but don't actually do it" )
44- parser .add_argument ("-i" , "--index-hostname" , type = valid_hostname , required = True , help = "Index hostname (required)" )
43+ host_group = parser .add_mutually_exclusive_group (required = True )
44+ host_group .add_argument ("--prod" , action = "store_true" , help = "Use production PyPI (pypi.org)" )
45+ host_group .add_argument ("--test" , action = "store_true" , help = "Use test PyPI (test.pypi.org)" )
4546parser .add_argument ("-m" , "--max-nightlies" , type = int , default = 2 , help = "Max number of nightlies of unreleased versions (default=2)" )
4647parser .add_argument ("-u" , "--username" , type = non_empty_string , help = "Username (required unless --dry)" )
4748args = parser .parse_args ()
@@ -62,21 +63,23 @@ def non_empty_string(value):
6263 if not otp :
6364 parser .error ("PYPI_CLEANUP_OTP environment variable is required when not in dry-run mode" )
6465
65- print (f"Dry run: { args .dry } " )
66- print (f"Max nightlies: { args .max_nightlies } " )
66+ dry_run = args .dry
67+ pypi_username = args .username
68+ max_dev_releases = args .max_nightlies
69+ if args .prod :
70+ pypi_url = _PYPI_URL_PROD
71+ else :
72+ pypi_url = _PYPI_URL_TEST
73+ pypi_password = password
74+ pypi_otp = otp
75+
76+ print (f"Dry run: { dry_run } " )
77+ print (f"Max nightlies: { max_dev_releases } " )
6778if not args .dry :
68- print (f"Hostname : { args . index_hostname } " )
69- print (f"Username: { args . username } " )
79+ print (f"URL : { pypi_url } " )
80+ print (f"Username: { pypi_username } " )
7081 print ("Password and OTP loaded from environment variables" )
7182
72- # deletes old dev wheels from pypi. evil hack.
73- actually_delete = not args .dry
74- pypi_username = args .username or "user"
75- max_dev_releases = args .max_nightlies
76- host = 'https://{}/' .format (args .index_hostname )
77- pypi_password = password or "password"
78- pypi_otp = otp or "otp"
79-
8083patterns = [re .compile (r".*\.dev\d+$" )]
8184###### NOTE: This code is taken from the pypi-cleanup package (https://github.com/arcivanov/pypi-cleanup/tree/master)
8285class CsfrParser (HTMLParser ):
@@ -185,8 +188,8 @@ def run(self):
185188 sorted_versions = sorted (versions , key = lambda x : int (x .split ('dev' )[- 1 ]))
186189 pkg_vers .extend (sorted_versions [:- self .max_dev_releases ])
187190
191+ print ("Following pkg_vers can be deleted: " , pkg_vers )
188192 if not self .do_it :
189- print ("Following pkg_vers can be deleted: " , pkg_vers )
190193 return
191194
192195 if not pkg_vers :
@@ -231,9 +234,9 @@ def run(self):
231234
232235 two_factor = False
233236 with s .post (
234- f"{ self .url } /account/login/" ,
235- data = {"csrf_token" : csrf , "username" : self .username , "password" : self .password },
236- headers = {"referer" : f"{ self .url } /account/login/" },
237+ f"{ self .url } /account/login/" ,
238+ data = {"csrf_token" : csrf , "username" : self .username , "password" : self .password },
239+ headers = {"referer" : f"{ self .url } /account/login/" },
237240 ) as r :
238241 r .raise_for_status ()
239242 if r .url == f"{ self .url } /account/login/" :
@@ -255,9 +258,9 @@ def run(self):
255258 for i in range (3 ):
256259 auth_code = pyotp .TOTP (self .otp ).now ()
257260 with s .post (
258- two_factor_url ,
259- data = {"csrf_token" : csrf , "method" : "totp" , "totp_value" : auth_code },
260- headers = {"referer" : two_factor_url },
261+ two_factor_url ,
262+ data = {"csrf_token" : csrf , "method" : "totp" , "totp_value" : auth_code },
263+ headers = {"referer" : two_factor_url },
261264 ) as r :
262265 r .raise_for_status ()
263266 if r .url == two_factor_url :
@@ -286,12 +289,12 @@ def run(self):
286289 referer = r .url
287290
288291 with s .post (
289- form_url ,
290- data = {
291- "csrf_token" : csrf ,
292- "confirm_delete_version" : pkg_ver ,
293- },
294- headers = {"referer" : referer },
292+ form_url ,
293+ data = {
294+ "csrf_token" : csrf ,
295+ "confirm_delete_version" : pkg_ver ,
296+ },
297+ headers = {"referer" : referer },
295298 ) as r :
296299 r .raise_for_status ()
297300
@@ -300,4 +303,4 @@ def run(self):
300303 logging .info (f"Would be deleting { self .package } version { pkg_ver } , but not doing it!" )
301304
302305
303- PypiCleanup (host , pypi_username , 'duckdb' , pypi_password , pypi_otp , patterns , actually_delete , max_dev_releases ).run ()
306+ PypiCleanup (pypi_url , pypi_username , 'duckdb' , pypi_password , pypi_otp , patterns , not dry_run , max_dev_releases ).run ()
0 commit comments