8
8
import sys
9
9
import asyncio
10
10
import aiohttp
11
+ import json
11
12
from rich import print
12
13
from datetime import datetime , timezone , timedelta
13
14
import humanize
14
15
from itertools import count
15
16
import diskcache
16
17
import pathlib
18
+ from pathlib import Path
17
19
from typing import Optional , List , Dict
18
20
import argparse
19
21
@@ -247,12 +249,34 @@ async def check_user_admin(
247
249
return (await response .json ())["role" ] == "admin"
248
250
249
251
250
- async def main (orgs , debug : bool , timelimit_days : int ):
252
+ async def main (orgs , debug : bool , timelimit_days : int , config_file : str ):
251
253
"""Main execution function."""
252
254
# Show cache status
253
255
print (f"[blue]Cache directory: { CACHE_DIR } (size: { get_cache_size ()} )[/blue]" )
254
256
print (f"[blue]Cache contains { len (cache )} items[/blue]" )
255
-
257
+ now = datetime .now (timezone .utc )
258
+ manual_users = {}
259
+ for line in Path (config_file ).read_text ().splitlines ():
260
+ if line .startswith ("#" ) or not line .strip ():
261
+ continue
262
+ user , inviter , date_last_activity , reason = [x .strip () for x in line .split (":" )]
263
+ manual_users [user ] = {
264
+ "manual_last_activity" : datetime .strptime (
265
+ date_last_activity , "%Y-%m"
266
+ ).replace (tzinfo = timezone .utc ),
267
+ "last_activity_reason" : reason ,
268
+ "inviter" : inviter ,
269
+ }
270
+ if not manual_users [user ]["last_activity_reason" ]:
271
+ print (
272
+ f"[yellow]Warning: No last_activity_reason for { user ['username' ]} , skipping[/yellow]"
273
+ )
274
+ continue
275
+ if now < manual_users [user ]["manual_last_activity" ]:
276
+ print (
277
+ f"[red]Warning: manual_last_activity for { user ['username' ]} is in the future, skipping[/red]"
278
+ )
279
+ continue
256
280
# check who the current user is
257
281
async with aiohttp .ClientSession () as session :
258
282
async with session .get (
@@ -296,7 +320,7 @@ async def main(orgs, debug: bool, timelimit_days: int):
296
320
if member ["login" ] not in all_members :
297
321
all_members [member ["login" ]] = []
298
322
all_members [member ["login" ]].append (org )
299
- if member [ "is_owner" ] :
323
+ if member . get ( "is_owner" , None ) :
300
324
org_owners .setdefault (org , []).append (member ["login" ])
301
325
302
326
# Get activity for each user
@@ -312,6 +336,11 @@ async def main(orgs, debug: bool, timelimit_days: int):
312
336
for (username , _ ), last_activity in zip (tasks , results ):
313
337
if last_activity is not None :
314
338
assert isinstance (last_activity , datetime ), last_activity
339
+
340
+ if last_activity is None :
341
+ last_activity = manual_users .get (username , {}).get (
342
+ "manual_last_activity" , None
343
+ )
315
344
user_activities .append (
316
345
(
317
346
username ,
@@ -320,6 +349,7 @@ async def main(orgs, debug: bool, timelimit_days: int):
320
349
)
321
350
)
322
351
for org in orgs :
352
+ # todo, check admin concurently
323
353
print (f"[bold]{ org } [/bold]" )
324
354
is_admin = await check_user_admin (session , org , current_user )
325
355
if is_admin :
@@ -358,7 +388,21 @@ async def main(orgs, debug: bool, timelimit_days: int):
358
388
)
359
389
orgs_str = ", " .join (user_orgs )
360
390
u_owner = " (owner)" if username in org_owners .get (org , []) else ""
361
- print (f" { username + u_owner :<20} : Last activity { last_activity_ago } " )
391
+ inviter = manual_users .get (username , {}).get ("inviter" , None )
392
+ if inviter :
393
+ inviter = f"[green]@{ inviter } [/green]"
394
+ else :
395
+ inviter = "[red]unknown[/red]"
396
+ reason = manual_users .get (username , {}).get (
397
+ "last_activity_reason" , None
398
+ )
399
+ if reason :
400
+ reason = f"[green]{ reason } [/green]"
401
+ else :
402
+ reason = "[red]unknown[/red]"
403
+ print (
404
+ f" { username + u_owner :<20} : Last activity { last_activity_ago } : reason: { reason } , inviter: { inviter } "
405
+ )
362
406
print (
363
407
f" Found [red]{ n_inactive } inactive[/red] and [green]{ n_active } active[/green] users in { org } with last activity more recent than { timelimit_days } days."
364
408
)
@@ -370,6 +414,12 @@ async def main(orgs, debug: bool, timelimit_days: int):
370
414
"--clear-cache" , action = "store_true" , help = "Clear the cache before running"
371
415
)
372
416
parser .add_argument ("--debug" , action = "store_true" , help = "Show debug information" )
417
+ parser .add_argument (
418
+ "--config-file" ,
419
+ type = str ,
420
+ default = "last_user_activity.json" ,
421
+ help = "Path to the config file (default: last_user_activity.json)" ,
422
+ )
373
423
374
424
parser .add_argument (
375
425
"--timelimit-days" ,
@@ -388,4 +438,4 @@ async def main(orgs, debug: bool, timelimit_days: int):
388
438
if args .clear_cache :
389
439
clear_cache ()
390
440
391
- asyncio .run (main (args .orgs , args .debug , args .timelimit_days ))
441
+ asyncio .run (main (args .orgs , args .debug , args .timelimit_days , args . config_file ))
0 commit comments