@@ -266,51 +266,60 @@ def command_hint(prod: str, subcmd: str, menu_label: str) -> str:
266266 return f"run `python3 { INSTALLER_NAME } { prod } { subcmd } `"
267267
268268
269- def read_install_mode (data_folder : pathlib .Path , prod : str , compose_file_name : str ) -> typing .Optional [str ]:
270- """Return 'docker', 'pip', or None for the ``prod`` install in data_folder.
271-
272- Reads the install marker file. Falls back to detecting a legacy Docker install
273- (the product's compose file + credentials) from before the marker was introduced.
269+ class InstallMarker :
270+ """Read/write the TestGen install marker file. Falls back to detecting
271+ a legacy Docker install (compose file + credentials) from before the
272+ marker was introduced.
274273 """
275- marker_path = data_folder / INSTALL_MARKER_FILE .format (prod )
276- if marker_path .exists ():
277- try :
278- data = json .loads (marker_path .read_text ())
279- except Exception :
280- LOG .exception ("Failed to read install marker at %s" , marker_path )
281- else :
282- install_mode = data .get ("install_mode" )
283- if install_mode in (INSTALL_MODE_DOCKER , INSTALL_MODE_PIP ):
284- return install_mode
285- LOG .warning ("Install marker has unexpected install_mode: %r" , install_mode )
286274
287- if (data_folder / compose_file_name ).exists () and (data_folder / CREDENTIALS_FILE .format (prod )).exists ():
288- LOG .info ("No marker present; detected legacy Docker install in %s" , data_folder )
289- return INSTALL_MODE_DOCKER
275+ def __init__ (self , data_folder : pathlib .Path , prod : str , compose_file_name : typing .Optional [str ] = None ):
276+ self ._data_folder = data_folder
277+ self ._prod = prod
278+ self ._compose_file_name = compose_file_name
279+ self .path = data_folder / INSTALL_MARKER_FILE .format (prod )
290280
291- return None
281+ def read (self ) -> typing .Optional [str ]:
282+ if self .path .exists ():
283+ try :
284+ data = json .loads (self .path .read_text ())
285+ except Exception :
286+ LOG .exception ("Failed to read install marker at %s" , self .path )
287+ else :
288+ install_mode = data .get ("install_mode" )
289+ if install_mode in (INSTALL_MODE_DOCKER , INSTALL_MODE_PIP ):
290+ return install_mode
291+ LOG .warning ("Install marker has unexpected install_mode: %r" , install_mode )
292+ if (
293+ self ._compose_file_name
294+ and (self ._data_folder / self ._compose_file_name ).exists ()
295+ and (self ._data_folder / CREDENTIALS_FILE .format (self ._prod )).exists ()
296+ ):
297+ LOG .info ("No marker present; detected legacy Docker install in %s" , self ._data_folder )
298+ return INSTALL_MODE_DOCKER
299+ return None
292300
301+ def write (self , mode : str , ** extra ) -> None :
302+ if mode not in (INSTALL_MODE_DOCKER , INSTALL_MODE_PIP ):
303+ raise ValueError (f"Unknown install_mode: { mode } " )
304+ now = datetime .datetime .now (datetime .timezone .utc ).isoformat ()
305+ created_on = now
306+ if self .path .exists ():
307+ try :
308+ existing = json .loads (self .path .read_text ())
309+ if isinstance (existing .get ("created_on" ), str ):
310+ created_on = existing ["created_on" ]
311+ except Exception :
312+ LOG .exception ("Failed to read existing install marker at %s" , self .path )
313+ self .path .write_text (
314+ json .dumps (
315+ {"install_mode" : mode , "created_on" : created_on , "last_updated_on" : now , ** extra },
316+ indent = 2 ,
317+ )
318+ )
293319
294- def write_install_marker (data_folder : pathlib .Path , prod : str , install_mode : str , ** extra ) -> None :
295- if install_mode not in (INSTALL_MODE_DOCKER , INSTALL_MODE_PIP ):
296- raise ValueError (f"Unknown install_mode: { install_mode } " )
297- marker_path = data_folder / INSTALL_MARKER_FILE .format (prod )
298- now = datetime .datetime .now (datetime .timezone .utc ).isoformat ()
299- created_on = now
300- if marker_path .exists ():
301- try :
302- existing = json .loads (marker_path .read_text ())
303- if isinstance (existing .get ("created_on" ), str ):
304- created_on = existing ["created_on" ]
305- except Exception :
306- LOG .exception ("Failed to read existing install marker at %s" , marker_path )
307- data = {
308- "install_mode" : install_mode ,
309- "created_on" : created_on ,
310- "last_updated_on" : now ,
311- ** extra ,
312- }
313- marker_path .write_text (json .dumps (data , indent = 2 ))
320+ def unlink (self ) -> None :
321+ if self .path .exists ():
322+ self .path .unlink ()
314323
315324
316325@contextlib .contextmanager
@@ -2737,7 +2746,7 @@ def get_requirements(self, args):
27372746 return []
27382747
27392748 def _resolve_install_mode (self , args ):
2740- existing = read_install_mode (self .data_folder , args .prod , args .compose_file_name )
2749+ existing = InstallMarker (self .data_folder , args .prod , args .compose_file_name ). read ( )
27412750 if existing :
27422751 CONSOLE .msg (f"Found an existing TestGen { existing } installation in { self .data_folder } ." )
27432752 CONSOLE .space ()
@@ -2769,14 +2778,14 @@ def _auto_select_mode(self, args):
27692778 CONSOLE .msg ("[d] Docker Compose (Recommended)" )
27702779 CONSOLE .msg (" The most stable TestGen experience for persistent use." )
27712780 CONSOLE .msg (" Provides a fully managed environment with an isolated PostgreSQL container." )
2772- prereq_status = " " .join (
2773- f"{ '(✓)' if ok else '(X)' } { req .label or req .key } " for req , ok in prereq_results
2774- )
2781+ prereq_status = " " .join (f"{ '(✓)' if ok else '(X)' } { req .label or req .key } " for req , ok in prereq_results )
27752782 CONSOLE .msg (f" Prerequisites: { prereq_status } " )
27762783 CONSOLE .space ()
27772784 CONSOLE .msg ("[p] Pip + embedded PostgreSQL" )
27782785 CONSOLE .msg (" A lightweight Python installation suited for evaluation." )
2779- CONSOLE .msg (" Sets up an isolated Python environment and manages the PostgreSQL database on the file system." )
2786+ CONSOLE .msg (
2787+ " Sets up an isolated Python environment and manages the PostgreSQL database on the file system."
2788+ )
27802789 CONSOLE .space ()
27812790
27822791 if docker_ready :
@@ -2805,7 +2814,7 @@ def _auto_select_mode(self, args):
28052814 def execute (self , args ):
28062815 self .intro_text = self .pip_intro if self ._resolved_mode == INSTALL_MODE_PIP else self .docker_intro
28072816 super ().execute (args )
2808- write_install_marker (self .data_folder , args .prod , self ._resolved_mode )
2817+ InstallMarker (self .data_folder , args .prod ). write ( self ._resolved_mode )
28092818 # Pip mode: keep the app running so the user has a one-command install
28102819 # experience. Docker mode already runs as detached containers via
28112820 # ``docker compose up --wait``, so no need to start anything here.
@@ -2889,7 +2898,7 @@ def get_requirements(self, args):
28892898 ]
28902899
28912900 def _resolve_install_mode (self , args ):
2892- mode = read_install_mode (self .data_folder , args .prod , args .compose_file_name )
2901+ mode = InstallMarker (self .data_folder , args .prod , args .compose_file_name ). read ( )
28932902 if mode is None :
28942903 CONSOLE .msg (f"No TestGen installation found in { self .data_folder } ." )
28952904 CONSOLE .msg (f"To install TestGen, { command_hint (args .prod , 'install' , 'Install TestGen' )} ." )
@@ -2902,7 +2911,7 @@ def _resolve_install_mode(self, args):
29022911
29032912 def execute (self , args ):
29042913 super ().execute (args )
2905- write_install_marker (self .data_folder , args .prod , self ._resolved_mode )
2914+ InstallMarker (self .data_folder , args .prod ). write ( self ._resolved_mode )
29062915
29072916
29082917class TestgenStartAction (Action , ComposeActionMixin ):
@@ -2928,7 +2937,7 @@ def get_requirements(self, args):
29282937 return []
29292938
29302939 def _resolve_install_mode (self , args ):
2931- mode = read_install_mode (self .data_folder , args .prod , args .compose_file_name )
2940+ mode = InstallMarker (self .data_folder , args .prod , args .compose_file_name ). read ( )
29322941 if mode is None :
29332942 CONSOLE .msg (f"No TestGen installation found in { self .data_folder } ." )
29342943 CONSOLE .msg (f"To install TestGen, { command_hint (args .prod , 'install' , 'Install TestGen' )} ." )
@@ -2992,7 +3001,7 @@ def get_requirements(self, args):
29923001 def _resolve_install_mode (self , args ):
29933002 # Unlike install/upgrade, "no install found" is not an abort here —
29943003 # ``tg delete`` is idempotent. execute() handles the None case.
2995- mode = read_install_mode (self .data_folder , args .prod , args .compose_file_name )
3004+ mode = InstallMarker (self .data_folder , args .prod , args .compose_file_name ). read ( )
29963005 self ._resolved_mode = mode
29973006 if mode is not None :
29983007 self .analytics .additional_properties ["install_mode" ] = mode
@@ -3008,7 +3017,7 @@ def execute(self, args):
30083017 self ._delete_docker (args )
30093018 else :
30103019 self ._delete_pip (args )
3011- remove_path (self .data_folder / INSTALL_MARKER_FILE . format ( args .prod ) )
3020+ InstallMarker (self .data_folder , args .prod , args . compose_file_name ). unlink ( )
30123021
30133022 def _delete_docker (self , args ):
30143023 if self .get_compose_file_path (args ).exists ():
@@ -3087,7 +3096,7 @@ def get_requirements(self, args):
30873096 return []
30883097
30893098 def _resolve_install_mode (self , args ):
3090- mode = read_install_mode (self .data_folder , args .prod , args .compose_file_name )
3099+ mode = InstallMarker (self .data_folder , args .prod , args .compose_file_name ). read ( )
30913100 if mode is None :
30923101 CONSOLE .msg (f"No TestGen installation found in { self .data_folder } ." )
30933102 CONSOLE .msg (f"To install TestGen, { command_hint (args .prod , 'install' , 'Install TestGen' )} ." )
@@ -3159,7 +3168,7 @@ def get_requirements(self, args):
31593168
31603169 def _resolve_install_mode (self , args ):
31613170 # Like delete: idempotent, so "no install" returns rather than aborts.
3162- mode = read_install_mode (self .data_folder , args .prod , args .compose_file_name )
3171+ mode = InstallMarker (self .data_folder , args .prod , args .compose_file_name ). read ( )
31633172 self ._resolved_mode = mode
31643173 if mode is not None :
31653174 self .analytics .additional_properties ["install_mode" ] = mode
0 commit comments