22Commandline interface for flow360.
33"""
44
5- import os . path
5+ import os
66from datetime import datetime
7- from os .path import expanduser
87
98import click
109import toml
1110from packaging .version import InvalidVersion , Version
1211
13- from flow360 .cli import dict_utils
12+ from flow360 .cli .auth import LoginError , resolve_target_environment , wait_for_login
13+ from flow360 .cli .auth_guidance import build_configure_command
1414from flow360 .environment import Env
15+ from flow360 .user_config import (
16+ config_file ,
17+ delete_apikey ,
18+ read_user_config ,
19+ store_apikey ,
20+ write_user_config ,
21+ )
1522from flow360 .version import __solver_version__ , __version__
1623
17- home = expanduser ("~" )
18- # pylint: disable=invalid-name
19- config_file = f"{ home } /.flow360/config.toml"
20-
2124if os .path .exists (config_file ):
2225 with open (config_file , encoding = "utf-8" ) as current_fh :
2326 current_config = toml .loads (current_fh .read ())
@@ -37,7 +40,9 @@ def flow360():
3740@click .option (
3841 "--apikey" , prompt = False if "APIKEY_PRESENT" in globals () else "API Key" , help = "API Key"
3942)
40- @click .option ("--profile" , prompt = False , default = "default" , help = "Profile, e.g., default, dev." )
43+ @click .option (
44+ "--profile" , prompt = False , default = "default" , help = "Profile, e.g., default, secondary."
45+ )
4146@click .option (
4247 "--dev" , prompt = False , type = bool , is_flag = True , help = "Only use this apikey in DEV environment."
4348)
@@ -61,46 +66,24 @@ def configure(apikey, profile, dev, uat, env, suppress_submit_warning, beta_feat
6166 Configure flow360.
6267 """
6368 changed = False
64- if not os .path .exists (f"{ home } /.flow360" ):
65- os .makedirs (f"{ home } /.flow360" )
66-
67- config = {}
68- if os .path .exists (config_file ):
69- with open (config_file , encoding = "utf-8" ) as file_handler :
70- config = toml .loads (file_handler .read ())
69+ config = read_user_config ()
70+ _ , storage_environment = resolve_target_environment (dev = dev , uat = uat , env = env )
7171
7272 if apikey is not None :
73- if dev is True :
74- entry = {profile : {"dev" : {"apikey" : apikey }}}
75- elif uat is True :
76- entry = {profile : {"uat" : {"apikey" : apikey }}}
77- elif env :
78- if env == "dev" :
79- raise ValueError ("Cannot set dev environment with --env, please use --dev instead." )
80- if env == "uat" :
81- raise ValueError ("Cannot set uat environment with --env, please use --uat instead." )
82- if env == "prod" :
83- raise ValueError (
84- "Cannot set prod environment with --env, please remove --env and its argument."
85- )
86- entry = {profile : {env : {"apikey" : apikey }}}
87- else :
88- entry = {profile : {"apikey" : apikey }}
89- dict_utils .merge_overwrite (config , entry )
73+ config = store_apikey (apikey , profile = profile , environment_name = storage_environment )
9074 changed = True
9175
9276 if suppress_submit_warning is not None :
93- dict_utils . merge_overwrite (
94- config , { "user" : { "config" : { " suppress_submit_warning": suppress_submit_warning }}}
95- )
77+ config . setdefault ( "user" , {}). setdefault ( "config" , {})[
78+ " suppress_submit_warning"
79+ ] = suppress_submit_warning
9680 changed = True
9781
9882 if beta_features is not None :
99- dict_utils . merge_overwrite ( config , { "user" : { "config" : { "beta_features" : beta_features }}})
83+ config . setdefault ( "user" , {}). setdefault ( "config" , {})[ "beta_features" ] = beta_features
10084 changed = True
10185
102- with open (config_file , "w" , encoding = "utf-8" ) as file_handler :
103- file_handler .write (toml .dumps (config ))
86+ write_user_config (config )
10487
10588 if not changed :
10689 click .echo ("Nothing to do. Your current config:" )
@@ -109,6 +92,88 @@ def configure(apikey, profile, dev, uat, env, suppress_submit_warning, beta_feat
10992 click .echo ("done." )
11093
11194
95+ @click .command ("login" , context_settings = {"show_default" : True })
96+ @click .option (
97+ "--profile" , prompt = False , default = "default" , help = "Profile, e.g., default, secondary."
98+ )
99+ @click .option ("--dev" , prompt = False , type = bool , is_flag = True , help = "Log in to DEV." )
100+ @click .option ("--uat" , prompt = False , type = bool , is_flag = True , help = "Log in to UAT." )
101+ @click .option ("--env" , prompt = False , default = None , help = "Log in to a named environment." )
102+ @click .option (
103+ "--port" ,
104+ type = click .IntRange (1 , 65535 ),
105+ default = None ,
106+ help = "Fixed localhost callback port. Defaults to an ephemeral port." ,
107+ )
108+ @click .option (
109+ "--timeout" , type = click .IntRange (1 , 3600 ), default = 120 , help = "Login timeout in seconds."
110+ )
111+ def login (profile , dev , uat , env , port , timeout ): # pylint: disable=too-many-arguments
112+ """
113+ Open a browser login flow and store the resulting API key.
114+ """
115+
116+ def announce_login (details ):
117+ click .echo (f"Starting local login server on { details ['callback_url' ]} ." )
118+ if details ["browser_opened" ] == "true" :
119+ click .echo ("If your browser did not open, navigate to this URL to authenticate:" )
120+ else :
121+ click .echo (
122+ "Could not open your browser automatically. Navigate to this URL to authenticate:"
123+ )
124+ click .echo ("" )
125+ click .echo (details ["login_url" ])
126+ click .echo ("" )
127+ click .echo ("Headless environment? Configure an API key manually with:" )
128+ click .echo (f" { build_configure_command (details ['environment' ], details ['profile' ])} " )
129+ click .echo ("" )
130+
131+ try :
132+ environment , _ = resolve_target_environment (dev = dev , uat = uat , env = env )
133+ result = wait_for_login (
134+ environment = environment ,
135+ profile = profile ,
136+ port = port ,
137+ timeout = timeout ,
138+ announce_login = announce_login ,
139+ )
140+ except (LoginError , ValueError ) as error :
141+ raise click .ClickException (str (error )) from error
142+
143+ if result .get ("email" ):
144+ click .echo (f"Successfully logged in as { result ['email' ]} " )
145+ else :
146+ click .echo ("Successfully logged in" )
147+
148+
149+ @click .command ("logout" , context_settings = {"show_default" : True })
150+ @click .option (
151+ "--profile" , prompt = False , default = "default" , help = "Profile, e.g., default, secondary."
152+ )
153+ @click .option ("--dev" , prompt = False , type = bool , is_flag = True , help = "Remove the DEV login." )
154+ @click .option ("--uat" , prompt = False , type = bool , is_flag = True , help = "Remove the UAT login." )
155+ @click .option ("--env" , prompt = False , default = None , help = "Remove the login for a named environment." )
156+ def logout (profile , dev , uat , env ): # pylint: disable=too-many-arguments
157+ """
158+ Remove a stored Flow360 API key.
159+ """
160+ try :
161+ environment , storage_environment = resolve_target_environment (dev = dev , uat = uat , env = env )
162+ except ValueError as error :
163+ raise click .ClickException (str (error )) from error
164+
165+ removed , _ = delete_apikey (profile = profile , environment_name = storage_environment )
166+ if not removed :
167+ click .echo (
168+ f"No stored API key found for profile '{ profile } ' in environment '{ environment .name } '."
169+ )
170+ return
171+
172+ click .echo (
173+ f"Removed stored API key for profile '{ profile } ' in environment '{ environment .name } '."
174+ )
175+
176+
112177# For displaying all projects
113178@click .command ("show_projects" , context_settings = {"show_default" : True })
114179@click .option ("--keyword" , "-k" , help = "Filter projects by keyword" , default = None , type = str )
@@ -250,5 +315,7 @@ def get_release_date(ver: Version) -> str:
250315
251316
252317flow360 .add_command (configure )
318+ flow360 .add_command (login )
319+ flow360 .add_command (logout )
253320flow360 .add_command (show_projects )
254321flow360 .add_command (version )
0 commit comments