Merge pull request #47 from DARC-e-V/feat/cli

feat: implement CLI controls for 50ohm downloader / builder
This commit is contained in:
Tim Grelka
2026-02-15 17:58:38 +01:00
committed by GitHub
10 changed files with 245 additions and 62 deletions
-11
View File
@@ -1,11 +0,0 @@
kind: pipeline
type: docker
name: default
steps:
- name: 50ohm.de
image: ghcr.io/astral-sh/uv:python3.12-bookworm
environment:
commands:
- uv pip install --system --break-system-packages --requirements requirements.txt
- pytest --junitxml=./test-reports/junit.xml
+6 -19
View File
@@ -1,22 +1,9 @@
FROM python:3.12 FROM python:3.12-slim
# RUN apt-get update -qq && apt-get install -y \ WORKDIR /app
# wget \ COPY requirements.txt requirements.txt
# libswitch-perl \ RUN pip3 install -r requirements.txt
# gnupg \
# && apt-get clean && rm -rf /var/lib/apt/lists/*
#
# COPY ./texlive.profile /tmp/
# RUN cd /tmp && wget -qO- https://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz | tar xz \
# && /tmp/install-tl*/install-tl -profile /tmp/texlive.profile \
# && rm -rf /tmp/install-tl-*
#
# ENV PATH="/usr/local/texlive/bin/x86_64-linux:${PATH}"
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY --parents api assets latex renderer src templates main.py LICENSE ./
COPY --from=texlive/texlive:latest /usr/local/texlive /usr/local/texlive ENTRYPOINT ["python3", "main.py"]
RUN $(find /usr/local/texlive -name tlmgr) path add && \
# Temporary fix for ConTeXt (#30)
(sed -i '/package.loaded\["data-ini"\]/a if os.selfpath then environment.ownbin=lfs.symlinktarget(os.selfpath..io.fileseparator..os.selfname);environment.ownpath=environment.ownbin:match("^.*"..io.fileseparator) else environment.ownpath=kpse.new("luatex"):var_value("SELFAUTOLOC");environment.ownbin=environment.ownpath..io.fileseparator..(arg[-2] or arg[-1] or arg[0] or "luatex"):match("[^"..io.fileseparator.."]*$") end' /usr/bin/mtxrun.lua || true)
+47
View File
@@ -0,0 +1,47 @@
from typing import Annotated
import typer
import src.build as build
import src.config as config
from src.edition import Edition
app = typer.Typer()
@app.command()
def main(
edition: Annotated[list[Edition], typer.Option(help="Edition to build, can be specified multiple times.")] = [ # noqa: B006 -- default value is required for typer
Edition.n,
Edition.e,
Edition.a,
Edition.ne,
Edition.ea,
Edition.nea,
],
input: Annotated[str | None, typer.Option("--input", "-i", help="Content source directory.")] = None,
output: Annotated[str | None, typer.Option("--output", "-o", help="Destination directory to build to.")] = None,
render_editions: Annotated[bool, typer.Option(help="Skip building editions.")] = True,
render_solutions: Annotated[bool, typer.Option(help="Skip building solutions.")] = True,
) -> None:
conf = config.Config(content_path=input, build_path=output)
bd = build.Build(conf)
# Build surrounding website
bd.build_website()
if render_editions:
# Build individual editions
for e in edition:
bd.build_edition(e)
if render_solutions:
# Build solution pages
bd.build_solutions()
# Copy assets to build folder
bd.build_assets()
if __name__ == "__main__":
app()
+2 -1
View File
@@ -9,8 +9,9 @@ dependencies = [
"joblib==1.5.1", "joblib==1.5.1",
"mistletoe==1.4.0", "mistletoe==1.4.0",
"requests==2.32.4", "requests==2.32.4",
"rich>=14.2.0",
"tqdm==4.67.1", "tqdm==4.67.1",
"gitpython==3.1.46", "typer>=0.21.1",
] ]
[tool.ruff] [tool.ruff]
+11
View File
@@ -0,0 +1,11 @@
import sys
from mistletoe import cli
def main() -> None:
cli.main(sys.argv[1:])
if __name__ == "__main__":
main()
+29 -9
View File
@@ -1,20 +1,40 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
certifi==2025.6.15 certifi==2025.6.15
# via requests
charset-normalizer==3.4.2 charset-normalizer==3.4.2
coverage==7.9.2 # via requests
click==8.3.1
# via typer
idna==3.10 idna==3.10
iniconfig==2.1.0 # via requests
jinja2==3.1.6 jinja2==3.1.6
# via 50ohm-de (pyproject.toml)
joblib==1.5.1 joblib==1.5.1
# via 50ohm-de (pyproject.toml)
markdown-it-py==4.0.0
# via rich
markupsafe==3.0.2 markupsafe==3.0.2
# via jinja2
mdurl==0.1.2
# via markdown-it-py
mistletoe==1.4.0 mistletoe==1.4.0
packaging==25.0 # via 50ohm-de (pyproject.toml)
pluggy==1.6.0
pygments==2.19.2 pygments==2.19.2
pytest==8.4.1 # via rich
pytest-cov==6.2.1
requests==2.32.4 requests==2.32.4
ruff==0.12.2 # via 50ohm-de (pyproject.toml)
rich==14.3.2
# via
# 50ohm-de (pyproject.toml)
# typer
shellingham==1.5.4
# via typer
tqdm==4.67.1 tqdm==4.67.1
ty==0.0.1a13 # via 50ohm-de (pyproject.toml)
typer==0.21.1
# via 50ohm-de (pyproject.toml)
typing-extensions==4.15.0
# via typer
urllib3==2.5.0 urllib3==2.5.0
gitpython==3.1.46 # via requests
+52 -18
View File
@@ -6,6 +6,7 @@ from pathlib import Path
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from mistletoe import Document from mistletoe import Document
from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeRemainingColumn
from tqdm import tqdm from tqdm import tqdm
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
@@ -18,7 +19,7 @@ class Build:
def __init__(self, config: Config): def __init__(self, config: Config):
self.config = config self.config = config
self.env = Environment(loader=FileSystemLoader("templates/")) self.env = Environment(loader=FileSystemLoader(self.config.p_templates))
self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers
self.questions = self.__parse_katalog() self.questions = self.__parse_katalog()
@@ -152,7 +153,7 @@ class Build:
except FileNotFoundError: except FileNotFoundError:
tqdm.write(f"\033[31mPhoto #{id} not found\033[0m") tqdm.write(f"\033[31mPhoto #{id} not found\033[0m")
def __build_chapter(self, edition, edition_name, number, chapter, next_chapter=None): def __build_chapter_index(self, edition, edition_name, number, chapter, next_chapter=None):
chapter_template = self.env.get_template("html/chapter.html") chapter_template = self.env.get_template("html/chapter.html")
next_chapter_template = self.env.get_template("html/next_chapter.html") next_chapter_template = self.env.get_template("html/next_chapter.html")
with (self.config.p_build / f"{edition}_chapter_{chapter['ident']}.html").open("w") as file: with (self.config.p_build / f"{edition}_chapter_{chapter['ident']}.html").open("w") as file:
@@ -241,7 +242,9 @@ class Build:
result = self.__build_page(result, course_wrapper=True) result = self.__build_page(result, course_wrapper=True)
file.write(result) file.write(result)
def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter, chapter_number=None): def __build_chapter_slidedeck(
self, edition, chapter, sections, next_chapter, chapter_number=None, progress: Progress = None
):
with (self.config.p_build / f"{edition}_slide_{chapter['ident']}.html").open("w") as file: with (self.config.p_build / f"{edition}_slide_{chapter['ident']}.html").open("w") as file:
slide_template = self.env.get_template("slide/slide.html") slide_template = self.env.get_template("slide/slide.html")
help_template = self.env.get_template("slide/help.html") help_template = self.env.get_template("slide/help.html")
@@ -270,7 +273,10 @@ class Build:
result += "</section>\n" result += "</section>\n"
section_counter = 0 section_counter = 0
for section in sections: slides_task = progress.add_task("Rendering slides ...")
for section in progress.track(sections, task_id=slides_task):
progress.update(slides_task, description=f"Rendering slides of {section['title']}")
if section["slide"] is None: if section["slide"] is None:
continue continue
@@ -293,6 +299,8 @@ class Build:
tmp += renderer.render(doc) tmp += renderer.render(doc)
result += f"<section>{tmp}</section>\n" result += f"<section>{tmp}</section>\n"
progress.remove_task(slides_task)
result += next_template.render( result += next_template.render(
edition=edition, edition=edition,
next_chapter=next_chapter, next_chapter=next_chapter,
@@ -332,17 +340,45 @@ class Build:
result = self.__build_page(result) result = self.__build_page(result)
file.write(result) file.write(result)
def build_edition(self, edition): def build_edition(self, edition: str):
self.config.p_build.mkdir(exist_ok=True) self.config.p_build.mkdir(exist_ok=True)
edition = edition.upper() edition = edition.upper()
with (self.config.p_data_toc / f"{edition}.json").open() as file: with (
(self.config.p_data_toc / f"{edition}.json").open() as file,
Progress(
TaskProgressColumn(),
BarColumn(),
TimeRemainingColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
) as progress,
):
chapter_task = progress.add_task(f"Building edition {edition} ...")
book = json.load(file) book = json.load(file)
edition_name = book["title"]
# Create index pages for chapters and slides.
self.__build_book_index(book)
self.__build_slide_index(book)
chapters = book["chapters"] chapters = book["chapters"]
for chapter in chapters: for chapter_number, chapter in enumerate(progress.track(chapters, task_id=chapter_task), 1):
for section in chapter["sections"]: progress.update(chapter_task, description=f"Building edition {edition}: Chapter {chapter['title']}")
# Determine next chapter for navigation (None if this is the last chapter)
next_chapter = chapters[chapter_number] if chapter_number < len(chapters) else None
self.__build_chapter_index(edition, edition_name, chapter_number, chapter, next_chapter)
# Open, parse and render each section.
section_task = progress.add_task(description="Rendering sections ...")
for section_number, section in enumerate(progress.track(chapter["sections"], task_id=section_task), 1):
progress.update(section_task, description=f"Rendering section {section['title']}")
# Read section and slide content from the corresponding files.
ident = section["ident"] ident = section["ident"]
section["content"] = None section["content"] = None
section["slide"] = None section["slide"] = None
@@ -353,16 +389,6 @@ class Build:
section_content = sfile.read() section_content = sfile.read()
section["slide"] = section_content section["slide"] = section_content
edition_name = book["title"]
self.__build_book_index(book)
self.__build_slide_index(book)
for chapter_number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"), 1):
next_chapter = chapters[chapter_number] if chapter_number < len(chapters) else None
self.__build_chapter(edition, edition_name, chapter_number, chapter, next_chapter)
self.__build_chapter_slidedeck(edition, chapter, chapter["sections"], next_chapter, chapter_number)
for section_number, section in enumerate(chapter["sections"], 1):
tqdm.write(f"Rendering section {section['title']}")
next_section = ( next_section = (
chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None
) )
@@ -377,6 +403,14 @@ class Build:
chapter_number, chapter_number,
) )
progress.remove_task(section_task)
# Build the slidedeck after all sections have been processed, as they require the files to be read.
self.__build_chapter_slidedeck(
edition, chapter, chapter["sections"], next_chapter, chapter_number, progress
)
progress.remove_task(chapter_task)
def build_assets(self): def build_assets(self):
self.config.p_build.mkdir(exist_ok=True) self.config.p_build.mkdir(exist_ok=True)
shutil.copytree( shutil.copytree(
+4 -3
View File
@@ -6,14 +6,14 @@ from pathlib import Path
class Config: class Config:
__env_prefix = "OHM" __env_prefix = "OHM"
def __init__(self): def __init__(self, content_path: str = None, build_path: str = None):
if os.path.isfile("config/config.json"): if os.path.isfile("config/config.json"):
with open("config/config.json") as file: with open("config/config.json") as file:
self.config = json.load(file) self.config = json.load(file)
else: else:
self.config = {} self.config = {}
self.p_data = Path(self.get_config_value("content_path", "/var/www/content")) self.p_data = Path(self.get_config_value("input", "content") if content_path is None else content_path)
self.p_data_toc = self.p_data / "toc" self.p_data_toc = self.p_data / "toc"
@@ -31,12 +31,13 @@ class Config:
self.p_data_fragenkatalog = self.p_data_questions / "fragenkatalog3b.json" self.p_data_fragenkatalog = self.p_data_questions / "fragenkatalog3b.json"
self.p_data_metadata = self.p_data_questions / "metadata3b.json" self.p_data_metadata = self.p_data_questions / "metadata3b.json"
self.p_build = Path("./build") self.p_build = Path(self.get_config_value("output", "./build") if build_path is None else build_path)
self.p_build_photos = self.p_build / "photos" self.p_build_photos = self.p_build / "photos"
self.p_build_pictures = self.p_build / "pictures" self.p_build_pictures = self.p_build / "pictures"
self.p_build_assets = self.p_build / "assets" self.p_build_assets = self.p_build / "assets"
self.p_assets = Path("./assets") self.p_assets = Path("./assets")
self.p_templates = Path("./templates")
def get_config_value(self, key: str, default=None): def get_config_value(self, key: str, default=None):
if key in self.config: if key in self.config:
+10
View File
@@ -0,0 +1,10 @@
from enum import Enum
class Edition(str, Enum):
n = "N"
e = "E"
a = "A"
ne = "NE"
ea = "EA"
nea = "NEA"
Generated
+84 -1
View File
@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
[[package]] [[package]]
@@ -11,7 +11,9 @@ dependencies = [
{ name = "joblib" }, { name = "joblib" },
{ name = "mistletoe" }, { name = "mistletoe" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typer" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -28,7 +30,9 @@ requires-dist = [
{ name = "joblib", specifier = "==1.5.1" }, { name = "joblib", specifier = "==1.5.1" },
{ name = "mistletoe", specifier = "==1.4.0" }, { name = "mistletoe", specifier = "==1.4.0" },
{ name = "requests", specifier = "==2.32.4" }, { name = "requests", specifier = "==2.32.4" },
{ name = "rich", specifier = ">=14.2.0" },
{ name = "tqdm", specifier = "==4.67.1" }, { name = "tqdm", specifier = "==4.67.1" },
{ name = "typer", specifier = ">=0.21.1" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -83,6 +87,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
] ]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -173,6 +189,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" },
] ]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "3.0.2" version = "3.0.2"
@@ -211,6 +239,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
] ]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]] [[package]]
name = "mistletoe" name = "mistletoe"
version = "1.4.0" version = "1.4.0"
@@ -292,6 +329,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
] ]
[[package]]
name = "rich"
version = "14.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.12.2" version = "0.12.2"
@@ -317,6 +367,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" }, { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
] ]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.67.1" version = "4.67.1"
@@ -354,6 +413,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/62/fe6cc92db2aa1a344937e5313ba3bbdaed24e54332b8a12fe28a92d314ce/ty-0.0.1a13-py3-none-win_arm64.whl", hash = "sha256:5f2237bb301078d51d006170992abb1e5f3b37cec47a11e6db416a76346f60b5", size = 6727050, upload-time = "2025-07-02T17:52:12.874Z" }, { url = "https://files.pythonhosted.org/packages/c7/62/fe6cc92db2aa1a344937e5313ba3bbdaed24e54332b8a12fe28a92d314ce/ty-0.0.1a13-py3-none-win_arm64.whl", hash = "sha256:5f2237bb301078d51d006170992abb1e5f3b37cec47a11e6db416a76346f60b5", size = 6727050, upload-time = "2025-07-02T17:52:12.874Z" },
] ]
[[package]]
name = "typer"
version = "0.21.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.5.0" version = "2.5.0"