youca-setup 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. youca_setup/__about__.py +9 -0
  2. youca_setup/__init__.py +16 -0
  3. youca_setup/cli.py +143 -0
  4. youca_setup/commands/__init__.py +21 -0
  5. youca_setup/commands/_port_utils.py +48 -0
  6. youca_setup/commands/_state.py +33 -0
  7. youca_setup/commands/backup.py +47 -0
  8. youca_setup/commands/doctor.py +82 -0
  9. youca_setup/commands/export.py +43 -0
  10. youca_setup/commands/import_cmd.py +61 -0
  11. youca_setup/commands/info.py +42 -0
  12. youca_setup/commands/init.py +318 -0
  13. youca_setup/commands/logs.py +43 -0
  14. youca_setup/commands/module.py +63 -0
  15. youca_setup/commands/projects.py +71 -0
  16. youca_setup/commands/remove.py +87 -0
  17. youca_setup/commands/restart.py +41 -0
  18. youca_setup/commands/restore.py +49 -0
  19. youca_setup/commands/shell.py +84 -0
  20. youca_setup/commands/start.py +48 -0
  21. youca_setup/commands/status.py +53 -0
  22. youca_setup/commands/stop.py +51 -0
  23. youca_setup/diagnostics/__init__.py +8 -0
  24. youca_setup/diagnostics/checks.py +241 -0
  25. youca_setup/diagnostics/fixer.py +131 -0
  26. youca_setup/docker/__init__.py +8 -0
  27. youca_setup/docker/compose.py +155 -0
  28. youca_setup/docker/manager.py +246 -0
  29. youca_setup/postgres/__init__.py +7 -0
  30. youca_setup/postgres/manager.py +183 -0
  31. youca_setup/project/__init__.py +13 -0
  32. youca_setup/project/backup.py +351 -0
  33. youca_setup/project/config.py +127 -0
  34. youca_setup/project/generator.py +146 -0
  35. youca_setup/project/loader.py +30 -0
  36. youca_setup/project/odoo_module.py +167 -0
  37. youca_setup/project/registry.py +69 -0
  38. youca_setup/project/templates/.gitignore.j2 +18 -0
  39. youca_setup/project/templates/README.md.j2 +27 -0
  40. youca_setup/project/templates/__init__.py +1 -0
  41. youca_setup/project/templates/docker-compose.yml.j2 +43 -0
  42. youca_setup/project/templates/env.j2 +6 -0
  43. youca_setup/project/templates/modules/__manifest__.py.j2 +14 -0
  44. youca_setup/project/templates/modules/models/__init__.py.j2 +1 -0
  45. youca_setup/project/templates/modules/models/model.py.j2 +10 -0
  46. youca_setup/project/templates/modules/security/ir.model.access.csv.j2 +2 -0
  47. youca_setup/project/templates/modules/tests/__init__.py.j2 +1 -0
  48. youca_setup/project/templates/modules/tests/test_model.py.j2 +7 -0
  49. youca_setup/project/templates/modules/views/model_views.xml.j2 +32 -0
  50. youca_setup/project/templates/odoo.conf.j2 +13 -0
  51. youca_setup/project/transport.py +107 -0
  52. youca_setup/utils/__init__.py +16 -0
  53. youca_setup/utils/filesystem.py +48 -0
  54. youca_setup/utils/ports.py +84 -0
  55. youca_setup/utils/secrets.py +20 -0
  56. youca_setup/utils/system.py +89 -0
  57. youca_setup/utils/tar.py +32 -0
  58. youca_setup-0.1.0.dist-info/METADATA +135 -0
  59. youca_setup-0.1.0.dist-info/RECORD +62 -0
  60. youca_setup-0.1.0.dist-info/WHEEL +4 -0
  61. youca_setup-0.1.0.dist-info/entry_points.txt +2 -0
  62. youca_setup-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,9 @@
1
+ """Metadata about the Youca Setup distribution."""
2
+
3
+ __version__ = "0.1.0"
4
+ __title__ = "youca-setup"
5
+ __summary__ = (
6
+ "Youca Setup for Odoo - CLI tool to create, configure and manage Odoo development environments."
7
+ )
8
+ __license__ = "MIT"
9
+ __uri__ = "https://github.com/Fitiafenohaja/youca-setup"
@@ -0,0 +1,16 @@
1
+ """Youca Setup for Odoo - CLI tool to create, configure and manage Odoo development environments.
2
+
3
+ Youca Setup orchestrates existing tools (Docker, PostgreSQL, Git) to bootstrap and
4
+ maintain an Odoo development environment with a minimal set of commands:
5
+
6
+ youca-setup init my-project
7
+ youca-setup start
8
+ youca-setup stop
9
+ youca-setup doctor
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from youca_setup.__about__ import __version__
15
+
16
+ __all__ = ["__version__"]
youca_setup/cli.py ADDED
@@ -0,0 +1,143 @@
1
+ """Main CLI entry point for Youca Setup.
2
+
3
+ Builds the Typer application, registers every subcommand from the
4
+ :mod:`youca_setup.commands` package and wires the Rich error rendering.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Annotated
10
+
11
+ import rich
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.traceback import Traceback
15
+
16
+ from youca_setup.__about__ import __title__, __version__
17
+
18
+ APP_NAME = __title__
19
+ APP_VERSION = __version__
20
+
21
+ _VERBOSE = False
22
+
23
+ app = typer.Typer(
24
+ name=APP_NAME,
25
+ help="Youca Setup for Odoo — create, configure and manage Odoo development environments.",
26
+ no_args_is_help=True,
27
+ pretty_exceptions_show_locals=False,
28
+ pretty_exceptions_short=True,
29
+ add_completion=True,
30
+ )
31
+
32
+ console = Console(stderr=True)
33
+
34
+
35
+ def _version_callback(value: bool) -> None:
36
+ """Print the current version and exit when ``--version`` is given."""
37
+ if value:
38
+ console.print(f"{APP_NAME} version {APP_VERSION}")
39
+ raise typer.Exit()
40
+
41
+
42
+ @app.callback()
43
+ def main(
44
+ ctx: typer.Context,
45
+ verbose: Annotated[
46
+ bool,
47
+ typer.Option(
48
+ "--verbose", "-v", help="Enable verbose mode: print full tracebacks on errors."
49
+ ),
50
+ ] = False,
51
+ version: Annotated[
52
+ bool,
53
+ typer.Option(
54
+ "--version",
55
+ help="Show the version and exit.",
56
+ callback=_version_callback,
57
+ is_eager=True,
58
+ ),
59
+ ] = False,
60
+ ) -> None:
61
+ """CLI entry point.
62
+
63
+ Args:
64
+ ctx: Typer context shared across subcommands.
65
+ verbose: Whether to display full tracebacks on errors.
66
+ version: Whether the ``--version`` flag was passed.
67
+ """
68
+ ctx.obj = {"verbose": verbose}
69
+ global _VERBOSE
70
+ _VERBOSE = verbose
71
+
72
+
73
+ def run() -> None:
74
+ """Run the CLI and render any uncaught error with Rich.
75
+
76
+ In debug mode (``--verbose``) the full traceback is shown; otherwise a
77
+ concise, actionable error message is printed.
78
+ """
79
+ try:
80
+ app()
81
+ except typer.Exit:
82
+ raise
83
+ except KeyboardInterrupt:
84
+ console.print("\n[bold yellow]Aborted by user.[/]")
85
+ raise typer.Exit(130) from None
86
+ except Exception as exc: # noqa: BLE001 - top-level safety net.
87
+ if _VERBOSE:
88
+ console.print(Traceback(show_locals=False, suppress=[typer, rich]))
89
+ else:
90
+ console.print(f"[bold red]Error:[/] {exc}")
91
+ console.print("Re-run with [bold]--verbose[/] for a full traceback.")
92
+ raise typer.Exit(1) from exc
93
+
94
+
95
+ def _register_commands() -> None:
96
+ """Register every subcommand on the CLI.
97
+
98
+ Single-command modules register their function directly as a top-level
99
+ command (``youca-setup start``, ``youca-setup info``, ...). The ``module`` group is
100
+ the only one with real subcommands and stays nested
101
+ (``youca-setup module create ...``).
102
+ """
103
+ from youca_setup.commands import (
104
+ backup,
105
+ doctor,
106
+ export,
107
+ import_cmd,
108
+ info,
109
+ init,
110
+ logs,
111
+ module,
112
+ projects,
113
+ remove,
114
+ restart,
115
+ restore,
116
+ shell,
117
+ start,
118
+ status,
119
+ stop,
120
+ )
121
+
122
+ app.command(name="init")(init.init)
123
+ app.command(name="start")(start.start)
124
+ app.command(name="stop")(stop.stop)
125
+ app.command(name="restart")(restart.restart)
126
+ app.command(name="status")(status.status)
127
+ app.command(name="logs")(logs.logs)
128
+ app.command(name="info")(info.info)
129
+ app.command(name="projects")(projects.projects)
130
+ app.command(name="remove")(remove.remove)
131
+ app.command(name="doctor")(doctor.doctor)
132
+ app.add_typer(module.app, name="module")
133
+ app.command(name="backup")(backup.backup)
134
+ app.command(name="restore")(restore.restore)
135
+ app.command(name="shell")(shell.shell)
136
+ app.command(name="export")(export.export)
137
+ app.command(name="import")(import_cmd.import_env)
138
+
139
+
140
+ _register_commands()
141
+
142
+ if __name__ == "__main__":
143
+ run()
@@ -0,0 +1,21 @@
1
+ """Youca Setup CLI subcommands.
2
+
3
+ Each subcommand lives in its own module and exposes a ``typer.Typer``
4
+ instance named ``app``. The main application in :mod:`youca_setup.cli` registers
5
+ them all at import time.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__ = [
11
+ "init",
12
+ "start",
13
+ "stop",
14
+ "restart",
15
+ "status",
16
+ "logs",
17
+ "doctor",
18
+ "module",
19
+ "backup",
20
+ "restore",
21
+ ]
@@ -0,0 +1,48 @@
1
+ """Shared helpers for the lifecycle commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.prompt import Confirm
10
+
11
+ from youca_setup.docker.compose import ComposeProject
12
+ from youca_setup.project.config import ProjectConfig
13
+ from youca_setup.utils.ports import find_free_port, is_port_free
14
+
15
+ console = Console()
16
+
17
+
18
+ def resolve_port_conflict(config: ProjectConfig, compose_file: Path) -> int | None:
19
+ """Check the HTTP port and, if occupied, switch to a free one.
20
+
21
+ Args:
22
+ config: Project configuration.
23
+ compose_file: Location of the Compose file.
24
+
25
+ Returns:
26
+ The resolved HTTP port, or ``None`` when nothing could be checked.
27
+
28
+ Raises:
29
+ typer.Exit: If the user declines the proposed alternative.
30
+ """
31
+ compose = ComposeProject(config, compose_file)
32
+ try:
33
+ port = compose.http_port()
34
+ except (FileNotFoundError, ValueError):
35
+ return None
36
+ if is_port_free(port):
37
+ return port
38
+ alternative = find_free_port(port)
39
+ accepted = Confirm.ask(
40
+ f"Port {port} is occupied. Use {alternative} instead?",
41
+ default=True,
42
+ )
43
+ if not accepted:
44
+ console.print("[bold red]Aborting: no free HTTP port chosen.[/]")
45
+ raise typer.Exit(code=1)
46
+ compose.set_http_port(alternative)
47
+ console.print(f"[yellow]Updated docker-compose.yml to use port {alternative}.[/]")
48
+ return alternative
@@ -0,0 +1,33 @@
1
+ """Shared helpers for the ``info`` and ``projects`` commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from youca_setup.docker.manager import DockerManager
6
+ from youca_setup.project.config import ProjectConfig
7
+
8
+
9
+ def project_state(config: ProjectConfig) -> str:
10
+ """Return a human-readable state of the project's services.
11
+
12
+ Args:
13
+ config: Project configuration.
14
+
15
+ Returns:
16
+ ``"running"`` when both services are up, ``"partial"`` when only one
17
+ is, ``"stopped"`` when none exist, ``"unknown"`` when Docker is
18
+ unreachable, or ``"system"`` for non-Docker projects.
19
+ """
20
+ if not config.docker:
21
+ return "system"
22
+ manager = DockerManager(config)
23
+ try:
24
+ db = manager.container_status(f"{config.name}-db-1")
25
+ odoo = manager.container_status(f"{config.name}-odoo-1")
26
+ except RuntimeError:
27
+ return "unknown"
28
+ statuses = [status for status in (db, odoo) if status is not None]
29
+ if len(statuses) == 2 and all(status == "running" for status in statuses):
30
+ return "running"
31
+ if statuses:
32
+ return "partial"
33
+ return "stopped"
@@ -0,0 +1,47 @@
1
+ """The ``backup`` command - backup database, filestore and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from youca_setup.project.backup import BackupManager
12
+ from youca_setup.project.loader import load_project
13
+
14
+ app = typer.Typer(help="Backup the Odoo project (database + filestore + config).")
15
+ console = Console()
16
+
17
+
18
+ @app.command()
19
+ def backup(
20
+ database: Annotated[
21
+ str | None,
22
+ typer.Option("--database", "-d", help="Database name. Defaults to the project name."),
23
+ ] = None,
24
+ output_dir: Annotated[
25
+ str, typer.Option("--output", "-o", help="Directory where the archive is written.")
26
+ ] = "backups",
27
+ ) -> None:
28
+ """Create a timestamped archive of the current project.
29
+
30
+ The archive contains the database dump (custom format), the filestore
31
+ volume, ``config/`` and ``youca-setup.yaml``.
32
+
33
+ Args:
34
+ database: Database to dump.
35
+ output_dir: Destination directory for the archive.
36
+ """
37
+ config = load_project()
38
+ manager = BackupManager(config)
39
+ try:
40
+ path = manager.create_backup(
41
+ database=database,
42
+ output_dir=Path(output_dir),
43
+ )
44
+ except (FileNotFoundError, RuntimeError) as exc:
45
+ console.print(f"[bold red]Backup failed:[/] {exc}")
46
+ raise typer.Exit(code=1) from None
47
+ console.print(f"[green]Backup created:[/] {path}")
@@ -0,0 +1,82 @@
1
+ """The ``doctor`` command - diagnose the development environment.
2
+
3
+ Runs :mod:`youca_setup.diagnostics.checks` and, when asked, applies fixes from
4
+ :mod:`youca_setup.diagnostics.fixer`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from youca_setup.diagnostics.checks import run_checks
16
+ from youca_setup.diagnostics.fixer import Fixer, propose_fixes
17
+ from youca_setup.project.config import ProjectConfig
18
+ from youca_setup.project.loader import load_project
19
+
20
+ app = typer.Typer(help="Check the environment for common issues.")
21
+ console = Console()
22
+
23
+
24
+ def _render(results: list[tuple[str, bool, str]]) -> None:
25
+ """Render check results as a Rich table.
26
+
27
+ Args:
28
+ results: ``(name, passed, message)`` rows.
29
+ """
30
+ table = Table(show_header=True, header_style="bold")
31
+ table.add_column("Check")
32
+ table.add_column("Status")
33
+ table.add_column("Detail")
34
+ for name, passed, message in results:
35
+ status = "[green]OK[/]" if passed else "[bold red]FAIL[/]"
36
+ table.add_row(name, status, message)
37
+ console.print(table)
38
+
39
+
40
+ @app.command()
41
+ def doctor(
42
+ fix: Annotated[
43
+ bool,
44
+ typer.Option(
45
+ "--fix",
46
+ "-f",
47
+ help="Attempt to fix detected issues (destructive actions ask for confirmation).",
48
+ ),
49
+ ] = False,
50
+ check: Annotated[
51
+ str | None,
52
+ typer.Option(
53
+ "--check", help="Run only the given check (e.g. 'docker', 'postgres', 'ports')."
54
+ ),
55
+ ] = None,
56
+ ) -> None:
57
+ """Diagnose Python, Docker, PostgreSQL, ports and project configuration.
58
+
59
+ Args:
60
+ fix: Whether issues should be auto-fixed where possible.
61
+ check: Restrict the run to a single named check.
62
+ """
63
+ try:
64
+ config: ProjectConfig | None = load_project()
65
+ except FileNotFoundError:
66
+ config = None
67
+
68
+ results = run_checks(config)
69
+ if check is not None:
70
+ results = [result for result in results if result.name == check]
71
+
72
+ _render([(result.name, result.passed, result.message) for result in results])
73
+
74
+ if not fix:
75
+ return
76
+
77
+ fixes = propose_fixes(config, results)
78
+ if not fixes:
79
+ console.print("[green]Nothing to fix.[/]")
80
+ return
81
+ Fixer().run(fixes)
82
+ console.print("[green]Done. Re-run 'youca-setup doctor' to confirm.[/]")
@@ -0,0 +1,43 @@
1
+ """The ``export`` command - export the project environment as a portable archive.
2
+
3
+ Creates a self-contained ``.tar.gz`` with the configuration files and addons,
4
+ omitting the database dump, filestore and backups.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Annotated
11
+
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+
16
+ from youca_setup.project.loader import load_project
17
+ from youca_setup.project.transport import export_environment
18
+
19
+ app = typer.Typer(help="Export the current project environment.")
20
+ console = Console()
21
+
22
+
23
+ @app.command()
24
+ def export(
25
+ output: Annotated[
26
+ Path | None,
27
+ typer.Option("--output", "-o", help="Output archive path."),
28
+ ] = None,
29
+ ) -> None:
30
+ """Export the current project environment to a portable ``.tar.gz`` file.
31
+
32
+ Args:
33
+ output: Destination path for the archive; defaults to ``<name>.tar.gz``
34
+ in the current directory.
35
+ """
36
+ config = load_project()
37
+ archive = output or Path.cwd() / f"{config.name}-env.tar.gz"
38
+ try:
39
+ path = export_environment(config, archive)
40
+ except FileNotFoundError as exc:
41
+ console.print(f"[bold red]Error:[/] {exc}")
42
+ raise typer.Exit(1) from exc
43
+ console.print(Panel(f"[bold green]Environment exported to {path}.[/]"))
@@ -0,0 +1,61 @@
1
+ """The ``import`` command - import a portable environment archive.
2
+
3
+ Extracts the archive into a new project directory, registers the project and
4
+ prints the next steps.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Annotated
11
+
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+
16
+ from youca_setup.project import registry
17
+ from youca_setup.project.transport import import_environment
18
+
19
+ app = typer.Typer(help="Import a Youca Setup environment archive.")
20
+ console = Console()
21
+
22
+
23
+ @app.command(name="import")
24
+ def import_env(
25
+ archive: Annotated[Path, typer.Argument(help="Path to the ``.tar.gz`` environment archive.")],
26
+ directory: Annotated[
27
+ Path | None,
28
+ typer.Option("--directory", "-d", help="Parent directory for the new project."),
29
+ ] = None,
30
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
31
+ ) -> None:
32
+ """Import a Youca Setup environment archive into a new project directory.
33
+
34
+ Args:
35
+ archive: Path to the ``.tar.gz`` archive.
36
+ directory: Parent directory for the project; defaults to cwd.
37
+ yes: Skip the confirmation prompt.
38
+ """
39
+ target_base = directory or Path.cwd()
40
+
41
+ try:
42
+ project_dir, config = import_environment(archive, target_base)
43
+ except FileNotFoundError as exc:
44
+ console.print(f"[bold red]Error:[/] {exc}")
45
+ raise typer.Exit(1) from exc
46
+ except FileExistsError as exc:
47
+ console.print(f"[bold red]Error:[/] {exc}")
48
+ raise typer.Exit(1) from exc
49
+ except ValueError as exc:
50
+ console.print(f"[bold red]Error:[/] {exc}")
51
+ raise typer.Exit(1) from exc
52
+
53
+ try:
54
+ registry.register(config, project_dir)
55
+ except OSError as exc:
56
+ console.print(f"[yellow]Could not register the project: {exc}[/]")
57
+
58
+ console.print(Panel(f"[bold green]Project '{config.name}' imported into {project_dir}.[/]"))
59
+ console.print("Next steps:\n")
60
+ console.print(f" [bold]cd {project_dir}[/]")
61
+ console.print(" [bold]youca-setup start[/]")
@@ -0,0 +1,42 @@
1
+ """The ``info`` command - show details of the current project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from youca_setup.commands._state import project_state
10
+ from youca_setup.project.loader import load_project
11
+
12
+ app = typer.Typer(help="Show details of the current project.")
13
+ console = Console()
14
+
15
+
16
+ @app.command("info")
17
+ def info() -> None:
18
+ """Print the configuration and state of the current project.
19
+
20
+ Raises:
21
+ FileNotFoundError: When run outside a Youca Setup project.
22
+ """
23
+ config = load_project()
24
+
25
+ table = Table(title=f"Youca Setup for Odoo — {config.name}", show_header=False)
26
+ table.add_column("Setting", style="cyan")
27
+ table.add_column("Value")
28
+
29
+ rows = [
30
+ ("Project name", config.name),
31
+ ("Odoo version", config.odoo_version),
32
+ ("PostgreSQL version", config.postgres_version),
33
+ ("PostgreSQL backend", config.postgres_backend),
34
+ ("Docker Compose", "yes" if config.docker else "no"),
35
+ ("HTTP port", str(config.http_port)),
36
+ ("Addons", ", ".join(config.addons) or "—"),
37
+ ("State", project_state(config)),
38
+ ]
39
+ for setting, value in rows:
40
+ table.add_row(setting, value)
41
+
42
+ console.print(table)