1- import string
2- import requests
3- import yaml
4- from termcolor import colored
5- from argparse import ArgumentParser
6- from copy import deepcopy
7- from datetime import datetime
8- from tabulate import tabulate
9- from datetime import timezone
10-
11-
12- def parse_tz (tz ):
13- if tz == "AoE" :
14- return "-1200"
15- elif tz .startswith ("UTC-" ):
16- return "-{:04d}" .format (int (tz [4 :]))
17- elif tz .startswith ("UTC+" ):
18- return "+{:04d}" .format (int (tz [4 :]))
19- else :
20- return "+0000"
21-
22-
23- def parse_args ():
24- parser = ArgumentParser (description = "cli for ccfddl" )
25- parser .add_argument ("--conf" , type = str , nargs = '+' ,
26- help = "A list of conference ids you want to filter, e.g.: '--conf CVPR ICML'" )
27- parser .add_argument ("--sub" , type = str , nargs = '+' ,
28- help = "A list of subcategories ids you want to filter, e.g.: '--sub AI CG'" )
29- parser .add_argument ("--rank" , type = str , nargs = '+' ,
30- help = "A list of ranks you want to filter, e.g.: '--rank C N'" )
31- args = parser .parse_args ()
32- # Convert all arguments to lowercase
33- for arg_name in vars (args ):
34- arg_value = getattr (args , arg_name )
35- if arg_value :
36- setattr (args , arg_name , [arg .lower () for arg in arg_value ])
37- return args
38-
39-
40- def format_duraton (ddl_time : datetime , now : datetime ) -> str :
41- duration = ddl_time - now
42- months , days = duration .days // 30 , duration .days
43- hours , remainder = divmod (duration .seconds , 3600 )
44- minutes , seconds = divmod (remainder , 60 )
45-
46- day_word_str = "days" if days > 1 else "day "
47- # for alignment
48- months_str , days_str , = str (months ).zfill (2 ), str (days ).zfill (2 )
49- hours_str , minutes_str = str (hours ).zfill (2 ), str (minutes ).zfill (2 )
50-
51- if days < 1 :
52- return colored (f'{ hours_str } :{ minutes_str } :{ seconds } ' , "red" )
53- if days < 30 :
54- return colored (f'{ days_str } { day_word_str } , { hours_str } :{ minutes_str } ' , "yellow" )
55- if days < 100 :
56- return colored (f"{ days_str } { day_word_str } " , "blue" )
57- return colored (f"{ months_str } months" , "green" )
58-
59-
60- def main ():
1+ """CCFDDL CLI - Conference Deadline Tracker.
2+
3+ A command-line tool for viewing and filtering conference deadlines.
4+ """
5+
6+ import argparse
7+ from datetime import datetime , timezone
8+
9+ from ccfddl import __version__
10+ from ccfddl .fetch import fetch_conferences , filter_results , process_conference_deadlines
11+ from ccfddl .output import list_categories , output_json , output_table
12+
13+
14+ def parse_args () -> argparse .Namespace :
15+ """Parse command-line arguments."""
16+ parser = argparse .ArgumentParser (
17+ description = "CCFDDL CLI - Conference Deadline Tracker" ,
18+ epilog = "Example: ccfddl --conf CVPR ICML --sub AI --rank A" ,
19+ )
20+ parser .add_argument (
21+ "--version" ,
22+ action = "version" ,
23+ version = f"%(prog)s { __version__ } " ,
24+ )
25+ parser .add_argument (
26+ "--conf" ,
27+ type = str ,
28+ nargs = "+" ,
29+ help = "Filter by conference IDs (e.g., --conf CVPR ICML)" ,
30+ )
31+ parser .add_argument (
32+ "--sub" ,
33+ type = str ,
34+ nargs = "+" ,
35+ help = "Filter by subcategories (e.g., --sub AI CG)" ,
36+ )
37+ parser .add_argument (
38+ "--rank" ,
39+ type = str ,
40+ nargs = "+" ,
41+ help = "Filter by CCF ranks (e.g., --rank A B)" ,
42+ )
43+ parser .add_argument (
44+ "--json" ,
45+ action = "store_true" ,
46+ help = "Output in JSON format" ,
47+ )
48+ parser .add_argument (
49+ "--list-categories" ,
50+ action = "store_true" ,
51+ help = "List all categories" ,
52+ )
53+ parser .add_argument (
54+ "--url" ,
55+ type = str ,
56+ default = "https://ccfddl.github.io/conference/allconf.yml" ,
57+ help = "URL to fetch conference data (default: ccfddl.github.io)" ,
58+ )
59+ return parser .parse_args ()
60+
61+
62+ def main () -> None :
63+ """Main entry point."""
6164 args = parse_args ()
62- yml_str = requests .get (
63- "https://ccfddl.github.io/conference/allconf.yml" ).content .decode ("utf-8" )
64- all_conf = yaml .safe_load (yml_str )
65-
66- all_conf_ext = []
67- now = datetime .now (tz = timezone .utc )
68- for conf in all_conf :
69- for c in conf ["confs" ]:
70- cur_conf = deepcopy (conf )
71- cur_conf ["title" ] = cur_conf ["title" ] + str (c ["year" ])
72- cur_conf .update (c )
73- time_obj = None
74- tz = parse_tz (c ["timezone" ])
75- for d in c ["timeline" ]:
76- try :
77- cur_d = datetime .strptime (
78- d ["deadline" ] + " {}" .format (tz ), '%Y-%m-%d %H:%M:%S %z' )
79- if cur_d < now :
80- continue
81- if time_obj is None or cur_d < time_obj :
82- time_obj = cur_d
83- except Exception as e :
84- pass
85- if time_obj is not None :
86- cur_conf ["time_obj" ] = time_obj
87- if time_obj > now :
88- all_conf_ext .append (cur_conf )
8965
90- all_conf_ext = sorted (all_conf_ext , key = lambda x : x ['time_obj' ])
66+ if args .list_categories :
67+ list_categories ()
68+ return
9169
92- # This is not an elegant solution.
93- # The purpose is to keep the above logic untouched,
94- # return alpha id(conf name) without digits(year)
95- def alpha_id (with_digits : string ) -> string :
96- return '' .join (char for char in with_digits .lower () if char .isalpha ())
97-
98- table = [["Title" , "Sub" , "Rank" , "DDL" , "Link" ]]
99- # Filter intersection by args
100- for x in all_conf_ext :
101- skip = False
102- if args .conf and alpha_id (x ["id" ]) not in args .conf :
103- skip = True
104- if args .sub and alpha_id (x ["sub" ]) not in args .sub :
105- skip = True
106- if args .rank and alpha_id (x ["rank" ]) not in args .rank :
107- skip = True
108- if skip :
109- continue
110- table .append (
111- [x ["title" ],
112- x ["sub" ],
113- x ["rank" ],
114- format_duraton (x ["time_obj" ], now ),
115- x ["link" ]]
116- )
117-
118- print (tabulate (table , headers = 'firstrow' , tablefmt = 'fancy_grid' ))
70+ now = datetime .now (tz = timezone .utc )
71+ conferences = fetch_conferences (args .url )
72+ results = process_conference_deadlines (conferences , now )
73+
74+ filtered = filter_results (
75+ results ,
76+ args .conf ,
77+ args .sub ,
78+ args .rank ,
79+ )
80+
81+ if args .json :
82+ output_json (filtered )
83+ else :
84+ output_table (filtered , now )
11985
12086
12187if __name__ == "__main__" :
122- main ()
88+ main ()
0 commit comments