diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0dd6fe..f809d0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.8.2 + rev: v0.8.4 hooks: # Run the linter. - id: ruff diff --git a/CHANGELOG.md b/CHANGELOG.md index d5be239..c1f0e36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 0.3.0 +- Rework the Textarea for Inputing Variables +- Add `duplicate` and `file was edited` hint in preview panel +- Fix Modal Save Screen start with Button disabled if Input is empty +- Fix Save Button enabling/disabling when content was changed + ## Version 0.2.3 - Fix CSS for modals and buttons - Small Text Adjustments diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9749fa4 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.Phony: test + +test: + uv run pytest diff --git a/pyproject.toml b/pyproject.toml index 6847fb4..97cc269 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dotenvhub" -version = "0.2.3" +version = "0.3.0" description = "Terminal App to manage .env files written in Python powered by Textual" readme = "README.md" authors = [ @@ -35,6 +35,8 @@ dot = "dotenvhub.app:run" [tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "file" addopts = "--cov src/dotenvhub --cov-report term-missing --verbose --color=yes" testpaths = ["tests"] @@ -48,4 +50,5 @@ dev = [ "pytest-cov>=6.0.0", "pytest>=8.3.4", "textual-dev>=1.7.0", + "pytest-asyncio>=0.25.0", ] diff --git a/src/dotenvhub/app.py b/src/dotenvhub/app.py index 768ad50..862489f 100644 --- a/src/dotenvhub/app.py +++ b/src/dotenvhub/app.py @@ -1,97 +1,48 @@ -""" -This is a skeleton file that can serve as a starting point for a Python -console script. To run this script uncomment the following lines in the -``[options.entry_points]`` section in ``setup.cfg``:: - - console_scripts = - fibonacci = dotenvhub.skeleton:run - -Then run ``pip install .`` (or ``pip install -e .`` for editable mode) -which will install the command ``fibonacci`` inside your current environment. - -Besides console scripts, the header (i.e. until ``_logger``...) of this file can -also be used as template for Python modules. - -Note: - This file can be renamed depending on your needs or safely removed if not needed. - -References: - - https://setuptools.pypa.io/en/latest/userguide/entry_point.html - - https://pip.pypa.io/en/stable/reference/pip_install -""" - import sys -from dotenvhub import cli_parser, constants, tui, utils +from dotenvhub.tui import DotEnvHub +from dotenvhub.cli_parser import parse_args +from dotenvhub.constants import ENV_FILE_DIR_PATH, CONFIG_FILE_PATH +from dotenvhub.config import create_init_config +from dotenvhub.utils import ( + create_copy_in_cwd, + get_env_content, + create_shell_export_str, + console, +) __author__ = "Zaloog" __copyright__ = "Zaloog" __license__ = "MIT" -# ---- Python API ---- -# The functions defined in this section can be imported by users in their -# Python scripts/interactive interpreter, e.g. via -# `from dotenvhub.skeleton import fib`, -# when using this Python module as a library. - - -# ---- CLI ---- -# The functions defined in this section are wrappers around the main Python -# API allowing them to be called directly from the terminal as a CLI -# executable/script. - - def main(args): - """Wrapper allowing :func:`fib` to be called with string arguments in a CLI fashion - - Instead of returning the value from :func:`fib`, it prints the result to the - ``stdout`` in a nicely formatted message. + create_init_config(conf_path=CONFIG_FILE_PATH, data_path=ENV_FILE_DIR_PATH) - Args: - args (List[str]): command line parameters as list of strings - (for example ``["--verbose", "42"]``). - """ - parsed_args = cli_parser.parse_args(args) + parsed_args = parse_args(args) if not args: - tui.DotEnvHub().run() + DotEnvHub(config_path=CONFIG_FILE_PATH, data_path=ENV_FILE_DIR_PATH).run() if parsed_args.mode == "shell": - content = utils.get_env_content( - filepath=constants.ENV_FILE_DIR_PATH / parsed_args.filename - ) + content = get_env_content(filepath=ENV_FILE_DIR_PATH / parsed_args.filename) - shell_str = utils.create_shell_export_str( + shell_str = create_shell_export_str( shell=parsed_args.shell, env_content=content ) - utils.console.print(f"Copied [blue]{shell_str}[/] to clipboard") + console.print(f"Copied [blue]{shell_str}[/] to clipboard") if parsed_args.mode == "copy": - utils.create_copy_in_cwd( - filepath=constants.ENV_FILE_DIR_PATH / parsed_args.filename, + create_copy_in_cwd( + filepath=ENV_FILE_DIR_PATH / parsed_args.filename, filename=parsed_args.export_name, ) def run(): - """Calls :func:`main` passing the CLI arguments extracted from :obj:`sys.argv` - - This function can be used as entry point to create console scripts with setuptools. - """ main(sys.argv[1:]) if __name__ == "__main__": - # ^ This is a guard statement that will prevent the following code from - # being executed in the case someone imports this file instead of - # executing it as a script. - # https://docs.python.org/3/library/__main__.html - - # After installing your project with pip, users can also run your Python - # modules as scripts via the ``-m`` flag, as defined in PEP 338:: - # - # python -m dotenvhub.skeleton 42 - # run() diff --git a/src/dotenvhub/assets/modal_save.css b/src/dotenvhub/assets/modal_save.tcss similarity index 100% rename from src/dotenvhub/assets/modal_save.css rename to src/dotenvhub/assets/modal_save.tcss diff --git a/src/dotenvhub/assets/modal_shell.css b/src/dotenvhub/assets/modal_shell.tcss similarity index 100% rename from src/dotenvhub/assets/modal_shell.css rename to src/dotenvhub/assets/modal_shell.tcss diff --git a/src/dotenvhub/assets/tui.css b/src/dotenvhub/assets/tui.tcss similarity index 81% rename from src/dotenvhub/assets/tui.css rename to src/dotenvhub/assets/tui.tcss index 4457257..21b8d2f 100644 --- a/src/dotenvhub/assets/tui.css +++ b/src/dotenvhub/assets/tui.tcss @@ -3,7 +3,7 @@ layout: grid; grid-size: 2; /* two columns */ grid-columns: 1fr; - grid-rows: 1fr; + grid-rows: 55% 45%; } /* ############################################### */ @@ -64,16 +64,35 @@ CollapsibleTitle { /* ############################################### */ /* Env File Preview */ #file-preview { - width: auto; + width: 1fr; background: $panel; border: red; - height: 100%; + height: 1fr; background: $boost; } +KeyValPair { + height:auto; + width:1fr; + + Label{ + width:3; + content-align:center middle; + margin:1 0; + } + ValueInput { + height:auto; + width:4fr; + margin:0 1 0 0; + } + VariableInput { + height:auto; + width:4fr; + } +} /* ############################################### */ /* Interaction General */ #interaction { - height: 100%; + height: 1fr; layout: grid; grid-size: 3; grid-columns: 1fr; @@ -114,6 +133,7 @@ CollapsibleTitle { /* Interaction Export Name */ #interaction-export-name { width: 1fr; + height:auto; column-span: 2; margin: 0 1 0 1 ; } @@ -135,7 +155,7 @@ CollapsibleTitle { margin: 1 0 0 0; } #horizontal-save-new > Button { - height: 1fr; + height: 3; width: 1fr; margin: 0 1; } diff --git a/src/dotenvhub/cli_parser.py b/src/dotenvhub/cli_parser.py index 55ae375..f7e7976 100644 --- a/src/dotenvhub/cli_parser.py +++ b/src/dotenvhub/cli_parser.py @@ -2,27 +2,18 @@ from dotenvhub import __version__ -from dotenvhub.config import cfg +from dotenvhub.config import DotEnvHubConfig from dotenvhub.constants import SHELLS def parse_args(args): - """Parse command line parameters - - Args: - args (List[str]): command line parameters as list of strings - (for example ``["--help"]``). - - Returns: - :obj:`argparse.Namespace`: command line parameters namespace - """ - + cfg = DotEnvHubConfig() parser = argparse.ArgumentParser(description="DotEnvHub your .env file manager") subparsers = parser.add_subparsers(dest="mode") parser.add_argument( "--version", action="version", - version=f"kanban-python {__version__}", + version=f"dotenvhub {__version__}", ) sub_exp = subparsers.add_parser("copy", help="Export target File to CWD") sub_exp.add_argument( diff --git a/src/dotenvhub/config.py b/src/dotenvhub/config.py index 57acd78..a27237c 100644 --- a/src/dotenvhub/config.py +++ b/src/dotenvhub/config.py @@ -1,24 +1,23 @@ import configparser from dotenvhub.constants import ( - CONFIG_FILE_NAME, CONFIG_FILE_PATH, - CONFIG_PATH, - DATA_PATH, ENV_FILE_DIR_PATH, ) -def create_init_config(conf_path=CONFIG_PATH, data_path=DATA_PATH): +def create_init_config(conf_path=CONFIG_FILE_PATH, data_path=ENV_FILE_DIR_PATH): + if check_config_exists(path=conf_path): + return + config = configparser.ConfigParser(default_section=None) config.optionxform = str config["settings"] = {"Shell": "bash"} if not ENV_FILE_DIR_PATH.exists(): - data_path.mkdir(exist_ok=True) ENV_FILE_DIR_PATH.mkdir(exist_ok=True) - with open(conf_path / CONFIG_FILE_NAME, "w") as configfile: + with open(conf_path, "w") as configfile: config.write(configfile) @@ -49,9 +48,3 @@ def shell(self) -> str: def shell(self, new_shell) -> str: self._config["settings"]["Shell"] = new_shell self.save() - - -if not check_config_exists(): - create_init_config() - -cfg = DotEnvHubConfig() diff --git a/src/dotenvhub/tui.py b/src/dotenvhub/tui.py index 9538f0e..c58f257 100644 --- a/src/dotenvhub/tui.py +++ b/src/dotenvhub/tui.py @@ -1,39 +1,73 @@ -from pathlib import Path - -from textual.app import App, ComposeResult -from textual.containers import Container -from textual.reactive import var -from textual.widgets import Footer, Header - -from dotenvhub.config import cfg -from dotenvhub.utils import update_file_tree -from dotenvhub.widgets.filepanel import EnvFileSelector -from dotenvhub.widgets.interactionpanel import InteractionPanel -from dotenvhub.widgets.previewpanel import FilePreviewer - - -class DotEnvHub(App): - CSS_PATH = Path("assets/tui.css") - - file_to_show = var("") - file_to_show_path = var("") - file_tree = var(update_file_tree()) - current_content = var("") - current_shell = var(cfg.shell) - - def compose(self) -> ComposeResult: - yield Header() - yield Footer() - - with Container(id="app-grid"): - file_selector = EnvFileSelector(id="file-selector") - file_selector.border_title = "Select your .env File" - yield file_selector - - file_previewer = FilePreviewer(id="file-preview") - file_previewer.border_title = "No Env File Selected" - yield file_previewer - - file_interaction = InteractionPanel(id="interaction") - file_interaction.border_title = "What do you want to do?" - yield file_interaction +from pathlib import Path + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Container +from textual.reactive import var +from textual.widgets import Footer, Header, Button + +from dotenvhub.config import DotEnvHubConfig +from dotenvhub.utils import update_file_tree +from dotenvhub.widgets.filepanel import EnvFileSelector +from dotenvhub.widgets.interactionpanel import InteractionPanel +from dotenvhub.widgets.previewpanel import FilePreviewer +from dotenvhub.constants import ENV_FILE_DIR_PATH, CONFIG_FILE_PATH + + +class DotEnvHub(App): + CSS_PATH = Path("assets/tui.tcss") + + cfg: DotEnvHubConfig + file_to_show = var("") + file_to_show_path = var("") + file_tree = var({}) + current_content = var("") + content_dict = var({}) + current_shell = var("") + + BINDINGS = [ + Binding(key="ctrl+n", action="new_file", description="New File"), + Binding(key="ctrl+s", action="save_file", description="Save"), + ] + + def __init__( + self, config_path: Path = CONFIG_FILE_PATH, data_path: Path = ENV_FILE_DIR_PATH + ): + self.config_path = config_path + self.data_path = data_path + super().__init__() + # self.file_tree = update_file_tree(path=data_path) + self.cfg = DotEnvHubConfig(path=config_path) + self.current_shell = self.cfg.shell + + def compose(self) -> ComposeResult: + self.file_tree = update_file_tree(path=self.data_path) + + yield Header() + yield Footer() + + with Container(id="app-grid"): + self.file_selector = EnvFileSelector(id="file-selector") + self.file_selector.border_title = "Select your .env File" + yield self.file_selector + + self.file_previewer = FilePreviewer(id="file-preview") + self.file_previewer.border_title = "Select file or Create a new one" + yield self.file_previewer + + self.file_interaction = InteractionPanel(id="interaction") + self.file_interaction.border_title = "What do you want to do?" + yield self.file_interaction + + def action_new_file(self): + self.query_one("#btn-new-file", Button).press() + + def action_save_file(self): + self.query_one("#btn-save-file", Button).press() + + def reset_values(self): + self.file_to_show = "" + self.file_to_show_path = "" + self.current_content = "" + self.content_dict = {} + self.app.file_previewer.border_title = "Select file or Create a new one" diff --git a/src/dotenvhub/utils.py b/src/dotenvhub/utils.py index ab43e91..94d316f 100644 --- a/src/dotenvhub/utils.py +++ b/src/dotenvhub/utils.py @@ -1,4 +1,5 @@ import os +from typing import Literal import shutil from pathlib import Path @@ -25,14 +26,27 @@ def copy_path_to_clipboard(path: Path) -> str: return str_path -def get_env_content(filepath: Path): +def get_env_content(filepath: Path) -> str: try: with open(filepath, "r") as env_file: - return "".join(env_file.readlines()) + return env_file.read() except FileNotFoundError: console.print("File [red]not found[/], make sure you entered a valid filename") +def env_content_to_dict(content: str) -> dict[str, str]: + content_dict = {} + for line in content.splitlines(): + key, val = line.split("=") + content_dict[key] = val + + return content_dict + + +def env_dict_to_content(content_dict: dict[str, str]) -> str: + return "\n".join(["=".join([key, val]) for key, val in content_dict.items()]) + + def create_copy_in_cwd(filename: str, filepath: Path): cwd = Path.cwd() try: @@ -42,7 +56,9 @@ def create_copy_in_cwd(filename: str, filepath: Path): console.print("File [red]not found[/], make sure you entered a valid filename") -def create_shell_export_str(shell, env_content): +def create_shell_export_str( + shell: Literal["pwsh", "cmd", "bash", "zsh"], env_content: str +) -> str: if shell == "pwsh": return create_pwsh_string(env_content=env_content) if shell == "cmd": @@ -53,7 +69,7 @@ def create_shell_export_str(shell, env_content): return create_bash_string(env_content=env_content) -def create_pwsh_string(env_content: str): +def create_pwsh_string(env_content: str) -> str: lines = [var.split("=") for var in env_content.split("\n") if var] key_val_list = [f'$env:{key.strip()}="{val.strip()}"' for key, val in lines] pwsh_str = " ; ".join(key_val_list) @@ -61,7 +77,7 @@ def create_pwsh_string(env_content: str): return pwsh_str -def create_cmd_string(env_content: str): +def create_cmd_string(env_content: str) -> str: lines = [var.split("=") for var in env_content.split("\n") if var] key_val_list = [f'set "{key.strip()}={val.strip()}"' for key, val in lines] cmd_str = " & ".join(key_val_list) @@ -69,7 +85,7 @@ def create_cmd_string(env_content: str): return cmd_str -def create_bash_string(env_content: str): +def create_bash_string(env_content: str) -> str: lines = [var.split("=") for var in env_content.split("\n") if var] key_val_list = [f"export {key.strip()}={val.strip()}" for key, val in lines] bash_str = " ; ".join(key_val_list) @@ -77,7 +93,7 @@ def create_bash_string(env_content: str): return bash_str -def write_to_file(path: Path, content): +def write_to_file(path: Path, content: str): path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w") as env_file: diff --git a/src/dotenvhub/widgets/filepanel.py b/src/dotenvhub/widgets/filepanel.py index 7db1a84..1a0cc73 100644 --- a/src/dotenvhub/widgets/filepanel.py +++ b/src/dotenvhub/widgets/filepanel.py @@ -1,29 +1,67 @@ -from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dotenvhub.tui import DotEnvHub from textual import on from textual.containers import VerticalScroll -from textual.widgets import Button, Collapsible, Label, ListItem, ListView, TextArea +from textual.widgets import Button, Collapsible, Label, ListItem, ListView, Input from dotenvhub.constants import ENV_FILE_DIR_PATH -from dotenvhub.utils import get_env_content, update_file_tree +from dotenvhub.utils import get_env_content, update_file_tree, env_content_to_dict +from dotenvhub.widgets.previewpanel import VariableInput + + +class CustomListItem(ListItem): + def __init__(self, file_name: str, dir_name: str = ".", *args, **kwargs): + self.file_name = file_name + self.dir_name = dir_name + if self.dir_name == ".": + self.complete_path = ENV_FILE_DIR_PATH / self.file_name + else: + self.complete_path = ENV_FILE_DIR_PATH / self.dir_name / self.file_name + super().__init__(*args, **kwargs) + + def compose(self): + yield Label(f":page_facing_up: {self.file_name}") + # yield Button( + # "Edit", id=f"btn-edit-{self.file_name}", classes="edit", variant="warning" + # ) + yield Button( + "Delete", id=f"btn-del-{self.file_name}", classes="delete", variant="error" + ) + + @on(Button.Pressed, ".delete") + async def action_delete_env_file(self): + # Delete File + self.complete_path.unlink() + try: + # If Folder Empty delete Folder + self.complete_path.parent.rmdir() + except OSError: + pass + + self.app.file_tree = update_file_tree() + self.app.query_one(EnvFileSelector).refresh(recompose=True) + + # Clear Text Missing Border Title + self.app.reset_values() + await self.app.file_previewer.clear() + + @on(Button.Pressed, ".edit") + def edit_env_file(self): + self.app.query_one(Input).focus() class EnvFileSelector(VerticalScroll): + app: "DotEnvHub" + def compose(self): for dirpath, filenames in self.app.file_tree.items(): if dirpath == ".": general_list = ListView( *[ - ListItem( - Label(f":page_facing_up: {file}"), - Button.warning( - "Edit", id=f"btn-edit-{file}", classes="edit" - ), - Button.error( - "Delete", id=f"btn-del-{file}", classes="delete" - ), - id=f"file-{file}", - ) + CustomListItem(file_name=file, dir_name=dirpath) for file in filenames ], initial_index=None, @@ -32,18 +70,7 @@ def compose(self): else: folder_list = ListView( *[ - ListItem( - Label(f":page_facing_up: {file}"), - Button.warning( - "Edit", id=f"btn-edit-{dirpath}-{file}", classes="edit" - ), - Button.error( - "Delete", - id=f"btn-del-{dirpath}-{file}", - classes="delete", - ), - id=f"{dirpath}-{file}", - ) + CustomListItem(file_name=file, dir_name=dirpath) for file in filenames ], id=f"collaps-{dirpath}", @@ -60,22 +87,20 @@ def compose(self): @on(ListView.Selected) def get_preview_file_path(self, event: ListView.Selected): - self.app.file_to_show = event.list_view.highlighted_child.id.split("-")[1] + selected_item = event.list_view.highlighted_child + self.app.file_to_show = selected_item.file_name + self.app.file_to_show_path = selected_item.complete_path + self.query(Button).remove_class("active") - event.list_view.highlighted_child.query(Button).add_class("active") + selected_item.query(Button).add_class("active") # only collapsible lists have ID if event.list_view.id: - folder = Path(event.list_view.id.split("-")[1]) - self.app.file_to_show_path = ( - ENV_FILE_DIR_PATH / folder / self.app.file_to_show + self.app.file_previewer.border_title = ( + f"{selected_item.dir_name}/{self.app.file_to_show}" ) - self.app.query_one( - "#file-preview" - ).border_title = f"{folder} / {self.app.file_to_show}" else: - self.app.file_to_show_path = ENV_FILE_DIR_PATH / self.app.file_to_show - self.app.query_one("#file-preview").border_title = self.app.file_to_show + self.app.file_previewer.border_title = self.app.file_to_show @on(ListView.Selected) def reset_highlights(self, event: ListView.Selected): @@ -93,39 +118,12 @@ def enable_buttons(self): self.app.query_one("#btn-save-file").disabled = True @on(ListView.Selected) - def update_preview_text(self): + async def update_preview_text(self): self.app.current_content = get_env_content(filepath=self.app.file_to_show_path) + self.app.content_dict = env_content_to_dict(content=self.app.current_content) - text_widget = self.app.query_one(TextArea) - text_widget.text = self.app.current_content - text_widget.action_cursor_page_down() - text_widget.disabled = True - - @on(Button.Pressed, ".delete") - async def delete_env_file(self, event: Button.Pressed): - folder_file_path = event.button.id[8:].replace("-", "/") - - # Delete File - (ENV_FILE_DIR_PATH / folder_file_path).unlink() - try: - # If Folder Empty delete Folder - (ENV_FILE_DIR_PATH / folder_file_path).parent.rmdir() - except OSError: - pass - - self.app.file_tree = update_file_tree() - self.app.query_one(EnvFileSelector).refresh(recompose=True) - - # Clear Text Missing Border Title - self.app.file_to_show = "" - self.app.file_to_show_path = "" - self.app.current_content = "" - - text_widget = self.app.query_one(TextArea) - text_widget.text = "" - - @on(Button.Pressed, ".edit") - def edit_env_file(self): - text_widget = self.app.query_one(TextArea) - text_widget.disabled = False - text_widget.focus() + await self.app.file_previewer.clear() + await self.app.file_previewer.load_values_from_dict( + env_dict=self.app.content_dict + ) + self.app.file_previewer.query_one(VariableInput).focus() diff --git a/src/dotenvhub/widgets/interactionpanel.py b/src/dotenvhub/widgets/interactionpanel.py index 76fd053..d8cc7f4 100644 --- a/src/dotenvhub/widgets/interactionpanel.py +++ b/src/dotenvhub/widgets/interactionpanel.py @@ -1,119 +1,131 @@ -from pathlib import Path - -from textual import on -from textual.containers import Container, Vertical, Horizontal -from textual.widgets import Button, Input, Label, ListView, TextArea - -from dotenvhub.utils import ( - copy_path_to_clipboard, - create_copy_in_cwd, - create_shell_export_str, - write_to_file, -) -from dotenvhub.widgets.modals import ModalSaveScreen, ModalShellSelector - - -class InteractionPanel(Container): - def compose(self): - yield Button( - "Create Shell\nString", - id="btn-shell-export", - disabled=True, - variant="primary", - ) - yield Button( - "Export File to current dir", - id="btn-file-export", - disabled=True, - variant="primary", - ) - yield Button( - "Copy Path to Clipboard", - id="btn-copy-path", - disabled=True, - variant="primary", - ) - with Vertical(id="interaction-shell-select"): - yield Label("Select Shell") - yield Button( - label=self.app.current_shell, id="btn-shell-select", variant="primary" - ) - with Vertical(id="interaction-export-name"): - yield Label("Export filename") - yield Input( - value=".env", - placeholder="env file name for export", - id="export-env-name", - ) - with Horizontal(id="horizontal-save-new"): - yield Button( - "New Env File", id="btn-new-file", disabled=False, variant="success" - ) - yield Button( - "Save Env File", id="btn-save-file", disabled=True, variant="success" - ) - - # Export Interactions - @on(Button.Pressed, "#btn-copy-path") - def copy_env_path(self): - copy_str = copy_path_to_clipboard(path=self.app.file_to_show_path) - self.notify(title="Copied to Clipboard", message=f"Path: [green]{copy_str}[/]") - - @on(Button.Pressed, "#btn-file-export") - def export_env_file(self): - export_filename = self.query_one(Input).value - create_copy_in_cwd( - filename=export_filename, filepath=self.app.file_to_show_path - ) - self.notify(title="Env File Created", message=f"Created: {export_filename}") - - @on(Button.Pressed, "#btn-shell-export") - def export_env_str_shell(self): - shell_str = create_shell_export_str( - shell=self.app.current_shell, env_content=self.app.current_content - ) - self.notify( - title="Copied to Clipboard", - message=f"Command: [green]{shell_str}[/]", - ) - - # Shell Select Interactions - @on(Button.Pressed, "#btn-shell-select") - def pop_modal_shell(self): - self.app.push_screen(ModalShellSelector()) - - # Env File Interactions - @on(Button.Pressed, "#btn-new-file") - def new_file(self, event: Button.Pressed): - self.app.file_to_show = "" - self.app.file_to_show_path = "" - self.app.current_content = "" - - text_widget = self.app.query_one(TextArea) - text_widget.text = "" - text_widget.disabled = False - text_widget.focus() - - event.button.disabled = True - self.query_one("#btn-save-file").disabled = False - - self.app.query_one("#file-preview").border_title = "Creating New .Env File ..." - - for views in self.app.query(ListView): - views.query(Button).remove_class("active") - views.index = None - - @on(Button.Pressed, "#btn-save-file") - def save_file(self, event: Button.Pressed): - text_widget = self.app.query_one(TextArea) - self.app.current_content = text_widget.text - text_widget.disabled = True - self.query_one("#btn-new-file").disabled = False - event.button.disabled = True - - if self.app.file_to_show: - write_to_file( - path=Path(self.app.file_to_show_path), content=self.app.current_content - ) - else: - self.app.push_screen(ModalSaveScreen()) +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dotenvhub.tui import DotEnvHub + +from textual import on +from textual.containers import Container, Vertical, Horizontal +from textual.widgets import Button, Input, Label, ListView + +from dotenvhub.utils import ( + copy_path_to_clipboard, + create_copy_in_cwd, + create_shell_export_str, + write_to_file, + env_dict_to_content, +) +from dotenvhub.widgets.modals import ModalSaveScreen, ModalShellSelector + + +class InteractionPanel(Container): + app: "DotEnvHub" + + def compose(self): + yield Button( + "Create Shell\nString", + id="btn-shell-export", + disabled=True, + variant="primary", + ) + yield Button( + "Export File to current dir", + id="btn-file-export", + disabled=True, + variant="primary", + ) + yield Button( + "Copy Path to Clipboard", + id="btn-copy-path", + disabled=True, + variant="primary", + ) + with Vertical(id="interaction-shell-select"): + yield Label("Select Shell") + yield Button( + label=self.app.current_shell, id="btn-shell-select", variant="primary" + ) + with Vertical(id="interaction-export-name"): + yield Label("Export filename") + yield Input( + value=".env", + placeholder="env file name for export", + id="export-env-name", + ) + with Horizontal(id="horizontal-save-new"): + yield Button( + "New Env File", id="btn-new-file", disabled=False, variant="success" + ) + yield Button( + "Save Env File", id="btn-save-file", disabled=True, variant="success" + ) + + # Export Interactions + @on(Button.Pressed, "#btn-copy-path") + def copy_env_path(self): + copy_str = copy_path_to_clipboard(path=self.app.file_to_show_path) + self.notify(title="Copied to Clipboard", message=f"Path: [green]{copy_str}[/]") + + @on(Button.Pressed, "#btn-file-export") + def export_env_file(self): + export_filename = self.query_one(Input).value + create_copy_in_cwd( + filename=export_filename, filepath=self.app.file_to_show_path + ) + self.notify(title="Env File Created", message=f"Created: {export_filename}") + + @on(Button.Pressed, "#btn-shell-export") + def export_env_str_shell(self): + shell_str = create_shell_export_str( + shell=self.app.current_shell, env_content=self.app.current_content + ) + self.notify( + title="Copied to Clipboard", + message=f"Command: [green]{shell_str}[/]", + ) + + # Shell Select Interactions + @on(Button.Pressed, "#btn-shell-select") + def pop_modal_shell(self): + self.app.push_screen(ModalShellSelector()) + + # Env File Interactions + @on(Button.Pressed, "#btn-new-file") + async def new_file(self, event: Button.Pressed): + self.app.reset_values() + + await self.app.file_previewer.new_file() + + event.button.disabled = True + self.query_one("#btn-save-file").disabled = False + + self.app.query_one("#file-preview").border_title = "Creating New .Env File ..." + + for views in self.app.query(ListView): + views.query(Button).remove_class("active") + views.index = None + + @on(Button.Pressed, "#btn-save-file") + def save_file(self, event: Button.Pressed): + self.app.file_previewer.update_content_dict() + self.app.file_previewer.has_changed = False + if not self.app.content_dict: + self.notify( + severity="warning", + title="Warning", + message="No valid Values to save", + ) + return + + self.query_one("#btn-new-file").disabled = False + # event.button.disabled = True + + self.app.current_content = env_dict_to_content( + content_dict=self.app.content_dict + ) + if self.app.file_to_show: + write_to_file( + path=Path(self.app.file_to_show_path), content=self.app.current_content + ) + else: + self.app.push_screen(ModalSaveScreen()) diff --git a/src/dotenvhub/widgets/modals.py b/src/dotenvhub/widgets/modals.py index e7eb43a..1906bcb 100644 --- a/src/dotenvhub/widgets/modals.py +++ b/src/dotenvhub/widgets/modals.py @@ -1,120 +1,118 @@ -from pathlib import Path - -from textual import on -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Vertical -from textual.reactive import reactive -from textual.screen import ModalScreen -from textual.validation import Regex -from textual.widgets import Button, Input, Label, TextArea - -from dotenvhub.config import cfg -from dotenvhub.constants import ENV_FILE_DIR_PATH, SHELLS -from dotenvhub.utils import update_file_tree, write_to_file -from dotenvhub.widgets.filepanel import EnvFileSelector - - -class ModalShellSelector(ModalScreen): - CSS_PATH = Path("../assets/modal_shell.css") - - def compose(self) -> ComposeResult: - shell_buttons = [ - Button(label=shell, id=shell, variant="primary", classes="shell") - for shell in SHELLS - ] - for btn in shell_buttons: - btn.can_focus = False - yield Vertical( - Label("Which Shell are you using?"), - *shell_buttons, - Button("Cancel", id="btn-modal-cancel"), - id="modal-shell-vert", - ) - - @on(Button.Pressed, "#btn-modal-cancel") - def close_window(self) -> None: - self.dismiss() - - @on(Button.Pressed, ".shell") - def pop_up_shell_select(self, event: Button.Pressed) -> None: - self.dismiss() - selected_shell = event.button.id - self.app.current_shell = selected_shell - cfg.shell = self.app.current_shell - - self.app.query_one("#btn-shell-select").label = self.app.current_shell - self.notify( - title="Shell Selected", - message=f"Current active Shell: [green]{self.app.current_shell}[/]", - ) - - -class ModalSaveScreen(ModalScreen): - CSS_PATH = Path("../assets/modal_save.css") - BINDINGS = [Binding(key="escape", action="close_window", show=False, priority=True)] - preview = reactive(":page_facing_up: Enter File Name") - - def compose(self) -> ComposeResult: - yield Vertical( - Label("How to save file: e.g. FOLDER/FILE"), - Input( - placeholder="New File Name", - id="inp-new-file-name", - valid_empty=False, - validators=[Regex("^[a-zA-Z0-9_.]*(/[a-zA-Z0-9_.]*)?$")], - validate_on=["changed", "submitted"], - ), - Label(self.preview, id="lbl-new-file-name"), - Button("Save", id="btn-modal-save", disabled=True), - Button("Cancel", id="btn-modal-cancel"), - id="modal-save-vert", - ) - - @on(Input.Submitted) - def press_button(self): - self.query_one("#btn-modal-save", Button).press() - - @on(Button.Pressed, "#btn-modal-cancel") - def action_close_window(self) -> None: - self.dismiss() - self.app.query_one(TextArea).disabled = False - self.app.query_one(TextArea).focus() - - @on(Button.Pressed, "#btn-modal-save") - async def save_new_file(self) -> None: - self.dismiss() - - new_path = ENV_FILE_DIR_PATH / self.query_one(Input).value - write_to_file(path=new_path, content=self.app.current_content) - - self.app.file_tree = update_file_tree() - - await self.app.query_one(EnvFileSelector).remove() - self.app.query_one("#app-grid").mount( - EnvFileSelector(id="file-selector"), before="#file-preview" - ) - - @on(Input.Changed, "#inp-new-file-name") - def format_name(self, event: Input.Changed): - text = event.input.value or event.input.placeholder - if "/" not in text: - preview_name = f":page_facing_up: {text}" - elif len(text.split("/")) == 2: - folder, file = [el.strip() for el in text.split("/")] - preview_name = f":file_folder: {folder} / :page_facing_up: {file}" - else: - preview_name = ":cross_mark: Enter a valid Folder/File Name" - - self.preview = preview_name - - if event.input.is_valid and event.input.value: - self.query_one("#btn-modal-save", Button).disabled = False - else: - self.query_one("#btn-modal-save", Button).disabled = True - - self.query_one("#lbl-new-file-name", Label).update(self.preview) - # self.query_one("#lbl-new-file-name").remove() - # self.mount( - # Label(self.preview, id="lbl-new-file-name"), after="#inp-new-file-name" - # ) +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dotenvhub.tui import DotEnvHub + +from textual import on +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical +from textual.reactive import reactive +from textual.screen import ModalScreen +from textual.validation import Regex +from textual.widgets import Button, Input, Label + +from dotenvhub.constants import ENV_FILE_DIR_PATH, SHELLS +from dotenvhub.utils import update_file_tree, write_to_file, env_dict_to_content +from dotenvhub.widgets.filepanel import EnvFileSelector + + +class ModalShellSelector(ModalScreen): + CSS_PATH = Path("../assets/modal_shell.tcss") + + def compose(self) -> ComposeResult: + shell_buttons = [ + Button(label=shell, id=shell, variant="primary", classes="shell") + for shell in SHELLS + ] + for btn in shell_buttons: + btn.can_focus = False + yield Vertical( + Label("Which Shell are you using?"), + *shell_buttons, + Button("Cancel", id="btn-modal-cancel"), + id="modal-shell-vert", + ) + + @on(Button.Pressed, "#btn-modal-cancel") + def close_window(self) -> None: + self.dismiss() + + @on(Button.Pressed, ".shell") + def pop_up_shell_select(self, event: Button.Pressed) -> None: + self.dismiss() + selected_shell = event.button.id + self.app.current_shell = selected_shell + self.app.cfg.shell = self.app.current_shell + + self.app.query_one("#btn-shell-select").label = self.app.current_shell + self.notify( + title="Shell Selected", + message=f"Current active Shell: [green]{self.app.current_shell}[/]", + ) + + +class ModalSaveScreen(ModalScreen): + app: "DotEnvHub" + CSS_PATH = Path("../assets/modal_save.tcss") + BINDINGS = [Binding(key="escape", action="close_window", show=False, priority=True)] + preview = reactive(":page_facing_up: Enter File Name") + + def compose(self) -> ComposeResult: + yield Vertical( + Label("How to save file: e.g. FOLDER/FILE"), + Input( + placeholder="New File Name", + id="inp-new-file-name", + valid_empty=False, + validators=[Regex("^[a-zA-Z0-9_.]*(/[a-zA-Z0-9_.]*)?$")], + validate_on=["changed", "submitted"], + ), + Label(self.preview, id="lbl-new-file-name"), + Button("Save", id="btn-modal-save", disabled=True), + Button("Cancel", id="btn-modal-cancel"), + id="modal-save-vert", + ) + + @on(Input.Submitted) + def press_button(self): + self.query_one("#btn-modal-save", Button).press() + + @on(Button.Pressed, "#btn-modal-cancel") + def action_close_window(self) -> None: + self.dismiss() + + @on(Button.Pressed, "#btn-modal-save") + def save_new_file(self) -> None: + self.dismiss() + + new_path = ENV_FILE_DIR_PATH / self.query_one(Input).value + write_to_file( + path=new_path, + content=env_dict_to_content(content_dict=self.app.content_dict), + ) + + self.app.file_tree = update_file_tree() + + self.app.query_one(EnvFileSelector).refresh(recompose=True) + + @on(Input.Changed, "#inp-new-file-name") + def format_name(self, event: Input.Changed): + text = event.input.value or event.input.placeholder + if "/" not in text: + preview_name = f":page_facing_up: {text}" + elif len(text.split("/")) == 2: + folder, file = [el.strip() for el in text.split("/")] + preview_name = f":file_folder: {folder} / :page_facing_up: {file}" + else: + preview_name = ":cross_mark: Enter a valid Folder/File Name" + + self.preview = preview_name + + if event.input.is_valid and event.input.value: + self.query_one("#btn-modal-save", Button).disabled = False + else: + self.query_one("#btn-modal-save", Button).disabled = True + + self.query_one("#lbl-new-file-name", Label).update(self.preview) diff --git a/src/dotenvhub/widgets/previewpanel.py b/src/dotenvhub/widgets/previewpanel.py index 92e6b56..b0e2afd 100644 --- a/src/dotenvhub/widgets/previewpanel.py +++ b/src/dotenvhub/widgets/previewpanel.py @@ -1,13 +1,123 @@ -from textual import on -from textual.containers import Horizontal -from textual.widgets import TextArea - - -class FilePreviewer(Horizontal): - def compose(self): - yield TextArea(id="text-preview") - - @on(TextArea.Changed) - def disable_buttons(self): - save_button = self.app.query_one("#btn-save-file") - save_button.disabled = False +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dotenvhub.tui import DotEnvHub + +from dataclasses import dataclass + +from textual import on +from textual.message import Message +from textual.reactive import reactive +from textual.containers import Horizontal, VerticalScroll +from textual.widgets import Input, Label + + +class VariableInput(Input): + def __init__(self, key): + with self.prevent(Input.Changed): + super().__init__(value=key, placeholder="Enter Variable") + + +class ValueInput(Input): + def __init__(self, value): + with self.prevent(Input.Changed): + super().__init__(value=value, placeholder="Enter Value") + + +class KeyValPair(Horizontal): + app: "DotEnvHub" + + @dataclass + class ValidMessage(Message): + kv_pair: KeyValPair + + @property + def control(self) -> KeyValPair: + return self.kv_pair + + valid: reactive[bool] = reactive(False) + key: reactive[str] = reactive("") + value: reactive[str] = reactive("") + + def __init__(self, key: str = "", value: str = ""): + super().__init__() + self.key = key + self.value = value + + def compose(self): + yield VariableInput(key=self.key) + yield Label("=") + yield ValueInput(value=self.value) + + @on(Input.Changed) + def check_if_valid(self): + self.key = self.query_one(VariableInput).value + self.value = self.query_one(ValueInput).value + + self.valid = all((self.key != "", self.value != "")) + + def watch_valid(self): + if self.valid: + self.styles.border_left = ("vkey", "green") + self.post_message(self.ValidMessage(kv_pair=self)) + else: + self.styles.border_left = ("vkey", "red") + + @on(VariableInput.Submitted) + def go_to_next(self, event: Input.Submitted): + self.app.action_focus_next() + + +class FilePreviewer(VerticalScroll): + app: "DotEnvHub" + has_changed: reactive[bool] = reactive(False, init=False) + + def __init__(self, id: str | None = None): + super().__init__(id=id) + + async def load_values_from_dict(self, env_dict: dict[str, str] | None = None): + for key, val in env_dict.items(): + kv_pair = KeyValPair(key=key, value=val) + kv_pair.valid = True + await self.mount(kv_pair) + + async def new_file(self): + await self.clear() + await self.mount(KeyValPair()) + self.query_one(KeyValPair).query_one(Input).focus() + + async def clear(self): + await self.remove_children() + + @on(Input.Changed) + def update_content_dict(self): + self.app.content_dict = {} + self.has_changed = True + for kv_pair in self.query(KeyValPair): + if not kv_pair.valid: + kv_pair.styles.border_left = ("vkey", "red") + continue + key, val = kv_pair.key, kv_pair.value + if key in self.app.content_dict.keys(): + kv_pair.styles.border_left = ("vkey", "yellow") + kv_pair.query_one(VariableInput).border_title = "duplicate" + kv_pair.query_one(VariableInput).styles.border = ("tall", "yellow") + continue + kv_pair.styles.border_left = ("vkey", "green") + kv_pair.query_one(VariableInput).border_title = "" + kv_pair.query_one(VariableInput).styles.border = None + self.app.content_dict[key] = val + + @on(KeyValPair.ValidMessage) + def add_new_keyvalpair(self): + if self.query_children(KeyValPair)[-1].valid: + self.mount(KeyValPair()) + + def watch_has_changed(self): + self.app.file_interaction.query_exactly_one( + "#btn-save-file" + ).disabled = not self.has_changed + self.app.file_previewer.border_subtitle = ( + "[yellow on black]file was edited[/]" if self.has_changed else None + ) diff --git a/tests/app_tests/test_filepanel.py b/tests/app_tests/test_filepanel.py new file mode 100644 index 0000000..0df655d --- /dev/null +++ b/tests/app_tests/test_filepanel.py @@ -0,0 +1,72 @@ +import pytest +from textual.widgets import ListItem + +APP_SIZE = (120, 80) + + +@pytest.mark.parametrize( + "file_list, amount", + [ + ([], 0), + (["test1"], 1), + (["test1", "test2"], 2), + (["test1", "test2", "test3"], 3), + ], +) +async def test_filepanel_file(test_app, test_data_path, file_list, amount): + for file in file_list: + if file: + (test_data_path / file).touch() + assert (test_data_path / file).exists() + + async with test_app.run_test(size=APP_SIZE) as pilot: + assert len(list(pilot.app.file_selector.query(ListItem))) == amount + if file_list: + assert ( + pilot.app.file_selector.query(ListItem).first().file_name + == file_list[0] + ) + + +@pytest.mark.parametrize( + "file_list, folder_list, amount", + [ + ([], [], 0), + (["test1"], ["."], 1), + (["test1", "test2"], [".", "folder1"], 4), + (["test1", "test2", "test3"], ["folder1", "folder2"], 6), + ], +) +async def test_filepanel_folder( + test_app, test_data_path, file_list, folder_list, amount +): + # create files + for folder in folder_list: + if not folder: + folder_path = None + elif folder == ".": + folder_path = test_data_path + else: + folder_path = test_data_path / folder + if not folder_path.exists(): + folder_path.mkdir(exist_ok=True) + + for file in file_list: + if file: + (folder_path / file).touch() + assert (folder_path / file).exists() + + # test if files are present in file_selector + async with test_app.run_test(size=APP_SIZE) as pilot: + assert len(list(pilot.app.file_selector.query(ListItem))) == amount + if file_list: + assert ( + pilot.app.file_selector.query(ListItem).first().file_name + == file_list[0] + ) + + if folder_list and (folder_list[0] != "."): + assert ( + pilot.app.file_selector.query(ListItem).first().dir_name + == folder_list[1] # ordered descending + ) diff --git a/tests/conftest.py b/tests/conftest.py index 1b9e470..89331cb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,34 @@ -""" - Dummy conftest.py for dotenvhub. +import pytest +from dotenvhub.tui import DotEnvHub +from dotenvhub.config import create_init_config +from dotenvhub.constants import ENV_FILE_DIR_NAME, CONFIG_FILE_NAME - If you don't know what this is for, just leave it empty. - Read more about conftest.py under: - - https://docs.pytest.org/en/stable/fixture.html - - https://docs.pytest.org/en/stable/writing_plugins.html -""" -import pytest +@pytest.fixture +def test_conf_path(tmp_path) -> str: + return tmp_path / CONFIG_FILE_NAME + + +@pytest.fixture +def test_data_path(tmp_path) -> str: + (tmp_path / ENV_FILE_DIR_NAME).mkdir() + return tmp_path / ENV_FILE_DIR_NAME @pytest.fixture -def test_file_content(): - return """ -USER=TESTUSER +def test_file_content() -> str: + return """USER=TESTUSER DB=TESTDB PORT=TESTPORT """ + + +@pytest.fixture +def test_dict() -> dict[str, str]: + return {"USER": "TESTUSER", "DB": "TESTDB", "PORT": "TESTPORT"} + + +@pytest.fixture +def test_app(test_conf_path, test_data_path) -> DotEnvHub: + create_init_config(conf_path=test_conf_path, data_path=test_data_path) + return DotEnvHub(config_path=test_conf_path, data_path=test_data_path) diff --git a/tests/test_utils.py b/tests/test_utils.py index bb31278..220c146 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,13 +3,18 @@ import pytest -from dotenvhub import utils +from dotenvhub.utils import ( + copy_path_to_clipboard, + create_shell_export_str, + env_content_to_dict, + env_dict_to_content, +) @pytest.mark.skipif(sys.platform == "linux", reason="does not run on ubuntu") def test_copy_path_to_clipboard(): test_path = "test_folder/test_file" - copied_path = utils.copy_path_to_clipboard(path=Path(test_path)) + copied_path = copy_path_to_clipboard(path=Path(test_path)) assert test_path == copied_path @@ -24,8 +29,15 @@ def test_copy_path_to_clipboard(): ], ) def test_create_shell_export_str(shell, test_file_content, expected_shell_str): - copied_str = utils.create_shell_export_str( - shell=shell, env_content=test_file_content - ) - + copied_str = create_shell_export_str(shell=shell, env_content=test_file_content) assert copied_str == expected_shell_str + + +def test_env_content_to_dict(test_file_content): + result = env_content_to_dict(content=test_file_content) + assert result == {"USER": "TESTUSER", "DB": "TESTDB", "PORT": "TESTPORT"} + + +def test_env_dict_to_content(test_dict): + result = env_dict_to_content(content_dict=test_dict) + assert result == """USER=TESTUSER\nDB=TESTDB\nPORT=TESTPORT""" diff --git a/uv.lock b/uv.lock index 310f6a7..793aab7 100644 --- a/uv.lock +++ b/uv.lock @@ -12,7 +12,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.11.10" +version = "3.11.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -24,68 +24,68 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/c4/3b5a937b16f6c2a0ada842a9066aad0b7a5708427d4a202a07bf09c67cbb/aiohttp-3.11.10.tar.gz", hash = "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e", size = 7668832 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/f2/ba44492f257a296c4bb910bf47acf41672421fd455540911b3f13d10d6cd/aiohttp-3.11.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cbad88a61fa743c5d283ad501b01c153820734118b65aee2bd7dbb735475ce0d", size = 708322 }, - { url = "https://files.pythonhosted.org/packages/2b/c7/22b0ed548c8660e978e736671f166907fb272d0a4281b2b6833310bce529/aiohttp-3.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80886dac673ceaef499de2f393fc80bb4481a129e6cb29e624a12e3296cc088f", size = 468211 }, - { url = "https://files.pythonhosted.org/packages/c9/0b/d326251888bb86ff7cb00b171e1cf3b0f0ed695622857f84a98bbc5f254b/aiohttp-3.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61b9bae80ed1f338c42f57c16918853dc51775fb5cb61da70d590de14d8b5fb4", size = 455370 }, - { url = "https://files.pythonhosted.org/packages/4e/83/28feef5a0bda728adf76e0d076566c26c6da3d29f0ccd998d07c260cae9d/aiohttp-3.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e2e576caec5c6a6b93f41626c9c02fc87cd91538b81a3670b2e04452a63def6", size = 1584399 }, - { url = "https://files.pythonhosted.org/packages/dc/97/6bdd39c4134ef243ffa9fd19a072ac9a0758d64b6d51eaaaaa34e67b8bcb/aiohttp-3.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02c13415b5732fb6ee7ff64583a5e6ed1c57aa68f17d2bda79c04888dfdc2769", size = 1632131 }, - { url = "https://files.pythonhosted.org/packages/1b/f1/8c3a1623b9d526986f03d8158c9c856e00531217998275cc6b4a14b2fb85/aiohttp-3.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfce37f31f20800a6a6620ce2cdd6737b82e42e06e6e9bd1b36f546feb3c44f", size = 1668081 }, - { url = "https://files.pythonhosted.org/packages/9c/3e/a2f4cee0dca934b1d2c4b6a7821040ce4452b9b2e4347c9be6cb10eaa835/aiohttp-3.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bbbfff4c679c64e6e23cb213f57cc2c9165c9a65d63717108a644eb5a7398df", size = 1589313 }, - { url = "https://files.pythonhosted.org/packages/fd/9c/93e9a8f39c78f0c6d938721101e28c57597046f78057ffced8a3fd571839/aiohttp-3.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49c7dbbc1a559ae14fc48387a115b7d4bbc84b4a2c3b9299c31696953c2a5219", size = 1544349 }, - { url = "https://files.pythonhosted.org/packages/68/d2/2054efe02be87a1af92cfcaf6875d7b2c34906c3ee2b90ce82afbc8927a5/aiohttp-3.11.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68386d78743e6570f054fe7949d6cb37ef2b672b4d3405ce91fafa996f7d9b4d", size = 1529018 }, - { url = "https://files.pythonhosted.org/packages/10/b0/a258bfd5ddd3d9c871a8d24e96531cb6e6f0cd98dc3028f0b98302454b23/aiohttp-3.11.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9ef405356ba989fb57f84cac66f7b0260772836191ccefbb987f414bcd2979d9", size = 1536357 }, - { url = "https://files.pythonhosted.org/packages/76/7f/8b60b93e7dc58d371813a9b8d451b7c9c9c4350f9c505edf6fae80e0812b/aiohttp-3.11.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5d6958671b296febe7f5f859bea581a21c1d05430d1bbdcf2b393599b1cdce77", size = 1607214 }, - { url = "https://files.pythonhosted.org/packages/2a/10/97a11dba0f6d16878164b92ce75e2e0196a2fd25560cae8283388a24289b/aiohttp-3.11.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:99b7920e7165be5a9e9a3a7f1b680f06f68ff0d0328ff4079e5163990d046767", size = 1628573 }, - { url = "https://files.pythonhosted.org/packages/45/66/70419d6cb9495ddcebfa54d3db07e6a9716049ef341ded1edd8982f9b7f9/aiohttp-3.11.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0dc49f42422163efb7e6f1df2636fe3db72713f6cd94688e339dbe33fe06d61d", size = 1564058 }, - { url = "https://files.pythonhosted.org/packages/2d/d6/d94506afaea3aca15ab3f4732d666ad80acd5a035a7478aa6377c9816cf3/aiohttp-3.11.10-cp310-cp310-win32.whl", hash = "sha256:40d1c7a7f750b5648642586ba7206999650208dbe5afbcc5284bcec6579c9b91", size = 416360 }, - { url = "https://files.pythonhosted.org/packages/55/03/731d1116d09ea7a3c6be731ab0eb1faa37b844d3e54fed28e3a6785ba5ab/aiohttp-3.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:68ff6f48b51bd78ea92b31079817aff539f6c8fc80b6b8d6ca347d7c02384e33", size = 441763 }, - { url = "https://files.pythonhosted.org/packages/db/7c/584d5ca19343c9462d054337828f72628e6dc204424f525df59ebfe75d1e/aiohttp-3.11.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:77c4aa15a89847b9891abf97f3d4048f3c2d667e00f8a623c89ad2dccee6771b", size = 708395 }, - { url = "https://files.pythonhosted.org/packages/cd/2d/61c33e01baeb23aebd07620ee4d780ff40f4c17c42289bf02a405f2ac312/aiohttp-3.11.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909af95a72cedbefe5596f0bdf3055740f96c1a4baa0dd11fd74ca4de0b4e3f1", size = 468281 }, - { url = "https://files.pythonhosted.org/packages/ab/70/0ddb3a61b835068eb0badbe8016b4b65b966bad5f8af0f2d63998ff4cfa4/aiohttp-3.11.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:386fbe79863eb564e9f3615b959e28b222259da0c48fd1be5929ac838bc65683", size = 455345 }, - { url = "https://files.pythonhosted.org/packages/44/8c/4e14e9c1767d9a6ab1af1fbad9df9c77e050b39b6afe9e8343ec1ba96508/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de34936eb1a647aa919655ff8d38b618e9f6b7f250cc19a57a4bf7fd2062b6d", size = 1685464 }, - { url = "https://files.pythonhosted.org/packages/ef/6e/1bab78ebb4f5a1c54f0fc10f8d52abc06816a9cb1db52b9c908e3d69f9a8/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c9527819b29cd2b9f52033e7fb9ff08073df49b4799c89cb5754624ecd98299", size = 1743427 }, - { url = "https://files.pythonhosted.org/packages/5d/5e/c1b03bef621a8cc51ff551ef223c6ac606fabe0e35c950f56d01423ec2aa/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a96e3e03300b41f261bbfd40dfdbf1c301e87eab7cd61c054b1f2e7c89b9e8", size = 1785188 }, - { url = "https://files.pythonhosted.org/packages/7c/b8/df6d76a149cbd969a58da478baec0be617287c496c842ddf21fe6bce07b3/aiohttp-3.11.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0", size = 1674911 }, - { url = "https://files.pythonhosted.org/packages/ee/8e/e460e7bb820a08cec399971fc3176afc8090dc32fb941f386e0c68bc4ecc/aiohttp-3.11.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03b6002e20938fc6ee0918c81d9e776bebccc84690e2b03ed132331cca065ee5", size = 1619570 }, - { url = "https://files.pythonhosted.org/packages/c2/ae/3b597e09eae4e75b77ee6c65443593d245bfa067ae6a5d895abaf27cce6c/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6362cc6c23c08d18ddbf0e8c4d5159b5df74fea1a5278ff4f2c79aed3f4e9f46", size = 1653772 }, - { url = "https://files.pythonhosted.org/packages/b8/d1/99852f2925992c4d7004e590344e5398eb163750de2a7c1fbe07f182d3c8/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3691ed7726fef54e928fe26344d930c0c8575bc968c3e239c2e1a04bd8cf7838", size = 1649787 }, - { url = "https://files.pythonhosted.org/packages/39/c0/ea24627e08d722d5a6a00b3f6c9763fe3ad4650b8485f7a7a56ff932e3af/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31d5093d3acd02b31c649d3a69bb072d539d4c7659b87caa4f6d2bcf57c2fa2b", size = 1732666 }, - { url = "https://files.pythonhosted.org/packages/f1/27/ab52dee4443ef8bdb26473b53c841caafd2bb637a8d85751694e089913bb/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8b3cf2dc0f0690a33f2d2b2cb15db87a65f1c609f53c37e226f84edb08d10f52", size = 1754910 }, - { url = "https://files.pythonhosted.org/packages/cd/08/57c919d6b1f3b70bc14433c080a6152bf99454b636eb8a88552de8baaca9/aiohttp-3.11.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbbaea811a2bba171197b08eea288b9402faa2bab2ba0858eecdd0a4105753a3", size = 1692502 }, - { url = "https://files.pythonhosted.org/packages/ae/37/015006f669275735049e0549c37cb79c7a4a9350cbee070bbccb5a5b4b8a/aiohttp-3.11.10-cp311-cp311-win32.whl", hash = "sha256:4b2c7ac59c5698a7a8207ba72d9e9c15b0fc484a560be0788b31312c2c5504e4", size = 416178 }, - { url = "https://files.pythonhosted.org/packages/cf/8d/7bb48ae503989b15114baf9f9b19398c86ae93d30959065bc061b31331ee/aiohttp-3.11.10-cp311-cp311-win_amd64.whl", hash = "sha256:974d3a2cce5fcfa32f06b13ccc8f20c6ad9c51802bb7f829eae8a1845c4019ec", size = 442269 }, - { url = "https://files.pythonhosted.org/packages/25/17/1dbe2f619f77795409c1a13ab395b98ed1b215d3e938cacde9b8ffdac53d/aiohttp-3.11.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b78f053a7ecfc35f0451d961dacdc671f4bcbc2f58241a7c820e9d82559844cf", size = 704448 }, - { url = "https://files.pythonhosted.org/packages/e3/9b/112247ad47e9d7f6640889c6e42cc0ded8c8345dd0033c66bcede799b051/aiohttp-3.11.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab7485222db0959a87fbe8125e233b5a6f01f4400785b36e8a7878170d8c3138", size = 463829 }, - { url = "https://files.pythonhosted.org/packages/8a/36/a64b583771fc673062a7a1374728a6241d49e2eda5a9041fbf248e18c804/aiohttp-3.11.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf14627232dfa8730453752e9cdc210966490992234d77ff90bc8dc0dce361d5", size = 455774 }, - { url = "https://files.pythonhosted.org/packages/e5/75/ee1b8f510978b3de5f185c62535b135e4fc3f5a247ca0c2245137a02d800/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076bc454a7e6fd646bc82ea7f98296be0b1219b5e3ef8a488afbdd8e81fbac50", size = 1682134 }, - { url = "https://files.pythonhosted.org/packages/87/46/65e8259432d5f73ca9ebf5edb645ef90e5303724e4e52477516cb4042240/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:482cafb7dc886bebeb6c9ba7925e03591a62ab34298ee70d3dd47ba966370d2c", size = 1736757 }, - { url = "https://files.pythonhosted.org/packages/03/f6/a6d1e791b7153fb2d101278f7146c0771b0e1569c547f8a8bc3035651984/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf3d1a519a324af764a46da4115bdbd566b3c73fb793ffb97f9111dbc684fc4d", size = 1793033 }, - { url = "https://files.pythonhosted.org/packages/a8/e9/1ac90733e36e7848693aece522936a13bf17eeb617da662f94adfafc1c25/aiohttp-3.11.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24213ba85a419103e641e55c27dc7ff03536c4873470c2478cce3311ba1eee7b", size = 1691609 }, - { url = "https://files.pythonhosted.org/packages/6d/a6/77b33da5a0bc04566c7ddcca94500f2c2a2334eecab4885387fffd1fc600/aiohttp-3.11.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b99acd4730ad1b196bfb03ee0803e4adac371ae8efa7e1cbc820200fc5ded109", size = 1619082 }, - { url = "https://files.pythonhosted.org/packages/48/94/5bf5f927d9a2fedd2c978adfb70a3680e16f46d178361685b56244eb52ed/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14cdb5a9570be5a04eec2ace174a48ae85833c2aadc86de68f55541f66ce42ab", size = 1641186 }, - { url = "https://files.pythonhosted.org/packages/99/2d/e85103aa01d1064e51bc50cb51e7b40150a8ff5d34e5a3173a46b241860b/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7e97d622cb083e86f18317282084bc9fbf261801b0192c34fe4b1febd9f7ae69", size = 1646280 }, - { url = "https://files.pythonhosted.org/packages/7b/e0/44651fda8c1d865a51b3a81f1956ea55ce16fc568fe7a3e05db7fc22f139/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:012f176945af138abc10c4a48743327a92b4ca9adc7a0e078077cdb5dbab7be0", size = 1701862 }, - { url = "https://files.pythonhosted.org/packages/4e/1e/0804459ae325a5b95f6f349778fb465f29d2b863e522b6a349db0aaad54c/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44224d815853962f48fe124748227773acd9686eba6dc102578defd6fc99e8d9", size = 1734373 }, - { url = "https://files.pythonhosted.org/packages/07/87/b8f6721668cad74bcc9c7cfe6d0230b304d1250196b221e54294a0d78dbe/aiohttp-3.11.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c87bf31b7fdab94ae3adbe4a48e711bfc5f89d21cf4c197e75561def39e223bc", size = 1694343 }, - { url = "https://files.pythonhosted.org/packages/4b/20/42813fc60d9178ba9b1b86c58a5441ddb6cf8ffdfe66387345bff173bcff/aiohttp-3.11.10-cp312-cp312-win32.whl", hash = "sha256:06a8e2ee1cbac16fe61e51e0b0c269400e781b13bcfc33f5425912391a542985", size = 411118 }, - { url = "https://files.pythonhosted.org/packages/3a/51/df9c263c861ce93998b5ad2ba3212caab2112d5b66dbe91ddbe90c41ded4/aiohttp-3.11.10-cp312-cp312-win_amd64.whl", hash = "sha256:be2b516f56ea883a3e14dda17059716593526e10fb6303189aaf5503937db408", size = 437424 }, - { url = "https://files.pythonhosted.org/packages/8c/1d/88bfdbe28a3d1ba5b94a235f188f27726caf8ade9a0e13574848f44fe0fe/aiohttp-3.11.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8cc5203b817b748adccb07f36390feb730b1bc5f56683445bfe924fc270b8816", size = 697755 }, - { url = "https://files.pythonhosted.org/packages/86/00/4c4619d6fe5c5be32f74d1422fc719b3e6cd7097af0c9e03877ca9bd4ebc/aiohttp-3.11.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ef359ebc6949e3a34c65ce20230fae70920714367c63afd80ea0c2702902ccf", size = 460440 }, - { url = "https://files.pythonhosted.org/packages/aa/1c/2f927408f50593a29465d198ec3c57c835c8602330233163e8d89c1093db/aiohttp-3.11.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9bca390cb247dbfaec3c664326e034ef23882c3f3bfa5fbf0b56cad0320aaca5", size = 452726 }, - { url = "https://files.pythonhosted.org/packages/06/6a/ff00ed0a2ba45c34b3c366aa5b0004b1a4adcec5a9b5f67dd0648ee1c88a/aiohttp-3.11.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:811f23b3351ca532af598405db1093f018edf81368e689d1b508c57dcc6b6a32", size = 1664944 }, - { url = "https://files.pythonhosted.org/packages/02/c2/61923f2a7c2e14d7424b3a526e054f0358f57ccdf5573d4d3d033b01921a/aiohttp-3.11.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddf5f7d877615f6a1e75971bfa5ac88609af3b74796ff3e06879e8422729fd01", size = 1717707 }, - { url = "https://files.pythonhosted.org/packages/8a/08/0d3d074b24d377569ec89d476a95ca918443099c0401bb31b331104e35d1/aiohttp-3.11.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ab29b8a0beb6f8eaf1e5049252cfe74adbaafd39ba91e10f18caeb0e99ffb34", size = 1774890 }, - { url = "https://files.pythonhosted.org/packages/e8/49/052ada2b6e90ed65f0e6a7e548614621b5f8dcd193cb9415d2e6bcecc94a/aiohttp-3.11.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c49a76c1038c2dd116fa443eba26bbb8e6c37e924e2513574856de3b6516be99", size = 1676945 }, - { url = "https://files.pythonhosted.org/packages/7c/9e/0c48e1a48e072a869b8b5e3920c9f6a8092861524a4a6f159cd7e6fda939/aiohttp-3.11.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f3dc0e330575f5b134918976a645e79adf333c0a1439dcf6899a80776c9ab39", size = 1602959 }, - { url = "https://files.pythonhosted.org/packages/ab/98/791f979093ff7f67f80344c182cb0ca4c2c60daed397ecaf454cc8d7a5cd/aiohttp-3.11.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:efb15a17a12497685304b2d976cb4939e55137df7b09fa53f1b6a023f01fcb4e", size = 1618058 }, - { url = "https://files.pythonhosted.org/packages/7b/5d/2d4b05feb3fd68eb7c8335f73c81079b56e582633b91002da695ccb439ef/aiohttp-3.11.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db1d0b28fcb7f1d35600150c3e4b490775251dea70f894bf15c678fdd84eda6a", size = 1616289 }, - { url = "https://files.pythonhosted.org/packages/50/83/68cc28c00fe681dce6150614f105efe98282da19252cd6e32dfa893bb328/aiohttp-3.11.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:15fccaf62a4889527539ecb86834084ecf6e9ea70588efde86e8bc775e0e7542", size = 1685239 }, - { url = "https://files.pythonhosted.org/packages/16/f9/68fc5c8928f63238ce9314f04f3f59d9190a4db924998bb9be99c7aacce8/aiohttp-3.11.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:593c114a2221444f30749cc5e5f4012488f56bd14de2af44fe23e1e9894a9c60", size = 1715078 }, - { url = "https://files.pythonhosted.org/packages/3f/e0/3dd3f0451c532c77e35780bafb2b6469a046bc15a6ec2e039475a1d2f161/aiohttp-3.11.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7852bbcb4d0d2f0c4d583f40c3bc750ee033265d80598d0f9cb6f372baa6b836", size = 1672544 }, - { url = "https://files.pythonhosted.org/packages/a5/b1/3530ab040dd5d7fb016b47115016f9b3a07ea29593b0e07e53dbe06a380c/aiohttp-3.11.10-cp313-cp313-win32.whl", hash = "sha256:65e55ca7debae8faaffee0ebb4b47a51b4075f01e9b641c31e554fd376595c6c", size = 409984 }, - { url = "https://files.pythonhosted.org/packages/49/1f/deed34e9fca639a7f873d01150d46925d3e1312051eaa591c1aa1f2e6ddc/aiohttp-3.11.10-cp313-cp313-win_amd64.whl", hash = "sha256:beb39a6d60a709ae3fb3516a1581777e7e8b76933bb88c8f4420d875bb0267c6", size = 435837 }, + { url = "https://files.pythonhosted.org/packages/75/7d/ff2e314b8f9e0b1df833e2d4778eaf23eae6b8cc8f922495d110ddcbf9e1/aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8", size = 708550 }, + { url = "https://files.pythonhosted.org/packages/09/b8/aeb4975d5bba233d6f246941f5957a5ad4e3def8b0855a72742e391925f2/aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5", size = 468430 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/5b620279b3df46e597008b09fa1e10027a39467387c2332657288e25811a/aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2", size = 455593 }, + { url = "https://files.pythonhosted.org/packages/d8/75/0cdf014b816867d86c0bc26f3d3e3f194198dbf33037890beed629cd4f8f/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43", size = 1584635 }, + { url = "https://files.pythonhosted.org/packages/df/2f/95b8f4e4dfeb57c1d9ad9fa911ede35a0249d75aa339edd2c2270dc539da/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f", size = 1632363 }, + { url = "https://files.pythonhosted.org/packages/39/cb/70cf69ea7c50f5b0021a84f4c59c3622b2b3b81695f48a2f0e42ef7eba6e/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d", size = 1668315 }, + { url = "https://files.pythonhosted.org/packages/2f/cc/3a3fc7a290eabc59839a7e15289cd48f33dd9337d06e301064e1e7fb26c5/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef", size = 1589546 }, + { url = "https://files.pythonhosted.org/packages/15/b4/0f7b0ed41ac6000e283e7332f0f608d734b675a8509763ca78e93714cfb0/aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438", size = 1544581 }, + { url = "https://files.pythonhosted.org/packages/58/b9/4d06470fd85c687b6b0e31935ef73dde6e31767c9576d617309a2206556f/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3", size = 1529256 }, + { url = "https://files.pythonhosted.org/packages/61/a2/6958b1b880fc017fd35f5dfb2c26a9a50c755b75fd9ae001dc2236a4fb79/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55", size = 1536592 }, + { url = "https://files.pythonhosted.org/packages/0f/dd/b974012a9551fd654f5bb95a6dd3f03d6e6472a17e1a8216dd42e9638d6c/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e", size = 1607446 }, + { url = "https://files.pythonhosted.org/packages/e0/d3/6c98fd87e638e51f074a3f2061e81fcb92123bcaf1439ac1b4a896446e40/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33", size = 1628809 }, + { url = "https://files.pythonhosted.org/packages/a8/2e/86e6f85cbca02be042c268c3d93e7f35977a0e127de56e319bdd1569eaa8/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c", size = 1564291 }, + { url = "https://files.pythonhosted.org/packages/0b/8d/1f4ef3503b767717f65e1f5178b0173ab03cba1a19997ebf7b052161189f/aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745", size = 416601 }, + { url = "https://files.pythonhosted.org/packages/ad/86/81cb83691b5ace3d9aa148dc42bacc3450d749fc88c5ec1973573c1c1779/aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9", size = 442007 }, + { url = "https://files.pythonhosted.org/packages/34/ae/e8806a9f054e15f1d18b04db75c23ec38ec954a10c0a68d3bd275d7e8be3/aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", size = 708624 }, + { url = "https://files.pythonhosted.org/packages/c7/e0/313ef1a333fb4d58d0c55a6acb3cd772f5d7756604b455181049e222c020/aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", size = 468507 }, + { url = "https://files.pythonhosted.org/packages/a9/60/03455476bf1f467e5b4a32a465c450548b2ce724eec39d69f737191f936a/aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/be/f9/469588603bd75bf02c8ffb8c8a0d4b217eed446b49d4a767684685aa33fd/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", size = 1685694 }, + { url = "https://files.pythonhosted.org/packages/88/b9/1b7fa43faf6c8616fa94c568dc1309ffee2b6b68b04ac268e5d64b738688/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", size = 1743660 }, + { url = "https://files.pythonhosted.org/packages/2a/8b/0248d19dbb16b67222e75f6aecedd014656225733157e5afaf6a6a07e2e8/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", size = 1785421 }, + { url = "https://files.pythonhosted.org/packages/c4/11/f478e071815a46ca0a5ae974651ff0c7a35898c55063305a896e58aa1247/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", size = 1675145 }, + { url = "https://files.pythonhosted.org/packages/26/5d/284d182fecbb5075ae10153ff7374f57314c93a8681666600e3a9e09c505/aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", size = 1619804 }, + { url = "https://files.pythonhosted.org/packages/1b/78/980064c2ad685c64ce0e8aeeb7ef1e53f43c5b005edcd7d32e60809c4992/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", size = 1654007 }, + { url = "https://files.pythonhosted.org/packages/21/8d/9e658d63b1438ad42b96f94da227f2e2c1d5c6001c9e8ffcc0bfb22e9105/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", size = 1650022 }, + { url = "https://files.pythonhosted.org/packages/85/fd/a032bf7f2755c2df4f87f9effa34ccc1ef5cea465377dbaeef93bb56bbd6/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", size = 1732899 }, + { url = "https://files.pythonhosted.org/packages/c5/0c/c2b85fde167dd440c7ba50af2aac20b5a5666392b174df54c00f888c5a75/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", size = 1755142 }, + { url = "https://files.pythonhosted.org/packages/bc/78/91ae1a3b3b3bed8b893c5d69c07023e151b1c95d79544ad04cf68f596c2f/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", size = 1692736 }, + { url = "https://files.pythonhosted.org/packages/77/89/a7ef9c4b4cdb546fcc650ca7f7395aaffbd267f0e1f648a436bec33c9b95/aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", size = 416418 }, + { url = "https://files.pythonhosted.org/packages/fc/db/2192489a8a51b52e06627506f8ac8df69ee221de88ab9bdea77aa793aa6a/aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", size = 442509 }, + { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666 }, + { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057 }, + { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996 }, + { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367 }, + { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989 }, + { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265 }, + { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841 }, + { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317 }, + { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416 }, + { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514 }, + { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095 }, + { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611 }, + { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576 }, + { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363 }, + { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666 }, + { url = "https://files.pythonhosted.org/packages/49/d1/d8af164f400bad432b63e1ac857d74a09311a8334b0481f2f64b158b50eb/aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9", size = 697982 }, + { url = "https://files.pythonhosted.org/packages/92/d1/faad3bf9fa4bfd26b95c69fc2e98937d52b1ff44f7e28131855a98d23a17/aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194", size = 460662 }, + { url = "https://files.pythonhosted.org/packages/db/61/0d71cc66d63909dabc4590f74eba71f91873a77ea52424401c2498d47536/aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f", size = 452950 }, + { url = "https://files.pythonhosted.org/packages/07/db/6d04bc7fd92784900704e16b745484ef45b77bd04e25f58f6febaadf7983/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104", size = 1665178 }, + { url = "https://files.pythonhosted.org/packages/54/5c/e95ade9ae29f375411884d9fd98e50535bf9fe316c9feb0f30cd2ac8f508/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff", size = 1717939 }, + { url = "https://files.pythonhosted.org/packages/6f/1c/1e7d5c5daea9e409ed70f7986001b8c9e3a49a50b28404498d30860edab6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3", size = 1775125 }, + { url = "https://files.pythonhosted.org/packages/5d/66/890987e44f7d2f33a130e37e01a164168e6aff06fce15217b6eaf14df4f6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1", size = 1677176 }, + { url = "https://files.pythonhosted.org/packages/8f/dc/e2ba57d7a52df6cdf1072fd5fa9c6301a68e1cd67415f189805d3eeb031d/aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4", size = 1603192 }, + { url = "https://files.pythonhosted.org/packages/6c/9e/8d08a57de79ca3a358da449405555e668f2c8871a7777ecd2f0e3912c272/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d", size = 1618296 }, + { url = "https://files.pythonhosted.org/packages/56/51/89822e3ec72db352c32e7fc1c690370e24e231837d9abd056490f3a49886/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87", size = 1616524 }, + { url = "https://files.pythonhosted.org/packages/2c/fa/e2e6d9398f462ffaa095e84717c1732916a57f1814502929ed67dd7568ef/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2", size = 1685471 }, + { url = "https://files.pythonhosted.org/packages/ae/5f/6bb976e619ca28a052e2c0ca7b0251ccd893f93d7c24a96abea38e332bf6/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12", size = 1715312 }, + { url = "https://files.pythonhosted.org/packages/79/c1/756a7e65aa087c7fac724d6c4c038f2faaa2a42fe56dbc1dd62a33ca7213/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5", size = 1672783 }, + { url = "https://files.pythonhosted.org/packages/73/ba/a6190ebb02176c7f75e6308da31f5d49f6477b651a3dcfaaaca865a298e2/aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d", size = 410229 }, + { url = "https://files.pythonhosted.org/packages/b8/62/c9fa5bafe03186a0e4699150a7fed9b1e73240996d0d2f0e5f70f3fdf471/aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", size = 436081 }, ] [[package]] @@ -103,14 +103,14 @@ wheels = [ [[package]] name = "aiosignal" -version = "1.3.1" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, ] [[package]] @@ -124,11 +124,11 @@ wheels = [ [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, ] [[package]] @@ -236,7 +236,7 @@ wheels = [ [[package]] name = "dotenvhub" -version = "0.2.2" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "platformdirs" }, @@ -248,6 +248,7 @@ dependencies = [ dev = [ { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "textual-dev" }, ] @@ -263,6 +264,7 @@ requires-dist = [ dev = [ { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "textual-dev", specifier = ">=1.7.0" }, ] @@ -785,6 +787,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] +[[package]] +name = "pytest-asyncio" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/82fcb4ee47d66d99f6cd1efc0b11b2a25029f303c599a5afda7c1bca4254/pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609", size = 53298 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/56/2ee0cab25c11d4e38738a2a98c645a8f002e2ecf7b5ed774c70d53b92bb1/pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3", size = 19245 }, +] + [[package]] name = "pytest-cov" version = "6.0.0" @@ -858,7 +872,7 @@ wheels = [ [[package]] name = "textual" -version = "0.89.1" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", extra = ["linkify", "plugins"] }, @@ -866,9 +880,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/cb/b3ff0e45d812997a527cb581a4cd602f0b28793450aa26201969fd6ce42c/textual-0.89.1.tar.gz", hash = "sha256:66befe80e2bca5a8c876cd8ceeaf01752267b6b1dc1d0f73071f1f1e15d90cc8", size = 1517074 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/b6/59b1de04bb4dca0f21ed7ba0b19309ed7f3f5de4396edf20cc2855e53085/textual-1.0.0.tar.gz", hash = "sha256:bec9fe63547c1c552569d1b75d309038b7d456c03f86dfa3706ddb099b151399", size = 1532733 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/02/650adf160774a43c206011d23283d568d2dbcd43cf7b40dff0a880885b47/textual-0.89.1-py3-none-any.whl", hash = "sha256:0a5d214df6e951b4a2c421e13d0b608482882471c1e34ea74a3631adede8054f", size = 656019 }, + { url = "https://files.pythonhosted.org/packages/ac/bb/5fb6656c625019cd653d5215237d7cd6e0b12e7eae4195c3d1c91b2136fc/textual-1.0.0-py3-none-any.whl", hash = "sha256:2d4a701781c05104925e463ae370c630567c70c2880e92ab838052e3e23c986f", size = 660456 }, ] [[package]]