Skip to content

Commit 97645ac

Browse files
Merge pull request #5 from aitomatic/feat/implement_login
Refactor login command
2 parents 22cc915 + 85cb267 commit 97645ac

9 files changed

Lines changed: 112 additions & 74 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ Both the above commands would install the package globally and `aito` will be av
2525

2626
## How to use
2727

28+
- `aito login`: Login to Aitomatic account
29+
- `aito app deploy`: Deploy app to Aitomatic cloud
30+
2831
## Feedback
32+
33+
In order to report issues, please open one in https://github.com/aitomatic/aitomatic-cli/issues

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ classifiers =
1818
packages =
1919
src
2020
src.app
21+
src.login
2122
install_requires =
2223
click >= 8.0.4
2324
requests >= 2.27.1
2425
python_requires = >=3.7
2526

2627
[options.entry_points]
2728
console_scripts =
28-
aito = src.cli:cli
29+
aito = src.aito:cli

src/aito.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import click
2+
import json
3+
from pathlib import Path
4+
from src.app.main import app
5+
from src.login.main import login
6+
7+
CREDENTIAL_FILE = Path.home().joinpath('.aitomatic/credentials')
8+
9+
10+
def load_config():
11+
if CREDENTIAL_FILE.exists():
12+
return json.loads(CREDENTIAL_FILE.read_text())
13+
else:
14+
return {}
15+
16+
17+
AUTH_INFO = load_config()
18+
19+
20+
@click.group(
21+
context_settings={'obj': AUTH_INFO},
22+
)
23+
def cli():
24+
'''Aitomatic CLI tool to help manage aitomatic projects and apps'''
25+
26+
27+
cli.add_command(login)
28+
cli.add_command(app)
29+
30+
31+
if __name__ == '__main__':
32+
cli()

src/app/cli.py

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/app/deploy.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import click
2-
from src.login.cli import authenticated
2+
from src.login.main import authenticated
33

44

55
@click.command()
66
@authenticated
77
def deploy():
8+
'''Deploy app to Aitomatic cluster'''
89
click.echo('Deploy app to Aitomatic')
910
# click.echo('User info', obj)

src/app/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import click
2+
from src.app.deploy import deploy
3+
4+
5+
@click.group()
6+
def app():
7+
'''CLI sub-command to help manage aitomatic apps'''
8+
9+
10+
app.add_command(deploy)

src/cli.py

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/login/cli.py renamed to src/login/main.py

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,127 +5,154 @@
55
from pathlib import Path
66
from functools import update_wrapper
77

8-
ORG = 'aitomaticinc.us.auth0.com';
8+
ORG = 'aitomaticinc.us.auth0.com'
99
CLIENT_ID = "zk9AB0KtNqJY0gVeF1p0ZmUb2tlcXpYq"
1010
AUDIENCE = "https://apps.aitomatic.com/dev"
1111
SCOPE = "openid profile email offline_access"
12-
CONFIG_FILE = Path.home().joinpath('.aitomatic')
12+
CREDENTIAL_FILE = Path.home().joinpath('.aitomatic/credentials')
1313

14-
@click.command(help='''
15-
Login to Aitomatic cloud
16-
''')
14+
15+
@click.command()
1716
@click.pass_obj
1817
def login(obj):
19-
if obj.get("at") is not None or CONFIG_FILE.exists():
20-
relogin = click.confirm("You're logged in. Do you want to log in again?", default=False, abort=False, prompt_suffix=': ', show_default=True, err=False)
18+
'''Login to Aitomatic cloud'''
19+
if obj.get("access_token") is not None or CREDENTIAL_FILE.exists():
20+
re_login = click.confirm(
21+
"You're logged in. Do you want to log in again?",
22+
default=False,
23+
abort=False,
24+
prompt_suffix=': ',
25+
show_default=True,
26+
err=False,
27+
)
2128

22-
if not relogin:
29+
if not re_login:
2330
exit(0)
2431

2532
do_login()
2633

34+
2735
def do_login():
2836
click.echo('Logging into Aitomatic cloud...')
2937
device_info = request_device_code()
3038
display_device_info(device_info)
3139
poll_authentication_status(device_info)
3240

41+
3342
def request_device_code():
3443
res = requests.post(
3544
url="https://{}/oauth/device/code".format(ORG),
3645
data={"client_id": CLIENT_ID, "scope": SCOPE, "audience": AUDIENCE},
37-
headers={ "content-type": "application/x-www-form-urlencoded" }
46+
headers={"content-type": "application/x-www-form-urlencoded"},
3847
)
3948

4049
# see details https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-device-authorization-flow#request-device-code
4150
# print(device_response)
4251
# {
43-
# 'device_code': 'kJ3fIJ90RYXbdAOlhns3v7t3',
44-
# 'user_code': '873280791',
45-
# 'verification_uri': 'https://aitomaticinc.us.auth0.com/activate',
46-
# 'expires_in': 900,
47-
# 'interval': 5,
52+
# 'device_code': 'kJ3fIJ90RYXbdAOlhns3v7t3',
53+
# 'user_code': '873280791',
54+
# 'verification_uri': 'https://aitomaticinc.us.auth0.com/activate',
55+
# 'expires_in': 900,
56+
# 'interval': 5,
4857
# 'verification_uri_complete': 'https://aitomaticinc.us.auth0.com/activate?user_code=873280791'
4958
# }
5059

5160
return res.json()
5261

62+
5363
def display_device_info(device_info):
5464
code = device_info['user_code']
5565
url = device_info['verification_uri_complete']
5666

57-
click.echo("""
67+
click.echo(
68+
"""
5869
Please visit:
5970
{}
6071
to login to Aitomatic cloud.
6172
6273
Verification code: {}
63-
""".format(url, code))
74+
""".format(
75+
url, code
76+
)
77+
)
6478

6579
click.launch(url)
66-
6780
click.echo("Waiting for authentication...")
6881

82+
6983
@click.pass_obj
7084
def poll_authentication_status(obj, device_info):
7185
res = requests.post(
7286
url="https://{}/oauth/token".format(ORG),
7387
data={
74-
"client_id": CLIENT_ID,
75-
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
76-
"device_code": device_info['device_code']
88+
"client_id": CLIENT_ID,
89+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
90+
"device_code": device_info['device_code'],
7791
},
78-
headers={ "content-type": "application/x-www-form-urlencoded" }
92+
headers={"content-type": "application/x-www-form-urlencoded"},
7993
)
80-
94+
8195
# response example
8296
# {'error': 'authorization_pending', 'error_description': 'User has yet to authorize device code.'}
8397
# {"error": "expired_token", "error_description": "..." }
8498
# {"error": "access_denied", "error_description": "..." }
8599
# {"access_token": "...", "id_token": "...", "refresh_token": "..."}
86-
polling_data = res.json();
100+
polling_data = res.json()
87101

88102
if polling_data.get('error') == 'authorization_pending':
89103
time.sleep(device_info['interval'])
90104
poll_authentication_status(device_info)
91105

92-
if polling_data.get('error') == 'expired_token' or polling_data.get('error') == 'access_denied':
106+
if (
107+
polling_data.get('error') == 'expired_token'
108+
or polling_data.get('error') == 'access_denied'
109+
):
93110
click.echo(polling_data['error_description'])
94111
exit(1)
95112

96113
if polling_data.get('access_token') is not None:
97-
save_config({ 'at': polling_data['access_token'], 'rt': polling_data['refresh_token'], 'id': polling_data['id_token'] })
114+
save_credential(
115+
{
116+
'access_token': polling_data['access_token'],
117+
'refresh_token': polling_data['refresh_token'],
118+
'id': polling_data['id_token'],
119+
}
120+
)
98121
click.echo("Login successful!")
99122

123+
100124
@click.pass_obj
101-
def save_config(obj, data):
102-
obj['at'] = data['at']
103-
obj['rt'] = data['rt']
125+
def save_credential(obj, data):
126+
obj['access_token'] = data['access_token']
127+
obj['refresh_token'] = data['refresh_token']
104128
obj['id'] = data['id']
105-
CONFIG_FILE.write_text(json.dumps(data))
129+
if not CREDENTIAL_FILE.exists():
130+
CREDENTIAL_FILE.parent.mkdir(parents=True)
131+
CREDENTIAL_FILE.write_text(json.dumps(data))
132+
106133

107134
def authenticated(f):
108135
@click.pass_obj
109136
def wrapper(obj, *args, **kwargs):
110-
token = obj and obj.get("at")
137+
token = obj and obj.get("access_token")
111138

112139
if token is None:
113140
prompt_login()
114141
exit(1)
115142

116143
res = requests.get(
117144
url="https://{}/userinfo".format(ORG),
118-
headers={ "Authorization": "Bearer {}".format(token) }
145+
headers={"Authorization": "Bearer {}".format(token)},
119146
)
120147

121-
if (res.status_code == 200):
148+
if res.status_code == 200:
122149
f(*args, **kwargs)
123150
else:
124151
prompt_login()
125152
exit(1)
126153

127154
return update_wrapper(wrapper, f)
128155

156+
129157
def prompt_login():
130158
click.echo("You're not logged in. Please run `aito login` first.")
131-

version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.0
1+
0.3.0

0 commit comments

Comments
 (0)