apexcodexpy 0.7.2__tar.gz → 0.7.4__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.7.2 → apexcodexpy-0.7.4}/PKG-INFO +1 -1
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/program.py +5 -2
- apexcodexpy-0.7.4/codex/infra/pyproject.py +190 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/reporters.py +4 -2
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/result.py +7 -2
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/service.py +22 -18
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/tools.py +4 -1
- apexcodexpy-0.7.4/codex/infra/typer/device.py +52 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/typer/root.py +0 -15
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/workflows/base.py +5 -7
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/workflows/ci.py +4 -1
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/yamler.py +5 -3
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/pyproject.toml +18 -1
- apexcodexpy-0.7.2/codex/infra/pyproject.py +0 -112
- apexcodexpy-0.7.2/codex/infra/typer/device.py +0 -28
- apexcodexpy-0.7.2/codex/infra/workflows/lock.py +0 -33
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/LICENSE +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/README.md +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/__init__.py +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/__init__.py +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/typer/__init__.py +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/typer/testing.py +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/workflows/__init__.py +1 -1
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/workflows/build.py +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/workflows/dependabot.py +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/infra/workflows/validate.py +0 -0
- {apexcodexpy-0.7.2 → apexcodexpy-0.7.4}/codex/runner.py +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
|
-
from collections.abc import Callable, Iterable, Iterator, MutableSequence
|
|
5
4
|
from dataclasses import dataclass, field, replace
|
|
6
|
-
from typing import Any, Protocol
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
7
6
|
|
|
8
7
|
from codex.infra.result import ComboResult, ProgramResult, TaskResult
|
|
9
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable, Iterable, Iterator, MutableSequence
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
@dataclass(frozen=True)
|
|
12
14
|
class Program:
|
|
@@ -22,6 +24,7 @@ class Program:
|
|
|
22
24
|
text=True,
|
|
23
25
|
encoding="utf-8",
|
|
24
26
|
capture_output=True,
|
|
27
|
+
check=False,
|
|
25
28
|
)
|
|
26
29
|
),
|
|
27
30
|
)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import hashlib
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
import tomlkit
|
|
11
|
+
|
|
12
|
+
from codex.infra.result import ProgramResult
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
|
|
17
|
+
from codex.infra.program import Executable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class PyProjectToml:
|
|
22
|
+
name: str = "PyProject"
|
|
23
|
+
path: Path = Path("pyproject.toml")
|
|
24
|
+
|
|
25
|
+
def sync(self, target: Path) -> Executable:
|
|
26
|
+
pyproject = PyProjectFile(target / self.path, standards=self.standards)
|
|
27
|
+
|
|
28
|
+
if not pyproject.exists():
|
|
29
|
+
return ProgramResult(name=self.name).fail(message=f"{self.name} not found.")
|
|
30
|
+
|
|
31
|
+
if not pyproject.is_outdated():
|
|
32
|
+
return ProgramResult(name=self.name).no_changes()
|
|
33
|
+
|
|
34
|
+
pyproject.sync()
|
|
35
|
+
|
|
36
|
+
return ProgramResult(name=self.name).workflow_success()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def standards(self) -> Mapping[str, Any]:
|
|
40
|
+
return {
|
|
41
|
+
"tool": {
|
|
42
|
+
"mypy": self._mypy,
|
|
43
|
+
"ruff": self._ruff,
|
|
44
|
+
"coverage": self._coveragepy,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def _mypy(self) -> dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"strict": True,
|
|
52
|
+
"ignore_missing_imports": True,
|
|
53
|
+
"show_error_codes": True,
|
|
54
|
+
"pretty": True,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def _ruff(self) -> dict[str, Any]:
|
|
59
|
+
return {
|
|
60
|
+
"lint": {
|
|
61
|
+
"select": [
|
|
62
|
+
"A",
|
|
63
|
+
"ARG",
|
|
64
|
+
"B",
|
|
65
|
+
"C4",
|
|
66
|
+
"PT",
|
|
67
|
+
"RSE",
|
|
68
|
+
"RET",
|
|
69
|
+
"SIM",
|
|
70
|
+
"T20",
|
|
71
|
+
"E",
|
|
72
|
+
"W",
|
|
73
|
+
"F",
|
|
74
|
+
"I",
|
|
75
|
+
"N",
|
|
76
|
+
"UP",
|
|
77
|
+
"RUF",
|
|
78
|
+
"FURB",
|
|
79
|
+
"PERF",
|
|
80
|
+
"LOG",
|
|
81
|
+
"PIE",
|
|
82
|
+
"SLF",
|
|
83
|
+
"DTZ",
|
|
84
|
+
"PTH",
|
|
85
|
+
"TRY",
|
|
86
|
+
"PL",
|
|
87
|
+
"TCH",
|
|
88
|
+
],
|
|
89
|
+
"mccabe": {
|
|
90
|
+
"max-complexity": 10,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def _coveragepy(self) -> dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"run": {
|
|
99
|
+
"branch": True,
|
|
100
|
+
},
|
|
101
|
+
"report": {
|
|
102
|
+
"skip_empty": True,
|
|
103
|
+
"skip_covered": True,
|
|
104
|
+
"show_missing": True,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class PyProjectFile:
|
|
111
|
+
path: Path
|
|
112
|
+
|
|
113
|
+
standards: Mapping[str, Any] = field(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
@cached_property
|
|
116
|
+
def current(self) -> Tomler:
|
|
117
|
+
return Tomler.read(self.path)
|
|
118
|
+
|
|
119
|
+
@cached_property
|
|
120
|
+
def latest(self) -> Tomler:
|
|
121
|
+
return self.current.update_with(self.standards)
|
|
122
|
+
|
|
123
|
+
def exists(self) -> bool:
|
|
124
|
+
return self.path.exists()
|
|
125
|
+
|
|
126
|
+
def is_outdated(self) -> bool:
|
|
127
|
+
return self.current != self.latest
|
|
128
|
+
|
|
129
|
+
def sync(self) -> None:
|
|
130
|
+
self.path.write_text(self.latest.dumps(), encoding="utf-8", newline="\n")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class Tomler:
|
|
135
|
+
document: Mapping[str, Any]
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def read(cls, path: Path) -> Tomler:
|
|
139
|
+
return cls(tomlkit.parse(path.read_text(encoding="utf-8")))
|
|
140
|
+
|
|
141
|
+
def dumps(self) -> str:
|
|
142
|
+
return tomlkit.dumps(self.document)
|
|
143
|
+
|
|
144
|
+
def update_with(self, source: Mapping[str, Any]) -> Tomler:
|
|
145
|
+
return Tomler(deep_merge(source, self.document))
|
|
146
|
+
|
|
147
|
+
def hashed(self) -> str:
|
|
148
|
+
return hashlib.sha256(self.dumps().encode()).hexdigest()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def deep_merge(
|
|
152
|
+
source: Mapping[str, Any],
|
|
153
|
+
destination: Mapping[str, Any],
|
|
154
|
+
) -> Mapping[str, Any]:
|
|
155
|
+
"""
|
|
156
|
+
Recursively merges two mappings, taking the source and merging it into
|
|
157
|
+
the destination. Performs a deep merge such that nested dictionaries or
|
|
158
|
+
arrays are combined intelligently.
|
|
159
|
+
|
|
160
|
+
The function identifies the type of each key-value pair in the `source`
|
|
161
|
+
and `destination` mappings and determines how to merge them based on
|
|
162
|
+
their data types, supporting nested dictionaries and TOML data
|
|
163
|
+
structures.
|
|
164
|
+
|
|
165
|
+
:param source: The source mapping to merge into the destination.
|
|
166
|
+
:param destination: The destination mapping that will be updated with the
|
|
167
|
+
data from `source`.
|
|
168
|
+
:return: A new mapping that represents the merged result of `source`
|
|
169
|
+
and `destination`.
|
|
170
|
+
"""
|
|
171
|
+
result = {**destination}
|
|
172
|
+
|
|
173
|
+
for key, value in source.items():
|
|
174
|
+
match (result.get(key), value):
|
|
175
|
+
case (dict(destination_value), dict(source_value)):
|
|
176
|
+
# Both exist and are dicts -> recursively merge
|
|
177
|
+
result[key] = deep_merge(source_value, destination_value)
|
|
178
|
+
case (_, dict(source_value)):
|
|
179
|
+
# Only source is a dict -> convert to TOML table
|
|
180
|
+
result[key] = deep_merge(source_value, {})
|
|
181
|
+
case (_, list(src_list)):
|
|
182
|
+
# Formatted TOML array
|
|
183
|
+
new_array = tomlkit.array()
|
|
184
|
+
new_array.extend(copy.deepcopy(src_list))
|
|
185
|
+
new_array.multiline(True)
|
|
186
|
+
result[key] = new_array
|
|
187
|
+
case _:
|
|
188
|
+
result[key] = copy.deepcopy(value)
|
|
189
|
+
|
|
190
|
+
return result
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Protocol
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol
|
|
5
5
|
|
|
6
|
-
from codex.infra.result import Reporter
|
|
7
6
|
from codex.infra.typer.device import TyperDevice
|
|
8
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from codex.infra.result import Reporter
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
@dataclass(frozen=True)
|
|
11
13
|
class SimpleReporter:
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from
|
|
5
|
-
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from subprocess import CompletedProcess
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class TaskResult(Protocol):
|
|
@@ -59,6 +61,9 @@ class ProgramResult:
|
|
|
59
61
|
def no_changes(self) -> ProgramResult:
|
|
60
62
|
return self.success(message="Already up to date.\n")
|
|
61
63
|
|
|
64
|
+
def workflow_success(self) -> ProgramResult:
|
|
65
|
+
return self.success(message="Workflow succeeded!\n")
|
|
66
|
+
|
|
62
67
|
def success(self, *, message: str = "") -> ProgramResult:
|
|
63
68
|
return ProgramResult(
|
|
64
69
|
name=self.name,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from pathlib import Path
|
|
5
4
|
from time import time
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from codex.infra.program import Command, Executable, MultiProgram, Program
|
|
8
8
|
from codex.infra.pyproject import PyProjectToml
|
|
@@ -22,7 +22,9 @@ from codex.infra.workflows import (
|
|
|
22
22
|
CheckCodeWorkflow,
|
|
23
23
|
DependabotWorkflow,
|
|
24
24
|
)
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from pathlib import Path
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@dataclass(frozen=True)
|
|
@@ -134,14 +136,12 @@ class CodexTools:
|
|
|
134
136
|
def sync(self, force: bool) -> Executable:
|
|
135
137
|
return (
|
|
136
138
|
MultiProgram()
|
|
137
|
-
.attach(PyProjectToml().sync(self.target))
|
|
138
139
|
.attach(DependabotWorkflow().sync(self.target, force=force))
|
|
139
140
|
.attach(CheckCodeWorkflow().sync(self.target, force=force))
|
|
140
141
|
.attach(
|
|
141
142
|
BuildCodeWorkflow(disclaimer=False).sync(self.target, force=force),
|
|
142
143
|
when=self.target.joinpath("Dockerfile").exists(),
|
|
143
144
|
)
|
|
144
|
-
.attach(CodexLockWorkflow().sync(self.target, force=force))
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
def lock(self) -> Executable:
|
|
@@ -204,23 +204,27 @@ class CodexTools:
|
|
|
204
204
|
|
|
205
205
|
def fix(self, unsafe: bool = False) -> Executable:
|
|
206
206
|
return (
|
|
207
|
-
|
|
207
|
+
MultiProgram()
|
|
208
|
+
.attach(PyProjectToml().sync(self.target))
|
|
208
209
|
.attach(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
PoetryRun()
|
|
211
|
+
.attach(
|
|
212
|
+
Program(
|
|
213
|
+
name="black",
|
|
214
|
+
command=Ruff(on=self.target).format(),
|
|
215
|
+
)
|
|
212
216
|
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
.attach(
|
|
218
|
+
Program(
|
|
219
|
+
name="ruff",
|
|
220
|
+
command=(
|
|
221
|
+
Ruff(on=self.target)
|
|
222
|
+
.lint()
|
|
223
|
+
.flag(fix=True)
|
|
224
|
+
.flag(unsafe_fixes=unsafe)
|
|
225
|
+
),
|
|
222
226
|
),
|
|
223
|
-
)
|
|
227
|
+
)
|
|
224
228
|
)
|
|
225
229
|
)
|
|
226
230
|
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from importlib import metadata
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
from codex.infra.program import (
|
|
9
10
|
BoundedProgram,
|
|
@@ -11,7 +12,9 @@ from codex.infra.program import (
|
|
|
11
12
|
MultiProgram,
|
|
12
13
|
Program,
|
|
13
14
|
)
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from codex.infra.result import TaskResult
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
@dataclass(frozen=True, kw_only=True)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from textwrap import dedent
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class TyperDevice:
|
|
15
|
+
color: str = typer.colors.RESET
|
|
16
|
+
|
|
17
|
+
def with_green(self) -> TyperDevice:
|
|
18
|
+
return TyperDevice(typer.colors.GREEN)
|
|
19
|
+
|
|
20
|
+
def with_red(self) -> TyperDevice:
|
|
21
|
+
return TyperDevice(typer.colors.RED)
|
|
22
|
+
|
|
23
|
+
def with_white(self) -> TyperDevice:
|
|
24
|
+
return TyperDevice(typer.colors.WHITE)
|
|
25
|
+
|
|
26
|
+
def with_yellow(self) -> TyperDevice:
|
|
27
|
+
return TyperDevice(typer.colors.YELLOW)
|
|
28
|
+
|
|
29
|
+
def echo(self, message: str) -> TyperDevice:
|
|
30
|
+
_Message.parse(message).display(using=lambda m: typer.secho(m, fg=self.color))
|
|
31
|
+
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class _Message:
|
|
37
|
+
raw: str
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def parse(raw: str) -> _Message | _NoMessage:
|
|
41
|
+
message = dedent(raw)
|
|
42
|
+
|
|
43
|
+
return _Message(message) if message.strip() else _NoMessage()
|
|
44
|
+
|
|
45
|
+
def display(self, using: Callable[[str], None]) -> None:
|
|
46
|
+
using(self.raw)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class _NoMessage:
|
|
51
|
+
def display(self, using: Callable[[str], None]) -> None:
|
|
52
|
+
_ = using
|
|
@@ -4,25 +4,10 @@ import typer
|
|
|
4
4
|
|
|
5
5
|
from codex.infra.reporters import DefaultReporter, SimpleReporter
|
|
6
6
|
from codex.infra.service import Codex, Git
|
|
7
|
-
from codex.infra.workflows.lock import CodexLockWorkflow
|
|
8
7
|
|
|
9
8
|
cli = typer.Typer(no_args_is_help=True)
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
@cli.callback()
|
|
13
|
-
def verify_codex_lock(ctx: typer.Context) -> None:
|
|
14
|
-
if ctx.invoked_subcommand == "sync":
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
if CodexLockWorkflow().verify():
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
typer.echo("Codex is out of sync!")
|
|
21
|
-
typer.echo("Run `codex sync` to bring configuration up to date")
|
|
22
|
-
|
|
23
|
-
raise typer.Exit(1)
|
|
24
|
-
|
|
25
|
-
|
|
26
11
|
@cli.command(
|
|
27
12
|
name="version",
|
|
28
13
|
help="Show the codex version and exit.",
|
|
@@ -3,13 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from datetime import UTC, datetime
|
|
6
|
-
from
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from codex.infra.program import Executable, LazyExecutable
|
|
9
9
|
from codex.infra.result import ProgramResult
|
|
10
10
|
from codex.infra.tools import Version
|
|
11
11
|
from codex.infra.yamler import Yamler
|
|
12
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
@dataclass(frozen=True, kw_only=True)
|
|
15
18
|
class YamlWorkflow(ABC):
|
|
@@ -43,14 +46,9 @@ class YamlWorkflow(ABC):
|
|
|
43
46
|
|
|
44
47
|
standards.dump_to(path)
|
|
45
48
|
|
|
46
|
-
return self.
|
|
49
|
+
return ProgramResult(name=self.name).workflow_success()
|
|
47
50
|
|
|
48
51
|
return LazyExecutable(_)
|
|
49
52
|
|
|
50
53
|
def _utcnow(self) -> str:
|
|
51
54
|
return datetime.now(tz=UTC).replace(microsecond=0).isoformat(sep=" ")
|
|
52
|
-
|
|
53
|
-
def success(self) -> ProgramResult:
|
|
54
|
-
return ProgramResult(name=self.name).success(
|
|
55
|
-
message=f"{self.name} workflow succeeded.\n"
|
|
56
|
-
)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import MutableMapping, MutableSequence
|
|
4
3
|
from dataclasses import dataclass, field, replace
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from codex.infra.yamler import Yamler
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import MutableMapping, MutableSequence
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
@dataclass(frozen=True)
|
|
10
13
|
class CiWorkflow:
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Iterable, Mapping
|
|
4
3
|
from dataclasses import dataclass
|
|
5
|
-
from
|
|
6
|
-
from typing import Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
7
5
|
|
|
8
6
|
from ruamel.yaml import YAML
|
|
9
7
|
from ruamel.yaml.comments import CommentedMap
|
|
10
8
|
from ruamel.yaml.scalarstring import FoldedScalarString
|
|
11
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterable, Mapping
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
@dataclass(frozen=True, kw_only=True)
|
|
14
16
|
class Yamler:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "apexcodexpy"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.4"
|
|
4
4
|
description = "The Non-Destructive Configuration Steward for Python Projects."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -54,6 +54,23 @@ select = [
|
|
|
54
54
|
"I",
|
|
55
55
|
"N",
|
|
56
56
|
"UP",
|
|
57
|
+
"RUF",
|
|
58
|
+
"FURB",
|
|
59
|
+
"PERF",
|
|
60
|
+
"LOG",
|
|
61
|
+
"PIE",
|
|
62
|
+
"SLF",
|
|
63
|
+
"DTZ",
|
|
64
|
+
"PTH",
|
|
65
|
+
"TRY",
|
|
66
|
+
"PL",
|
|
67
|
+
"TCH",
|
|
68
|
+
]
|
|
69
|
+
ignore = [
|
|
70
|
+
"TRY003",
|
|
71
|
+
"PLR0913",
|
|
72
|
+
"PLR0917",
|
|
73
|
+
"PLR2004",
|
|
57
74
|
]
|
|
58
75
|
|
|
59
76
|
[tool.ruff.lint.mccabe]
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import tomlkit
|
|
8
|
-
|
|
9
|
-
from codex.infra.program import Executable
|
|
10
|
-
from codex.infra.result import ProgramResult
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _mypy_config() -> dict[str, Any]:
|
|
14
|
-
return {
|
|
15
|
-
"strict": True,
|
|
16
|
-
"ignore_missing_imports": True,
|
|
17
|
-
"show_error_codes": True,
|
|
18
|
-
"pretty": True,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _ruff_config() -> dict[str, Any]:
|
|
23
|
-
return {
|
|
24
|
-
"lint": {
|
|
25
|
-
"select": [
|
|
26
|
-
"A",
|
|
27
|
-
"ARG",
|
|
28
|
-
"B",
|
|
29
|
-
"C4",
|
|
30
|
-
"PT",
|
|
31
|
-
"RSE",
|
|
32
|
-
"RET",
|
|
33
|
-
"SIM",
|
|
34
|
-
"T20",
|
|
35
|
-
"E",
|
|
36
|
-
"W",
|
|
37
|
-
"F",
|
|
38
|
-
"I",
|
|
39
|
-
"N",
|
|
40
|
-
"UP",
|
|
41
|
-
],
|
|
42
|
-
"mccabe": {"max-complexity": 10},
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _coverage_config() -> dict[str, Any]:
|
|
48
|
-
return {
|
|
49
|
-
"run": {
|
|
50
|
-
"branch": True,
|
|
51
|
-
},
|
|
52
|
-
"report": {
|
|
53
|
-
"skip_empty": True,
|
|
54
|
-
"skip_covered": True,
|
|
55
|
-
"show_missing": True,
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
STANDARDS = {
|
|
61
|
-
"tool": {
|
|
62
|
-
"mypy": _mypy_config(),
|
|
63
|
-
"ruff": _ruff_config(),
|
|
64
|
-
"coverage": _coverage_config(),
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@dataclass(frozen=True)
|
|
70
|
-
class PyProjectToml:
|
|
71
|
-
_result: ProgramResult = field(init=False, default=ProgramResult(name="PyProject"))
|
|
72
|
-
|
|
73
|
-
NAME = "pyproject.toml"
|
|
74
|
-
|
|
75
|
-
def sync(self, target: Path) -> Executable:
|
|
76
|
-
return self._sync(self.toml_on(target))
|
|
77
|
-
|
|
78
|
-
def _sync(self, pyproject: Path) -> Executable:
|
|
79
|
-
if not pyproject.exists():
|
|
80
|
-
return self._result.fail(message=f"{self.NAME} not found.")
|
|
81
|
-
|
|
82
|
-
document = tomlkit.parse(pyproject.read_text(encoding="utf-8"))
|
|
83
|
-
original = tomlkit.dumps(document)
|
|
84
|
-
deep_merge(STANDARDS, document)
|
|
85
|
-
modified = tomlkit.dumps(document)
|
|
86
|
-
|
|
87
|
-
if original == modified:
|
|
88
|
-
return self._result.no_changes()
|
|
89
|
-
|
|
90
|
-
pyproject.write_text(modified, encoding="utf-8", newline="\n")
|
|
91
|
-
|
|
92
|
-
return self._result.success(message=f"{self.NAME} synchronized successfully.\n")
|
|
93
|
-
|
|
94
|
-
def toml_on(self, path: Path) -> Path:
|
|
95
|
-
return path / self.NAME
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def deep_merge(source: dict[str, Any], destination: dict[str, Any]) -> None:
|
|
99
|
-
"""Recursively merges the source into destination with TOML formatting."""
|
|
100
|
-
for key, value in source.items():
|
|
101
|
-
match value:
|
|
102
|
-
case dict():
|
|
103
|
-
if key not in destination:
|
|
104
|
-
destination[key] = tomlkit.table()
|
|
105
|
-
deep_merge(value, destination[key])
|
|
106
|
-
case list():
|
|
107
|
-
new_array = tomlkit.array()
|
|
108
|
-
new_array.extend(value)
|
|
109
|
-
new_array.multiline(True)
|
|
110
|
-
destination[key] = new_array
|
|
111
|
-
case _:
|
|
112
|
-
destination[key] = value
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass(frozen=True)
|
|
9
|
-
class TyperDevice:
|
|
10
|
-
color: str = typer.colors.RESET
|
|
11
|
-
|
|
12
|
-
def with_green(self) -> TyperDevice:
|
|
13
|
-
return TyperDevice(typer.colors.GREEN)
|
|
14
|
-
|
|
15
|
-
def with_red(self) -> TyperDevice:
|
|
16
|
-
return TyperDevice(typer.colors.RED)
|
|
17
|
-
|
|
18
|
-
def with_white(self) -> TyperDevice:
|
|
19
|
-
return TyperDevice(typer.colors.WHITE)
|
|
20
|
-
|
|
21
|
-
def with_yellow(self) -> TyperDevice:
|
|
22
|
-
return TyperDevice(typer.colors.YELLOW)
|
|
23
|
-
|
|
24
|
-
def echo(self, message: str) -> TyperDevice:
|
|
25
|
-
if message.strip():
|
|
26
|
-
typer.secho(message, fg=self.color)
|
|
27
|
-
|
|
28
|
-
return self
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from codex.infra.yamler import Yamler
|
|
8
|
-
|
|
9
|
-
from .base import YamlWorkflow
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def hash_of(path: Path) -> str:
|
|
13
|
-
with path.open("rb") as f:
|
|
14
|
-
return hashlib.file_digest(f, hashlib.sha256).hexdigest()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass(frozen=True, kw_only=True)
|
|
18
|
-
class CodexLockWorkflow(YamlWorkflow):
|
|
19
|
-
name: str = "CodexLock"
|
|
20
|
-
path: Path = Path("codex.lock")
|
|
21
|
-
|
|
22
|
-
@property
|
|
23
|
-
def standards(self) -> Yamler:
|
|
24
|
-
return Yamler.parse(
|
|
25
|
-
{
|
|
26
|
-
"hashes": {
|
|
27
|
-
"pyproject": hash_of(Path("pyproject.toml")),
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
def verify(self) -> bool:
|
|
33
|
-
return self.path.exists() and self.standards == Yamler.read(self.path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|