@@ -9,12 +9,12 @@ if sys.version_info[:2] < (3, 8):
99 print ('Error: Python 3.8 or higher is required' , file = sys .stderr )
1010 sys .exit (1 )
1111
12-
12+ import base64
1313import json
1414import os
1515
16- from argparse import ArgumentParser , ArgumentTypeError
17- from typing import Any , Callable
16+ from argparse import _SubParsersAction , ArgumentParser , ArgumentTypeError
17+ from typing import Any , Callable , TypedDict
1818from urllib .error import HTTPError , URLError
1919from urllib .parse import urlparse
2020from urllib .request import Request , urlopen
@@ -38,10 +38,11 @@ Features:
3838 - Store and retrieve configuration (e.g., base URL)
3939"""
4040
41- USER_CONFIG_FILEPATH = os .path .expanduser ('~/.odc-cli.json' )
42-
4341config : Config # initialized in main
4442
43+ # --- Types ---
44+
45+ JwtPayload = TypedDict ('JwtPayload' , {'basePermissionLevel' : str })
4546
4647# --- Decorators ---
4748
@@ -51,7 +52,20 @@ def require_url(fn: Callable[..., Any]) -> Callable[..., Any]:
5152
5253 def wrapper (* args : Any , ** kwargs : Any ) -> None :
5354 if config .base_url is None :
54- raise RuntimeError (f'URL must be defined (hint: use { PROGRAM_NAME } config set-url)' )
55+ print (f'Error: URL must be defined (hint: use { PROGRAM_NAME } config set-url)' , file = sys .stderr )
56+ sys .exit (1 )
57+ fn (* args , ** kwargs )
58+
59+ return wrapper
60+
61+
62+ def require_token (fn : Callable [..., Any ]) -> Callable [..., Any ]:
63+ """Decorator to ensure that the user is logged in before executing the function"""
64+
65+ def wrapper (* args : Any , ** kwargs : Any ) -> None :
66+ if config .access_token is None :
67+ print (f'Error: Must be logged in (hint: use { PROGRAM_NAME } auth login)' , file = sys .stderr )
68+ sys .exit (1 )
5569 fn (* args , ** kwargs )
5670
5771 return wrapper
@@ -61,6 +75,8 @@ def require_url(fn: Callable[..., Any]) -> Callable[..., Any]:
6175
6276
6377class ArgumentTypes :
78+ """Static methods to validate cli args"""
79+
6480 @staticmethod
6581 def valid_url_or_none (url : str ) -> str | None :
6682 if url == 'None' :
@@ -71,8 +87,31 @@ class ArgumentTypes:
7187 return url
7288
7389
90+ class JWTDecoder :
91+ """Decodes the payload of a JSON Web Token (JWT) without validating its signature"""
92+
93+ @classmethod
94+ def decode_payload (cls , token : str ) -> dict [str , Any ]:
95+ try :
96+ parts = token .split ('.' )
97+ if len (parts ) != 3 :
98+ raise ValueError ('Invalid JWT format: expected header.payload.signature' )
99+ payload_b64 = parts [1 ]
100+ decoded_bytes = cls ._base64url_decode (payload_b64 )
101+ payload = json .loads (decoded_bytes .decode ('utf-8' ))
102+ return payload
103+ except Exception as e :
104+ raise RuntimeError ('Failed to decode JWT payload' ) from e
105+
106+ @staticmethod
107+ def _base64url_decode (input_str : str ):
108+ padding = '=' * (- len (input_str ) % 4 )
109+ input_str += padding
110+ return base64 .urlsafe_b64decode (input_str )
111+
112+
74113class Config :
75- """Manages user configuration stored in a JSON file"""
114+ """Manages program configuration stored in a JSON file"""
76115
77116 _dict : dict [str , Any ]
78117 _filepath = os .path .expanduser ('~/.odc-cli.json' )
@@ -99,6 +138,8 @@ class Config:
99138 @base_url .setter
100139 def base_url (self , value : str | None ) -> None :
101140 self ._dict ['base_url' ] = value
141+ if value is None :
142+ self ._dict ['access_token' ] = None
102143
103144 def write (self ) -> None :
104145 with open (self ._filepath , 'w' ) as file :
@@ -133,11 +174,17 @@ class HttpResponse:
133174class HttpClient :
134175 """Simplified HTTP client for making requests"""
135176
177+ _default_headers : dict [str , str ] = {'Content-Type' : 'application/json' }
178+
179+ @classmethod
180+ def get (cls , url : str ) -> HttpResponse :
181+ req = Request (url , headers = cls ._default_headers , method = 'GET' )
182+ return cls ._send (req )
183+
136184 @classmethod
137185 def post (cls , url : str , data : Any ) -> HttpResponse :
138- headers : dict [str , str ] = {'Content-Type' : 'application/json' }
139186 json_data = json .dumps (data ).encode ('utf-8' )
140- req = Request (url , data = json_data , headers = headers , method = 'POST' )
187+ req = Request (url , data = json_data , headers = cls . _default_headers , method = 'POST' )
141188 return cls ._send (req )
142189
143190 @staticmethod
@@ -187,6 +234,7 @@ class AuthCommands:
187234 return None
188235 config .access_token = response .data ['accessToken' ]
189236 print ('Success!' )
237+ config .write ()
190238
191239
192240class ConfigCommands :
@@ -199,41 +247,81 @@ class ConfigCommands:
199247 config .base_url = url
200248 config .write ()
201249
250+ @require_url
251+ @require_token
252+ @staticmethod
253+ def get_permissions () -> None :
254+ assert config .access_token is not None
255+ payload = JWTDecoder .decode_payload (config .access_token )
256+ permissions = payload .get ('permissions' )
257+ assert isinstance (permissions , list )
258+ print (json .dumps (permissions , indent = 2 ))
259+
260+
261+ class SetupCommands :
262+ @require_url
263+ @staticmethod
264+ def get_status () -> None :
265+ response = HttpClient .get (f'{ config .base_url } /v1/setup' )
266+ print (response )
267+
202268
203269# --- Main CLI Entrypoint ---
204270
205271
206- def main () -> None :
207- global config
208- config = Config ()
272+ class CLI :
273+ parser : ArgumentParser
274+ subparsers : _SubParsersAction [ArgumentParser ]
275+
276+ def __init__ (self ) -> None :
277+ self .parser = ArgumentParser (prog = PROGRAM_NAME , description = PROGRAM_DESCRIPTION )
278+ self .subparsers = self .parser .add_subparsers (help = 'subcommand help' , required = True )
279+ self ._create_auth_parser ()
280+ self ._create_config_parser ()
281+ self ._create_setup_parser ()
282+
283+ def run (self ) -> None :
284+ kwargs = vars (self .parser .parse_args ())
285+ fn = kwargs .pop ('fn' )
286+ fn (** kwargs )
287+
288+ def _create_auth_parser (self ):
289+ parser = self .subparsers .add_parser ('auth' )
290+ subparsers = parser .add_subparsers (required = True )
209291
210- parser = ArgumentParser (prog = PROGRAM_NAME , description = PROGRAM_DESCRIPTION )
211- subparsers = parser .add_subparsers (help = 'subcommand help' , required = True )
292+ login_parser = subparsers .add_parser ('login' )
293+ login_parser .add_argument ('--username' , type = str , required = True )
294+ login_parser .add_argument ('--password' , type = str , required = True )
295+ login_parser .set_defaults (fn = AuthCommands .login )
212296
213- ## AUTH
214- auth_parser = subparsers .add_parser ('auth ' )
215- auth_subparsers = auth_parser .add_subparsers (required = True )
297+ def _create_config_parser ( self ):
298+ parser = self . subparsers .add_parser ('config ' )
299+ subparsers = parser .add_subparsers (required = True )
216300
217- login_parser = auth_subparsers .add_parser ('login' )
218- login_parser .add_argument ('--username' , type = str , required = True )
219- login_parser .add_argument ('--password' , type = str , required = True )
220- login_parser .set_defaults (fn = AuthCommands .login )
301+ get_config_url_parser = subparsers .add_parser ('get-url' )
302+ get_config_url_parser .set_defaults (fn = ConfigCommands .get_url )
221303
222- ## CONFIG
304+ set_config_parser = subparsers .add_parser ('set-url' )
305+ set_config_parser .add_argument ('url' , type = ArgumentTypes .valid_url_or_none )
306+ set_config_parser .set_defaults (fn = ConfigCommands .set_url )
223307
224- config_parser = subparsers .add_parser ('config ' )
225- config_subparsers = config_parser . add_subparsers ( required = True )
308+ get_permissions_parser = subparsers .add_parser ('get-permissions ' )
309+ get_permissions_parser . set_defaults ( fn = ConfigCommands . get_permissions )
226310
227- get_config_url_parser = config_subparsers .add_parser ('get-url' )
228- get_config_url_parser .set_defaults (fn = ConfigCommands .get_url )
311+ def _create_setup_parser (self ):
312+ setup_parser = self .subparsers .add_parser ('setup' )
313+ setup_subparsers = setup_parser .add_subparsers (required = True )
229314
230- set_config_parser = config_subparsers .add_parser ('set-url' )
231- set_config_parser .add_argument ('url' , type = ArgumentTypes .valid_url_or_none )
232- set_config_parser .set_defaults (fn = ConfigCommands .set_url )
315+ get_setup_state_parser = setup_subparsers .add_parser ('get-state' )
316+ get_setup_state_parser .set_defaults (fn = SetupCommands .get_status )
317+
318+
319+ def main () -> None :
320+ global config
321+ config = Config ()
233322
234- kwargs = vars (parser .parse_args ())
235- fn = kwargs .pop ('fn' )
236- fn (** kwargs )
323+ cli = CLI ()
324+ cli .run ()
237325
238326
239327if __name__ == '__main__' :
0 commit comments