refactoring stage 7

- implement StorageManager list_entries
- fix show
- fix find
- fix some minor
This commit is contained in:
2026-05-06 02:22:28 +09:00
parent 7706721eb1
commit d4aaefa130

255
run.py
View File

@@ -439,6 +439,65 @@ class StorageManager:
/ f"{filename}.{lang.value}" / f"{filename}.{lang.value}"
) )
def list_entries(
self,
location: str | None = None,
lang: Language | None = None,
completed: bool | None = None,
) -> list[tuple[str, str, str, bool]]:
"""저장된 파일 목록을 반환합니다.
Args:
location: location 필터 (None이면 전체)
lang: language 필터 (None이면 전체)
completed: completed 필터 (None이면 both, True이면 completed만, False이면 uncompleted만)
Returns:
list[tuple[str, str, str, bool]]: (location, lang, filename, is_completed)
"""
result: list[tuple[str, str, str, bool]] = []
if not STORAGE_DIR.is_dir():
return result
for loc_dir in sorted(STORAGE_DIR.iterdir()):
if not loc_dir.is_dir():
continue
loc_name = loc_dir.name
if location and loc_name != location:
continue
for lang_dir in sorted(loc_dir.iterdir()):
if not lang_dir.is_dir():
continue
lang_name = lang_dir.name
if Language.convert_ext(lang_name) is Language.UNDEFINED:
continue
if lang and lang_name != lang.value:
continue
for f in sorted(
lang_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem)
):
if f.is_file():
result.append((loc_name, lang_name, f.stem, False))
completed_dir = lang_dir / "completed"
if completed_dir.is_dir():
for f in sorted(
completed_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem)
):
if f.is_file():
result.append((loc_name, lang_name, f.stem, True))
if completed is not None:
result = [e for e in result if e[3] == completed]
return result
# ======================= # =======================
# MAIN CLI LOGIC # MAIN CLI LOGIC
@@ -547,8 +606,7 @@ def run(lang: str, testcase: tuple[str, ...], verbose: bool):
) )
# 테케 분석 # 테케 분석
testcase: list[str] = list(testcase) tcs: list[int] = _parse_range_string_list(list(testcase))
tcs: list[int] = _parse_range_string_list(testcase)
# build # build
try: try:
@@ -657,7 +715,7 @@ def load(target: str, force: bool):
@click.command(name="export") @click.command(name="export")
@click.argument("from", "from_", type=str, nargs=-1) @click.argument("from_", type=str, nargs=-1)
@click.option( @click.option(
"--all", "all_", default=False, is_flag=True, help="Export all files in the space" "--all", "all_", default=False, is_flag=True, help="Export all files in the space"
) )
@@ -810,91 +868,41 @@ def init(count: int):
@click.command(name="show") @click.command(name="show")
@click.argument("filter", type=str, default=None, required=False) @click.argument("filter_", type=str, default=None, required=False)
@click.option( @click.option("--completed", "completed_mode", flag_value="completed", default=True)
"--completed/--no-completed", @click.option("--uncompleted", "completed_mode", flag_value="uncompleted")
"-c/-nc", @click.option("--both-completed", "completed_mode", flag_value="both")
default=False, def show(filter_: str | None, completed_mode: str):
help="Filter by completed status (default: uncompleted only)", """Show all stored problems in storage directory.
)
@click.option(
"--all",
"show_all",
is_flag=True,
default=False,
help="Show all problems regardless of completed status",
)
def show(filter: str | None, completed: bool, show_all: bool):
"""
Show all stored problems in storage directory.
FILTER format (optional): FILTER format (optional):
location e.g. zeta location e.g. zeta
location/lang e.g. zeta/rs location/lang e.g. zeta/rs
/lang e.g. /py /lang e.g. /py
FILTER MODE (default: --completed):
--completed show completed only
--uncompleted show uncompleted only
--both-completed show both completed and uncompleted
""" """
location = None completed = {"completed": True, "uncompleted": False, "both": None}[completed_mode]
lang = None
if filter:
parts = filter.split("/", 1)
if len(parts) == 2:
location = parts[0] or None
lang = parts[1] or None
else:
location = parts[0]
storage = pathlib.Path(STORAGE_DIR) location = lang = None
if not storage.is_dir(): if filter_:
raise click.ClickException(f"Storage directory '{STORAGE_DIR}' not found") parts = filter_.split("/", 1)
location = parts[0] if parts[0].strip() else None
lang = parts[1] if len(parts) == 2 else None
# Collect all files entries = StorageManager().list_entries(
entries = [] # (location, language, filename, is_completed) location=location,
lang=Language.convert_name(lang) if lang else None,
for loc_dir in sorted(storage.iterdir()): completed=completed,
if not loc_dir.is_dir(): )
continue
loc_name = loc_dir.name
if location and loc_name != location:
continue
for lang_dir in sorted(loc_dir.iterdir()):
if not lang_dir.is_dir():
continue
lang_name = lang_dir.name
if (
lang
and lang_name != Language.convert_name(lang).value
and lang_name != lang
):
continue
# uncompleted files
for f in sorted(
lang_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem)
):
if f.is_file():
entries.append((loc_name, lang_name, f.stem, False))
# completed files
completed_dir = lang_dir / "completed"
if completed_dir.is_dir():
for f in sorted(
completed_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem)
):
if f.is_file():
entries.append((loc_name, lang_name, f.stem, True))
# Filter by completed status
if not show_all:
entries = [e for e in entries if e[3] == completed]
if not entries: if not entries:
click.echo("No problems found.") click.echo(" - ")
return return
# Display
total = len(entries) total = len(entries)
completed_count = sum(1 for e in entries if e[3]) completed_count = sum(1 for e in entries if e[3])
uncompleted_count = total - completed_count uncompleted_count = total - completed_count
@@ -906,7 +914,6 @@ def show(filter: str | None, completed: bool, show_all: bool):
) )
click.echo() click.echo()
# Group by location
current_loc = None current_loc = None
current_lang = None current_lang = None
for loc_name, lang_name, file_name, is_completed in entries: for loc_name, lang_name, file_name, is_completed in entries:
@@ -928,90 +935,64 @@ def show(filter: str | None, completed: bool, show_all: bool):
@click.command(name="find") @click.command(name="find")
@click.argument("keyword", type=str) @click.argument("filter_", type=str)
@click.option( @click.option(
"--completed/--no-completed", "--completed/--no-completed",
"-c/-nc", "-c/-nc",
default=None, default=None,
help="Filter by completed status (default: all)", help="Filter by completed status (default: all)",
) )
def find(keyword: str, completed: bool | None): def find(filter_: str, completed: bool | None):
""" """
Find problems by keyword in storage. Find problems by filter in storage.
KEYWORD format: FILTER format:
keyword e.g. 2447 filter e.g. 2447
keyword.ext e.g. 2447.py (filters by language) filter.ext e.g. 2447.py (filters by language)
.ext e.g. .py (all files of a language) .ext e.g. .py (all files of a language)
location/keyword e.g. zeta/2447 location/filter e.g. zeta/2447
location/keyword.ext e.g. zeta/2447.py location/filter.ext e.g. zeta/2447.py
location/.ext e.g. zeta/.py location/.ext e.g. zeta/.py
""" """
location = None location: str | None = None
lang = None filter_base: str | None = None
if "/" in keyword: lang: Language | None = None
parts = keyword.split("/", 1)
if "/" in filter_:
parts = filter_.split("/", 1)
location = parts[0] or None location = parts[0] or None
keyword = parts[1] filter_ = parts[1]
else:
location = None
# Extract language from extension if "." in filter_:
if "." in keyword: filter_base, ext = filter_.rsplit(".", 1)
keyword_base, ext = keyword.rsplit(".", 1)
lang_candidate = Language.convert_ext(ext) lang_candidate = Language.convert_ext(ext)
if lang_candidate is not Language.UNDEFINED: if lang_candidate is not Language.UNDEFINED:
lang = lang_candidate.value lang = lang_candidate
filter_base = None if filter_base == "" else filter_base
else: else:
keyword_base = keyword filter_base = filter_
else: else:
keyword_base = keyword filter_base = filter_
storage = pathlib.Path(STORAGE_DIR)
if not storage.is_dir():
raise click.ClickException(f"Storage directory '{STORAGE_DIR}' not found")
entries = [] # (location, language, filename, is_completed)
for loc_dir in sorted(storage.iterdir()):
if not loc_dir.is_dir():
continue
loc_name = loc_dir.name
if location and loc_name != location:
continue
for lang_dir in sorted(loc_dir.iterdir()):
if not lang_dir.is_dir():
continue
lang_name = lang_dir.name
if lang and lang_name != lang:
continue
for f in lang_dir.iterdir():
if f.is_file() and keyword_base.lower() in f.stem.lower():
entries.append((loc_name, lang_name, f.stem, False))
completed_dir = lang_dir / "completed"
if completed_dir.is_dir():
for f in completed_dir.iterdir():
if f.is_file() and keyword_base.lower() in f.stem.lower():
entries.append((loc_name, lang_name, f.stem, True))
if completed is not None:
entries = [e for e in entries if e[3] == completed]
entries = StorageManager().list_entries(
location=location, lang=lang, completed=completed
)
if filter_base is not None:
entries = [e for e in entries if filter_base.lower() in e[2].lower()]
entries.sort(key=lambda e: (e[0], e[1], _natural_sort_key(e[2]))) entries.sort(key=lambda e: (e[0], e[1], _natural_sort_key(e[2])))
if not entries: if not entries:
click.echo("No problems found.") click.echo("No problems found.")
return return
total = len(entries) total_count = len(entries)
completed_count = sum(1 for e in entries if e[3]) completed_count = sum(1 for e in entries if e[3])
uncompleted_count = total - completed_count uncompleted_count = sum(1 for e in entries if not e[3])
click.secho( click.secho(
f"Found: {total} (completed: {completed_count}, uncompleted: {uncompleted_count})", f"Found: {total_count} (completed: {completed_count}, uncompleted: {uncompleted_count})",
fg="cyan", fg="cyan",
bold=True, bold=True,
) )
@@ -1039,7 +1020,7 @@ def find(keyword: str, completed: bool | None):
@click.command(name="fetchprob") @click.command(name="fetchprob")
@click.argument("target", type=str, nargs=1, required=True) @click.argument("target", type=str, nargs=1, required=True)
@click.option("--force", "-f", is_flag=True, help="Overwrite existing HTML files") @click.option("--force", "-f", is_flag=True, help="Overwrite existing static files")
def fetchprob(target: str, force: bool): def fetchprob(target: str, force: bool):
"""(disabled) Fetch BOJ problem HTML - BOJ is closing, keeping for future multi-location support.""" """(disabled) Fetch BOJ problem HTML - BOJ is closing, keeping for future multi-location support."""
del target, force del target, force