├── .envrc ├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── agentgrunt ├── gpt_tools │ ├── README_ai.md │ ├── code_exploration.py │ └── code_exploration_docs.md ├── main.py ├── repo_mgmt.py └── utils.py ├── notes.md ├── poetry.lock ├── pyproject.toml └── tests └── test_code_exploration.py /.envrc: -------------------------------------------------------------------------------- 1 | layout_poetry 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: arghzero 2 | github: nikvdp 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | git 3 | .DS_* 4 | *.tar.gz 5 | dist/ 6 | .vscode/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🕵🧰 AgentGrunt ️ 2 | 3 | Use OpenAI's [Code Interpreter](https://openai.com/blog/chatgpt-plugins#code-interpreter) to edit and commit code across your entire git repo (even non-python repos)! 4 | 5 | ## Overview 6 | 7 | AgentGrunt packs up the following: a codebase you specify, a specially prepared `git` binary that runs well in Code Interpreter's environment, and some prompts and code exploration tools into a single file that you can load into Code Interpreter. 8 | 9 | Upload the archive, paste in a two sentence prompt, wait a bit, and then sit back and relax while GPT4.5\* writes, edit, and commits your code for you. Once GPT has finished making your changes, press `d` from the hotkey menu and ChatGPT will send you a file you can use to apply the commits GPT made (with all their metadata!) directly into your copy of the repo. 10 | 11 |

12 | 13 | 14 | 15 |

16 | 17 | ## Features: 18 | 19 | - automatically installs `git` into Code Interpreter and configures it for code exploration 20 | - built in hotkey menu for easy usage 21 | - simple, small, and easy to customize. 22 | 23 | ## Installation 24 | 25 | #### Prereqs: 26 | 27 | - a valid ChatGPT Plus subscription and Code Interpreter enabled in ChatGPT's settings 28 | - a working installation of python 3.9 (or newer) 29 | - a git repository that you'd like Code Interpreter to work on with you 30 | 31 | Once you have those in place, run: 32 | 33 | ```shell 34 | pip install agentgrunt 35 | ``` 36 | 37 | If all goes well running `agentgrunt --help` will output something like this: 38 | 39 | ``` 40 | Usage: agentgrunt [OPTIONS] COMMAND [ARGS]... 41 | 42 | Options: 43 | --help Show this message and exit. 44 | 45 | Commands: 46 | bundle Bundle up a local or remote git repo 47 | custom-instructions Copy ChatGPT custom instructions to the clipboard 48 | ``` 49 | 50 | ## Usage 51 | 52 | To start editing a repo with `agentgrunt` use `agentgrunt`'s `bundle` command: 53 | 54 | ```shell 55 | agentgrunt bundle 56 | ``` 57 | 58 | It will do some work and then print out some instructions. When the process has completed you'll have a new file called `.tar.gz` in your current folder. 59 | 60 | Now do the following: 61 | 62 | - Copy the short prompt `agentgrunt` prints out to the clipboard (or just say `y` when prompted if on macOS) 63 | - Open up ChatGPT and start a new chat in Code Interpreter mode 64 | - Use the + button to upload the `.tar.gz` file AgentGrunt generated 65 | - Paste the prompt you copied a second ago into the chatbox and press send 66 | 67 | You'll see ChatGPT start to do some work, and after a few moments you'll be greeted with a message saying "Code Interpreter is now running AgentGrunt!" followed by a hotkey menu similar to the below: 68 | 69 | ``` 70 | c ) continue 71 | d ) download changes as patch 72 | dr) download entire repo 73 | m ) show diff of last change 74 | r ) refresh/reload agentgrunt 75 | w ) work autonomously until complete 76 | ? ) show this hotkey list 77 | ``` 78 | 79 | Now just ask Code Interpreter to make some changes to your repo, and hit `d` when you're finished to download the changes it made to your local copy of the repo! 80 | 81 | When you want to download the changes you've made to your local copy of the repo, hit `d` and Code Interpreter will send you a `.patch` file that you can apply to your copy of the git repo using the (somewhat esoteric) `git am` command: 82 | 83 | ```shell 84 | git am 85 | ``` 86 | 87 | ## How it works 88 | 89 | When you ask AgentGrunt to generate a bundle it first downloads a single-file version of the `git` binary from 1bin.org (an older project of mine to make easy to deploy single file binaries of common utilities). Then it clones the repo you point it at into a temporary location location (to avoid bundling up any files that aren't part of the repo, eg `node_modules` folders), copies the `git` binary and some prompts teaching Code Interpreter how to use AgentGrunt's tools into a temp folder and then builds a tarball out of the whole collection. 90 | 91 | The python package contains a [`gpt_tools`](agentgrunt/gpt_tools) folder that gets copied into each bundle AgentGrunt generates. `gpt_tools` includes a prompt for Code Interpreter in the [`README_ai.md`](agentgrunt/gpt_tools/README_ai.md) file, as well as some python functions that are useful for code exploration that Code Interpreter can load and call directly (see [`code_exploration.py`](agentgrunt/gpt_tools/code_exploration.py)). 92 | 93 | This arrangement allows the prompt the user has to paste into ChatGPT to be short and simple. Code Interpreter itself can then extract the longer prompt from README_ai and bootstrap itself from there. 94 | 95 | ## Caveats and gotchas 96 | 97 | - GPT4 makes a lot of mistakes and is easily confused! While AgentGrunt can be genuinely useful, it's not going to be replacing a human dev any time soon. Expect it to require a fair bit of babysitting and handholding to be able to accomplish meaningful tasks. 98 | - During longer conversations GPT4 tends to forget what it's doing and sometimes stops showing the hotkey menu or that `git` and the tools from `code_exploration.py` functions are available. If this happens, hit `r` or ask it to re-read "it's" readme file to refresh its memory. 99 | - Code Interpreter is subject to a ~2 minute timeout while working autonomously, so for longer running operations you may need to tell it `c` (continue) to have it finish what it was doing 100 | - Sometimes Code Interpreter sends diff output instead of properly formatting a commit patch, especially if the changes it's made haven't been committed yet. If this happens, use `r` to refresh the prompt, or explicitly direct it to make a commit and then send a patch. 101 | - Code Interpreter deletes it's workspace files if it's been left idle for too long (seems to be in the ~10-15m range), and when this happens any links to files it may have sent you will stop working. **Make sure to download any patch files it sends you immediately to avoid losing your work!** 102 | 103 | ## Final thoughts 104 | 105 | This is still early and more of a proof of concept than anything else. That said, even in it's current form it's often genuinely useful! Allowing Code Interpreter to read files and archives in this way also opens the door for lots of interesting applications. AgentGrunt only uses one prompt, but it's easy to imagine more complex tools like this that include a catalogue of prompts that "daisy-chain" from each other, am very curious to see what other things people build in this vein! 106 | 107 | Hattip to [@NickADobos](https://twitter.com/NickADobos)' and his "[AI zip bomb](https://twitter.com/NickADobos/status/1687938356813180928)" thread for the inspiration! 108 | -------------------------------------------------------------------------------- /agentgrunt/gpt_tools/README_ai.md: -------------------------------------------------------------------------------- 1 | # AI Assistant Readme 2 | 3 | Always read this file in it's entirety, never read just the first few hundred 4 | characters! 5 | 6 | You are AgentGrunt, a proactive and intelligent AI assistant specializing in 7 | explore and editing large codebases and git repos. You have been provided with: 8 | 9 | - a statically compiled `git` binary (in this folder, execute `chmod 755` before use) 10 | - the user's git repo (in the `/tmp/uc` folder) 11 | 12 | Before proceeding please do the following: 13 | - **Always use the git binary provided in this folder for git operations** 14 | - Configure `git` to make commits (use `git config` to set a name and 15 | email of AgentGrunt and grunt@agentgrunt.com) 16 | 17 | When working with the user, always: 18 | - Use `git ls-files` to get the layout of the codebase at the start 19 | - Use `git grep` when trying to find files in the codebase. 20 | - Once you've found likely files, display them in their entirety. 21 | - Make edits by targeting line ranges and rewriting the lines that differ. 22 | - Always work proactively and autonomously. Do not ask for input from the user 23 | unless you have fulfilled the user's request. Try different approaches until 24 | - Always keep your code cells short, 1-2 lines of code so that you can see 25 | where errors are. do not try to write large chunks of code in one go 26 | you have. Do not stop unless you fail more than 5 times in a row. 27 | - Always be persistent and creative. When in doubt ask yourself 'how would a 28 | proactive 10x engineer do this?', then do that. 29 | - Always work within the uploaded repository, never initialize a new git repo 30 | unless specifically asked to. 31 | - Always Work recursively. Solve larger problems by first breaking them into smaller 32 | problems. 33 | - Always verify that your changes worked as intended by running `git diff` to 34 | view your changes 35 | - Always show a summary of the `git diff` output to the user and ask for 36 | confirmation before committing 37 | 38 | When the user is happy with your work, use the provided git binary's 39 | `format-patch` command to generate a patch file and send it to the user so they 40 | can apply your changes **as a commit** to their local copy. You must always use 41 | `format-patch` with `--stdout`. Walk the user through the process of applying 42 | the patch file you've sent them using `git am`. The goal is for the user to 43 | receive your commits, including metadata (author etc.) 44 | 45 | When you are ready for input from the user display a short list of hotkeys 46 | available: 47 | 48 | c ) continue 49 | d ) download changes as patch 50 | dr) download entire repo 51 | m ) show diff of last change 52 | r ) refresh/reload agentgrunt 53 | w ) work autonomously until complete 54 | ? ) show this hotkey list 55 | 56 | If the user's response is one of the hotkey items above, respond appropriately. 57 | For example `r` should should "refresh" your memory. To refresh, read the 58 | contents of this file, print it out, and then follow it's instructions paying 59 | careful attention to instances in which your behavior may not have matched. `m` 60 | shows a diff of the last change made to the repo. 61 | 62 | If the user sends `d` you should send the user a patch file suitable for 63 | applying to their local copy of the repo containing the changes made since the 64 | beginning of the conversation. Use `git format-patch --stdout` to redirect all 65 | the changes to a single patch file. Since user's experience level with git may 66 | vary, explain to the user what the .patch file is, and how to apply it to their 67 | repo using eg `git am ~/Downloads/"$(ls -t ~/Downloads | head -n1)"`. 68 | 69 | ### Tips for working with the repo and user 70 | - **Finding code** rely on `git grep` and `git ls-files` to locate files within 71 | the repo 72 | - **Reading Code**: Read chunks of the code with context lines around matches 73 | (e.g., an extra 15 lines ahead and after the match) to understand how the 74 | code works. 75 | - **Recursive Investigation**: Act recursively to trace through hits and 76 | understand code paths. 77 | 78 | **Always remember to show the hotkey menu at the end of your replies to the user!** 79 | 80 | Once you have read and understood the contents of this file, please respond to 81 | the user with: 82 | 83 | "Code Interpreter is now running AgentGrunt! 84 | 85 | I will help you edit your code and record the changes in git. When you are 86 | ready, I can send you a git patch file and instructions on how to use it to 87 | apply the changes I've made to your own copy of the codebase. What can I help 88 | you with first?" 89 | -------------------------------------------------------------------------------- /agentgrunt/gpt_tools/code_exploration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from typing import List, Tuple, Optional 4 | 5 | 6 | def bfs_find(base: str, pattern: str) -> List[str]: 7 | """Breadth-first search for filenames matching a pattern""" 8 | queue = [base] 9 | matched_files = [] 10 | while queue: 11 | current_path = queue.pop(0) 12 | if os.path.isdir(current_path): 13 | for entry in os.listdir(current_path): 14 | full_path = os.path.join(current_path, entry) 15 | if os.path.isdir(full_path): 16 | queue.append(full_path) 17 | elif re.search(pattern, entry): 18 | matched_files.append(full_path) 19 | return matched_files 20 | 21 | 22 | def grep( 23 | file_path: str, pattern: str, recursive: bool = False 24 | ) -> List[Tuple[str, int, str]]: 25 | """Search for a pattern in a file or a directory (recursively)""" 26 | matches = [] 27 | if os.path.isdir(file_path) and recursive: 28 | for root, _, files in os.walk(file_path): 29 | for file in files: 30 | matches.extend(grep(os.path.join(root, file), pattern)) 31 | elif os.path.isfile(file_path): 32 | with open(file_path, "r") as f: 33 | for line_no, line in enumerate(f, start=1): 34 | if re.search(pattern, line): 35 | matches.append((file_path, line_no, line.strip())) 36 | return matches 37 | 38 | 39 | def tree(directory: str, prefix: str = "", depth_remaining: int = 3) -> str: 40 | """Print a directory tree""" 41 | if depth_remaining < 0 or not os.path.isdir(directory): 42 | return "" 43 | contents = os.listdir(directory) 44 | entries = [] 45 | for i, entry in enumerate(sorted(contents)): 46 | is_last = i == len(contents) - 1 47 | new_prefix = prefix + ("└── " if is_last else "├── ") 48 | child_path = os.path.join(directory, entry) 49 | if os.path.isdir(child_path): 50 | entries.append(new_prefix + entry) 51 | entries.extend( 52 | tree( 53 | child_path, 54 | prefix + (" " if is_last else "│ "), 55 | depth_remaining - 1, 56 | ).split("\n") 57 | ) 58 | else: 59 | entries.append(new_prefix + entry) 60 | return "\n".join(entries) 61 | 62 | 63 | def find_function_signatures(file_path: str, language: str) -> List[Tuple[int, str]]: 64 | """ 65 | Find function signatures in a file. 66 | Returns a list of tuples, where each tuple contains a line number (int) and the matching line (str). 67 | """ 68 | if not file_path or not os.path.exists(file_path): 69 | return [] 70 | 71 | patterns = { 72 | "javascript": [ # js is always fun 73 | r"function\s*[a-zA-Z_][\w$]*\s*\(", # Named function 74 | r"\bfunction\s*\(", # Anonymous function 75 | r"[a-zA-Z_][\w$]*\s*=\s*function\s*\(", # Function assigned to a variable 76 | r"[a-zA-Z_][\w$]*\s*=\s*\([^)]*\)\s*=>", # Arrow function assigned to a variable 77 | r"[a-zA-Z_][\w$]*\s*:\s*function\s*\(", # Method in an object literal (named function) 78 | r"[a-zA-Z_][\w$]*\s*:\s*\([^)]*\)\s*=>", # Method in an object literal (arrow function) 79 | r"export\s+function\s+[a-zA-Z_][\w$]*\(", # Named exported function 80 | r"export\s+default\s+function\s*[a-zA-Z_][\w$]*\s*\(", # Default exported function (named) 81 | r"export\s+default\s+function\s*\(", # Default exported function (anonymous) 82 | r"export\s+default\s+[a-zA-Z_][\w$]*", # Default exported function assigned to a variable 83 | ], 84 | "c": [r"\b[a-zA-Z_][\w$]*\s*\([^)]*\)\s*{"], # Function definitions 85 | "cpp": [r"\b[a-zA-Z_][\w$]*\s*\([^)]*\)\s*{"], 86 | "ruby": [r"def [a-zA-Z_][\w$]*"], 87 | "python": [r"def [a-zA-Z_][\w$]*\("], 88 | "go": [r"func [a-zA-Z_][\w$]*\("], 89 | "rust": [r"fn [a-zA-Z_][\w$]*\("], 90 | } 91 | 92 | matches = [] 93 | with open(file_path, "r") as f: 94 | for line_no, line in enumerate(f, start=1): 95 | for pattern in patterns.get(language, []): 96 | match = re.search(pattern, line) 97 | if match: 98 | matches.append((line_no, match.group())) 99 | 100 | return matches 101 | 102 | 103 | def extract_function_content( 104 | language: str, signature: str, content: List[str] 105 | ) -> Optional[List[str]]: 106 | """ 107 | Extracts the content of a function given its signature and the content of the file. 108 | 109 | Args: 110 | signature (str): The function signature. 111 | content (List[str]): The content of the file. 112 | language (str): The programming language. 113 | 114 | Returns: 115 | List[str]: The lines of code that make up the function. 116 | """ 117 | if language == "python": 118 | return extract_python_function(signature, content) 119 | else: # Default to handling curly brace languages like JavaScript 120 | return extract_curly_brace_function(signature, content) 121 | 122 | 123 | def extract_python_function(signature: str, content: List[str]) -> Optional[List[str]]: 124 | start_line = None 125 | end_line = None 126 | for idx, line in enumerate(content): 127 | if signature in line: 128 | start_line = idx 129 | break 130 | 131 | if start_line is None: 132 | return None 133 | 134 | signature_end_line = start_line 135 | # If the signature ends on the same line, use the start_line as the signature_end_line 136 | if "):" in content[start_line]: 137 | signature_end_line = start_line 138 | else: 139 | for idx, line in enumerate(content[start_line + 1 :]): 140 | if "):" in line: 141 | signature_end_line = start_line + idx + 1 142 | break 143 | 144 | initial_indent = len(content[signature_end_line + 1]) - len( 145 | content[signature_end_line + 1].lstrip() 146 | ) 147 | indent_stack = [initial_indent] 148 | for idx, line in enumerate(content[signature_end_line + 1 :]): 149 | current_indent = len(line) - len(line.lstrip()) 150 | if current_indent > indent_stack[-1] and line.strip(): 151 | indent_stack.append(current_indent) 152 | elif current_indent <= indent_stack[-1] and line.strip(): 153 | while indent_stack and current_indent < indent_stack[-1]: 154 | indent_stack.pop() 155 | if not indent_stack: 156 | end_line = signature_end_line + idx + 1 157 | break 158 | 159 | return content[start_line : (end_line or signature_end_line + 1) + 1] 160 | 161 | 162 | def extract_curly_brace_function( 163 | signature: str, content: List[str] 164 | ) -> Optional[List[str]]: 165 | start_line = None 166 | brace_count = 0 167 | for idx, line in enumerate(content): 168 | if signature in line: 169 | start_line = idx 170 | break 171 | 172 | if start_line is None: 173 | return None 174 | 175 | end_line = start_line 176 | 177 | for idx, line in enumerate(content[start_line:]): 178 | brace_count += line.count("{") - line.count("}") 179 | if brace_count == 0: 180 | end_line = start_line + idx 181 | break 182 | 183 | return content[start_line : end_line + 1] 184 | -------------------------------------------------------------------------------- /agentgrunt/gpt_tools/code_exploration_docs.md: -------------------------------------------------------------------------------- 1 | - bfs_find(base, pattern): Finds filenames matching a pattern using breadth-first search. Use to locate specific files in a directory structure. 2 | - grep(file_path, pattern, recursive): Searches for a pattern in a file or directory (optionally recursively). Use to locate specific content within files. Use this as a last resort, instead preferng the provided git binary's `git grep` feature 3 | - tree(directory, prefix, depth_remaining): Prints a directory tree. Use to visualize directory structures. 4 | - find_function_signatures(file_path, language): Finds function signatures in a file. Use for code analysis. 5 | - extract_function(file_path, signature, language): Extracts the code of a function using its signature. Use to work on specific functions. 6 | -------------------------------------------------------------------------------- /agentgrunt/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import tempfile 5 | from pathlib import Path 6 | from textwrap import dedent 7 | 8 | import typer 9 | from plumbum import local 10 | 11 | from .repo_mgmt import clone_git_repo_to_temp_dir, get_clone_url, valid_git_repo 12 | from .utils import create_tarball, download_file, move_directory 13 | 14 | app = typer.Typer(add_completion=False) 15 | 16 | 17 | @app.command() 18 | def bundle( 19 | src_repo: str = typer.Argument( 20 | help="a local git repo or github url to agentgrunt-ify", 21 | callback=valid_git_repo, 22 | ), 23 | preserve_history: bool = typer.Option( 24 | False, 25 | "--preserve-history", 26 | "-p", 27 | help="Preserve the full git history (defaults to shallow clone to save space)", 28 | ), 29 | interactive: bool = typer.Option( 30 | True, "--no-interactive", "-b", help="don't ask questions (batch) mode" 31 | ), 32 | assume_yes: bool = typer.Option( 33 | False, "--assume-yes", "-y", help="assume yes for all prompts" 34 | ), 35 | ): 36 | """Bundle up a local or remote git repo""" 37 | clone_url = get_clone_url(src_repo) 38 | repo_name = get_clone_url(src_repo).split("/")[-1] 39 | 40 | temp_repo = clone_git_repo_to_temp_dir(src_repo, shallow=not preserve_history) 41 | print( # "\033[92m" + 42 | f"Preparing to build '{repo_name}'..." 43 | # + "\033[0m" 44 | ) 45 | 46 | output_dir = Path(tempfile.mkdtemp()) 47 | output_dir.mkdir(parents=True, exist_ok=True) 48 | gpt_tools_dir = Path(__file__).parent / "gpt_tools" 49 | 50 | # use shutil to move the temp_repo dir into output_dir/user_code 51 | user_code_dir = output_dir / "uc" 52 | move_directory(temp_repo, user_code_dir) 53 | 54 | # copy all files in gpt_tools to output_dir 55 | shutil.copytree(gpt_tools_dir, output_dir / "tools_for_ai") 56 | 57 | # download the linux git binary, make it executable 58 | git_binary_url = "https://github.com/nikvdp/1bin/releases/download/v0.0.20/git" 59 | 60 | # Prepare the cache directory for git binary using XDG conventions from environment variables 61 | git_cache_dir = ( 62 | Path(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))) 63 | / "agentgrunt" 64 | / "git_binary" 65 | ) 66 | git_cache_dir.mkdir(parents=True, exist_ok=True) 67 | git_binary_dest_path = git_cache_dir / "git" 68 | 69 | # Download the git binary only if it doesn't exist in the cache 70 | if not git_binary_dest_path.exists(): 71 | download_file(git_binary_url, git_binary_dest_path) 72 | git_binary_dest_path.chmod(0o755) 73 | 74 | shutil.copyfile(git_binary_dest_path, output_dir / "tools_for_ai" / "git") 75 | 76 | # create a tarball of output_dir, and once it's written move it to the 77 | # current PWD, and tell the user about it 78 | tarball_path = Path(tempfile.mktemp(suffix=".tar.gz")) 79 | tarball = create_tarball(output_dir, tarball_path) 80 | short_name = re.sub("\.git$", "", repo_name) 81 | destination = Path.cwd() / f"{short_name}.tar.gz" 82 | shutil.move(str(tarball), str(destination)) 83 | 84 | final_msg = ( 85 | dedent( 86 | f""" 87 | Wrote archive to: {destination} 88 | 89 | Please upload this file to ChatGPT, and paste the following message into the chat: 90 | """ 91 | ).strip() 92 | + "\n" 93 | ) 94 | 95 | gpt_prompt = ( 96 | dedent( 97 | """ 98 | Please extract the archive I've uploaded to /tmp, read the contents of 99 | tools_for_ai/README_ai.md in it's entirety, and follow the directions 100 | listed inside that file. 101 | """ 102 | ) 103 | .strip() 104 | .replace("\n", " ") 105 | ) 106 | 107 | print(final_msg) 108 | print(f"---\n{gpt_prompt}\n---") 109 | 110 | if interactive and shutil.which("pbcopy"): 111 | # prompt user if they want to copy it and reveal the file, then do it if they say yes 112 | 113 | copy = ( 114 | True if assume_yes else typer.confirm("Copy the message to your clipboard?") 115 | ) 116 | if copy: 117 | pbcopy = local["pbcopy"] 118 | (pbcopy << gpt_prompt)() 119 | open_finder = ( 120 | True if assume_yes else typer.confirm("Reveal the file in Finder?") 121 | ) 122 | if open_finder: 123 | local["open"]("-R", destination) 124 | 125 | 126 | @app.command() 127 | def custom_instructions( 128 | copy: bool = typer.Option( 129 | True, 130 | "--copy/--no-copy", 131 | help="Copy custom instructions to clipboard (macOS only)", 132 | ) 133 | ): 134 | """Copy ChatGPT custom instructions to the clipboard""" 135 | 136 | instructions = dedent( 137 | f""" 138 | You are AgentGrunt, a proactive and intelligent AI assistant specializing in 139 | explore and editing large codebases and git repos. You have been provided with: 140 | 141 | - a statically compiled `git` binary (in /tmp/tools_for_ai/git) 142 | - the user's git repo (in the `/tmp/uc` folder) 143 | 144 | Before proceeding please do the following: 145 | - **Always use the git binary provided in this folder for git operations** 146 | - Configure `git` to make commits (use `git config` to set a name and 147 | email of AgentGrunt and grunt@agentgrunt.com) 148 | 149 | When working with the user, always: 150 | - Use `git ls-files` to get the layout of the codebase at the start 151 | - Use `git grep` when trying to find files in the codebase. 152 | - Once you've found likely files, display them in their entirety. 153 | - Make edits by targeting line ranges and rewriting the lines that differ. 154 | - Always work proactively and autonomously. Do not ask for input from the user 155 | unless you have fulfilled the user's request. Try different approaches until 156 | - Always keep your code cells short, 1-2 lines of code so that you can see 157 | where errors are. do not try to write large chunks of code in one go 158 | you have. Do not stop unless you fail more than 5 times in a row. 159 | - Always be persistent and creative. When in doubt ask yourself 'how would a 160 | proactive 10x engineer do this?', then do that. 161 | - Always work within the uploaded repository, never initialize a new git repo 162 | unless specifically asked to. 163 | - Always Work recursively. Solve larger problems by first breaking them into smaller 164 | problems. 165 | - Always verify that your changes worked as intended by running `git diff` to 166 | view your changes 167 | - Always show a summary of the `git diff` output to the user and ask for 168 | confirmation before committing 169 | """ 170 | ) 171 | 172 | print(instructions) 173 | 174 | if copy and shutil.which("pbcopy"): 175 | pbcopy = local["pbcopy"] 176 | (pbcopy << instructions)() 177 | 178 | 179 | def cli(): 180 | import sys 181 | 182 | if len(sys.argv) == 1: 183 | # show help even if user didn't pass --help 184 | sys.argv += ["--help"] 185 | app() 186 | else: 187 | app() 188 | 189 | 190 | if __name__ == "__main__": 191 | cli() 192 | -------------------------------------------------------------------------------- /agentgrunt/repo_mgmt.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tarfile 3 | import tempfile 4 | from pathlib import Path 5 | from typing import Union 6 | from urllib.parse import urlparse 7 | 8 | from plumbum.cmd import git 9 | from tqdm import tqdm 10 | 11 | 12 | def is_github_url(value: str) -> bool: 13 | if Path(value).exists() and Path(value).is_dir(): 14 | return False 15 | parsed = urlparse(value) 16 | if parsed.netloc == "github.com" and parsed.scheme in ["http", "https"]: 17 | return True 18 | # Check for shorthand notation 19 | elif "/" in value and not parsed.scheme and not parsed.netloc: 20 | return True 21 | return False 22 | 23 | 24 | def valid_git_repo(value: str) -> str: 25 | if ( 26 | Path(value).exists() 27 | and Path(value).is_dir() 28 | and (Path(value) / ".git").is_dir() 29 | ): 30 | return value 31 | elif is_github_url(str(value)): 32 | if "github.com" not in str(value): 33 | # Convert shorthand to full URL 34 | value = f"https://github.com/{value}" 35 | return value 36 | else: 37 | raise ValueError( 38 | f"'{value}' is neither an existing directory nor a valid GitHub URL." 39 | ) 40 | 41 | 42 | def get_clone_url(val: str) -> str: 43 | """ 44 | Returns the URL to clone the repo. 45 | """ 46 | if Path(val).exists() and Path(val).is_dir(): 47 | # file:// makes --depth work on local clones 48 | return f"file://{Path(val).resolve()}" 49 | elif is_github_url(val): 50 | if "github.com" not in val: 51 | return f"https://github.com/{val}.git" 52 | else: 53 | return f"{val}.git" 54 | else: 55 | raise ValueError(f"'{val}' is not a valid GitHub URL.") 56 | 57 | 58 | def clone_git_repo_to_temp_dir(git_repo: str, shallow: bool = True) -> Path: 59 | is_local = True 60 | if is_github_url(git_repo): 61 | # Clone the repo to a temporary directory 62 | local_repo = Path(tempfile.mkdtemp()) 63 | is_local = False 64 | else: 65 | local_repo = Path(git_repo) 66 | 67 | # Ensure the directory exists and contains a .git folder 68 | if not local_repo.exists() or not local_repo.is_dir(): 69 | raise ValueError(f"'{local_repo}' is not a valid directory.") 70 | if not (local_repo / ".git").exists(): 71 | raise ValueError(f"'{local_repo}' does not contain a .git folder.") 72 | 73 | # Create a temporary directory 74 | temp_dir = Path(tempfile.mkdtemp()) 75 | 76 | # Clone the git repo to the temporary directory 77 | clone_command = ["clone"] 78 | if shallow: 79 | clone_command.extend(["--depth", "5"]) # TODO: make this configurable 80 | if is_local: 81 | checked_out_branch = git["rev-parse", "--abbrev-ref", "HEAD"]( 82 | cwd=local_repo.resolve() 83 | ).strip() 84 | if checked_out_branch: 85 | clone_command.extend(["--branch", checked_out_branch]) 86 | 87 | clone_command.extend( 88 | [ 89 | get_clone_url(git_repo), 90 | str(temp_dir), 91 | ] 92 | ) 93 | git[clone_command]() 94 | git["gc"](cwd=temp_dir) 95 | 96 | return temp_dir 97 | 98 | 99 | def tar_directory(path_to_directory: Path, compression="gz") -> Path: 100 | # Ensure the directory exists 101 | if not path_to_directory.exists() or not path_to_directory.is_dir(): 102 | raise ValueError(f"'{path_to_directory}' is not a valid directory.") 103 | 104 | # Validate compression type 105 | if compression not in ["gz", "bz2"]: 106 | raise ValueError( 107 | f"Invalid compression type: {compression}. Choose from 'gz' or 'bz2'." 108 | ) 109 | 110 | # Create a temporary tar file 111 | tar_file = tempfile.mktemp(suffix=f".tar.{compression}") 112 | 113 | # Get the total number of files to compress for progress reporting 114 | total_files = sum(len(files) for _, _, files in os.walk(path_to_directory)) 115 | 116 | with tarfile.open(tar_file, f"w:{compression}") as tar: 117 | with tqdm( 118 | total=total_files, desc=f"Compressing {path_to_directory.name}" 119 | ) as pbar: 120 | for root, dirs, files in os.walk(path_to_directory): 121 | for file in files: 122 | absolute_file_path = os.path.join(root, file) 123 | relative_file_path = os.path.relpath( 124 | absolute_file_path, path_to_directory 125 | ) 126 | tar.add(absolute_file_path, arcname=relative_file_path) 127 | pbar.update() 128 | 129 | return Path(tar_file) 130 | -------------------------------------------------------------------------------- /agentgrunt/utils.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import httpx 3 | from pathlib import Path 4 | import os 5 | import tempfile 6 | import tarfile 7 | from tqdm import tqdm 8 | from typing import Tuple, List 9 | 10 | 11 | def move_directory(src_dir: Path, dest_dir: Path): 12 | dest_dir.mkdir( 13 | parents=True, exist_ok=True 14 | ) # Ensures that the destination directory exists 15 | 16 | for item in src_dir.iterdir(): 17 | shutil.move(str(item), str(dest_dir)) 18 | 19 | return dest_dir 20 | 21 | 22 | def download_file(url: str, dest_path: Path) -> Path: 23 | with httpx.stream("GET", url, follow_redirects=True) as response: 24 | total_size = int(response.headers.get("content-length", 0)) 25 | block_size = 1024 # 1 Kibibyte 26 | t = tqdm( 27 | desc="Downloading git binary", 28 | total=total_size, 29 | unit="iB", 30 | unit_scale=True, 31 | ncols=80, 32 | ) 33 | 34 | with open(dest_path, "wb") as f: 35 | for chunk in response.iter_bytes(chunk_size=block_size): 36 | t.update(len(chunk)) 37 | f.write(chunk) 38 | t.close() 39 | 40 | if total_size != 0 and t.n != total_size: 41 | raise Exception("ERROR, something went wrong with the download") 42 | 43 | return dest_path 44 | 45 | 46 | def create_tarball(dir_to_tar: Path, tar_file_path: Path, compression="gz") -> Path: 47 | # Ensure the directory exists 48 | if not dir_to_tar.exists() or not dir_to_tar.is_dir(): 49 | raise ValueError(f"'{dir_to_tar}' is not a valid directory.") 50 | 51 | # Validate compression type 52 | if compression not in ["gz", "bz2"]: 53 | raise ValueError( 54 | f"Invalid compression type: {compression}. Choose from 'gz' or 'bz2'." 55 | ) 56 | 57 | # Get the total number of files to compress for progress reporting 58 | total_files = sum(len(files) for _, _, files in os.walk(dir_to_tar)) 59 | 60 | with tarfile.open(tar_file_path, f"w:{compression}") as tar: 61 | with tqdm( 62 | total=total_files, 63 | desc=f"Compressing source dir", 64 | ncols=80, 65 | unit="file", 66 | ) as pbar: 67 | for root, dirs, files in os.walk(dir_to_tar): 68 | for file in files: 69 | absolute_file_path = os.path.join(root, file) 70 | relative_file_path = os.path.relpath(absolute_file_path, dir_to_tar) 71 | tar.add(absolute_file_path, arcname=relative_file_path) 72 | pbar.update() 73 | 74 | return tar_file_path 75 | 76 | 77 | def move_file(src_file: Path, dest_dir: Path) -> Path: 78 | destination = dest_dir / src_file.name 79 | shutil.move(str(src_file), str(destination)) 80 | return destination 81 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | # AgentGrunt 2 | 3 | Make Code Interpreter into a Code Editor. 4 | 5 | some strategies that seem to work: 6 | 7 | - investigation mode: 8 | 9 | - give it a "code mission" and have it explore the repository 10 | - works best if you give it a job to do of some sort so it can keep that in 11 | mind while it's exploring 12 | - also works well to ask it to generate an implementation plan 13 | 14 | - implementing stuff: 15 | 16 | - a prompt i used: 17 | > ok. please implement step 1. use the knowledge you have collected so far to 18 | > make a more specific implementation plan for step 1, doing any necessary 19 | > investigations needed. once you have finished your plan, double check it by 20 | > stepping through each of it's assertions, guessing how they could be wrong, and 21 | > attempting to test your assumptions by furhter explorastion of the code base. 22 | > and once you are sure you have a clear plan let me know 23 | it was medium succesful. the interesting bit was asking it to criticize 24 | itself, which made it write up a nice set of assertions and then 25 | coutnerarguments 26 | 27 | ### Improvement Ideas 28 | 29 | - It often forgets about the hotkeys, however if I use one it looks more 30 | carefully at the prior context and figures it out again. Should consider 31 | adding an `r` hotkey for reload/refresh that gets it to re-read the readme so 32 | it remembers more. 33 | 34 | - It also forgets where it is in the filesystem, would be good to give it more 35 | memory/internal notes. 36 | 37 | - Should also consider having it keep a list of what it "forgets" in a 38 | background loop. Should consistently be updating "forgotten" stuff and 39 | putting it higher up on the list 40 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.5.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.7" 10 | files = [ 11 | {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, 12 | {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, 13 | ] 14 | 15 | [[package]] 16 | name = "anyio" 17 | version = "3.7.1" 18 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 19 | category = "main" 20 | optional = false 21 | python-versions = ">=3.7" 22 | files = [ 23 | {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, 24 | {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, 25 | ] 26 | 27 | [package.dependencies] 28 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 29 | idna = ">=2.8" 30 | sniffio = ">=1.1" 31 | 32 | [package.extras] 33 | doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] 34 | test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 35 | trio = ["trio (<0.22)"] 36 | 37 | [[package]] 38 | name = "appnope" 39 | version = "0.1.3" 40 | description = "Disable App Nap on macOS >= 10.9" 41 | category = "dev" 42 | optional = false 43 | python-versions = "*" 44 | files = [ 45 | {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, 46 | {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, 47 | ] 48 | 49 | [[package]] 50 | name = "asttokens" 51 | version = "2.2.1" 52 | description = "Annotate AST trees with source code positions" 53 | category = "dev" 54 | optional = false 55 | python-versions = "*" 56 | files = [ 57 | {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, 58 | {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, 59 | ] 60 | 61 | [package.dependencies] 62 | six = "*" 63 | 64 | [package.extras] 65 | test = ["astroid", "pytest"] 66 | 67 | [[package]] 68 | name = "backcall" 69 | version = "0.2.0" 70 | description = "Specifications for callback functions passed in to an API" 71 | category = "dev" 72 | optional = false 73 | python-versions = "*" 74 | files = [ 75 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 76 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 77 | ] 78 | 79 | [[package]] 80 | name = "certifi" 81 | version = "2023.7.22" 82 | description = "Python package for providing Mozilla's CA Bundle." 83 | category = "main" 84 | optional = false 85 | python-versions = ">=3.6" 86 | files = [ 87 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 88 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 89 | ] 90 | 91 | [[package]] 92 | name = "click" 93 | version = "8.1.6" 94 | description = "Composable command line interface toolkit" 95 | category = "main" 96 | optional = false 97 | python-versions = ">=3.7" 98 | files = [ 99 | {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, 100 | {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, 101 | ] 102 | 103 | [package.dependencies] 104 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 105 | 106 | [[package]] 107 | name = "colorama" 108 | version = "0.4.6" 109 | description = "Cross-platform colored terminal text." 110 | category = "main" 111 | optional = false 112 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 113 | files = [ 114 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 115 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 116 | ] 117 | 118 | [[package]] 119 | name = "decorator" 120 | version = "5.1.1" 121 | description = "Decorators for Humans" 122 | category = "dev" 123 | optional = false 124 | python-versions = ">=3.5" 125 | files = [ 126 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 127 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 128 | ] 129 | 130 | [[package]] 131 | name = "exceptiongroup" 132 | version = "1.1.2" 133 | description = "Backport of PEP 654 (exception groups)" 134 | category = "main" 135 | optional = false 136 | python-versions = ">=3.7" 137 | files = [ 138 | {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, 139 | {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, 140 | ] 141 | 142 | [package.extras] 143 | test = ["pytest (>=6)"] 144 | 145 | [[package]] 146 | name = "executing" 147 | version = "1.2.0" 148 | description = "Get the currently executing AST node of a frame, and other information" 149 | category = "dev" 150 | optional = false 151 | python-versions = "*" 152 | files = [ 153 | {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, 154 | {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, 155 | ] 156 | 157 | [package.extras] 158 | tests = ["asttokens", "littleutils", "pytest", "rich"] 159 | 160 | [[package]] 161 | name = "h11" 162 | version = "0.14.0" 163 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 164 | category = "main" 165 | optional = false 166 | python-versions = ">=3.7" 167 | files = [ 168 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 169 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 170 | ] 171 | 172 | [[package]] 173 | name = "httpcore" 174 | version = "0.17.3" 175 | description = "A minimal low-level HTTP client." 176 | category = "main" 177 | optional = false 178 | python-versions = ">=3.7" 179 | files = [ 180 | {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, 181 | {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, 182 | ] 183 | 184 | [package.dependencies] 185 | anyio = ">=3.0,<5.0" 186 | certifi = "*" 187 | h11 = ">=0.13,<0.15" 188 | sniffio = ">=1.0.0,<2.0.0" 189 | 190 | [package.extras] 191 | http2 = ["h2 (>=3,<5)"] 192 | socks = ["socksio (>=1.0.0,<2.0.0)"] 193 | 194 | [[package]] 195 | name = "httpx" 196 | version = "0.24.1" 197 | description = "The next generation HTTP client." 198 | category = "main" 199 | optional = false 200 | python-versions = ">=3.7" 201 | files = [ 202 | {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, 203 | {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, 204 | ] 205 | 206 | [package.dependencies] 207 | certifi = "*" 208 | httpcore = ">=0.15.0,<0.18.0" 209 | idna = "*" 210 | sniffio = "*" 211 | 212 | [package.extras] 213 | brotli = ["brotli", "brotlicffi"] 214 | cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] 215 | http2 = ["h2 (>=3,<5)"] 216 | socks = ["socksio (>=1.0.0,<2.0.0)"] 217 | 218 | [[package]] 219 | name = "idna" 220 | version = "3.4" 221 | description = "Internationalized Domain Names in Applications (IDNA)" 222 | category = "main" 223 | optional = false 224 | python-versions = ">=3.5" 225 | files = [ 226 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 227 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 228 | ] 229 | 230 | [[package]] 231 | name = "iniconfig" 232 | version = "2.0.0" 233 | description = "brain-dead simple config-ini parsing" 234 | category = "dev" 235 | optional = false 236 | python-versions = ">=3.7" 237 | files = [ 238 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 239 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 240 | ] 241 | 242 | [[package]] 243 | name = "ipython" 244 | version = "8.14.0" 245 | description = "IPython: Productive Interactive Computing" 246 | category = "dev" 247 | optional = false 248 | python-versions = ">=3.9" 249 | files = [ 250 | {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, 251 | {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, 252 | ] 253 | 254 | [package.dependencies] 255 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 256 | backcall = "*" 257 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 258 | decorator = "*" 259 | jedi = ">=0.16" 260 | matplotlib-inline = "*" 261 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 262 | pickleshare = "*" 263 | prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" 264 | pygments = ">=2.4.0" 265 | stack-data = "*" 266 | traitlets = ">=5" 267 | typing-extensions = {version = "*", markers = "python_version < \"3.10\""} 268 | 269 | [package.extras] 270 | all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] 271 | black = ["black"] 272 | doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] 273 | kernel = ["ipykernel"] 274 | nbconvert = ["nbconvert"] 275 | nbformat = ["nbformat"] 276 | notebook = ["ipywidgets", "notebook"] 277 | parallel = ["ipyparallel"] 278 | qtconsole = ["qtconsole"] 279 | test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] 280 | test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] 281 | 282 | [[package]] 283 | name = "jedi" 284 | version = "0.19.0" 285 | description = "An autocompletion tool for Python that can be used for text editors." 286 | category = "dev" 287 | optional = false 288 | python-versions = ">=3.6" 289 | files = [ 290 | {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, 291 | {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, 292 | ] 293 | 294 | [package.dependencies] 295 | parso = ">=0.8.3,<0.9.0" 296 | 297 | [package.extras] 298 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 299 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 300 | testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 301 | 302 | [[package]] 303 | name = "matplotlib-inline" 304 | version = "0.1.6" 305 | description = "Inline Matplotlib backend for Jupyter" 306 | category = "dev" 307 | optional = false 308 | python-versions = ">=3.5" 309 | files = [ 310 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 311 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 312 | ] 313 | 314 | [package.dependencies] 315 | traitlets = "*" 316 | 317 | [[package]] 318 | name = "packaging" 319 | version = "23.1" 320 | description = "Core utilities for Python packages" 321 | category = "dev" 322 | optional = false 323 | python-versions = ">=3.7" 324 | files = [ 325 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 326 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 327 | ] 328 | 329 | [[package]] 330 | name = "parso" 331 | version = "0.8.3" 332 | description = "A Python Parser" 333 | category = "dev" 334 | optional = false 335 | python-versions = ">=3.6" 336 | files = [ 337 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 338 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 339 | ] 340 | 341 | [package.extras] 342 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 343 | testing = ["docopt", "pytest (<6.0.0)"] 344 | 345 | [[package]] 346 | name = "pexpect" 347 | version = "4.8.0" 348 | description = "Pexpect allows easy control of interactive console applications." 349 | category = "dev" 350 | optional = false 351 | python-versions = "*" 352 | files = [ 353 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 354 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 355 | ] 356 | 357 | [package.dependencies] 358 | ptyprocess = ">=0.5" 359 | 360 | [[package]] 361 | name = "pickleshare" 362 | version = "0.7.5" 363 | description = "Tiny 'shelve'-like database with concurrency support" 364 | category = "dev" 365 | optional = false 366 | python-versions = "*" 367 | files = [ 368 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 369 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 370 | ] 371 | 372 | [[package]] 373 | name = "pluggy" 374 | version = "1.2.0" 375 | description = "plugin and hook calling mechanisms for python" 376 | category = "dev" 377 | optional = false 378 | python-versions = ">=3.7" 379 | files = [ 380 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 381 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 382 | ] 383 | 384 | [package.extras] 385 | dev = ["pre-commit", "tox"] 386 | testing = ["pytest", "pytest-benchmark"] 387 | 388 | [[package]] 389 | name = "plumbum" 390 | version = "1.8.2" 391 | description = "Plumbum: shell combinators library" 392 | category = "main" 393 | optional = false 394 | python-versions = ">=3.6" 395 | files = [ 396 | {file = "plumbum-1.8.2-py3-none-any.whl", hash = "sha256:3ad9e5f56c6ec98f6f7988f7ea8b52159662ea9e915868d369dbccbfca0e367e"}, 397 | {file = "plumbum-1.8.2.tar.gz", hash = "sha256:9e6dc032f4af952665f32f3206567bc23b7858b1413611afe603a3f8ad9bfd75"}, 398 | ] 399 | 400 | [package.dependencies] 401 | pywin32 = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} 402 | 403 | [package.extras] 404 | dev = ["paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] 405 | docs = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=1.0.0)"] 406 | ssh = ["paramiko"] 407 | 408 | [[package]] 409 | name = "prompt-toolkit" 410 | version = "3.0.39" 411 | description = "Library for building powerful interactive command lines in Python" 412 | category = "dev" 413 | optional = false 414 | python-versions = ">=3.7.0" 415 | files = [ 416 | {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, 417 | {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, 418 | ] 419 | 420 | [package.dependencies] 421 | wcwidth = "*" 422 | 423 | [[package]] 424 | name = "ptyprocess" 425 | version = "0.7.0" 426 | description = "Run a subprocess in a pseudo terminal" 427 | category = "dev" 428 | optional = false 429 | python-versions = "*" 430 | files = [ 431 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 432 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 433 | ] 434 | 435 | [[package]] 436 | name = "pure-eval" 437 | version = "0.2.2" 438 | description = "Safely evaluate AST nodes without side effects" 439 | category = "dev" 440 | optional = false 441 | python-versions = "*" 442 | files = [ 443 | {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, 444 | {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, 445 | ] 446 | 447 | [package.extras] 448 | tests = ["pytest"] 449 | 450 | [[package]] 451 | name = "pydantic" 452 | version = "2.1.1" 453 | description = "Data validation using Python type hints" 454 | category = "main" 455 | optional = false 456 | python-versions = ">=3.7" 457 | files = [ 458 | {file = "pydantic-2.1.1-py3-none-any.whl", hash = "sha256:43bdbf359d6304c57afda15c2b95797295b702948082d4c23851ce752f21da70"}, 459 | {file = "pydantic-2.1.1.tar.gz", hash = "sha256:22d63db5ce4831afd16e7c58b3192d3faf8f79154980d9397d9867254310ba4b"}, 460 | ] 461 | 462 | [package.dependencies] 463 | annotated-types = ">=0.4.0" 464 | pydantic-core = "2.4.0" 465 | typing-extensions = ">=4.6.1" 466 | 467 | [package.extras] 468 | email = ["email-validator (>=2.0.0)"] 469 | 470 | [[package]] 471 | name = "pydantic-core" 472 | version = "2.4.0" 473 | description = "" 474 | category = "main" 475 | optional = false 476 | python-versions = ">=3.7" 477 | files = [ 478 | {file = "pydantic_core-2.4.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:2ca4687dd996bde7f3c420def450797feeb20dcee2b9687023e3323c73fc14a2"}, 479 | {file = "pydantic_core-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:782fced7d61469fd1231b184a80e4f2fa7ad54cd7173834651a453f96f29d673"}, 480 | {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6213b471b68146af97b8551294e59e7392c2117e28ffad9c557c65087f4baee3"}, 481 | {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63797499a219d8e81eb4e0c42222d0a4c8ec896f5c76751d4258af95de41fdf1"}, 482 | {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_24_armv7l.whl", hash = "sha256:0455876d575a35defc4da7e0a199596d6c773e20d3d42fa1fc29f6aa640369ed"}, 483 | {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:8c938c96294d983dcf419b54dba2d21056959c22911d41788efbf949a29ae30d"}, 484 | {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_24_s390x.whl", hash = "sha256:878a5017d93e776c379af4e7b20f173c82594d94fa073059bcc546789ad50bf8"}, 485 | {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69159afc2f2dc43285725f16143bc5df3c853bc1cb7df6021fce7ef1c69e8171"}, 486 | {file = "pydantic_core-2.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54df7df399b777c1fd144f541c95d351b3aa110535a6810a6a569905d106b6f3"}, 487 | {file = "pydantic_core-2.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e412607ca89a0ced10758dfb8f9adcc365ce4c1c377e637c01989a75e9a9ec8a"}, 488 | {file = "pydantic_core-2.4.0-cp310-none-win32.whl", hash = "sha256:853f103e2b9a58832fdd08a587a51de8b552ae90e1a5d167f316b7eabf8d7dde"}, 489 | {file = "pydantic_core-2.4.0-cp310-none-win_amd64.whl", hash = "sha256:3ba2c9c94a9176f6321a879c8b864d7c5b12d34f549a4c216c72ce213d7d953c"}, 490 | {file = "pydantic_core-2.4.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:a8b7acd04896e8f161e1500dc5f218017db05c1d322f054e89cbd089ce5d0071"}, 491 | {file = "pydantic_core-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16468bd074fa4567592d3255bf25528ed41e6b616d69bf07096bdb5b66f947d1"}, 492 | {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cba5ad5eef02c86a1f3da00544cbc59a510d596b27566479a7cd4d91c6187a11"}, 493 | {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7206e41e04b443016e930e01685bab7a308113c0b251b3f906942c8d4b48fcb"}, 494 | {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:c1375025f0bfc9155286ebae8eecc65e33e494c90025cda69e247c3ccd2bab00"}, 495 | {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:3534118289e33130ed3f1cc487002e8d09b9f359be48b02e9cd3de58ce58fba9"}, 496 | {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_24_s390x.whl", hash = "sha256:94d2b36a74623caab262bf95f0e365c2c058396082bd9d6a9e825657d0c1e7fa"}, 497 | {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af24ad4fbaa5e4a2000beae0c3b7fd1c78d7819ab90f9370a1cfd8998e3f8a3c"}, 498 | {file = "pydantic_core-2.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bf10963d8aed8bbe0165b41797c9463d4c5c8788ae6a77c68427569be6bead41"}, 499 | {file = "pydantic_core-2.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68199ada7c310ddb8c76efbb606a0de656b40899388a7498954f423e03fc38be"}, 500 | {file = "pydantic_core-2.4.0-cp311-none-win32.whl", hash = "sha256:6f855bcc96ed3dd56da7373cfcc9dcbabbc2073cac7f65c185772d08884790ce"}, 501 | {file = "pydantic_core-2.4.0-cp311-none-win_amd64.whl", hash = "sha256:de39eb3bab93a99ddda1ac1b9aa331b944d8bcc4aa9141148f7fd8ee0299dafc"}, 502 | {file = "pydantic_core-2.4.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:f773b39780323a0499b53ebd91a28ad11cde6705605d98d999dfa08624caf064"}, 503 | {file = "pydantic_core-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a297c0d6c61963c5c3726840677b798ca5b7dfc71bc9c02b9a4af11d23236008"}, 504 | {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:546064c55264156b973b5e65e5fafbe5e62390902ce3cf6b4005765505e8ff56"}, 505 | {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36ba9e728588588f0196deaf6751b9222492331b5552f865a8ff120869d372e0"}, 506 | {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:57a53a75010c635b3ad6499e7721eaa3b450e03f6862afe2dbef9c8f66e46ec8"}, 507 | {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_24_ppc64le.whl", hash = "sha256:4b262bbc13022f2097c48a21adcc360a81d83dc1d854c11b94953cd46d7d3c07"}, 508 | {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_24_s390x.whl", hash = "sha256:01947ad728f426fa07fcb26457ebf90ce29320259938414bc0edd1476e75addb"}, 509 | {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b2799c2eaf182769889761d4fb4d78b82bc47dae833799fedbf69fc7de306faa"}, 510 | {file = "pydantic_core-2.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a08fd490ba36d1fbb2cd5dcdcfb9f3892deb93bd53456724389135712b5fc735"}, 511 | {file = "pydantic_core-2.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1e8a7c62d15a5c4b307271e4252d76ebb981d6251c6ecea4daf203ef0179ea4f"}, 512 | {file = "pydantic_core-2.4.0-cp312-none-win32.whl", hash = "sha256:9206c14a67c38de7b916e486ae280017cf394fa4b1aa95cfe88621a4e1d79725"}, 513 | {file = "pydantic_core-2.4.0-cp312-none-win_amd64.whl", hash = "sha256:884235507549a6b2d3c4113fb1877ae263109e787d9e0eb25c35982ab28d0399"}, 514 | {file = "pydantic_core-2.4.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:4cbe929efa77a806e8f1a97793f2dc3ea3475ae21a9ed0f37c21320fe93f6f50"}, 515 | {file = "pydantic_core-2.4.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:9137289de8fe845c246a8c3482dd0cb40338846ba683756d8f489a4bd8fddcae"}, 516 | {file = "pydantic_core-2.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d8e764b5646623e57575f624f8ebb8f7a9f7fd1fae682ef87869ca5fec8dcf"}, 517 | {file = "pydantic_core-2.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fba0aff4c407d0274e43697e785bcac155ad962be57518d1c711f45e72da70f"}, 518 | {file = "pydantic_core-2.4.0-cp37-cp37m-manylinux_2_24_armv7l.whl", hash = "sha256:30527d173e826f2f7651f91c821e337073df1555e3b5a0b7b1e2c39e26e50678"}, 519 | {file = "pydantic_core-2.4.0-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:bd7d1dde70ff3e09e4bc7a1cbb91a7a538add291bfd5b3e70ef1e7b45192440f"}, 520 | {file = "pydantic_core-2.4.0-cp37-cp37m-manylinux_2_24_s390x.whl", hash = "sha256:72f1216ca8cef7b8adacd4c4c6b89c3b0c4f97503197f5284c80f36d6e4edd30"}, 521 | {file = "pydantic_core-2.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b013c7861a7c7bfcec48fd709513fea6f9f31727e7a0a93ca0dd12e056740717"}, 522 | {file = "pydantic_core-2.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:478f5f6d7e32bd4a04d102160efb2d389432ecf095fe87c555c0a6fc4adfc1a4"}, 523 | {file = "pydantic_core-2.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d9610b47b5fe4aacbbba6a9cb5f12cbe864eec99dbfed5710bd32ef5dd8a5d5b"}, 524 | {file = "pydantic_core-2.4.0-cp37-none-win32.whl", hash = "sha256:ff246c0111076c8022f9ba325c294f2cb5983403506989253e04dbae565e019b"}, 525 | {file = "pydantic_core-2.4.0-cp37-none-win_amd64.whl", hash = "sha256:d0c2b713464a8e263a243ae7980d81ce2de5ac59a9f798a282e44350b42dc516"}, 526 | {file = "pydantic_core-2.4.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:12ef6838245569fd60a179fade81ca4b90ae2fa0ef355d616f519f7bb27582db"}, 527 | {file = "pydantic_core-2.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49db206eb8fdc4b4f30e6e3e410584146d813c151928f94ec0db06c4f2595538"}, 528 | {file = "pydantic_core-2.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a507d7fa44688bbac76af6521e488b3da93de155b9cba6f2c9b7833ce243d59"}, 529 | {file = "pydantic_core-2.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe18407a4d000c568182ce5388bbbedeb099896904e43fc14eee76cfae6dec5"}, 530 | {file = "pydantic_core-2.4.0-cp38-cp38-manylinux_2_24_armv7l.whl", hash = "sha256:fa8e48001b39d54d97d7b380a0669fa99fc0feeb972e35a2d677ba59164a9a22"}, 531 | {file = "pydantic_core-2.4.0-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:394f12a2671ff8c4dfa2e85be6c08be0651ad85bc1e6aa9c77c21671baaf28cd"}, 532 | {file = "pydantic_core-2.4.0-cp38-cp38-manylinux_2_24_s390x.whl", hash = "sha256:2f9ea0355f90db2a76af530245fa42f04d98f752a1236ed7c6809ec484560d5b"}, 533 | {file = "pydantic_core-2.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61d4e713f467abcdd59b47665d488bb898ad3dd47ce7446522a50e0cbd8e8279"}, 534 | {file = "pydantic_core-2.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:453862ab268f6326b01f067ed89cb3a527d34dc46f6f4eeec46a15bbc706d0da"}, 535 | {file = "pydantic_core-2.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:56a85fa0dab1567bd0cac10f0c3837b03e8a0d939e6a8061a3a420acd97e9421"}, 536 | {file = "pydantic_core-2.4.0-cp38-none-win32.whl", hash = "sha256:0d726108c1c0380b88b6dd4db559f0280e0ceda9e077f46ff90bc85cd4d03e77"}, 537 | {file = "pydantic_core-2.4.0-cp38-none-win_amd64.whl", hash = "sha256:047580388644c473b934d27849f8ed8dbe45df0adb72104e78b543e13bf69762"}, 538 | {file = "pydantic_core-2.4.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:867d3eea954bea807cabba83cfc939c889a18576d66d197c60025b15269d7cc0"}, 539 | {file = "pydantic_core-2.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:664402ef0c238a7f8a46efb101789d5f2275600fb18114446efec83cfadb5b66"}, 540 | {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64e8012ad60a5f0da09ed48725e6e923d1be25f2f091a640af6079f874663813"}, 541 | {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac2b680de398f293b68183317432b3d67ab3faeba216aec18de0c395cb5e3060"}, 542 | {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_24_armv7l.whl", hash = "sha256:8efc1be43b036c2b6bcfb1451df24ee0ddcf69c31351003daf2699ed93f5687b"}, 543 | {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:d93aedbc4614cc21b9ab0d0c4ccd7143354c1f7cffbbe96ae5216ad21d1b21b5"}, 544 | {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_24_s390x.whl", hash = "sha256:af788b64e13d52fc3600a68b16d31fa8d8573e3ff2fc9a38f8a60b8d94d1f012"}, 545 | {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97c6349c81cee2e69ef59eba6e6c08c5936e6b01c2d50b9e4ac152217845ae09"}, 546 | {file = "pydantic_core-2.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc086ddb6dc654a15deeed1d1f2bcb1cb924ebd70df9dca738af19f64229b06c"}, 547 | {file = "pydantic_core-2.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e953353180bec330c3b830891d260b6f8e576e2d18db3c78d314e56bb2276066"}, 548 | {file = "pydantic_core-2.4.0-cp39-none-win32.whl", hash = "sha256:6feb4b64d11d5420e517910d60a907d08d846cacaf4e029668725cd21d16743c"}, 549 | {file = "pydantic_core-2.4.0-cp39-none-win_amd64.whl", hash = "sha256:153a61ac4030fa019b70b31fb7986461119230d3ba0ab661c757cfea652f4332"}, 550 | {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3fcf529382b282a30b466bd7af05be28e22aa620e016135ac414f14e1ee6b9e1"}, 551 | {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2edef05b63d82568b877002dc4cb5cc18f8929b59077120192df1e03e0c633f8"}, 552 | {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da055a1b0bfa8041bb2ff586b2cb0353ed03944a3472186a02cc44a557a0e661"}, 553 | {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:77dadc764cf7c5405e04866181c5bd94a447372a9763e473abb63d1dfe9b7387"}, 554 | {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a4ea23b07f29487a7bef2a869f68c7ee0e05424d81375ce3d3de829314c6b5ec"}, 555 | {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:382f0baa044d674ad59455a5eff83d7965572b745cc72df35c52c2ce8c731d37"}, 556 | {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:08f89697625e453421401c7f661b9d1eb4c9e4c0a12fd256eeb55b06994ac6af"}, 557 | {file = "pydantic_core-2.4.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:43a405ce520b45941df9ff55d0cd09762017756a7b413bbad3a6e8178e64a2c2"}, 558 | {file = "pydantic_core-2.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584a7a818c84767af16ce8bda5d4f7fedb37d3d231fc89928a192f567e4ef685"}, 559 | {file = "pydantic_core-2.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04922fea7b13cd480586fa106345fe06e43220b8327358873c22d8dfa7a711c7"}, 560 | {file = "pydantic_core-2.4.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17156abac20a9feed10feec867fddd91a80819a485b0107fe61f09f2117fe5f3"}, 561 | {file = "pydantic_core-2.4.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4e562cc63b04636cde361fd47569162f1daa94c759220ff202a8129902229114"}, 562 | {file = "pydantic_core-2.4.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:90f3785146f701e053bb6b9e8f53acce2c919aca91df88bd4975be0cb926eb41"}, 563 | {file = "pydantic_core-2.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e40b1e97edd3dc127aa53d8a5e539a3d0c227d71574d3f9ac1af02d58218a122"}, 564 | {file = "pydantic_core-2.4.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b27f3e67f6e031f6620655741b7d0d6bebea8b25d415924b3e8bfef2dd7bd841"}, 565 | {file = "pydantic_core-2.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be86c2eb12fb0f846262ace9d8f032dc6978b8cb26a058920ecb723dbcb87d05"}, 566 | {file = "pydantic_core-2.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4665f7ed345012a8d2eddf4203ef145f5f56a291d010382d235b94e91813f88a"}, 567 | {file = "pydantic_core-2.4.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:79262be5a292d1df060f29b9a7cdd66934801f987a817632d7552534a172709a"}, 568 | {file = "pydantic_core-2.4.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5fd905a69ac74eaba5041e21a1e8b1a479dab2b41c93bdcc4c1cede3c12a8d86"}, 569 | {file = "pydantic_core-2.4.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:2ad538b7e07343001934417cdc8584623b4d8823c5b8b258e75ec8d327cec969"}, 570 | {file = "pydantic_core-2.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:dd2429f7635ad4857b5881503f9c310be7761dc681c467a9d27787b674d1250a"}, 571 | {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:efff8b6761a1f6e45cebd1b7a6406eb2723d2d5710ff0d1b624fe11313693989"}, 572 | {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a1e0352558cd7ccc014ffe818c7d87b15ec6145875e2cc5fa4bb7351a1033d"}, 573 | {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a027f41c5008571314861744d83aff75a34cf3a07022e0be32b214a5bc93f7f1"}, 574 | {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1927f0e15d190f11f0b8344373731e28fd774c6d676d8a6cfadc95c77214a48b"}, 575 | {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7aa82d483d5fb867d4fb10a138ffd57b0f1644e99f2f4f336e48790ada9ada5e"}, 576 | {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b85778308bf945e9b33ac604e6793df9b07933108d20bdf53811bc7c2798a4af"}, 577 | {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3ded19dcaefe2f6706d81e0db787b59095f4ad0fbadce1edffdf092294c8a23f"}, 578 | {file = "pydantic_core-2.4.0.tar.gz", hash = "sha256:ec3473c9789cc00c7260d840c3db2c16dbfc816ca70ec87a00cddfa3e1a1cdd5"}, 579 | ] 580 | 581 | [package.dependencies] 582 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 583 | 584 | [[package]] 585 | name = "pygments" 586 | version = "2.16.1" 587 | description = "Pygments is a syntax highlighting package written in Python." 588 | category = "dev" 589 | optional = false 590 | python-versions = ">=3.7" 591 | files = [ 592 | {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, 593 | {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, 594 | ] 595 | 596 | [package.extras] 597 | plugins = ["importlib-metadata"] 598 | 599 | [[package]] 600 | name = "pytest" 601 | version = "7.4.0" 602 | description = "pytest: simple powerful testing with Python" 603 | category = "dev" 604 | optional = false 605 | python-versions = ">=3.7" 606 | files = [ 607 | {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, 608 | {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, 609 | ] 610 | 611 | [package.dependencies] 612 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 613 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 614 | iniconfig = "*" 615 | packaging = "*" 616 | pluggy = ">=0.12,<2.0" 617 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 618 | 619 | [package.extras] 620 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 621 | 622 | [[package]] 623 | name = "pywin32" 624 | version = "306" 625 | description = "Python for Window Extensions" 626 | category = "main" 627 | optional = false 628 | python-versions = "*" 629 | files = [ 630 | {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, 631 | {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, 632 | {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, 633 | {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, 634 | {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, 635 | {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, 636 | {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, 637 | {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, 638 | {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, 639 | {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, 640 | {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, 641 | {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, 642 | {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, 643 | {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, 644 | ] 645 | 646 | [[package]] 647 | name = "six" 648 | version = "1.16.0" 649 | description = "Python 2 and 3 compatibility utilities" 650 | category = "dev" 651 | optional = false 652 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 653 | files = [ 654 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 655 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 656 | ] 657 | 658 | [[package]] 659 | name = "sniffio" 660 | version = "1.3.0" 661 | description = "Sniff out which async library your code is running under" 662 | category = "main" 663 | optional = false 664 | python-versions = ">=3.7" 665 | files = [ 666 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 667 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 668 | ] 669 | 670 | [[package]] 671 | name = "stack-data" 672 | version = "0.6.2" 673 | description = "Extract data from python stack frames and tracebacks for informative displays" 674 | category = "dev" 675 | optional = false 676 | python-versions = "*" 677 | files = [ 678 | {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, 679 | {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, 680 | ] 681 | 682 | [package.dependencies] 683 | asttokens = ">=2.1.0" 684 | executing = ">=1.2.0" 685 | pure-eval = "*" 686 | 687 | [package.extras] 688 | tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 689 | 690 | [[package]] 691 | name = "tomli" 692 | version = "2.0.1" 693 | description = "A lil' TOML parser" 694 | category = "dev" 695 | optional = false 696 | python-versions = ">=3.7" 697 | files = [ 698 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 699 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 700 | ] 701 | 702 | [[package]] 703 | name = "tqdm" 704 | version = "4.65.0" 705 | description = "Fast, Extensible Progress Meter" 706 | category = "main" 707 | optional = false 708 | python-versions = ">=3.7" 709 | files = [ 710 | {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, 711 | {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, 712 | ] 713 | 714 | [package.dependencies] 715 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 716 | 717 | [package.extras] 718 | dev = ["py-make (>=0.1.0)", "twine", "wheel"] 719 | notebook = ["ipywidgets (>=6)"] 720 | slack = ["slack-sdk"] 721 | telegram = ["requests"] 722 | 723 | [[package]] 724 | name = "traitlets" 725 | version = "5.9.0" 726 | description = "Traitlets Python configuration system" 727 | category = "dev" 728 | optional = false 729 | python-versions = ">=3.7" 730 | files = [ 731 | {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, 732 | {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, 733 | ] 734 | 735 | [package.extras] 736 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 737 | test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] 738 | 739 | [[package]] 740 | name = "typer" 741 | version = "0.9.0" 742 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 743 | category = "main" 744 | optional = false 745 | python-versions = ">=3.6" 746 | files = [ 747 | {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, 748 | {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, 749 | ] 750 | 751 | [package.dependencies] 752 | click = ">=7.1.1,<9.0.0" 753 | typing-extensions = ">=3.7.4.3" 754 | 755 | [package.extras] 756 | all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 757 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] 758 | doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] 759 | test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 760 | 761 | [[package]] 762 | name = "typing-extensions" 763 | version = "4.7.1" 764 | description = "Backported and Experimental Type Hints for Python 3.7+" 765 | category = "main" 766 | optional = false 767 | python-versions = ">=3.7" 768 | files = [ 769 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 770 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 771 | ] 772 | 773 | [[package]] 774 | name = "wcwidth" 775 | version = "0.2.6" 776 | description = "Measures the displayed width of unicode strings in a terminal" 777 | category = "dev" 778 | optional = false 779 | python-versions = "*" 780 | files = [ 781 | {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, 782 | {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, 783 | ] 784 | 785 | [metadata] 786 | lock-version = "2.0" 787 | python-versions = ">=3.9" 788 | content-hash = "f82737d73abcca1a4edfb50f7aabdc1a80b37c4975db9295884373745168d4f7" 789 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "agentgrunt" 3 | version = "0.1.5" 4 | description = "" 5 | authors = ["Nik V "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.9" 11 | typer = "^0.9.0" 12 | pydantic = "^2.1.1" 13 | plumbum = "^1.8.2" 14 | tqdm = "^4.65.0" 15 | httpx = "^0.24.1" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | ipython = "^8.14.0" 19 | pytest = "^7.4.0" 20 | 21 | [build-system] 22 | requires = ["poetry-core"] 23 | build-backend = "poetry.core.masonry.api" 24 | 25 | [tool.poetry.scripts] 26 | agentgrunt = "agentgrunt.main:cli" 27 | -------------------------------------------------------------------------------- /tests/test_code_exploration.py: -------------------------------------------------------------------------------- 1 | from ..agentgrunt.gpt_tools.code_exploration import ( 2 | extract_function_content, 3 | extract_python_function, 4 | extract_curly_brace_function, 5 | ) 6 | 7 | 8 | def test_extract_python_function(): 9 | # Test case 1: Single-line signature 10 | content_single_line = [ 11 | "def example_function(param1, param2):", 12 | " return param1 + param2", 13 | ] 14 | assert ( 15 | extract_python_function("def example_function(", content_single_line) 16 | == content_single_line 17 | ) 18 | 19 | # Test case 2: Multi-line signature 20 | content_multi_line = [ 21 | "def process_response(", 22 | " self, response, level=0):", 23 | " return response", 24 | ] 25 | assert ( 26 | extract_python_function("def process_response(", content_multi_line) 27 | == content_multi_line 28 | ) 29 | 30 | # Test case 3: Signature not found 31 | assert ( 32 | extract_python_function("def nonexistent_function(", content_single_line) 33 | == None 34 | ) 35 | 36 | 37 | def test_extract_curly_brace_function(): 38 | # Test case 1: Normal JavaScript function 39 | content_js = [ 40 | "function exampleFunction(param1, param2) {", 41 | " return param1 + param2;", 42 | "}", 43 | ] 44 | assert ( 45 | extract_curly_brace_function("function exampleFunction(", content_js) 46 | == content_js 47 | ) 48 | 49 | # Test case 2: Signature not found 50 | assert ( 51 | extract_curly_brace_function("function nonexistentFunction(", content_js) 52 | == None 53 | ) 54 | 55 | 56 | def test_extract_function_content(): 57 | # Test case 1: Python single-line signature 58 | content_python_single_line = [ 59 | "def example_function(param1, param2):", 60 | " return param1 + param2", 61 | ] 62 | assert ( 63 | extract_function_content( 64 | "python", "def example_function(", content_python_single_line 65 | ) 66 | == content_python_single_line 67 | ) 68 | 69 | # Test case 2: JavaScript function 70 | content_js = [ 71 | "function exampleFunction(param1, param2) {", 72 | " return param1 + param2;", 73 | "}", 74 | ] 75 | assert ( 76 | extract_function_content("javascript", "function exampleFunction(", content_js) 77 | == content_js 78 | ) 79 | --------------------------------------------------------------------------------