From 4daf7015ef60d76f284c0c156b17538b26870f0a Mon Sep 17 00:00:00 2001 From: Ahmad Hakim Date: Sat, 8 Aug 2026 16:08:31 +0200 Subject: [PATCH] feature(dev): new system --- README.md | 4 + assets/docs/getting-started/dev.md | 138 ++++++++----- assets/llms.txt | 2 +- assets/social/dev.webp | Bin 7218 -> 7160 bytes cli/dev.py | 314 +++++++++++++++++++++++++++++ dev.py | 237 ---------------------- docs/getting_started/dev.md | 138 ++++++++----- pyproject.toml | 2 + uv.lock | 24 +++ 9 files changed, 527 insertions(+), 332 deletions(-) create mode 100644 cli/dev.py delete mode 100644 dev.py diff --git a/README.md b/README.md index 1fc1210..60b5dda 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Visit the [Introduction](https://native.buridan.dev/docs/getting-started/introdu Visit the [CLI](https://native.buridan.dev/docs/getting-started/cli) page for information on how to use the CLI tool. +## Dev (CLI) + +Visit the [dev](https://native.buridan.dev/docs/getting-started/dev) page for information on how to use the local dev environment when working with this repo. + ## Components Visit [Components](https://native.buridan.dev/components) section to see all available components. diff --git a/assets/docs/getting-started/dev.md b/assets/docs/getting-started/dev.md index 8689cc1..20d250f 100644 --- a/assets/docs/getting-started/dev.md +++ b/assets/docs/getting-started/dev.md @@ -1,73 +1,119 @@ --- -title: "dev.py" -description: "Interactive development CLI for selecting and running Buridan documentation pages locally." +title: "dev" +description: "Interactive and command-line tool for selecting and running Buridan documentation pages locally." order: 4 --- -# dev.py +# dev -The `dev.py` script is an interactive CLI tool used to control which documentation pages are loaded during local development. - -It replaces manual environment variable setup and makes it easier to selectively run parts of the documentation site while developing. +The `dev` command is a CLI tool used to control which documentation pages are loaded during local development. +It replaces manual environment variable setup and makes it easier to selectively run parts of the documentation site while developing. Pages can be picked interactively through a fuzzy search prompt, or loaded directly by name. # Requirements -This script uses [questionary](https://github.com/tmbo/questionary) for interactive prompts. - -Install it using your project setup: +This tool uses [questionary](https://github.com/tmbo/questionary) for the environment/confirm prompts, and [InquirerPy](https://github.com/kazhala/InquirerPy) for fuzzy-searchable page selection. Both are declared in the `dev` dependency group, so they're installed automatically. -```uv -uv add questionary +```bash +uv sync ``` -You can also add it manually to your `pyproject.toml`: +This also registers the `dev` command itself, via the `[project.scripts]` entry in `pyproject.toml`: ```pyproject.toml -dependencies = ["questionary"] +[project.scripts] +dev = "cli.dev:main" ``` -# Overview +# Usage -Instead of running the full documentation build every time, `dev.py` lets you: +Run the command from your project root: -- Select specific pages -- Select entire sections -- Select pages within specific sections -- Run the app in dev or prod mode +```bash +uv run dev +``` -It then automatically configures the correct environment variables and starts the Reflex server. +With no arguments, this starts the interactive flow: choose an environment, then fuzzy-search and multi-select pages. -# Usage +To skip the prompts entirely, pass pages, sections, or a mode directly as arguments — see below. -Run the script from your project root to start the interactive flow: +# Command Reference -```uv -uv run dev.py +```text +uv run dev [pages...] [--section SECTION [SECTION ...]] [--prod] ``` -# Flow +| Argument | Description | +| ----------------- | --------------------------------------------------------------------------------------------- | +| `pages` | Page names (e.g. `button`) or full paths (e.g. `components/button`) to load, space-separated. | +| `--section`, `-s` | Load every page in the given section(s). Accepts one or more section names. | +| `--prod` | Run in prod mode, loading the full site. Same as passing `prod` as the only argument. | -The CLI follows this flow: +If any pages or sections are passed on the command line, `dev` runs immediately in dev mode with those pages — no prompts, no confirmation step. -1. Select environment (dev / prod) -2. Select mode (pages / sections / section-pages) -3. Select pages or sections depending on mode -4. Confirm execution -5. Launch Reflex +If nothing is passed, `dev` falls back to the interactive flow (environment select → fuzzy page search). +## Examples -# Environment +Interactive picker (no arguments): -Before selecting what to run, you must choose the environment. +```bash +uv run dev +``` -This determines whether the CLI runs the full documentation site or a filtered development subset. +Load specific pages by name: -## Dev +```bash +uv run dev button input +``` + +Load a page by its full `section/page` path — useful if a page name exists in more than one section: + +```bash +uv run dev components/button +``` + +Load an entire section: + +```bash +uv run dev --section components +``` + +Load multiple sections at once: + +```bash +uv run dev --section components auth +``` + +Mix individual pages and whole sections in one command: + +```bash +uv run dev button --section auth +``` + +Run the full site in prod mode, skipping all prompts: + +```bash +uv run dev prod +``` + +```bash +uv run dev --prod +``` -Development mode allows you to work on specific parts of the documentation without loading the entire site. +# Page Name Resolution -It enables selective loading of pages through the `dev.py` CLI. +When you pass a bare page name (e.g. `button`), `dev` looks it up across every section: + +- **Unique match** — resolves automatically to its full path (e.g. `components/button`). +- **Ambiguous match** — if the same page name exists in more than one section, `dev` prints an error listing every match and asks you to use the full `section/page` path instead of guessing. +- **No match** — prints an error naming the page that couldn't be found. Nothing is launched if any requested page or section fails to resolve. + +# Environment + +## Dev + +Development mode allows you to work on specific parts of the documentation without loading the entire site. It's the default mode whenever pages or sections are provided, or when you're using the interactive picker. Use this mode when: @@ -77,18 +123,16 @@ Use this mode when: ## Prod -Production mode runs the full documentation site without any filtering. - -# Modes - -## Pages +Production mode runs the full documentation site without any filtering. Trigger it with `uv run dev prod`, `uv run dev --prod`, or by selecting **prod** in the interactive picker. -Select individual pages from all available documentation. Useful when working on specific content or debugging a single page. +# Interactive Page Selection -## Sections +When no arguments are passed, page selection happens through a single fuzzy-searchable, multi-select prompt: -Select one or more sections. All pages inside those sections will be loaded automatically. Useful when working on a feature area. +- Type to filter the list by page name or section name +- Press `Tab` to select multiple entries +- Press `Enter` to confirm your selection -## Section Pages +Each section also has a virtual **"(entire section)"** entry at the top of its group, letting you pull in every page in that section with one selection instead of picking them individually. You can mix and match — for example, select an entire section plus a couple of individual pages from other sections in the same pass. -Select one or more sections first, then choose specific pages inside them. Useful when you want focused work inside a feature area without loading everything. +If no pages are selected, the CLI exits without launching Reflex. When pages are selected interactively, you'll be asked to confirm before Reflex starts; this confirmation step is skipped when pages are passed directly as command-line arguments. diff --git a/assets/llms.txt b/assets/llms.txt index 71075c3..315ceb5 100644 --- a/assets/llms.txt +++ b/assets/llms.txt @@ -7,7 +7,7 @@ - [Installation](https://native.buridan.dev/docs/getting-started/installation): Installation page. - [Skills](https://native.buridan.dev/docs/getting-started/skills): Skills page. - [CLI](https://native.buridan.dev/docs/getting-started/cli): CLI page. -- [dev.py](https://native.buridan.dev/docs/getting-started/dev): dev.py page. +- [dev](https://native.buridan.dev/docs/getting-started/dev): dev page. - [JavaScript](https://native.buridan.dev/docs/getting-started/javascript): JavaScript page. - [Changelog](https://native.buridan.dev/docs/getting-started/changelog): Changelog page. diff --git a/assets/social/dev.webp b/assets/social/dev.webp index d3745b130c0cae1e02c08aa9d92d432b443461a9..6cbd001ca3f38d91039bfefc27ba36fd6d3778a7 100644 GIT binary patch literal 7160 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 P= 1: + errors.append( + f"'{name}' is ambiguous (matches: {', '.join(matches)}) " + f"— use the full path, e.g. '{matches[0]}'" + ) + continue + path = matches[0] + + if path not in seen: + seen.add(path) + resolved.append(path) + + for section in section_names: + if section not in sections: + errors.append(f"section not found: '{section}'") + continue + for page in sections[section]: + path = f"{section}/{page}" + if path not in seen: + seen.add(path) + resolved.append(path) + + if errors: + for e in errors: + print(f"Error: {e}") + return None + + if not resolved: + return None + + return resolved + + +def select_env(): + env = questionary.select( + "Run mode:", + choices=["dev", "prod"], + ).ask() + + if env is None: + return None + + return env + + +def select_pages_fuzzy(sections): + """ + Fuzzy search + multi-select over all pages, plus a virtual + "whole section" entry so you can grab an entire section without + tabbing through every page in it individually. + """ + choices = [] + + for section, pages in sections.items(): + # virtual "select entire section" choice + # choices.append( + # Choice( + # value=f"__section__:{section}", + # name=f"[{section}] (entire section)", + # ) + # ) + for page in pages: + path = f"{section}/{page}" + choices.append(Choice(value=path, name=f" {path}")) + + result = inquirer.fuzzy( + message="Search pages (type to filter, tab to select multiple, enter to confirm):", + choices=choices, + multiselect=True, + max_height="70%", + instruction="(type name, section name, or 'entire section')", + ).execute() + + if not result: + return None + + # expand any "__section__:x" picks into their full page list + pages = [] + seen = set() + + for item in result: + if item.startswith("__section__:"): + section = item.split(":", 1)[1] + for page in sections.get(section, []): + path = f"{section}/{page}" + if path not in seen: + seen.add(path) + pages.append(path) + else: + if item not in seen: + seen.add(item) + pages.append(item) + + return pages + + +def run_reflex(env, pages): + """ + Runs Reflex and prints correct URLs ONLY after the actual port is known. + """ + if env == "dev": + os.environ["BURIDAN_DEV_MODE"] = "true" + + if pages: + os.environ["BURIDAN_DEV_PAGES"] = ",".join(pages) + else: + os.environ.pop("BURIDAN_DEV_PAGES", None) + else: + os.environ.pop("BURIDAN_DEV_MODE", None) + os.environ.pop("BURIDAN_DEV_PAGES", None) + + cmd = ["uv", "run", "reflex", "run", "--env", env] + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + port = None + + for line in process.stdout: + print(line, end="") + + # Detect actual runtime port from Reflex output (only once) + if port is None and "App running at:" in line: + try: + port = line.split("http://localhost:")[1].split("/")[0] + + # Print correct clickable URLs ONLY when server is ready + if pages: + base_url = f"http://localhost:{port}/docs" + + print("\nLoading specified URLs:") + for page in pages: + print(f"{base_url}/{page}") + + except Exception: + pass + + return port + + +def main(): + sections = build_index() + + parser = build_arg_parser() + args = parser.parse_args() + + # ---------------- CLI FAST PATH: PROD ---------------- + # `uv run dev prod` or `uv run dev --prod` + if args.prod or (args.pages and args.pages[0] == "prod"): + print("Launching full site (prod mode)...") + run_reflex("prod", pages=None) + return + + requested_pages = list(args.pages) + # allow (and ignore) an optional literal "dev" prefix, e.g. `dev.py dev button` + if requested_pages and requested_pages[0] == "dev": + requested_pages = requested_pages[1:] + + # ---------------- CLI FAST PATH: PAGES GIVEN DIRECTLY ---------------- + if requested_pages or args.section: + resolved_pages = resolve_pages(requested_pages, args.section, sections) + if resolved_pages is None: + return + + print(f"Running reflex in dev with {len(resolved_pages)} page(s):") + for page in resolved_pages: + print(f" {page}") + + run_reflex("dev", resolved_pages) + return + + # ---------------- INTERACTIVE FLOW (no args given) ---------------- + env = select_env() + if env is None: + print("Exiting") + return + + if env == "prod": + print("Launching full site (prod mode)...") + run_reflex(env, pages=None) + return + + selected_pages = select_pages_fuzzy(sections) + if selected_pages is None: + print("Exiting") + return + + confirm = questionary.confirm( + f"Run reflex in {env} with {len(selected_pages)} pages?" + ).ask() + + if not confirm: + print("Cancelled") + return + + run_reflex(env, selected_pages) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nExiting") diff --git a/dev.py b/dev.py deleted file mode 100644 index 048a802..0000000 --- a/dev.py +++ /dev/null @@ -1,237 +0,0 @@ -import os -import subprocess -from pathlib import Path - -import questionary - -DOCS_DIR = Path("docs") - - -def build_index(): - """ - Builds: - { - "getting-started": ["introduction", "installation"], - "components": ["button", "input"] - } - """ - sections = {} - - for file in DOCS_DIR.glob("**/*.md"): - rel = file.relative_to(DOCS_DIR) - parts = rel.with_suffix("").parts - - if len(parts) < 2: - continue - - section = parts[0].replace("_", "-") - page = parts[1].replace("_", "-") - - sections.setdefault(section, []).append(page) - - for k in sections: - sections[k].sort() - - return sections - - -def expand_sections(selected_sections, sections): - pages = [] - for section in selected_sections: - for page in sections.get(section, []): - pages.append(f"{section}/{page}") - return pages - - -def select_env(): - env = questionary.select( - "Run mode:", - choices=["dev", "prod"], - ).ask() - - if env is None: - return None - - return env - - -def select_mode(): - mode = questionary.select( - "What do you want to select?", - choices=[ - "pages", - "sections", - "section-pages", - ], - ).ask() - - if mode is None: - return None - - return mode - - -def select_pages(sections): - choices = [] - - for section, pages in sections.items(): - choices.append(questionary.Separator(f"[{section}]")) - for page in pages: - choices.append(f"{section}/{page}") - - result = questionary.checkbox( - "Select pages:", - choices=choices, - ).ask() - - if not result: - return None - - return result - - -def select_sections(sections): - result = questionary.checkbox( - "Select sections:", - choices=list(sections.keys()), - ).ask() - - if not result: - return None - - return result - - -def select_pages_from_sections(selected_sections, sections): - choices = [] - - for section in selected_sections: - pages = sections.get(section, []) - - choices.append(questionary.Separator(f"[{section}]")) - - for page in pages: - choices.append(f"{section}/{page}") - - result = questionary.checkbox( - "Select pages:", - choices=choices, - ).ask() - - if not result: - return None - - return result - - -def run_reflex(env, pages): - """ - Runs Reflex and prints correct URLs ONLY after the actual port is known. - """ - if env == "dev": - os.environ["BURIDAN_DEV_MODE"] = "true" - - if pages: - os.environ["BURIDAN_DEV_PAGES"] = ",".join(pages) - else: - os.environ.pop("BURIDAN_DEV_PAGES", None) - else: - os.environ.pop("BURIDAN_DEV_MODE", None) - os.environ.pop("BURIDAN_DEV_PAGES", None) - - cmd = ["uv", "run", "reflex", "run", "--env", env] - - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - - port = None - - for line in process.stdout: - print(line, end="") - - # Detect actual runtime port from Reflex output - if "App running at:" in line: - try: - port = line.split("http://localhost:")[1].split("/")[0] - - # Print correct clickable URLs ONLY when server is ready - if pages: - base_url = f"http://localhost:{port}/docs" - - print("\nLoading specified URLs:") - for page in pages: - print(f"{base_url}/{page}") - - except Exception: - pass - - return port - - -def main(): - sections = build_index() - - env = select_env() - if env is None: - print("Exiting") - return - - # ---------------- FAST PATH: PROD ---------------- - if env == "prod": - print("Launching full site (prod mode)...") - run_reflex(env, pages=None) - return - - # ---------------- DEV FLOW ---------------- - mode = select_mode() - if mode is None: - print("Exiting") - return - - selected_pages = [] - - if mode == "pages": - selected_pages = select_pages(sections) - if selected_pages is None: - print("Exiting") - return - - elif mode == "sections": - selected_sections = select_sections(sections) - if selected_sections is None: - print("Exiting") - return - - selected_pages = expand_sections(selected_sections, sections) - - elif mode == "section-pages": - selected_sections = select_sections(sections) - if selected_sections is None: - print("Exiting") - return - - selected_pages = select_pages_from_sections(selected_sections, sections) - if selected_pages is None: - print("Exiting") - return - - confirm = questionary.confirm( - f"Run reflex in {env} with {len(selected_pages)} pages?" - ).ask() - - if not confirm: - print("Cancelled") - return - - run_reflex(env, selected_pages) - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("\nExiting") diff --git a/docs/getting_started/dev.md b/docs/getting_started/dev.md index 8689cc1..20d250f 100644 --- a/docs/getting_started/dev.md +++ b/docs/getting_started/dev.md @@ -1,73 +1,119 @@ --- -title: "dev.py" -description: "Interactive development CLI for selecting and running Buridan documentation pages locally." +title: "dev" +description: "Interactive and command-line tool for selecting and running Buridan documentation pages locally." order: 4 --- -# dev.py +# dev -The `dev.py` script is an interactive CLI tool used to control which documentation pages are loaded during local development. - -It replaces manual environment variable setup and makes it easier to selectively run parts of the documentation site while developing. +The `dev` command is a CLI tool used to control which documentation pages are loaded during local development. +It replaces manual environment variable setup and makes it easier to selectively run parts of the documentation site while developing. Pages can be picked interactively through a fuzzy search prompt, or loaded directly by name. # Requirements -This script uses [questionary](https://github.com/tmbo/questionary) for interactive prompts. - -Install it using your project setup: +This tool uses [questionary](https://github.com/tmbo/questionary) for the environment/confirm prompts, and [InquirerPy](https://github.com/kazhala/InquirerPy) for fuzzy-searchable page selection. Both are declared in the `dev` dependency group, so they're installed automatically. -```uv -uv add questionary +```bash +uv sync ``` -You can also add it manually to your `pyproject.toml`: +This also registers the `dev` command itself, via the `[project.scripts]` entry in `pyproject.toml`: ```pyproject.toml -dependencies = ["questionary"] +[project.scripts] +dev = "cli.dev:main" ``` -# Overview +# Usage -Instead of running the full documentation build every time, `dev.py` lets you: +Run the command from your project root: -- Select specific pages -- Select entire sections -- Select pages within specific sections -- Run the app in dev or prod mode +```bash +uv run dev +``` -It then automatically configures the correct environment variables and starts the Reflex server. +With no arguments, this starts the interactive flow: choose an environment, then fuzzy-search and multi-select pages. -# Usage +To skip the prompts entirely, pass pages, sections, or a mode directly as arguments — see below. -Run the script from your project root to start the interactive flow: +# Command Reference -```uv -uv run dev.py +```text +uv run dev [pages...] [--section SECTION [SECTION ...]] [--prod] ``` -# Flow +| Argument | Description | +| ----------------- | --------------------------------------------------------------------------------------------- | +| `pages` | Page names (e.g. `button`) or full paths (e.g. `components/button`) to load, space-separated. | +| `--section`, `-s` | Load every page in the given section(s). Accepts one or more section names. | +| `--prod` | Run in prod mode, loading the full site. Same as passing `prod` as the only argument. | -The CLI follows this flow: +If any pages or sections are passed on the command line, `dev` runs immediately in dev mode with those pages — no prompts, no confirmation step. -1. Select environment (dev / prod) -2. Select mode (pages / sections / section-pages) -3. Select pages or sections depending on mode -4. Confirm execution -5. Launch Reflex +If nothing is passed, `dev` falls back to the interactive flow (environment select → fuzzy page search). +## Examples -# Environment +Interactive picker (no arguments): -Before selecting what to run, you must choose the environment. +```bash +uv run dev +``` -This determines whether the CLI runs the full documentation site or a filtered development subset. +Load specific pages by name: -## Dev +```bash +uv run dev button input +``` + +Load a page by its full `section/page` path — useful if a page name exists in more than one section: + +```bash +uv run dev components/button +``` + +Load an entire section: + +```bash +uv run dev --section components +``` + +Load multiple sections at once: + +```bash +uv run dev --section components auth +``` + +Mix individual pages and whole sections in one command: + +```bash +uv run dev button --section auth +``` + +Run the full site in prod mode, skipping all prompts: + +```bash +uv run dev prod +``` + +```bash +uv run dev --prod +``` -Development mode allows you to work on specific parts of the documentation without loading the entire site. +# Page Name Resolution -It enables selective loading of pages through the `dev.py` CLI. +When you pass a bare page name (e.g. `button`), `dev` looks it up across every section: + +- **Unique match** — resolves automatically to its full path (e.g. `components/button`). +- **Ambiguous match** — if the same page name exists in more than one section, `dev` prints an error listing every match and asks you to use the full `section/page` path instead of guessing. +- **No match** — prints an error naming the page that couldn't be found. Nothing is launched if any requested page or section fails to resolve. + +# Environment + +## Dev + +Development mode allows you to work on specific parts of the documentation without loading the entire site. It's the default mode whenever pages or sections are provided, or when you're using the interactive picker. Use this mode when: @@ -77,18 +123,16 @@ Use this mode when: ## Prod -Production mode runs the full documentation site without any filtering. - -# Modes - -## Pages +Production mode runs the full documentation site without any filtering. Trigger it with `uv run dev prod`, `uv run dev --prod`, or by selecting **prod** in the interactive picker. -Select individual pages from all available documentation. Useful when working on specific content or debugging a single page. +# Interactive Page Selection -## Sections +When no arguments are passed, page selection happens through a single fuzzy-searchable, multi-select prompt: -Select one or more sections. All pages inside those sections will be loaded automatically. Useful when working on a feature area. +- Type to filter the list by page name or section name +- Press `Tab` to select multiple entries +- Press `Enter` to confirm your selection -## Section Pages +Each section also has a virtual **"(entire section)"** entry at the top of its group, letting you pull in every page in that section with one selection instead of picking them individually. You can mix and match — for example, select an entire section plus a couple of individual pages from other sections in the same pass. -Select one or more sections first, then choose specific pages inside them. Useful when you want focused work inside a feature area without loading everything. +If no pages are selected, the CLI exits without launching Reflex. When pages are selected interactively, you'll be asked to confirm before Reflex starts; this confirmation step is skipped when pages are passed directly as command-line arguments. diff --git a/pyproject.toml b/pyproject.toml index 9815b3a..147b4cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dev = [ "ruff>=0.15.22", "black", "pytest>=9.1.1", + "inquirerpy", ] [tool.ruff] @@ -61,6 +62,7 @@ indent-style = "space" [project.scripts] buridan = "cli.main:main" +dev = "cli.dev:main" [build-system] requires = ["hatchling"] diff --git a/uv.lock b/uv.lock index c329bd4..254dcdc 100644 --- a/uv.lock +++ b/uv.lock @@ -102,6 +102,7 @@ dev = [ { name = "beautifulsoup4" }, { name = "black" }, { name = "google-genai" }, + { name = "inquirerpy" }, { name = "markdown" }, { name = "pillow" }, { name = "pytest" }, @@ -118,6 +119,7 @@ dev = [ { name = "beautifulsoup4", specifier = ">=4.15.0" }, { name = "black" }, { name = "google-genai" }, + { name = "inquirerpy" }, { name = "markdown", specifier = ">=3.10.2" }, { name = "pillow", specifier = ">=12.2.0" }, { name = "pytest", specifier = ">=9.1.1" }, @@ -572,6 +574,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "inquirerpy" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pfzy" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/73/7570847b9da026e07053da3bbe2ac7ea6cde6bb2cbd3c7a5a950fa0ae40b/InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e", size = 44431, upload-time = "2022-06-27T23:11:20.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/ff/3b59672c47c6284e8005b42e84ceba13864aa0f39f067c973d1af02f5d91/InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4", size = 67677, upload-time = "2022-06-27T23:11:17.723Z" }, +] + [[package]] name = "markdown" version = "3.10.2" @@ -629,6 +644,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pfzy" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/5a/32b50c077c86bfccc7bed4881c5a2b823518f5450a30e639db5d3711952e/pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1", size = 8396, upload-time = "2022-01-28T02:26:17.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537, upload-time = "2022-01-28T02:26:16.047Z" }, +] + [[package]] name = "pillow" version = "12.3.0"