diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 17c602e..e2e5d3b 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,17 +7,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.12", "3.13"] + python-version: ["3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - - name: Analysing the code with pylint - run: | - pylint $(git ls-files '*.py') + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e3fdd14 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black + files: src/ + types: [file, python] + + - repo: https://github.com/myint/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + args: + - --in-place + - --imports=pydantic + files: src/ + types: [file, python] + + - repo: https://github.com/pycqa/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + files: src/ + types: [file, python] + args: [--max-line-length=131, --ignore, "F401"] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + files: src/ + types: [file, python] diff --git a/Makefile b/Makefile index e429656..e515cf7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: console console-quiet .uv +.PHONY: console console-quiet .uv install pre-commit .uv: @if ! which uv > /dev/null 2>&1; then \ @@ -14,4 +14,10 @@ console: .uv console-quiet: .uv @uv run textual console -x EVENT - #@uv run textual console -x SYSTEM -x EVENT -x DEBUG -x INFO \ No newline at end of file + #@uv run textual console -x SYSTEM -x EVENT -x DEBUG -x INFO + +pre-commit: .uv install + @uv run pre-commit run --all-files + +install: .uv + @uv run pre-commit install diff --git a/README.md b/README.md index e69de29..2512791 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,5 @@ +- composer install +- composer update +- composer outdated +- composer actions +- diff --git a/pyproject.toml b/pyproject.toml index c57743e..c6d77af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,5 +13,6 @@ dependencies = [ [tool.uv] dev-dependencies = [ + "pre-commit>=4.0.1", "textual-dev>=1.6.1", ] diff --git a/src/composer_utils.py b/src/composer_utils.py new file mode 100644 index 0000000..499193f --- /dev/null +++ b/src/composer_utils.py @@ -0,0 +1,31 @@ +import subprocess + + +def composer_updatable(path: str) -> dict[str, str]: + with subprocess.Popen( + ["composer", "update", "--dry-run"], + cwd=path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) as process: + stdout, stderr = process.communicate() + lines = stdout.strip().split("\n") + packages: dict[str, str] = {} + + # Processing lines for packages + for line in lines: + if line.startswith(" - Upgrading"): + # Extract package name and target version + parts = line.split("(") + package_name = line.strip().split(" ")[2] # Get the package name + version_info = ( + parts[1].strip().rstrip(")") + ) # Get the version info (v2.2.9 => v2.3.0) + target_version = version_info.split("=>")[ + -1 + ].strip() # Get the target version + + # Append to the packages list as a dictionary + packages[package_name] = target_version + return packages diff --git a/src/main.py b/src/main.py index 3c1a984..85acc6d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,18 +1,45 @@ import typer +from pydantic_core._pydantic_core import ValidationError +from rich import print +from composer_utils import composer_updatable +from models import Project +from presentation import MainApp from settings import settings app = typer.Typer() + @app.command() -def tui() -> None: - print("Launch tui") - # app = LegbaApp() - # app.run() +def tui(project_path: str) -> None: + try: + project = Project(path=project_path) + print(f"Composer present: {project.composer}") + except ValidationError as e: + print("Validation error:", e) + exit(1) + + print(f"Launch tui for {project.name} project") + app = MainApp(project) + app.run() + + +@app.command() +def debug(project_path: str) -> None: + try: + project = Project(path=project_path) + print(f"Composer present: {project.composer}") + except ValidationError as e: + print("Validation error:", e) + exit(1) + + print(project.composer_json) + composer_updatable(project) + def main() -> None: app(prog_name=settings.__app_name__) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..5ce17c6 --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1 @@ +from .project import Project diff --git a/src/models/composer.py b/src/models/composer.py new file mode 100644 index 0000000..1c224db --- /dev/null +++ b/src/models/composer.py @@ -0,0 +1,54 @@ +import json +import os + +from pydantic import BaseModel, Field +from rich import print + + +class Composer(BaseModel): + project_path: str + type: str + license: str + minimum_stability: str = Field(str, alias="minimum-stability") + prefer_stable: bool = Field(False, alias="prefer-stable") + required_packages: dict[str, str] + required_packages_dev: dict[str, str] + locked_packages: dict[str, str] + locked_packages_dev: dict[str, str] + scripts: dict[str, str | list[str] | dict[str, str]] + + @classmethod + def from_json(cls, project_path: str): + # json_path = + with open(os.path.join(project_path, "composer.json"), "r") as file: + data = json.load(file) + required_packages = data.pop("require", []) + required_packages_dev = data.pop("require-dev", []) + + lock_file = os.path.join(project_path, "composer.lock") + if os.path.exists(lock_file): + with open(lock_file, "r") as file: + lock_data = json.load(file) + locked_packages = { + package["name"]: package["version"] + for package in lock_data.pop("packages", []) + } + locked_packages_dev = { + package["name"]: package["version"] + for package in lock_data.pop("packages-dev", []) + } + + return cls( + project_path=project_path, + required_packages_dev=required_packages_dev, + required_packages=required_packages, + locked_packages=locked_packages, + locked_packages_dev=locked_packages_dev, + **data + ) + + @property + def manual_scripts(self) -> list[str]: + exclude = ("auto-scripts", "post-install-cmd", "post-update-cmd") + return list(filter(lambda x: x not in exclude, list(self.scripts.keys()))) + # return list(self.scripts.keys()) diff --git a/src/models/project.py b/src/models/project.py new file mode 100644 index 0000000..aa3fd87 --- /dev/null +++ b/src/models/project.py @@ -0,0 +1,36 @@ +import os +from functools import cached_property +from typing import Optional + +from pydantic import BaseModel, Field, field_validator, model_validator + +from models.composer import Composer + + +class Project(BaseModel): + path: str + composer: Optional[bool] = Field(default=False) + + @cached_property + def name(self) -> str: + return os.path.basename(self.path) + + @field_validator("path", mode="before") + def check_directory_exists(cls, v) -> str: + if not os.path.isdir(v): + raise ValueError(f"Provided path '{v}' is not a valid directory.") + return v + + @model_validator(mode="after") + def check_composer_file(self): + # Check if the directory contains a composer.json file + composer_file = os.path.join(self.path, "composer.json") + if os.path.exists(composer_file): + self.composer = True + return self + + @cached_property + def composer_json(self) -> Optional[Composer]: + if not self.composer: + return None + return Composer.from_json(self.path) diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py new file mode 100644 index 0000000..cf981cf --- /dev/null +++ b/src/presentation/__init__.py @@ -0,0 +1,31 @@ +from textual.app import App, ComposeResult +from textual.widgets import Footer, Header, TabbedContent + +from models import Project + +from .composer import ComposerPan +from .docker import DockerPan +from .summary import ProjectSummaryPan + + +class MainApp(App): + """A Textual app to manage stopwatches.""" + + TITLE = "DX Companion" + BINDINGS = [ + ("d", "toggle_dark", "Toggle dark mode"), + ] + CSS_PATH = "../tcss/layout.tcss" + _project: Project + + def __init__(self, project: Project): + self._project = project + super().__init__() + + def compose(self) -> ComposeResult: + yield Header() + with TabbedContent(initial="summary-pan"): + yield ProjectSummaryPan(project=self._project) + yield ComposerPan(composer_dir=self._project.path) + yield DockerPan(project=self._project) + yield Footer() diff --git a/src/presentation/component/__init__.py b/src/presentation/component/__init__.py new file mode 100644 index 0000000..9141791 --- /dev/null +++ b/src/presentation/component/__init__.py @@ -0,0 +1 @@ +from .terminal_modal import TerminalModal diff --git a/src/presentation/component/terminal_modal.py b/src/presentation/component/terminal_modal.py new file mode 100644 index 0000000..a95de95 --- /dev/null +++ b/src/presentation/component/terminal_modal.py @@ -0,0 +1,70 @@ +import subprocess + +from textual import on, work +from textual.app import ComposeResult +from textual.containers import Container, Horizontal +from textual.screen import ModalScreen +from textual.widgets import Button, RichLog, Static + + +class TerminalModal(ModalScreen): + def __init__( + self, command: str | list[str], path: str, use_stderr: bool = False, **kwargs + ): + super().__init__(**kwargs) + self.command = command + self.path = path + self.modal_title = f"Running: {" ".join(self.command)}" + self.use_stderr = use_stderr + self.terminal = RichLog( + id="terminal_command", + highlight=True, + markup=True, + classes="modal_container", + ) + + def compose(self) -> ComposeResult: + with Container(id="modal_container"): + with Horizontal(classes="modal_title"): + yield Static(self.modal_title) + yield self.terminal + with Horizontal(classes="button_container"): + yield Button("Close", id="modal_close") + + def on_mount(self) -> None: + self._start() + + @work(exclusive=True, thread=True) + async def _start(self) -> None: + self.terminal.write(f"Path: [bold blue]{self.path}[/bold blue]") + self.terminal.write(f"Command: [bold blue]{" ".join(self.command)}[/bold blue]") + self.terminal.write( + "----------------------------------------------------------------", + shrink=True, + ) + self.log(f"Running: {self.command} in {self.path}") + with subprocess.Popen( + self.command, + cwd=self.path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) as process: + stdout, stderr = process.communicate() + if stderr and self.use_stderr: + self.terminal.write(f"[italic]{stderr}[/italic]") + self.terminal.write(stdout) + + self.terminal.write( + "----------------------------------------------------------------", + shrink=True, + ) + self.terminal.write(f"Return code [bold]{process.returncode}[/bold]") + if process.returncode == 0: + self.terminal.write("[bold green]Completed![/bold green]") + else: + self.terminal.write("[bold red]Completed with errors![/bold red]") + + @on(Button.Pressed, "#modal_close") + def on_close(self, event: Button.Pressed) -> None: + self.app.pop_screen() diff --git a/src/presentation/composer/__init__.py b/src/presentation/composer/__init__.py new file mode 100644 index 0000000..ca038c1 --- /dev/null +++ b/src/presentation/composer/__init__.py @@ -0,0 +1,3 @@ +from .composer_packages_table import ComposerPackagesTable +from .composer_pan import ComposerPan +from .composer_script_button import ComposerScriptButton diff --git a/src/presentation/composer/composer_packages_table.py b/src/presentation/composer/composer_packages_table.py new file mode 100644 index 0000000..af69c6a --- /dev/null +++ b/src/presentation/composer/composer_packages_table.py @@ -0,0 +1,51 @@ +from rich.text import Text +from textual.widgets import DataTable + + +class ComposerPackagesTable(DataTable): + """ + DataTable for listing packages for a project with + - package name + - package required version (as defined in composer.json) + - package installed (as defined in composer.lock) + - package update (newer version than the installed one, and still matching the requirements) + """ + + def __init__(self, title: str, **kwargs): + super().__init__(**kwargs) + self.border_title = title + self.cursor_type = "row" + self.add_columns(*("Package", "Required", "Locked", "Upgrade")) + + def set_requirements( + self, + required_packages: dict[str, str], + locked_packages: dict[str, str], + packages_updatable: dict[str, str], + ) -> None: + for package, version in required_packages.items(): + styled_row = [ + Text(str(package), justify="left"), + Text(str(version), style="italic #03AC13", justify="right"), + ] + if package in locked_packages: + styled_row.append( + Text( + str(locked_packages[package]), + style="italic #FF0000", + justify="right", + ) + ) + else: + styled_row.append("") + if package in packages_updatable: + styled_row.append( + Text( + str(packages_updatable[package]), + style="italic #00FF00", + justify="right", + ) + ) + else: + styled_row.append("") + self.add_row(*styled_row) diff --git a/src/presentation/composer/composer_pan.py b/src/presentation/composer/composer_pan.py new file mode 100644 index 0000000..f2093ed --- /dev/null +++ b/src/presentation/composer/composer_pan.py @@ -0,0 +1,78 @@ +from textual import on, work +from textual.app import ComposeResult +from textual.containers import Container, Horizontal +from textual.widgets import Button, TabPane +from textual.worker import Worker, WorkerState + +from composer_utils import composer_updatable +from models.composer import Composer +from presentation.component import TerminalModal + +from . import ComposerPackagesTable, ComposerScriptButton + + +class ComposerPan(TabPane): + def __init__(self, composer_dir: str, **kwargs): + self.composer_dir = composer_dir + self.composer = Composer.from_json(composer_dir) + super().__init__(**kwargs, title="Composer", id="composer-pan") + + def compose(self) -> ComposeResult: + with Container(): + yield ComposerPackagesTable( + title="Composer packages", id="composer-packages-table" + ) + yield ComposerPackagesTable( + title="Composer packages-dev", id="composer-packages-dev-table" + ) + yield Horizontal(id="composer-actions") + + async def on_mount(self): + self.loading = True + self._load_composer() + + @work(exclusive=True, thread=True) + async def _load_composer(self) -> dict[str, str]: + # return {} + return composer_updatable(self.composer_dir) + + @on(Worker.StateChanged) + async def refresh_listview(self, event: Worker.StateChanged) -> None: + """Called when the worker state changes.""" + if event.state == WorkerState.SUCCESS: + packages_updatable = event.worker.result + package_table: ComposerPackagesTable = self.query_one( + "#composer-packages-table" + ) + package_table.set_requirements( + self.composer.required_packages, + self.composer.locked_packages, + packages_updatable, + ) + package_dev_table: ComposerPackagesTable = self.query_one( + "#composer-packages-dev-table" + ) + package_dev_table.set_requirements( + self.composer.required_packages_dev, + self.composer.locked_packages_dev, + packages_updatable, + ) + + scripts = self.query_one("#composer-actions") + for script in self.composer.manual_scripts: + # self.log(f"Bouton {script}") + new_button = ComposerScriptButton(script_name=script) + await scripts.mount(new_button) + + self.loading = False + + @on(ComposerScriptButton.Pressed) + def on_pressed(self, event: Button.Pressed) -> None: + if isinstance(event.button, ComposerScriptButton): + self.app.push_screen( + TerminalModal( + command=["composer", "--no-ansi", event.button.script_name], + path=self.composer_dir, + use_stderr=True, + ) + ) diff --git a/src/presentation/composer/composer_script_button.py b/src/presentation/composer/composer_script_button.py new file mode 100644 index 0000000..9d232df --- /dev/null +++ b/src/presentation/composer/composer_script_button.py @@ -0,0 +1,11 @@ +from textual.widgets import Button + + +class ComposerScriptButton(Button): + """ + Button to launch a composer script defined in the composer.json file + """ + + def __init__(self, script_name: str, **kwargs): + self.script_name = script_name + super().__init__(script_name, id=f"composer-button-{script_name}", **kwargs) diff --git a/src/presentation/docker/__init__.py b/src/presentation/docker/__init__.py new file mode 100644 index 0000000..a9fd013 --- /dev/null +++ b/src/presentation/docker/__init__.py @@ -0,0 +1,18 @@ +from rich.text import Text +from textual.app import ComposeResult +from textual.containers import Container +from textual.widgets import Label, TabPane + +from models import Project + + +class DockerPan(TabPane): + def __init__(self, project: Project, **kwargs): + self.project = project + super().__init__(**kwargs, title="Docker", id="docker-pan") + + def compose(self) -> ComposeResult: + with Container(id="project_docker"): + yield Label( + Text(str("Work in progress"), style="italic #03AC13", justify="right") + ) diff --git a/src/presentation/summary/__init__.py b/src/presentation/summary/__init__.py new file mode 100644 index 0000000..341d202 --- /dev/null +++ b/src/presentation/summary/__init__.py @@ -0,0 +1,17 @@ +from rich.text import Text +from textual.app import ComposeResult +from textual.containers import Container +from textual.widgets import Label, TabPane + +from models import Project + + +class ProjectSummaryPan(TabPane): + def __init__(self, project: Project, **kwargs): + self.project = project + super().__init__(**kwargs, title="Summary", id="summary-pan") + + def compose(self) -> ComposeResult: + with Container(id="project_summary"): + yield Label(Text(str("Project :"), style="italic #03AC13", justify="right")) + yield Label(Text(str(self.project.name), style="italic")) diff --git a/src/settings.py b/src/settings.py index a004aae..98856ee 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,14 +1,11 @@ -from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( - env_file='.env', - env_file_encoding='utf-8', - extra='ignore' + env_file=".env", env_file_encoding="utf-8", extra="ignore" ) - __app_name__ = 'Project Manager' + __app_name__ = "Project Manager" # pocket_consumer_key: str = Field() # pocket_access_token: str = Field('') # pocket_username: str = Field('') @@ -19,4 +16,4 @@ class Settings(BaseSettings): settings = Settings() if __name__ == "__main__": - print(settings.model_dump()) \ No newline at end of file + print(settings.model_dump()) diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss new file mode 100644 index 0000000..c977c04 --- /dev/null +++ b/src/tcss/layout.tcss @@ -0,0 +1,74 @@ +#project_container { + layout: grid; + grid-size: 2 1; +} + +#project_summary { + layout: grid; + grid-size: 2 2; + grid-gutter: 1; + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; + Label { + width: 100%; + } +} + +ComposerPan { + Container { + layout: grid; + grid-size: 1 3; + + ComposerPackagesTable { + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; + } + + #composer-packages-table { + row-span: 2; + } + } + #composer-actions { + height: 3; + } +} + +ComposerScriptButton { + width: auto; +} + + +# ##### +# Terminal Modal + +TerminalModal { + align: center middle; + + #modal_container { + margin: 5 10; + } + + .modal_title { + height: 1; + background: $primary-background; + width: 100%; + padding: 0 1; + } + .button_container { + height: 3; + background: $primary-background; + width: 100%; + align: center middle; + } +} + + +TerminalModal > Container > Horizontal > Label { + padding: 0 1; + width: 97; +} +TerminalModal > Container> RichLog { + padding: 1 1; +} diff --git a/uv.lock b/uv.lock index 8468d8f..ef46e03 100644 --- a/uv.lock +++ b/uv.lock @@ -103,6 +103,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + [[package]] name = "click" version = "8.1.7" @@ -124,6 +133,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + [[package]] name = "frozenlist" version = "1.4.1" @@ -148,6 +175,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/10/466fe96dae1bff622021ee687f68e5524d6392b0a2f80d05001cd3a451ba/frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", size = 11552 }, ] +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, +] + [[package]] name = "idna" version = "3.10" @@ -329,6 +365,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -338,6 +383,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + [[package]] name = "project-manager" version = "0.1.0" @@ -351,6 +412,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "pre-commit" }, { name = "textual-dev" }, ] @@ -363,7 +425,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "textual-dev", specifier = ">=1.6.1" }] +dev = [ + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "textual-dev", specifier = ">=1.6.1" }, +] [[package]] name = "propcache" @@ -486,6 +551,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, ] +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + [[package]] name = "rich" version = "13.9.2" @@ -589,6 +680,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229 }, ] +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, +] + [[package]] name = "yarl" version = "1.15.1"