33"""
44
55import importlib
6+ import json
67import sys
78from datetime import UTC , datetime , timedelta
89
@@ -305,7 +306,26 @@ 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+ @click .option (
318+ "--token" ,
319+ "provided_token" ,
320+ type = str ,
321+ help = "Use a pre-generated token instead of generating a new one." ,
322+ )
323+ def add (
324+ description : str ,
325+ expires_days : int | None ,
326+ output_format : str ,
327+ provided_token : str | None ,
328+ ) -> None :
309329 """Add a new authentication token."""
310330 import asyncio
311331
@@ -315,9 +335,9 @@ def add(description: str, expires_days: int | None):
315335 async def create_token ():
316336 redis = await get_redis_conn ()
317337
318- # Generate token
319- token = generate_token ()
320- token_hash = hash_token (token )
338+ # Determine token value
339+ token_value = provided_token or generate_token ()
340+ token_hash = hash_token (token_value )
321341
322342 # Calculate expiration
323343 now = datetime .now (UTC )
@@ -344,20 +364,43 @@ async def create_token():
344364 list_key = Keys .auth_tokens_list_key ()
345365 await redis .sadd (list_key , token_hash )
346366
367+ return token_value , token_info
368+
369+ token , token_info = asyncio .run (create_token ())
370+
371+ if output_format == "json" :
372+ data = {
373+ "token" : token ,
374+ "description" : token_info .description ,
375+ "created_at" : token_info .created_at .isoformat (),
376+ "expires_at" : token_info .expires_at .isoformat ()
377+ if token_info .expires_at
378+ else None ,
379+ "hash" : token_info .token_hash ,
380+ }
381+ click .echo (json .dumps (data ))
382+ else :
383+ expires_at = token_info .expires_at
347384 click .echo ("Token created successfully!" )
348385 click .echo (f"Token: { token } " )
349- click .echo (f"Description: { description } " )
386+ click .echo (f"Description: { token_info . description } " )
350387 if expires_at :
351388 click .echo (f"Expires: { expires_at .isoformat ()} " )
352389 else :
353390 click .echo ("Expires: Never" )
354391 click .echo ("\n WARNING: Save this token securely. It will not be shown again." )
355392
356- asyncio .run (create_token ())
357-
358393
359394@token .command ()
360- def list ():
395+ @click .option (
396+ "--format" ,
397+ "output_format" ,
398+ type = click .Choice (["text" , "json" ]),
399+ default = "text" ,
400+ show_default = True ,
401+ help = "Output format." ,
402+ )
403+ def list (output_format : str ):
361404 """List all authentication tokens."""
362405 import asyncio
363406
@@ -371,12 +414,16 @@ async def list_tokens():
371414 list_key = Keys .auth_tokens_list_key ()
372415 token_hashes = await redis .smembers (list_key )
373416
417+ tokens_data = []
418+
374419 if not token_hashes :
375- click .echo ("No tokens found." )
376- return
420+ if output_format == "text" :
421+ click .echo ("No tokens found." )
422+ return tokens_data
377423
378- click .echo ("Authentication Tokens:" )
379- click .echo ("=" * 50 )
424+ if output_format == "text" :
425+ click .echo ("Authentication Tokens:" )
426+ click .echo ("=" * 50 )
380427
381428 for token_hash in token_hashes :
382429 key = Keys .auth_token_key (token_hash )
@@ -390,27 +437,62 @@ async def list_tokens():
390437 try :
391438 token_info = TokenInfo .model_validate_json (token_data )
392439
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 )
440+ tokens_data .append (
441+ {
442+ "hash" : token_hash ,
443+ "description" : token_info .description ,
444+ "created_at" : token_info .created_at .isoformat (),
445+ "expires_at" : token_info .expires_at .isoformat ()
446+ if token_info .expires_at
447+ else None ,
448+ "status" : (
449+ "Never Expires"
450+ if not token_info .expires_at
451+ else (
452+ "EXPIRED"
453+ if datetime .now (UTC ) > token_info .expires_at
454+ else "Active"
455+ )
456+ ),
457+ }
458+ )
459+
460+ if output_format == "text" :
461+ # Mask the token hash for display
462+ masked_hash = token_hash [:8 ] + "..." + token_hash [- 8 :]
463+
464+ click .echo (f"Token: { masked_hash } " )
465+ click .echo (f"Description: { token_info .description } " )
466+ click .echo (f"Created: { token_info .created_at .isoformat ()} " )
467+ if token_info .expires_at :
468+ click .echo (f"Expires: { token_info .expires_at .isoformat ()} " )
469+ else :
470+ click .echo ("Expires: Never" )
471+ click .echo ("-" * 30 )
404472
405473 except Exception as e :
406- click .echo (f"Error processing token { token_hash } : { e } " )
474+ if output_format == "text" :
475+ click .echo (f"Error processing token { token_hash } : { e } " )
476+
477+ return tokens_data
478+
479+ tokens_data = asyncio .run (list_tokens ())
407480
408- asyncio .run (list_tokens ())
481+ if output_format == "json" :
482+ click .echo (json .dumps (tokens_data ))
409483
410484
411485@token .command ()
412486@click .argument ("token_hash" )
413- def show (token_hash : str ):
487+ @click .option (
488+ "--format" ,
489+ "output_format" ,
490+ type = click .Choice (["text" , "json" ]),
491+ default = "text" ,
492+ show_default = True ,
493+ help = "Output format." ,
494+ )
495+ def show (token_hash : str , output_format : str ):
414496 """Show details for a specific token."""
415497 import asyncio
416498
@@ -430,45 +512,90 @@ async def show_token():
430512 matching_hashes = [h for h in token_hashes if h .startswith (token_hash )]
431513
432514 if not matching_hashes :
433- click .echo (f"No token found matching '{ token_hash } '" )
434- return
515+ if output_format == "json" :
516+ click .echo (
517+ json .dumps ({"error" : f"No token found matching '{ token_hash } '" })
518+ )
519+ sys .exit (1 )
520+ else :
521+ click .echo (f"No token found matching '{ token_hash } '" )
522+ sys .exit (1 )
435523 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
524+ if output_format == "json" :
525+ click .echo (
526+ json .dumps (
527+ {
528+ "error" : f"Multiple tokens match '{ token_hash } '" ,
529+ "matches" : [
530+ f"{ h [:8 ]} ...{ h [- 8 :]} " for h in matching_hashes
531+ ],
532+ }
533+ )
534+ )
535+ sys .exit (1 )
536+ else :
537+ click .echo (f"Multiple tokens match '{ token_hash } ':" )
538+ for h in matching_hashes :
539+ click .echo (f" { h [:8 ]} ...{ h [- 8 :]} " )
540+ sys .exit (1 )
440541 token_hash = matching_hashes [0 ]
441542
442543 key = Keys .auth_token_key (token_hash )
443544 token_data = await redis .get (key )
444545
445546 if not token_data :
446- click .echo (f"Token not found: { token_hash } " )
447- return
547+ if output_format == "json" :
548+ click .echo (json .dumps ({"error" : f"Token not found: { token_hash } " }))
549+ sys .exit (1 )
550+ else :
551+ click .echo (f"Token not found: { token_hash } " )
552+ sys .exit (1 )
448553
449554 try :
450555 token_info = TokenInfo .model_validate_json (token_data )
451556
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" )
557+ if token_info .expires_at and datetime .now (UTC ) > token_info .expires_at :
558+ status = "EXPIRED"
464559 else :
465- click .echo ("Expires: Never" )
466- click .echo ("Status: Active" )
560+ status = "Active"
467561
468- except Exception as e :
469- click .echo (f"Error processing token: { e } " )
562+ return token_hash , token_info , status
470563
471- asyncio .run (show_token ())
564+ except Exception as e :
565+ if output_format == "json" :
566+ click .echo (
567+ json .dumps ({"error" : f"Failed to parse token data: { str (e )} " })
568+ )
569+ sys .exit (1 )
570+ else :
571+ click .echo (f"Error processing token: { e } " , err = True )
572+ sys .exit (1 )
573+
574+ result = asyncio .run (show_token ())
575+ token_hash , token_info , status = result
576+
577+ if output_format == "json" :
578+ data = {
579+ "hash" : token_hash ,
580+ "description" : token_info .description ,
581+ "created_at" : token_info .created_at .isoformat (),
582+ "expires_at" : token_info .expires_at .isoformat ()
583+ if token_info .expires_at
584+ else None ,
585+ "status" : status ,
586+ }
587+ click .echo (json .dumps (data ))
588+ else :
589+ click .echo ("Token Details:" )
590+ click .echo ("=" * 30 )
591+ click .echo (f"Hash: { token_hash } " )
592+ click .echo (f"Description: { token_info .description } " )
593+ click .echo (f"Created: { token_info .created_at .isoformat ()} " )
594+ if token_info .expires_at :
595+ click .echo (f"Expires: { token_info .expires_at .isoformat ()} " )
596+ else :
597+ click .echo ("Expires: Never" )
598+ click .echo (f"Status: { status } " )
472599
473600
474601@token .command ()
0 commit comments