44To use (RELEASE is something like 3.3.5rc2):
55
66* Copy this script to dl-files (it needs access to all the release files).
7- You could also download all files, then you need to adapt the "ftp_root "
8- string below .
7+ You could also download all files, then you need to use the "--ftp-root "
8+ argument .
99
10- * Make sure all download files are in place in the correct /srv/www.python.org
11- subdirectory.
10+ * Make sure all download files are in place in the correct FTP subdirectory.
1211
1312* Create a new Release object via the Django admin (adding via API is
1413 currently broken), the name MUST be "Python RELEASE".
2322Georg Brandl, March 2014.
2423"""
2524
25+ import argparse
2626import hashlib
2727import json
2828import os
@@ -70,8 +70,6 @@ def run_cmd(
7070 )
7171 sys .exit ()
7272
73- base_url = "https://www.python.org/api/v1/"
74- ftp_root = "/srv/www.python.org/ftp/python/"
7573download_root = "https://www.python.org/ftp/python/"
7674
7775tag_cre = re .compile (r"(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:([ab]|rc)(\d+))?$" )
@@ -98,44 +96,47 @@ def get_file_descriptions(
9896) -> list [tuple [re .Pattern [str ], tuple [str , int , bool , str ]]]:
9997 v = minor_version_tuple (release )
10098 rx = re .compile
101- # value is (file "name", OS id , download button, file "description").
102- # OS=0 means no ReleaseFile object. Only one matching *file* (not regex)
99+ # value is (file "name", OS slug , download button, file "description").
100+ # OS=None means no ReleaseFile object. Only one matching *file* (not regex)
103101 # per OS can have download=True.
104102 return [
105- (rx (r"\.tgz$" ), ("Gzipped source tarball" , 3 , False , "" )),
106- (rx (r"\.tar\.xz$" ), ("XZ compressed source tarball" , 3 , True , "" )),
103+ (rx (r"\.tgz$" ), ("Gzipped source tarball" , "source" , False , "" )),
104+ (rx (r"\.tar\.xz$" ), ("XZ compressed source tarball" , "source" , True , "" )),
107105 (
108106 rx (r"windows-.+\.json" ),
109107 (
110108 "Windows release manifest" ,
111- 1 ,
109+ "windows" ,
112110 False ,
113111 f"Install with 'py install { v [0 ]} .{ v [1 ]} '" ,
114112 ),
115113 ),
116114 (
117115 rx (r"-embed-amd64\.zip$" ),
118- ("Windows embeddable package (64-bit)" , 1 , False , "" ),
116+ ("Windows embeddable package (64-bit)" , "windows" , False , "" ),
119117 ),
120118 (
121119 rx (r"-embed-arm64\.zip$" ),
122- ("Windows embeddable package (ARM64)" , 1 , False , "" ),
120+ ("Windows embeddable package (ARM64)" , "windows" , False , "" ),
121+ ),
122+ (
123+ rx (r"-arm64\.exe$" ),
124+ ("Windows installer (ARM64)" , "windows" , False , "Experimental" ),
123125 ),
124- (rx (r"-arm64\.exe$" ), ("Windows installer (ARM64)" , 1 , False , "Experimental" )),
125126 (
126127 rx (r"-amd64\.exe$" ),
127- ("Windows installer (64-bit)" , 1 , v >= (3 , 9 ), "Recommended" ),
128+ ("Windows installer (64-bit)" , "windows" , v >= (3 , 9 ), "Recommended" ),
128129 ),
129130 (
130131 rx (r"-embed-win32\.zip$" ),
131- ("Windows embeddable package (32-bit)" , 1 , False , "" ),
132+ ("Windows embeddable package (32-bit)" , "windows" , False , "" ),
132133 ),
133- (rx (r"\.exe$" ), ("Windows installer (32-bit)" , 1 , v < (3 , 9 ), "" )),
134+ (rx (r"\.exe$" ), ("Windows installer (32-bit)" , "windows" , v < (3 , 9 ), "" )),
134135 (
135136 rx (r"-macosx10\.5(_rev\d)?\.(dm|pk)g$" ),
136137 (
137138 "macOS 32-bit i386/PPC installer" ,
138- 2 ,
139+ "macos" ,
139140 False ,
140141 "for Mac OS X 10.5 and later" ,
141142 ),
@@ -144,7 +145,7 @@ def get_file_descriptions(
144145 rx (r"-macosx10\.6(_rev\d)?\.(dm|pk)g$" ),
145146 (
146147 "macOS 64-bit/32-bit Intel installer" ,
147- 2 ,
148+ "macos" ,
148149 False ,
149150 "for Mac OS X 10.6 and later" ,
150151 ),
@@ -153,7 +154,7 @@ def get_file_descriptions(
153154 rx (r"-macos(x)?10\.9\.(dm|pk)g$" ),
154155 (
155156 "macOS 64-bit Intel-only installer" ,
156- 2 ,
157+ "macos" ,
157158 False ,
158159 "for macOS 10.9 and later, deprecated" ,
159160 ),
@@ -162,11 +163,19 @@ def get_file_descriptions(
162163 rx (r"-macos(x)?1[1-9](\.[0-9]*)?\.pkg$" ),
163164 (
164165 "macOS 64-bit universal2 installer" ,
165- 2 ,
166+ "macos" ,
166167 True ,
167168 f"for macOS { '10.13' if v >= (3 , 12 , 6 ) else '10.9' } and later" ,
168169 ),
169170 ),
171+ (
172+ rx (r"aarch64-linux-android.tar.gz$" ),
173+ ("Android embeddable package (aarch64)" , "android" , False , "" ),
174+ ),
175+ (
176+ rx (r"x86_64-linux-android.tar.gz$" ),
177+ ("Android embeddable package (x86_64)" , "android" , False , "" ),
178+ ),
170179 ]
171180
172181
@@ -182,14 +191,14 @@ def sigfile_for(release: str, rfile: str) -> str:
182191 return download_root + f"{ release } /{ rfile } .asc"
183192
184193
185- def md5sum_for (release : str , rfile : str ) -> str :
194+ def md5sum_for (filename : str ) -> str :
186195 return hashlib .md5 (
187- open (ftp_root + base_version ( release ) + "/" + rfile , "rb" ).read ()
196+ open (filename , "rb" ).read (),
188197 ).hexdigest ()
189198
190199
191- def filesize_for (release : str , rfile : str ) -> int :
192- return path .getsize (ftp_root + base_version ( release ) + "/" + rfile )
200+ def filesize_for (filename : str ) -> int :
201+ return path .getsize (filename )
193202
194203
195204def make_slug (text : str ) -> str :
@@ -215,6 +224,7 @@ def minor_version_tuple(release: str) -> tuple[int, int]:
215224
216225
217226def build_file_dict (
227+ ftp_root : str ,
218228 release : str ,
219229 rfile : str ,
220230 rel_pk : int ,
@@ -224,6 +234,7 @@ def build_file_dict(
224234 add_desc : str ,
225235) -> dict [str , Any ]:
226236 """Return a dictionary with all needed fields for a ReleaseFile object."""
237+ filename = path .join (ftp_root , base_version (release ), rfile )
227238 d = {
228239 "name" : file_desc ,
229240 "slug" : slug_for (release ) + "-" + make_slug (file_desc )[:40 ],
@@ -232,36 +243,38 @@ def build_file_dict(
232243 "description" : add_desc ,
233244 "is_source" : os_pk == 3 ,
234245 "url" : download_root + f"{ base_version (release )} /{ rfile } " ,
235- "md5_sum" : md5sum_for (release , rfile ),
236- "filesize" : filesize_for (release , rfile ),
246+ "md5_sum" : md5sum_for (filename ),
247+ "filesize" : filesize_for (filename ),
237248 "download_button" : add_download ,
238249 }
239250 # Upload GPG signature
240- if os .path .exists (ftp_root + f" { base_version ( release ) } / { rfile } .asc" ):
251+ if os .path .exists (filename + " .asc" ):
241252 d ["gpg_signature_file" ] = sigfile_for (base_version (release ), rfile )
242253 # Upload Sigstore signature
243- if os .path .exists (ftp_root + f" { base_version ( release ) } / { rfile } .sig" ):
254+ if os .path .exists (filename + " .sig" ):
244255 d ["sigstore_signature_file" ] = (
245256 download_root + f"{ base_version (release )} /{ rfile } .sig"
246257 )
247258 # Upload Sigstore certificate
248- if os .path .exists (ftp_root + f" { base_version ( release ) } / { rfile } .crt" ):
259+ if os .path .exists (filename + " .crt" ):
249260 d ["sigstore_cert_file" ] = download_root + f"{ base_version (release )} /{ rfile } .crt"
250261 # Upload Sigstore bundle
251- if os .path .exists (ftp_root + f" { base_version ( release ) } / { rfile } .sigstore" ):
262+ if os .path .exists (filename + " .sigstore" ):
252263 d ["sigstore_bundle_file" ] = (
253264 download_root + f"{ base_version (release )} /{ rfile } .sigstore"
254265 )
255266 # Upload SPDX SBOM file
256- if os .path .exists (ftp_root + f" { base_version ( release ) } / { rfile } .spdx.json" ):
267+ if os .path .exists (filename + " .spdx.json" ):
257268 d ["sbom_spdx2_file" ] = (
258269 download_root + f"{ base_version (release )} /{ rfile } .spdx.json"
259270 )
260271
261272 return d
262273
263274
264- def list_files (release : str ) -> Generator [tuple [str , str , int , bool , str ], None , None ]:
275+ def list_files (
276+ ftp_root : str , release : str
277+ ) -> Generator [tuple [str , str , int , bool , str ], None , None ]:
265278 """List all of the release's download files."""
266279 reldir = base_version (release )
267280 for rfile in os .listdir (path .join (ftp_root , reldir )):
@@ -283,15 +296,14 @@ def list_files(release: str) -> Generator[tuple[str, str, int, bool, str], None,
283296
284297 for rx , info in get_file_descriptions (release ):
285298 if rx .search (rfile ):
286- file_desc , os_pk , add_download , add_desc = info
287- yield rfile , file_desc , os_pk , add_download , add_desc
299+ yield (rfile , * info )
288300 break
289301 else :
290302 print (f" File { reldir } /{ rfile } not recognized" )
291303 continue
292304
293305
294- def query_object (objtype : str , ** params : Any ) -> int :
306+ def query_object (base_url : str , objtype : str , ** params : Any ) -> int :
295307 """Find an API object by query parameters."""
296308 uri = base_url + f"downloads/{ objtype } /"
297309 uri += "?" + "&" .join (f"{ k } ={ v } " for k , v in params .items ())
@@ -302,7 +314,7 @@ def query_object(objtype: str, **params: Any) -> int:
302314 return int (obj ["resource_uri" ].strip ("/" ).split ("/" )[- 1 ])
303315
304316
305- def post_object (objtype : str , datadict : dict [str , Any ]) -> int :
317+ def post_object (base_url : str , objtype : str , datadict : dict [str , Any ]) -> int :
306318 """Create a new API object."""
307319 resp = requests .post (
308320 base_url + "downloads/" + objtype + "/" ,
@@ -324,11 +336,11 @@ def post_object(objtype: str, datadict: dict[str, Any]) -> int:
324336
325337
326338def sign_release_files_with_sigstore (
327- release : str , release_files : list [tuple [str , str , int , bool , str ]]
339+ ftp_root : str , release : str , release_files : list [tuple [str , str , int , bool , str ]]
328340) -> None :
329341 filenames = [
330342 ftp_root + f"{ base_version (release )} /{ rfile } "
331- for rfile , file_desc , os_pk , add_download , add_desc in release_files
343+ for rfile , * _ in release_files
332344 ]
333345
334346 def has_sigstore_signature (filename : str ) -> bool :
@@ -445,34 +457,65 @@ def has_sigstore_signature(filename: str) -> bool:
445457 )
446458
447459
460+ def parse_args () -> argparse .Namespace :
461+ def ensure_trailing_slash (s : str ):
462+ if not s .endswith ("/" ):
463+ s += "/"
464+ return s
465+
466+ parser = argparse .ArgumentParser ()
467+ parser .add_argument (
468+ "--base-url" ,
469+ metavar = "URL" ,
470+ type = ensure_trailing_slash ,
471+ default = "https://www.python.org/api/v1/" ,
472+ help = "API URL; defaults to %(default)s" ,
473+ )
474+ parser .add_argument (
475+ "--ftp-root" ,
476+ metavar = "DIR" ,
477+ type = ensure_trailing_slash ,
478+ default = "/srv/www.python.org/ftp/python" ,
479+ help = "FTP root; defaults to %(default)s" ,
480+ )
481+ parser .add_argument (
482+ "release" ,
483+ help = "Python version number, e.g. 3.3.5rc2" ,
484+ )
485+ return parser .parse_args ()
486+
487+
448488def main () -> None :
449- rel = sys .argv [1 ]
489+ args = parse_args ()
490+ rel = args .release
450491 print ("Querying python.org for release" , rel )
451- rel_pk = query_object ("release" , name = "Python+" + rel )
492+ rel_pk = query_object (args . base_url , "release" , name = "Python+" + rel )
452493 print ("Found Release object: id =" , rel_pk )
453- release_files = list (list_files (rel ))
454- sign_release_files_with_sigstore (rel , release_files )
494+
495+ release_files = list (list_files (args .ftp_root , rel ))
496+ sign_release_files_with_sigstore (args .ftp_root , rel , release_files )
455497 n = 0
456498 file_dicts = {}
457- for rfile , file_desc , os_pk , add_download , add_desc in release_files :
499+ for rfile , file_desc , os_slug , add_download , add_desc in release_files :
500+ if not os_slug :
501+ continue
502+ os_pk = query_object (args .base_url , "os" , slug = os_slug )
458503 file_dict = build_file_dict (
459- rel , rfile , rel_pk , file_desc , os_pk , add_download , add_desc
504+ args . ftp_root , rel , rfile , rel_pk , file_desc , os_pk , add_download , add_desc
460505 )
461506 key = file_dict ["slug" ]
462- if not os_pk :
463- continue
464507 print ("Creating ReleaseFile object for" , rfile , key )
465508 if key in file_dicts :
466509 raise RuntimeError (f"duplicate slug generated: { key } " )
467510 file_dicts [key ] = file_dict
468511 print ("Deleting previous release files" )
469512 resp = requests .delete (
470- base_url + f"downloads/release_file/?release={ rel_pk } " , headers = headers
513+ args . base_url + f"downloads/release_file/?release={ rel_pk } " , headers = headers
471514 )
472515 if resp .status_code != 204 :
473516 raise RuntimeError (f"deleting previous releases failed: { resp .status_code } " )
474517 for file_dict in file_dicts .values ():
475- file_pk = post_object ("release_file" , file_dict )
518+ file_pk = post_object (args . base_url , "release_file" , file_dict )
476519 if file_pk >= 0 :
477520 print ("Created as id =" , file_pk )
478521 n += 1
0 commit comments