├── .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 |
--------------------------------------------------------------------------------