Skip to content

Commit d1bc1b9

Browse files
committed
update cli and add tui info to readme
1 parent 5302aff commit d1bc1b9

19 files changed

Lines changed: 1568 additions & 143 deletions

.readme_assets/CCF-Deadlines_TUI_2026-05-01T19_50_27_068657.svg

Lines changed: 306 additions & 0 deletions
Loading

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ English | [简体中文](https://translate.google.com/translate?sl=auto&tl=zh&u=
3131
<td align="center"><b><a href="https://github.com/superpung/swiftbar-ccfddl/">SwiftBar Plugin</a><br></b></td>
3232
</tr>
3333
<tr>
34-
<td align="center"><img src=".readme_assets/screenshot_pycli.png" width="280px"/></td>
34+
<td align="center"><img src=".readme_assets/screenshot_pycli.png" width="280px"/></td>
3535
<td align="center"><img src=".readme_assets/screenshot_raycast.png" width="280px"/></td>
3636
<td align="center"><img src="https://raw.githubusercontent.com/superpung/swiftbar-ccfddl/refs/heads/main/docs/preview.png" width="280px"/></td>
3737
</tr>
3838
<tr>
3939
<td align="center"><b><a href="https://github.com/ccfddl/ccf-deadlines/tree/main/extensions/ical">iCal Subscription</a><br></b></td>
4040
<td align="center"><b><a href="https://github.com/ccfddl/ccf-deadlines/tree/main/extensions/chrome">Chrome Extension</a><br></b></td>
41+
<td align="center"><b><a href="https://github.com/ccfddl/tui">TUI App</a><br></b></td>
4142
</tr>
4243
<tr>
4344
<td align="center"><img src=".readme_assets/screenshot_iCal.jpg" width="280px"/></td>
4445
<td align="center"><img src=".readme_assets/screenshot_ccf-ddl-tracker.png" width="280px"/></td>
46+
<td align="center"><img src=".readme_assets/CCF-Deadlines_TUI_2026-05-01T19_50_27_068657.svg" width="280px"/></td>
4547
</tr>
4648
</table>
4749

extensions/cli/ccfddl/__init__.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""CCFDDL CLI - Conference Deadline Tracker."""
2+
3+
__version__ = "0.2.0"
4+
__author__ = "0x4f5da2"
5+
6+
from ccfddl.utils import load_mapping, get_timezone, reverse_index, format_duration, parse_datetime_with_tz
7+
from ccfddl.models import (
8+
Conference,
9+
ConferenceYear,
10+
Timeline,
11+
Rank,
12+
Category,
13+
CATEGORIES,
14+
VALID_SUBS,
15+
get_category_by_sub,
16+
get_all_subs,
17+
is_valid_sub,
18+
)
19+
from ccfddl.fetch import fetch_conferences, process_conference_deadlines, filter_results, extract_alpha_id
20+
from ccfddl.output import output_table, output_json, list_categories, format_colored_duration
21+
22+
__all__ = [
23+
"__version__",
24+
"__author__",
25+
"load_mapping",
26+
"get_timezone",
27+
"reverse_index",
28+
"format_duration",
29+
"parse_datetime_with_tz",
30+
"Conference",
31+
"ConferenceYear",
32+
"Timeline",
33+
"Rank",
34+
"Category",
35+
"CATEGORIES",
36+
"VALID_SUBS",
37+
"get_category_by_sub",
38+
"get_all_subs",
39+
"is_valid_sub",
40+
"fetch_conferences",
41+
"process_conference_deadlines",
42+
"filter_results",
43+
"extract_alpha_id",
44+
"output_table",
45+
"output_json",
46+
"list_categories",
47+
"format_colored_duration",
48+
]

extensions/cli/ccfddl/__main__.py

Lines changed: 82 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,88 @@
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

12187
if __name__ == "__main__":
122-
main()
88+
main()

extensions/cli/ccfddl/convert_to_ical.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
def load_mapping(path: str = "conference/types.yml"):
1212
with open(path, encoding="utf-8") as f:
1313
types = yaml.safe_load(f)
14+
if types is None:
15+
return {}
1416
SUB_MAPPING = {}
1517
for types_data in types:
1618
SUB_MAPPING[types_data["sub"]] = types_data["name"]
@@ -220,6 +222,9 @@ def reverse_index(file_paths: list[str], subs: list[str]):
220222
with open(file_path, "r", encoding="utf-8") as f:
221223
conferences = yaml.safe_load(f)
222224

225+
if conferences is None:
226+
continue
227+
223228
for conf_data in conferences:
224229
sub = conf_data["sub"]
225230
rank = conf_data["rank"]

extensions/cli/ccfddl/convert_to_rss.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from datetime import datetime, timedelta, timezone
33
from email.utils import format_datetime
44

5-
from convert_to_ical import load_mapping, get_timezone, reverse_index
5+
from ccfddl.convert_to_ical import load_mapping, get_timezone, reverse_index
66

77
import yaml
88

@@ -31,6 +31,9 @@ def convert_to_rss(
3131
with open(file_path, "r", encoding="utf-8") as f:
3232
conferences = yaml.safe_load(f)
3333

34+
if conferences is None:
35+
continue
36+
3437
for conf_data in conferences:
3538
title = conf_data["title"]
3639
sub = conf_data["sub"]

0 commit comments

Comments
 (0)