4
4
It implements disk-based caching to minimize API requests and respect rate limits.
5
5
"""
6
6
7
+ import argparse
8
+ import asyncio
7
9
import os
10
+ import pathlib
8
11
import sys
9
- import asyncio
10
- import aiohttp
11
- from rich import print
12
- from datetime import datetime , timezone , timedelta
13
- import humanize
12
+ from datetime import datetime , timedelta , timezone
14
13
from itertools import count
14
+ from pathlib import Path
15
+ from typing import Dict , List , Optional
16
+
17
+ import aiohttp
15
18
import diskcache
16
- import pathlib
17
- from typing import Optional , List , Dict
18
- import argparse
19
+ import humanize
20
+ from rich import print
19
21
20
22
default_orgs = [
21
23
"binder-examples" ,
@@ -81,6 +83,7 @@ def get(self, key, default=None, retry=False):
81
83
82
84
83
85
# Configure DiskCache in the current directory
86
+ # todo: auto clear after ~24 hours
84
87
CACHE_DIR = "github_cache"
85
88
cache = DateTimeCache (CACHE_DIR )
86
89
@@ -247,12 +250,34 @@ async def check_user_admin(
247
250
return (await response .json ())["role" ] == "admin"
248
251
249
252
250
- async def main (orgs , debug : bool , timelimit_days : int ):
253
+ async def main (orgs , debug : bool , timelimit_days : int , config_file : str ):
251
254
"""Main execution function."""
252
255
# Show cache status
253
256
print (f"[blue]Cache directory: { CACHE_DIR } (size: { get_cache_size ()} )[/blue]" )
254
257
print (f"[blue]Cache contains { len (cache )} items[/blue]" )
255
-
258
+ now = datetime .now (timezone .utc )
259
+ manual_users = {}
260
+ for line in Path (config_file ).read_text ().splitlines ():
261
+ if line .startswith ("#" ) or not line .strip ():
262
+ continue
263
+ user , inviter , date_last_activity , reason = [x .strip () for x in line .split (":" )]
264
+ manual_users [user ] = {
265
+ "manual_last_activity" : datetime .strptime (
266
+ date_last_activity , "%Y-%m"
267
+ ).replace (tzinfo = timezone .utc ),
268
+ "last_activity_reason" : reason ,
269
+ "inviter" : inviter ,
270
+ }
271
+ if not manual_users [user ]["last_activity_reason" ]:
272
+ print (
273
+ f"[yellow]Warning: No last_activity_reason for { user ['username' ]} , skipping[/yellow]"
274
+ )
275
+ continue
276
+ if now < manual_users [user ]["manual_last_activity" ]:
277
+ print (
278
+ f"[red]Warning: manual_last_activity for { user ['username' ]} is in the future, skipping[/red]"
279
+ )
280
+ continue
256
281
# check who the current user is
257
282
async with aiohttp .ClientSession () as session :
258
283
async with session .get (
@@ -296,7 +321,7 @@ async def main(orgs, debug: bool, timelimit_days: int):
296
321
if member ["login" ] not in all_members :
297
322
all_members [member ["login" ]] = []
298
323
all_members [member ["login" ]].append (org )
299
- if member [ "is_owner" ] :
324
+ if member . get ( "is_owner" , None ) :
300
325
org_owners .setdefault (org , []).append (member ["login" ])
301
326
302
327
# Get activity for each user
@@ -312,16 +337,25 @@ async def main(orgs, debug: bool, timelimit_days: int):
312
337
for (username , _ ), last_activity in zip (tasks , results ):
313
338
if last_activity is not None :
314
339
assert isinstance (last_activity , datetime ), last_activity
340
+
341
+ if last_activity is None :
342
+ last_activity = manual_users .get (username , {}).get (
343
+ "manual_last_activity" , None
344
+ )
315
345
user_activities .append (
316
346
(
317
347
username ,
318
348
last_activity if last_activity is not None else None ,
319
349
all_members [username ],
320
350
)
321
351
)
322
- for org in orgs :
352
+
353
+ admin_check_tasks = [
354
+ check_user_admin (session , org , current_user ) for org in orgs
355
+ ]
356
+ admin_check_results = await asyncio .gather (* admin_check_tasks )
357
+ for org , is_admin in zip (orgs , admin_check_results ):
323
358
print (f"[bold]{ org } [/bold]" )
324
- is_admin = await check_user_admin (session , org , current_user )
325
359
if is_admin :
326
360
if debug :
327
361
print (f" [green]{ current_user } is an admin in { org } [/green]" )
@@ -347,6 +381,8 @@ async def main(orgs, debug: bool, timelimit_days: int):
347
381
- timedelta (days = timelimit_days )
348
382
):
349
383
n_active += 1
384
+ if debug :
385
+ print (f" [green]{ username } [/green] is active in { org } " )
350
386
continue
351
387
n_inactive += 1
352
388
last_activity_ago = (
@@ -356,9 +392,22 @@ async def main(orgs, debug: bool, timelimit_days: int):
356
392
if last_activity
357
393
else "[red]never[/red]"
358
394
)
359
- orgs_str = ", " .join (user_orgs )
360
395
u_owner = " (owner)" if username in org_owners .get (org , []) else ""
361
- print (f" { username + u_owner :<20} : Last activity { last_activity_ago } " )
396
+ inviter = manual_users .get (username , {}).get ("inviter" , None )
397
+ if inviter :
398
+ inviter = f"[green]@{ inviter } [/green]"
399
+ else :
400
+ inviter = "[red]unknown[/red]"
401
+ reason = manual_users .get (username , {}).get (
402
+ "last_activity_reason" , None
403
+ )
404
+ if reason :
405
+ reason = f"[green]{ reason } [/green]"
406
+ else :
407
+ reason = "[red]unknown[/red]"
408
+ print (
409
+ f" { username + u_owner :<20} : Last activity { last_activity_ago } : reason: { reason } , inviter: { inviter } "
410
+ )
362
411
print (
363
412
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
413
)
@@ -370,6 +419,12 @@ async def main(orgs, debug: bool, timelimit_days: int):
370
419
"--clear-cache" , action = "store_true" , help = "Clear the cache before running"
371
420
)
372
421
parser .add_argument ("--debug" , action = "store_true" , help = "Show debug information" )
422
+ parser .add_argument (
423
+ "--config-file" ,
424
+ type = str ,
425
+ default = "last_user_activity.json" ,
426
+ help = "Path to the config file (default: last_user_activity.json)" ,
427
+ )
373
428
374
429
parser .add_argument (
375
430
"--timelimit-days" ,
@@ -388,4 +443,4 @@ async def main(orgs, debug: bool, timelimit_days: int):
388
443
if args .clear_cache :
389
444
clear_cache ()
390
445
391
- asyncio .run (main (args .orgs , args .debug , args .timelimit_days ))
446
+ asyncio .run (main (args .orgs , args .debug , args .timelimit_days , args . config_file ))
0 commit comments