diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9cf55be --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: Build and Deploy Documentation + +on: + push: + branches: + - master # Adjust if your main branch is named differently + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write # Needed to push to gh-pages + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for proper git info + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests (including doctests) + run: | + pytest --doctest-modules plotille/ -v + + - name: Generate documentation + run: | + python scripts/generate_docs.py + + - name: Build MkDocs site + run: | + mkdocs build --strict + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + cname: plotille.tammo.io diff --git a/.gitignore b/.gitignore index f66f345..b684515 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,23 @@ build/ *.so *.c *.html +!docs/overrides/*.html + +# Brython runtime files (generated, should be installed via make docs-setup) +docs/brython.js +docs/brython_stdlib.js +docs/unicode.txt +docs/README.txt +docs/demo.html +docs/ansi_up.js +# Documentation build artifacts +docs/Lib/ +docs/assets/example-outputs/ +docs/cookbook/ +docs/index.md +docs/stylesheets/ansi-colors.css + + *.pyc __pycache__/ dist/ @@ -19,3 +36,7 @@ Pipfile.lock cov_html/ coverage.xml pip-wheel-metadata/ +site/ +.mypy_cache/ +.ruff_cache/ +.uv/ diff --git a/Makefile b/Makefile index 9a6f543..c72ffea 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: style tests install +.PHONY: style tests install docs docs-setup docs-serve fmt: uv run --locked ruff format . @@ -17,3 +17,27 @@ tests: install: uv install + +docs-copy-plotille: + @echo "Copy plotille to docs folder" + @rm -rf docs/Lib + @mkdir -p docs/Lib/site-packages/plotille + @cp plotille/*.py docs/Lib/site-packages/plotille/ + +docs-brython: + @echo "Installing Brython runtime..." + @rm -f docs/brython.js docs/brython_stdlib.js docs/unicode.txt docs/demo.html docs/README.txt docs/index.html + @echo "Y" | uv run brython-cli install --install-dir docs + @rm -f docs/demo.html docs/README.txt docs/index.html + @echo "āœ“ Brython installed to docs/" + +docs/ansi_up.js: + @echo "Downloading AnsiUp library..." + @curl -sL -o docs/ansi_up.js https://unpkg.com/ansi_up@6.0.2/ansi_up.js + @echo "āœ“ AnsiUp downloaded to docs/" + +docs: docs-copy-plotille docs/ansi_up.js docs-brython + uv run python docs/generate_docs.py + +docs-serve: docs + uv run mkdocs serve --dev-addr 127.0.0.1:8000 diff --git a/README.md b/README.md index 6b7ff8e..696b434 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,13 @@ If you are still using python 2.7, please use plotille v4 or before. With v5 I a ## Documentation +šŸ“š **Full documentation available at [plotille.tammo.io](https://plotille.tammo.io)** + +Features: +- **Interactive examples** - Edit and run code in your browser +- **Complete API reference** - Auto-generated from source +- **Cookbook** - Examples organized by complexity + ```python In [1]: import plotille In [2]: import numpy as np diff --git a/docs/api/canvas.md b/docs/api/canvas.md new file mode 100644 index 0000000..47d17b1 --- /dev/null +++ b/docs/api/canvas.md @@ -0,0 +1,17 @@ +# Canvas + +Low-level canvas for direct drawing. + +::: plotille.Canvas + options: + show_root_heading: true + show_source: true + members: + - __init__ + - point + - line + - rect + - text + - braille_image + - image + - plot diff --git a/docs/api/figure.md b/docs/api/figure.md new file mode 100644 index 0000000..cd377bd --- /dev/null +++ b/docs/api/figure.md @@ -0,0 +1,21 @@ +# Figure + +The Figure class for composing complex visualizations. + +::: plotille.Figure + options: + show_root_heading: true + show_source: true + members: + - __init__ + - plot + - scatter + - histogram + - text + - axvline + - axhline + - axvspan + - axhspan + - imgshow + - show + - clear diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..b1cda6c --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,20 @@ +# API Reference + +Complete API documentation for plotille. + +## High-Level Functions + +Quick plotting functions for simple use cases. + +- [Plotting Functions](plotting.md) - `plot()`, `scatter()`, `histogram()` +- [Figure Class](figure.md) - Compose complex multi-plot visualizations + +## Core Components + +- [Canvas](canvas.md) - Low-level drawing primitives +- [Colors](colors.md) - Color handling and themes + +## Utilities + +- [Input Formatting](formatting.md) - Data preprocessing +- [Data Types](datatypes.md) - Internal data structures diff --git a/docs/api/plotting.md b/docs/api/plotting.md new file mode 100644 index 0000000..1125074 --- /dev/null +++ b/docs/api/plotting.md @@ -0,0 +1,31 @@ +# Plotting Functions + +High-level plotting functions for quick visualizations. + +## plot + +::: plotille.plot + options: + show_root_heading: true + show_source: true + +## scatter + +::: plotille.scatter + options: + show_root_heading: true + show_source: true + +## hist + +::: plotille.hist + options: + show_root_heading: true + show_source: true + +## histogram + +::: plotille.histogram + options: + show_root_heading: true + show_source: true diff --git a/docs/generate_docs.py b/docs/generate_docs.py new file mode 100644 index 0000000..b4d278e --- /dev/null +++ b/docs/generate_docs.py @@ -0,0 +1,649 @@ +import ast +import math +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import NamedTuple + +from ansi2html import Ansi2HTMLConverter +from ansi2html.style import get_styles + +import plotille + +conv = Ansi2HTMLConverter() + + +class ExampleInfo(NamedTuple): + """Information about an example file.""" + + path: Path + name: str + description: str + imports: set[str] + is_interactive: bool + + +def extract_imports(source_code: str) -> set[str]: + """ + Extract all imported module names from Python source. + + Args: + source_code: Python source code as string + + Returns: + Set of top-level module names imported + + >>> extract_imports("import numpy\\nfrom PIL import Image") + {'numpy', 'PIL'} + >>> extract_imports("import plotille\\nfrom plotille import Canvas") + {'plotille'} + """ + try: + tree = ast.parse(source_code) + except SyntaxError: + return set() + + imports = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + # Get top-level module name + imports.add(alias.name.split(".")[0]) + elif isinstance(node, ast.ImportFrom): + if node.module: + imports.add(node.module.split(".")[0]) + + return imports + + +def extract_description(source_code: str) -> str: + """ + Extract description from module docstring or initial comments. + + Args: + source_code: Python source code + + Returns: + Description string or empty string + + >>> extract_description('\"\"\"Test module\"\"\"\\nprint("hi")') + 'Test module' + >>> extract_description('# A comment\\nprint("hi")') + 'A comment' + """ + try: + tree = ast.parse(source_code) + docstring = ast.get_docstring(tree) + if docstring: + return docstring.strip().split("\n")[0] # First line only + except SyntaxError: + pass + + # Fall back to first comment + lines = source_code.split("\n") + for line in lines: + stripped = line.strip() + if stripped.startswith("#"): + return stripped[1:].strip() + + return "" + + +def is_interactive(imports: set[str]) -> bool: + """ + Determine if example can run in Brython (no external deps). + + Args: + imports: Set of imported module names + + Returns: + True if example is Brython-compatible + + >>> is_interactive({'plotille', 'math', 'random'}) + True + >>> is_interactive({'plotille', 'numpy'}) + False + >>> is_interactive({'PIL', 'plotille'}) + False + """ + # These are NOT available in Brython + blocked_modules = {"numpy", "PIL", "pandas", "matplotlib", "scipy"} + + # Check if any blocked modules are used + return not bool(imports & blocked_modules) + + +def strip_license_header(source_code: str) -> str: + """ + Remove MIT license header from source code. + + Args: + source_code: Python source code possibly containing license header + + Returns: + Source code with license header removed + """ + lines = source_code.split("\n") + + # Look for MIT license pattern + if "# The MIT License" in source_code: + # Find the end of the license block (first non-comment/non-blank line after license) + in_license = False + start_index = 0 + + for i, line in enumerate(lines): + stripped = line.strip() + + # Start of license + if "MIT License" in line: + in_license = True + start_index = i + continue + + # End of license block (first non-comment, non-blank line) + if in_license and stripped and not stripped.startswith("#"): + # Remove everything from start_index to just before this line + return "\n".join(lines[i:]) + + # If we didn't find the end, remove first 23 lines (typical license length) + if in_license: + return "\n".join(lines[23:]) + + return source_code + + +def analyze_example(example_path: Path) -> ExampleInfo: + """ + Analyze a single example file. + + Args: + example_path: Path to example .py file + + Returns: + ExampleInfo with analysis results + """ + source_code = example_path.read_text() + + # Strip license header before analyzing + source_code_clean = strip_license_header(source_code) + + imports = extract_imports(source_code_clean) + description = extract_description(source_code_clean) + name = example_path.stem + + return ExampleInfo( + path=example_path, + name=name, + description=description or f"Example: {name}", + imports=imports, + is_interactive=is_interactive(imports), + ) + + +def categorize_example(info: ExampleInfo) -> str: + """ + Categorize example into a section. + + Args: + info: ExampleInfo to categorize + + Returns: + Category name: 'basic', 'figures', 'canvas', or 'advanced' + + >>> from pathlib import Path + >>> info = ExampleInfo(Path("scatter.py"), "scatter", "", {'plotille'}, True) + >>> categorize_example(info) + 'basic' + >>> info = ExampleInfo(Path("img.py"), "img", "", {'PIL', 'plotille'}, False) + >>> categorize_example(info) + 'advanced' + """ + name_lower = info.name.lower() + + # Canvas examples + if "canvas" in name_lower or "draw" in name_lower: + return "canvas" + + # Figure examples (multi-plot) + if "figure" in name_lower or "subplot" in name_lower: + return "figures" + + # Advanced (external deps or complex) + if not info.is_interactive or "image" in name_lower or "img" in name_lower: + return "advanced" + + # Default to basic + return "basic" + + +@dataclass +class ExampleOutput: + """Captured output from running an example.""" + + stdout: str + stderr: str + returncode: int + success: bool + + +def execute_example(example_path: Path, timeout: int = 30) -> ExampleOutput: + """ + Execute an example and capture its output. + + Args: + example_path: Path to example Python file + timeout: Maximum execution time in seconds + + Returns: + ExampleOutput with captured stdout/stderr + """ + try: + result = subprocess.run( + [sys.executable, str(example_path)], + capture_output=True, + text=True, + timeout=timeout, + cwd=example_path.parent, + env={"FORCE_COLOR": "1"}, + ) + + return ExampleOutput( + stdout=conv.convert(result.stdout, full=False), + stderr=conv.convert(result.stderr, full=False), + returncode=result.returncode, + success=result.returncode == 0, + ) + except subprocess.TimeoutExpired: + return ExampleOutput( + stdout="", + stderr=f"Example timed out after {timeout} seconds", + returncode=-1, + success=False, + ) + except Exception as e: + return ExampleOutput( + stdout="", + stderr=f"Error executing example: {e}", + returncode=-1, + success=False, + ) + + +def save_example_output( + info: ExampleInfo, output: ExampleOutput, output_dir: Path +) -> Path: + """ + Save example output to a file. + + Args: + info: ExampleInfo for the example + output: ExampleOutput to save + output_dir: Directory to save output files + + Returns: + Path to saved output file + """ + output_dir.mkdir(parents=True, exist_ok=True) + output_file = output_dir / f"{info.name}.txt" + + content = output.stdout + if not output.success and output.stderr: + content += f"\n\nErrors:\n{output.stderr}" + + output_file.write_text(content) + return output_file + + +def generate_static_outputs( + examples: list[ExampleInfo], output_dir: Path +) -> dict[str, Path]: + """ + Execute static examples and save their outputs. + + Args: + examples: List of ExampleInfo to process + output_dir: Directory to save outputs + + Returns: + Dict mapping example name to output file path + """ + outputs = {} + + static_examples = [e for e in examples if not e.is_interactive] + + print(f"\nGenerating outputs for {len(static_examples)} static examples...") + + for info in static_examples: + print(f" Executing {info.name}...", end=" ") + + output = execute_example(info.path) + + if output.success: + output_path = save_example_output(info, output, output_dir) + outputs[info.name] = output_path + print("āœ“") + else: + print("āœ— (failed)") + if output.stderr: + print(f" Error: {output.stderr[:100]}") + + return outputs + + +def generate_interactive_example_markdown(info: ExampleInfo) -> str: + """ + Generate markdown for an interactive example. + + Args: + info: ExampleInfo for the example + + Returns: + Markdown string with interactive code editor + """ + source_code = info.path.read_text() + + # Strip license header + source_code = strip_license_header(source_code) + + # Escape backticks in code for markdown + escaped_code = source_code.replace("```", "\\`\\`\\`") + + return f"""## {info.name} + +{info.description} + +
+
+ [python3 {info.name}.py] + +
+
+
+ +
+
+ root@plotille:~$ python3 {info.name}.py +
+
+
+
+ +""" + + +def generate_static_example_markdown( + info: ExampleInfo, output_path: Path | None +) -> str: + """ + Generate markdown for a static example with pre-rendered output. + + Args: + info: ExampleInfo for the example + output_path: Path to pre-rendered output file, or None if not available + + Returns: + Markdown string with code and output + """ + source_code = info.path.read_text() + + # Strip license header + source_code = strip_license_header(source_code) + + # Read pre-rendered output + if output_path and output_path.is_file(): + output = output_path.read_text() + else: + output = "Output not available (dependencies not installed during build)" + + deps = ", ".join(sorted(info.imports - {"plotille"})) + + return f"""## {info.name} + +{info.description} + +!!! info "External Dependencies" + This example requires: **{deps}** + + Output is pre-rendered below. To run interactively, install dependencies locally. + +**Code:** + +```python +{source_code} +``` + +**Output:** + +
+
+ [output: {info.name}.py] +
+
+
{output}
+
+
+ +""" + + +def generate_category_page( + category: str, + examples: list[ExampleInfo], + output_paths: dict[str, Path], + docs_dir: Path, +) -> Path: + """ + Generate a markdown page for a category of examples. + + Args: + category: Category name + examples: List of examples in this category + output_paths: Dict of pre-rendered output paths + docs_dir: Documentation directory + + Returns: + Path to generated markdown file + """ + category_titles = { + "basic": "Basic Plots", + "figures": "Complex Figures", + "canvas": "Canvas Drawing", + "advanced": "Advanced Examples", + } + + title = category_titles.get(category, category.title()) + + # Build page content + content = [f"# {title}\n"] + + # Add description + descriptions = { + "basic": "Simple plotting examples to get started with plotille.", + "figures": "Multi-plot figures and complex visualizations.", + "canvas": "Direct canvas manipulation for custom drawings.", + "advanced": "Examples using external libraries like NumPy and Pillow.", + } + + if category in descriptions: + content.append(f"{descriptions[category]}\n") + + # Add each example + for info in examples: + if info.is_interactive: + markdown = generate_interactive_example_markdown(info) + else: + output_path = output_paths.get(info.name) + markdown = generate_static_example_markdown(info, output_path) + + content.append(markdown) + + # Write file + category_dir = docs_dir / "cookbook" + category_dir.mkdir(parents=True, exist_ok=True) + + output_file = category_dir / f"{category}.md" + output_file.write_text("\n".join(content)) + + return output_file + + +def generate_hero_plot() -> str: + """ + Generate a sample plot for the hero animation. + + Returns: + String containing plotille plot output + """ + try: + X = [i / 10 for i in range(-31, 32)] + Y = [math.sin(x) for x in X] + plot_output = plotille.plot( + X, Y, width=60, height=10, X_label="X", Y_label="", lc="red" + ) + + return conv.convert(plot_output, full=False) + except Exception as e: + # Fallback if generation fails + return f"Error generating plot: {e}" + + +def generate_home_page(docs_dir: Path) -> Path: + """ + Generate the home/index page. + + Args: + docs_dir: Documentation directory + + Returns: + Path to generated index.md + """ + # Generate the hero plot + hero_plot = generate_hero_plot() + + # Change to f-string to include hero_plot + content = f"""# plotille + +
+
+ [root@plotille ~]$ +
+
+
{hero_plot}
+
+
+ +Plot in the terminal using braille dots, with no dependencies. + +## Features + +- **Scatter plots, line plots, histograms** - Basic plotting functions +- **Complex figures** - Compose multiple plots with legends +- **Canvas drawing** - Direct pixel manipulation for custom visualizations +- **Image rendering** - Display images using braille dots or background colors +- **Color support** - Multiple color modes: names, byte values, RGB +- **No dependencies** - Pure Python with no external requirements + +## Quick Start + +Install plotille: + +```bash +pip install plotille +``` + +Create your first plot: + +```python +import plotille +import math + +X = [i/10 for i in range(-30, 30)] +Y = [math.sin(x) for x in X] + +print(plotille.plot(X, Y, height=20, width=60)) +``` + +## Explore + +Browse the [cookbook](cookbook/basic.md) to see interactive examples you can edit and run in your browser. + +""" + + index_file = docs_dir / "index.md" + index_file.write_text(content) + return index_file + + +def generate_color_styles(docs_dir: Path) -> Path: + css_file = docs_dir / "stylesheets" / "ansi-colors.css" + css_file.write_text( + "\n".join( + [ + f".ansi-output {r}" + for r in get_styles(conv.dark_bg, conv.line_wrap, conv.scheme) + ] + ) + ) + return css_file + + +def main() -> int: + """Main entry point.""" + project_root = Path(__file__).parent.parent + examples_dir = project_root / "examples" + output_dir = project_root / "docs" / "assets" / "example-outputs" + docs_dir = project_root / "docs" + + if not examples_dir.exists(): + print(f"Error: {examples_dir} not found", file=sys.stderr) + return 1 + + # Analyze all Python files + examples = [] + SKIP_EXAMPLES = {"__init__.py", "performance_example.py"} + + for example_file in sorted(examples_dir.glob("*.py")): + # Skip files that aren't user-facing examples + if example_file.name in SKIP_EXAMPLES: + continue + info = analyze_example(example_file) + examples.append(info) + + # Categorize + categories: dict[str, list[ExampleInfo]] = {} + for info in examples: + category = categorize_example(info) + categories.setdefault(category, []).append(info) + + # Print summary + print(f"Found {len(examples)} examples") + for category, items in sorted(categories.items()): + interactive_count = sum(1 for e in items if e.is_interactive) + print(f" {category}: {len(items)} examples ({interactive_count} interactive)") + + # Generate static outputs + output_paths = generate_static_outputs(examples, output_dir) + print(f"\nGenerated {len(output_paths)} static outputs") + + # Generate category pages + print("\nGenerating category pages...") + for category, items in sorted(categories.items()): + page_path = generate_category_page(category, items, output_paths, docs_dir) + print(f" {category}: {page_path}") + + # Generate home page + print("\nGenerating home page...") + index_path = generate_home_page(docs_dir) + print(f" index: {index_path}") + + print("\nGenerating color styles...") + css_path = generate_color_styles(docs_dir) + print(f" olor-styles: {css_path}") + + print("\nāœ“ Documentation generation complete") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/javascripts/brython-executor.py b/docs/javascripts/brython-executor.py new file mode 100644 index 0000000..7d7f66c --- /dev/null +++ b/docs/javascripts/brython-executor.py @@ -0,0 +1,84 @@ +# ABOUTME: Python code executed in browser via Brython for running examples. +# ABOUTME: Provides run_code() function callable from JavaScript. + +import sys +import traceback + +from browser import document, window + + +class OutputCapture: + """Capture stdout output in a Brython-compatible way.""" + + def __init__(self): + self.output = [] + + def write(self, text): + self.output.append(str(text)) + + def flush(self): + pass + + def isatty(self): + return True # Pretend we're a TTY to enable colors + + def getvalue(self): + return "".join(self.output) + + +def run_code(code, output_element_id): + """ + Execute Python code and display output. + + Args: + code: Python code string to execute + output_element_id: ID of DOM element to write output to + """ + output_elem = document[output_element_id] + + if not output_elem: + print(f"Error: Output element {output_element_id} not found") + return + + # Clear previous output + output_elem.text = "" + output_elem.classList.remove("error") + + # Show running indicator + output_elem.text = "Running..." + + # Capture stdout + capture = OutputCapture() + old_stdout = sys.stdout + sys.stdout = capture + + try: + # Execute the user's code + exec(code, {"__name__": "__main__"}) + + # Get captured output + raw_output = capture.getvalue() + + # Convert ANSI codes to HTML if AnsiUp is available + if raw_output: + if window.ansiUpConverter: + html_output = window.ansiUpConverter.ansi_to_html(raw_output) + output_elem.innerHTML = html_output + else: + output_elem.text = raw_output + else: + output_elem.text = "(no output)" + + except Exception as e: + # Display error + output_elem.classList.add("error") + error_msg = f"Error: {e}\n\n{traceback.format_exc()}" + output_elem.text = error_msg + + finally: + # Restore stdout + sys.stdout = old_stdout + + +# Make run_code available to JavaScript +window.pythonRunCode = run_code diff --git a/docs/javascripts/brython-setup.js b/docs/javascripts/brython-setup.js new file mode 100644 index 0000000..7183fba --- /dev/null +++ b/docs/javascripts/brython-setup.js @@ -0,0 +1,82 @@ +/** + * Brython setup and initialization for plotille documentation. + */ + +// Track if Brython is ready +let brythonReady = false; + +// Initialize Brython when page loads +document.addEventListener('DOMContentLoaded', function() { + // Check if Brython is loaded + if (typeof brython === 'undefined') { + console.error('Brython not loaded'); + return; + } + + // Initialize Brython + // Brython automatically searches Lib/site-packages/ for modules + brython({ + debug: 1 // Show errors in console + }); + + // Wait for Python executor to be ready + const checkReady = setInterval(() => { + if (window.pythonRunCode) { + brythonReady = true; + clearInterval(checkReady); + console.log('Brython initialized and executor ready'); + } + }, 50); + + // Timeout after 5 seconds + setTimeout(() => { + if (!brythonReady) { + clearInterval(checkReady); + console.error('Brython executor not ready after 5 seconds'); + } + }, 5000); +}); + +/** + * Execute Python code in an example. + * + * @param {string} exampleName - Name of the example to run + */ +function runExample(exampleName) { + const editor = document.getElementById(`editor-${exampleName}`); + const outputContainer = document.getElementById(`output-${exampleName}`); + const outputContent = outputContainer ? outputContainer.querySelector('.output-content') : null; + + if (!editor || !outputContent) { + console.error(`Example ${exampleName} not found`); + return; + } + + // Check if Brython executor is ready + if (!brythonReady || !window.pythonRunCode) { + outputContent.textContent = 'Brython not ready yet...'; + outputContent.classList.add('error'); + console.error('Brython executor not ready'); + return; + } + + const code = editor.value; + + // Give the output content div an ID if it doesn't have one + if (!outputContent.id) { + outputContent.id = `output-content-${exampleName}`; + } + + // Call the Python function to execute the code + // It will handle output capture and display + try { + window.pythonRunCode(code, outputContent.id); + } catch (error) { + outputContent.classList.add('error'); + outputContent.textContent = `Error: ${error.message}`; + console.error('Brython execution error:', error); + } +} + +// Make runExample globally available +window.runExample = runExample; diff --git a/docs/javascripts/codemirror-setup.js b/docs/javascripts/codemirror-setup.js new file mode 100644 index 0000000..27303d5 --- /dev/null +++ b/docs/javascripts/codemirror-setup.js @@ -0,0 +1,37 @@ +/** + * Textarea enhancement for plotille documentation. + * + * Enhances textarea elements with monospace font and tab key support. + * Note: Full CodeMirror integration skipped per YAGNI principle. + */ + +document.addEventListener('DOMContentLoaded', function() { + // Find all code editor textareas + const editors = document.querySelectorAll('.code-editor'); + + editors.forEach(textarea => { + const editorId = textarea.id; + + // Style the textarea + textarea.style.fontFamily = "'IBM Plex Mono', monospace"; + textarea.style.fontSize = '14px'; + textarea.style.lineHeight = '1.5'; + textarea.style.tabSize = '4'; + + // Add tab key support + textarea.addEventListener('keydown', function(e) { + if (e.key === 'Tab') { + e.preventDefault(); + const start = this.selectionStart; + const end = this.selectionEnd; + const value = this.value; + + // Insert 4 spaces + this.value = value.substring(0, start) + ' ' + value.substring(end); + this.selectionStart = this.selectionEnd = start + 4; + } + }); + + console.log(`Editor initialized: ${editorId}`); + }); +}); diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 0000000..db0a6e7 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block scripts %} + {{ super() }} + + + + + + +{% endblock %} diff --git a/docs/plans/css-simplification-plan.md b/docs/plans/css-simplification-plan.md new file mode 100644 index 0000000..4f03f83 --- /dev/null +++ b/docs/plans/css-simplification-plan.md @@ -0,0 +1,386 @@ +# CSS Simplification Plan - Return to Standard Material Theme + +## Philosophy + +**Old approach (wrong):** Override everything with amber phosphor CRT aesthetic +**New approach (correct):** Use Material's default theme, style only terminal windows + +**Principle:** The documentation is standard Material. The interactive examples are terminal windows. That's the only distinction. + +--- + +## Step 1: Create Minimal terminal.css + +Replace the entire `docs/stylesheets/terminal.css` with this minimal version: + +```css +/* + * ABOUTME: Terminal window styling for plotille interactive examples. + * ABOUTME: Uses standard Material theme for everything except terminal components. + */ + +/* Terminal window components only - no global overrides */ + +:root { + /* Terminal-specific colors (not site-wide) */ + --terminal-bg: #1e1e1e; + --terminal-fg: #d4d4d4; + --terminal-border: #3e3e3e; + --terminal-header-bg: #2d2d2d; + --terminal-button-bg: #0e639c; + --terminal-button-hover: #1177bb; +} + +/* Terminal window structure */ +.terminal-window { + background: var(--terminal-bg); + border: 1px solid var(--terminal-border); + border-radius: 6px; + margin: 1.5rem 0; + overflow: hidden; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; +} + +.terminal-header { + background: var(--terminal-header-bg); + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--terminal-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.terminal-title { + color: var(--terminal-fg); + font-size: 0.85rem; + font-weight: 500; +} + +.terminal-run-btn { + background: var(--terminal-button-bg); + color: #ffffff; + border: none; + padding: 0.25rem 0.75rem; + border-radius: 3px; + cursor: pointer; + font-size: 0.85rem; + font-weight: 500; + transition: background 0.2s; +} + +.terminal-run-btn:hover { + background: var(--terminal-button-hover); +} + +.terminal-body { + padding: 1rem; + background: var(--terminal-bg); +} + +/* Code editor (textarea) */ +.code-editor-wrapper { + border: 1px solid var(--terminal-border); + border-radius: 4px; + overflow: hidden; +} + +.code-editor { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + background: var(--terminal-bg); + color: var(--terminal-fg); + width: 100%; + min-height: 300px; + padding: 1rem; + border: none; + resize: vertical; + line-height: 1.5; + tab-size: 4; +} + +.code-editor:focus { + outline: none; + box-shadow: 0 0 0 2px var(--terminal-button-bg); +} + +/* Terminal output */ +.terminal-output { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + color: var(--terminal-fg); + white-space: pre-wrap; + margin-top: 1rem; + line-height: 1.4; + max-height: 600px; + overflow-y: auto; +} + +.terminal-prompt { + color: #4ec9b0; /* Teal for prompt */ + display: block; + margin-bottom: 0.5rem; +} + +.output-content { + margin-top: 0.5rem; +} + +.output-content.error { + color: #f48771; /* Soft red for errors */ +} + +/* Hero terminal on home page */ +.hero-terminal { + margin: 2rem 0; +} + +.hero-plot { + font-size: 0.75rem; +} +``` + +**That's it. 120 lines total. No global overrides.** + +--- + +## Step 2: Update mkdocs.yml + +Remove custom theme configuration, use Material defaults: + +```yaml +site_name: plotille +site_url: https://plotille.tammo.io +site_description: Plot in the terminal using braille dots +site_author: Tammo Ippen +repo_url: https://github.com/tammoippen/plotille +repo_name: tammoippen/plotille + +theme: + name: material + # Remove: custom_dir, palette.scheme overrides + # Keep only essential features + features: + - content.code.copy + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_source: true + show_signature_annotations: true + separate_signature: true + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - admonition + - pymdownx.details + +# Only load minimal terminal CSS +extra_css: + - stylesheets/terminal.css + +# Keep Brython and scripts +extra_javascript: + - brython.js + - brython_stdlib.js + - ansi_up.js + - javascripts/codemirror-setup.js + - javascripts/brython-setup.js + +nav: + - Home: index.md + - Cookbook: + - Basic Plots: cookbook/basic.md + - Advanced Examples: cookbook/advanced.md +``` + +--- + +## Step 3: Remove Unnecessary Files + +**Delete these (if they exist):** +- `docs/overrides/` (if only used for theme overrides, not Brython) + +**Keep:** +- `docs/overrides/main.html` if it contains Brython executor (check first) + +**Update `.gitignore`:** + +```gitignore +# Site build output +site/ + +# Documentation build artifacts +docs/Lib/ +docs/assets/example-outputs/ +``` + +--- + +## Step 4: Clean Git History + +Remove build artifacts from git: + +```bash +# Remove tracked build artifacts +git rm -r --cached docs/Lib/ +git rm -r --cached docs/assets/example-outputs/ + +# Commit the cleanup +git add .gitignore +git commit -m "Remove build artifacts and simplify CSS to standard Material theme" +``` + +--- + +## Step 5: Test the Simplified Version + +```bash +# Rebuild docs +python scripts/generate_docs.py + +# Serve and check +make doc-serve +``` + +**What you should see:** +- Standard Material theme (light or dark based on system preference) +- Normal readable text, standard colors +- Navigation works normally +- Search works normally +- Code blocks use Material's syntax highlighting +- Terminal windows (interactive examples) look like actual terminals (dark with monospace) +- Clear visual distinction: "this is a terminal" vs "this is documentation" + +--- + +## What Gets Removed vs What Stays + +### āŒ REMOVE (was in old terminal.css): +- All global color overrides (`[data-md-color-scheme="plotille"]`) +- Global font changes (VT323, IBM Plex Mono everywhere) +- CRT scanline effects +- Phosphor glow effects +- Global text shadows +- Navigation styling overrides +- Header styling overrides +- Search results styling overrides +- Code block syntax highlighting overrides (use Material's defaults) +- Admonition overrides +- Link color overrides +- Sidebar overrides + +### āœ… KEEP (in new terminal.css): +- `.terminal-window` structure +- `.terminal-header` styling +- `.terminal-title` styling +- `.terminal-run-btn` styling +- `.terminal-body` styling +- `.code-editor` textarea styling +- `.terminal-output` styling +- `.terminal-prompt` styling +- `.hero-terminal` styling (if kept on home page) + +--- + +## Updated Implementation Plan - Phase 5 + +Replace the old Task 5.1 with this: + +### Task 5.1: Create Minimal Terminal Window Styling + +**What:** Style only the terminal window components for interactive examples. + +**Files to create:** +- `docs/stylesheets/terminal.css` + +**Actions:** + +1. Create `docs/stylesheets/terminal.css` with minimal terminal-only styling (see Step 1 above) + +2. Update `mkdocs.yml` to use standard Material theme (see Step 2 above) + +**Philosophy:** +- Use Material's defaults for everything +- Only style terminal windows to look like actual terminals +- No global theme overrides +- No custom fonts site-wide +- No special effects (scanlines, glows, etc.) + +**Visual design:** +- Documentation: Standard Material theme (clean, professional, readable) +- Terminal windows: Dark background (#1e1e1e), light text (#d4d4d4), monospace font +- Clear visual separation between docs and interactive code + +**Test:** +```bash +mkdocs serve +# Visit http://127.0.0.1:8000 +# Verify: +# - Standard Material theme throughout +# - Terminal windows look distinct (dark, monospace) +# - Everything is readable +# - No custom colors bleeding into navigation/search/etc. +``` + +**Commit:** `Add minimal terminal window styling` + +--- + +## Benefits of This Approach + +**Readability:** +- Material's theme is professionally designed and tested +- High contrast, accessible colors +- No custom syntax highlighting to maintain + +**Maintainability:** +- 120 lines of CSS vs 547 lines +- Only terminal components, easy to understand +- No fighting with Material's defaults +- Easy to update when Material releases new versions + +**Performance:** +- No global overrides means faster rendering +- No complex animations or effects +- Smaller CSS file + +**User Experience:** +- Familiar documentation UI (Material is widely used) +- Clear distinction: "I'm reading docs" vs "I'm in a terminal" +- No accessibility issues from custom color schemes + +**Development:** +- Easier to debug +- Material's theme switcher works (light/dark mode) +- Search, navigation, all features work as designed + +--- + +## Migration Checklist + +- [ ] Create new minimal `docs/stylesheets/terminal.css` (120 lines) +- [ ] Update `mkdocs.yml` to remove custom theme config +- [ ] Add `docs/Lib/` and `docs/assets/example-outputs/` to `.gitignore` +- [ ] Run `git rm -r --cached` on build artifacts +- [ ] Delete old 547-line terminal.css +- [ ] Test locally: `make doc-serve` +- [ ] Verify terminal windows still look good +- [ ] Verify Material theme works everywhere else +- [ ] Commit: "Simplify to standard Material theme with minimal terminal styling" + +--- + +## Result + +**Before:** Custom amber phosphor theme everywhere, 547 lines of CSS, readability issues +**After:** Standard Material theme, 120 lines of CSS for terminal windows only, clean and readable + +The documentation looks professional. The terminal windows look like terminals. No confusion, no maintenance burden. diff --git a/docs/plans/documentation-system-implementation.md b/docs/plans/documentation-system-implementation.md new file mode 100644 index 0000000..e993c4b --- /dev/null +++ b/docs/plans/documentation-system-implementation.md @@ -0,0 +1,3379 @@ +# Plotille Documentation System - Implementation Plan + +## Overview + +This plan implements a comprehensive documentation system for plotille with: +- Example-driven structure (cookbook first, API reference second) +- Interactive browser-based examples using Brython +- Amber phosphor CRT terminal aesthetic +- Auto-deployment to plotille.tammo.io via GitHub Pages +- Strict doctest enforcement + +**Key Principles:** +- TDD: Write tests before implementation +- YAGNI: Only build what's specified, no extras +- DRY: Extract common patterns +- Frequent commits: After each completed task + +## Prerequisites + +You'll be working with: +- **MkDocs**: Static site generator for Python projects +- **mkdocstrings**: Auto-generates API docs from docstrings +- **Brython**: Browser-based Python runtime +- **CodeMirror 6**: Code editor component +- **GitHub Actions**: CI/CD pipeline + +## Phase 1: Project Setup & Dependencies + +### Task 1.1: Install MkDocs and Core Plugins + +**What:** Set up MkDocs with necessary plugins. + +**Files to modify:** +- `pyproject.toml` + +**Actions:** + +1. Add documentation dependencies to `[dependency-groups]` section in `pyproject.toml`: + +```toml +[dependency-groups] +dev = [ + # ... existing dev dependencies ... + "mkdocs>=1.5.0", + "mkdocs-material>=9.5.0", + "mkdocstrings[python]>=0.24.0", + "mkdocs-gen-files>=0.5.0", + "mkdocs-literate-nav>=0.6.0", +] +``` + +2. Install dependencies: +```bash +pip install -e ".[dev]" +``` + +**Test:** +```bash +mkdocs --version # Should output version info +python -c "import mkdocstrings; print('OK')" # Should print OK +``` + +**Commit:** `Add MkDocs and documentation dependencies` + +--- + +### Task 1.2: Create Basic MkDocs Configuration + +**What:** Set up minimal MkDocs config to verify installation works. + +**Files to create:** +- `mkdocs.yml` + +**Actions:** + +1. Create `mkdocs.yml` in project root: + +```yaml +site_name: plotille +site_url: https://plotille.tammo.io +site_description: Plot in the terminal using braille dots +site_author: Tammo Ippen +repo_url: https://github.com/tammoippen/plotille +repo_name: tammoippen/plotille + +theme: + name: material + palette: + scheme: default + features: + - content.code.copy + - navigation.sections + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_source: true + show_root_heading: true + show_root_full_path: false + show_signature_annotations: true + separate_signature: true + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.tabbed + +nav: + - Home: index.md +``` + +2. Create minimal `docs/` directory: +```bash +mkdir -p docs +``` + +3. Create placeholder `docs/index.md`: + +```markdown +# plotille + +Terminal plotting with braille dots. + +Documentation coming soon. +``` + +**Test:** +```bash +mkdocs serve # Should start dev server on http://127.0.0.1:8000 +# Open in browser, verify page loads +``` + +**Commit:** `Add basic MkDocs configuration` + +--- + +### Task 1.3: Add Doctest to Test Suite + +**What:** Configure pytest to run doctests on all modules. + +**Files to modify:** +- `pyproject.toml` + +**Actions:** + +1. Update pytest configuration in `pyproject.toml`: + +```toml +[tool.pytest.ini_options] +addopts = "--cov=plotille --cov-branch --cov-report term-missing --cov-report xml --cov-report html:cov_html --doctest-modules --doctest-continue-on-failure" +testpaths = ["tests", "plotille"] +``` + +**Test:** +```bash +pytest --doctest-modules plotille/ +# Should run (may have 0 doctests initially, that's OK) +``` + +**Commit:** `Enable doctest in pytest configuration` + +--- + +## Phase 2: Example Analysis & Classification System + +### Task 2.1: Create Example Parser Script + +**What:** Build a script that analyzes example files to classify them as interactive vs. static. + +**Files to create:** +- `scripts/generate_docs.py` + +**Actions:** + +1. Create `scripts/` directory: +```bash +mkdir -p scripts +``` + +2. Create `scripts/generate_docs.py`: + +```python +#!/usr/bin/env python3 +""" +Generate documentation from examples. + +This script: +1. Scans examples/ directory +2. Classifies examples by dependencies +3. Generates markdown files for MkDocs +""" +import ast +import sys +from pathlib import Path +from typing import NamedTuple + + +class ExampleInfo(NamedTuple): + """Information about an example file.""" + path: Path + name: str + description: str + imports: set[str] + is_interactive: bool + + +def extract_imports(source_code: str) -> set[str]: + """ + Extract all imported module names from Python source. + + Args: + source_code: Python source code as string + + Returns: + Set of top-level module names imported + + >>> extract_imports("import numpy\\nfrom PIL import Image") + {'numpy', 'PIL'} + >>> extract_imports("import plotille\\nfrom plotille import Canvas") + {'plotille'} + """ + try: + tree = ast.parse(source_code) + except SyntaxError: + return set() + + imports = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + # Get top-level module name + imports.add(alias.name.split('.')[0]) + elif isinstance(node, ast.ImportFrom): + if node.module: + imports.add(node.module.split('.')[0]) + + return imports + + +def extract_description(source_code: str) -> str: + """ + Extract description from module docstring or initial comments. + + Args: + source_code: Python source code + + Returns: + Description string or empty string + + >>> extract_description('\"\"\"Test module\"\"\"\\nprint("hi")') + 'Test module' + >>> extract_description('# A comment\\nprint("hi")') + 'A comment' + """ + try: + tree = ast.parse(source_code) + docstring = ast.get_docstring(tree) + if docstring: + return docstring.strip().split('\n')[0] # First line only + except SyntaxError: + pass + + # Fall back to first comment + lines = source_code.split('\n') + for line in lines: + stripped = line.strip() + if stripped.startswith('#'): + return stripped[1:].strip() + + return "" + + +def is_interactive(imports: set[str]) -> bool: + """ + Determine if example can run in Brython (no external deps). + + Args: + imports: Set of imported module names + + Returns: + True if example is Brython-compatible + + >>> is_interactive({'plotille', 'math', 'random'}) + True + >>> is_interactive({'plotille', 'numpy'}) + False + >>> is_interactive({'PIL', 'plotille'}) + False + """ + # These are NOT available in Brython + blocked_modules = {'numpy', 'PIL', 'pandas', 'matplotlib', 'scipy'} + + # Check if any blocked modules are used + return not bool(imports & blocked_modules) + + +def analyze_example(example_path: Path) -> ExampleInfo: + """ + Analyze a single example file. + + Args: + example_path: Path to example .py file + + Returns: + ExampleInfo with analysis results + """ + source_code = example_path.read_text() + imports = extract_imports(source_code) + description = extract_description(source_code) + name = example_path.stem + + return ExampleInfo( + path=example_path, + name=name, + description=description or f"Example: {name}", + imports=imports, + is_interactive=is_interactive(imports) + ) + + +def main() -> int: + """Main entry point for testing.""" + # Find examples directory + project_root = Path(__file__).parent.parent + examples_dir = project_root / "examples" + + if not examples_dir.exists(): + print(f"Error: {examples_dir} not found", file=sys.stderr) + return 1 + + # Analyze all Python files + examples = [] + for example_file in sorted(examples_dir.glob("*.py")): + info = analyze_example(example_file) + examples.append(info) + + # Print summary + print(f"Found {len(examples)} examples") + print(f"Interactive: {sum(1 for e in examples if e.is_interactive)}") + print(f"Static: {sum(1 for e in examples if not e.is_interactive)}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) +``` + +**Test:** + +1. Create a simple test file `tests/test_generate_docs.py`: + +```python +"""Tests for documentation generation script.""" +import sys +from pathlib import Path + +# Add scripts to path for testing +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +import generate_docs + + +def test_extract_imports_simple(): + """Test extracting imports from simple code.""" + code = "import numpy\nimport plotille" + imports = generate_docs.extract_imports(code) + assert imports == {'numpy', 'plotille'} + + +def test_extract_imports_from(): + """Test extracting from-imports.""" + code = "from PIL import Image" + imports = generate_docs.extract_imports(code) + assert imports == {'PIL'} + + +def test_extract_description_docstring(): + """Test extracting description from docstring.""" + code = '"""This is a test"""\nprint("hi")' + desc = generate_docs.extract_description(code) + assert desc == "This is a test" + + +def test_extract_description_comment(): + """Test extracting description from comment.""" + code = "# This is a comment\nprint('hi')" + desc = generate_docs.extract_description(code) + assert desc == "This is a comment" + + +def test_is_interactive_pure(): + """Test interactive detection for pure plotille.""" + imports = {'plotille', 'math', 'random'} + assert generate_docs.is_interactive(imports) is True + + +def test_is_interactive_numpy(): + """Test interactive detection with numpy.""" + imports = {'plotille', 'numpy'} + assert generate_docs.is_interactive(imports) is False + + +def test_is_interactive_pillow(): + """Test interactive detection with PIL.""" + imports = {'PIL', 'plotille'} + assert generate_docs.is_interactive(imports) is False +``` + +2. Run tests: +```bash +pytest tests/test_generate_docs.py -v +# All tests should pass +``` + +3. Test script manually: +```bash +python scripts/generate_docs.py +# Should print summary of examples found +``` + +**Commit:** `Add example analysis script with tests` + +--- + +### Task 2.2: Add Example Categorization Logic + +**What:** Extend the script to categorize examples into logical groups. + +**Files to modify:** +- `scripts/generate_docs.py` + +**Actions:** + +1. Add categorization logic to `scripts/generate_docs.py`: + +```python +# Add after ExampleInfo class definition + +def categorize_example(info: ExampleInfo) -> str: + """ + Categorize example into a section. + + Args: + info: ExampleInfo to categorize + + Returns: + Category name: 'basic', 'figures', 'canvas', or 'advanced' + + >>> from pathlib import Path + >>> info = ExampleInfo(Path("scatter.py"), "scatter", "", {'plotille'}, True) + >>> categorize_example(info) + 'basic' + >>> info = ExampleInfo(Path("img.py"), "img", "", {'PIL', 'plotille'}, False) + >>> categorize_example(info) + 'advanced' + """ + name_lower = info.name.lower() + + # Canvas examples + if 'canvas' in name_lower or 'draw' in name_lower: + return 'canvas' + + # Figure examples (multi-plot) + if 'figure' in name_lower or 'subplot' in name_lower: + return 'figures' + + # Advanced (external deps or complex) + if not info.is_interactive or 'image' in name_lower or 'img' in name_lower: + return 'advanced' + + # Default to basic + return 'basic' + + +# Add test for categorization in main() +def main() -> int: + """Main entry point for testing.""" + project_root = Path(__file__).parent.parent + examples_dir = project_root / "examples" + + if not examples_dir.exists(): + print(f"Error: {examples_dir} not found", file=sys.stderr) + return 1 + + # Analyze all Python files + examples = [] + for example_file in sorted(examples_dir.glob("*.py")): + info = analyze_example(example_file) + examples.append(info) + + # Categorize + categories: dict[str, list[ExampleInfo]] = {} + for info in examples: + category = categorize_example(info) + categories.setdefault(category, []).append(info) + + # Print summary + print(f"Found {len(examples)} examples") + for category, items in sorted(categories.items()): + interactive_count = sum(1 for e in items if e.is_interactive) + print(f" {category}: {len(items)} examples ({interactive_count} interactive)") + + return 0 +``` + +**Test:** + +1. Add test to `tests/test_generate_docs.py`: + +```python +def test_categorize_basic(): + """Test basic example categorization.""" + from pathlib import Path + info = generate_docs.ExampleInfo( + Path("scatter.py"), "scatter", "", {'plotille'}, True + ) + assert generate_docs.categorize_example(info) == 'basic' + + +def test_categorize_canvas(): + """Test canvas example categorization.""" + from pathlib import Path + info = generate_docs.ExampleInfo( + Path("canvas_test.py"), "canvas_test", "", {'plotille'}, True + ) + assert generate_docs.categorize_example(info) == 'canvas' + + +def test_categorize_advanced(): + """Test advanced example categorization.""" + from pathlib import Path + info = generate_docs.ExampleInfo( + Path("image.py"), "image", "", {'PIL', 'plotille'}, False + ) + assert generate_docs.categorize_example(info) == 'advanced' +``` + +2. Run tests: +```bash +pytest tests/test_generate_docs.py -v +python scripts/generate_docs.py +# Should show categorized summary +``` + +**Commit:** `Add example categorization logic` + +--- + +## Phase 3: Static Example Pre-rendering + +### Task 3.1: Implement Example Execution and Output Capture + +**What:** Add ability to execute examples and capture their terminal output. + +**Files to modify:** +- `scripts/generate_docs.py` + +**Actions:** + +1. Add execution logic to `scripts/generate_docs.py`: + +```python +# Add imports at top +import subprocess +import shutil +from dataclasses import dataclass + +# Add after ExampleInfo + + +@dataclass +class ExampleOutput: + """Captured output from running an example.""" + stdout: str + stderr: str + returncode: int + success: bool + + +def execute_example(example_path: Path, timeout: int = 30) -> ExampleOutput: + """ + Execute an example and capture its output. + + Args: + example_path: Path to example Python file + timeout: Maximum execution time in seconds + + Returns: + ExampleOutput with captured stdout/stderr + """ + try: + result = subprocess.run( + [sys.executable, str(example_path)], + capture_output=True, + text=True, + timeout=timeout, + cwd=example_path.parent, + ) + + return ExampleOutput( + stdout=result.stdout, + stderr=result.stderr, + returncode=result.returncode, + success=result.returncode == 0, + ) + except subprocess.TimeoutExpired: + return ExampleOutput( + stdout="", + stderr=f"Example timed out after {timeout} seconds", + returncode=-1, + success=False, + ) + except Exception as e: + return ExampleOutput( + stdout="", + stderr=f"Error executing example: {e}", + returncode=-1, + success=False, + ) + + +def save_example_output( + info: ExampleInfo, + output: ExampleOutput, + output_dir: Path, +) -> Path: + """ + Save example output to a file. + + Args: + info: ExampleInfo for the example + output: ExampleOutput to save + output_dir: Directory to save output files + + Returns: + Path to saved output file + """ + output_dir.mkdir(parents=True, exist_ok=True) + output_file = output_dir / f"{info.name}.txt" + + content = output.stdout + if not output.success and output.stderr: + content += f"\n\nErrors:\n{output.stderr}" + + output_file.write_text(content) + return output_file +``` + +**Test:** + +1. Add test to `tests/test_generate_docs.py`: + +```python +def test_execute_example(tmp_path): + """Test executing a simple example.""" + # Create a simple test example + test_example = tmp_path / "test.py" + test_example.write_text('print("Hello from example")') + + output = generate_docs.execute_example(test_example) + + assert output.success is True + assert output.returncode == 0 + assert "Hello from example" in output.stdout + + +def test_execute_example_error(tmp_path): + """Test executing an example that fails.""" + test_example = tmp_path / "test.py" + test_example.write_text('raise ValueError("test error")') + + output = generate_docs.execute_example(test_example) + + assert output.success is False + assert output.returncode != 0 + assert "ValueError" in output.stderr + + +def test_save_example_output(tmp_path): + """Test saving example output.""" + from pathlib import Path + + info = generate_docs.ExampleInfo( + Path("test.py"), "test", "desc", set(), True + ) + output = generate_docs.ExampleOutput( + stdout="test output", stderr="", returncode=0, success=True + ) + + output_dir = tmp_path / "outputs" + saved_path = generate_docs.save_example_output(info, output, output_dir) + + assert saved_path.exists() + assert saved_path.read_text() == "test output" +``` + +2. Run tests: +```bash +pytest tests/test_generate_docs.py::test_execute_example -v +pytest tests/test_generate_docs.py::test_save_example_output -v +``` + +**Commit:** `Add example execution and output capture` + +--- + +### Task 3.2: Generate Pre-rendered Output for Static Examples + +**What:** Execute all static examples during doc generation and save outputs. + +**Files to modify:** +- `scripts/generate_docs.py` + +**Actions:** + +1. Add generation function to `scripts/generate_docs.py`: + +```python +def generate_static_outputs( + examples: list[ExampleInfo], + output_dir: Path, +) -> dict[str, Path]: + """ + Execute static examples and save their outputs. + + Args: + examples: List of ExampleInfo to process + output_dir: Directory to save outputs + + Returns: + Dict mapping example name to output file path + """ + outputs = {} + + static_examples = [e for e in examples if not e.is_interactive] + + print(f"\nGenerating outputs for {len(static_examples)} static examples...") + + for info in static_examples: + print(f" Executing {info.name}...", end=" ") + + output = execute_example(info.path) + + if output.success: + output_path = save_example_output(info, output, output_dir) + outputs[info.name] = output_path + print("āœ“") + else: + print(f"āœ— (failed)") + if output.stderr: + print(f" Error: {output.stderr[:100]}") + + return outputs + + +# Update main() to call this function +def main() -> int: + """Main entry point.""" + project_root = Path(__file__).parent.parent + examples_dir = project_root / "examples" + output_dir = project_root / "docs" / "assets" / "example-outputs" + + if not examples_dir.exists(): + print(f"Error: {examples_dir} not found", file=sys.stderr) + return 1 + + # Analyze all Python files + examples = [] + for example_file in sorted(examples_dir.glob("*.py")): + info = analyze_example(example_file) + examples.append(info) + + # Categorize + categories: dict[str, list[ExampleInfo]] = {} + for info in examples: + category = categorize_example(info) + categories.setdefault(category, []).append(info) + + # Print summary + print(f"Found {len(examples)} examples") + for category, items in sorted(categories.items()): + interactive_count = sum(1 for e in items if e.is_interactive) + print(f" {category}: {len(items)} examples ({interactive_count} interactive)") + + # Generate static outputs + outputs = generate_static_outputs(examples, output_dir) + print(f"\nGenerated {len(outputs)} static outputs") + + return 0 +``` + +**Test:** + +Run the script manually to verify it executes examples: + +```bash +python scripts/generate_docs.py +# Should execute static examples and save outputs to docs/assets/example-outputs/ +ls docs/assets/example-outputs/ +# Verify output files were created +``` + +**Commit:** `Add static example pre-rendering` + +--- + +## Phase 4: Markdown Generation for Examples + +### Task 4.1: Create Markdown Templates + +**What:** Build templates for rendering examples as markdown. + +**Files to modify:** +- `scripts/generate_docs.py` + +**Actions:** + +1. Add template functions to `scripts/generate_docs.py`: + +```python +def generate_interactive_example_markdown(info: ExampleInfo) -> str: + """ + Generate markdown for an interactive example. + + Args: + info: ExampleInfo for the example + + Returns: + Markdown string with interactive code editor + """ + source_code = info.path.read_text() + + # Escape backticks in code for markdown + escaped_code = source_code.replace('```', '\\`\\`\\`') + + return f"""## {info.name} + +{info.description} + +
+
+ [python3 {info.name}.py] + +
+
+
+ +
+
+ root@plotille:~$ python3 {info.name}.py +
+
+
+
+ +""" + + +def generate_static_example_markdown( + info: ExampleInfo, + output_path: Path, +) -> str: + """ + Generate markdown for a static example with pre-rendered output. + + Args: + info: ExampleInfo for the example + output_path: Path to pre-rendered output file + + Returns: + Markdown string with code and output + """ + source_code = info.path.read_text() + + # Read pre-rendered output + if output_path.exists(): + output = output_path.read_text() + else: + output = "Output not available" + + deps = ', '.join(sorted(info.imports - {'plotille'})) + + return f"""## {info.name} + +{info.description} + +!!! info "External Dependencies" + This example requires: **{deps}** + + Output is pre-rendered below. To run interactively, install dependencies locally. + +**Code:** + +```python +{source_code} +``` + +**Output:** + +
+
+ [output: {info.name}.py] +
+
+
{output}
+
+
+ +""" +``` + +**Test:** + +1. Add test to `tests/test_generate_docs.py`: + +```python +def test_generate_interactive_example_markdown(): + """Test generating markdown for interactive example.""" + from pathlib import Path + + info = generate_docs.ExampleInfo( + path=Path("test.py"), + name="test", + description="Test example", + imports={'plotille'}, + is_interactive=True, + ) + + # Mock the file reading + import unittest.mock as mock + with mock.patch.object(Path, 'read_text', return_value='print("hi")'): + markdown = generate_docs.generate_interactive_example_markdown(info) + + assert '## test' in markdown + assert 'Test example' in markdown + assert 'interactive-example' in markdown + assert 'print("hi")' in markdown + + +def test_generate_static_example_markdown(tmp_path): + """Test generating markdown for static example.""" + from pathlib import Path + + info = generate_docs.ExampleInfo( + path=Path("test.py"), + name="test", + description="Test example", + imports={'plotille', 'numpy'}, + is_interactive=False, + ) + + # Create mock output file + output_path = tmp_path / "test.txt" + output_path.write_text("Example output here") + + import unittest.mock as mock + with mock.patch.object(Path, 'read_text', return_value='print("hi")'): + markdown = generate_docs.generate_static_example_markdown(info, output_path) + + assert '## test' in markdown + assert 'Test example' in markdown + assert 'numpy' in markdown + assert 'Example output here' in markdown +``` + +2. Run tests: +```bash +pytest tests/test_generate_docs.py::test_generate_interactive_example_markdown -v +pytest tests/test_generate_docs.py::test_generate_static_example_markdown -v +``` + +**Commit:** `Add markdown generation templates for examples` + +--- + +### Task 4.2: Generate Category Pages + +**What:** Create markdown files for each category of examples. + +**Files to modify:** +- `scripts/generate_docs.py` + +**Actions:** + +1. Add page generation to `scripts/generate_docs.py`: + +```python +def generate_category_page( + category: str, + examples: list[ExampleInfo], + output_paths: dict[str, Path], + docs_dir: Path, +) -> Path: + """ + Generate a markdown page for a category of examples. + + Args: + category: Category name + examples: List of examples in this category + output_paths: Dict of pre-rendered output paths + docs_dir: Documentation directory + + Returns: + Path to generated markdown file + """ + category_titles = { + 'basic': 'Basic Plots', + 'figures': 'Complex Figures', + 'canvas': 'Canvas Drawing', + 'advanced': 'Advanced Examples', + } + + title = category_titles.get(category, category.title()) + + # Build page content + content = [f"# {title}\n"] + + # Add description + descriptions = { + 'basic': 'Simple plotting examples to get started with plotille.', + 'figures': 'Multi-plot figures and complex visualizations.', + 'canvas': 'Direct canvas manipulation for custom drawings.', + 'advanced': 'Examples using external libraries like NumPy and Pillow.', + } + + if category in descriptions: + content.append(f"{descriptions[category]}\n") + + # Add each example + for info in examples: + if info.is_interactive: + markdown = generate_interactive_example_markdown(info) + else: + output_path = output_paths.get(info.name, Path()) + markdown = generate_static_example_markdown(info, output_path) + + content.append(markdown) + + # Write file + category_dir = docs_dir / "cookbook" + category_dir.mkdir(parents=True, exist_ok=True) + + output_file = category_dir / f"{category}.md" + output_file.write_text('\n'.join(content)) + + return output_file + + +# Update main() to generate pages +def main() -> int: + """Main entry point.""" + project_root = Path(__file__).parent.parent + examples_dir = project_root / "examples" + output_dir = project_root / "docs" / "assets" / "example-outputs" + docs_dir = project_root / "docs" + + if not examples_dir.exists(): + print(f"Error: {examples_dir} not found", file=sys.stderr) + return 1 + + # Analyze all Python files + examples = [] + for example_file in sorted(examples_dir.glob("*.py")): + info = analyze_example(example_file) + examples.append(info) + + # Categorize + categories: dict[str, list[ExampleInfo]] = {} + for info in examples: + category = categorize_example(info) + categories.setdefault(category, []).append(info) + + # Print summary + print(f"Found {len(examples)} examples") + for category, items in sorted(categories.items()): + interactive_count = sum(1 for e in items if e.is_interactive) + print(f" {category}: {len(items)} examples ({interactive_count} interactive)") + + # Generate static outputs + output_paths = generate_static_outputs(examples, output_dir) + print(f"\nGenerated {len(output_paths)} static outputs") + + # Generate category pages + print("\nGenerating category pages...") + for category, items in sorted(categories.items()): + page_path = generate_category_page(category, items, output_paths, docs_dir) + print(f" {category}: {page_path}") + + print("\nāœ“ Documentation generation complete") + return 0 +``` + +**Test:** + +```bash +python scripts/generate_docs.py +# Should generate markdown files in docs/cookbook/ +ls docs/cookbook/ +# Verify .md files exist: basic.md, figures.md, canvas.md, advanced.md +``` + +**Commit:** `Add category page generation` + +--- + +### Task 4.3: Generate Home Page + +**What:** Create the hero home page with animated introduction. + +**Files to create:** +- `docs/index.md` + +**Actions:** + +1. Update `scripts/generate_docs.py` to generate home page: + +```python +def generate_home_page(docs_dir: Path) -> Path: + """ + Generate the home/index page. + + Args: + docs_dir: Documentation directory + + Returns: + Path to generated index.md + """ + content = """# plotille + +
+
+ [root@plotille ~]$ +
+
+

+    
+
+ +Plot in the terminal using braille dots, with no dependencies. + +## Features + +- **Scatter plots, line plots, histograms** - Basic plotting functions +- **Complex figures** - Compose multiple plots with legends +- **Canvas drawing** - Direct pixel manipulation for custom visualizations +- **Image rendering** - Display images using braille dots or background colors +- **Color support** - Multiple color modes: names, byte values, RGB +- **No dependencies** - Pure Python with no external requirements + +## Quick Start + +Install plotille: + +```bash +pip install plotille +``` + +Create your first plot: + +```python +import plotille +import math + +X = [i/10 for i in range(-30, 30)] +Y = [math.sin(x) for x in X] + +print(plotille.plot(X, Y, height=20, width=60)) +``` + +## Explore + +Browse the [cookbook](cookbook/basic.md) to see interactive examples you can edit and run in your browser. + +""" + + index_file = docs_dir / "index.md" + index_file.write_text(content) + return index_file + + +# Add to main() +def main() -> int: + """Main entry point.""" + # ... existing code ... + + # Generate category pages + print("\nGenerating category pages...") + for category, items in sorted(categories.items()): + page_path = generate_category_page(category, items, output_paths, docs_dir) + print(f" {category}: {page_path}") + + # Generate home page + print("\nGenerating home page...") + index_path = generate_home_page(docs_dir) + print(f" index: {index_path}") + + print("\nāœ“ Documentation generation complete") + return 0 +``` + +**Test:** + +```bash +python scripts/generate_docs.py +# Verify docs/index.md was created +cat docs/index.md +# Should show hero content +``` + +**Commit:** `Add home page generation` + +--- + +## Phase 5: Amber Phosphor Theme + +### Task 5.1: Create Custom CSS for Terminal Aesthetic + +**What:** Build CSS for the amber phosphor CRT theme. + +**Files to create:** +- `docs/stylesheets/terminal.css` + +**Actions:** + +1. Create directory: +```bash +mkdir -p docs/stylesheets +``` + +2. Create `docs/stylesheets/terminal.css`: + +```css +/* + * Plotille Documentation Theme + * Amber Phosphor CRT Terminal Aesthetic + */ + +/* Import retro terminal fonts */ +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=VT323&display=swap'); + +:root { + /* Amber phosphor color palette */ + --amber-black: #0a0a0a; + --amber-dark: #1a1200; + --amber-dim: #cc8800; + --amber-base: #ffb000; + --amber-bright: #ffd000; + --amber-glow: rgba(255, 176, 0, 0.4); + + /* Spacing */ + --terminal-padding: 1rem; + --terminal-border: 2px; +} + +/* Global overrides for Material theme */ +[data-md-color-scheme="plotille"] { + --md-primary-fg-color: var(--amber-base); + --md-primary-fg-color--light: var(--amber-bright); + --md-primary-fg-color--dark: var(--amber-dim); + --md-accent-fg-color: var(--amber-bright); + + --md-default-bg-color: var(--amber-black); + --md-default-fg-color: var(--amber-base); + --md-code-bg-color: var(--amber-dark); + --md-code-fg-color: var(--amber-base); +} + +/* Typography */ +body { + font-family: 'IBM Plex Mono', monospace; + background: var(--amber-black); + color: var(--amber-base); +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'VT323', monospace; + color: var(--amber-bright); + text-shadow: 0 0 8px var(--amber-glow); + letter-spacing: 0.05em; +} + +/* CRT scanline effect */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient( + transparent 50%, + rgba(0, 0, 0, 0.1) 50% + ); + background-size: 100% 4px; + pointer-events: none; + z-index: 9999; + animation: scanline 8s linear infinite; +} + +@keyframes scanline { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(4px); + } +} + +/* Phosphor glow on text */ +.md-content { + text-shadow: 0 0 2px var(--amber-glow); +} + +/* Terminal window styling */ +.terminal-window { + background: var(--amber-black); + border: var(--terminal-border) solid var(--amber-dim); + border-radius: 4px; + margin: 1.5rem 0; + box-shadow: 0 0 20px var(--amber-glow); + overflow: hidden; +} + +.terminal-header { + background: var(--amber-dark); + padding: 0.5rem var(--terminal-padding); + border-bottom: 1px solid var(--amber-dim); + display: flex; + justify-content: space-between; + align-items: center; +} + +.terminal-title { + font-family: 'IBM Plex Mono', monospace; + color: var(--amber-base); + font-size: 0.9rem; +} + +.terminal-run-btn { + font-family: 'VT323', monospace; + background: var(--amber-dim); + color: var(--amber-black); + border: 1px solid var(--amber-base); + padding: 0.25rem 0.75rem; + cursor: pointer; + font-size: 1rem; + transition: all 0.2s; +} + +.terminal-run-btn:hover { + background: var(--amber-base); + box-shadow: 0 0 10px var(--amber-glow); +} + +.terminal-body { + padding: var(--terminal-padding); + background: var(--amber-black); +} + +.terminal-output { + font-family: 'IBM Plex Mono', monospace; + color: var(--amber-base); + white-space: pre; + margin-top: 1rem; + line-height: 1.4; +} + +.terminal-prompt { + color: var(--amber-bright); + display: block; + margin-bottom: 0.5rem; +} + +.terminal-prompt::after { + content: ''; + display: inline-block; + width: 8px; + height: 14px; + background: var(--amber-base); + margin-left: 4px; + animation: blink 1s step-end infinite; +} + +@keyframes blink { + 50% { + opacity: 0; + } +} + +/* Code editor styling */ +.code-editor-wrapper { + border: 1px solid var(--amber-dim); + border-radius: 2px; +} + +.code-editor { + font-family: 'IBM Plex Mono', monospace; + background: var(--amber-dark); + color: var(--amber-base); + width: 100%; + min-height: 300px; + padding: 1rem; + border: none; + resize: vertical; +} + +/* Static example output */ +.static-example .terminal-output { + max-height: 600px; + overflow-y: auto; +} + +/* Hero terminal animation */ +.hero-terminal { + margin: 2rem 0; + font-size: 0.8rem; +} + +.hero-plot { + min-height: 300px; + margin: 0; +} + +/* Braille dot decorations */ +.braille-divider { + text-align: center; + color: var(--amber-dim); + font-size: 1.5rem; + margin: 2rem 0; + opacity: 0.3; +} + +/* Navigation styled as terminal prompt */ +.md-sidebar--primary { + background: var(--amber-black); +} + +.md-nav__link { + font-family: 'IBM Plex Mono', monospace; + color: var(--amber-base); +} + +.md-nav__link--active { + color: var(--amber-bright); +} + +.md-nav__link::before { + content: '> '; + color: var(--amber-dim); + opacity: 0; + transition: opacity 0.2s; +} + +.md-nav__link--active::before, +.md-nav__link:hover::before { + opacity: 1; +} + +/* Code blocks */ +.highlight { + background: var(--amber-dark) !important; + border: 1px solid var(--amber-dim); +} + +.highlight pre { + color: var(--amber-base); +} + +/* Admonitions (info boxes) */ +.admonition { + background: var(--amber-dark); + border-left: 4px solid var(--amber-base); + color: var(--amber-base); +} +``` + +**Test:** + +1. Update `mkdocs.yml` to use custom CSS: + +```yaml +theme: + name: material + palette: + scheme: plotille + custom_dir: docs/overrides + features: + - content.code.copy + - navigation.sections + +extra_css: + - stylesheets/terminal.css +``` + +2. Test the site: +```bash +mkdocs serve +# Open browser to http://127.0.0.1:8000 +# Verify amber color scheme appears +``` + +**Commit:** `Add amber phosphor terminal theme CSS` + +--- + +### Task 5.2: Add Terminal Font Files (Optional Fallback) + +**What:** Optionally bundle fonts locally for offline use. + +**Files to create:** +- `docs/stylesheets/fonts.css` (if bundling fonts) + +**Actions:** + +This task is optional. If you want to bundle fonts locally instead of using Google Fonts CDN: + +1. Download IBM Plex Mono and VT323 font files +2. Place in `docs/fonts/` directory +3. Update CSS `@font-face` declarations +4. Update import in `terminal.css` + +For now, **skip this task** and rely on Google Fonts CDN for simplicity (YAGNI). + +**Commit:** (Skip) + +--- + +## Phase 6: Brython Integration + +### Task 6.1: Add Brython Runtime and Setup + +**What:** Include Brython library and initialize runtime. + +**Files to create:** +- `docs/javascripts/brython-setup.js` + +**Actions:** + +1. Create directory: +```bash +mkdir -p docs/javascripts +``` + +2. Create `docs/javascripts/brython-setup.js`: + +```javascript +/** + * Brython setup and initialization for plotille documentation. + */ + +// Initialize Brython when page loads +document.addEventListener('DOMContentLoaded', function() { + // Check if Brython is loaded + if (typeof brython === 'undefined') { + console.error('Brython not loaded'); + return; + } + + // Initialize Brython + brython({ + debug: 1, // Show errors in console + pythonpath: ['/src/lib'] + }); + + console.log('Brython initialized'); +}); + +/** + * Execute Python code in an example. + * + * @param {string} exampleName - Name of the example to run + */ +function runExample(exampleName) { + const editor = document.getElementById(`editor-${exampleName}`); + const outputDiv = document.querySelector(`#output-${exampleName} .output-content`); + + if (!editor || !outputDiv) { + console.error(`Example ${exampleName} not found`); + return; + } + + const code = editor.value; + + // Clear previous output + outputDiv.textContent = ''; + outputDiv.classList.remove('error'); + + // Create output capture + let capturedOutput = []; + + // Redirect stdout + const originalWrite = console.log; + console.log = function(...args) { + capturedOutput.push(args.join(' ')); + originalWrite.apply(console, args); + }; + + try { + // Execute Python code + window.__BRYTHON__.python_to_js(code); + const result = eval(window.__BRYTHON__.imported['__main__']); + + // Display output + if (capturedOutput.length > 0) { + outputDiv.textContent = capturedOutput.join('\n'); + } else if (result !== undefined) { + outputDiv.textContent = String(result); + } else { + outputDiv.textContent = '(no output)'; + } + } catch (error) { + // Display error + outputDiv.classList.add('error'); + outputDiv.textContent = `Error: ${error.message}\n\n${error.stack || ''}`; + } finally { + // Restore stdout + console.log = originalWrite; + } +} + +// Make runExample globally available +window.runExample = runExample; +``` + +3. Update `mkdocs.yml` to include Brython: + +```yaml +extra_javascript: + - https://cdn.jsdelivr.net/npm/brython@3.12.0/brython.min.js + - https://cdn.jsdelivr.net/npm/brython@3.12.0/brython_stdlib.js + - javascripts/brython-setup.js + +extra_css: + - stylesheets/terminal.css +``` + +**Test:** + +1. Create a simple test page `docs/test-brython.md`: + +```markdown +# Brython Test + +
+
+ [python3 test.py] + +
+
+ +
+ root@plotille:~$ +
+
+
+
+``` + +2. Update `mkdocs.yml` nav to include test page: + +```yaml +nav: + - Home: index.md + - Test: test-brython.md +``` + +3. Test: +```bash +mkdocs serve +# Visit http://127.0.0.1:8000/test-brython/ +# Click RUN button, verify "Hello from Brython!" appears +``` + +**Commit:** `Add Brython runtime integration` + +--- + +### Task 6.2: Implement Plotille Mock for Brython + +**What:** Since plotille won't work directly in Brython, create a browser-compatible version. + +**Considerations:** + +This is complex. Plotille uses features that may not work in Brython. For the initial implementation: + +**Option A:** Bundle the actual plotille source and hope it works in Brython +**Option B:** Create a simplified browser-compatible version +**Option C:** Use a server-side execution API (more complex) + +**Recommended approach:** Try Option A first (simplest per YAGNI). If plotille doesn't work in Brython, we'll need to discuss alternatives. + +**Files to create:** +- `docs/javascripts/plotille-brython.js` (loader script) + +**Actions:** + +1. Copy plotille source to docs for Brython access: + +```bash +# Create a script to copy plotille sources +cat > scripts/copy_plotille_for_brython.py << 'EOF' +#!/usr/bin/env python3 +"""Copy plotille source files for Brython access.""" +import shutil +from pathlib import Path + +def main(): + project_root = Path(__file__).parent.parent + source_dir = project_root / "plotille" + dest_dir = project_root / "docs" / "src" / "lib" / "plotille" + + # Remove old copy + if dest_dir.exists(): + shutil.rmtree(dest_dir) + + # Copy plotille source + shutil.copytree(source_dir, dest_dir) + print(f"Copied plotille to {dest_dir}") + +if __name__ == "__main__": + main() +EOF + +chmod +x scripts/copy_plotille_for_brython.py +``` + +2. Run the copy script: +```bash +python scripts/copy_plotille_for_brython.py +``` + +3. Update `scripts/generate_docs.py` to run this during generation: + +```python +# Add at the end of main() before return +def main() -> int: + # ... existing code ... + + # Copy plotille for Brython + print("\nCopying plotille for Brython...") + copy_script = project_root / "scripts" / "copy_plotille_for_brython.py" + subprocess.run([sys.executable, str(copy_script)], check=True) + + print("\nāœ“ Documentation generation complete") + return 0 +``` + +**Test:** + +This requires actual testing with examples. We'll validate this in the next phase when integrating CodeMirror. + +**Commit:** `Add plotille source copying for Brython access` + +--- + +## Phase 7: CodeMirror Integration + +### Task 7.1: Add CodeMirror 6 Setup + +**What:** Integrate CodeMirror 6 for code editing with Python syntax highlighting. + +**Files to create:** +- `docs/javascripts/codemirror-setup.js` + +**Actions:** + +1. Update `mkdocs.yml` to include CodeMirror from CDN: + +```yaml +extra_javascript: + - https://cdn.jsdelivr.net/npm/brython@3.12.0/brython.min.js + - https://cdn.jsdelivr.net/npm/brython@3.12.0/brython_stdlib.js + # CodeMirror 6 + - https://cdn.jsdelivr.net/npm/codemirror@6.0.1/dist/index.min.js + - https://cdn.jsdelivr.net/npm/@codemirror/lang-python@6.1.3/dist/index.min.js + - https://cdn.jsdelivr.net/npm/@codemirror/theme-one-dark@6.1.2/dist/index.min.js + - javascripts/codemirror-setup.js + - javascripts/brython-setup.js +``` + +2. Create `docs/javascripts/codemirror-setup.js`: + +```javascript +/** + * CodeMirror 6 setup for plotille documentation. + * + * Converts textarea elements into CodeMirror editors with Python highlighting. + */ + +document.addEventListener('DOMContentLoaded', function() { + // Wait for CodeMirror to load + if (typeof CodeMirror === 'undefined') { + console.error('CodeMirror not loaded'); + return; + } + + // Find all code editor textareas + const editors = document.querySelectorAll('.code-editor'); + + editors.forEach(textarea => { + const editorId = textarea.id; + const initialCode = textarea.value; + + // Create CodeMirror editor + // Note: This uses basic textarea for now + // Full CodeMirror 6 integration would require bundling + // For simplicity, we'll enhance the textarea with basic features + + textarea.style.fontFamily = "'IBM Plex Mono', monospace"; + textarea.style.fontSize = '14px'; + textarea.style.lineHeight = '1.5'; + textarea.style.tabSize = '4'; + + // Add tab key support + textarea.addEventListener('keydown', function(e) { + if (e.key === 'Tab') { + e.preventDefault(); + const start = this.selectionStart; + const end = this.selectionEnd; + const value = this.value; + + // Insert 4 spaces + this.value = value.substring(0, start) + ' ' + value.substring(end); + this.selectionStart = this.selectionEnd = start + 4; + } + }); + + console.log(`Editor initialized: ${editorId}`); + }); +}); +``` + +**Note:** Full CodeMirror 6 integration from CDN is complex. The above provides basic textarea enhancement. If you need full CodeMirror features (syntax highlighting, autocomplete), you'll need to either: + +A) Bundle CodeMirror properly with a build step +B) Use a simpler approach (current implementation) +C) Use CDN but with more complex module loading + +For now, **proceed with enhanced textarea** (YAGNI). Full CodeMirror can be added later if needed. + +**Test:** + +```bash +mkdocs serve +# Visit test-brython page +# Verify code editor has monospace font and tab key works +``` + +**Commit:** `Add CodeMirror setup with textarea enhancement` + +--- + +### Task 7.2: Improve Brython Execution with Output Capture + +**What:** Better stdout capturing for Brython execution. + +**Files to modify:** +- `docs/javascripts/brython-setup.js` + +**Actions:** + +Replace the `runExample` function in `brython-setup.js`: + +```javascript +/** + * Execute Python code in an example with proper output capture. + * + * @param {string} exampleName - Name of the example to run + */ +function runExample(exampleName) { + const editor = document.getElementById(`editor-${exampleName}`); + const outputDiv = document.querySelector(`#output-${exampleName} .output-content`); + + if (!editor || !outputDiv) { + console.error(`Example ${exampleName} not found`); + return; + } + + const code = editor.value; + + // Clear previous output + outputDiv.textContent = ''; + outputDiv.classList.remove('error'); + + // Show loading indicator + outputDiv.textContent = 'Running...'; + + // Use setTimeout to allow UI update + setTimeout(() => { + try { + // Create a new output buffer + let outputBuffer = []; + + // Monkey-patch print for output capture + const printFunc = function(...args) { + const line = args.join(' '); + outputBuffer.push(line); + }; + + // Inject print into the Python code + const wrappedCode = ` +import sys +from io import StringIO + +__output__ = StringIO() +__old_stdout__ = sys.stdout +sys.stdout = __output__ + +try: +${code.split('\n').map(line => ' ' + line).join('\n')} +finally: + sys.stdout = __old_stdout__ + print(__output__.getvalue(), end='') +`; + + // Execute with Brython + const script = document.createElement('script'); + script.type = 'text/python'; + script.id = `brython-script-${exampleName}`; + script.textContent = wrappedCode; + + // Add output capture + window.__brython_output__ = ''; + const oldLog = console.log; + console.log = function(...args) { + window.__brython_output__ += args.join(' ') + '\n'; + oldLog.apply(console, args); + }; + + document.body.appendChild(script); + + // Run Brython on this script + if (window.brython) { + brython({debug: 1, ids: [script.id]}); + } + + // Restore console.log + console.log = oldLog; + + // Small delay to capture output + setTimeout(() => { + const output = window.__brython_output__ || '(no output)'; + outputDiv.textContent = output; + + // Clean up + script.remove(); + delete window.__brython_output__; + }, 100); + + } catch (error) { + // Display error + outputDiv.classList.add('error'); + outputDiv.textContent = `Error: ${error.message}`; + console.error('Brython execution error:', error); + } + }, 10); +} +``` + +**Test:** + +```bash +mkdocs serve +# Test the example execution again +# Verify output is captured correctly +``` + +**Commit:** `Improve Brython output capture` + +--- + +## Phase 8: API Documentation with mkdocstrings + +### Task 8.1: Configure mkdocstrings for API Reference + +**What:** Set up automatic API documentation generation from docstrings. + +**Files to modify:** +- `mkdocs.yml` + +**Actions:** + +1. Update `mkdocs.yml` plugins section: + +```yaml +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_source: true + show_root_heading: true + show_root_full_path: false + show_signature_annotations: true + separate_signature: true + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: true + merge_init_into_class: true + paths: [plotille] +``` + +2. Create API reference structure in `docs/api/`: + +```bash +mkdir -p docs/api +``` + +3. Create `docs/api/index.md`: + +```markdown +# API Reference + +Complete API documentation for plotille. + +## High-Level Functions + +Quick plotting functions for simple use cases. + +- [Plotting Functions](plotting.md) - `plot()`, `scatter()`, `histogram()` +- [Figure Class](figure.md) - Compose complex multi-plot visualizations + +## Core Components + +- [Canvas](canvas.md) - Low-level drawing primitives +- [Colors](colors.md) - Color handling and themes + +## Utilities + +- [Input Formatting](formatting.md) - Data preprocessing +- [Data Types](datatypes.md) - Internal data structures +``` + +4. Create `docs/api/plotting.md`: + +```markdown +# Plotting Functions + +High-level plotting functions for quick visualizations. + +## plot + +::: plotille.plot + options: + show_root_heading: true + show_source: true + +## scatter + +::: plotille.scatter + options: + show_root_heading: true + show_source: true + +## hist + +::: plotille.hist + options: + show_root_heading: true + show_source: true + +## histogram + +::: plotille.histogram + options: + show_root_heading: true + show_source: true +``` + +5. Create `docs/api/figure.md`: + +```markdown +# Figure + +The Figure class for composing complex visualizations. + +::: plotille.Figure + options: + show_root_heading: true + show_source: true + members: + - __init__ + - plot + - scatter + - histogram + - text + - axvline + - axhline + - axvspan + - axhspan + - imgshow + - show + - clear +``` + +6. Create `docs/api/canvas.md`: + +```markdown +# Canvas + +Low-level canvas for direct drawing. + +::: plotille.Canvas + options: + show_root_heading: true + show_source: true + members: + - __init__ + - point + - line + - rect + - text + - braille_image + - image + - plot +``` + +**Test:** + +```bash +mkdocs serve +# Visit http://127.0.0.1:8000/api/ +# Verify API documentation appears with docstrings +``` + +**Commit:** `Add mkdocstrings API reference configuration` + +--- + +### Task 8.2: Enhance Docstrings with Examples (Sample) + +**What:** Add doctest examples to key functions as a template. + +**Files to modify:** +- `plotille/_graphs.py` (or wherever `plot()` is defined) + +**Actions:** + +This task demonstrates enhancing ONE function as an example. You'll repeat this pattern for other functions. + +1. Find the `plot()` function (likely in `plotille/__init__.py` or `plotille/_graphs.py`) + +2. Enhance its docstring with a doctest example: + +```python +def plot( + X, + Y, + width=80, + height=40, + X_label='X', + Y_label='Y', + linesep=os.linesep, + interp='linear', + x_min=None, + x_max=None, + y_min=None, + y_max=None, + lc=None, + bg=None, + color_mode='names', + origin=True, + marker=None, +): + """ + Create plot with X, Y values and linear interpolation between points. + + Parameters: + X: List[float] X values. + Y: List[float] Y values. X and Y must have the same number of entries. + width: int The number of characters for the width (columns) of the canvas. + height: int The number of characters for the height (rows) of the canvas. + X_label: str Label for X-axis. + Y_label: str Label for Y-axis. max 8 characters. + linesep: str The requested line separator. default: os.linesep + interp: Optional[str] Specify interpolation; values None, 'linear' + x_min, x_max: float Limits for the displayed X values. + y_min, y_max: float Limits for the displayed Y values. + lc: multiple Give the line color. + bg: multiple Give the background color. + color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb' + see plotille.color.__docs__ + origin: bool Whether to print the origin. default: True + marker: str Instead of braille dots set a marker char for actual values. + + Returns: + str: plot over `X`, `Y`. + + Examples: + Simple line plot: + + >>> import plotille + >>> X = [1, 2, 3, 4, 5] + >>> Y = [1, 4, 2, 3, 5] + >>> result = plotille.plot(X, Y, width=40, height=10) + >>> 'ā €' in result # Contains braille dots + True + >>> 'X' in result # Contains axis label + True + + Plot with custom range: + + >>> result = plotille.plot([0, 1], [0, 1], width=20, height=5, + ... x_min=0, x_max=1, y_min=0, y_max=1) + >>> len(result) > 0 + True + """ + # ... existing implementation ... +``` + +**Test:** + +```bash +# Run doctests +pytest --doctest-modules plotille/_graphs.py -v +# Or wherever plot() is defined + +# Should show doctests passing +``` + +**Commit:** `Add doctest examples to plot() function` + +--- + +### Task 8.3: Add Doctests to Core Functions (Iterative) + +**What:** Systematically add doctest examples to all public functions. + +**Files to modify:** +- All files in `plotille/` directory with public functions + +**Actions:** + +This is a large task. Break it down: + +1. Create a checklist of functions to document: + +```bash +# Generate list of public functions +python -c " +import plotille +import inspect + +members = inspect.getmembers(plotille, inspect.isfunction) +public = [name for name, _ in members if not name.startswith('_')] +for name in sorted(public): + print(f'- [ ] {name}') +" > docs/plans/doctest-checklist.md +``` + +2. For each function: + - Read existing docstring + - Add at least one `>>>` example showing basic usage + - Add edge case examples if relevant + - Run `pytest --doctest-modules` to verify + - Commit with message like `Add doctests to scatter() function` + +3. Prioritize by importance: + - High-level functions first (`plot`, `scatter`, `hist`, etc.) + - Then `Figure` methods + - Then `Canvas` methods + - Finally utility functions + +**Test:** + +After each function: +```bash +pytest --doctest-modules plotille/ -v +# All doctests should pass +``` + +**This is iterative:** Do a few functions, commit, repeat. Don't do all at once. + +**Commit pattern:** `Add doctests to ()` + +--- + +## Phase 9: Navigation and Site Structure + +### Task 9.1: Update Navigation in mkdocs.yml + +**What:** Define the site navigation structure. + +**Files to modify:** +- `mkdocs.yml` + +**Actions:** + +Update the `nav` section in `mkdocs.yml`: + +```yaml +nav: + - Home: index.md + - Cookbook: + - Basic Plots: cookbook/basic.md + - Complex Figures: cookbook/figures.md + - Canvas Drawing: cookbook/canvas.md + - Advanced Examples: cookbook/advanced.md + - API Reference: + - Overview: api/index.md + - Plotting Functions: api/plotting.md + - Figure: api/figure.md + - Canvas: api/canvas.md +``` + +**Test:** + +```bash +mkdocs serve +# Verify navigation structure appears correctly +# All links work +``` + +**Commit:** `Configure site navigation structure` + +--- + +### Task 9.2: Customize Navigation Sidebar Styling + +**What:** Style the navigation to match terminal aesthetic. + +**Files to modify:** +- `docs/stylesheets/terminal.css` + +**Actions:** + +Add to `docs/stylesheets/terminal.css`: + +```css +/* Navigation as terminal directory listing */ +.md-nav { + font-family: 'IBM Plex Mono', monospace; +} + +.md-nav__title { + font-family: 'VT323', monospace; + color: var(--amber-bright); + font-size: 1.2rem; + text-shadow: 0 0 5px var(--amber-glow); +} + +.md-nav__list { + list-style: none; +} + +.md-nav__item { + position: relative; +} + +.md-nav__link { + color: var(--amber-base); + padding-left: 1.5rem; + transition: color 0.2s, text-shadow 0.2s; +} + +.md-nav__link:hover { + color: var(--amber-bright); + text-shadow: 0 0 8px var(--amber-glow); +} + +/* Terminal prompt indicator for active item */ +.md-nav__link--active { + color: var(--amber-bright); + font-weight: 600; +} + +.md-nav__link--active::before { + content: 'root@plotille:~$'; + position: absolute; + left: -8rem; + color: var(--amber-dim); + font-size: 0.85rem; + opacity: 0.7; +} + +/* Folder icons using braille */ +.md-nav__item--nested > .md-nav__link::before { + content: 'ā æ '; + color: var(--amber-dim); +} + +.md-nav__item:not(.md-nav__item--nested) > .md-nav__link::before { + content: '⣿ '; + color: var(--amber-dim); + font-size: 0.6rem; +} +``` + +**Test:** + +```bash +mkdocs serve +# Check navigation sidebar styling +# Verify braille icons appear +# Test hover effects +``` + +**Commit:** `Add terminal-styled navigation sidebar` + +--- + +## Phase 10: Hero Animation + +### Task 10.1: Create Animated Hero Plot + +**What:** Animate a plotille graph appearing on the home page. + +**Files to create:** +- `docs/javascripts/hero-animation.js` + +**Actions:** + +1. Create `docs/javascripts/hero-animation.js`: + +```javascript +/** + * Animated hero plot for home page. + * + * Draws a sine wave using braille dots, character by character. + */ + +function animateHeroPlot() { + const heroPlot = document.getElementById('hero-animation'); + + if (!heroPlot) { + return; + } + + // Example plotille output (sine wave) + // This would ideally be generated server-side + const plotOutput = ` 5.00┤ ā”°ā Šā ‰ā ‰ā ‰ā¢‰ā”­ā ‹ + ┤ ā¢€ā” ā Š ⢸ + ┤ ā¢€ā” ā Šā  ⢸ + ┤ ā¢€ā” ā Šā  ⢸ + ┤ ā¢€ā” ā Šā  ⢸ + 0.00┼⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤┠⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢼⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤ + ┤ ⢠⠊⠁ ⢸ + ┤ ā¢€ā” ā Š ⢸ + ┤ ā¢€ā” ā Šā  ⢸ + ┤ ā¢€ā” ā Šā  ⢸ + -5.00ā”¤ā£€ā£€ā£€ā” ā ¤ā Šā  ā ˆā ‰ā ‰ā ‰ā ‘ā ’ā ¤ā£€ā£€ā£€ + └──────────────────────────────────────────────────────── + -3.14 X 3.14`; + + // Animate character by character + const chars = plotOutput.split(''); + let index = 0; + + heroPlot.textContent = ''; + + const interval = setInterval(() => { + if (index < chars.length) { + heroPlot.textContent += chars[index]; + index++; + } else { + clearInterval(interval); + } + }, 5); // 5ms per character = ~1 second for 200 chars +} + +// Run animation when page loads +document.addEventListener('DOMContentLoaded', function() { + setTimeout(animateHeroPlot, 500); // Slight delay after page load +}); +``` + +2. Update `mkdocs.yml` to include the script: + +```yaml +extra_javascript: + - https://cdn.jsdelivr.net/npm/brython@3.12.0/brython.min.js + - https://cdn.jsdelivr.net/npm/brython@3.12.0/brython_stdlib.js + - javascripts/codemirror-setup.js + - javascripts/brython-setup.js + - javascripts/hero-animation.js +``` + +**Better approach:** Generate the hero plot dynamically during doc build. + +3. Update `scripts/generate_docs.py` to generate hero plot: + +```python +def generate_hero_plot() -> str: + """ + Generate a sample plot for the hero animation. + + Returns: + String containing plotille plot output + """ + try: + import plotille + import math + + X = [i / 10 for i in range(-31, 32)] + Y = [math.sin(x) for x in X] + + plot_output = plotille.plot( + X, Y, + width=60, + height=10, + X_label='X', + Y_label='', + ) + + return plot_output + except Exception as e: + # Fallback if generation fails + return "Error generating plot" + + +# Update generate_home_page() +def generate_home_page(docs_dir: Path) -> Path: + """Generate the home/index page.""" + hero_plot = generate_hero_plot() + + content = f"""# plotille + +
+
+ [root@plotille ~]$ +
+
+
{hero_plot}
+
+
+ +Plot in the terminal using braille dots, with no dependencies. + +## Features + +- **Scatter plots, line plots, histograms** - Basic plotting functions +- **Complex figures** - Compose multiple plots with legends +- **Canvas drawing** - Direct pixel manipulation for custom visualizations +- **Image rendering** - Display images using braille dots or background colors +- **Color support** - Multiple color modes: names, byte values, RGB +- **No dependencies** - Pure Python with no external requirements + +## Quick Start + +Install plotille: + +```bash +pip install plotille +``` + +Create your first plot: + +```python +import plotille +import math + +X = [i/10 for i in range(-30, 30)] +Y = [math.sin(x) for x in X] + +print(plotille.plot(X, Y, height=20, width=60)) +``` + +## Explore + +Browse the [cookbook](cookbook/basic/) to see interactive examples you can edit and run in your browser. + +""" + + index_file = docs_dir / "index.md" + index_file.write_text(content) + return index_file +``` + +**Test:** + +```bash +python scripts/generate_docs.py +mkdocs serve +# Visit home page, verify plot appears in hero section +``` + +**Commit:** `Add hero plot to home page` + +--- + +## Phase 11: GitHub Actions CI/CD + +### Task 11.1: Create Documentation Build Workflow + +**What:** Automate doc building and deployment on push to main. + +**Files to create:** +- `.github/workflows/docs.yml` + +**Actions:** + +1. Create directory: +```bash +mkdir -p .github/workflows +``` + +2. Create `.github/workflows/docs.yml`: + +```yaml +name: Build and Deploy Documentation + +on: + push: + branches: + - master # Adjust if your main branch is named differently + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write # Needed to push to gh-pages + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for proper git info + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests (including doctests) + run: | + pytest --doctest-modules plotille/ -v + + - name: Generate documentation + run: | + python scripts/generate_docs.py + + - name: Build MkDocs site + run: | + mkdocs build --strict + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + cname: plotille.tammo.io +``` + +**Test:** + +You can't fully test this until you push to GitHub, but you can verify the workflow syntax: + +```bash +# Install actionlint for workflow validation (optional) +# brew install actionlint # macOS +# Or download from https://github.com/rhysd/actionlint + +# Validate workflow +actionlint .github/workflows/docs.yml + +# Or just verify it's valid YAML +python -c "import yaml; yaml.safe_load(open('.github/workflows/docs.yml'))" +``` + +**Commit:** `Add GitHub Actions workflow for documentation` + +--- + +### Task 11.2: Configure GitHub Pages Settings + +**What:** Set up GitHub repository for Pages deployment. + +**Actions (to be done in GitHub web interface):** + +1. Push the changes to GitHub: +```bash +git push origin master # Or your main branch +``` + +2. Go to repository Settings → Pages + +3. Under "Build and deployment": + - Source: "Deploy from a branch" + - Branch: Select `gh-pages` and `/ (root)` + - Click "Save" + +4. Under "Custom domain": + - Enter: `plotille.tammo.io` + - Click "Save" + - Wait for DNS check to complete + +5. Enable "Enforce HTTPS" once DNS check passes + +**DNS Configuration (at your domain registrar):** + +Add CNAME record: +``` +Host: plotille +Points to: tammoippen.github.io +``` + +**Test:** + +After workflow runs and DNS propagates: +- Visit https://plotille.tammo.io +- Verify site loads with documentation +- Check that HTTPS works + +**Commit:** (No code changes, just documentation of steps) + +--- + +## Phase 12: Testing and Polish + +### Task 12.1: Manual Testing Checklist + +**What:** Comprehensive testing of the documentation site. + +**Actions:** + +Create a testing checklist `docs/plans/testing-checklist.md`: + +```markdown +# Documentation Testing Checklist + +## Visual Design +- [ ] Amber phosphor color scheme applies throughout +- [ ] IBM Plex Mono font loads correctly +- [ ] VT323 font loads for headers +- [ ] Scanline effect is visible but not distracting +- [ ] Terminal windows have proper styling +- [ ] Navigation sidebar matches terminal aesthetic +- [ ] Responsive design works on mobile + +## Navigation +- [ ] All navigation links work +- [ ] Breadcrumbs function correctly +- [ ] Search works (if enabled) +- [ ] Active page is highlighted in sidebar +- [ ] Braille dot icons appear in navigation + +## Home Page +- [ ] Hero plot displays correctly +- [ ] Quick start code block renders +- [ ] Links to cookbook work + +## Cookbook Pages +- [ ] All four category pages exist (basic, figures, canvas, advanced) +- [ ] Examples are categorized correctly +- [ ] Interactive examples have working editors +- [ ] Run buttons work for interactive examples +- [ ] Output displays correctly +- [ ] Static examples show pre-rendered output +- [ ] Dependency warnings show for static examples + +## Interactive Examples +- [ ] Code editor is editable +- [ ] Tab key inserts spaces +- [ ] Run button executes code +- [ ] Output appears in terminal-styled div +- [ ] Errors display clearly +- [ ] Can modify code and re-run +- [ ] Multiple examples on same page don't interfere + +## API Reference +- [ ] All API pages exist +- [ ] Docstrings render correctly +- [ ] Type hints display properly +- [ ] Function signatures are clear +- [ ] Examples in docstrings render +- [ ] Cross-references link correctly +- [ ] Source code links work + +## Doctests +- [ ] All doctests pass: `pytest --doctest-modules plotille/` +- [ ] Coverage is reasonable (aim for 80%+ of public functions) + +## Build Process +- [ ] `python scripts/generate_docs.py` completes without errors +- [ ] `mkdocs build` completes without warnings +- [ ] Generated site is in `site/` directory +- [ ] No broken links in built site + +## CI/CD +- [ ] GitHub Actions workflow runs successfully +- [ ] Documentation deploys to gh-pages branch +- [ ] Site is accessible at plotille.tammo.io +- [ ] HTTPS works +- [ ] Custom domain configured correctly + +## Performance +- [ ] Page load time is reasonable (<3s) +- [ ] No console errors in browser +- [ ] Brython loads correctly +- [ ] Fonts load without flash of unstyled text + +## Browser Compatibility +- [ ] Works in Chrome/Chromium +- [ ] Works in Firefox +- [ ] Works in Safari +- [ ] Works in Edge +``` + +Work through this checklist systematically, fixing issues as you find them. + +**Commit pattern:** `Fix: ` for each fix + +--- + +### Task 12.2: Add Error Styling for Interactive Examples + +**What:** Better error display when code fails. + +**Files to modify:** +- `docs/stylesheets/terminal.css` + +**Actions:** + +Add error styling to `terminal.css`: + +```css +/* Error output styling */ +.terminal-output .error { + color: #ff6b6b; + background: rgba(255, 0, 0, 0.1); + border-left: 3px solid #ff6b6b; + padding-left: 0.5rem; +} + +.output-content.error { + color: #ff6b6b; +} + +/* Loading state */ +.terminal-output .loading::after { + content: '...'; + animation: loading 1.5s infinite; +} + +@keyframes loading { + 0%, 100% { opacity: 0; } + 50% { opacity: 1; } +} +``` + +**Test:** + +Create a test example that intentionally errors: + +```markdown +# Error Test + +
+
+ [python3 error.py] + +
+
+ +
+ root@plotille:~$ +
+
+
+
+``` + +Verify error displays with red styling. + +**Commit:** `Add error styling for interactive examples` + +--- + +### Task 12.3: Add README Section About Documentation + +**What:** Update project README to link to the new documentation site. + +**Files to modify:** +- `README.md` + +**Actions:** + +Add a documentation section near the top of `README.md`: + +```markdown +## Documentation + +šŸ“š **Full documentation available at [plotille.tammo.io](https://plotille.tammo.io)** + +Features: +- **Interactive examples** - Edit and run code in your browser +- **Complete API reference** - Auto-generated from source +- **Cookbook** - Examples organized by complexity + +``` + +**Commit:** `Add documentation link to README` + +--- + +## Phase 13: Refinement and Edge Cases + +### Task 13.1: Handle Empty/Missing Examples Gracefully + +**What:** Ensure generation script handles edge cases. + +**Files to modify:** +- `scripts/generate_docs.py` + +**Actions:** + +Add validation to generation script: + +```python +def main() -> int: + """Main entry point.""" + project_root = Path(__file__).parent.parent + examples_dir = project_root / "examples" + output_dir = project_root / "docs" / "assets" / "example-outputs" + docs_dir = project_root / "docs" + + if not examples_dir.exists(): + print(f"Error: {examples_dir} not found", file=sys.stderr) + return 1 + + # Analyze all Python files + examples = [] + for example_file in sorted(examples_dir.glob("*.py")): + try: + info = analyze_example(example_file) + examples.append(info) + except Exception as e: + print(f"Warning: Failed to analyze {example_file.name}: {e}", + file=sys.stderr) + continue + + if not examples: + print("Warning: No examples found", file=sys.stderr) + # Generate placeholder pages + for category in ['basic', 'figures', 'canvas', 'advanced']: + category_dir = docs_dir / "cookbook" + category_dir.mkdir(parents=True, exist_ok=True) + placeholder = category_dir / f"{category}.md" + placeholder.write_text(f"# {category.title()}\n\nNo examples yet.\n") + return 0 + + # ... rest of existing code ... +``` + +**Test:** + +```bash +# Test with no examples (temporarily) +mv examples examples.backup +mkdir examples +python scripts/generate_docs.py +# Should handle gracefully + +# Restore +rmdir examples +mv examples.backup examples +``` + +**Commit:** `Add error handling for missing examples` + +--- + +### Task 13.2: Add Source File Headers + +**What:** Ensure all documentation source files have descriptive headers. + +**Files to check:** +- `scripts/generate_docs.py` +- `docs/javascripts/*.js` +- `docs/stylesheets/*.css` + +**Actions:** + +Add headers following the CLAUDE.md rule about ABOUTME comments: + +Example for `scripts/generate_docs.py`: + +```python +#!/usr/bin/env python3 +# ABOUTME: Generates plotille documentation from examples and source code. +# ABOUTME: Analyzes examples, executes static ones, and creates markdown pages. +""" +Generate documentation from examples. + +This script: +1. Scans examples/ directory +2. Classifies examples by dependencies +3. Generates markdown files for MkDocs +""" +``` + +Check each file and add appropriate headers. + +**Commit:** `Add ABOUTME headers to documentation files` + +--- + +### Task 13.3: Performance Optimization - Lazy Load Brython + +**What:** Only load Brython on pages that need it. + +**Files to modify:** +- `mkdocs.yml` +- `docs/javascripts/brython-setup.js` + +**Actions:** + +1. Update `mkdocs.yml` to conditionally load Brython: + +```yaml +# Move Brython scripts to be loaded only when needed +extra_javascript: + - javascripts/codemirror-setup.js + - javascripts/hero-animation.js + # Brython loaded conditionally +``` + +2. Update example pages to load Brython: + +In `scripts/generate_docs.py`, update `generate_interactive_example_markdown()`: + +```python +def generate_interactive_example_markdown(info: ExampleInfo) -> str: + """Generate markdown for an interactive example.""" + source_code = info.path.read_text() + escaped_code = source_code.replace('```', '\\`\\`\\`') + + # Add script tag to load Brython on this page + brython_loader = ''' + + + +''' + + return f"""{brython_loader} + +## {info.name} + +{info.description} + +
+ ... +
+ +""" +``` + +**Note:** This optimization is optional (YAGNI). Only implement if page load time is actually slow. + +**Commit:** (Optional) `Optimize: Lazy load Brython on interactive pages` + +--- + +## Phase 14: Finalization + +### Task 14.1: Write Documentation for Contributors + +**What:** Document the doc system for future maintainers. + +**Files to create:** +- `docs/contributing.md` +- `CONTRIBUTING.md` (link to above) + +**Actions:** + +1. Create `docs/contributing.md`: + +```markdown +# Contributing to plotille + +Thank you for contributing to plotille! + +## Documentation System + +The documentation is built with MkDocs and auto-deployed to https://plotille.tammo.io + +### Structure + +- `docs/` - Documentation source files (markdown) +- `examples/` - Example scripts (auto-imported to docs) +- `scripts/generate_docs.py` - Documentation generation script +- `mkdocs.yml` - MkDocs configuration + +### Local Development + +1. Install dependencies: + ```bash + pip install -e ".[dev]" + ``` + +2. Generate docs from examples: + ```bash + python scripts/generate_docs.py + ``` + +3. Serve locally: + ```bash + mkdocs serve + ``` + +4. Visit http://127.0.0.1:8000 + +### Adding Examples + +1. Create a new `.py` file in `examples/` +2. Add a docstring or comment at the top describing it +3. Run `python scripts/generate_docs.py` +4. The example will automatically appear in the cookbook + +Examples using only plotille + stdlib will be interactive in the browser. +Examples using numpy, Pillow, etc. will show pre-rendered output. + +### Updating API Documentation + +API docs are auto-generated from docstrings using mkdocstrings. + +1. Update docstrings in `plotille/` source files +2. Add examples using doctest format (`>>>`) +3. Run tests: `pytest --doctest-modules plotille/` +4. Rebuild docs: `mkdocs build` + +All doctest examples must pass before deploying. + +### Deployment + +Documentation auto-deploys on push to `master`: +1. GitHub Actions runs tests +2. Generates documentation +3. Builds MkDocs site +4. Deploys to gh-pages branch +5. Available at https://plotille.tammo.io + +### Theme Customization + +The documentation uses a custom amber phosphor CRT theme: +- Colors: `docs/stylesheets/terminal.css` +- JavaScript: `docs/javascripts/` +- Theme: Material for MkDocs with heavy customization +``` + +2. Create `CONTRIBUTING.md` at project root: + +```markdown +# Contributing + +See the full contributing guide: https://plotille.tammo.io/contributing/ +``` + +3. Update `mkdocs.yml` nav: + +```yaml +nav: + - Home: index.md + - Cookbook: + - Basic Plots: cookbook/basic.md + - Complex Figures: cookbook/figures.md + - Canvas Drawing: cookbook/canvas.md + - Advanced Examples: cookbook/advanced.md + - API Reference: + - Overview: api/index.md + - Plotting Functions: api/plotting.md + - Figure: api/figure.md + - Canvas: api/canvas.md + - Contributing: contributing.md +``` + +**Commit:** `Add contributing documentation` + +--- + +### Task 14.2: Final Testing and Launch + +**What:** Complete final testing before announcement. + +**Actions:** + +1. Work through complete testing checklist (Task 12.1) + +2. Test on multiple browsers and devices + +3. Check performance with browser DevTools + +4. Verify all links work (use link checker): +```bash +# Optional: install link checker +# pip install linkchecker + +# Build site +mkdocs build + +# Check links +# linkchecker site/index.html +``` + +5. Get a fresh pair of eyes to review (if possible) + +**Commit:** `Final polish and testing` + +--- + +### Task 14.3: Create Announcement + +**What:** Prepare announcement of new documentation. + +**Files to create:** +- `docs/plans/launch-announcement.md` + +**Actions:** + +Create launch announcement draft: + +```markdown +# plotille Documentation Launch + +New comprehensive documentation site now available at **https://plotille.tammo.io**! + +## What's New + +šŸ–„ļø **Interactive Examples** - Edit and run plotille code directly in your browser +šŸ“š **Complete API Reference** - Auto-generated from source with examples +šŸŽØ **Terminal Aesthetic** - Amber phosphor CRT theme +šŸ” **Searchable** - Find functions and examples quickly +šŸ“± **Responsive** - Works on mobile and desktop + +## Highlights + +- **Cookbook-first approach** - Learn by example +- **Live code editing** - Powered by Brython +- **Tested documentation** - All examples verified with doctests +- **Auto-deployed** - Always up to date with latest release + +Check it out: https://plotille.tammo.io + +--- + +Technical details: +- Built with MkDocs + mkdocstrings +- Custom amber phosphor terminal theme +- Interactive examples via Brython +- Deployed via GitHub Actions to GitHub Pages +``` + +**Commit:** `Add launch announcement` + +--- + +## Summary and Next Steps + +You've now implemented a complete documentation system for plotille! + +### What You Built + +1. āœ… **Documentation generator** - Analyzes and categorizes examples +2. āœ… **Static pre-rendering** - Executes examples during build +3. āœ… **Interactive examples** - Brython-powered browser execution +4. āœ… **API reference** - Auto-generated with mkdocstrings +5. āœ… **Terminal aesthetic** - Amber phosphor CRT theme +6. āœ… **CI/CD pipeline** - Auto-deploy on push to main +7. āœ… **Doctest integration** - Tested documentation examples + +### Commands Reference + +```bash +# Generate documentation +python scripts/generate_docs.py + +# Serve locally +mkdocs serve + +# Build for production +mkdocs build + +# Run doctests +pytest --doctest-modules plotille/ + +# Deploy (via CI) +git push origin master +``` + +### Maintenance + +- **Add examples**: Just add `.py` files to `examples/` +- **Update API docs**: Edit docstrings in `plotille/` source +- **Theme changes**: Edit `docs/stylesheets/terminal.css` +- **Behavior changes**: Edit `docs/javascripts/*.js` + +### Known Limitations + +1. **Brython compatibility**: Some Python features may not work in browser +2. **External dependencies**: numpy/Pillow examples show pre-rendered output only +3. **Performance**: Loading Brython adds ~500KB to page size +4. **Browser support**: Requires modern browser with ES6 support + +### Future Enhancements (Optional) + +- Full CodeMirror 6 integration with syntax highlighting +- Version switching (using mike plugin) +- More sophisticated Brython output capture +- Screenshot generation for social media sharing +- Dark/light theme toggle +- More examples and tutorials + +--- + +## Troubleshooting + +### Common Issues + +**Problem**: `mkdocs serve` shows errors +**Solution**: Check `mkdocs.yml` syntax with YAML linter + +**Problem**: Examples don't execute +**Solution**: Check browser console for JavaScript errors + +**Problem**: Doctest failures +**Solution**: Run `pytest --doctest-modules plotille/ -v` to see which tests fail + +**Problem**: GitHub Pages 404 +**Solution**: Verify gh-pages branch exists and contains built site + +**Problem**: Custom domain not working +**Solution**: Check DNS propagation and CNAME file in gh-pages branch + +**Problem**: Fonts not loading +**Solution**: Check network tab in browser DevTools, verify Google Fonts CDN accessible + +--- + +## Testing Strategy for Each Phase + +Follow Test-Driven Development: + +1. **Unit tests first** - Write tests for utility functions +2. **Integration tests** - Test script end-to-end +3. **Manual testing** - Verify in browser +4. **Commit frequently** - After each passing test + +### Example TDD Flow + +```bash +# 1. Write test +echo "def test_new_feature(): assert False" >> tests/test_generate_docs.py + +# 2. Run test (should fail) +pytest tests/test_generate_docs.py::test_new_feature -v + +# 3. Implement feature +# ... edit code ... + +# 4. Run test (should pass) +pytest tests/test_generate_docs.py::test_new_feature -v + +# 5. Commit +git add -A +git commit -m "Add feature X with tests" +``` + +--- + +**End of Implementation Plan** + +This plan provides complete step-by-step instructions for implementing the plotille documentation system. Follow each task sequentially, test thoroughly, and commit frequently. Good luck! diff --git a/docs/plans/final-review.md b/docs/plans/final-review.md new file mode 100644 index 0000000..c183692 --- /dev/null +++ b/docs/plans/final-review.md @@ -0,0 +1,240 @@ +# Final Review - Brython Implementation + +## Overall Assessment + +**Excellent work!** The implementer has successfully rewritten the Brython execution system with a much more robust approach. The build completes successfully and the infrastructure is in place for interactive examples. + +## āœ… What's Working Excellently + +### 1. Brython Execution Architecture ⭐⭐⭐ +**Outstanding implementation** - Much better than the original plan: + +- **Permanent Python executor** (`brython-executor.py`) loaded once, runs all examples +- **Clean separation** between Python (executor logic) and JavaScript (DOM interaction) +- **Proper stdout capture** using custom `OutputCapture` class with `isatty()` support +- **Error handling** with full tracebacks displayed to users +- **Ready indicator** using polling to ensure executor is loaded before use + +**Key advantage:** This approach is more maintainable and follows Brython best practices. + +### 2. Local Brython Installation āœ… +- Brython CDN replaced with local files (~5MB total) +- Ensures consistent behavior and offline capability +- Files: `docs/brython.js`, `docs/brython_stdlib.js` + +### 3. AnsiUp Integration for Colors āœ… +- Added `ansi_up.js` for ANSI color code → HTML conversion +- Loaded as ES6 module and exposed to Python executor +- Allows colored terminal output in browser (plotille uses ANSI codes) +- `isatty() = True` in OutputCapture enables plotille colors + +### 4. Proper Brython Module Path āœ… +- Plotille copied to `Lib/site-packages/plotille/` (Brython's standard location) +- Brython automatically searches this path for imports +- No need for custom pythonpath configuration + +### 5. Build Process Fixed āœ… +- `generate_docs.py` completes successfully +- `IsADirectoryError` bug fixed: `output_paths.get(info.name)` returns None properly +- Static examples handled correctly with "Output not available" message +- `__pycache__` excluded from plotille copy + +### 6. Minor Issues Fixed āœ… +- CodeMirror check removed (was misleading) +- Test page removed from navigation +- Textarea enhancement works correctly + +## āš ļø One Issue Found + +### Hero Plot Generation Fails + +**Issue:** Home page shows: +``` +Error generating plot: No module named 'plotille' +``` + +**Root Cause:** When `generate_docs.py` runs, it tries to `import plotille` to generate the hero plot, but plotille isn't in the Python path during script execution. + +**Previous State:** The plot was working earlier (git diff shows it had the actual sine wave plot with braille characters). + +**Why it broke:** Likely environment changed during one of the recent commits, or script is now run in different context. + +**Fix Required in `scripts/generate_docs.py`:** + +Update the `generate_hero_plot()` function to add the project root to sys.path: + +```python +def generate_hero_plot() -> str: + """ + Generate a sample plot for the hero animation. + + Returns: + String containing plotille plot output + """ + try: + import math + import sys + from pathlib import Path + + # Add project root to path so plotille can be imported + project_root = Path(__file__).parent.parent + if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + + import plotille + + X = [i / 10 for i in range(-31, 32)] + Y = [math.sin(x) for x in X] + + plot_output = plotille.plot( + X, + Y, + width=60, + height=10, + X_label="X", + Y_label="", + ) + + return plot_output + except Exception as e: + # Fallback if generation fails + return f"Error generating plot: {e}" +``` + +**Test:** +```bash +python scripts/generate_docs.py +# Should see actual plot in docs/index.md, not error message + +head -20 docs/index.md +# Should see braille characters in the hero-plot section +``` + +**Commit:** +```bash +git add scripts/generate_docs.py docs/index.md +git commit -m "Fix hero plot generation by adding project root to sys.path" +``` + +## šŸ“‹ Final Testing Checklist + +Once the hero plot is fixed, verify everything works: + +### Build Process +- [x] `python scripts/generate_docs.py` completes without errors +- [x] No `__pycache__` in `docs/Lib/site-packages/plotille/` +- [ ] Hero plot shows actual sine wave (after fix) +- [x] Static examples show "Output not available" message +- [x] Basic and Advanced cookbook pages generated + +### Browser Functionality +Visit http://127.0.0.1:8000 and check: + +#### Home Page +- [ ] Hero terminal shows sine wave plot (after fix) +- [x] Quick start code block displays correctly +- [x] Navigation shows Home, Cookbook sections + +#### Test Page (`/test-brython/`) +- [ ] Click `[RUN]` button +- [ ] Should see "Hello from Brython!" and "4" in output +- [ ] No console errors + +#### Basic Cookbook (`/cookbook/basic/`) +- [ ] Examples load with code in textareas +- [ ] Tab key inserts 4 spaces +- [ ] Click `[EXEC]` on simple example (e.g., `house_example`) +- [ ] Output appears in terminal output area +- [ ] Try editing code and re-running + +#### Advanced Cookbook (`/cookbook/advanced/`) +- [x] Static examples show pre-rendered output or "not available" message +- [x] Dependency warnings display correctly +- [x] Code blocks format properly + +### Expected Limitations + +**Plotille examples may not all work in Brython:** +- Brython has limited stdlib (some features plotille uses may not be available) +- Unicode/braille handling might differ +- ANSI color codes should work with AnsiUp + +**This is documented and acceptable:** +- Simple Python examples (print, math, loops) should work āœ“ +- Plotille examples may work partially or not at all āš ļø +- Static examples cover the not-working cases āœ“ + +## šŸŽÆ Summary + +### What's Been Accomplished + +**Phase 5 (Terminal Theme):** āœ… Complete +- Amber phosphor CRT aesthetic +- Color contrast issues fixed +- Terminal window styling + +**Phase 6 (Brython Integration):** āœ… Complete with improvements +- Better implementation than original plan +- Permanent executor script approach +- Local Brython files +- AnsiUp color support + +**Phase 7 (CodeMirror/Editor):** āœ… Complete +- Textarea enhancement functional +- Tab key support +- Monospace font styling + +### Files Changed Summary + +**New Files:** +- `docs/overrides/main.html` - Template override for Brython executor +- `docs/javascripts/brython-executor.py` - Python code running in browser +- `docs/javascripts/brython-setup.js` - JavaScript initialization +- `docs/javascripts/codemirror-setup.js` - Textarea enhancements +- `docs/brython.js` - Local Brython runtime +- `docs/brython_stdlib.js` - Brython standard library +- `docs/ansi_up.js` - ANSI color conversion +- `docs/Lib/site-packages/plotille/` - Plotille for Brython + +**Modified Files:** +- `scripts/generate_docs.py` - Fixed build bugs, added hero plot +- `scripts/copy_plotille_for_brython.py` - Exclude pycache, use Lib/site-packages +- `mkdocs.yml` - Local Brython, removed test from nav, custom_dir +- `docs/stylesheets/terminal.css` - Color contrast fixes + +### Remaining Work + +**Immediate (5 minutes):** +1. Fix hero plot sys.path issue + +**Next Phase (Not Started Yet):** +- Phase 8: API Documentation with mkdocstrings +- Phase 9: Navigation and Site Structure (partially done) +- Phase 10: Hero Animation (partially done, needs fix) +- Phase 11-14: CI/CD, testing, refinement + +### Code Quality Notes + +**Strengths:** +- Clean separation of concerns +- Good error handling +- Follows Brython best practices +- Well-commented code +- ABOUTME headers present + +**No major issues found** - just the one hero plot import bug. + +--- + +## Recommendation + +āœ… **Approve with one minor fix** + +The Brython implementation is excellent and actually better than what was originally planned. Once the hero plot import issue is fixed (5-minute change), this phase is complete and ready to proceed to Phase 8 (API Documentation). + +**Next Steps:** +1. Apply hero plot fix +2. Test in browser (especially plotille examples to see which work) +3. Commit and proceed to Phase 8: API Documentation + +Great work by the implementer! The permanent executor approach is more elegant than the dynamic script creation approach in the original plan. diff --git a/docs/plans/implementation-plan-updates.md b/docs/plans/implementation-plan-updates.md new file mode 100644 index 0000000..8c6ebda --- /dev/null +++ b/docs/plans/implementation-plan-updates.md @@ -0,0 +1,323 @@ +# Implementation Plan Updates - Simplified CSS Approach + +## Changes to Original Plan + +This document updates `documentation-system-implementation.md` to reflect the simplified approach of using standard Material theme instead of custom amber phosphor CRT aesthetic. + +--- + +## Phase 5: Terminal Window Styling (REVISED) + +### Task 5.1: Create Minimal Terminal Window Styling (REPLACES OLD TASK 5.1) + +**What:** Style only the terminal window components for interactive examples. Use standard Material theme for everything else. + +**Files to create:** +- `docs/stylesheets/terminal.css` (minimal, ~120 lines) + +**Actions:** + +1. Create `docs/stylesheets/terminal.css`: + +```css +/* + * ABOUTME: Terminal window styling for plotille interactive examples. + * ABOUTME: Uses standard Material theme for everything except terminal components. + */ + +:root { + --terminal-bg: #1e1e1e; + --terminal-fg: #d4d4d4; + --terminal-border: #3e3e3e; + --terminal-header-bg: #2d2d2d; + --terminal-button-bg: #0e639c; + --terminal-button-hover: #1177bb; +} + +.terminal-window { + background: var(--terminal-bg); + border: 1px solid var(--terminal-border); + border-radius: 6px; + margin: 1.5rem 0; + overflow: hidden; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; +} + +.terminal-header { + background: var(--terminal-header-bg); + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--terminal-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.terminal-title { + color: var(--terminal-fg); + font-size: 0.85rem; + font-weight: 500; +} + +.terminal-run-btn { + background: var(--terminal-button-bg); + color: #ffffff; + border: none; + padding: 0.25rem 0.75rem; + border-radius: 3px; + cursor: pointer; + font-size: 0.85rem; + font-weight: 500; + transition: background 0.2s; +} + +.terminal-run-btn:hover { + background: var(--terminal-button-hover); +} + +.terminal-body { + padding: 1rem; + background: var(--terminal-bg); +} + +.code-editor-wrapper { + border: 1px solid var(--terminal-border); + border-radius: 4px; + overflow: hidden; +} + +.code-editor { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + background: var(--terminal-bg); + color: var(--terminal-fg); + width: 100%; + min-height: 300px; + padding: 1rem; + border: none; + resize: vertical; + line-height: 1.5; + tab-size: 4; +} + +.code-editor:focus { + outline: none; + box-shadow: 0 0 0 2px var(--terminal-button-bg); +} + +.terminal-output { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + color: var(--terminal-fg); + white-space: pre-wrap; + margin-top: 1rem; + line-height: 1.4; + max-height: 600px; + overflow-y: auto; +} + +.terminal-prompt { + color: #4ec9b0; + display: block; + margin-bottom: 0.5rem; +} + +.output-content { + margin-top: 0.5rem; +} + +.output-content.error { + color: #f48771; +} + +.hero-terminal { + margin: 2rem 0; +} + +.hero-plot { + font-size: 0.75rem; +} +``` + +2. Update `mkdocs.yml` to use standard Material theme: + +```yaml +site_name: plotille +site_url: https://plotille.tammo.io +site_description: Plot in the terminal using braille dots +site_author: Tammo Ippen +repo_url: https://github.com/tammoippen/plotille +repo_name: tammoippen/plotille + +theme: + name: material + features: + - content.code.copy + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_source: true + show_signature_annotations: true + separate_signature: true + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - admonition + - pymdownx.details + +extra_css: + - stylesheets/terminal.css + +extra_javascript: + - brython.js + - brython_stdlib.js + - ansi_up.js + - javascripts/codemirror-setup.js + - javascripts/brython-setup.js + +nav: + - Home: index.md + - Cookbook: + - Basic Plots: cookbook/basic.md + - Advanced Examples: cookbook/advanced.md +``` + +3. Update `.gitignore`: + +```gitignore +# Site build output +site/ + +# Documentation build artifacts +docs/Lib/ +docs/assets/example-outputs/ +``` + +4. Remove build artifacts from git: + +```bash +git rm -r --cached docs/Lib/ +git rm -r --cached docs/assets/example-outputs/ +``` + +**Test:** +```bash +python scripts/generate_docs.py +mkdocs serve +# Visit http://127.0.0.1:8000 +# Verify: +# - Standard Material theme (clean, professional) +# - Terminal windows look like terminals (dark, monospace) +# - Everything readable and accessible +# - Search works with default styling +# - Code blocks use Material's syntax highlighting +``` + +**Commit:** `Add minimal terminal window styling with standard Material theme` + +**Design Goals:** +- **Simplicity:** ~120 lines of CSS vs 547 +- **Maintainability:** Only style what's necessary +- **Accessibility:** Use Material's tested, accessible defaults +- **Clarity:** Clear visual distinction between docs and terminals + +--- + +### Task 5.2: REMOVED + +Font bundling task is no longer needed - use system/browser default fonts via Material theme. + +--- + +## Phase 10: Hero Animation (SIMPLIFIED) + +### Task 10.1: SIMPLIFIED - Static Hero Plot + +The original task included JavaScript animation. Simplified version: + +**What:** Display a static plotille plot on the home page. No animation needed. + +**This is already implemented** in `generate_home_page()` which generates the hero plot. The plot appears in the hero-terminal div which is styled by the minimal terminal.css. + +**No additional work needed** for this task. The hero plot is generated during `python scripts/generate_docs.py` and displayed in a terminal window styled by terminal.css. + +--- + +## Removed Sections + +The following sections from the original plan are **NO LONGER NEEDED**: + +### From Phase 5: +- āŒ All the "amber phosphor color palette" variables +- āŒ Global font imports (VT323, IBM Plex Mono) +- āŒ CRT scanline effects +- āŒ Global Material theme overrides +- āŒ Custom navigation styling +- āŒ Custom header styling +- āŒ Custom search styling +- āŒ Syntax highlighting overrides +- āŒ Admonition overrides + +### From Phase 10: +- āŒ JavaScript hero animation (keep static plot) +- āŒ Character-by-character reveal animation +- āŒ `docs/javascripts/hero-animation.js` file + +--- + +## What Stays the Same + +These phases are **unchanged**: + +- **Phase 1:** Project Setup & Dependencies āœ“ +- **Phase 2:** Example Analysis & Classification āœ“ +- **Phase 3:** Static Example Pre-rendering āœ“ +- **Phase 4:** Markdown Generation āœ“ +- **Phase 6:** Brython Integration āœ“ +- **Phase 7:** CodeMirror/Editor Enhancement āœ“ +- **Phase 8:** API Documentation āœ“ +- **Phase 9:** Navigation (already minimal) āœ“ +- **Phase 11:** GitHub Actions CI/CD āœ“ +- **Phase 12-14:** Testing, refinement, launch āœ“ + +--- + +## Summary of Changes + +**Philosophy Change:** +- **Old:** Custom amber phosphor CRT theme everywhere +- **New:** Standard Material theme, terminal styling only for terminals + +**CSS Reduction:** +- **Old:** 547 lines of global overrides +- **New:** 120 lines of terminal-only styling + +**Visual Design:** +- **Old:** Amber on black everywhere, CRT effects, custom fonts +- **New:** Material's defaults everywhere, terminal windows look like VS Code terminals + +**Maintenance:** +- **Old:** Fight Material's updates, maintain custom syntax highlighting +- **New:** Ride Material's updates, zero maintenance on theme + +**Benefits:** +- Simpler implementation +- Better readability +- More accessible +- Easier to maintain +- Faster page loads +- Professional appearance + +**Trade-offs:** +- Less "unique" visual identity +- No retro CRT aesthetic + +**Conclusion:** The simplified approach better serves the primary goal: **documenting plotille clearly and making it easy for users to try examples interactively.** diff --git a/docs/plans/implementation-review.md b/docs/plans/implementation-review.md new file mode 100644 index 0000000..96f1555 --- /dev/null +++ b/docs/plans/implementation-review.md @@ -0,0 +1,448 @@ +# Documentation Implementation Review + +## Overall Assessment + +Excellent progress on Tasks 4.2, 4.3, and 5.1! The structure is solid and the amber phosphor theme foundation is in place. However, there are several issues that need to be addressed before proceeding to the next phase. + +**Dev server is running at:** http://127.0.0.1:8000 + +--- + +## Critical Issues (Must Fix Before Proceeding) + +### 1. Color Contrast Problems - CRITICAL āŒ + +**Issue:** The current color scheme has severe readability issues: +- White/light text on yellow backgrounds is not readable +- H1 headers appear to have black font with yellow shadow/glow, making them unreadable +- Material theme overrides are creating poor contrast combinations + +**Root Cause:** The CSS is setting Material theme primary colors to amber, which Material then uses for backgrounds in some components. This creates amber-on-amber or light-on-amber combinations. + +**Fix Required in `docs/stylesheets/terminal.css`:** + +```css +/* Global overrides for Material theme */ +[data-md-color-scheme="plotille"] { + /* Keep amber for accents, but use proper backgrounds */ + --md-primary-fg-color: var(--amber-base); + --md-primary-fg-color--light: var(--amber-bright); + --md-primary-fg-color--dark: var(--amber-dim); + --md-accent-fg-color: var(--amber-bright); + + /* Force dark backgrounds with amber text */ + --md-default-bg-color: var(--amber-black); + --md-default-fg-color: var(--amber-base); + --md-code-bg-color: var(--amber-dark); + --md-code-fg-color: var(--amber-base); + + /* Add these to prevent light backgrounds */ + --md-typeset-a-color: var(--amber-bright); + --md-typeset-color: var(--amber-base); +} + +/* Ensure headers are always readable */ +h1, h2, h3, h4, h5, h6 { + font-family: 'VT323', monospace; + color: var(--amber-bright) !important; + background: transparent !important; + text-shadow: 0 0 8px var(--amber-glow); + letter-spacing: 0.05em; +} + +/* Force main content area to be dark */ +.md-main, +.md-content, +.md-content__inner { + background-color: var(--amber-black) !important; + color: var(--amber-base) !important; +} + +/* Ensure sidebar is dark */ +.md-sidebar { + background-color: var(--amber-black) !important; +} + +/* Links should be bright amber and underlined for visibility */ +a { + color: var(--amber-bright) !important; + text-decoration: underline; +} + +a:hover { + color: var(--amber-bright) !important; + text-shadow: 0 0 10px var(--amber-glow); +} +``` + +**Test:** After applying, verify that: +- All text is readable (amber on black, never black on amber) +- Headers are bright amber with glow effect +- No white or light backgrounds appear anywhere +- Links are clearly visible + +--- + +### 2. `__init__.py` Being Treated as Example āŒ + +**Issue:** The file `examples/__init__.py` is being processed and appears in `basic.md` as "## __init__" with empty content. + +**Fix Required in `scripts/generate_docs.py`:** + +In the `main()` function, modify the loop to skip `__init__.py`: + +```python +# Analyze all Python files +examples = [] +for example_file in sorted(examples_dir.glob("*.py")): + # Skip __init__.py files + if example_file.name == "__init__.py": + continue + info = analyze_example(example_file) + examples.append(info) +``` + +**Test:** Run `python scripts/generate_docs.py` and verify `__init__` no longer appears in `docs/cookbook/basic.md`. + +--- + +### 3. Performance Example Should Not Be in Cookbook āŒ + +**Issue:** `performance_example.py` appears in `basic.md` but it's not a useful example for users - it's for internal performance testing. + +**Fix Required in `scripts/generate_docs.py`:** + +Add performance examples to the skip list: + +```python +# Analyze all Python files +examples = [] +SKIP_EXAMPLES = {"__init__.py", "performance_example.py"} + +for example_file in sorted(examples_dir.glob("*.py")): + # Skip files that aren't user-facing examples + if example_file.name in SKIP_EXAMPLES: + continue + info = analyze_example(example_file) + examples.append(info) +``` + +**Test:** Verify `performance_example` no longer appears in the generated cookbook pages. + +--- + +### 4. License Headers in Examples āŒ + +**Issue:** All examples include the full MIT license header (23 lines), which clutters the documentation. Users don't need to see this in the browser. + +**Fix Required in `scripts/generate_docs.py`:** + +Add a function to strip license headers: + +```python +def strip_license_header(source_code: str) -> str: + """ + Remove MIT license header from source code. + + Args: + source_code: Python source code possibly containing license header + + Returns: + Source code with license header removed + """ + lines = source_code.split('\n') + + # Look for MIT license pattern + if '# The MIT License' in source_code: + # Find the end of the license block (first non-comment/non-blank line after license) + in_license = False + start_index = 0 + + for i, line in enumerate(lines): + stripped = line.strip() + + # Start of license + if 'MIT License' in line: + in_license = True + start_index = i + continue + + # End of license block (first non-comment, non-blank line) + if in_license and stripped and not stripped.startswith('#'): + # Remove everything from start_index to just before this line + return '\n'.join(lines[i:]) + + # If we didn't find the end, remove first 23 lines (typical license length) + if in_license: + return '\n'.join(lines[23:]) + + return source_code +``` + +Then update `generate_interactive_example_markdown()` and `generate_static_example_markdown()`: + +```python +def generate_interactive_example_markdown(info: ExampleInfo) -> str: + """Generate markdown for an interactive example.""" + source_code = info.path.read_text() + + # Strip license header + source_code = strip_license_header(source_code) + + # Escape backticks in code for markdown + escaped_code = source_code.replace("```", "\\`\\`\\`") + + # ... rest of function ... +``` + +```python +def generate_static_example_markdown(info: ExampleInfo, output_path: Path) -> str: + """Generate markdown for a static example with pre-rendered output.""" + source_code = info.path.read_text() + + # Strip license header + source_code = strip_license_header(source_code) + + # Read pre-rendered output + # ... rest of function ... +``` + +**Test:** Verify examples in cookbook pages start with `import` statements, not license headers. + +--- + +### 5. Hero Plot is Empty āŒ + +**Issue:** The home page has `
` but it's empty. According to the plan (Task 10.1), the hero should display an actual plotille sine wave.
+
+**Fix Required in `scripts/generate_docs.py`:**
+
+Add the `generate_hero_plot()` function:
+
+```python
+def generate_hero_plot() -> str:
+    """
+    Generate a sample plot for the hero animation.
+
+    Returns:
+        String containing plotille plot output
+    """
+    try:
+        import plotille
+        import math
+
+        X = [i / 10 for i in range(-31, 32)]
+        Y = [math.sin(x) for x in X]
+
+        plot_output = plotille.plot(
+            X, Y,
+            width=60,
+            height=10,
+            X_label='X',
+            Y_label='',
+        )
+
+        return plot_output
+    except Exception as e:
+        # Fallback if generation fails
+        return f"Error generating plot: {e}"
+```
+
+Then update `generate_home_page()`:
+
+```python
+def generate_home_page(docs_dir: Path) -> Path:
+    """Generate the home/index page."""
+    # Generate the hero plot
+    hero_plot = generate_hero_plot()
+
+    # Change to f-string to include hero_plot
+    content = f"""# plotille
+
+
+
+ [root@plotille ~]$ +
+
+
{hero_plot}
+
+
+ +Plot in the terminal using braille dots, with no dependencies. + +## Features + +- **Scatter plots, line plots, histograms** - Basic plotting functions +- **Complex figures** - Compose multiple plots with legends +- **Canvas drawing** - Direct pixel manipulation for custom visualizations +- **Image rendering** - Display images using braille dots or background colors +- **Color support** - Multiple color modes: names, byte values, RGB +- **No dependencies** - Pure Python with no external requirements + +## Quick Start + +Install plotille: + +```bash +pip install plotille +``` + +Create your first plot: + +```python +import plotille +import math + +X = [i/10 for i in range(-30, 30)] +Y = [math.sin(x) for x in X] + +print(plotille.plot(X, Y, height=20, width=60)) +``` + +## Explore + +Browse the [cookbook](cookbook/basic.md) to see interactive examples you can edit and run in your browser. + +""" + + index_file = docs_dir / "index.md" + index_file.write_text(content) + return index_file +``` + +**Test:** Verify home page shows a sine wave plot in the hero terminal. + +--- + +### 6. Navigation Incomplete āš ļø + +**Issue:** `mkdocs.yml` only lists `- Home: index.md`. The cookbook pages should be in the navigation. + +**Fix Required in `mkdocs.yml`:** + +Update the `nav` section: + +```yaml +nav: + - Home: index.md + - Cookbook: + - Basic Plots: cookbook/basic.md + - Advanced Examples: cookbook/advanced.md +``` + +**Note:** Only `basic` and `advanced` categories exist because no examples matched `figures` or `canvas` patterns. This is correct behavior based on the current examples. + +**Test:** Verify navigation sidebar shows Home and Cookbook sections. + +--- + +## Expected Issue (Not Yet Implemented) + +### 7. `runExample is not defined` Error āœ“ Expected + +**Issue:** Clicking `[EXEC]` buttons in examples shows: +``` +Uncaught ReferenceError: runExample is not defined +``` + +**Reason:** This is expected! Brython integration (Phase 6) hasn't been implemented yet. The HTML structure is in place, but the JavaScript to make it work comes in Tasks 6.1-6.2 and Task 7.2. + +**Do NOT fix this yet.** This will be handled in: +- Task 6.1: Add Brython runtime and setup JavaScript +- Task 7.2: Implement proper Brython execution with output capture + +**For now:** The buttons are non-functional, which is correct for this stage. + +--- + +## How to Apply Fixes + +### Step 1: Update `scripts/generate_docs.py` + +Apply fixes for issues #2, #3, #4, and #5: + +1. Add `strip_license_header()` function +2. Update example loop to skip `__init__.py` and `performance_example.py` +3. Update both markdown generation functions to call `strip_license_header()` +4. Add `generate_hero_plot()` function +5. Update `generate_home_page()` to use `generate_hero_plot()` and f-strings + +### Step 2: Update `docs/stylesheets/terminal.css` + +Apply fix for issue #1 (color contrast): + +1. Add `!important` to critical background/foreground rules +2. Add explicit rules for `.md-main`, `.md-content`, `.md-sidebar` +3. Fix link styling for visibility +4. Ensure headers always have proper contrast + +### Step 3: Update `mkdocs.yml` + +Apply fix for issue #6 (navigation): + +1. Add Cookbook section with basic and advanced pages + +### Step 4: Regenerate and Test + +```bash +# Regenerate documentation with all fixes +python scripts/generate_docs.py + +# Verify changes +cat docs/index.md | grep "hero-plot" # Should contain plot output +grep -c "MIT License" docs/cookbook/basic.md # Should be 0 +grep -c "__init__" docs/cookbook/basic.md # Should be 0 +grep -c "performance_example" docs/cookbook/basic.md # Should be 0 + +# Check site in browser (server should auto-reload) +# Visit http://127.0.0.1:8000 +``` + +### Step 5: Commit + +```bash +git add scripts/generate_docs.py docs/ mkdocs.yml +git commit -m "Fix color contrast, filter unwanted examples, add hero plot, update nav" +``` + +--- + +## Testing Checklist + +After applying all fixes, verify in browser at http://127.0.0.1:8000: + +### Color & Contrast +- [ ] All text is readable (amber on black throughout) +- [ ] Headers are bright amber, clearly visible +- [ ] No white or light backgrounds anywhere +- [ ] Links are underlined and bright amber +- [ ] Terminal windows have proper amber/black contrast +- [ ] Sidebar is dark with amber text + +### Content +- [ ] Home page shows sine wave plot in hero terminal +- [ ] No `__init__` example in basic cookbook +- [ ] No `performance_example` in basic cookbook +- [ ] Examples start with imports, not license headers +- [ ] Navigation shows "Home" and "Cookbook" sections +- [ ] Cookbook has "Basic Plots" and "Advanced Examples" pages + +### Expected Non-Working Features (OK for now) +- [ ] `[EXEC]` buttons show "runExample is not defined" error (expected - will be fixed in Phase 6) + +--- + +## Summary + +**Total fixes needed:** 6 critical issues + +**Files to modify:** +1. `scripts/generate_docs.py` - Issues #2, #3, #4, #5 +2. `docs/stylesheets/terminal.css` - Issue #1 +3. `mkdocs.yml` - Issue #6 + +**Estimated time:** 30-45 minutes + +Once these fixes are applied and tested, you're ready to proceed with **Phase 6: Brython Integration** (Tasks 6.1-6.2) which will make the `[EXEC]` buttons functional. + +Great work so far! The foundation is solid - these are just refinements to get the styling and content filtering right before moving forward. diff --git a/docs/plans/phase11-12-review.md b/docs/plans/phase11-12-review.md new file mode 100644 index 0000000..2fd376c --- /dev/null +++ b/docs/plans/phase11-12-review.md @@ -0,0 +1,355 @@ +# Phase 11-12 Implementation Review + +## Overall Assessment + +Good work on completing Tasks 11.1, 12.1, 12.2, and 12.3! The implementation is mostly solid with clear documentation and proper error handling. The workflow is well-structured and the testing checklist is comprehensive. + +**Status:** +- āœ… Task 11.1: GitHub Actions workflow created +- āœ… Task 12.1: Testing checklist created +- āœ… Task 12.2: Error styling enhanced +- āœ… Task 12.3: README updated with documentation link +- āœ… Tasks 9.2 and 10: Correctly skipped (outdated/already done) + +--- + +## What's Working Well āœ… + +### 1. GitHub Actions Workflow - Good Quality + +**File:** `.github/workflows/docs.yml` + +**Strengths:** +- āœ… Clear, descriptive job name: "Build and Deploy Documentation" +- āœ… Dual triggers: push to master + manual dispatch +- āœ… Proper permissions set (`contents: write`) +- āœ… Uses modern action versions (checkout@v4, setup-python@v5) +- āœ… Full git history fetched (`fetch-depth: 0`) +- āœ… Python 3.11 specified (good version choice) +- āœ… Correct dependency installation: `pip install -e ".[dev]"` +- āœ… Tests run before docs build (fail fast on broken tests) +- āœ… Includes doctests: `pytest --doctest-modules plotille/ -v` +- āœ… Generates docs with custom script +- āœ… Uses `mkdocs build --strict` (catches warnings as errors) +- āœ… Deploys to gh-pages using standard action +- āœ… CNAME configured for custom domain: plotille.tammo.io +- āœ… Clean, well-commented, easy to understand + +**Quality:** Professional implementation following GitHub Actions best practices. + +### 2. Error Styling - Well Implemented + +**File:** `docs/stylesheets/terminal.css` + +**Added features:** +```css +/* Error output styling */ +.terminal-output .error { + color: #ff6b6b; + background: rgba(255, 0, 0, 0.1); + border-left: 3px solid #ff6b6b; + padding-left: 0.5rem; +} + +.output-content.error { + color: #ff6b6b; +} + +/* Loading state */ +.terminal-output .loading::after { + content: '...'; + animation: loading 1.5s infinite; +} + +@keyframes loading { + 0%, 100% { opacity: 0; } + 50% { opacity: 1; } +} +``` + +**Strengths:** +- āœ… Clear visual distinction for errors (red color, background, left border) +- āœ… Subtle, professional styling (not garish) +- āœ… Loading animation provides feedback during execution +- āœ… Uses standard red error color (#ff6b6b - soft red, good contrast) +- āœ… Animation is smooth and not distracting +- āœ… Follows the minimal CSS philosophy + +**Quality:** Clean, effective, and consistent with Material theme approach. + +### 3. README Documentation Section - Clear and Concise + +**File:** `README.md` + +**Content:** +```markdown +## Documentation + +šŸ“š **Full documentation available at [plotille.tammo.io](https://plotille.tammo.io)** + +Features: +- **Interactive examples** - Edit and run code in your browser +- **Complete API reference** - Auto-generated from source +- **Cookbook** - Examples organized by complexity +``` + +**Strengths:** +- āœ… Prominent placement at top of README +- āœ… Clear link to documentation site +- āœ… Emoji makes it stand out +- āœ… Highlights key features users will find valuable +- āœ… Concise - doesn't overwhelm +- āœ… Encourages exploration + +**Quality:** Effective marketing of the documentation. + +### 4. Testing Checklist - Comprehensive + +**File:** `docs/plans/testing-checklist.md` + +**Coverage includes:** +- Visual design (updated for Material theme) +- Navigation +- Home page +- Cookbook pages +- Interactive examples +- API reference +- Doctests +- Build process +- CI/CD +- Performance +- Browser compatibility + +**Strengths:** +- āœ… 75 specific checkpoints +- āœ… Covers all major areas +- āœ… Organized by section +- āœ… Includes technical tests (pytest commands) +- āœ… Includes UX tests (browser compatibility) +- āœ… Updated to reflect Material theme (not amber phosphor) + +--- + +## Issues Found āš ļø + +### Issue 1: Testing Checklist Outdated Reference + +**Problem:** Line 22 in `docs/plans/testing-checklist.md`: + +```markdown +- [ ] All four category pages exist (basic, figures, canvas, advanced) +``` + +**Reality:** Only two category pages exist (basic, advanced). We removed figures and canvas from navigation because they don't have examples. + +**Impact:** Minor - someone following the checklist will be confused. + +**Fix:** + +Change line 22 from: +```markdown +- [ ] All four category pages exist (basic, figures, canvas, advanced) +``` + +To: +```markdown +- [ ] Both cookbook pages exist (basic, advanced) +``` + +And change line 23: +```markdown +- [ ] Examples are categorized correctly +``` + +To: +```markdown +- [ ] Examples are categorized correctly (basic for interactive, advanced for static) +``` + +--- + +### Issue 2: Potential Dependency Management Discrepancy (Minor) + +**Observation:** The project uses `uv` (has `uv.lock` file) locally, but the GitHub Actions workflow uses `pip`. + +**Current workflow:** +```yaml +- name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" +``` + +**Why this might matter:** +- `uv` might have different dependency resolution than `pip` +- Could lead to "works locally, fails in CI" scenarios +- `uv` is much faster than `pip` + +**Impact:** +- Low - likely to work fine since dependencies are well-specified +- Could cause occasional CI failures if pip resolves differently than uv + +**Options:** + +**Option A:** Keep as-is (pip in CI) +- Simpler, more standard +- Works with GitHub's Python setup action +- Good enough for most projects + +**Option B:** Use uv in CI (more consistent) +```yaml +- name: Set up Python and uv + uses: astral-sh/setup-uv@v3 + with: + version: "latest" + +- name: Install dependencies + run: | + uv sync --dev +``` + +**Recommendation:** +- **Keep Option A for now** (current implementation) +- Only switch to uv if you encounter actual dependency resolution issues +- This follows YAGNI principle + +**Not a bug**, just a consideration for future if issues arise. + +--- + +## Testing Recommendations + +Before the workflow runs on GitHub, test locally: + +### 1. Test Workflow Steps Locally + +```bash +# 1. Test dependency installation +pip install -e ".[dev]" + +# 2. Test pytest with doctests +pytest --doctest-modules plotille/ -v + +# 3. Test doc generation +python scripts/generate_docs.py + +# 4. Test mkdocs build with strict mode +mkdocs build --strict + +# 5. Verify site directory created +ls -la site/ + +# 6. Test site locally +mkdocs serve +# Visit http://127.0.0.1:8000 and test interactive examples +``` + +### 2. Verify Error Styling + +```bash +mkdocs serve +# Visit http://127.0.0.1:8000/cookbook/basic/ +# Click [EXEC] on an example +# Modify code to cause an error (e.g., undefined variable) +# Run it +# Verify error displays with red styling and left border +``` + +### 3. Check README Rendering + +Visit your GitHub repository page and verify: +- Documentation section is visible +- Link works (after site is deployed) +- Formatting looks good + +### 4. Test Workflow Validation + +```bash +# Validate workflow YAML syntax +# (GitHub has strict YAML parsing) +python -c "import yaml; yaml.safe_load(open('.github/workflows/docs.yml'))" + +# Or use actionlint if available +# brew install actionlint +# actionlint .github/workflows/docs.yml +``` + +--- + +## GitHub Pages Setup (Task 11.2) + +**Note:** This needs to be done manually in the GitHub web interface. + +**Steps:** + +1. Push the `docs` branch to GitHub: +```bash +git push origin docs +``` + +2. Go to repository Settings → Pages + +3. Under "Build and deployment": + - Source: "Deploy from a branch" + - Branch: Select `gh-pages` (will be created by workflow) + - Folder: `/ (root)` + +4. Under "Custom domain": + - Enter: `plotille.tammo.io` + - Wait for DNS check + +5. Enable "Enforce HTTPS" once DNS check passes + +6. **DNS Configuration** (at your domain registrar): + - Add CNAME record: + - Host: `plotille` + - Points to: `tammoippen.github.io` + +7. **Trigger the workflow:** + - Merge `docs` branch to `master`, OR + - Manually trigger via Actions tab → "Build and Deploy Documentation" → Run workflow + +8. **Verify deployment:** + - Check Actions tab for successful run + - Visit https://plotille.tammo.io (after DNS propagates) + +--- + +## Summary + +**Completed Successfully:** +- āœ… GitHub Actions workflow with all necessary steps +- āœ… Testing checklist with 75+ checkpoints +- āœ… Error and loading state styling for terminal +- āœ… README updated with clear documentation link +- āœ… All code committed to docs branch + +**Needs Attention:** +- āš ļø Update testing checklist (line 22-23) to reflect reality (2 cookbook pages, not 4) + +**Considerations for Future:** +- šŸ’” Potential pip vs uv discrepancy (not urgent, only if issues arise) + +**Quality Assessment:** +- Code quality: Very good +- Documentation: Clear and comprehensive +- Workflow structure: Professional +- Following standards: Yes +- Ready for deployment: Yes (after checklist fix) + +**Time to fix:** 2 minutes (edit testing checklist) + +--- + +## Next Steps + +1. **Fix testing checklist** (update lines 22-23) +2. **Test workflow locally** (run all 5 steps) +3. **Push to GitHub** (`git push origin docs`) +4. **Merge to master** (or create PR) +5. **Configure GitHub Pages** (Task 11.2 - manual steps above) +6. **Wait for workflow to run** (check Actions tab) +7. **Verify site** (visit plotille.tammo.io after DNS propagates) +8. **Run through testing checklist** (docs/plans/testing-checklist.md) + +The implementation is production-ready and follows best practices. Great work! diff --git a/docs/plans/phase6-7-review.md b/docs/plans/phase6-7-review.md new file mode 100644 index 0000000..b1e0b76 --- /dev/null +++ b/docs/plans/phase6-7-review.md @@ -0,0 +1,364 @@ +# Phase 6-7 Implementation Review + +## Overall Assessment + +Good progress on Tasks 5.2-7.1, but there are **critical bugs** that prevent the documentation from building, plus the Brython implementation needs completion of Task 7.2. The infrastructure is mostly in place but needs fixes before it can work. + +**Current Status:** +- āœ… Task 5.2: Font bundling skipped (correct per YAGNI) +- āœ… Task 6.1: Brython runtime added +- āœ… Task 6.2: Plotille source copied successfully +- āœ… Task 7.1: Textarea enhancement added +- āŒ Task 7.2: **NOT COMPLETED** - This is required for Brython to actually work + +--- + +## Critical Issues (Build-Breaking) + +### 1. Documentation Build Crashes āŒ **CRITICAL** + +**Issue:** Running `python scripts/generate_docs.py` crashes with: +``` +IsADirectoryError: [Errno 21] Is a directory: '.' +``` + +**Root Cause:** When static examples fail to execute (because numpy/PIL aren't installed), they don't get added to `output_paths` dict. Then `generate_category_page()` calls: +```python +output_path = output_paths.get(info.name, Path()) +``` + +This returns `Path()` which is `.` (current directory). Later, `generate_static_example_markdown()` checks: +```python +if output_path.exists(): + output = output_path.read_text() +``` + +`Path('.')` exists (as a directory), so it tries to read a directory as a file → crash. + +**Fix Required in `scripts/generate_docs.py`:** + +Change `generate_category_page()`: + +```python +def generate_category_page( + category: str, + examples: list[ExampleInfo], + output_paths: dict[str, Path], + docs_dir: Path, +) -> Path: + """Generate a markdown page for a category of examples.""" + # ... existing code ... + + # Add each example + for info in examples: + if info.is_interactive: + markdown = generate_interactive_example_markdown(info) + else: + # FIX: Pass None if output doesn't exist, not Path() + output_path = output_paths.get(info.name) # Returns None if not found + markdown = generate_static_example_markdown(info, output_path) + + content.append(markdown) + + # ... rest of function ... +``` + +And update `generate_static_example_markdown()`: + +```python +def generate_static_example_markdown( + info: ExampleInfo, + output_path: Path | None, # Allow None +) -> str: + """Generate markdown for a static example with pre-rendered output.""" + source_code = info.path.read_text() + + # Strip license header + source_code = strip_license_header(source_code) + + # Read pre-rendered output + # FIX: Check for None and valid file + if output_path and output_path.is_file(): + output = output_path.read_text() + else: + output = "Output not available (dependencies not installed during build)" + + deps = ", ".join(sorted(info.imports - {"plotille"})) + + # ... rest of function ... +``` + +**Test:** +```bash +python scripts/generate_docs.py +# Should complete without errors +``` + +--- + +## Major Issues (Functionality Broken) + +### 2. Task 7.2 Not Completed āš ļø **REQUIRED** + +**Issue:** The current `runExample()` function in `brython-setup.js` uses incorrect Brython API and won't work properly: + +```javascript +// Current implementation (from Task 6.1) - INCORRECT: +window.__BRYTHON__.python_to_js(code); // This API doesn't exist +const result = eval(window.__BRYTHON__.imported['__main__']); // Wrong approach +``` + +**Impact:** Clicking `[EXEC]` buttons will either: +- Do nothing +- Show errors +- Not capture output correctly + +**What's Missing:** Task 7.2 in the implementation plan specifies a completely different approach that: +- Creates a ` + + + + + +

Plotille Brython Compatibility Test

+ +

This page tests whether plotille can run in the browser using Brython (Python in the browser).

+ +

Test: Olympic Rings Example

+
+ +import plotille +import plotille.data as plt_data + +fig = plotille.Figure() +fig.width = 50 +fig.height = 20 + +fig.set_x_limits(min_=0, max_=600) +fig.set_y_limits(min_=0, max_=500) + +centers = [] +centers.append([250, 200, "blue"]) +centers.append([375, 200, "white"]) +centers.append([500, 200, "red"]) +centers.append([310, 250, "yellow"]) +centers.append([435, 250, "green"]) + +for ring in centers: + X, Y = plt_data.circle(x_center=ring[0], y_center=500 - ring[1], radius=50) + fig.plot(X, Y, lc=ring[2]) + +print(fig.show(legend=False)) + +
+ + + +
+ +

Output:

+
+ + + + diff --git a/tests/test_generate_docs.py b/tests/test_generate_docs.py new file mode 100644 index 0000000..dce92af --- /dev/null +++ b/tests/test_generate_docs.py @@ -0,0 +1,176 @@ +"""Tests for documentation generation script.""" + +import sys +from pathlib import Path + +# Add scripts to path for testing +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +import generate_docs + + +def test_extract_imports_simple() -> None: + """Test extracting imports from simple code.""" + code = "import numpy\nimport plotille" + imports = generate_docs.extract_imports(code) + assert imports == {"numpy", "plotille"} + + +def test_extract_imports_from() -> None: + """Test extracting from-imports.""" + code = "from PIL import Image" + imports = generate_docs.extract_imports(code) + assert imports == {"PIL"} + + +def test_extract_description_docstring() -> None: + """Test extracting description from docstring.""" + code = '"""This is a test"""\nprint("hi")' + desc = generate_docs.extract_description(code) + assert desc == "This is a test" + + +def test_extract_description_comment() -> None: + """Test extracting description from comment.""" + code = "# This is a comment\nprint('hi')" + desc = generate_docs.extract_description(code) + assert desc == "This is a comment" + + +def test_is_interactive_pure() -> None: + """Test interactive detection for pure plotille.""" + imports = {"plotille", "math", "random"} + assert generate_docs.is_interactive(imports) is True + + +def test_is_interactive_numpy() -> None: + """Test interactive detection with numpy.""" + imports = {"plotille", "numpy"} + assert generate_docs.is_interactive(imports) is False + + +def test_is_interactive_pillow() -> None: + """Test interactive detection with PIL.""" + imports = {"PIL", "plotille"} + assert generate_docs.is_interactive(imports) is False + + +def test_categorize_basic() -> None: + """Test basic example categorization.""" + from pathlib import Path + + info = generate_docs.ExampleInfo( + Path("scatter.py"), "scatter", "", {"plotille"}, True + ) + assert generate_docs.categorize_example(info) == "basic" + + +def test_categorize_canvas() -> None: + """Test canvas example categorization.""" + from pathlib import Path + + info = generate_docs.ExampleInfo( + Path("canvas_test.py"), "canvas_test", "", {"plotille"}, True + ) + assert generate_docs.categorize_example(info) == "canvas" + + +def test_categorize_advanced() -> None: + """Test advanced example categorization.""" + from pathlib import Path + + info = generate_docs.ExampleInfo( + Path("image.py"), "image", "", {"PIL", "plotille"}, False + ) + assert generate_docs.categorize_example(info) == "advanced" + + +def test_execute_example(tmp_path): + """Test executing a simple example.""" + # Create a simple test example + test_example = tmp_path / "test.py" + test_example.write_text('print("Hello from example")') + + output = generate_docs.execute_example(test_example) + + assert output.success is True + assert output.returncode == 0 + assert "Hello from example" in output.stdout + + +def test_execute_example_error(tmp_path): + """Test executing an example that fails.""" + test_example = tmp_path / "test.py" + test_example.write_text('raise ValueError("test error")') + + output = generate_docs.execute_example(test_example) + + assert output.success is False + assert output.returncode != 0 + assert "ValueError" in output.stderr + + +def test_save_example_output(tmp_path): + """Test saving example output.""" + from pathlib import Path + + info = generate_docs.ExampleInfo(Path("test.py"), "test", "desc", set(), True) + output = generate_docs.ExampleOutput( + stdout="test output", stderr="", returncode=0, success=True + ) + + output_dir = tmp_path / "outputs" + saved_path = generate_docs.save_example_output(info, output, output_dir) + + assert saved_path.exists() + assert saved_path.read_text() == "test output" + + +def test_generate_interactive_example_markdown(tmp_path): + """Test generating markdown for interactive example.""" + + # Create actual test file + test_py = tmp_path / "test.py" + test_py.write_text('print("hi")') + + info = generate_docs.ExampleInfo( + path=test_py, + name="test", + description="Test example", + imports={"plotille"}, + is_interactive=True, + ) + + markdown = generate_docs.generate_interactive_example_markdown(info) + + assert "## test" in markdown + assert "Test example" in markdown + assert "interactive-example" in markdown + assert 'print("hi")' in markdown + + +def test_generate_static_example_markdown(tmp_path): + """Test generating markdown for static example.""" + + # Create actual test files + test_py = tmp_path / "test.py" + test_py.write_text('print("hi")') + + info = generate_docs.ExampleInfo( + path=test_py, + name="test", + description="Test example", + imports={"plotille", "numpy"}, + is_interactive=False, + ) + + # Create mock output file + output_path = tmp_path / "test.txt" + output_path.write_text("Example output here") + + markdown = generate_docs.generate_static_example_markdown(info, output_path) + + assert "## test" in markdown + assert "Test example" in markdown + assert "numpy" in markdown + assert "Example output here" in markdown diff --git a/uv.lock b/uv.lock index 0db8973..bd55cdc 100644 --- a/uv.lock +++ b/uv.lock @@ -2,10 +2,186 @@ version = 1 revision = 3 requires-python = ">=3.10, <4" resolution-markers = [ - "python_full_version >= '3.11'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] +[[package]] +name = "ansi2html" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/d5/e3546dcd5e4a9566f4ed8708df5853e83ca627461a5b048a861c6f8e7a26/ansi2html-1.9.2.tar.gz", hash = "sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5", size = 44300, upload-time = "2024-06-22T17:33:23.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/71/aee71b836e9ee2741d5694b80d74bfc7c8cd5dbdf7a9f3035fcf80d792b1/ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5", size = 17614, upload-time = "2024-06-22T17:33:21.852Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "brython" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/e45c6e3810e67d2bc8bb16ceb83b318a0f5a4a2fd720fafce013ef95e279/brython-3.11.1.tar.gz", hash = "sha256:1c1a645772727f1508bdb4075491b86d4330ea55ae7ca566bcdac3c27ee87cd0", size = 1533629, upload-time = "2023-01-30T14:24:00.757Z" } + +[[package]] +name = "brython" +version = "3.11.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/03e40b5af20bbccab502c136d131fb3fc67e154034ac4ec4cff2401264f3/brython-3.11.3.tar.gz", hash = "sha256:aac080464df0f208ddc22b1b667fcdd17c13abc3eeac4d8aca04dcd424fb625b", size = 1555405, upload-time = "2023-06-19T10:29:11.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/75/7068220ab2ed9edd630d4ae1f7961307c79ef41d8e55cae2ea72556b443f/brython-3.11.3-py3-none-any.whl", hash = "sha256:a0332fdb630a759a5a8cff3a9d0ff0bcad34dd6ef600f0f7920d90bb52c92cc7", size = 1576196, upload-time = "2023-06-19T10:29:09.292Z" }, +] + +[[package]] +name = "brython" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/a1/fe7c4f852412be11888aa1cce8140627a0b21220ba5a6c2eb12f21c8fa56/brython-3.14.0.tar.gz", hash = "sha256:e52ee7c01cd52c62cabfc0d15313f8d47b16746523241fa14474e07de4e212de", size = 1574480, upload-time = "2025-10-11T15:19:24.349Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/39/cc486893d99eb85d276057b3b105ecd7fd37735bb5929feb72872d4516b9/brython-3.14.0-py3-none-any.whl", hash = "sha256:84cb0188e4e11ba813b756d502637da86010c9c3467eb28be3aedc3b3b96708f", size = 1598879, upload-time = "2025-10-11T15:19:22.448Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[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]] name = "colorama" version = "0.4.6" @@ -131,6 +307,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -140,6 +349,18 @@ 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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "librt" version = "0.7.4" @@ -213,6 +434,253 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, ] +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, +] + +[[package]] +name = "mkdocs-gen-files" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/35/f26349f7fa18414eb2e25d75a6fa9c7e3186c36e1d227c0b2d785a7bd5c4/mkdocs_gen_files-0.6.0.tar.gz", hash = "sha256:52022dc14dcc0451e05e54a8f5d5e7760351b6701eff816d1e9739577ec5635e", size = 8642, upload-time = "2025-11-23T12:13:22.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/ec/72417415563c60ae01b36f0d497f1f4c803972f447ef4fb7f7746d6e07db/mkdocs_gen_files-0.6.0-py3-none-any.whl", hash = "sha256:815af15f3e2dbfda379629c1b95c02c8e6f232edf2a901186ea3b204ab1135b2", size = 8182, upload-time = "2025-11-23T12:13:20.756Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-literate-nav" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/5f/99aa379b305cd1c2084d42db3d26f6de0ea9bf2cc1d10ed17f61aff35b9a/mkdocs_literate_nav-0.6.2.tar.gz", hash = "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75", size = 17419, upload-time = "2025-03-18T21:53:09.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/84/b5b14d2745e4dd1a90115186284e9ee1b4d0863104011ab46abb7355a1c3/mkdocs_literate_nav-0.6.2-py3-none-any.whl", hash = "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", size = 13261, upload-time = "2025-03-18T21:53:08.1Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/13/10bbf9d56565fd91b91e6f5a8cd9b9d8a2b101c4e8ad6eeafa35a706301d/mkdocstrings-1.0.0.tar.gz", hash = "sha256:351a006dbb27aefce241ade110d3cd040c1145b7a3eb5fd5ac23f03ed67f401a", size = 101086, upload-time = "2025-11-27T15:39:40.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/fc/80aa31b79133634721cf7855d37b76ea49773599214896f2ff10be03de2a/mkdocstrings-1.0.0-py3-none-any.whl", hash = "sha256:4c50eb960bff6e05dfc631f6bc00dfabffbcb29c5ff25f676d64daae05ed82fa", size = 35135, upload-time = "2025-11-27T15:39:39.301Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, +] + [[package]] name = "mypy" version = "1.19.1" @@ -338,7 +806,8 @@ name = "numpy" version = "2.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11'", + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } wheels = [ @@ -424,6 +893,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -531,13 +1009,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, ] +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + [[package]] name = "plotille" -version = "5.2.0" +version = "6.0.0" source = { editable = "." } [package.dev-dependencies] dev = [ + { name = "ansi2html" }, + { name = "brython", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "brython", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "brython", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "mkdocs" }, + { name = "mkdocs-gen-files" }, + { name = "mkdocs-literate-nav" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -553,6 +1049,13 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "ansi2html", specifier = ">=1.9.2" }, + { name = "brython", specifier = ">=3.11.1" }, + { name = "mkdocs", specifier = ">=1.5.0" }, + { name = "mkdocs-gen-files", specifier = ">=0.5.0" }, + { name = "mkdocs-literate-nav", specifier = ">=0.6.0" }, + { name = "mkdocs-material", specifier = ">=9.5.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.24.0" }, { name = "mypy" }, { name = "numpy" }, { name = "pillow" }, @@ -581,6 +1084,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -625,6 +1141,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + [[package]] name = "ruff" version = "0.14.10" @@ -651,6 +1270,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -733,3 +1361,44 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 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]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +]