apexcodexpy 0.2.2__tar.gz → 0.3.1__tar.gz
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.
- {apexcodexpy-0.2.2 → apexcodexpy-0.3.1}/PKG-INFO +1 -1
- apexcodexpy-0.3.1/codex/infra/__init__.py +0 -0
- {apexcodexpy-0.2.2/codex/tasks → apexcodexpy-0.3.1/codex/infra}/dependabot.py +10 -8
- apexcodexpy-0.3.1/codex/infra/program.py +126 -0
- apexcodexpy-0.2.2/codex/tasks/update.py → apexcodexpy-0.3.1/codex/infra/pyproject.py +11 -10
- {apexcodexpy-0.2.2/codex → apexcodexpy-0.3.1/codex/infra}/reporters.py +20 -17
- apexcodexpy-0.3.1/codex/infra/result.py +107 -0
- apexcodexpy-0.3.1/codex/infra/service.py +257 -0
- apexcodexpy-0.3.1/codex/infra/tools.py +118 -0
- apexcodexpy-0.3.1/codex/infra/typer/__init__.py +7 -0
- apexcodexpy-0.3.1/codex/infra/typer/device.py +28 -0
- apexcodexpy-0.2.2/codex/cli.py → apexcodexpy-0.3.1/codex/infra/typer/root.py +190 -255
- apexcodexpy-0.3.1/codex/infra/typer/testing.py +94 -0
- apexcodexpy-0.3.1/codex/runner.py +7 -0
- {apexcodexpy-0.2.2 → apexcodexpy-0.3.1}/pyproject.toml +2 -2
- apexcodexpy-0.2.2/codex/service.py +0 -191
- apexcodexpy-0.2.2/codex/tasks/__init__.py +0 -11
- apexcodexpy-0.2.2/codex/tasks/result.py +0 -164
- apexcodexpy-0.2.2/codex/tools.py +0 -127
- {apexcodexpy-0.2.2 → apexcodexpy-0.3.1}/LICENSE +0 -0
- {apexcodexpy-0.2.2 → apexcodexpy-0.3.1}/README.md +0 -0
- {apexcodexpy-0.2.2 → apexcodexpy-0.3.1}/codex/__init__.py +0 -0
|
File without changes
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
|
-
from codex.
|
|
10
|
+
from codex.infra.program import Executable
|
|
11
|
+
from codex.infra.result import ProgramResult
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def _pip_config() -> dict[str, Any]:
|
|
@@ -55,19 +56,20 @@ STANDARDS = {
|
|
|
55
56
|
class DependabotYaml:
|
|
56
57
|
dry_run: bool = False
|
|
57
58
|
|
|
59
|
+
_result: ProgramResult = field(init=False, default=ProgramResult(name="DependaBot"))
|
|
60
|
+
|
|
58
61
|
NAME = "dependabot.yml"
|
|
59
62
|
|
|
60
|
-
def sync(self, target: Path) ->
|
|
63
|
+
def sync(self, target: Path) -> Executable:
|
|
61
64
|
dependabot = self.yaml_on(target)
|
|
62
65
|
dependabot.parent.mkdir(parents=True, exist_ok=True)
|
|
63
66
|
|
|
64
67
|
return self._sync(dependabot)
|
|
65
68
|
|
|
66
|
-
def _sync(self, dependabot: Path) ->
|
|
69
|
+
def _sync(self, dependabot: Path) -> Executable:
|
|
67
70
|
if self.dry_run:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
+
dumped = json.dumps(STANDARDS, indent=2)
|
|
72
|
+
return self._result.success(message=f"Changes to apply:\n{dumped}")
|
|
71
73
|
|
|
72
74
|
with dependabot.open(mode="w", encoding="utf-8", newline="\n") as stream:
|
|
73
75
|
yaml.dump(
|
|
@@ -78,7 +80,7 @@ class DependabotYaml:
|
|
|
78
80
|
default_flow_style=False,
|
|
79
81
|
)
|
|
80
82
|
|
|
81
|
-
return
|
|
83
|
+
return self._result.success(message=f"{self.NAME} synchronized successfully.\n")
|
|
82
84
|
|
|
83
85
|
def yaml_on(self, path: Path) -> Path:
|
|
84
86
|
return path / ".github" / self.NAME
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from collections.abc import Iterable, Iterator, MutableSequence
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
from codex.infra.result import ComboResult, ProgramResult, TaskResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Program:
|
|
13
|
+
command: Command
|
|
14
|
+
|
|
15
|
+
name: str = "unknown"
|
|
16
|
+
|
|
17
|
+
def execute(self) -> ProgramResult:
|
|
18
|
+
return replace(
|
|
19
|
+
ProgramResult(name=self.name.capitalize()).parse(
|
|
20
|
+
subprocess.run(
|
|
21
|
+
list(self.command),
|
|
22
|
+
text=True,
|
|
23
|
+
encoding="utf-8",
|
|
24
|
+
capture_output=True,
|
|
25
|
+
)
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Command:
|
|
32
|
+
name: str
|
|
33
|
+
|
|
34
|
+
_arguments: list[str] = field(init=False, default_factory=list)
|
|
35
|
+
|
|
36
|
+
def with_a(self, **kwargs: Any) -> Command:
|
|
37
|
+
return self.and_a(**kwargs)
|
|
38
|
+
|
|
39
|
+
def and_a(self, **kwargs: Any) -> Command:
|
|
40
|
+
assert len(kwargs) == 1, "with_a/and_a supports one argument at a time."
|
|
41
|
+
|
|
42
|
+
name, value = kwargs.popitem()
|
|
43
|
+
|
|
44
|
+
return self.append(f"--{name}={value}")
|
|
45
|
+
|
|
46
|
+
def flag(self, **kwargs: bool | None) -> Command:
|
|
47
|
+
for name, value in kwargs.items():
|
|
48
|
+
flag = self.dashify(name)
|
|
49
|
+
|
|
50
|
+
if value is not None:
|
|
51
|
+
self.append(f"--{flag}" if value else f"--no-{flag}")
|
|
52
|
+
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def dashify(self, name: str) -> str:
|
|
56
|
+
return name.replace("_", "-")
|
|
57
|
+
|
|
58
|
+
def extend(self, arguments: Iterable[Any]) -> Command:
|
|
59
|
+
for argument in arguments:
|
|
60
|
+
self.append(argument)
|
|
61
|
+
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def append(self, argument: Any) -> Command:
|
|
65
|
+
if argument is not None:
|
|
66
|
+
self._arguments.extend(str(argument).split())
|
|
67
|
+
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __str__(self) -> str:
|
|
71
|
+
return " ".join(self)
|
|
72
|
+
|
|
73
|
+
def __iter__(self) -> Iterator[str]:
|
|
74
|
+
yield from self.name.split()
|
|
75
|
+
|
|
76
|
+
for argument in self._arguments:
|
|
77
|
+
yield from str(argument).split()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Executable(Protocol):
|
|
81
|
+
def execute(self) -> TaskResult:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class MultiProgram:
|
|
87
|
+
sub_programs: MutableSequence[Executable] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
def attach(self, program: Executable) -> MultiProgram:
|
|
90
|
+
self.sub_programs.append(program)
|
|
91
|
+
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def execute(self) -> TaskResult:
|
|
95
|
+
return ComboResult([program.execute() for program in self.sub_programs])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class BoundedProgram:
|
|
100
|
+
before: MultiProgram = field(init=False, default_factory=MultiProgram)
|
|
101
|
+
action: MultiProgram = field(init=False, default_factory=MultiProgram)
|
|
102
|
+
after: MultiProgram = field(init=False, default_factory=MultiProgram)
|
|
103
|
+
|
|
104
|
+
def setup(self, program: Executable) -> BoundedProgram:
|
|
105
|
+
self.before.attach(program)
|
|
106
|
+
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def teardown(self, program: Executable) -> BoundedProgram:
|
|
110
|
+
self.after.attach(program)
|
|
111
|
+
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def attach(self, program: Executable) -> BoundedProgram:
|
|
115
|
+
self.action.attach(program)
|
|
116
|
+
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def execute(self) -> TaskResult:
|
|
120
|
+
return (
|
|
121
|
+
MultiProgram()
|
|
122
|
+
.attach(self.before)
|
|
123
|
+
.attach(self.action)
|
|
124
|
+
.attach(self.after)
|
|
125
|
+
.execute()
|
|
126
|
+
)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
import tomlkit
|
|
8
8
|
|
|
9
|
-
from codex.
|
|
9
|
+
from codex.infra.program import Executable
|
|
10
|
+
from codex.infra.result import ProgramResult
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def _mypy_config() -> dict[str, Any]:
|
|
@@ -69,14 +70,16 @@ STANDARDS = {
|
|
|
69
70
|
class PyProjectToml:
|
|
70
71
|
dry_run: bool = False
|
|
71
72
|
|
|
73
|
+
_result: ProgramResult = field(init=False, default=ProgramResult(name="PyProject"))
|
|
74
|
+
|
|
72
75
|
NAME = "pyproject.toml"
|
|
73
76
|
|
|
74
|
-
def sync(self, target: Path) ->
|
|
77
|
+
def sync(self, target: Path) -> Executable:
|
|
75
78
|
return self._sync(self.toml_on(target))
|
|
76
79
|
|
|
77
|
-
def _sync(self, pyproject: Path) ->
|
|
80
|
+
def _sync(self, pyproject: Path) -> Executable:
|
|
78
81
|
if not pyproject.exists():
|
|
79
|
-
return
|
|
82
|
+
return self._result.fail(message=f"{self.NAME} not found.")
|
|
80
83
|
|
|
81
84
|
document = tomlkit.parse(pyproject.read_text(encoding="utf-8"))
|
|
82
85
|
|
|
@@ -86,16 +89,14 @@ class PyProjectToml:
|
|
|
86
89
|
modified = tomlkit.dumps(document)
|
|
87
90
|
|
|
88
91
|
if original == modified:
|
|
89
|
-
return
|
|
92
|
+
return self._result.success(message=f"{self.NAME} is already up to date.\n")
|
|
90
93
|
|
|
91
94
|
if self.dry_run:
|
|
92
|
-
return
|
|
93
|
-
stdout=f"Changes would be applied:\n{modified}"
|
|
94
|
-
)
|
|
95
|
+
return self._result.success(message=f"Changes to apply:\n{modified}")
|
|
95
96
|
|
|
96
97
|
pyproject.write_text(modified, encoding="utf-8", newline="\n")
|
|
97
98
|
|
|
98
|
-
return
|
|
99
|
+
return self._result.success(message=f"{self.NAME} synchronized successfully.\n")
|
|
99
100
|
|
|
100
101
|
def toml_on(self, path: Path) -> Path:
|
|
101
102
|
return path / self.NAME
|
|
@@ -3,12 +3,19 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Protocol
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
from codex.infra.result import Reporter
|
|
7
|
+
from codex.infra.typer.device import TyperDevice
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@dataclass(frozen=True)
|
|
10
11
|
class DefaultReporter:
|
|
11
|
-
device: _Device = field(default_factory=
|
|
12
|
+
device: _Device = field(default_factory=TyperDevice)
|
|
13
|
+
|
|
14
|
+
def silenced(self, *, when: bool) -> Reporter:
|
|
15
|
+
if when:
|
|
16
|
+
return DefaultReporter(SilentDevice())
|
|
17
|
+
|
|
18
|
+
return self
|
|
12
19
|
|
|
13
20
|
def success_of(self, name: str) -> None:
|
|
14
21
|
self.device.with_green().echo(f"✅ {name} passed!")
|
|
@@ -40,24 +47,20 @@ class _Device(Protocol):
|
|
|
40
47
|
pass
|
|
41
48
|
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def with_green(self) -> TyperDevice:
|
|
48
|
-
return TyperDevice(typer.colors.GREEN)
|
|
50
|
+
class SilentDevice: # pragma: no cover
|
|
51
|
+
def with_green(self) -> _Device:
|
|
52
|
+
return self
|
|
49
53
|
|
|
50
|
-
def with_red(self) ->
|
|
51
|
-
return
|
|
54
|
+
def with_red(self) -> _Device:
|
|
55
|
+
return self
|
|
52
56
|
|
|
53
|
-
def with_white(self) ->
|
|
54
|
-
return
|
|
57
|
+
def with_white(self) -> _Device:
|
|
58
|
+
return self
|
|
55
59
|
|
|
56
|
-
def with_yellow(self) ->
|
|
57
|
-
return
|
|
60
|
+
def with_yellow(self) -> _Device:
|
|
61
|
+
return self
|
|
58
62
|
|
|
59
|
-
def echo(self, message: str) ->
|
|
60
|
-
|
|
61
|
-
typer.secho(message, fg=self.color)
|
|
63
|
+
def echo(self, message: str) -> _Device:
|
|
64
|
+
_ = message
|
|
62
65
|
|
|
63
66
|
return self
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from subprocess import CompletedProcess
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskResult(Protocol):
|
|
9
|
+
def report(self, using: Reporter) -> TaskResult:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
def exit(self, using: type[Exception] | None = None) -> int:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ComboResult:
|
|
18
|
+
results: list[TaskResult] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def _exit_code(self) -> int:
|
|
22
|
+
return sum(result.exit() for result in self.results)
|
|
23
|
+
|
|
24
|
+
def report(self, using: Reporter) -> ComboResult:
|
|
25
|
+
for result in self.results:
|
|
26
|
+
result.report(using)
|
|
27
|
+
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def exit(self, using: type[Exception] | None = None) -> int:
|
|
31
|
+
if using is None:
|
|
32
|
+
return self._exit_code
|
|
33
|
+
|
|
34
|
+
raise using(self._exit_code)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, kw_only=True)
|
|
38
|
+
class ProgramResult:
|
|
39
|
+
name: str
|
|
40
|
+
|
|
41
|
+
stdout: str = ""
|
|
42
|
+
stderr: str = ""
|
|
43
|
+
exit_code: int = 0
|
|
44
|
+
|
|
45
|
+
def __int__(self) -> int:
|
|
46
|
+
return self.exit_code
|
|
47
|
+
|
|
48
|
+
def __bool__(self) -> bool:
|
|
49
|
+
return self.exit_code == 0
|
|
50
|
+
|
|
51
|
+
def parse(self, result: CompletedProcess[Any]) -> ProgramResult:
|
|
52
|
+
return ProgramResult(
|
|
53
|
+
name=self.name,
|
|
54
|
+
stdout=result.stdout or "",
|
|
55
|
+
stderr=result.stderr or "",
|
|
56
|
+
exit_code=result.returncode,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def success(self, *, message: str = "") -> ProgramResult:
|
|
60
|
+
return ProgramResult(
|
|
61
|
+
name=self.name,
|
|
62
|
+
stdout=message or self.stdout,
|
|
63
|
+
stderr=self.stderr,
|
|
64
|
+
exit_code=0,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def fail(self, *, message: str = "") -> ProgramResult:
|
|
68
|
+
return ProgramResult(
|
|
69
|
+
name=self.name,
|
|
70
|
+
stdout=self.stdout,
|
|
71
|
+
stderr=message or self.stderr,
|
|
72
|
+
exit_code=1,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def execute(self) -> ProgramResult:
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def report(self, using: Reporter) -> ProgramResult:
|
|
79
|
+
if self.exit_code == 0:
|
|
80
|
+
using.success_of(self.name)
|
|
81
|
+
else:
|
|
82
|
+
using.failure_of(self.name)
|
|
83
|
+
|
|
84
|
+
using.out(self.stdout)
|
|
85
|
+
using.error(self.stderr)
|
|
86
|
+
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def exit(self, using: type[Exception] | None = None) -> int:
|
|
90
|
+
if using is None:
|
|
91
|
+
return self.exit_code
|
|
92
|
+
|
|
93
|
+
raise using(self.exit_code)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Reporter(Protocol):
|
|
97
|
+
def success_of(self, name: str) -> None:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def failure_of(self, name: str) -> None:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
def out(self, message: str) -> None:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
def error(self, message: str) -> None:
|
|
107
|
+
pass
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from time import time
|
|
6
|
+
|
|
7
|
+
from codex.infra.dependabot import DependabotYaml
|
|
8
|
+
from codex.infra.program import (
|
|
9
|
+
Command,
|
|
10
|
+
Executable,
|
|
11
|
+
MultiProgram,
|
|
12
|
+
Program,
|
|
13
|
+
)
|
|
14
|
+
from codex.infra.pyproject import PyProjectToml
|
|
15
|
+
from codex.infra.result import TaskResult
|
|
16
|
+
from codex.infra.tools import MyPy, Pip, Poetry, PoetryRun, Pytest, Ruff
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Codex:
|
|
21
|
+
tools: CodexTools
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def target(cls, target: Path) -> Codex:
|
|
25
|
+
return cls(CodexTools(target))
|
|
26
|
+
|
|
27
|
+
def ci(self, on: bool) -> Codex | CiCodex:
|
|
28
|
+
if on:
|
|
29
|
+
return CiCodex(tools=self.tools)
|
|
30
|
+
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def sync(self, dry_run: bool = False) -> TaskResult:
|
|
34
|
+
return self.tools.sync(dry_run=dry_run).execute()
|
|
35
|
+
|
|
36
|
+
def lock(self) -> TaskResult:
|
|
37
|
+
return self.tools.lock().execute()
|
|
38
|
+
|
|
39
|
+
def update(self) -> TaskResult:
|
|
40
|
+
return self.tools.update().execute()
|
|
41
|
+
|
|
42
|
+
def install(self) -> TaskResult:
|
|
43
|
+
return self.tools.install().execute()
|
|
44
|
+
|
|
45
|
+
def lint(self) -> TaskResult:
|
|
46
|
+
return self.tools.lint().execute()
|
|
47
|
+
|
|
48
|
+
def fix(self, unsafe: bool = False) -> TaskResult:
|
|
49
|
+
return self.tools.fix(unsafe=unsafe).execute()
|
|
50
|
+
|
|
51
|
+
def unit(self) -> TaskResult:
|
|
52
|
+
return self.tools.unit().execute()
|
|
53
|
+
|
|
54
|
+
def integration(self) -> TaskResult:
|
|
55
|
+
return self.tools.integration().execute()
|
|
56
|
+
|
|
57
|
+
def behaviour(self) -> TaskResult:
|
|
58
|
+
return self.tools.behaviour().execute()
|
|
59
|
+
|
|
60
|
+
def build(self, tag: str) -> TaskResult:
|
|
61
|
+
return self.tools.build(tag).execute()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class CiCodex:
|
|
66
|
+
tools: CodexTools
|
|
67
|
+
|
|
68
|
+
def lint(self) -> TaskResult:
|
|
69
|
+
return (
|
|
70
|
+
MultiProgram()
|
|
71
|
+
.attach(self.tools.install())
|
|
72
|
+
.attach(self.tools.lint())
|
|
73
|
+
.execute()
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def unit(self) -> TaskResult:
|
|
77
|
+
return (
|
|
78
|
+
MultiProgram()
|
|
79
|
+
.attach(self.tools.install())
|
|
80
|
+
.attach(self.tools.unit(in_ci=True))
|
|
81
|
+
.execute()
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def integration(self) -> TaskResult:
|
|
85
|
+
return self.tools.integration().execute()
|
|
86
|
+
|
|
87
|
+
def behaviour(self) -> TaskResult:
|
|
88
|
+
return (
|
|
89
|
+
MultiProgram()
|
|
90
|
+
.attach(self.tools.install())
|
|
91
|
+
.attach(self.tools.behaviour(in_ci=True))
|
|
92
|
+
.execute()
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class CodexTools:
|
|
98
|
+
target: Path
|
|
99
|
+
|
|
100
|
+
def sync(self, dry_run: bool = False) -> Executable:
|
|
101
|
+
return (
|
|
102
|
+
MultiProgram()
|
|
103
|
+
.attach(PyProjectToml(dry_run=dry_run).sync(self.target))
|
|
104
|
+
.attach(DependabotYaml(dry_run=dry_run).sync(self.target))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def lock(self) -> Executable:
|
|
108
|
+
return Program(name="lock", command=Poetry().lock())
|
|
109
|
+
|
|
110
|
+
def update(self) -> Executable:
|
|
111
|
+
return Program(name="update", command=Poetry().update())
|
|
112
|
+
|
|
113
|
+
def install(self) -> Executable:
|
|
114
|
+
return (
|
|
115
|
+
MultiProgram()
|
|
116
|
+
.attach(
|
|
117
|
+
Program(
|
|
118
|
+
name="tools",
|
|
119
|
+
command=Pip().install("pip", "poetry"),
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
.attach(
|
|
123
|
+
Program(
|
|
124
|
+
name="dependencies",
|
|
125
|
+
command=Poetry().install(),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def lint(self) -> Executable:
|
|
131
|
+
return (
|
|
132
|
+
PoetryRun()
|
|
133
|
+
.attach(
|
|
134
|
+
Program(
|
|
135
|
+
name="poetry",
|
|
136
|
+
command=Poetry().lint(self.target),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
.attach(
|
|
140
|
+
Program(
|
|
141
|
+
name="black",
|
|
142
|
+
command=Ruff(on=self.target).format().flag(check=True),
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
.attach(
|
|
146
|
+
Program(
|
|
147
|
+
name="ruff",
|
|
148
|
+
command=Ruff(on=self.target).lint(),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
.attach(
|
|
152
|
+
Program(
|
|
153
|
+
name="mypy",
|
|
154
|
+
command=MyPy(on=self.target).check(),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def fix(self, unsafe: bool = False) -> Executable:
|
|
160
|
+
return (
|
|
161
|
+
PoetryRun()
|
|
162
|
+
.attach(
|
|
163
|
+
Program(
|
|
164
|
+
name="black",
|
|
165
|
+
command=Ruff(on=self.target).format(),
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
.attach(
|
|
169
|
+
Program(
|
|
170
|
+
name="ruff",
|
|
171
|
+
command=(
|
|
172
|
+
Ruff(on=self.target)
|
|
173
|
+
.lint()
|
|
174
|
+
.flag(fix=True)
|
|
175
|
+
.flag(unsafe_fixes=unsafe)
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def unit(self, in_ci: bool = False) -> Executable:
|
|
182
|
+
return PoetryRun().attach(
|
|
183
|
+
Program(
|
|
184
|
+
name="test",
|
|
185
|
+
command=(
|
|
186
|
+
Pytest.on(self.target)
|
|
187
|
+
.run()
|
|
188
|
+
.flag(cov=not in_ci or None)
|
|
189
|
+
.flag(last_failed=not in_ci or None)
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def behaviour(self, in_ci: bool = False) -> Executable:
|
|
195
|
+
return PoetryRun().attach(
|
|
196
|
+
Program(
|
|
197
|
+
name="behave",
|
|
198
|
+
command=(
|
|
199
|
+
Command("behave")
|
|
200
|
+
.append(self.target / "tests" / "features")
|
|
201
|
+
.with_a(format="progress" if in_ci else "pretty")
|
|
202
|
+
.flag(summary=not in_ci)
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def integration(self) -> Executable:
|
|
208
|
+
return (
|
|
209
|
+
Poetry()
|
|
210
|
+
.export(self.target)
|
|
211
|
+
.attach(
|
|
212
|
+
PoetryRun().attach(
|
|
213
|
+
Program(
|
|
214
|
+
name="test",
|
|
215
|
+
command=Pytest.on(self.target / "tests" / "integration").run(),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def build(self, tag: str) -> Executable:
|
|
222
|
+
return (
|
|
223
|
+
Poetry()
|
|
224
|
+
.export(self.target)
|
|
225
|
+
.attach(
|
|
226
|
+
Program(
|
|
227
|
+
name="build",
|
|
228
|
+
command=(
|
|
229
|
+
Command("docker build")
|
|
230
|
+
.flag(tag=True)
|
|
231
|
+
.append(tag)
|
|
232
|
+
.flag(build_arg=True)
|
|
233
|
+
.with_a(RELEASE=Git().revision() or str(int(time())))
|
|
234
|
+
.append(self.target)
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass(frozen=True)
|
|
242
|
+
class Git:
|
|
243
|
+
def revision(self, of: str = "HEAD", short: bool = True) -> str:
|
|
244
|
+
return (
|
|
245
|
+
Program(Command("git rev-parse").flag(short=short).append(of))
|
|
246
|
+
.execute()
|
|
247
|
+
.stdout
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def amend(self) -> TaskResult:
|
|
251
|
+
return Program(
|
|
252
|
+
name="amend",
|
|
253
|
+
command=Command("git commit")
|
|
254
|
+
.flag(all=True)
|
|
255
|
+
.flag(amend=True)
|
|
256
|
+
.flag(edit=False),
|
|
257
|
+
).execute()
|