apexcodexpy 0.5.4__tar.gz → 0.6.2__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.
Files changed (25) hide show
  1. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/PKG-INFO +1 -1
  2. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/service.py +11 -6
  3. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/typer/root.py +7 -1
  4. apexcodexpy-0.6.2/codex/infra/workflows/__init__.py +9 -0
  5. apexcodexpy-0.6.2/codex/infra/workflows/base.py +53 -0
  6. apexcodexpy-0.6.2/codex/infra/workflows/build.py +78 -0
  7. apexcodexpy-0.6.2/codex/infra/workflows/ci.py +57 -0
  8. apexcodexpy-0.6.2/codex/infra/workflows/dependabot.py +62 -0
  9. apexcodexpy-0.6.2/codex/infra/workflows/validate.py +81 -0
  10. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/yamler.py +1 -1
  11. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/pyproject.toml +1 -1
  12. apexcodexpy-0.5.4/codex/infra/workflows.py +0 -222
  13. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/LICENSE +0 -0
  14. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/README.md +0 -0
  15. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/__init__.py +0 -0
  16. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/__init__.py +0 -0
  17. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/program.py +0 -0
  18. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/pyproject.py +0 -0
  19. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/reporters.py +0 -0
  20. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/result.py +0 -0
  21. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/tools.py +0 -0
  22. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/typer/__init__.py +0 -0
  23. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/typer/device.py +0 -0
  24. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/infra/typer/testing.py +0 -0
  25. {apexcodexpy-0.5.4 → apexcodexpy-0.6.2}/codex/runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apexcodexpy
3
- Version: 0.5.4
3
+ Version: 0.6.2
4
4
  Summary: The Non-Destructive Configuration Steward for Python Projects.
5
5
  License-File: LICENSE
6
6
  Author: Apex Dev
@@ -17,7 +17,11 @@ from codex.infra.tools import (
17
17
  Ruff,
18
18
  Version,
19
19
  )
20
- from codex.infra.workflows import CheckCodeWorkflow, DependabotWorkflow
20
+ from codex.infra.workflows import (
21
+ BuildCodeWorkflow,
22
+ CheckCodeWorkflow,
23
+ DependabotWorkflow,
24
+ )
21
25
 
22
26
 
23
27
  @dataclass(frozen=True)
@@ -38,8 +42,8 @@ class Codex:
38
42
  def version() -> TaskResult:
39
43
  return ProgramResult(name="version").success(message=str(Version()))
40
44
 
41
- def sync(self) -> TaskResult:
42
- return self.tools.sync().execute()
45
+ def sync(self, force: bool) -> TaskResult:
46
+ return self.tools.sync(force).execute()
43
47
 
44
48
  def lock(self) -> TaskResult:
45
49
  return self.tools.lock().execute()
@@ -126,12 +130,13 @@ class CiCodex:
126
130
  class CodexTools:
127
131
  target: Path
128
132
 
129
- def sync(self) -> Executable:
133
+ def sync(self, force: bool) -> Executable:
130
134
  return (
131
135
  MultiProgram()
132
136
  .attach(PyProjectToml().sync(self.target))
133
- .attach(DependabotWorkflow().sync(self.target))
134
- .attach(CheckCodeWorkflow().sync(self.target))
137
+ .attach(DependabotWorkflow().sync(self.target, force=force))
138
+ .attach(CheckCodeWorkflow().sync(self.target, force=force))
139
+ .attach(BuildCodeWorkflow(disclaimer=False).sync(self.target, force=force))
135
140
  )
136
141
 
137
142
  def lock(self) -> Executable:
@@ -27,10 +27,16 @@ def _(
27
27
  "-p",
28
28
  help="Target directory.",
29
29
  ),
30
+ force: bool = typer.Option(
31
+ False,
32
+ "--force",
33
+ "-f",
34
+ help="Force sync.",
35
+ ),
30
36
  ) -> None:
31
37
  (
32
38
  Codex.target(Path(path))
33
- .sync()
39
+ .sync(force=force)
34
40
  .report(using=DefaultReporter())
35
41
  .exit(using=typer.Exit)
36
42
  )
@@ -0,0 +1,9 @@
1
+ from .build import BuildCodeWorkflow
2
+ from .dependabot import DependabotWorkflow
3
+ from .validate import CheckCodeWorkflow
4
+
5
+ __all__ = [
6
+ "DependabotWorkflow",
7
+ "BuildCodeWorkflow",
8
+ "CheckCodeWorkflow",
9
+ ]
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+
8
+ from codex.infra.program import Executable
9
+ from codex.infra.result import ProgramResult
10
+ from codex.infra.tools import Version
11
+ from codex.infra.yamler import Yamler
12
+
13
+
14
+ @dataclass(frozen=True, kw_only=True)
15
+ class YamlWorkflow(ABC):
16
+ name: str
17
+ path: Path
18
+
19
+ disclaimer: bool = True
20
+
21
+ @property
22
+ @abstractmethod
23
+ def standards(self) -> Yamler:
24
+ pass
25
+
26
+ def sync(self, target: Path, force: bool = False) -> Executable:
27
+ path = target / self.path
28
+ path.parent.mkdir(parents=True, exist_ok=True)
29
+ path.touch(exist_ok=True)
30
+
31
+ standards = self.standards
32
+ if not force and Yamler.read(path) == standards:
33
+ return ProgramResult(name=self.name).no_changes()
34
+
35
+ if self.disclaimer:
36
+ standards = standards.comment(
37
+ "Auto Generated",
38
+ f"By: Apex Codex {Version()}",
39
+ f"At: {self._utcnow()}",
40
+ "Do not edit this file manually!\n\n",
41
+ )
42
+
43
+ standards.dump_to(path)
44
+
45
+ return self.success()
46
+
47
+ def _utcnow(self) -> str:
48
+ return datetime.now(tz=UTC).replace(microsecond=0).isoformat(sep=" ")
49
+
50
+ def success(self) -> ProgramResult:
51
+ return ProgramResult(name=self.name).success(
52
+ message=f"{self.name} workflow succeeded.\n"
53
+ )
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from ruamel.yaml.scalarstring import FoldedScalarString
7
+
8
+ from codex.infra.tools import Version
9
+ from codex.infra.yamler import Yamler
10
+
11
+ from .base import YamlWorkflow
12
+ from .ci import CiJob
13
+
14
+
15
+ @dataclass(frozen=True, kw_only=True)
16
+ class BuildCodeWorkflow(YamlWorkflow):
17
+ name: str = "BuildCode"
18
+ path: Path = Path(".github") / "workflows" / "build_code.yml"
19
+
20
+ @property
21
+ def standards(self) -> Yamler:
22
+ workflow = Yamler.read(self.path).as_mapping()
23
+ workflow["name"] = "Build Python Application"
24
+ workflow["on"] = {"push": {"branches": ["main"]}}
25
+ workflow["permissions"] = "write-all"
26
+ workflow["jobs"] = workflow.get("jobs", {})
27
+ workflow["jobs"]["build"] = (
28
+ CiJob(name="build")
29
+ .with_step(
30
+ Yamler.parse(
31
+ {
32
+ "name": "Checkout Code",
33
+ "uses": "actions/checkout@v7",
34
+ }
35
+ )
36
+ )
37
+ .with_step(
38
+ Yamler.parse(
39
+ {
40
+ "name": "Setup Python",
41
+ "uses": "actions/setup-python@v6",
42
+ "with": {
43
+ "python-version": Version.of_python(),
44
+ "pip-install": "apexcodexpy",
45
+ },
46
+ }
47
+ )
48
+ )
49
+ .run(
50
+ step="Build Docker image",
51
+ command=FoldedScalarString(
52
+ "\n".join(
53
+ [
54
+ "codex build",
55
+ " --tag ${{ github.event.repository.name }}:latest",
56
+ ]
57
+ )
58
+ ),
59
+ )
60
+ .run(
61
+ step="Push Docker image",
62
+ command=FoldedScalarString(
63
+ "\n".join(
64
+ [
65
+ "codex push",
66
+ " --tag ${{ github.event.repository.name }}:latest",
67
+ " --token ${{ secrets.GITHUB_TOKEN }}",
68
+ " --actor ${{ github.actor }}",
69
+ " --organization ${{ github.repository_owner }}",
70
+ ]
71
+ )
72
+ ),
73
+ )
74
+ .yamlify()
75
+ .as_mapping()
76
+ )
77
+
78
+ return Yamler.parse(workflow)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping, MutableSequence
4
+ from dataclasses import dataclass, field, replace
5
+
6
+ from codex.infra.yamler import Yamler
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CiWorkflow:
11
+ name: str
12
+
13
+ jobs: MutableMapping[str, Yamler] = field(default_factory=dict)
14
+
15
+ def with_job(self, value: CiJob) -> CiWorkflow:
16
+ if value.is_enabled:
17
+ self.jobs[value.name] = value.yamlify()
18
+
19
+ return self
20
+
21
+ def yamlify(self) -> Yamler:
22
+ return Yamler.parse(
23
+ {
24
+ "name": self.name,
25
+ "on": {"pull_request": {"branches": ["main"]}},
26
+ "permissions": {"contents": "read"},
27
+ "jobs": {name: job.as_mapping() for name, job in self.jobs.items()},
28
+ }
29
+ )
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CiJob:
34
+ name: str
35
+
36
+ is_enabled: bool = True
37
+ platform: str = "ubuntu-latest"
38
+ steps: MutableSequence[Yamler] = field(default_factory=list)
39
+
40
+ def with_step(self, step: Yamler) -> CiJob:
41
+ self.steps.append(step)
42
+
43
+ return self
44
+
45
+ def run(self, step: str, command: str) -> CiJob:
46
+ return self.with_step(Yamler.parse({"name": step, "run": command}))
47
+
48
+ def enabled(self, when: bool) -> CiJob:
49
+ return replace(self, is_enabled=when)
50
+
51
+ def yamlify(self) -> Yamler:
52
+ return Yamler.parse(
53
+ {
54
+ "runs-on": self.platform,
55
+ "steps": [step.as_mapping() for step in self.steps],
56
+ }
57
+ )
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from codex.infra.yamler import Yamler
7
+
8
+ from .base import YamlWorkflow
9
+
10
+
11
+ @dataclass(frozen=True, kw_only=True)
12
+ class DependabotWorkflow(YamlWorkflow):
13
+ name: str = "Dependabot"
14
+ path: Path = Path(".github") / "dependabot.yml"
15
+
16
+ @property
17
+ def standards(self) -> Yamler:
18
+ return Yamler.parse(
19
+ {
20
+ "version": 2,
21
+ "updates": [
22
+ self._pip_config().as_mapping(),
23
+ self._gh_actions_config().as_mapping(),
24
+ ],
25
+ }
26
+ )
27
+
28
+ @staticmethod
29
+ def _pip_config() -> Yamler:
30
+ return Yamler.parse(
31
+ {
32
+ "package-ecosystem": "pip",
33
+ "directory": "/",
34
+ "schedule": {
35
+ "interval": "weekly",
36
+ "day": "thursday",
37
+ },
38
+ "groups": {
39
+ "python": {
40
+ "patterns": ["*"],
41
+ }
42
+ },
43
+ }
44
+ )
45
+
46
+ @staticmethod
47
+ def _gh_actions_config() -> Yamler:
48
+ return Yamler.parse(
49
+ {
50
+ "package-ecosystem": "github-actions",
51
+ "directory": "/",
52
+ "schedule": {
53
+ "interval": "weekly",
54
+ "day": "thursday",
55
+ },
56
+ "groups": {
57
+ "gh-actions": {
58
+ "patterns": ["*"],
59
+ }
60
+ },
61
+ }
62
+ )
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from codex.infra.tools import Version
7
+ from codex.infra.yamler import Yamler
8
+
9
+ from .base import YamlWorkflow
10
+ from .ci import CiJob, CiWorkflow
11
+
12
+
13
+ @dataclass(frozen=True, kw_only=True)
14
+ class CheckCodeWorkflow(YamlWorkflow):
15
+ name: str = "CheckCode"
16
+ path: Path = Path(".github") / "workflows" / "check_code.yml"
17
+
18
+ CHECKOUT = Yamler.parse(
19
+ {
20
+ "name": "Checkout Code",
21
+ "uses": "actions/checkout@v7",
22
+ }
23
+ ).anchor("checkout")
24
+
25
+ SETUP = Yamler.parse(
26
+ {
27
+ "name": "Setup Python",
28
+ "uses": "actions/setup-python@v6",
29
+ "with": {
30
+ "python-version": Version.of_python(),
31
+ "pip-install": f"apexcodexpy=={Version()}",
32
+ },
33
+ }
34
+ ).anchor("setup")
35
+
36
+ @property
37
+ def standards(self) -> Yamler:
38
+ return (
39
+ CiWorkflow(name="Validate Python Code")
40
+ .with_job(
41
+ CiJob(name="lint")
42
+ .with_step(self.CHECKOUT)
43
+ .with_step(self.SETUP)
44
+ .run(
45
+ step="Lint Code",
46
+ command="codex lint --ci",
47
+ )
48
+ .enabled(True)
49
+ )
50
+ .with_job(
51
+ CiJob(name="test-unit")
52
+ .with_step(self.CHECKOUT)
53
+ .with_step(self.SETUP)
54
+ .run(
55
+ step="Run unit tests",
56
+ command="codex test unit --ci",
57
+ )
58
+ .enabled(True)
59
+ )
60
+ .with_job(
61
+ CiJob(name="test-integration")
62
+ .with_step(self.CHECKOUT)
63
+ .with_step(self.SETUP)
64
+ .run(
65
+ step="Run integration tests",
66
+ command="codex test integration --ci",
67
+ )
68
+ .enabled(when=Version().is_installed("pytest-docker"))
69
+ )
70
+ .with_job(
71
+ CiJob(name="test-behaviour")
72
+ .with_step(self.CHECKOUT)
73
+ .with_step(self.SETUP)
74
+ .run(
75
+ step="Run behaviour tests",
76
+ command="codex test behaviour --ci",
77
+ )
78
+ .enabled(when=Version().is_installed("behave"))
79
+ )
80
+ .yamlify()
81
+ )
@@ -18,7 +18,7 @@ class Yamler:
18
18
 
19
19
  @classmethod
20
20
  def read(cls, path: Path) -> Yamler:
21
- return cls(value=YAML().load(path))
21
+ return cls(value=YAML(typ="safe").load(path) or CommentedMap())
22
22
 
23
23
  @classmethod
24
24
  def parse(cls, data: Mapping[str, Any]) -> Yamler:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "apexcodexpy"
3
- version = "0.5.4"
3
+ version = "0.6.2"
4
4
  description = "The Non-Destructive Configuration Steward for Python Projects."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,222 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC, abstractmethod
4
- from collections.abc import MutableMapping, MutableSequence
5
- from dataclasses import dataclass, field, replace
6
- from datetime import UTC, datetime
7
- from pathlib import Path
8
-
9
- from codex.infra.program import Executable
10
- from codex.infra.result import ProgramResult
11
- from codex.infra.tools import Version
12
- from codex.infra.yamler import Yamler
13
-
14
-
15
- @dataclass(frozen=True, kw_only=True)
16
- class YamlWorkflow(ABC):
17
- name: str
18
- path: Path
19
-
20
- @property
21
- @abstractmethod
22
- def standards(self) -> Yamler:
23
- pass
24
-
25
- def sync(self, target: Path) -> Executable:
26
- path = target / self.path
27
- path.parent.mkdir(parents=True, exist_ok=True)
28
- path.touch(exist_ok=True)
29
-
30
- if Yamler.read(path) == self.standards:
31
- return ProgramResult(name=self.name).no_changes()
32
-
33
- self.standards.comment(
34
- "Auto Generated",
35
- f"By: Apex Codex {Version()}",
36
- f"At: {self._utcnow()}",
37
- "Do not edit this file manually!\n\n",
38
- ).dump_to(path)
39
-
40
- return self.success()
41
-
42
- def _utcnow(self) -> str:
43
- return datetime.now(tz=UTC).replace(microsecond=0).isoformat(sep=" ")
44
-
45
- def success(self) -> ProgramResult:
46
- return ProgramResult(name=self.name).success(
47
- message=f"{self.name} workflow succeeded.\n"
48
- )
49
-
50
-
51
- @dataclass(frozen=True, kw_only=True)
52
- class DependabotWorkflow(YamlWorkflow):
53
- name: str = "Dependabot"
54
- path: Path = Path(".github") / "dependabot.yml"
55
-
56
- @property
57
- def standards(self) -> Yamler:
58
- return Yamler.parse(
59
- {
60
- "version": 2,
61
- "updates": [
62
- self._pip_config().as_mapping(),
63
- self._gh_actions_config().as_mapping(),
64
- ],
65
- }
66
- )
67
-
68
- @staticmethod
69
- def _pip_config() -> Yamler:
70
- return Yamler.parse(
71
- {
72
- "package-ecosystem": "pip",
73
- "directory": "/",
74
- "schedule": {
75
- "interval": "weekly",
76
- "day": "thursday",
77
- },
78
- "groups": {
79
- "python": {
80
- "patterns": ["*"],
81
- }
82
- },
83
- }
84
- )
85
-
86
- @staticmethod
87
- def _gh_actions_config() -> Yamler:
88
- return Yamler.parse(
89
- {
90
- "package-ecosystem": "github-actions",
91
- "directory": "/",
92
- "schedule": {
93
- "interval": "weekly",
94
- "day": "thursday",
95
- },
96
- "groups": {
97
- "gh-actions": {
98
- "patterns": ["*"],
99
- }
100
- },
101
- }
102
- )
103
-
104
-
105
- @dataclass(frozen=True, kw_only=True)
106
- class CheckCodeWorkflow(YamlWorkflow):
107
- name: str = "CheckCode"
108
- path: Path = Path(".github") / "workflows" / "check_code.yml"
109
-
110
- @property
111
- def standards(self) -> Yamler:
112
- return (
113
- CiWorkflow(name="Validate Python Code")
114
- .with_job(
115
- CodeCheckJob(name="lint")
116
- .run(
117
- step="Lint Code",
118
- command="codex lint --ci",
119
- )
120
- .enabled(True)
121
- )
122
- .with_job(
123
- CodeCheckJob(name="test-unit")
124
- .run(
125
- step="Run unit tests",
126
- command="codex test unit --ci",
127
- )
128
- .enabled(True)
129
- )
130
- .with_job(
131
- CodeCheckJob(name="test-integration")
132
- .run(
133
- step="Run integration tests",
134
- command="codex test integration --ci",
135
- )
136
- .enabled(when=Version().is_installed("pytest-docker"))
137
- )
138
- .with_job(
139
- CodeCheckJob(name="test-behaviour")
140
- .run(
141
- step="Run behaviour tests",
142
- command="codex test behaviour --ci",
143
- )
144
- .enabled(when=Version().is_installed("behave"))
145
- )
146
- .yamlify()
147
- )
148
-
149
-
150
- @dataclass(frozen=True)
151
- class CiWorkflow:
152
- name: str
153
-
154
- jobs: MutableMapping[str, Yamler] = field(default_factory=dict)
155
-
156
- def with_job(self, value: CiJob) -> CiWorkflow:
157
- if value.is_enabled:
158
- self.jobs[value.name] = value.yamlify()
159
-
160
- return self
161
-
162
- def yamlify(self) -> Yamler:
163
- return Yamler.parse(
164
- {
165
- "name": self.name,
166
- "on": {"pull_request": {"branches": ["main"]}},
167
- "permissions": {"contents": "read"},
168
- "jobs": {name: job.as_mapping() for name, job in self.jobs.items()},
169
- }
170
- )
171
-
172
-
173
- @dataclass(frozen=True)
174
- class CiJob:
175
- name: str
176
-
177
- is_enabled: bool = True
178
- platform: str = "ubuntu-latest"
179
- steps: MutableSequence[Yamler] = field(default_factory=list)
180
-
181
- def with_step(self, step: Yamler) -> CiJob:
182
- self.steps.append(step)
183
-
184
- return self
185
-
186
- def enabled(self, when: bool) -> CiJob:
187
- return replace(self, is_enabled=when)
188
-
189
- def yamlify(self) -> Yamler:
190
- return Yamler.parse(
191
- {
192
- "runs-on": self.platform,
193
- "steps": [step.as_mapping() for step in self.steps],
194
- }
195
- )
196
-
197
-
198
- CHECKOUT = Yamler.parse(
199
- {
200
- "name": "Checkout Code",
201
- "uses": "actions/checkout@v7",
202
- }
203
- ).anchor("checkout")
204
-
205
- SETUP = Yamler.parse(
206
- {
207
- "name": "Setup Python",
208
- "uses": "actions/setup-python@v6",
209
- "with": {
210
- "python-version": Version.of_python(),
211
- "pip-install": f"apexcodexpy=={Version()}",
212
- },
213
- }
214
- ).anchor("setup")
215
-
216
-
217
- @dataclass(frozen=True)
218
- class CodeCheckJob(CiJob):
219
- steps: MutableSequence[Yamler] = field(default_factory=lambda: [CHECKOUT, SETUP])
220
-
221
- def run(self, step: str, command: str) -> CiJob:
222
- return self.with_step(Yamler.parse({"name": step, "run": command}))
File without changes
File without changes
File without changes