apexcodexpy 0.3.2__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apexcodexpy
3
- Version: 0.3.2
3
+ Version: 0.4.1
4
4
  Summary: The Non-Destructive Configuration Steward for Python Projects.
5
5
  License-File: LICENSE
6
6
  Author: Apex Dev
@@ -68,8 +68,6 @@ STANDARDS = {
68
68
 
69
69
  @dataclass(frozen=True)
70
70
  class PyProjectToml:
71
- dry_run: bool = False
72
-
73
71
  _result: ProgramResult = field(init=False, default=ProgramResult(name="PyProject"))
74
72
 
75
73
  NAME = "pyproject.toml"
@@ -82,17 +80,12 @@ class PyProjectToml:
82
80
  return self._result.fail(message=f"{self.NAME} not found.")
83
81
 
84
82
  document = tomlkit.parse(pyproject.read_text(encoding="utf-8"))
85
-
86
- # Create a copy for comparison if dry-run
87
83
  original = tomlkit.dumps(document)
88
84
  deep_merge(STANDARDS, document)
89
85
  modified = tomlkit.dumps(document)
90
86
 
91
87
  if original == modified:
92
- return self._result.success(message=f"{self.NAME} is already up to date.\n")
93
-
94
- if self.dry_run:
95
- return self._result.success(message=f"Changes to apply:\n{modified}")
88
+ return self._result.success(message="Already up to date.\n")
96
89
 
97
90
  pyproject.write_text(modified, encoding="utf-8", newline="\n")
98
91
 
@@ -4,16 +4,11 @@ from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
  from time import time
6
6
 
7
- from codex.infra.dependabot import DependabotYaml
8
- from codex.infra.program import (
9
- Command,
10
- Executable,
11
- MultiProgram,
12
- Program,
13
- )
7
+ from codex.infra.program import Command, Executable, MultiProgram, Program
14
8
  from codex.infra.pyproject import PyProjectToml
15
9
  from codex.infra.result import TaskResult
16
10
  from codex.infra.tools import MyPy, Pip, Poetry, PoetryRun, Pytest, Ruff
11
+ from codex.infra.workflows import CheckCodeWorkflow, DependabotWorkflow
17
12
 
18
13
 
19
14
  @dataclass(frozen=True)
@@ -30,8 +25,8 @@ class Codex:
30
25
 
31
26
  return self
32
27
 
33
- def sync(self, dry_run: bool = False) -> TaskResult:
34
- return self.tools.sync(dry_run=dry_run).execute()
28
+ def sync(self) -> TaskResult:
29
+ return self.tools.sync().execute()
35
30
 
36
31
  def lock(self) -> TaskResult:
37
32
  return self.tools.lock().execute()
@@ -102,11 +97,12 @@ class CiCodex:
102
97
  class CodexTools:
103
98
  target: Path
104
99
 
105
- def sync(self, dry_run: bool = False) -> Executable:
100
+ def sync(self) -> Executable:
106
101
  return (
107
102
  MultiProgram()
108
- .attach(PyProjectToml(dry_run=dry_run).sync(self.target))
109
- .attach(DependabotYaml(dry_run=dry_run).sync(self.target))
103
+ .attach(PyProjectToml().sync(self.target))
104
+ .attach(DependabotWorkflow().sync(self.target))
105
+ .attach(CheckCodeWorkflow().sync(self.target))
110
106
  )
111
107
 
112
108
  def lock(self) -> Executable:
@@ -19,15 +19,10 @@ def _(
19
19
  "-p",
20
20
  help="Target directory.",
21
21
  ),
22
- dry_run: bool = typer.Option(
23
- False,
24
- "--dry-run",
25
- help="Show changes without applying them.",
26
- ),
27
22
  ) -> None:
28
23
  (
29
24
  Codex.target(Path(path))
30
- .sync(dry_run=dry_run)
25
+ .sync()
31
26
  .report(using=DefaultReporter())
32
27
  .exit(using=typer.Exit)
33
28
  )
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from datetime import UTC, datetime
7
+ from functools import cached_property
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+ from yaml import MappingNode
13
+
14
+ from codex.infra.program import Executable
15
+ from codex.infra.result import ProgramResult
16
+
17
+
18
+ @dataclass(frozen=True, kw_only=True)
19
+ class YamlWorkflow(ABC):
20
+ name: str
21
+ path: Path
22
+
23
+ @property
24
+ @abstractmethod
25
+ def standards(self) -> Mapping[Any, Any]:
26
+ pass
27
+
28
+ def sync(self, target: Path) -> Executable:
29
+ path = target / self.path
30
+ path.parent.mkdir(parents=True, exist_ok=True)
31
+ path.write_text(
32
+ "# Auto Generated\n"
33
+ "# By: Apex Codex\n"
34
+ f"# At: {self._utcnow()}\n"
35
+ "# Do not edit this file manually!\n\n"
36
+ + yaml.dump(
37
+ self.standards,
38
+ Dumper=CustomDumper,
39
+ sort_keys=False,
40
+ default_flow_style=False,
41
+ ).replace("'on':", "on:"),
42
+ encoding="utf-8",
43
+ newline="\n",
44
+ )
45
+
46
+ return self.success()
47
+
48
+ def _utcnow(self) -> str:
49
+ return datetime.now(tz=UTC).replace(microsecond=0).isoformat(sep=" ")
50
+
51
+ def success(self) -> ProgramResult:
52
+ return ProgramResult(name=self.name).success(
53
+ message=f"{self.name} workflow succeeded.\n"
54
+ )
55
+
56
+
57
+ class CustomDumper(yaml.Dumper):
58
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
59
+ self.custom_anchors = iter(["checkout", "setup-python"])
60
+ super().__init__(*args, **kwargs)
61
+
62
+ def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
63
+ _ = indentless
64
+
65
+ return super().increase_indent(flow, False)
66
+
67
+ def generate_anchor(self, node: MappingNode) -> str:
68
+ """Override PyYAML's default 'id001' generation logic."""
69
+ try:
70
+ return next(self.custom_anchors)
71
+ except StopIteration:
72
+ return super().generate_anchor(node) # type: ignore
73
+
74
+
75
+ @dataclass(frozen=True, kw_only=True)
76
+ class CheckCodeWorkflow(YamlWorkflow):
77
+ name: str = "CheckCode"
78
+ path: Path = Path(".github") / "workflows" / "check_code.yml"
79
+
80
+ python_version: float | int = 3.13
81
+ checkout_version: str = "v6"
82
+ setup_python_version: str = "v6"
83
+
84
+ @property
85
+ def standards(self) -> Mapping[str, Any]:
86
+ return {
87
+ "name": "Validate Python Code",
88
+ "on": {"pull_request": {"branches": ["main"]}},
89
+ "permissions": {"contents": "read"},
90
+ "jobs": self._jobs(
91
+ lint="codex lint --ci",
92
+ test_unit="codex test unit --ci",
93
+ test_integration="codex test integration --ci",
94
+ test_behaviour="codex test behaviour --ci",
95
+ ),
96
+ }
97
+
98
+ def _jobs(self, **commands: str) -> dict[str, Any]:
99
+ return {
100
+ name.replace("_", "-"): {
101
+ "runs-on": "ubuntu-latest",
102
+ "steps": [
103
+ self._checkout,
104
+ self._setup,
105
+ {
106
+ "name": name.capitalize().replace("_", " "),
107
+ "run": value,
108
+ },
109
+ ],
110
+ }
111
+ for name, value in commands.items()
112
+ }
113
+
114
+ @cached_property
115
+ def _checkout(self) -> dict[str, str]:
116
+ return {"uses": f"actions/checkout@{self.checkout_version}"}
117
+
118
+ @cached_property
119
+ def _setup(self) -> dict[str, Any]:
120
+ return {
121
+ "name": "Set up Python",
122
+ "uses": "actions/setup-python@v6",
123
+ "with": {
124
+ "python-version": self.python_version,
125
+ "pip-install": "apexcodexpy",
126
+ },
127
+ }
128
+
129
+
130
+ @dataclass(frozen=True, kw_only=True)
131
+ class DependabotWorkflow(YamlWorkflow):
132
+ name: str = "Dependabot"
133
+ path: Path = Path(".github") / "dependabot.yml"
134
+
135
+ @property
136
+ def standards(self) -> Mapping[str, Any]:
137
+ return {
138
+ "version": 2,
139
+ "updates": [
140
+ self._pip_config(),
141
+ self._gh_actions_config(),
142
+ ],
143
+ }
144
+
145
+ @staticmethod
146
+ def _pip_config() -> dict[str, Any]:
147
+ return {
148
+ "package-ecosystem": "pip",
149
+ "directory": "/",
150
+ "schedule": {
151
+ "interval": "weekly",
152
+ "day": "thursday",
153
+ },
154
+ "groups": {
155
+ "python": {
156
+ "patterns": ["*"],
157
+ }
158
+ },
159
+ }
160
+
161
+ @staticmethod
162
+ def _gh_actions_config() -> dict[str, Any]:
163
+ return {
164
+ "package-ecosystem": "github-actions",
165
+ "directory": "/",
166
+ "schedule": {
167
+ "interval": "weekly",
168
+ "day": "thursday",
169
+ },
170
+ "groups": {
171
+ "gh-actions": {
172
+ "patterns": ["*"],
173
+ }
174
+ },
175
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "apexcodexpy"
3
- version = "0.3.2"
3
+ version = "0.4.1"
4
4
  description = "The Non-Destructive Configuration Steward for Python Projects."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,95 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from dataclasses import dataclass, field
5
- from pathlib import Path
6
- from typing import Any
7
-
8
- import yaml
9
-
10
- from codex.infra.program import Executable
11
- from codex.infra.result import ProgramResult
12
-
13
-
14
- def _pip_config() -> dict[str, Any]:
15
- return {
16
- "package-ecosystem": "pip",
17
- "directory": "/",
18
- "schedule": {
19
- "interval": "weekly",
20
- "day": "thursday",
21
- },
22
- "groups": {
23
- "python": {
24
- "patterns": ["*"],
25
- }
26
- },
27
- }
28
-
29
-
30
- def _gh_actions_config() -> dict[str, Any]:
31
- return {
32
- "package-ecosystem": "github-actions",
33
- "directory": "/",
34
- "schedule": {
35
- "interval": "weekly",
36
- "day": "thursday",
37
- },
38
- "groups": {
39
- "gh-actions": {
40
- "patterns": ["*"],
41
- }
42
- },
43
- }
44
-
45
-
46
- STANDARDS = {
47
- "version": 2,
48
- "updates": [
49
- _pip_config(),
50
- _gh_actions_config(),
51
- ],
52
- }
53
-
54
-
55
- @dataclass(frozen=True)
56
- class DependabotYaml:
57
- dry_run: bool = False
58
-
59
- _result: ProgramResult = field(init=False, default=ProgramResult(name="DependaBot"))
60
-
61
- NAME = "dependabot.yml"
62
-
63
- def sync(self, target: Path) -> Executable:
64
- dependabot = self.yaml_on(target)
65
- dependabot.parent.mkdir(parents=True, exist_ok=True)
66
-
67
- return self._sync(dependabot)
68
-
69
- def _sync(self, dependabot: Path) -> Executable:
70
- if self.dry_run:
71
- dumped = json.dumps(STANDARDS, indent=2)
72
- return self._result.success(message=f"Changes to apply:\n{dumped}")
73
-
74
- with dependabot.open(mode="w", encoding="utf-8", newline="\n") as stream:
75
- yaml.dump(
76
- STANDARDS,
77
- stream,
78
- Dumper=IndentDumper,
79
- sort_keys=False,
80
- default_flow_style=False,
81
- )
82
-
83
- return self._result.success(message=f"{self.NAME} synchronized successfully.\n")
84
-
85
- def yaml_on(self, path: Path) -> Path:
86
- return path / ".github" / self.NAME
87
-
88
-
89
- class IndentDumper(yaml.Dumper):
90
- """Custom dumper to force indentation for list items."""
91
-
92
- def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
93
- _ = indentless
94
-
95
- return super().increase_indent(flow, False)
File without changes
File without changes
File without changes