33"""
44
55import importlib
6+ import json
67import sys
78from datetime import UTC , datetime , timedelta
89
@@ -305,7 +306,15 @@ def token():
305306@token .command ()
306307@click .option ("--description" , "-d" , required = True , help = "Token description" )
307308@click .option ("--expires-days" , "-e" , type = int , help = "Token expiration in days" )
308- def add (description : str , expires_days : int | None ):
309+ @click .option (
310+ "--format" ,
311+ "output_format" ,
312+ type = click .Choice (["text" , "json" ]),
313+ default = "text" ,
314+ show_default = True ,
315+ help = "Output format." ,
316+ )
317+ def add (description : str , expires_days : int | None , output_format : str ):
309318 """Add a new authentication token."""
310319 import asyncio
311320
@@ -344,20 +353,43 @@ async def create_token():
344353 list_key = Keys .auth_tokens_list_key ()
345354 await redis .sadd (list_key , token_hash )
346355
356+ return token , token_info
357+
358+ token , token_info = asyncio .run (create_token ())
359+
360+ if output_format == "json" :
361+ data = {
362+ "token" : token ,
363+ "description" : token_info .description ,
364+ "created_at" : token_info .created_at .isoformat (),
365+ "expires_at" : token_info .expires_at .isoformat ()
366+ if token_info .expires_at
367+ else None ,
368+ "hash" : token_info .token_hash ,
369+ }
370+ click .echo (json .dumps (data ))
371+ else :
372+ expires_at = token_info .expires_at
347373 click .echo ("Token created successfully!" )
348374 click .echo (f"Token: { token } " )
349- click .echo (f"Description: { description } " )
375+ click .echo (f"Description: { token_info . description } " )
350376 if expires_at :
351377 click .echo (f"Expires: { expires_at .isoformat ()} " )
352378 else :
353379 click .echo ("Expires: Never" )
354380 click .echo ("\n WARNING: Save this token securely. It will not be shown again." )
355381
356- asyncio .run (create_token ())
357-
358382
359383@token .command ()
360- def list ():
384+ @click .option (
385+ "--format" ,
386+ "output_format" ,
387+ type = click .Choice (["text" , "json" ]),
388+ default = "text" ,
389+ show_default = True ,
390+ help = "Output format." ,
391+ )
392+ def list (output_format : str ):
361393 """List all authentication tokens."""
362394 import asyncio
363395
@@ -371,12 +403,16 @@ async def list_tokens():
371403 list_key = Keys .auth_tokens_list_key ()
372404 token_hashes = await redis .smembers (list_key )
373405
406+ tokens_data = []
407+
374408 if not token_hashes :
375- click .echo ("No tokens found." )
376- return
409+ if output_format == "text" :
410+ click .echo ("No tokens found." )
411+ return tokens_data
377412
378- click .echo ("Authentication Tokens:" )
379- click .echo ("=" * 50 )
413+ if output_format == "text" :
414+ click .echo ("Authentication Tokens:" )
415+ click .echo ("=" * 50 )
380416
381417 for token_hash in token_hashes :
382418 key = Keys .auth_token_key (token_hash )
@@ -390,27 +426,57 @@ async def list_tokens():
390426 try :
391427 token_info = TokenInfo .model_validate_json (token_data )
392428
393- # Mask the token hash for display
394- masked_hash = token_hash [:8 ] + "..." + token_hash [- 8 :]
395-
396- click .echo (f"Token: { masked_hash } " )
397- click .echo (f"Description: { token_info .description } " )
398- click .echo (f"Created: { token_info .created_at .isoformat ()} " )
399- if token_info .expires_at :
400- click .echo (f"Expires: { token_info .expires_at .isoformat ()} " )
401- else :
402- click .echo ("Expires: Never" )
403- click .echo ("-" * 30 )
429+ tokens_data .append (
430+ {
431+ "hash" : token_hash ,
432+ "description" : token_info .description ,
433+ "created_at" : token_info .created_at .isoformat (),
434+ "expires_at" : token_info .expires_at .isoformat ()
435+ if token_info .expires_at
436+ else None ,
437+ "expired" : bool (
438+ token_info .expires_at
439+ and datetime .now (UTC ) > token_info .expires_at
440+ ),
441+ }
442+ )
443+
444+ if output_format == "text" :
445+ # Mask the token hash for display
446+ masked_hash = token_hash [:8 ] + "..." + token_hash [- 8 :]
447+
448+ click .echo (f"Token: { masked_hash } " )
449+ click .echo (f"Description: { token_info .description } " )
450+ click .echo (f"Created: { token_info .created_at .isoformat ()} " )
451+ if token_info .expires_at :
452+ click .echo (f"Expires: { token_info .expires_at .isoformat ()} " )
453+ else :
454+ click .echo ("Expires: Never" )
455+ click .echo ("-" * 30 )
404456
405457 except Exception as e :
406- click .echo (f"Error processing token { token_hash } : { e } " )
458+ if output_format == "text" :
459+ click .echo (f"Error processing token { token_hash } : { e } " )
460+
461+ return tokens_data
462+
463+ tokens_data = asyncio .run (list_tokens ())
407464
408- asyncio .run (list_tokens ())
465+ if output_format == "json" :
466+ click .echo (json .dumps (tokens_data ))
409467
410468
411469@token .command ()
412470@click .argument ("token_hash" )
413- def show (token_hash : str ):
471+ @click .option (
472+ "--format" ,
473+ "output_format" ,
474+ type = click .Choice (["text" , "json" ]),
475+ default = "text" ,
476+ show_default = True ,
477+ help = "Output format." ,
478+ )
479+ def show (token_hash : str , output_format : str ):
414480 """Show details for a specific token."""
415481 import asyncio
416482
@@ -430,45 +496,69 @@ async def show_token():
430496 matching_hashes = [h for h in token_hashes if h .startswith (token_hash )]
431497
432498 if not matching_hashes :
433- click .echo (f"No token found matching '{ token_hash } '" )
434- return
499+ if output_format == "text" :
500+ click .echo (f"No token found matching '{ token_hash } '" )
501+ return None
435502 if len (matching_hashes ) > 1 :
436- click .echo (f"Multiple tokens match '{ token_hash } ':" )
437- for h in matching_hashes :
438- click .echo (f" { h [:8 ]} ...{ h [- 8 :]} " )
439- return
503+ if output_format == "text" :
504+ click .echo (f"Multiple tokens match '{ token_hash } ':" )
505+ for h in matching_hashes :
506+ click .echo (f" { h [:8 ]} ...{ h [- 8 :]} " )
507+ return None
440508 token_hash = matching_hashes [0 ]
441509
442510 key = Keys .auth_token_key (token_hash )
443511 token_data = await redis .get (key )
444512
445513 if not token_data :
446- click .echo (f"Token not found: { token_hash } " )
447- return
514+ if output_format == "text" :
515+ click .echo (f"Token not found: { token_hash } " )
516+ return None
448517
449518 try :
450519 token_info = TokenInfo .model_validate_json (token_data )
451520
452- click .echo ("Token Details:" )
453- click .echo ("=" * 30 )
454- click .echo (f"Hash: { token_hash } " )
455- click .echo (f"Description: { token_info .description } " )
456- click .echo (f"Created: { token_info .created_at .isoformat ()} " )
457- if token_info .expires_at :
458- click .echo (f"Expires: { token_info .expires_at .isoformat ()} " )
459- # Check if expired
460- if datetime .now (UTC ) > token_info .expires_at :
461- click .echo ("Status: EXPIRED" )
462- else :
463- click .echo ("Status: Active" )
521+ if token_info .expires_at and datetime .now (UTC ) > token_info .expires_at :
522+ status = "EXPIRED"
464523 else :
465- click .echo ("Expires: Never" )
466- click .echo ("Status: Active" )
524+ status = "Active"
467525
468- except Exception as e :
469- click .echo (f"Error processing token: { e } " )
526+ return token_hash , token_info , status
470527
471- asyncio .run (show_token ())
528+ except Exception as e :
529+ if output_format == "text" :
530+ click .echo (f"Error processing token: { e } " )
531+ return None
532+
533+ result = asyncio .run (show_token ())
534+
535+ if result is None :
536+ return
537+
538+ token_hash , token_info , status = result
539+
540+ if output_format == "json" :
541+ data = {
542+ "hash" : token_hash ,
543+ "description" : token_info .description ,
544+ "created_at" : token_info .created_at .isoformat (),
545+ "expires_at" : token_info .expires_at .isoformat ()
546+ if token_info .expires_at
547+ else None ,
548+ "status" : status ,
549+ }
550+ click .echo (json .dumps (data ))
551+ else :
552+ click .echo ("Token Details:" )
553+ click .echo ("=" * 30 )
554+ click .echo (f"Hash: { token_hash } " )
555+ click .echo (f"Description: { token_info .description } " )
556+ click .echo (f"Created: { token_info .created_at .isoformat ()} " )
557+ if token_info .expires_at :
558+ click .echo (f"Expires: { token_info .expires_at .isoformat ()} " )
559+ else :
560+ click .echo ("Expires: Never" )
561+ click .echo (f"Status: { status } " )
472562
473563
474564@token .command ()
0 commit comments