From 75880cab136c67933543e597048789bfb0a43148 Mon Sep 17 00:00:00 2001 From: Jeckel Date: Tue, 15 Oct 2024 15:51:08 +0200 Subject: [PATCH 01/14] Parse composer.json file into a Composer pydantic object --- README.md | 5 +++++ src/composer.py | 31 +++++++++++++++++++++++++++ src/main.py | 34 ++++++++++++++++++++++++++---- src/models/__init__.py | 1 + src/models/composer.py | 16 ++++++++++++++ src/models/project.py | 37 ++++++++++++++++++++++++++++++++ src/presentation/__init__.py | 41 ++++++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/composer.py create mode 100644 src/models/__init__.py create mode 100644 src/models/composer.py create mode 100644 src/models/project.py create mode 100644 src/presentation/__init__.py diff --git a/README.md b/README.md index e69de29..ed1bbc2 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,5 @@ +- composer install +- composer update +- composer outdated +- composer actions +- \ No newline at end of file diff --git a/src/composer.py b/src/composer.py new file mode 100644 index 0000000..6d46491 --- /dev/null +++ b/src/composer.py @@ -0,0 +1,31 @@ +import subprocess +from typing import Optional, List + +from models import Project + + +def run_composer(command, project: Project, extra_args: Optional[List[str]] = None): + composer_command = ["composer", command] + if extra_args: + composer_command.extend(extra_args) + + result = subprocess.run(composer_command, cwd=project.path, capture_output=True, text=True) + print("stdout:", result.stdout) + print("stderr:", result.stderr) + + # + # with subprocess.Popen( + # composer_command, + # cwd=project.path, + # stdout=subprocess.PIPE, + # # stderr=subprocess.PIPE, + # # text=True + # ) as process: + # print(f"Sortie: {process.stdout.read().strip()}") + # + # stderr = process.communicate()[1] + # if stderr: + # print(f"Erreurs: {stderr}") + +# Exemple d'utilisation +# output = run_composer("install", working_directory="/path/to/your/project") \ No newline at end of file diff --git a/src/main.py b/src/main.py index 3c1a984..9fe88ed 100644 --- a/src/main.py +++ b/src/main.py @@ -1,14 +1,40 @@ import typer +from pydantic_core._pydantic_core import ValidationError +from composer import run_composer +from models import Project +from presentation import MainApp from settings import settings +from rich import print + 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) + # run_composer('outdated', project, ['--no-ansi', '--ignore-platform-reqs']) + # run_composer('outdated', project, ['--dry-run', '--no-ansi']) def main() -> None: app(prog_name=settings.__app_name__) diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..3ca442d --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1 @@ +from .project import Project \ No newline at end of file diff --git a/src/models/composer.py b/src/models/composer.py new file mode 100644 index 0000000..ad5f134 --- /dev/null +++ b/src/models/composer.py @@ -0,0 +1,16 @@ +import json + +from pydantic import BaseModel, Field + +class Composer(BaseModel): + type: str + license: str + minimum_stability: str = Field(str, alias="minimum-stability") + prefer_stable: bool = Field(False, alias="prefer-stable") + require: dict[str, str] + require_dev: dict[str, str] = Field(alias="require-dev") + + @classmethod + def from_json(cls, json_path: str): + with open(json_path, "r") as file: + return cls(**json.load(file)) diff --git a/src/models/project.py b/src/models/project.py new file mode 100644 index 0000000..d81be6b --- /dev/null +++ b/src/models/project.py @@ -0,0 +1,37 @@ +import os +from functools import cached_property + +from pydantic import BaseModel, Field, field_validator, model_validator +from typing import Optional + +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): + 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 + return Composer.from_json(os.path.join(self.path, 'composer.json')) + diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py new file mode 100644 index 0000000..1487b83 --- /dev/null +++ b/src/presentation/__init__.py @@ -0,0 +1,41 @@ +from textual import on +from textual.app import App, ComposeResult +from textual.containers import VerticalScroll, Container +from textual.widgets import Header, Footer, Button + +from models import Project + +class MainApp(App): + """A Textual app to manage stopwatches.""" + + TITLE = "Project Manager" + # SCREENS = {"get_pocket": GetPocketScreen} + BINDINGS = [ + ("d", "toggle_dark", "Toggle dark mode"), + ] + _project: Project + + def __init__(self, project: Project): + self._project = project + super().__init__() + # self.dark = False + + # BINDINGS = [("b", "", "GetPocketScreen")] + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Header() + with Container(): + yield Button(f"Composer {self._project.name}", id='get_pocket_button') + yield Footer() + + def action_toggle_dark(self) -> None: + """An action to toggle dark mode.""" + self.dark = not self.dark + + @on(Button.Pressed, "#get_pocket_button") + def on_get_pocket_button_pressed(self, event: Button.Pressed) -> None: + pass + # self.push_screen('get_pocket') + + From 7aaf96919bbd209ebb0d4ff937101f06b9b8d8f7 Mon Sep 17 00:00:00 2001 From: Jeckel Date: Tue, 15 Oct 2024 19:49:52 +0200 Subject: [PATCH 02/14] Upgrade project dependency list and layout --- src/models/composer.py | 13 ++++++++++--- src/presentation/__init__.py | 20 ++++++++++++++++---- src/presentation/composer.py | 19 +++++++++++++++++++ src/tcss/layout.tcss | 23 +++++++++++++++++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/presentation/composer.py create mode 100644 src/tcss/layout.tcss diff --git a/src/models/composer.py b/src/models/composer.py index ad5f134..08e46ee 100644 --- a/src/models/composer.py +++ b/src/models/composer.py @@ -2,15 +2,22 @@ from pydantic import BaseModel, Field +class Package(BaseModel): + name: str + version: str + class Composer(BaseModel): type: str license: str minimum_stability: str = Field(str, alias="minimum-stability") prefer_stable: bool = Field(False, alias="prefer-stable") - require: dict[str, str] - require_dev: dict[str, str] = Field(alias="require-dev") + require: dict[str, Package] + require_dev: dict[str, Package] @classmethod def from_json(cls, json_path: str): with open(json_path, "r") as file: - return cls(**json.load(file)) + data = json.load(file) + require = {package:Package(name=package, version=version) for package, version in data.pop("require", []).items()} + require_dev = {package:Package(name=package, version=version) for package, version in data.pop("require-dev", []).items()} + return cls(require_dev=require_dev, require=require, **data) diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index 1487b83..962a6c7 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -1,9 +1,12 @@ +from rich.text import Text from textual import on from textual.app import App, ComposeResult from textual.containers import VerticalScroll, Container -from textual.widgets import Header, Footer, Button +from textual.widgets import Header, Footer, Button, DataTable, Label from models import Project +from .composer import ComposerRequireTable + class MainApp(App): """A Textual app to manage stopwatches.""" @@ -13,22 +16,31 @@ class MainApp(App): BINDINGS = [ ("d", "toggle_dark", "Toggle dark mode"), ] + CSS_PATH = "../tcss/layout.tcss" _project: Project def __init__(self, project: Project): self._project = project super().__init__() - # self.dark = False # BINDINGS = [("b", "", "GetPocketScreen")] def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header() - with Container(): - yield Button(f"Composer {self._project.name}", id='get_pocket_button') + with Container(id="project_container"): + with Container(id="project_summary"): + yield Label(Text(str("Project :"), style="italic #03AC13", justify="right")) + yield Label(Text(str(self._project.name), style="italic")) + yield ComposerRequireTable(title="Composer requirements", id="composer_table") + yield Footer() + def on_mount(self) -> None: + table = self.query_one(ComposerRequireTable) + table.set_requirements(self._project.composer_json.require) + + def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" self.dark = not self.dark diff --git a/src/presentation/composer.py b/src/presentation/composer.py new file mode 100644 index 0000000..59344d9 --- /dev/null +++ b/src/presentation/composer.py @@ -0,0 +1,19 @@ +from rich.text import Text +from textual.widgets import DataTable + +from models.composer import Package + + +class ComposerRequireTable(DataTable): + def __init__(self, title: str, **kwargs): + super().__init__(**kwargs) + self.border_title = title + self.add_columns(*('Package', 'Version')) + + def set_requirements(self, requirements: dict[str, Package]) -> None: + for package in requirements.values(): + styled_row = ( + Text(str(package.name), justify="left"), + Text(str(package.version), style="italic #03AC13", justify="right") + ) + self.add_row(*styled_row) \ No newline at end of file diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss new file mode 100644 index 0000000..6926636 --- /dev/null +++ b/src/tcss/layout.tcss @@ -0,0 +1,23 @@ +#project_container { + layout: grid; + grid-size: 2 1; +} + +#project_summary { + layout: grid; + grid-size: 2 1; + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; + Label { + width: 100%; + padding: 0 1; + } +} + +ComposerRequireTable { + # padding: 1 1; + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; +} \ No newline at end of file From 8ead2006fbeb7536171a153c2cd2962bc6288a49 Mon Sep 17 00:00:00 2001 From: Jeckel Date: Wed, 16 Oct 2024 08:53:20 +0200 Subject: [PATCH 03/14] Init composer scripts --- src/models/composer.py | 15 +++++++-------- src/presentation/__init__.py | 3 ++- src/presentation/composer.py | 26 ++++++++++++++++++-------- src/tcss/layout.tcss | 2 +- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/models/composer.py b/src/models/composer.py index 08e46ee..63d4334 100644 --- a/src/models/composer.py +++ b/src/models/composer.py @@ -2,22 +2,21 @@ from pydantic import BaseModel, Field -class Package(BaseModel): - name: str - version: str - class Composer(BaseModel): type: str license: str minimum_stability: str = Field(str, alias="minimum-stability") prefer_stable: bool = Field(False, alias="prefer-stable") - require: dict[str, Package] - require_dev: dict[str, Package] + require: dict[str, str] + require_dev: dict[str, str] + scripts: dict[str, str|list[str]|dict[str, str]] @classmethod def from_json(cls, json_path: str): with open(json_path, "r") as file: data = json.load(file) - require = {package:Package(name=package, version=version) for package, version in data.pop("require", []).items()} - require_dev = {package:Package(name=package, version=version) for package, version in data.pop("require-dev", []).items()} + require = data.pop("require", []) + require_dev = data.pop("require-dev", []) + # require = {package:Package(name=package, version=version) for package, version in data.pop("require", []).items()} + # require_dev = {package:Package(name=package, version=version) for package, version in data.pop("require-dev", []).items()} return cls(require_dev=require_dev, require=require, **data) diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index 962a6c7..1b5422f 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -5,7 +5,7 @@ from textual.widgets import Header, Footer, Button, DataTable, Label from models import Project -from .composer import ComposerRequireTable +from .composer import ComposerRequireTable, ComposerScripts class MainApp(App): @@ -32,6 +32,7 @@ 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")) + yield ComposerScripts(id="composer_scripts") yield ComposerRequireTable(title="Composer requirements", id="composer_table") yield Footer() diff --git a/src/presentation/composer.py b/src/presentation/composer.py index 59344d9..0bc8ce0 100644 --- a/src/presentation/composer.py +++ b/src/presentation/composer.py @@ -1,7 +1,7 @@ +from textual.app import ComposeResult +from textual.containers import Container from rich.text import Text -from textual.widgets import DataTable - -from models.composer import Package +from textual.widgets import DataTable, Button class ComposerRequireTable(DataTable): @@ -10,10 +10,20 @@ def __init__(self, title: str, **kwargs): self.border_title = title self.add_columns(*('Package', 'Version')) - def set_requirements(self, requirements: dict[str, Package]) -> None: - for package in requirements.values(): + def set_requirements(self, requirements: dict[str, str]) -> None: + for package, version in requirements.items(): styled_row = ( - Text(str(package.name), justify="left"), - Text(str(package.version), style="italic #03AC13", justify="right") + Text(str(package), justify="left"), + Text(str(version), style="italic #03AC13", justify="right") ) - self.add_row(*styled_row) \ No newline at end of file + self.add_row(*styled_row) + +class ComposerScripts(Container): + BORDER_TITLE = "Composer Scripts" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def compose(self) -> ComposeResult: + yield Button('cs-fix') + yield Button('analize') \ No newline at end of file diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss index 6926636..3656eea 100644 --- a/src/tcss/layout.tcss +++ b/src/tcss/layout.tcss @@ -5,7 +5,7 @@ #project_summary { layout: grid; - grid-size: 2 1; + grid-size: 2 2; border-title-color: $accent; border: $primary-background round; content-align: center middle; From ec949048fa7358a2e386a51cc12b609aac2904b6 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Wed, 16 Oct 2024 13:29:21 +0200 Subject: [PATCH 04/14] Add composer script button which open a terminal modal (with htop in it) --- pyproject.toml | 1 + src/presentation/__init__.py | 20 ++++++++++++------ src/presentation/composer.py | 40 ++++++++++++++++++++++++++++++++---- src/tcss/layout.tcss | 29 ++++++++++++++++++++++---- uv.lock | 36 ++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c57743e..fd271d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12" dependencies = [ "pydantic-settings>=2.5.2", "pydantic>=2.9.2", + "textual-terminal>=0.3.0", "textual>=0.83.0", "typer>=0.12.5", ] diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index 1b5422f..f36d887 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -5,7 +5,7 @@ from textual.widgets import Header, Footer, Button, DataTable, Label from models import Project -from .composer import ComposerRequireTable, ComposerScripts +from .composer import ComposerRequireTable, ComposerScripts, ComposerScriptButton, ComposerScriptModal class MainApp(App): @@ -37,18 +37,26 @@ def compose(self) -> ComposeResult: yield Footer() - def on_mount(self) -> None: + async def on_mount(self) -> None: table = self.query_one(ComposerRequireTable) table.set_requirements(self._project.composer_json.require) + scripts = self.query_one(ComposerScripts) + for script in self._project.composer_json.scripts.keys(): + self.log(f"Bouton {script}") + new_button = ComposerScriptButton(script_name=script) + await scripts.mount(new_button) def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" self.dark = not self.dark - @on(Button.Pressed, "#get_pocket_button") - def on_get_pocket_button_pressed(self, event: Button.Pressed) -> None: - pass - # self.push_screen('get_pocket') + @on(Button.Pressed) + def on_pressed(self, event: Button.Pressed) -> None: + if isinstance(event.button, ComposerScriptButton): + # self.log(f'container {event.button.id}') + self.push_screen(ComposerScriptModal(event.button.script_name)) + # else: + # self.pop_screen() diff --git a/src/presentation/composer.py b/src/presentation/composer.py index 0bc8ce0..2ba4511 100644 --- a/src/presentation/composer.py +++ b/src/presentation/composer.py @@ -1,7 +1,10 @@ +from textual import on from textual.app import ComposeResult from textual.containers import Container from rich.text import Text -from textual.widgets import DataTable, Button +from textual.screen import ModalScreen +from textual.widgets import DataTable, Button, Label +from textual_terminal import Terminal class ComposerRequireTable(DataTable): @@ -18,12 +21,41 @@ def set_requirements(self, requirements: dict[str, str]) -> None: ) self.add_row(*styled_row) +class ComposerScriptButton(Button): + def __init__(self, script_name: str, **kwargs): + self.script_name = script_name + super().__init__(f"Bouton {script_name}", id=f"composer-button-{script_name}", **kwargs) + self.script_name = script_name + class ComposerScripts(Container): BORDER_TITLE = "Composer Scripts" def __init__(self, **kwargs): super().__init__(**kwargs) - def compose(self) -> ComposeResult: - yield Button('cs-fix') - yield Button('analize') \ No newline at end of file + # @on(Button.Pressed) + # def on_button_pressed(self, event: Button.Pressed) -> None: + # # todo check button instance of ComposerScriptButton + # self.log(f'composer {event.button.script_name}') + # pass + +class ComposerScriptModal(ModalScreen): + BORDER_TITLE = "Composer script ?" + def __init__(self, script: str, **kwargs): + super().__init__(**kwargs) + self.script = script + + def compose(self): + with Container(): + yield Label(f"Running script {self.script} in a terminal") + yield Button.success("Close", id="composer_modal_close") + yield Terminal(command="htop", id="terminal_composer_script", default_colors="textual") + + def on_mount(self) -> None: + self.log('on ready') + terminal: Terminal = self.query_one("#terminal_composer_script") + terminal.start() + + @on(Button.Pressed, "#composer_modal_close") + def on_close(self, event: Button.Pressed) -> None: + self.app.pop_screen() diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss index 3656eea..f3f1be2 100644 --- a/src/tcss/layout.tcss +++ b/src/tcss/layout.tcss @@ -6,12 +6,12 @@ #project_summary { layout: grid; grid-size: 2 2; - border-title-color: $accent; - border: $primary-background round; - content-align: center middle; + grid-gutter: 1; + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; Label { width: 100%; - padding: 0 1; } } @@ -20,4 +20,25 @@ ComposerRequireTable { border-title-color: $accent; border: $primary-background round; content-align: center middle; +} + +ComposerScripts { + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; +} + +ComposerScriptModal { + align: center middle; +} + +ComposerScriptModal > Container { + padding: 1 2; + background: $panel; + width: 100; + height: 50; +} + +ComposerScriptModal > Container > Button { + margin: 1 0; } \ No newline at end of file diff --git a/uv.lock b/uv.lock index 8468d8f..67dd469 100644 --- a/uv.lock +++ b/uv.lock @@ -346,6 +346,7 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "textual" }, + { name = "textual-terminal" }, { name = "typer" }, ] @@ -359,6 +360,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.9.2" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "textual", specifier = ">=0.83.0" }, + { name = "textual-terminal", specifier = ">=0.3.0" }, { name = "typer", specifier = ">=0.12.5" }, ] @@ -477,6 +479,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] +[[package]] +name = "pyte" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/ab/b599762933eba04de7dc5b31ae083112a6c9a9db15b01d3109ad797559d9/pyte-0.8.2.tar.gz", hash = "sha256:5af970e843fa96a97149d64e170c984721f20e52227a2f57f0a54207f08f083f", size = 92301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/d0/bb522283b90853afbf506cd5b71c650cf708829914efd0003d615cf426cd/pyte-0.8.2-py3-none-any.whl", hash = "sha256:85db42a35798a5aafa96ac4d8da78b090b2c933248819157fc0e6f78876a0135", size = 31627 }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -556,6 +570,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/a9/01d35770fde8d889e1fe28b726188cf28801e57afd369c614cd2bc100ee4/textual_serve-1.1.1-py3-none-any.whl", hash = "sha256:568782f1c0e60e3f7039d9121e1cb5c2f4ca1aaf6d6bd7aeb833d5763a534cb2", size = 445034 }, ] +[[package]] +name = "textual-terminal" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyte" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/fe/94e6a50d388c9c8ce0da27f71ca5abecd99a15d4347b3d6dccc1dedfe76f/textual_terminal-0.3.0.tar.gz", hash = "sha256:594e7323b74d1e395cf3ce39216cef78cb7c64d01ea71a60ea7b0f9b198e4c87", size = 9482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/3d/c9202997d383a317b14de28e2c7925b56a83edb0a74012ee39b86217850f/textual_terminal-0.3.0-py3-none-any.whl", hash = "sha256:f00b22f5a59ef6d60b61453d7a98354647487c00342d4b0324221db18c4a90f1", size = 12529 }, +] + [[package]] name = "typer" version = "0.12.5" @@ -589,6 +616,15 @@ 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 = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + [[package]] name = "yarl" version = "1.15.1" From f68aabd879a6210559d468e047d97ab39c019d0d Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Wed, 16 Oct 2024 17:44:30 +0200 Subject: [PATCH 05/14] Run composer action in Modal --- pyproject.toml | 1 - src/main.py | 22 +++++++++++++++-- src/models/composer.py | 6 +++++ src/presentation/__init__.py | 14 +++++++---- src/presentation/composer.py | 39 +++-------------------------- src/presentation/terminal.py | 48 ++++++++++++++++++++++++++++++++++++ src/tcss/layout.tcss | 42 ++++++++++++++++++++++--------- uv.lock | 36 --------------------------- 8 files changed, 118 insertions(+), 90 deletions(-) create mode 100644 src/presentation/terminal.py diff --git a/pyproject.toml b/pyproject.toml index fd271d8..c57743e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ requires-python = ">=3.12" dependencies = [ "pydantic-settings>=2.5.2", "pydantic>=2.9.2", - "textual-terminal>=0.3.0", "textual>=0.83.0", "typer>=0.12.5", ] diff --git a/src/main.py b/src/main.py index 9fe88ed..4e79d0b 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,6 @@ import typer from pydantic_core._pydantic_core import ValidationError +from textual.app import ComposeResult from composer import run_composer from models import Project @@ -33,8 +34,25 @@ def debug(project_path: str) -> None: exit(1) print(project.composer_json) - # run_composer('outdated', project, ['--no-ansi', '--ignore-platform-reqs']) - # run_composer('outdated', project, ['--dry-run', '--no-ansi']) + + from textual_terminal import Terminal + + from textual.app import App + class TerminalApp(App): + def compose(self) -> ComposeResult: + #cd /home/jeckel/Workspace/10_Clients/Lamy-Liaisons/sf-ecustomer && composer cs-fix + yield Terminal(command="cd /home/jeckel/Workspace/10_Clients/Lamy-Liaisons/sf-ecustomer && ls", id="terminal_htop") + # yield Terminal(command="bash", id="terminal_bash") + + def on_ready(self) -> None: + terminal_htop: Terminal = self.query_one("#terminal_htop") + terminal_htop.start() + + # terminal_bash: Terminal = self.query_one("#terminal_bash") + # terminal_bash.start() + app = TerminalApp() + app.run() + def main() -> None: app(prog_name=settings.__app_name__) diff --git a/src/models/composer.py b/src/models/composer.py index 63d4334..442c50e 100644 --- a/src/models/composer.py +++ b/src/models/composer.py @@ -20,3 +20,9 @@ def from_json(cls, json_path: str): # require = {package:Package(name=package, version=version) for package, version in data.pop("require", []).items()} # require_dev = {package:Package(name=package, version=version) for package, version in data.pop("require-dev", []).items()} return cls(require_dev=require_dev, require=require, **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/presentation/__init__.py b/src/presentation/__init__.py index f36d887..715e0c1 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -5,7 +5,8 @@ from textual.widgets import Header, Footer, Button, DataTable, Label from models import Project -from .composer import ComposerRequireTable, ComposerScripts, ComposerScriptButton, ComposerScriptModal +from .composer import ComposerRequireTable, ComposerScripts, ComposerScriptButton +from .terminal import TerminalModal class MainApp(App): @@ -28,11 +29,12 @@ def __init__(self, project: Project): def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header() + yield ComposerScripts(id="composer_scripts") with Container(id="project_container"): with Container(id="project_summary"): yield Label(Text(str("Project :"), style="italic #03AC13", justify="right")) yield Label(Text(str(self._project.name), style="italic")) - yield ComposerScripts(id="composer_scripts") + # yield ComposerScripts(id="composer_scripts") yield ComposerRequireTable(title="Composer requirements", id="composer_table") yield Footer() @@ -41,7 +43,7 @@ async def on_mount(self) -> None: table = self.query_one(ComposerRequireTable) table.set_requirements(self._project.composer_json.require) scripts = self.query_one(ComposerScripts) - for script in self._project.composer_json.scripts.keys(): + for script in self._project.composer_json.manual_scripts: self.log(f"Bouton {script}") new_button = ComposerScriptButton(script_name=script) await scripts.mount(new_button) @@ -54,8 +56,10 @@ def action_toggle_dark(self) -> None: @on(Button.Pressed) def on_pressed(self, event: Button.Pressed) -> None: if isinstance(event.button, ComposerScriptButton): - # self.log(f'container {event.button.id}') - self.push_screen(ComposerScriptModal(event.button.script_name)) + # command = f"cd {self._project.path} && composer {event.button.script_name}" + # self.log(command) + self.push_screen(TerminalModal(command=['composer', event.button.script_name], path=self._project.path)) + # self.push_screen(TerminalModal(command='ls -lah', path=self._project.path)) # else: # self.pop_screen() diff --git a/src/presentation/composer.py b/src/presentation/composer.py index 2ba4511..939cae4 100644 --- a/src/presentation/composer.py +++ b/src/presentation/composer.py @@ -1,10 +1,6 @@ -from textual import on -from textual.app import ComposeResult -from textual.containers import Container +from textual.containers import Container, VerticalScroll, Horizontal from rich.text import Text -from textual.screen import ModalScreen -from textual.widgets import DataTable, Button, Label -from textual_terminal import Terminal +from textual.widgets import DataTable, Button, Label, RichLog class ComposerRequireTable(DataTable): @@ -24,38 +20,11 @@ def set_requirements(self, requirements: dict[str, str]) -> None: class ComposerScriptButton(Button): def __init__(self, script_name: str, **kwargs): self.script_name = script_name - super().__init__(f"Bouton {script_name}", id=f"composer-button-{script_name}", **kwargs) + super().__init__(script_name, id=f"composer-button-{script_name}", **kwargs) self.script_name = script_name -class ComposerScripts(Container): +class ComposerScripts(Horizontal): BORDER_TITLE = "Composer Scripts" def __init__(self, **kwargs): super().__init__(**kwargs) - - # @on(Button.Pressed) - # def on_button_pressed(self, event: Button.Pressed) -> None: - # # todo check button instance of ComposerScriptButton - # self.log(f'composer {event.button.script_name}') - # pass - -class ComposerScriptModal(ModalScreen): - BORDER_TITLE = "Composer script ?" - def __init__(self, script: str, **kwargs): - super().__init__(**kwargs) - self.script = script - - def compose(self): - with Container(): - yield Label(f"Running script {self.script} in a terminal") - yield Button.success("Close", id="composer_modal_close") - yield Terminal(command="htop", id="terminal_composer_script", default_colors="textual") - - def on_mount(self) -> None: - self.log('on ready') - terminal: Terminal = self.query_one("#terminal_composer_script") - terminal.start() - - @on(Button.Pressed, "#composer_modal_close") - def on_close(self, event: Button.Pressed) -> None: - self.app.pop_screen() diff --git a/src/presentation/terminal.py b/src/presentation/terminal.py new file mode 100644 index 0000000..61c4dad --- /dev/null +++ b/src/presentation/terminal.py @@ -0,0 +1,48 @@ +import subprocess + +from textual import on, work +from textual.containers import Container, Horizontal, Grid +from textual.screen import ModalScreen +from textual.widgets import Label, Button, RichLog + + +class TerminalModal(ModalScreen): + BORDER_TITLE = "Composer script ?" + TITLE = 'Terminal' + SUB_TITLE = 'Terminal Sub' + def __init__(self, command: str, path: str|list[str], **kwargs): + super().__init__(**kwargs) + self.command = command + self.path = path + + def compose(self): + with Container(): + with Horizontal(): + yield Label(f"Running: {" ".join(self.command)}") + yield Button("X", id="terminal_modal_close", variant="primary") + yield RichLog(id="terminal_command", highlight=True, markup=True) + + def on_mount(self) -> None: + self._start() + + + @work(exclusive=True, thread=True) + async def _start(self) -> None: + terminal: RichLog = self.query_one("#terminal_command") + terminal.write(f"[frame]Running command [italic red]{" ".join(self.command)}[/italic red][/frame]") + 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: + output = process.stdout.read().strip() + self.log(f"Sortie: {output}") + terminal.write(output) + terminal.write("[italic red]Completed![/italic red]") + + @on(Button.Pressed, "#terminal_modal_close") + def on_close(self, event: Button.Pressed) -> None: + self.app.pop_screen() diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss index f3f1be2..8d3160d 100644 --- a/src/tcss/layout.tcss +++ b/src/tcss/layout.tcss @@ -23,22 +23,42 @@ ComposerRequireTable { } ComposerScripts { - border-title-color: $accent; - border: $primary-background round; - content-align: center middle; +# border-title-color: $accent; +# border: $primary-background round; +# content-align: center middle; + height: auto; +# column-span: 2; } -ComposerScriptModal { - align: center middle; +ComposerScriptButton { + width: auto; } -ComposerScriptModal > Container { - padding: 1 2; + +# ##### +# Terminal Modal + +TerminalModal { + align: center middle; +} +TerminalModal > Container { background: $panel; width: 100; height: 50; } - -ComposerScriptModal > Container > Button { - margin: 1 0; -} \ No newline at end of file +TerminalModal > Container > Horizontal { + height: 1; + background: blue 20%; +} +TerminalModal > Container > Horizontal > Label { + padding: 0 1; + width: 97; +} +TerminalModal > Container> RichLog { + padding: 1 1; +} +#terminal_modal_close { + border: none; + min-width: 1; + height: 1; +} diff --git a/uv.lock b/uv.lock index 67dd469..8468d8f 100644 --- a/uv.lock +++ b/uv.lock @@ -346,7 +346,6 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "textual" }, - { name = "textual-terminal" }, { name = "typer" }, ] @@ -360,7 +359,6 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.9.2" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "textual", specifier = ">=0.83.0" }, - { name = "textual-terminal", specifier = ">=0.3.0" }, { name = "typer", specifier = ">=0.12.5" }, ] @@ -479,18 +477,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] -[[package]] -name = "pyte" -version = "0.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/ab/b599762933eba04de7dc5b31ae083112a6c9a9db15b01d3109ad797559d9/pyte-0.8.2.tar.gz", hash = "sha256:5af970e843fa96a97149d64e170c984721f20e52227a2f57f0a54207f08f083f", size = 92301 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/d0/bb522283b90853afbf506cd5b71c650cf708829914efd0003d615cf426cd/pyte-0.8.2-py3-none-any.whl", hash = "sha256:85db42a35798a5aafa96ac4d8da78b090b2c933248819157fc0e6f78876a0135", size = 31627 }, -] - [[package]] name = "python-dotenv" version = "1.0.1" @@ -570,19 +556,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/a9/01d35770fde8d889e1fe28b726188cf28801e57afd369c614cd2bc100ee4/textual_serve-1.1.1-py3-none-any.whl", hash = "sha256:568782f1c0e60e3f7039d9121e1cb5c2f4ca1aaf6d6bd7aeb833d5763a534cb2", size = 445034 }, ] -[[package]] -name = "textual-terminal" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyte" }, - { name = "textual" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/fe/94e6a50d388c9c8ce0da27f71ca5abecd99a15d4347b3d6dccc1dedfe76f/textual_terminal-0.3.0.tar.gz", hash = "sha256:594e7323b74d1e395cf3ce39216cef78cb7c64d01ea71a60ea7b0f9b198e4c87", size = 9482 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/3d/c9202997d383a317b14de28e2c7925b56a83edb0a74012ee39b86217850f/textual_terminal-0.3.0-py3-none-any.whl", hash = "sha256:f00b22f5a59ef6d60b61453d7a98354647487c00342d4b0324221db18c4a90f1", size = 12529 }, -] - [[package]] name = "typer" version = "0.12.5" @@ -616,15 +589,6 @@ 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 = "wcwidth" -version = "0.2.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, -] - [[package]] name = "yarl" version = "1.15.1" From a49f9b4145ff3ffc6d435df52dfe525e46237ee9 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Wed, 16 Oct 2024 18:53:26 +0200 Subject: [PATCH 06/14] Add locked version of packages --- src/main.py | 21 --------------------- src/models/composer.py | 36 +++++++++++++++++++++++++++--------- src/models/project.py | 2 +- src/presentation/__init__.py | 2 +- src/presentation/composer.py | 15 ++++++++------- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/main.py b/src/main.py index 4e79d0b..a3dd896 100644 --- a/src/main.py +++ b/src/main.py @@ -1,8 +1,5 @@ import typer from pydantic_core._pydantic_core import ValidationError -from textual.app import ComposeResult - -from composer import run_composer from models import Project from presentation import MainApp from settings import settings @@ -35,24 +32,6 @@ def debug(project_path: str) -> None: print(project.composer_json) - from textual_terminal import Terminal - - from textual.app import App - class TerminalApp(App): - def compose(self) -> ComposeResult: - #cd /home/jeckel/Workspace/10_Clients/Lamy-Liaisons/sf-ecustomer && composer cs-fix - yield Terminal(command="cd /home/jeckel/Workspace/10_Clients/Lamy-Liaisons/sf-ecustomer && ls", id="terminal_htop") - # yield Terminal(command="bash", id="terminal_bash") - - def on_ready(self) -> None: - terminal_htop: Terminal = self.query_one("#terminal_htop") - terminal_htop.start() - - # terminal_bash: Terminal = self.query_one("#terminal_bash") - # terminal_bash.start() - app = TerminalApp() - app.run() - def main() -> None: app(prog_name=settings.__app_name__) diff --git a/src/models/composer.py b/src/models/composer.py index 442c50e..fd8dd43 100644 --- a/src/models/composer.py +++ b/src/models/composer.py @@ -1,25 +1,43 @@ 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") - require: dict[str, str] - require_dev: dict[str, str] + 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, json_path: str): - with open(json_path, "r") as file: + 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) - require = data.pop("require", []) - require_dev = data.pop("require-dev", []) - # require = {package:Package(name=package, version=version) for package, version in data.pop("require", []).items()} - # require_dev = {package:Package(name=package, version=version) for package, version in data.pop("require-dev", []).items()} - return cls(require_dev=require_dev, require=require, **data) + 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]: diff --git a/src/models/project.py b/src/models/project.py index d81be6b..e2b807f 100644 --- a/src/models/project.py +++ b/src/models/project.py @@ -33,5 +33,5 @@ def check_composer_file(self): def composer_json(self) -> Optional[Composer]: if not self.composer: return - return Composer.from_json(os.path.join(self.path, 'composer.json')) + return Composer.from_json(self.path) diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index 715e0c1..f00ea58 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -41,7 +41,7 @@ def compose(self) -> ComposeResult: async def on_mount(self) -> None: table = self.query_one(ComposerRequireTable) - table.set_requirements(self._project.composer_json.require) + table.set_requirements(self._project.composer_json.required_packages, self._project.composer_json.locked_packages) scripts = self.query_one(ComposerScripts) for script in self._project.composer_json.manual_scripts: self.log(f"Bouton {script}") diff --git a/src/presentation/composer.py b/src/presentation/composer.py index 939cae4..16ec016 100644 --- a/src/presentation/composer.py +++ b/src/presentation/composer.py @@ -7,14 +7,17 @@ class ComposerRequireTable(DataTable): def __init__(self, title: str, **kwargs): super().__init__(**kwargs) self.border_title = title - self.add_columns(*('Package', 'Version')) + self.cursor_type = 'row' + self.add_columns(*('Package', 'Required', 'Locked')) - def set_requirements(self, requirements: dict[str, str]) -> None: - for package, version in requirements.items(): - styled_row = ( + def set_requirements(self, required_packages: dict[str, str], locked_packages: 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")) self.add_row(*styled_row) class ComposerScriptButton(Button): @@ -24,7 +27,5 @@ def __init__(self, script_name: str, **kwargs): self.script_name = script_name class ComposerScripts(Horizontal): - BORDER_TITLE = "Composer Scripts" - def __init__(self, **kwargs): super().__init__(**kwargs) From d9d39e804baedb5a7b19b79a9c27890dbbff6138 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Wed, 16 Oct 2024 19:21:17 +0200 Subject: [PATCH 07/14] Detect packages ready to upgrade --- src/composer.py | 31 ------------------------------- src/composer_utils.py | 35 +++++++++++++++++++++++++++++++++++ src/main.py | 3 +++ src/presentation/__init__.py | 7 ++++++- src/presentation/composer.py | 10 ++++++++-- src/presentation/terminal.py | 4 +++- 6 files changed, 55 insertions(+), 35 deletions(-) delete mode 100644 src/composer.py create mode 100644 src/composer_utils.py diff --git a/src/composer.py b/src/composer.py deleted file mode 100644 index 6d46491..0000000 --- a/src/composer.py +++ /dev/null @@ -1,31 +0,0 @@ -import subprocess -from typing import Optional, List - -from models import Project - - -def run_composer(command, project: Project, extra_args: Optional[List[str]] = None): - composer_command = ["composer", command] - if extra_args: - composer_command.extend(extra_args) - - result = subprocess.run(composer_command, cwd=project.path, capture_output=True, text=True) - print("stdout:", result.stdout) - print("stderr:", result.stderr) - - # - # with subprocess.Popen( - # composer_command, - # cwd=project.path, - # stdout=subprocess.PIPE, - # # stderr=subprocess.PIPE, - # # text=True - # ) as process: - # print(f"Sortie: {process.stdout.read().strip()}") - # - # stderr = process.communicate()[1] - # if stderr: - # print(f"Erreurs: {stderr}") - -# Exemple d'utilisation -# output = run_composer("install", working_directory="/path/to/your/project") \ No newline at end of file diff --git a/src/composer_utils.py b/src/composer_utils.py new file mode 100644 index 0000000..4b97140 --- /dev/null +++ b/src/composer_utils.py @@ -0,0 +1,35 @@ +import subprocess +from typing import Optional, List +from rich import print + +from models import Project + +def composer_updatable(project: Project) -> dict[str, str]: + with subprocess.Popen( + ['composer', 'update', '--dry-run'], + cwd=project.path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) as process: + # output = process.stdout.read().strip() + # print(f'Sortie {output}') + output = process.stderr.read().strip() + # print(f'SortieErr {output}') + + # Split the output into lines + lines = output.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 \ No newline at end of file diff --git a/src/main.py b/src/main.py index a3dd896..6a9de19 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,7 @@ import typer from pydantic_core._pydantic_core import ValidationError + +from composer_utils import composer_updatable from models import Project from presentation import MainApp from settings import settings @@ -31,6 +33,7 @@ def debug(project_path: str) -> None: exit(1) print(project.composer_json) + composer_updatable(project) def main() -> None: diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index f00ea58..e0571b6 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -4,6 +4,7 @@ from textual.containers import VerticalScroll, Container from textual.widgets import Header, Footer, Button, DataTable, Label +from composer_utils import composer_updatable from models import Project from .composer import ComposerRequireTable, ComposerScripts, ComposerScriptButton from .terminal import TerminalModal @@ -41,7 +42,11 @@ def compose(self) -> ComposeResult: async def on_mount(self) -> None: table = self.query_one(ComposerRequireTable) - table.set_requirements(self._project.composer_json.required_packages, self._project.composer_json.locked_packages) + packages_updatable = composer_updatable(self._project) + table.set_requirements( + self._project.composer_json.required_packages, + self._project.composer_json.locked_packages, + packages_updatable) scripts = self.query_one(ComposerScripts) for script in self._project.composer_json.manual_scripts: self.log(f"Bouton {script}") diff --git a/src/presentation/composer.py b/src/presentation/composer.py index 16ec016..77a4cf8 100644 --- a/src/presentation/composer.py +++ b/src/presentation/composer.py @@ -8,9 +8,9 @@ def __init__(self, title: str, **kwargs): super().__init__(**kwargs) self.border_title = title self.cursor_type = 'row' - self.add_columns(*('Package', 'Required', 'Locked')) + self.add_columns(*('Package', 'Required', 'Locked', 'Upgrade')) - def set_requirements(self, required_packages: dict[str, str], locked_packages: dict[str, str]) -> None: + 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"), @@ -18,6 +18,12 @@ def set_requirements(self, required_packages: dict[str, str], locked_packages: d ] 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) class ComposerScriptButton(Button): diff --git a/src/presentation/terminal.py b/src/presentation/terminal.py index 61c4dad..7654e47 100644 --- a/src/presentation/terminal.py +++ b/src/presentation/terminal.py @@ -39,8 +39,10 @@ async def _start(self) -> None: text=True ) as process: output = process.stdout.read().strip() - self.log(f"Sortie: {output}") terminal.write(output) + # stderr = process.stderr.read().strip() + # if stderr: + # terminal.write(f"[italic red]{stderr}[/italic red]") terminal.write("[italic red]Completed![/italic red]") @on(Button.Pressed, "#terminal_modal_close") From c9aa6dec9684dcf8f4cb1e8545f735292d6eb735 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Thu, 17 Oct 2024 11:10:01 +0200 Subject: [PATCH 08/14] Cleanup composer integration --- src/composer_utils.py | 4 +- src/presentation/__init__.py | 59 +++-------------- src/presentation/component/__init__.py | 1 + src/presentation/component/terminal_modal.py | 63 ++++++++++++++++++ src/presentation/composer/__init__.py | 3 + .../composer_packages_table.py} | 24 +++---- src/presentation/composer/composer_pan.py | 60 +++++++++++++++++ .../composer/composer_script_button.py | 10 +++ src/presentation/docker/__init__.py | 16 +++++ src/presentation/summary/__init__.py | 17 +++++ src/presentation/terminal.py | 50 -------------- src/tcss/layout.tcss | 65 +++++++++++-------- 12 files changed, 229 insertions(+), 143 deletions(-) create mode 100644 src/presentation/component/__init__.py create mode 100644 src/presentation/component/terminal_modal.py create mode 100644 src/presentation/composer/__init__.py rename src/presentation/{composer.py => composer/composer_packages_table.py} (65%) create mode 100644 src/presentation/composer/composer_pan.py create mode 100644 src/presentation/composer/composer_script_button.py create mode 100644 src/presentation/docker/__init__.py create mode 100644 src/presentation/summary/__init__.py delete mode 100644 src/presentation/terminal.py diff --git a/src/composer_utils.py b/src/composer_utils.py index 4b97140..32d07fb 100644 --- a/src/composer_utils.py +++ b/src/composer_utils.py @@ -4,10 +4,10 @@ from models import Project -def composer_updatable(project: Project) -> dict[str, str]: +def composer_updatable(path: str) -> dict[str, str]: with subprocess.Popen( ['composer', 'update', '--dry-run'], - cwd=project.path, + cwd=path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index e0571b6..7f539e4 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -1,20 +1,16 @@ -from rich.text import Text -from textual import on from textual.app import App, ComposeResult -from textual.containers import VerticalScroll, Container -from textual.widgets import Header, Footer, Button, DataTable, Label +from textual.widgets import TabbedContent, Header, Footer -from composer_utils import composer_updatable from models import Project -from .composer import ComposerRequireTable, ComposerScripts, ComposerScriptButton -from .terminal import TerminalModal +from .composer import ComposerPan +from .docker import DockerPan +from .summary import ProjectSummaryPan class MainApp(App): """A Textual app to manage stopwatches.""" - TITLE = "Project Manager" - # SCREENS = {"get_pocket": GetPocketScreen} + TITLE = "DX Companion" BINDINGS = [ ("d", "toggle_dark", "Toggle dark mode"), ] @@ -25,47 +21,10 @@ def __init__(self, project: Project): self._project = project super().__init__() - # BINDINGS = [("b", "", "GetPocketScreen")] - def compose(self) -> ComposeResult: - """Create child widgets for the app.""" yield Header() - yield ComposerScripts(id="composer_scripts") - with Container(id="project_container"): - with Container(id="project_summary"): - yield Label(Text(str("Project :"), style="italic #03AC13", justify="right")) - yield Label(Text(str(self._project.name), style="italic")) - # yield ComposerScripts(id="composer_scripts") - yield ComposerRequireTable(title="Composer requirements", id="composer_table") - + with TabbedContent(initial="summary-pan"): + yield ProjectSummaryPan(project=self._project) + yield ComposerPan(composer_dir=self._project.path) + yield DockerPan(project=self._project) yield Footer() - - async def on_mount(self) -> None: - table = self.query_one(ComposerRequireTable) - packages_updatable = composer_updatable(self._project) - table.set_requirements( - self._project.composer_json.required_packages, - self._project.composer_json.locked_packages, - packages_updatable) - scripts = self.query_one(ComposerScripts) - for script in self._project.composer_json.manual_scripts: - self.log(f"Bouton {script}") - new_button = ComposerScriptButton(script_name=script) - await scripts.mount(new_button) - - - def action_toggle_dark(self) -> None: - """An action to toggle dark mode.""" - self.dark = not self.dark - - @on(Button.Pressed) - def on_pressed(self, event: Button.Pressed) -> None: - if isinstance(event.button, ComposerScriptButton): - # command = f"cd {self._project.path} && composer {event.button.script_name}" - # self.log(command) - self.push_screen(TerminalModal(command=['composer', event.button.script_name], path=self._project.path)) - # self.push_screen(TerminalModal(command='ls -lah', path=self._project.path)) - # else: - # self.pop_screen() - - diff --git a/src/presentation/component/__init__.py b/src/presentation/component/__init__.py new file mode 100644 index 0000000..bb414a0 --- /dev/null +++ b/src/presentation/component/__init__.py @@ -0,0 +1 @@ +from .terminal_modal import TerminalModal \ No newline at end of file diff --git a/src/presentation/component/terminal_modal.py b/src/presentation/component/terminal_modal.py new file mode 100644 index 0000000..f50ee0d --- /dev/null +++ b/src/presentation/component/terminal_modal.py @@ -0,0 +1,63 @@ +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 Static, Button, RichLog + + +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() \ No newline at end of file diff --git a/src/presentation/composer/__init__.py b/src/presentation/composer/__init__.py new file mode 100644 index 0000000..e2ec0f6 --- /dev/null +++ b/src/presentation/composer/__init__.py @@ -0,0 +1,3 @@ +from .composer_script_button import ComposerScriptButton +from .composer_packages_table import ComposerPackagesTable +from .composer_pan import ComposerPan \ No newline at end of file diff --git a/src/presentation/composer.py b/src/presentation/composer/composer_packages_table.py similarity index 65% rename from src/presentation/composer.py rename to src/presentation/composer/composer_packages_table.py index 77a4cf8..21a38a7 100644 --- a/src/presentation/composer.py +++ b/src/presentation/composer/composer_packages_table.py @@ -1,9 +1,15 @@ -from textual.containers import Container, VerticalScroll, Horizontal from rich.text import Text -from textual.widgets import DataTable, Button, Label, RichLog +from textual.widgets import DataTable -class ComposerRequireTable(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 @@ -24,14 +30,4 @@ def set_requirements(self, required_packages: dict[str, str], locked_packages: d styled_row.append(Text(str(packages_updatable[package]), style="italic #00FF00", justify="right")) else: styled_row.append("") - self.add_row(*styled_row) - -class ComposerScriptButton(Button): - def __init__(self, script_name: str, **kwargs): - self.script_name = script_name - super().__init__(script_name, id=f"composer-button-{script_name}", **kwargs) - self.script_name = script_name - -class ComposerScripts(Horizontal): - def __init__(self, **kwargs): - super().__init__(**kwargs) + self.add_row(*styled_row) \ No newline at end of file diff --git a/src/presentation/composer/composer_pan.py b/src/presentation/composer/composer_pan.py new file mode 100644 index 0000000..ee880e1 --- /dev/null +++ b/src/presentation/composer/composer_pan.py @@ -0,0 +1,60 @@ +from textual import work, on +from textual.app import ComposeResult +from textual.containers import Container, Horizontal +from textual.widgets import TabPane +from textual.worker import Worker, WorkerState + +from composer_utils import composer_updatable +from models.composer import Composer +from . import ComposerScriptButton, ComposerPackagesTable +from presentation.component import TerminalModal + + +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 + table : ComposerPackagesTable = self.query_one("#composer-packages-table") + table.set_requirements(self.composer.required_packages, self.composer.locked_packages, packages_updatable) + table : ComposerPackagesTable = self.query_one("#composer-packages-dev-table") + 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: ComposerScriptButton.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)) \ No newline at end of file diff --git a/src/presentation/composer/composer_script_button.py b/src/presentation/composer/composer_script_button.py new file mode 100644 index 0000000..f0352d8 --- /dev/null +++ b/src/presentation/composer/composer_script_button.py @@ -0,0 +1,10 @@ +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..4bcd9be --- /dev/null +++ b/src/presentation/docker/__init__.py @@ -0,0 +1,16 @@ +from rich.text import Text +from textual.app import ComposeResult +from textual.containers import Container +from textual.widgets import TabPane, Label + +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..0ffe17c --- /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 TabPane, Label + +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")) \ No newline at end of file diff --git a/src/presentation/terminal.py b/src/presentation/terminal.py deleted file mode 100644 index 7654e47..0000000 --- a/src/presentation/terminal.py +++ /dev/null @@ -1,50 +0,0 @@ -import subprocess - -from textual import on, work -from textual.containers import Container, Horizontal, Grid -from textual.screen import ModalScreen -from textual.widgets import Label, Button, RichLog - - -class TerminalModal(ModalScreen): - BORDER_TITLE = "Composer script ?" - TITLE = 'Terminal' - SUB_TITLE = 'Terminal Sub' - def __init__(self, command: str, path: str|list[str], **kwargs): - super().__init__(**kwargs) - self.command = command - self.path = path - - def compose(self): - with Container(): - with Horizontal(): - yield Label(f"Running: {" ".join(self.command)}") - yield Button("X", id="terminal_modal_close", variant="primary") - yield RichLog(id="terminal_command", highlight=True, markup=True) - - def on_mount(self) -> None: - self._start() - - - @work(exclusive=True, thread=True) - async def _start(self) -> None: - terminal: RichLog = self.query_one("#terminal_command") - terminal.write(f"[frame]Running command [italic red]{" ".join(self.command)}[/italic red][/frame]") - 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: - output = process.stdout.read().strip() - terminal.write(output) - # stderr = process.stderr.read().strip() - # if stderr: - # terminal.write(f"[italic red]{stderr}[/italic red]") - terminal.write("[italic red]Completed![/italic red]") - - @on(Button.Pressed, "#terminal_modal_close") - def on_close(self, event: Button.Pressed) -> None: - self.app.pop_screen() diff --git a/src/tcss/layout.tcss b/src/tcss/layout.tcss index 8d3160d..b9ec1e0 100644 --- a/src/tcss/layout.tcss +++ b/src/tcss/layout.tcss @@ -15,19 +15,24 @@ } } -ComposerRequireTable { - # padding: 1 1; - border-title-color: $accent; - border: $primary-background round; - content-align: center middle; -} +ComposerPan { + Container { + layout: grid; + grid-size: 1 3; + + ComposerPackagesTable { + border-title-color: $accent; + border: $primary-background round; + content-align: center middle; + } -ComposerScripts { -# border-title-color: $accent; -# border: $primary-background round; -# content-align: center middle; - height: auto; -# column-span: 2; + #composer-packages-table { + row-span: 2; + } + } + #composer-actions { + height: 3; + } } ComposerScriptButton { @@ -39,17 +44,27 @@ ComposerScriptButton { # Terminal Modal TerminalModal { - align: center middle; -} -TerminalModal > Container { - background: $panel; - width: 100; - height: 50; -} -TerminalModal > Container > Horizontal { - height: 1; - background: blue 20%; + 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; @@ -57,8 +72,4 @@ TerminalModal > Container > Horizontal > Label { TerminalModal > Container> RichLog { padding: 1 1; } -#terminal_modal_close { - border: none; - min-width: 1; - height: 1; -} + From f6449edc9d6daa85266a1966e327038f4cca9c81 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Thu, 17 Oct 2024 11:14:06 +0200 Subject: [PATCH 09/14] Update github action --- .github/workflows/pylint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 17c602e..dd6c476 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,7 +7,7 @@ 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 }} @@ -20,4 +20,4 @@ jobs: pip install pylint - name: Analysing the code with pylint run: | - pylint $(git ls-files '*.py') + cd src && pylint $(git ls-files '*.py') From 8c20048ae6806da4fd0e1c3d91b244284e4077db Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Thu, 17 Oct 2024 16:38:07 +0200 Subject: [PATCH 10/14] Fix code style --- .pre-commit-config.yaml | 29 +++++ Makefile | 5 +- README.md | 2 +- pyproject.toml | 1 + src/composer_utils.py | 29 +++-- src/main.py | 7 +- src/models/__init__.py | 2 +- src/models/composer.py | 20 +++- src/models/project.py | 9 +- src/presentation/component/__init__.py | 2 +- src/presentation/component/terminal_modal.py | 37 +++--- src/presentation/composer/__init__.py | 4 +- .../composer/composer_packages_table.py | 32 ++++-- src/presentation/composer/composer_pan.py | 44 ++++--- .../composer/composer_script_button.py | 1 + src/presentation/docker/__init__.py | 6 +- src/presentation/summary/__init__.py | 4 +- src/settings.py | 9 +- src/tcss/layout.tcss | 1 - uv.lock | 107 +++++++++++++++++- 20 files changed, 271 insertions(+), 80 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..17ea2b8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.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 + + - 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/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + files: src/ diff --git a/Makefile b/Makefile index e429656..ff7c697 100644 --- a/Makefile +++ b/Makefile @@ -14,4 +14,7 @@ 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 + @uv run pre-commit run --all-files diff --git a/README.md b/README.md index ed1bbc2..2512791 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ - composer update - composer outdated - composer actions -- \ No newline at end of file +- 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 index 32d07fb..36c722d 100644 --- a/src/composer_utils.py +++ b/src/composer_utils.py @@ -1,16 +1,17 @@ import subprocess -from typing import Optional, List + from rich import print from models import Project + 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 + ["composer", "update", "--dry-run"], + cwd=path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, ) as process: # output = process.stdout.read().strip() # print(f'Sortie {output}') @@ -18,18 +19,22 @@ def composer_updatable(path: str) -> dict[str, str]: # print(f'SortieErr {output}') # Split the output into lines - lines = output.strip().split('\n') + lines = output.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 + 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 \ No newline at end of file + return packages diff --git a/src/main.py b/src/main.py index 6a9de19..85acc6d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,15 +1,15 @@ 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 -from rich import print - app = typer.Typer() + @app.command() def tui(project_path: str) -> None: try: @@ -23,6 +23,7 @@ def tui(project_path: str) -> None: app = MainApp(project) app.run() + @app.command() def debug(project_path: str) -> None: try: @@ -41,4 +42,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/models/__init__.py b/src/models/__init__.py index 3ca442d..5ce17c6 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1 +1 @@ -from .project import Project \ No newline at end of file +from .project import Project diff --git a/src/models/composer.py b/src/models/composer.py index fd8dd43..1c224db 100644 --- a/src/models/composer.py +++ b/src/models/composer.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, Field from rich import print + class Composer(BaseModel): project_path: str type: str @@ -14,22 +15,28 @@ class Composer(BaseModel): 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]] + 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: + 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') + 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", [])} + 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, @@ -37,7 +44,8 @@ def from_json(cls, project_path: str): required_packages=required_packages, locked_packages=locked_packages, locked_packages_dev=locked_packages_dev, - **data) + **data + ) @property def manual_scripts(self) -> list[str]: diff --git a/src/models/project.py b/src/models/project.py index e2b807f..fc763e1 100644 --- a/src/models/project.py +++ b/src/models/project.py @@ -1,8 +1,8 @@ import os from functools import cached_property +from typing import Optional from pydantic import BaseModel, Field, field_validator, model_validator -from typing import Optional from models.composer import Composer @@ -15,16 +15,16 @@ class Project(BaseModel): def name(self) -> str: return os.path.basename(self.path) - @field_validator('path', mode='before') + @field_validator("path", mode="before") def check_directory_exists(cls, v): if not os.path.isdir(v): raise ValueError(f"Provided path '{v}' is not a valid directory.") return v - @model_validator(mode='after') + @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') + composer_file = os.path.join(self.path, "composer.json") if os.path.exists(composer_file): self.composer = True return self @@ -34,4 +34,3 @@ def composer_json(self) -> Optional[Composer]: if not self.composer: return return Composer.from_json(self.path) - diff --git a/src/presentation/component/__init__.py b/src/presentation/component/__init__.py index bb414a0..9141791 100644 --- a/src/presentation/component/__init__.py +++ b/src/presentation/component/__init__.py @@ -1 +1 @@ -from .terminal_modal import TerminalModal \ No newline at end of file +from .terminal_modal import TerminalModal diff --git a/src/presentation/component/terminal_modal.py b/src/presentation/component/terminal_modal.py index f50ee0d..a95de95 100644 --- a/src/presentation/component/terminal_modal.py +++ b/src/presentation/component/terminal_modal.py @@ -4,23 +4,24 @@ from textual.app import ComposeResult from textual.containers import Container, Horizontal from textual.screen import ModalScreen -from textual.widgets import Static, Button, RichLog +from textual.widgets import Button, RichLog, Static class TerminalModal(ModalScreen): def __init__( - self, - command: str|list[str], - path: str, - use_stderr: bool = False, - **kwargs + 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") + self.terminal = RichLog( + id="terminal_command", + highlight=True, + markup=True, + classes="modal_container", + ) def compose(self) -> ComposeResult: with Container(id="modal_container"): @@ -37,21 +38,27 @@ def on_mount(self) -> None: 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.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 + 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( + "----------------------------------------------------------------", + shrink=True, + ) self.terminal.write(f"Return code [bold]{process.returncode}[/bold]") if process.returncode == 0: self.terminal.write("[bold green]Completed![/bold green]") @@ -60,4 +67,4 @@ async def _start(self) -> None: @on(Button.Pressed, "#modal_close") def on_close(self, event: Button.Pressed) -> None: - self.app.pop_screen() \ No newline at end of file + self.app.pop_screen() diff --git a/src/presentation/composer/__init__.py b/src/presentation/composer/__init__.py index e2ec0f6..ca038c1 100644 --- a/src/presentation/composer/__init__.py +++ b/src/presentation/composer/__init__.py @@ -1,3 +1,3 @@ -from .composer_script_button import ComposerScriptButton from .composer_packages_table import ComposerPackagesTable -from .composer_pan import ComposerPan \ No newline at end of file +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 index 21a38a7..af69c6a 100644 --- a/src/presentation/composer/composer_packages_table.py +++ b/src/presentation/composer/composer_packages_table.py @@ -10,24 +10,42 @@ class ComposerPackagesTable(DataTable): - 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')) + 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: + 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") + 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")) + 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")) + styled_row.append( + Text( + str(packages_updatable[package]), + style="italic #00FF00", + justify="right", + ) + ) else: styled_row.append("") - self.add_row(*styled_row) \ No newline at end of file + self.add_row(*styled_row) diff --git a/src/presentation/composer/composer_pan.py b/src/presentation/composer/composer_pan.py index ee880e1..d9bc424 100644 --- a/src/presentation/composer/composer_pan.py +++ b/src/presentation/composer/composer_pan.py @@ -1,4 +1,4 @@ -from textual import work, on +from textual import on, work from textual.app import ComposeResult from textual.containers import Container, Horizontal from textual.widgets import TabPane @@ -6,9 +6,10 @@ from composer_utils import composer_updatable from models.composer import Composer -from . import ComposerScriptButton, ComposerPackagesTable from presentation.component import TerminalModal +from . import ComposerPackagesTable, ComposerScriptButton + class ComposerPan(TabPane): def __init__(self, composer_dir: str, **kwargs): @@ -16,34 +17,44 @@ def __init__(self, composer_dir: str, **kwargs): 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 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 - table : ComposerPackagesTable = self.query_one("#composer-packages-table") - table.set_requirements(self.composer.required_packages, self.composer.locked_packages, packages_updatable) - table : ComposerPackagesTable = self.query_one("#composer-packages-dev-table") - table.set_requirements(self.composer.required_packages_dev, self.composer.locked_packages_dev, packages_updatable) + table: ComposerPackagesTable = self.query_one("#composer-packages-table") + table.set_requirements( + self.composer.required_packages, + self.composer.locked_packages, + packages_updatable, + ) + table: ComposerPackagesTable = self.query_one( + "#composer-packages-dev-table" + ) + 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: @@ -53,8 +64,13 @@ async def refresh_listview(self, event: Worker.StateChanged) -> None: self.loading = False - @on(ComposerScriptButton.Pressed) def on_pressed(self, event: ComposerScriptButton.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)) \ No newline at end of file + 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 index f0352d8..9d232df 100644 --- a/src/presentation/composer/composer_script_button.py +++ b/src/presentation/composer/composer_script_button.py @@ -5,6 +5,7 @@ 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 index 4bcd9be..a9fd013 100644 --- a/src/presentation/docker/__init__.py +++ b/src/presentation/docker/__init__.py @@ -1,7 +1,7 @@ from rich.text import Text from textual.app import ComposeResult from textual.containers import Container -from textual.widgets import TabPane, Label +from textual.widgets import Label, TabPane from models import Project @@ -13,4 +13,6 @@ def __init__(self, project: Project, **kwargs): def compose(self) -> ComposeResult: with Container(id="project_docker"): - yield Label(Text(str("Work in progress"), style="italic #03AC13", justify="right")) + 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 index 0ffe17c..341d202 100644 --- a/src/presentation/summary/__init__.py +++ b/src/presentation/summary/__init__.py @@ -1,7 +1,7 @@ from rich.text import Text from textual.app import ComposeResult from textual.containers import Container -from textual.widgets import TabPane, Label +from textual.widgets import Label, TabPane from models import Project @@ -14,4 +14,4 @@ def __init__(self, project: Project, **kwargs): 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")) \ No newline at end of file + 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 index b9ec1e0..c977c04 100644 --- a/src/tcss/layout.tcss +++ b/src/tcss/layout.tcss @@ -72,4 +72,3 @@ TerminalModal > Container > Horizontal > Label { 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" From 881452a2a6c1b7bd9711e625b62eb0409653a6cb Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Thu, 17 Oct 2024 16:47:23 +0200 Subject: [PATCH 11/14] Fix flake8 --- .pre-commit-config.yaml | 19 +++++++++++++++---- src/composer_utils.py | 8 -------- src/presentation/__init__.py | 3 ++- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17ea2b8..a48da19 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,14 +2,16 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace - repo: https://github.com/psf/black rev: 22.10.0 hooks: - - id: black + - id: black + files: src/ + types: [file, python] - repo: https://github.com/myint/autoflake rev: v2.3.1 @@ -27,3 +29,12 @@ repos: - id: isort name: isort (python) 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"] diff --git a/src/composer_utils.py b/src/composer_utils.py index 36c722d..77454d7 100644 --- a/src/composer_utils.py +++ b/src/composer_utils.py @@ -1,9 +1,5 @@ import subprocess -from rich import print - -from models import Project - def composer_updatable(path: str) -> dict[str, str]: with subprocess.Popen( @@ -13,11 +9,7 @@ def composer_updatable(path: str) -> dict[str, str]: stderr=subprocess.PIPE, text=True, ) as process: - # output = process.stdout.read().strip() - # print(f'Sortie {output}') output = process.stderr.read().strip() - # print(f'SortieErr {output}') - # Split the output into lines lines = output.strip().split("\n") packages: dict[str, str] = {} diff --git a/src/presentation/__init__.py b/src/presentation/__init__.py index 7f539e4..cf981cf 100644 --- a/src/presentation/__init__.py +++ b/src/presentation/__init__.py @@ -1,7 +1,8 @@ from textual.app import App, ComposeResult -from textual.widgets import TabbedContent, Header, Footer +from textual.widgets import Footer, Header, TabbedContent from models import Project + from .composer import ComposerPan from .docker import DockerPan from .summary import ProjectSummaryPan From c1c2a46ff695a0e2f9bba9d24e07a19bbb8b8886 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Thu, 17 Oct 2024 16:54:33 +0200 Subject: [PATCH 12/14] Fix MyPy --- .pre-commit-config.yaml | 8 ++++++++ src/composer_utils.py | 5 ++--- src/models/project.py | 4 ++-- src/presentation/composer/composer_pan.py | 14 ++++++++------ 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a48da19..8395377 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,3 +38,11 @@ repos: 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] +# args: [--config-file=./.styleconfigs/mypy.ini] diff --git a/src/composer_utils.py b/src/composer_utils.py index 77454d7..499193f 100644 --- a/src/composer_utils.py +++ b/src/composer_utils.py @@ -9,9 +9,8 @@ def composer_updatable(path: str) -> dict[str, str]: stderr=subprocess.PIPE, text=True, ) as process: - output = process.stderr.read().strip() - # Split the output into lines - lines = output.strip().split("\n") + stdout, stderr = process.communicate() + lines = stdout.strip().split("\n") packages: dict[str, str] = {} # Processing lines for packages diff --git a/src/models/project.py b/src/models/project.py index fc763e1..aa3fd87 100644 --- a/src/models/project.py +++ b/src/models/project.py @@ -16,7 +16,7 @@ def name(self) -> str: return os.path.basename(self.path) @field_validator("path", mode="before") - def check_directory_exists(cls, v): + 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 @@ -32,5 +32,5 @@ def check_composer_file(self): @cached_property def composer_json(self) -> Optional[Composer]: if not self.composer: - return + return None return Composer.from_json(self.path) diff --git a/src/presentation/composer/composer_pan.py b/src/presentation/composer/composer_pan.py index d9bc424..f2093ed 100644 --- a/src/presentation/composer/composer_pan.py +++ b/src/presentation/composer/composer_pan.py @@ -1,7 +1,7 @@ from textual import on, work from textual.app import ComposeResult from textual.containers import Container, Horizontal -from textual.widgets import TabPane +from textual.widgets import Button, TabPane from textual.worker import Worker, WorkerState from composer_utils import composer_updatable @@ -41,16 +41,18 @@ 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 - table: ComposerPackagesTable = self.query_one("#composer-packages-table") - table.set_requirements( + package_table: ComposerPackagesTable = self.query_one( + "#composer-packages-table" + ) + package_table.set_requirements( self.composer.required_packages, self.composer.locked_packages, packages_updatable, ) - table: ComposerPackagesTable = self.query_one( + package_dev_table: ComposerPackagesTable = self.query_one( "#composer-packages-dev-table" ) - table.set_requirements( + package_dev_table.set_requirements( self.composer.required_packages_dev, self.composer.locked_packages_dev, packages_updatable, @@ -65,7 +67,7 @@ async def refresh_listview(self, event: Worker.StateChanged) -> None: self.loading = False @on(ComposerScriptButton.Pressed) - def on_pressed(self, event: ComposerScriptButton.Pressed) -> None: + def on_pressed(self, event: Button.Pressed) -> None: if isinstance(event.button, ComposerScriptButton): self.app.push_screen( TerminalModal( From 15d61c3fd799cdd128a60f6866e075ad5bc2bf8e Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Thu, 17 Oct 2024 16:57:09 +0200 Subject: [PATCH 13/14] Fix github action --- .github/workflows/pylint.yml | 9 ++------- .pre-commit-config.yaml | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index dd6c476..e2e5d3b 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -14,10 +14,5 @@ jobs: 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: | - cd src && 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 index 8395377..d357bd7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer From 45ccca88c79dec3459e720224434b0f2f0fa6c05 Mon Sep 17 00:00:00 2001 From: Julien Mercier-Rojas Date: Thu, 17 Oct 2024 17:03:05 +0200 Subject: [PATCH 14/14] Remove obsolete config --- .pre-commit-config.yaml | 9 --------- Makefile | 7 +++++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d357bd7..e3fdd14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,14 +23,6 @@ repos: files: src/ types: [file, python] - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - name: isort (python) - files: src/ - types: [ file, python ] - - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: @@ -45,4 +37,3 @@ repos: - id: mypy files: src/ types: [file, python] -# args: [--config-file=./.styleconfigs/mypy.ini] diff --git a/Makefile b/Makefile index ff7c697..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 \ @@ -16,5 +16,8 @@ console-quiet: .uv @uv run textual console -x EVENT #@uv run textual console -x SYSTEM -x EVENT -x DEBUG -x INFO -pre-commit: .uv +pre-commit: .uv install @uv run pre-commit run --all-files + +install: .uv + @uv run pre-commit install