55import ctypes
66import errno
77import hashlib
8+ import http .client
89import json
910import os
1011import re
2425from datetime import datetime , timezone
2526from importlib .resources import files
2627from pathlib import Path
27- from typing import Any
2828
2929from openshell_middleware_init import __version__
3030
3636_PYTHON_PACKAGE_PATTERN = re .compile (r"^[a-z][a-z0-9_]*$" )
3737_PROJECT_NAME_PATTERN = re .compile (r"^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$" )
3838_NETWORK_ATTEMPTS = 4
39+ _RUST_KEYWORDS = {
40+ "abstract" ,
41+ "as" ,
42+ "async" ,
43+ "await" ,
44+ "become" ,
45+ "box" ,
46+ "break" ,
47+ "const" ,
48+ "continue" ,
49+ "crate" ,
50+ "do" ,
51+ "dyn" ,
52+ "else" ,
53+ "enum" ,
54+ "extern" ,
55+ "false" ,
56+ "final" ,
57+ "fn" ,
58+ "for" ,
59+ "gen" ,
60+ "if" ,
61+ "impl" ,
62+ "in" ,
63+ "let" ,
64+ "loop" ,
65+ "macro" ,
66+ "match" ,
67+ "mod" ,
68+ "move" ,
69+ "mut" ,
70+ "override" ,
71+ "priv" ,
72+ "pub" ,
73+ "ref" ,
74+ "return" ,
75+ "self" ,
76+ "Self" ,
77+ "static" ,
78+ "struct" ,
79+ "super" ,
80+ "trait" ,
81+ "true" ,
82+ "try" ,
83+ "type" ,
84+ "typeof" ,
85+ "union" ,
86+ "unsized" ,
87+ "unsafe" ,
88+ "use" ,
89+ "virtual" ,
90+ "where" ,
91+ "while" ,
92+ "yield" ,
93+ }
3994
4095
4196class InitializationError (RuntimeError ):
@@ -74,6 +129,7 @@ class TemplateContext:
74129 distribution_name : str
75130 package_name : str
76131 rust_crate_name : str
132+ rust_lib_name : str
77133 service_name : str
78134
79135 @property
@@ -83,6 +139,7 @@ def replacements(self) -> Mapping[str, str]:
83139 "__DISTRIBUTION_NAME__" : self .distribution_name ,
84140 "__PACKAGE_NAME__" : self .package_name ,
85141 "__RUST_CRATE_NAME__" : self .rust_crate_name ,
142+ "__RUST_LIB_NAME__" : self .rust_lib_name ,
86143 "__SERVICE_NAME__" : self .service_name ,
87144 }
88145
@@ -125,6 +182,7 @@ def initialize_project(
125182 dir = destination .parent ,
126183 )
127184 )
185+ staging_path .chmod (0o755 )
128186 _write_reservation_metadata (reservation , staging_path )
129187 proto , proto_url = downloader (version )
130188 _validate_proto (proto , version )
@@ -185,7 +243,8 @@ def _template_context(name: str, language: str, package_name: str | None) -> Tem
185243 raise InitializationError ("--package-name is only valid with --language python" )
186244
187245 distribution_name = re .sub (r"[._]+" , "-" , normalized_name )
188- derived_package = re .sub (r"[^a-z0-9]+" , "_" , normalized_name ).strip ("_" )
246+ identifier = re .sub (r"[^a-z0-9]+" , "_" , normalized_name ).strip ("_" )
247+ derived_package = identifier
189248 if not derived_package or not derived_package [0 ].isalpha ():
190249 derived_package = f"middleware_{ derived_package } " .rstrip ("_" )
191250 effective_package = package_name if package_name is not None else derived_package
@@ -195,11 +254,17 @@ def _template_context(name: str, language: str, package_name: str | None) -> Tem
195254 "lowercase letters, digits, and underscores"
196255 )
197256 service_name = normalized_name .replace ("_" , "-" ).replace ("." , "-" )
257+ rust_lib_name = identifier
258+ rust_crate_name = distribution_name
259+ if not rust_lib_name [0 ].isalpha () or rust_lib_name in _RUST_KEYWORDS :
260+ rust_lib_name = f"middleware_{ rust_lib_name } "
261+ rust_crate_name = f"middleware-{ distribution_name } "
198262 return TemplateContext (
199263 project_name = normalized_name ,
200264 distribution_name = distribution_name ,
201265 package_name = effective_package ,
202- rust_crate_name = distribution_name ,
266+ rust_crate_name = rust_crate_name ,
267+ rust_lib_name = rust_lib_name ,
203268 service_name = service_name ,
204269 )
205270
@@ -223,9 +288,8 @@ def _resolve_latest_version() -> str:
223288 headers = {"User-Agent" : f"openshell-middleware-init/{ __version__ } " },
224289 )
225290 try :
226- with _urlopen_with_retries (request ) as response :
227- resolved_url = response .geturl ()
228- except (OSError , urllib .error .URLError ) as error :
291+ _ , resolved_url = _fetch_url (request )
292+ except (OSError , urllib .error .URLError , http .client .IncompleteRead ) as error :
229293 raise InitializationError ("could not resolve OpenShell's latest release" ) from error
230294 prefix = f"{ _REPOSITORY_URL } /releases/tag/"
231295 if not resolved_url .startswith (prefix ):
@@ -243,30 +307,47 @@ def _download_proto(version: str) -> tuple[bytes, str]:
243307 headers = {"User-Agent" : f"openshell-middleware-init/{ __version__ } " },
244308 )
245309 try :
246- with _urlopen_with_retries (request ) as response :
247- return response .read (), url
248- except (OSError , urllib .error .URLError ) as error :
310+ body , _ = _fetch_url (request )
311+ return body , url
312+ except urllib .error .HTTPError as error :
313+ if error .code == 404 :
314+ raise InitializationError (
315+ f"{ version } does not expose { _PROTO_PATH } ; choose a middleware-capable release"
316+ ) from error
317+ raise InitializationError (
318+ f"could not download { _PROTO_PATH } for { version } : HTTP { error .code } "
319+ ) from error
320+ except (OSError , urllib .error .URLError , http .client .IncompleteRead ) as error :
249321 raise InitializationError (
250- f"{ version } does not expose { _PROTO_PATH } ; choose a middleware-capable release "
322+ f"could not download { _PROTO_PATH } for { version } : { _network_error_reason ( error ) } "
251323 ) from error
252324
253325
254- def _urlopen_with_retries (request : urllib .request .Request ) -> Any :
255- """Open a URL with the same initial attempt plus three retries as the spike."""
326+ def _fetch_url (request : urllib .request .Request ) -> tuple [ bytes , str ] :
327+ """Fetch a complete response with the same transfer retries as the spike."""
256328 for attempt in range (_NETWORK_ATTEMPTS ):
257329 try :
258- return urllib .request .urlopen (request , timeout = 30 )
330+ with urllib .request .urlopen (request , timeout = 30 ) as response :
331+ return response .read (), response .geturl ()
259332 except urllib .error .HTTPError as error :
260333 retryable = error .code in {408 , 429 } or 500 <= error .code < 600
261334 if not retryable or attempt == _NETWORK_ATTEMPTS - 1 :
262335 raise
263- except (OSError , urllib .error .URLError ):
336+ except (OSError , urllib .error .URLError , http . client . IncompleteRead ):
264337 if attempt == _NETWORK_ATTEMPTS - 1 :
265338 raise
266339 time .sleep (0.25 * (2 ** attempt ))
267340 raise AssertionError ("network retry loop exhausted without returning or raising" )
268341
269342
343+ def _network_error_reason (
344+ error : OSError | urllib .error .URLError | http .client .IncompleteRead ,
345+ ) -> str :
346+ if isinstance (error , urllib .error .URLError ):
347+ return str (error .reason )
348+ return str (error )
349+
350+
270351def _validate_proto (proto : bytes , version : str ) -> None :
271352 required_fragments = (
272353 b"package openshell.middleware.v1;" ,
0 commit comments