Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 306 additions & 0 deletions .readme_assets/CCF-Deadlines_TUI_2026-05-01T19_50_27_068657.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,19 @@ English | [简体中文](https://translate.google.com/translate?sl=auto&tl=zh&u=
<td align="center"><b><a href="https://github.com/superpung/swiftbar-ccfddl/">SwiftBar Plugin</a><br></b></td>
</tr>
<tr>
<td align="center"><img src=".readme_assets/screenshot_pycli.png" width="280px"/></td>
<td align="center"><img src=".readme_assets/screenshot_pycli.png" width="280px"/></td>
<td align="center"><img src=".readme_assets/screenshot_raycast.png" width="280px"/></td>
<td align="center"><img src="https://raw.githubusercontent.com/superpung/swiftbar-ccfddl/refs/heads/main/docs/preview.png" width="280px"/></td>
</tr>
<tr>
<td align="center"><b><a href="https://github.com/ccfddl/ccf-deadlines/tree/main/extensions/ical">iCal Subscription</a><br></b></td>
<td align="center"><b><a href="https://github.com/ccfddl/ccf-deadlines/tree/main/extensions/chrome">Chrome Extension</a><br></b></td>
<td align="center"><b><a href="https://github.com/ccfddl/tui">TUI App</a><br></b></td>
</tr>
<tr>
<td align="center"><img src=".readme_assets/screenshot_iCal.jpg" width="280px"/></td>
<td align="center"><img src=".readme_assets/screenshot_ccf-ddl-tracker.png" width="280px"/></td>
<td align="center"><img src=".readme_assets/CCF-Deadlines_TUI_2026-05-01T19_50_27_068657.svg" width="280px"/></td>
</tr>
</table>

Expand Down
48 changes: 48 additions & 0 deletions extensions/cli/ccfddl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""CCFDDL CLI - Conference Deadline Tracker."""

__version__ = "0.2.0"
__author__ = "0x4f5da2"

from ccfddl.utils import load_mapping, get_timezone, reverse_index, format_duration, parse_datetime_with_tz
from ccfddl.models import (
Conference,
ConferenceYear,
Timeline,
Rank,
Category,
CATEGORIES,
VALID_SUBS,
get_category_by_sub,
get_all_subs,
is_valid_sub,
)
from ccfddl.fetch import fetch_conferences, process_conference_deadlines, filter_results, extract_alpha_id
from ccfddl.output import output_table, output_json, list_categories, format_colored_duration

__all__ = [
"__version__",
"__author__",
"load_mapping",
"get_timezone",
"reverse_index",
"format_duration",
"parse_datetime_with_tz",
"Conference",
"ConferenceYear",
"Timeline",
"Rank",
"Category",
"CATEGORIES",
"VALID_SUBS",
"get_category_by_sub",
"get_all_subs",
"is_valid_sub",
"fetch_conferences",
"process_conference_deadlines",
"filter_results",
"extract_alpha_id",
"output_table",
"output_json",
"list_categories",
"format_colored_duration",
]
196 changes: 81 additions & 115 deletions extensions/cli/ccfddl/__main__.py
Original file line number Diff line number Diff line change
@@ -1,121 +1,87 @@
import string
import requests
import yaml
from termcolor import colored
from argparse import ArgumentParser
from copy import deepcopy
from datetime import datetime
from tabulate import tabulate
from datetime import timezone


def parse_tz(tz):
if tz == "AoE":
return "-1200"
elif tz.startswith("UTC-"):
return "-{:04d}".format(int(tz[4:]))
elif tz.startswith("UTC+"):
return "+{:04d}".format(int(tz[4:]))
else:
return "+0000"


def parse_args():
parser = ArgumentParser(description="cli for ccfddl")
parser.add_argument("--conf", type=str, nargs='+',
help="A list of conference ids you want to filter, e.g.: '--conf CVPR ICML'")
parser.add_argument("--sub", type=str, nargs='+',
help="A list of subcategories ids you want to filter, e.g.: '--sub AI CG'")
parser.add_argument("--rank", type=str, nargs='+',
help="A list of ranks you want to filter, e.g.: '--rank C N'")
args = parser.parse_args()
# Convert all arguments to lowercase
for arg_name in vars(args):
arg_value = getattr(args, arg_name)
if arg_value:
setattr(args, arg_name, [arg.lower() for arg in arg_value])
return args


def format_duraton(ddl_time: datetime, now: datetime) -> str:
duration = ddl_time - now
months, days= duration.days // 30, duration.days
hours, remainder= divmod(duration.seconds, 3600)
minutes, seconds = divmod(remainder, 60)

day_word_str = "days" if days > 1 else "day "
# for alignment
months_str, days_str, = str(months).zfill(2), str(days).zfill(2)
hours_str, minutes_str = str(hours).zfill(2), str(minutes).zfill(2)

if days < 1:
return colored(f'{hours_str}:{minutes_str}:{seconds}', "red")
if days < 30:
return colored(f'{days_str} {day_word_str}, {hours_str}:{minutes_str}', "yellow")
if days < 100:
return colored(f"{days_str} {day_word_str}", "blue")
return colored(f"{months_str} months", "green")


def main():
"""CCFDDL CLI - Conference Deadline Tracker.

A command-line tool for viewing and filtering conference deadlines.
"""

import argparse
from datetime import datetime, timezone

from ccfddl import __version__
from ccfddl.fetch import fetch_conferences, filter_results, process_conference_deadlines
from ccfddl.output import list_categories, output_json, output_table


def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="CCFDDL CLI - Conference Deadline Tracker",
epilog="Example: ccfddl --conf CVPR ICML --sub AI --rank A",
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {__version__}",
)
parser.add_argument(
"--conf",
type=str,
nargs="+",
help="Filter by conference IDs (e.g., --conf CVPR ICML)",
)
parser.add_argument(
"--sub",
type=str,
nargs="+",
help="Filter by subcategories (e.g., --sub AI CG)",
)
parser.add_argument(
"--rank",
type=str,
nargs="+",
help="Filter by CCF ranks (e.g., --rank A B)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
parser.add_argument(
"--list-categories",
action="store_true",
help="List all categories",
)
parser.add_argument(
"--url",
type=str,
default="https://ccfddl.github.io/conference/allconf.yml",
help="URL to fetch conference data (default: ccfddl.github.io)",
)
return parser.parse_args()


def main() -> None:
"""Main entry point."""
args = parse_args()
yml_str = requests.get(
"https://ccfddl.github.io/conference/allconf.yml").content.decode("utf-8")
all_conf = yaml.safe_load(yml_str)

all_conf_ext = []
now = datetime.now(tz=timezone.utc)
for conf in all_conf:
for c in conf["confs"]:
cur_conf = deepcopy(conf)
cur_conf["title"] = cur_conf["title"] + str(c["year"])
cur_conf.update(c)
time_obj = None
tz = parse_tz(c["timezone"])
for d in c["timeline"]:
try:
cur_d = datetime.strptime(
d["deadline"] + " {}".format(tz), '%Y-%m-%d %H:%M:%S %z')
if cur_d < now:
continue
if time_obj is None or cur_d < time_obj:
time_obj = cur_d
except Exception as e:
pass
if time_obj is not None:
cur_conf["time_obj"] = time_obj
if time_obj > now:
all_conf_ext.append(cur_conf)

all_conf_ext = sorted(all_conf_ext, key=lambda x: x['time_obj'])
if args.list_categories:
list_categories()
return

# This is not an elegant solution.
# The purpose is to keep the above logic untouched,
# return alpha id(conf name) without digits(year)
def alpha_id(with_digits: string) -> string:
return ''.join(char for char in with_digits.lower() if char.isalpha())

table = [["Title", "Sub", "Rank", "DDL", "Link"]]
# Filter intersection by args
for x in all_conf_ext:
skip = False
if args.conf and alpha_id(x["id"]) not in args.conf:
skip = True
if args.sub and alpha_id(x["sub"]) not in args.sub:
skip = True
if args.rank and alpha_id(x["rank"]) not in args.rank:
skip = True
if skip:
continue
table.append(
[x["title"],
x["sub"],
x["rank"],
format_duraton(x["time_obj"], now),
x["link"]]
)

print(tabulate(table, headers='firstrow', tablefmt='fancy_grid'))
now = datetime.now(tz=timezone.utc)
conferences = fetch_conferences(args.url)
results = process_conference_deadlines(conferences, now)

filtered = filter_results(
results,
args.conf,
args.sub,
args.rank,
)

if args.json:
output_json(filtered)
else:
output_table(filtered, now)


if __name__ == "__main__":
Expand Down
5 changes: 5 additions & 0 deletions extensions/cli/ccfddl/convert_to_ical.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
def load_mapping(path: str = "conference/types.yml"):
with open(path, encoding="utf-8") as f:
types = yaml.safe_load(f)
if types is None:
return {}
SUB_MAPPING = {}
for types_data in types:
SUB_MAPPING[types_data["sub"]] = types_data["name"]
Expand Down Expand Up @@ -220,6 +222,9 @@ def reverse_index(file_paths: list[str], subs: list[str]):
with open(file_path, "r", encoding="utf-8") as f:
conferences = yaml.safe_load(f)

if conferences is None:
continue

for conf_data in conferences:
sub = conf_data["sub"]
rank = conf_data["rank"]
Expand Down
5 changes: 4 additions & 1 deletion extensions/cli/ccfddl/convert_to_rss.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime, timedelta, timezone
from email.utils import format_datetime

from convert_to_ical import load_mapping, get_timezone, reverse_index
from ccfddl.convert_to_ical import load_mapping, get_timezone, reverse_index

import yaml

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

if conferences is None:
continue

for conf_data in conferences:
title = conf_data["title"]
sub = conf_data["sub"]
Expand Down
Loading
Loading