commit 49d337b04a35141266c49ad48f205cd3a43eefdb Author: tcsenpai Date: Wed Jan 15 14:31:35 2025 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06e9a88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Node +node_modules/ + +*.pdf +*.xlsx +*.xls \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e54937 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# Overseer + +A command-line tool for scanning and managing code comments in your projects. It helps you track TODOs, FIXMEs, and other comment markers across your codebase. + +![overseer](./screenshot.png) + +## Features + +- 🔍 Scan for comment markers (TODO, FIXME, etc.) in your codebase +- 📁 Filter files by name (partial or exact match) +- 🎯 Customizable comment markers to include/exclude +- 📝 Show code context around comments +- 📤 Export results to different formats (text, JSON, PDF) +- 🚫 Respects .gitignore patterns +- 🔒 Skip hidden files and directories + +## Configuration and compatibility + +### Supported languages + +- TypeScript +- JavaScript +- Python +- C/C++ +- Java +- Ruby +- PHP + +To add support for a new language, you would simply add its patterns to the `COMMENT_PATTERNS` dictionary in `config.py`. + +### Default markers + +- TODO +- FIXME +- REVIEW +- NOTE +- ! +- ? + +You can add your own markers by editing the `config.py` file. + +## Installation + +### Clone the repository + +```bash +git clone https://github.com/tcsenpai/overseer.git +cd overseer +``` + +### Install dependencies + +```bash +pip install -r requirements.txt +``` + +or, using uv: + +NOTE: Using uv, you can run the script directly without installing dependencies as `uv run python main.py` (see below for usage), as the `pyproject.toml` file is present. You can still install dependencies using the command below if you prefer. + +```bash +uv pip install -r requirements.txt +``` + +## Usage + +```bash +python main.py --help +``` + +or, using uv: + +```bash +uv run python main.py --help +``` + +```bash + +usage: main.py [-h] [--workspace WORKSPACE] [--skip SKIP [SKIP ...]] [--include-all] [--no-context] [--export {pdf,xlsx}] [--output OUTPUT] + [--filename FILENAME] [--complete-match] [--case-sensitive] + +Scan TypeScript project comments + +options: + -h, --help show this help message and exit + --workspace WORKSPACE, -w WORKSPACE + Path to the workspace directory + --skip SKIP [SKIP ...], -s SKIP [SKIP ...] + Markers to skip (e.g., --skip NOTE TODO) + --include-all, -a Include all markers (override default skip) + --no-context, -nc Don't show context lines around comments + --export {pdf,xlsx}, -e {pdf,xlsx} + Export format (pdf or xlsx) + --output OUTPUT, -o OUTPUT + Output file path for export + +filename filtering: + --filename FILENAME, -f FILENAME + Filter files by filename (case insensitive by default) + --complete-match, -c Match complete filename instead of partial (only with -f) + --case-sensitive, -C Make filename filter case sensitive (only with -f) +``` + +## Examples + +```bash + main.py # Scan all files with default settings + main.py -w /path/to/project # Scan a specific workspace + main.py -f test.py # Find comments in files containing 'test.py' (case insensitive) + main.py -f test.py -c # Find comments in files named exactly 'test.py' + main.py -f Test.py -C # Find comments with case-sensitive filename match + main.py -f test.py -c -C # Find comments in files named exactly 'test.py' (case sensitive) + main.py --skip TODO FIXME # Skip TODO and FIXME comments + main.py -a # Include all comment types + main.py -e pdf -o comments.pdf # Export comments to PDF +``` + +## Command Line Options + +| Option | Short | Description | +| ------------------ | ----- | -------------------------------------------------- | +| `--workspace` | `-w` | Specify workspace directory to scan | +| `--filename` | `-f` | Filter files by name (case insensitive by default) | +| `--complete-match` | `-c` | Match complete filename instead of partial | +| `--case-sensitive` | `-C` | Make filename filter case sensitive | +| `--skip` | | Skip specified comment markers | +| `--include-all` | `-a` | Include all comment types | +| `--no-context` | | Hide code context around comments | +| `--export` | `-e` | Export format (text, json, pdf) | +| `--output` | `-o` | Output file path | + +## Configuration + +The tool respects: + +- `.gitignore` patterns in your project +- Hidden files and directories (starting with .) +- Project-specific file patterns (configured in `config.py`) diff --git a/config.py b/config.py new file mode 100644 index 0000000..07da7cf --- /dev/null +++ b/config.py @@ -0,0 +1,97 @@ +from typing import Dict +from pathlib import Path + +# Default workspace path (can be overridden by CLI argument) +DEFAULT_WORKSPACE = Path.cwd() # Current working directory by default + +# Supported file patterns +FILE_PATTERNS = [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx", + "*.py", + "*.rs", + "*.go", + "*.java", + "*.c", + "*.h", + "*.cpp", + "*.hpp", + "*.cs", + "*.vb", + "*.sql", + "*.md", + "*.txt", + "*.yaml", + "*.yml", + #"*.json", + "*.xml", + "*.html", + "*.css", + "*.scss", + "*.sass", + "*.less", + "*.styl", + "*.stylus", + "*.scss", + "*.sass", + "*.less", + "*.styl", + "*.stylus", + "*.scss", + "*.sass", + "*.less", + "*.styl", + "*.stylus", +] + +# Comment markers and their descriptions +COMMENT_MARKERS: Dict[str, str] = { + "!": "Important", + "TODO": "To Do", + "?": "Question", + "REVIEW": "Needs Review", + "FIXME": "Fix Required", + "NOTE": "Note", +} + +# Color scheme for different comment types +COMMENT_COLORS: Dict[str, str] = { + "!": "red", + "TODO": "yellow", + "?": "blue", + "REVIEW": "magenta", + "FIXME": "red", + "NOTE": "green", +} + +# Additional exclusions +DEFAULT_EXCLUDES = {"node_modules", "__pycache__", "build", "dist"} + +# Default settings +DEFAULT_SKIP_MARKERS = {"NOTE"} # Skip NOTE comments by default +CONTEXT_LINES = 2 # Number of lines before and after to show + +# Output formats +EXPORT_FORMATS = ["pdf", "xlsx"] # Supported export formats + +COMMENT_PATTERNS = { + # Default pattern (for unknown extensions) + "default": {"single": ["//"], "multiline": None}, + # Python + "py": {"single": ["#"], "multiline": ['"""', '"""']}, # Also handles ''' + # JavaScript/TypeScript + "js": {"single": ["//"], "multiline": ["/*", "*/"]}, + "ts": {"single": ["//"], "multiline": ["/*", "*/"]}, + # C/C++ + "c": {"single": ["//"], "multiline": ["/*", "*/"]}, + "cpp": {"single": ["//"], "multiline": ["/*", "*/"]}, + "h": {"single": ["//"], "multiline": ["/*", "*/"]}, + # Java + "java": {"single": ["//"], "multiline": ["/*", "*/"]}, + # Ruby + "rb": {"single": ["#"], "multiline": ["=begin", "=end"]}, + # PHP + "php": {"single": ["//"], "multiline": ["/*", "*/"]}, +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..1304107 --- /dev/null +++ b/main.py @@ -0,0 +1,536 @@ +from pathlib import Path +from typing import List, Dict, Set +from rich.console import Console +from rich.table import Table +import os +import config +import argparse +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern +import pandas as pd +from fpdf import FPDF +from rich.progress import ( + Progress, + SpinnerColumn, + TextColumn, + TimeElapsedColumn, + BarColumn, + TaskProgressColumn, +) + + +class CommentScanner: + def __init__( + self, + workspace_path: str = None, + skip_markers: Set[str] = None, + show_context: bool = True, + ): + # Resolve relative paths + workspace_path = workspace_path or config.DEFAULT_WORKSPACE + self.workspace_path = Path(workspace_path).resolve() + self.console = Console() + self.exclude_patterns = self._load_gitignore() + self.skip_markers = skip_markers or config.DEFAULT_SKIP_MARKERS + self.show_context = show_context + + def _load_gitignore(self) -> PathSpec: + gitignore_patterns = [] + gitignore_path = self.workspace_path / ".gitignore" + + # Add default exclusions + for exclude in config.DEFAULT_EXCLUDES: + gitignore_patterns.append(exclude) + + # Read .gitignore if it exists + if gitignore_path.exists(): + with open(gitignore_path, "r", encoding="utf-8") as f: + gitignore_patterns.extend( + line.strip() + for line in f + if line.strip() and not line.startswith("#") + ) + + return PathSpec.from_lines(GitWildMatchPattern, gitignore_patterns) + + def should_skip_path( + self, + path: Path, + filename_filter: str = None, + case_sensitive: bool = False, + complete_match: bool = False, + ) -> bool: + """Check if a path should be skipped based on exclusion rules and filename filter.""" + try: + # Convert path to relative path from workspace root + rel_path = path.relative_to(self.workspace_path) + + # Apply filename filter if provided + if filename_filter: + filename = path.name + if complete_match: + if case_sensitive: + if filename != filename_filter: + return True + else: + if filename.lower() != filename_filter.lower(): + return True + else: + if case_sensitive: + if filename_filter not in filename: + return True + else: + if filename_filter.lower() not in filename.lower(): + return True + + # Check if path matches gitignore patterns + if self.exclude_patterns.match_file(str(rel_path)): + return True + + # Skip hidden files and directories + if any(part.startswith(".") for part in path.parts): + return True + + return False + except ValueError: # For paths outside workspace + return True + + def get_context_lines(self, all_lines: List[str], comment_line_idx: int) -> str: + context = [] + start_idx = max(0, comment_line_idx - config.CONTEXT_LINES) + end_idx = min(len(all_lines), comment_line_idx + config.CONTEXT_LINES + 1) + + # Get lines before + for i in range(start_idx, comment_line_idx): + line = all_lines[i].strip() + if line: # Skip empty lines + context.append(f" {line}") + + # Add the comment line itself + context.append(f"→ {all_lines[comment_line_idx].strip()}") + + # Get lines after + for i in range(comment_line_idx + 1, end_idx): + line = all_lines[i].strip() + if line: # Skip empty lines + context.append(f" {line}") + + return "\n".join(context) + + def scan_file(self, file_path: Path) -> List[Dict]: + comments = [] + file_extension = file_path.suffix.lower()[1:] + + # Skip files we don't support + if file_extension not in config.COMMENT_PATTERNS: + return comments + + comment_patterns = config.COMMENT_PATTERNS[file_extension] + + # Pre-compile patterns for faster matching + single_patterns = comment_patterns.get("single", []) + multiline_pattern = comment_patterns.get("multiline") + + # Quick check if file might contain any markers + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + if not any(marker in content for marker in config.COMMENT_MARKERS): + return comments + + # Reset file pointer and continue with line-by-line processing + f.seek(0) + lines = f.readlines() + except UnicodeDecodeError: + return comments # Skip binary files + + in_multiline_comment = False + multiline_content = [] + + for line_num, line in enumerate(lines): + stripped_line = line.strip() + if not stripped_line: # Skip empty lines early + continue + + # Fast path: check if line might contain any comment + if not any( + pattern in stripped_line for pattern in single_patterns + ) and not ( + multiline_pattern + and ( + multiline_pattern[0] in stripped_line + or multiline_pattern[1] in stripped_line + ) + ): + continue + + # Handle multiline comments + if multiline_pattern: + start_pattern, end_pattern = multiline_pattern + + if ( + start_pattern in stripped_line + and end_pattern + in stripped_line[ + stripped_line.find(start_pattern) + len(start_pattern) : + ] + ): + comment_text = stripped_line[ + stripped_line.find(start_pattern) + + len(start_pattern) : stripped_line.rfind(end_pattern) + ].strip() + self._process_comment( + comment_text, comments, file_path, line_num, lines + ) + continue + + if start_pattern in stripped_line and not in_multiline_comment: + in_multiline_comment = True + multiline_content = [ + stripped_line[ + stripped_line.find(start_pattern) + len(start_pattern) : + ].strip() + ] + continue + + if in_multiline_comment: + if end_pattern in stripped_line: + in_multiline_comment = False + multiline_content.append( + stripped_line[: stripped_line.find(end_pattern)].strip() + ) + comment_text = " ".join(multiline_content) + self._process_comment( + comment_text, comments, file_path, line_num, lines + ) + multiline_content = [] + else: + multiline_content.append(stripped_line) + continue + + # Handle single-line comments + for pattern in single_patterns: + if pattern in stripped_line: + comment_text = stripped_line[ + stripped_line.find(pattern) + len(pattern) : + ].strip() + self._process_comment( + comment_text, comments, file_path, line_num, lines + ) + break + + return comments + + def _process_comment( + self, + comment_text: str, + comments: List[Dict], + file_path: Path, + line_num: int, + lines: List[str], + ) -> None: + """Helper method to process and add valid comments to the comments list.""" + for marker in config.COMMENT_MARKERS: + if comment_text.startswith(marker) and marker not in self.skip_markers: + comments.append( + { + "type": marker, + "text": comment_text[len(marker) :].strip(), + "file": str(file_path.relative_to(self.workspace_path)), + "line": line_num + 1, + "context": self.get_context_lines(lines, line_num), + } + ) + break + + def scan_workspace( + self, + filename_filter: str = None, + case_sensitive: bool = True, + complete_match: bool = False, + ) -> List[Dict]: + all_comments = [] + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + TimeElapsedColumn(), + console=self.console, + ) as progress: + # Start with an indeterminate progress bar + scan_task = progress.add_task("[cyan]Scanning files...", total=None) + files_processed = 0 + + for pattern in config.FILE_PATTERNS: + try: + for file_path in self.workspace_path.rglob(pattern): + if file_path.is_file() and not self.should_skip_path( + file_path, filename_filter, case_sensitive, complete_match + ): + try: + files_processed += 1 + progress.update( + scan_task, + completed=files_processed, + description=f"[cyan]Scanning: {file_path.name}", + ) + file_comments = self.scan_file(file_path) + if file_comments: # Only extend if we found comments + all_comments.extend(file_comments) + except Exception as e: + self.console.print( + f"Error scanning {file_path}: {e}", style="red" + ) + except Exception as e: + self.console.print(f"Error during workspace scan: {e}", style="red") + + return all_comments + + def display_comments(self, comments: List[Dict]): + table = Table(title="Project Comments Overview", show_lines=True) + + table.add_column("Type", style="bold") + table.add_column("Comment") + if self.show_context: + table.add_column("Context", style="dim") + table.add_column("File", style="dim") + table.add_column("Line", style="dim") + + for comment in sorted(comments, key=lambda x: x["type"]): + row = [ + config.COMMENT_MARKERS[comment["type"]], + comment["text"], + comment["file"], + str(comment["line"]), + ] + if self.show_context: + row.insert(2, comment["context"]) + + table.add_row( + *row, style=config.COMMENT_COLORS.get(comment["type"], "white") + ) + + self.console.print(table) + + def export_to_pdf(self, comments: List[Dict], output_path: str): + class PDF(FPDF): + def multi_cell_row(self, heights, cols, border=1): + # Calculate max number of lines for all columns + max_lines = 0 + lines = [] + # Adjust widths based on whether context is shown + if self.show_context: + widths = [20, 60, 60, 60, 20] # Type, Comment, Context, File, Line + else: + widths = [20, 60, 60, 20] # Type, Comment, File, Line + + x_start = self.get_x() + for i, col in enumerate(cols): + self.set_x(x_start) + lines.append( + self.multi_cell( + widths[i], heights, col, border=border, split_only=True + ) + ) + max_lines = max(max_lines, len(lines[-1])) + + # Draw multi-cells with same height + height_of_line = heights + x_start = self.get_x() + for i in range(max_lines): + self.set_x(x_start) + for j, width in enumerate(widths): + content = lines[j][i] if i < len(lines[j]) else "" + self.multi_cell(width, height_of_line, content, border=border) + self.set_xy(self.get_x() + width, self.get_y() - height_of_line) + self.ln(height_of_line) + + return max_lines * height_of_line + + pdf = PDF() + pdf.set_auto_page_break(auto=True, margin=15) + pdf.add_page() + pdf.set_font("Arial", size=10) + pdf.show_context = self.show_context + + # Add title + pdf.set_font("Arial", "B", 14) + pdf.cell(0, 10, "Project Comments Overview", ln=True, align="C") + pdf.ln(5) + pdf.set_font("Arial", size=10) + + # Headers + headers = ["Type", "Comment", "File", "Line"] + if self.show_context: + headers.insert(2, "Context") + + pdf.set_fill_color(240, 240, 240) + pdf.multi_cell_row(8, headers) + + # Content + for comment in sorted(comments, key=lambda x: x["type"]): + try: + row = [ + config.COMMENT_MARKERS[comment["type"]], + comment["text"], + comment["file"], + str(comment["line"]), + ] + + if self.show_context: + # Clean up context for PDF compatibility + context = comment["context"] + context = context.replace("→", ">") + context = context.encode("ascii", "replace").decode("ascii") + context = context.replace( + "\n", " | " + ) # Replace line breaks with separator + row.insert(2, context) + + # Clean up all cells for PDF compatibility + row = [ + str(cell).encode("ascii", "replace").decode("ascii") for cell in row + ] + + pdf.multi_cell_row(8, row) + + except Exception as e: + self.console.print( + f"Warning: Skipped row due to encoding issue: {e}", style="yellow" + ) + + pdf.output(output_path) + + def export_to_excel(self, comments: List[Dict], output_path: str): + df_data = [] + for comment in comments: + row = { + "Type": config.COMMENT_MARKERS[comment["type"]], + "Comment": comment["text"], + "File": comment["file"], + "Line": comment["line"], + } + if self.show_context: + row["Context"] = comment["context"] + df_data.append(row) + + df = pd.DataFrame(df_data) + df.to_excel(output_path, index=False, engine="openpyxl") + + +def main(): + parser = argparse.ArgumentParser( + description="Scan TypeScript project comments", + epilog=""" +Examples: + %(prog)s # Scan all files with default settings + %(prog)s -w /path/to/project # Scan a specific workspace + %(prog)s -f test.py # Find comments in files containing 'test.py' (case insensitive) + %(prog)s -f test.py -c # Find comments in files named exactly 'test.py' + %(prog)s -f Test.py -C # Find comments with case-sensitive filename match + %(prog)s -f test.py -c -C # Find comments in files named exactly 'test.py' (case sensitive) + %(prog)s --skip TODO FIXME # Skip TODO and FIXME comments + %(prog)s -a # Include all comment types + %(prog)s -e pdf -o comments.pdf # Export comments to PDF + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--workspace", "-w", type=str, help="Path to the workspace directory" + ) + parser.add_argument( + "--skip", + "-s", + type=str, + nargs="+", + help="Markers to skip (e.g., --skip NOTE TODO)", + default=list(config.DEFAULT_SKIP_MARKERS), + ) + parser.add_argument( + "--include-all", + "-a", + action="store_true", + help="Include all markers (override default skip)", + ) + parser.add_argument( + "--no-context", + "-nc", + action="store_true", + help="Don't show context lines around comments", + ) + parser.add_argument( + "--export", + "-e", + type=str, + choices=config.EXPORT_FORMATS, + help="Export format (pdf or xlsx)", + ) + parser.add_argument("--output", "-o", type=str, help="Output file path for export") + + # Create a filename filter group + filename_group = parser.add_argument_group("filename filtering") + filename_group.add_argument( + "--filename", + "-f", + type=str, + help="Filter files by filename (case insensitive by default)", + ) + filename_group.add_argument( + "--complete-match", + "-c", + action="store_true", + help="Match complete filename instead of partial (only with -f)", + ) + filename_group.add_argument( + "--case-sensitive", + "-C", + action="store_true", + help="Make filename filter case sensitive (only with -f)", + ) + + args = parser.parse_args() + + # Update validation + if args.case_sensitive and not args.filename: + parser.error("--case-sensitive can only be used with --filename") + if args.complete_match and not args.filename: + parser.error("--complete-match can only be used with --filename") + + try: + skip_markers = set() if args.include_all else set(args.skip) + scanner = CommentScanner( + args.workspace, skip_markers, show_context=not args.no_context + ) + comments = scanner.scan_workspace( + filename_filter=args.filename, + case_sensitive=args.case_sensitive, + complete_match=args.complete_match, + ) + + if not comments: + scanner.console.print("No comments found!", style="yellow") + return + + # Display in console + scanner.display_comments(comments) + + # Export if requested + if args.export: + if not args.output: + raise ValueError("Output path (-o) is required when exporting") + + if args.export == "pdf": + scanner.export_to_pdf(comments, args.output) + elif args.export == "xlsx": + scanner.export_to_excel(comments, args.output) + + scanner.console.print(f"\nExported to {args.output}", style="green") + + except Exception as e: + Console().print(f"Error: {str(e)}", style="red") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d187df2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "work-manager" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "fpdf>=1.7.2", + "openpyxl>=3.1.5", + "pandas>=2.2.3", + "pathspec>=0.12.1", + "rich>=13.9.4", +] diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..19a6901 Binary files /dev/null and b/screenshot.png differ diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..98622fc --- /dev/null +++ b/uv.lock @@ -0,0 +1,234 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version < '3.12'", + "python_full_version >= '3.12'", +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, +] + +[[package]] +name = "fpdf" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/c6/608a9e6c172bf9124aa687ec8b9f0e8e5d697d59a5f4fad0e2d5ec2a7556/fpdf-1.7.2.tar.gz", hash = "sha256:125840783289e7d12552b1e86ab692c37322e7a65b96a99e0ea86cca041b6779", size = 39504 } + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "numpy" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/fdbf6a7871703df6160b5cf3dd774074b086d278172285c52c2758b76305/numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918", size = 20227662 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/14/645887347124e101d983e1daf95b48dc3e136bf8525cb4257bf9eab1b768/numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484", size = 21217379 }, + { url = "https://files.pythonhosted.org/packages/9f/fd/2279000cf29f58ccfd3778cbf4670dfe3f7ce772df5e198c5abe9e88b7d7/numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7", size = 14388520 }, + { url = "https://files.pythonhosted.org/packages/58/b0/034eb5d5ba12d66ab658ff3455a31f20add0b78df8203c6a7451bd1bee21/numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb", size = 5389286 }, + { url = "https://files.pythonhosted.org/packages/5d/69/6f3cccde92e82e7835fdb475c2bf439761cbf8a1daa7c07338e1e132dfec/numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5", size = 6930345 }, + { url = "https://files.pythonhosted.org/packages/d1/72/1cd38e91ab563e67f584293fcc6aca855c9ae46dba42e6b5ff4600022899/numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73", size = 14335748 }, + { url = "https://files.pythonhosted.org/packages/f2/d4/f999444e86986f3533e7151c272bd8186c55dda554284def18557e013a2a/numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591", size = 16391057 }, + { url = "https://files.pythonhosted.org/packages/99/7b/85cef6a3ae1b19542b7afd97d0b296526b6ef9e3c43ea0c4d9c4404fb2d0/numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8", size = 15556943 }, + { url = "https://files.pythonhosted.org/packages/69/7e/b83cc884c3508e91af78760f6b17ab46ad649831b1fa35acb3eb26d9e6d2/numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0", size = 18180785 }, + { url = "https://files.pythonhosted.org/packages/b2/9f/eb4a9a38867de059dcd4b6e18d47c3867fbd3795d4c9557bb49278f94087/numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd", size = 6568983 }, + { url = "https://files.pythonhosted.org/packages/6d/1e/be3b9f3073da2f8c7fa361fcdc231b548266b0781029fdbaf75eeab997fd/numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16", size = 12917260 }, + { url = "https://files.pythonhosted.org/packages/62/12/b928871c570d4a87ab13d2cc19f8817f17e340d5481621930e76b80ffb7d/numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab", size = 20909861 }, + { url = "https://files.pythonhosted.org/packages/3d/c3/59df91ae1d8ad7c5e03efd63fd785dec62d96b0fe56d1f9ab600b55009af/numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa", size = 14095776 }, + { url = "https://files.pythonhosted.org/packages/af/4e/8ed5868efc8e601fb69419644a280e9c482b75691466b73bfaab7d86922c/numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315", size = 5126239 }, + { url = "https://files.pythonhosted.org/packages/1a/74/dd0bbe650d7bc0014b051f092f2de65e34a8155aabb1287698919d124d7f/numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355", size = 6659296 }, + { url = "https://files.pythonhosted.org/packages/7f/11/4ebd7a3f4a655764dc98481f97bd0a662fb340d1001be6050606be13e162/numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7", size = 14047121 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/c1f1d978166eb6b98ad009503e4d93a8c1962d0eb14a885c352ee0276a54/numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d", size = 16096599 }, + { url = "https://files.pythonhosted.org/packages/3d/6d/0e22afd5fcbb4d8d0091f3f46bf4e8906399c458d4293da23292c0ba5022/numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51", size = 15243932 }, + { url = "https://files.pythonhosted.org/packages/03/39/e4e5832820131ba424092b9610d996b37e5557180f8e2d6aebb05c31ae54/numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046", size = 17861032 }, + { url = "https://files.pythonhosted.org/packages/5f/8a/3794313acbf5e70df2d5c7d2aba8718676f8d054a05abe59e48417fb2981/numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2", size = 6274018 }, + { url = "https://files.pythonhosted.org/packages/17/c1/c31d3637f2641e25c7a19adf2ae822fdaf4ddd198b05d79a92a9ce7cb63e/numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8", size = 12613843 }, + { url = "https://files.pythonhosted.org/packages/20/d6/91a26e671c396e0c10e327b763485ee295f5a5a7a48c553f18417e5a0ed5/numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780", size = 20896464 }, + { url = "https://files.pythonhosted.org/packages/8c/40/5792ccccd91d45e87d9e00033abc4f6ca8a828467b193f711139ff1f1cd9/numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821", size = 14111350 }, + { url = "https://files.pythonhosted.org/packages/c0/2a/fb0a27f846cb857cef0c4c92bef89f133a3a1abb4e16bba1c4dace2e9b49/numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e", size = 5111629 }, + { url = "https://files.pythonhosted.org/packages/eb/e5/8e81bb9d84db88b047baf4e8b681a3e48d6390bc4d4e4453eca428ecbb49/numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348", size = 6645865 }, + { url = "https://files.pythonhosted.org/packages/7a/1a/a90ceb191dd2f9e2897c69dde93ccc2d57dd21ce2acbd7b0333e8eea4e8d/numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59", size = 14043508 }, + { url = "https://files.pythonhosted.org/packages/f1/5a/e572284c86a59dec0871a49cd4e5351e20b9c751399d5f1d79628c0542cb/numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af", size = 16094100 }, + { url = "https://files.pythonhosted.org/packages/0c/2c/a79d24f364788386d85899dd280a94f30b0950be4b4a545f4fa4ed1d4ca7/numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51", size = 15239691 }, + { url = "https://files.pythonhosted.org/packages/cf/79/1e20fd1c9ce5a932111f964b544facc5bb9bde7865f5b42f00b4a6a9192b/numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716", size = 17856571 }, + { url = "https://files.pythonhosted.org/packages/be/5b/cc155e107f75d694f562bdc84a26cc930569f3dfdfbccb3420b626065777/numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e", size = 6270841 }, + { url = "https://files.pythonhosted.org/packages/44/be/0e5cd009d2162e4138d79a5afb3b5d2341f0fe4777ab6e675aa3d4a42e21/numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60", size = 12606618 }, + { url = "https://files.pythonhosted.org/packages/a8/87/04ddf02dd86fb17c7485a5f87b605c4437966d53de1e3745d450343a6f56/numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e", size = 20921004 }, + { url = "https://files.pythonhosted.org/packages/6e/3e/d0e9e32ab14005425d180ef950badf31b862f3839c5b927796648b11f88a/numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712", size = 14119910 }, + { url = "https://files.pythonhosted.org/packages/b5/5b/aa2d1905b04a8fb681e08742bb79a7bddfc160c7ce8e1ff6d5c821be0236/numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008", size = 5153612 }, + { url = "https://files.pythonhosted.org/packages/ce/35/6831808028df0648d9b43c5df7e1051129aa0d562525bacb70019c5f5030/numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84", size = 6668401 }, + { url = "https://files.pythonhosted.org/packages/b1/38/10ef509ad63a5946cc042f98d838daebfe7eaf45b9daaf13df2086b15ff9/numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631", size = 14014198 }, + { url = "https://files.pythonhosted.org/packages/df/f8/c80968ae01df23e249ee0a4487fae55a4c0fe2f838dfe9cc907aa8aea0fa/numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d", size = 16076211 }, + { url = "https://files.pythonhosted.org/packages/09/69/05c169376016a0b614b432967ac46ff14269eaffab80040ec03ae1ae8e2c/numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5", size = 15220266 }, + { url = "https://files.pythonhosted.org/packages/f1/ff/94a4ce67ea909f41cf7ea712aebbe832dc67decad22944a1020bb398a5ee/numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71", size = 17852844 }, + { url = "https://files.pythonhosted.org/packages/46/72/8a5dbce4020dfc595592333ef2fbb0a187d084ca243b67766d29d03e0096/numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2", size = 6326007 }, + { url = "https://files.pythonhosted.org/packages/7b/9c/4fce9cf39dde2562584e4cfd351a0140240f82c0e3569ce25a250f47037d/numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268", size = 12693107 }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[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 } +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 }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "work-manager" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fpdf" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "pathspec" }, + { name = "rich" }, +] + +[package.metadata] +requires-dist = [ + { name = "fpdf", specifier = ">=1.7.2" }, + { name = "openpyxl", specifier = ">=3.1.5" }, + { name = "pandas", specifier = ">=2.2.3" }, + { name = "pathspec", specifier = ">=0.12.1" }, + { name = "rich", specifier = ">=13.9.4" }, +]