bakefile 0.0.4__py3-none-any.whl → 0.0.9__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.
- bake/__init__.py +9 -0
- bake/bakebook/bakebook.py +85 -0
- bake/bakebook/decorator.py +50 -0
- bake/bakebook/get.py +175 -0
- bake/cli/bake/__init__.py +3 -0
- bake/cli/bake/__main__.py +5 -0
- bake/cli/bake/main.py +74 -0
- bake/cli/bake/reinvocation.py +63 -0
- bake/cli/bakefile/__init__.py +3 -0
- bake/cli/bakefile/__main__.py +5 -0
- bake/cli/bakefile/add_inline.py +29 -0
- bake/cli/bakefile/export.py +212 -0
- bake/cli/bakefile/find_python.py +18 -0
- bake/cli/bakefile/init.py +56 -0
- bake/cli/bakefile/lint.py +77 -0
- bake/cli/bakefile/main.py +43 -0
- bake/cli/bakefile/uv.py +146 -0
- bake/cli/common/app.py +54 -0
- bake/cli/common/callback.py +13 -0
- bake/cli/common/context.py +145 -0
- bake/cli/common/exception_handler.py +57 -0
- bake/cli/common/obj.py +216 -0
- bake/cli/common/params.py +72 -0
- bake/cli/utils/__init__.py +0 -0
- bake/cli/utils/version.py +18 -0
- bake/manage/__init__.py +0 -0
- bake/manage/add_inline.py +71 -0
- bake/manage/find_python.py +210 -0
- bake/manage/lint.py +101 -0
- bake/manage/run_uv.py +88 -0
- bake/manage/write_bakefile.py +20 -0
- bake/py.typed +0 -0
- bake/samples/__init__.py +0 -0
- bake/samples/simple.py +8 -0
- bake/ui/__init__.py +11 -0
- bake/ui/console.py +58 -0
- bake/ui/logger/__init__.py +33 -0
- bake/ui/logger/capsys.py +158 -0
- bake/ui/logger/setup.py +53 -0
- bake/ui/logger/utils.py +215 -0
- bake/ui/params.py +5 -0
- bake/ui/run/__init__.py +5 -0
- bake/ui/run/run.py +546 -0
- bake/ui/run/script.py +74 -0
- bake/ui/run/splitter.py +257 -0
- bake/ui/run/uv.py +83 -0
- bake/ui/style.py +2 -0
- bake/utils/__init__.py +11 -0
- bake/utils/constants.py +21 -0
- {bakefile → bake/utils}/env.py +3 -1
- bake/utils/exceptions.py +17 -0
- {bakefile-0.0.4.dist-info → bakefile-0.0.9.dist-info}/METADATA +16 -2
- bakefile-0.0.9.dist-info/RECORD +68 -0
- {bakefile-0.0.4.dist-info → bakefile-0.0.9.dist-info}/WHEEL +2 -2
- bakefile-0.0.9.dist-info/entry_points.txt +5 -0
- bakelib/__init__.py +23 -0
- bakelib/environ/__init__.py +14 -0
- bakelib/environ/bakebook.py +30 -0
- bakelib/environ/base.py +112 -0
- bakelib/environ/get_bakebook.py +49 -0
- bakelib/environ/presets.py +70 -0
- bakelib/space/__init__.py +0 -0
- bakelib/space/base.py +193 -0
- bakelib/space/python.py +89 -0
- bakelib/space/utils.py +118 -0
- bakefile/__init__.py +0 -13
- bakefile/cli/bake/__init__.py +0 -3
- bakefile/cli/bake/main.py +0 -127
- bakefile/cli/bake/resolve_bakebook.py +0 -103
- bakefile/cli/bake/utils.py +0 -25
- bakefile/cli/bakefile.py +0 -19
- bakefile/cli/utils/version.py +0 -9
- bakefile/exceptions.py +0 -9
- bakefile-0.0.4.dist-info/RECORD +0 -16
- bakefile-0.0.4.dist-info/entry_points.txt +0 -4
- {bakefile/cli/utils → bake/bakebook}/__init__.py +0 -0
- {bakefile → bake}/cli/__init__.py +0 -0
- /bakefile/py.typed → /bake/cli/common/__init__.py +0 -0
bakelib/environ/base.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
from pydantic import GetCoreSchemaHandler
|
|
4
|
+
from pydantic_core import CoreSchema, core_schema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseEnv(str):
|
|
8
|
+
"""BaseEnvironment string with comparison and Pydantic support.
|
|
9
|
+
|
|
10
|
+
Inherits from str to provide natural string behavior while adding
|
|
11
|
+
comparison operators for priority-based ordering.
|
|
12
|
+
|
|
13
|
+
ENV_ORDER is a list where each element is either:
|
|
14
|
+
- A string (normal priority, order matters)
|
|
15
|
+
- A set of strings (equal priority group)
|
|
16
|
+
|
|
17
|
+
Example: ["dev", "staging", {"prod", "share"}]
|
|
18
|
+
- "dev" has highest priority (index 0)
|
|
19
|
+
- "staging" has medium priority (index 1)
|
|
20
|
+
- "prod" and "share" have equal lowest priority (both in set at index 2)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
ENV_ORDER: ClassVar[list[str | set[str]]] = ["dev", "staging", "prod"]
|
|
24
|
+
|
|
25
|
+
def __init__(self, value: str):
|
|
26
|
+
if not self._is_valid(value):
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"Invalid {self.__class__.__name__}: '{value}'. "
|
|
29
|
+
f"Must be one of: {self._flattened_env_order()}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _is_valid(cls, value: str) -> bool:
|
|
34
|
+
try:
|
|
35
|
+
cls._get_priority_index(value)
|
|
36
|
+
return True
|
|
37
|
+
except ValueError:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _flattened_env_order(cls) -> list[str]:
|
|
42
|
+
result: list[str] = []
|
|
43
|
+
for item in cls.ENV_ORDER:
|
|
44
|
+
if isinstance(item, set):
|
|
45
|
+
result.extend(sorted(item))
|
|
46
|
+
else:
|
|
47
|
+
result.append(item)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _get_priority_index(cls, value: str) -> int:
|
|
52
|
+
for idx, item in enumerate(cls.ENV_ORDER):
|
|
53
|
+
is_in_set = isinstance(item, set) and value in item
|
|
54
|
+
is_equal = item == value
|
|
55
|
+
if is_in_set or is_equal:
|
|
56
|
+
return idx
|
|
57
|
+
raise ValueError(f"Value '{value}' not found in ENV_ORDER")
|
|
58
|
+
|
|
59
|
+
def __lt__(self, other: str) -> bool:
|
|
60
|
+
if type(other) is not type(self):
|
|
61
|
+
return NotImplemented
|
|
62
|
+
self_idx = self._get_priority_index(str(self))
|
|
63
|
+
other_idx = self._get_priority_index(str(other))
|
|
64
|
+
if self_idx != other_idx:
|
|
65
|
+
return self_idx < other_idx
|
|
66
|
+
# Same priority group, use alphabetical as tiebreaker
|
|
67
|
+
return str(self) < str(other)
|
|
68
|
+
|
|
69
|
+
def __le__(self, other: str) -> bool:
|
|
70
|
+
if type(other) is not type(self):
|
|
71
|
+
return NotImplemented
|
|
72
|
+
return self < other or self == other
|
|
73
|
+
|
|
74
|
+
def __gt__(self, other: str) -> bool:
|
|
75
|
+
if type(other) is not type(self):
|
|
76
|
+
return NotImplemented
|
|
77
|
+
return not (self < other) and self != other
|
|
78
|
+
|
|
79
|
+
def __ge__(self, other: str) -> bool:
|
|
80
|
+
if type(other) is not type(self):
|
|
81
|
+
return NotImplemented
|
|
82
|
+
return not (self < other)
|
|
83
|
+
|
|
84
|
+
def __eq__(self, other: object) -> bool:
|
|
85
|
+
if type(other) is not type(self):
|
|
86
|
+
return False
|
|
87
|
+
return str(self) == str(other)
|
|
88
|
+
|
|
89
|
+
def __ne__(self, other: object) -> bool:
|
|
90
|
+
return not self.__eq__(other)
|
|
91
|
+
|
|
92
|
+
def __hash__(self) -> int:
|
|
93
|
+
return hash(str(self))
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return f"{self.__class__.__name__}('{self!s}')"
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def __get_pydantic_core_schema__(
|
|
100
|
+
cls, source_type: Any, handler: GetCoreSchemaHandler
|
|
101
|
+
) -> CoreSchema:
|
|
102
|
+
"""Pydantic v2 integration for custom type validation."""
|
|
103
|
+
return core_schema.no_info_after_validator_function(cls, handler(str))
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def validate(cls, v: object) -> "BaseEnv":
|
|
107
|
+
"""Validate and convert input to BaseEnv."""
|
|
108
|
+
if isinstance(v, cls):
|
|
109
|
+
return v
|
|
110
|
+
if isinstance(v, str):
|
|
111
|
+
return cls(v)
|
|
112
|
+
raise ValueError(f"Cannot convert {type(v).__name__} to {cls.__name__}")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv as _load_dotenv
|
|
5
|
+
|
|
6
|
+
from .bakebook import EnvBakebook
|
|
7
|
+
|
|
8
|
+
# TODO: When min Python >= 3.12, use PEP 695 type parameter syntax:
|
|
9
|
+
# def get_bakebook[E: EnvBakebook](bakebooks: list[E], ...) -> E:
|
|
10
|
+
E = TypeVar("E", bound=EnvBakebook)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_bakebook(
|
|
14
|
+
bakebooks: list[E],
|
|
15
|
+
*,
|
|
16
|
+
env_var_name: str = "ENV",
|
|
17
|
+
env_value: str | None = None,
|
|
18
|
+
load_dotenv: bool = True,
|
|
19
|
+
) -> E:
|
|
20
|
+
if not bakebooks:
|
|
21
|
+
raise ValueError("bakebooks list cannot be empty")
|
|
22
|
+
|
|
23
|
+
# Convert to dict for O(1) lookup and duplicate detection
|
|
24
|
+
bakebooks_by_env: dict[str, E] = {}
|
|
25
|
+
for bb in bakebooks:
|
|
26
|
+
if not hasattr(bb, "env") or bb.env is None:
|
|
27
|
+
raise ValueError(f"All bakebooks must have an 'env' attribute. Found: {bb}")
|
|
28
|
+
env = str(bb.env)
|
|
29
|
+
if env in bakebooks_by_env:
|
|
30
|
+
raise ValueError(f"Duplicate env '{env}' found in bakebooks list")
|
|
31
|
+
bakebooks_by_env[env] = bb
|
|
32
|
+
|
|
33
|
+
# Get environment value
|
|
34
|
+
if env_value is None:
|
|
35
|
+
if load_dotenv:
|
|
36
|
+
_load_dotenv()
|
|
37
|
+
env_value = os.getenv(env_var_name)
|
|
38
|
+
|
|
39
|
+
# Env var not set - return lowest priority (min)
|
|
40
|
+
if env_value is None or env_value == "":
|
|
41
|
+
return bakebooks_by_env[str(min(bb.env for bb in bakebooks))]
|
|
42
|
+
# If env var is set, require exact match
|
|
43
|
+
elif env_value in bakebooks_by_env:
|
|
44
|
+
return bakebooks_by_env[env_value]
|
|
45
|
+
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"No bakebook found with env='{env_value}'. "
|
|
48
|
+
f"Available envs: {sorted(bakebooks_by_env.keys())}"
|
|
49
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Preset environment configurations.
|
|
2
|
+
|
|
3
|
+
This module contains pre-configured environment classes for common use cases.
|
|
4
|
+
Users can also create their own environments by inheriting from BaseEnv.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from bakelib.environ.base import BaseEnv
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GcpLandingZoneEnv(BaseEnv):
|
|
13
|
+
"""GCP Landing Zone base environments.
|
|
14
|
+
|
|
15
|
+
Environment codes follow GCP Security Foundations Blueprint conventions:
|
|
16
|
+
https://docs.cloud.google.com/architecture/blueprints/security-foundations/summary
|
|
17
|
+
|
|
18
|
+
Environment Codes:
|
|
19
|
+
- d - Development
|
|
20
|
+
- n - Nonproduction
|
|
21
|
+
- p - Production
|
|
22
|
+
- s - Shared
|
|
23
|
+
- b - Bootstrap
|
|
24
|
+
- c - Common
|
|
25
|
+
- net - Network
|
|
26
|
+
|
|
27
|
+
Tiers (ordered by priority, lower index = higher priority):
|
|
28
|
+
- d (development) - lowest priority
|
|
29
|
+
- n (nonprod)
|
|
30
|
+
- p/s/b/c/net - highest priority, all equal (production + shared)
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
env = GcpLandingZoneEnv("d")
|
|
34
|
+
assert env < GcpLandingZoneEnv("n")
|
|
35
|
+
assert GcpLandingZoneEnv("n") < GcpLandingZoneEnv("p")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
ENV_ORDER: ClassVar[list[str | set[str]]] = [
|
|
39
|
+
"d",
|
|
40
|
+
"n",
|
|
41
|
+
{"p", "s", "b", "c", "net"},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
def is_shared(self) -> bool:
|
|
45
|
+
return self.code in {"s", "b", "c", "net"}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def name(self) -> str:
|
|
49
|
+
names = {
|
|
50
|
+
"d": "Development",
|
|
51
|
+
"n": "Nonproduction",
|
|
52
|
+
"p": "Production",
|
|
53
|
+
"s": "Shared",
|
|
54
|
+
"b": "Bootstrap",
|
|
55
|
+
"c": "Common",
|
|
56
|
+
"net": "Network",
|
|
57
|
+
}
|
|
58
|
+
return names[self.code]
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def code(self) -> str:
|
|
62
|
+
return str(self)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def secondary_name(self) -> str:
|
|
66
|
+
return self.name if not self.is_shared() else "Shared"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def secondary_code(self) -> str:
|
|
70
|
+
return self.code if not self.is_shared() else "s"
|
|
File without changes
|
bakelib/space/base.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, Literal
|
|
3
|
+
|
|
4
|
+
import orjson
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from bake import Bakebook, Context, command
|
|
8
|
+
from bake.ui import console
|
|
9
|
+
|
|
10
|
+
from .utils import (
|
|
11
|
+
HOMWBREW_BIN,
|
|
12
|
+
LOCAL_BIN,
|
|
13
|
+
VENV_BIN,
|
|
14
|
+
PlatformType,
|
|
15
|
+
ToolInfo,
|
|
16
|
+
get_expected_paths,
|
|
17
|
+
get_platform,
|
|
18
|
+
remove_git_clean_candidates,
|
|
19
|
+
setup_brew,
|
|
20
|
+
setup_bun,
|
|
21
|
+
setup_uv,
|
|
22
|
+
setup_uv_tool,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseSpace(Bakebook):
|
|
27
|
+
def _no_implementation(self, ctx: Context | None = None, *args, **kwargs):
|
|
28
|
+
_ = ctx, args, kwargs
|
|
29
|
+
console.error("No implementation")
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
@command(help="Run linters and formatters")
|
|
33
|
+
def lint(self, ctx: Context) -> None:
|
|
34
|
+
ctx.run('bunx prettier@latest --write "**/*.{js,jsx,ts,tsx,css,json,json5,yaml,yml,md\'}"')
|
|
35
|
+
|
|
36
|
+
@command(help="Run unit tests")
|
|
37
|
+
def test(self, ctx: Context) -> None:
|
|
38
|
+
self._no_implementation(ctx)
|
|
39
|
+
|
|
40
|
+
@command(help="Run integration tests")
|
|
41
|
+
def test_integration(self, ctx: Context) -> None:
|
|
42
|
+
self._no_implementation(ctx)
|
|
43
|
+
|
|
44
|
+
@command(help="Run all tests")
|
|
45
|
+
def test_all(self, ctx: Context) -> None:
|
|
46
|
+
self._no_implementation(ctx)
|
|
47
|
+
|
|
48
|
+
@command(help="Clean gitignored files with optional exclusions")
|
|
49
|
+
def clean(
|
|
50
|
+
self,
|
|
51
|
+
ctx: Context,
|
|
52
|
+
exclude_patterns: Annotated[
|
|
53
|
+
list[str] | None,
|
|
54
|
+
typer.Option(
|
|
55
|
+
"--exclude-patterns",
|
|
56
|
+
"-e",
|
|
57
|
+
help="Patterns to exclude",
|
|
58
|
+
),
|
|
59
|
+
] = None,
|
|
60
|
+
use_default_excludes: Annotated[
|
|
61
|
+
bool,
|
|
62
|
+
typer.Option(
|
|
63
|
+
"--no-default-excludes",
|
|
64
|
+
help="Do not apply default exclude patterns",
|
|
65
|
+
is_flag=True,
|
|
66
|
+
),
|
|
67
|
+
] = False,
|
|
68
|
+
) -> None:
|
|
69
|
+
results = ctx.run("git clean -fdX -n", stream=False, dry_run=False, echo=True)
|
|
70
|
+
|
|
71
|
+
exclude_patterns: set[str] = set(exclude_patterns if exclude_patterns else [])
|
|
72
|
+
|
|
73
|
+
if not use_default_excludes:
|
|
74
|
+
exclude_patterns |= {".env", ".cache"}
|
|
75
|
+
|
|
76
|
+
console.err.print(f"Exclude pattens: {exclude_patterns}")
|
|
77
|
+
|
|
78
|
+
remove_git_clean_candidates(
|
|
79
|
+
git_clean_dry_run_output=results.stdout,
|
|
80
|
+
exclude_patterns=exclude_patterns,
|
|
81
|
+
dry_run=ctx.dry_run,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@command(help="Clean all gitignored files")
|
|
85
|
+
def clean_all(self, ctx: Context) -> None:
|
|
86
|
+
ctx.run("git clean -fdX")
|
|
87
|
+
|
|
88
|
+
def setup_tool_managers(self, ctx: Context, platform: PlatformType) -> None:
|
|
89
|
+
_ = platform
|
|
90
|
+
setup_brew(ctx)
|
|
91
|
+
|
|
92
|
+
def setup_tools(self, ctx: Context, platform: PlatformType) -> None:
|
|
93
|
+
_ = platform
|
|
94
|
+
setup_bun(ctx)
|
|
95
|
+
setup_uv(ctx)
|
|
96
|
+
setup_uv_tool(ctx)
|
|
97
|
+
|
|
98
|
+
def setup_project(self, ctx: Context) -> None:
|
|
99
|
+
ctx.run("uv run pre-commit install")
|
|
100
|
+
|
|
101
|
+
@command(help="Setup development environment")
|
|
102
|
+
def setup_dev(self, ctx: Context) -> None:
|
|
103
|
+
platform = get_platform()
|
|
104
|
+
console.echo(f"Detected platform: {platform}")
|
|
105
|
+
|
|
106
|
+
if platform != "macos":
|
|
107
|
+
console.warning(f"Platform '{platform}' is not supported. Running in dry-run mode.")
|
|
108
|
+
overridden_dry_run = True
|
|
109
|
+
else:
|
|
110
|
+
overridden_dry_run = ctx.dry_run
|
|
111
|
+
|
|
112
|
+
with ctx.override_dry_run(overridden_dry_run):
|
|
113
|
+
self.clean(ctx=ctx)
|
|
114
|
+
self.setup_tool_managers(ctx=ctx, platform=platform)
|
|
115
|
+
self.setup_tools(ctx=ctx, platform=platform)
|
|
116
|
+
self.setup_project(ctx=ctx)
|
|
117
|
+
|
|
118
|
+
def _assert_which_path(
|
|
119
|
+
self,
|
|
120
|
+
ctx: Context,
|
|
121
|
+
tool_name: str,
|
|
122
|
+
tool_info: ToolInfo,
|
|
123
|
+
) -> bool:
|
|
124
|
+
result = ctx.run(f"which {tool_name}", stream=False)
|
|
125
|
+
if ctx.dry_run:
|
|
126
|
+
return True
|
|
127
|
+
actual_path = Path(result.stdout.strip())
|
|
128
|
+
|
|
129
|
+
if actual_path in set(tool_info.expected_paths):
|
|
130
|
+
console.success(f"{tool_name}: {actual_path}")
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
console.warning(f"{tool_name}: unexpected location (got {actual_path})")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def _get_tools(self) -> dict[str, ToolInfo]:
|
|
137
|
+
return {
|
|
138
|
+
# homebrew only
|
|
139
|
+
"bun": ToolInfo(expected_paths=get_expected_paths("bun", {HOMWBREW_BIN})),
|
|
140
|
+
# homebrew or venv
|
|
141
|
+
"uv": ToolInfo(expected_paths=get_expected_paths("uv", {HOMWBREW_BIN, VENV_BIN})),
|
|
142
|
+
# local or venv
|
|
143
|
+
"bakefile": ToolInfo(
|
|
144
|
+
expected_paths=get_expected_paths("bakefile", {LOCAL_BIN, VENV_BIN})
|
|
145
|
+
),
|
|
146
|
+
"pre-commit": ToolInfo(
|
|
147
|
+
expected_paths=get_expected_paths("pre-commit", {LOCAL_BIN, VENV_BIN})
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@command(help="List development tools")
|
|
152
|
+
def tools(
|
|
153
|
+
self,
|
|
154
|
+
ctx: Context,
|
|
155
|
+
format: Annotated[
|
|
156
|
+
Literal["json", "names"],
|
|
157
|
+
typer.Option("--format", "-f", help="Output format"),
|
|
158
|
+
] = "json",
|
|
159
|
+
) -> None:
|
|
160
|
+
_ = ctx
|
|
161
|
+
tools = self._get_tools()
|
|
162
|
+
if format == "json":
|
|
163
|
+
output: dict[str, dict[str, str | None]] = {k: v.model_dump() for k, v in tools.items()}
|
|
164
|
+
console.echo(orjson.dumps(output, option=orjson.OPT_INDENT_2).decode())
|
|
165
|
+
else:
|
|
166
|
+
console.echo("\n".join(sorted(tools.keys())))
|
|
167
|
+
|
|
168
|
+
@command(help="Assert development environment setup")
|
|
169
|
+
def assert_setup_dev(
|
|
170
|
+
self,
|
|
171
|
+
ctx: Context,
|
|
172
|
+
skip_test: Annotated[
|
|
173
|
+
bool,
|
|
174
|
+
typer.Option(
|
|
175
|
+
"--skip-test",
|
|
176
|
+
"-s",
|
|
177
|
+
help="Skip running tests",
|
|
178
|
+
is_flag=True,
|
|
179
|
+
),
|
|
180
|
+
] = False,
|
|
181
|
+
) -> None:
|
|
182
|
+
tools = self._get_tools()
|
|
183
|
+
for tool_name, tool_info in tools.items():
|
|
184
|
+
self._assert_which_path(ctx, tool_name, tool_info)
|
|
185
|
+
|
|
186
|
+
self.lint(ctx)
|
|
187
|
+
if not skip_test:
|
|
188
|
+
self.test(ctx)
|
|
189
|
+
|
|
190
|
+
@command(help="Upgrade all dependencies")
|
|
191
|
+
def update(self, ctx: Context) -> None:
|
|
192
|
+
ctx.run("uv python upgrade")
|
|
193
|
+
ctx.run("uv tool upgrade --all")
|
bakelib/space/python.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from bake import Context, params
|
|
4
|
+
|
|
5
|
+
from .base import BaseSpace, ToolInfo
|
|
6
|
+
from .utils import VENV_BIN, get_expected_paths
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_python_version() -> str | None:
|
|
10
|
+
path = Path(".python-version")
|
|
11
|
+
if not path.exists():
|
|
12
|
+
return None
|
|
13
|
+
return path.read_text().strip()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PythonSpace(BaseSpace):
|
|
17
|
+
def _get_tools(self) -> dict[str, ToolInfo]:
|
|
18
|
+
tools = super()._get_tools()
|
|
19
|
+
tools["python"] = ToolInfo(
|
|
20
|
+
version=_get_python_version(),
|
|
21
|
+
expected_paths=list(get_expected_paths("python", {VENV_BIN})),
|
|
22
|
+
)
|
|
23
|
+
return tools
|
|
24
|
+
|
|
25
|
+
def lint(self, ctx: Context) -> None:
|
|
26
|
+
super().lint(ctx=ctx)
|
|
27
|
+
|
|
28
|
+
ctx.run(
|
|
29
|
+
"uv run toml-sort --sort-inline-arrays --in-place "
|
|
30
|
+
"--sort-first=project,dependency-groups pyproject.toml"
|
|
31
|
+
)
|
|
32
|
+
ctx.run("uv run ruff format --exit-non-zero-on-format .")
|
|
33
|
+
ctx.run("uv run ruff check --fix --exit-non-zero-on-fix .")
|
|
34
|
+
ctx.run("uv run ty check --error-on-warning --no-progress .")
|
|
35
|
+
ctx.run("uv run deptry .")
|
|
36
|
+
|
|
37
|
+
def _test(
|
|
38
|
+
self,
|
|
39
|
+
ctx: Context,
|
|
40
|
+
*,
|
|
41
|
+
tests_paths: str | list[str],
|
|
42
|
+
verbose: bool = False,
|
|
43
|
+
coverage_report: bool = True,
|
|
44
|
+
) -> None:
|
|
45
|
+
paths = tests_paths if isinstance(tests_paths, str) else " ".join(tests_paths)
|
|
46
|
+
|
|
47
|
+
cmd = f"uv run pytest {paths}"
|
|
48
|
+
|
|
49
|
+
if coverage_report:
|
|
50
|
+
cmd += " --cov=src --cov-report=html --cov-report=term-missing --cov-report=xml"
|
|
51
|
+
|
|
52
|
+
if verbose:
|
|
53
|
+
cmd += " -s -v"
|
|
54
|
+
|
|
55
|
+
ctx.run(cmd)
|
|
56
|
+
|
|
57
|
+
def test_integration(
|
|
58
|
+
self,
|
|
59
|
+
ctx: Context,
|
|
60
|
+
verbose: params.verbose_bool = False,
|
|
61
|
+
) -> None:
|
|
62
|
+
integration_tests_path = "tests/integration/"
|
|
63
|
+
if Path(integration_tests_path).exists():
|
|
64
|
+
tests_path = integration_tests_path
|
|
65
|
+
self._test(ctx, tests_paths=tests_path, verbose=verbose)
|
|
66
|
+
else:
|
|
67
|
+
self._no_implementation(ctx)
|
|
68
|
+
|
|
69
|
+
def test(self, ctx: Context) -> None:
|
|
70
|
+
unit_tests_path = "tests/unit/"
|
|
71
|
+
tests_path = unit_tests_path if Path(unit_tests_path).exists() else "tests/"
|
|
72
|
+
self._test(ctx, tests_paths=tests_path)
|
|
73
|
+
|
|
74
|
+
def test_all(self, ctx: Context) -> None:
|
|
75
|
+
unit_tests_path = "tests/unit/"
|
|
76
|
+
if Path(unit_tests_path).exists():
|
|
77
|
+
tests_path = "tests/"
|
|
78
|
+
self._test(ctx, tests_paths=tests_path)
|
|
79
|
+
else:
|
|
80
|
+
self._no_implementation(ctx)
|
|
81
|
+
|
|
82
|
+
def setup_project(self, ctx: Context) -> None:
|
|
83
|
+
super().setup_project(ctx=ctx)
|
|
84
|
+
ctx.run("uv sync --all-extras --all-groups --frozen")
|
|
85
|
+
|
|
86
|
+
def update(self, ctx: Context) -> None:
|
|
87
|
+
super().update(ctx=ctx)
|
|
88
|
+
ctx.run("uv lock --upgrade")
|
|
89
|
+
ctx.run("uv sync --all-extras --all-groups")
|
bakelib/space/utils.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import pathspec
|
|
8
|
+
from pathspec.patterns.gitignore.basic import GitIgnoreBasicPattern
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from bake import Context
|
|
12
|
+
from bake.ui import console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def setup_brew(ctx: Context) -> None:
|
|
16
|
+
ctx.run("brew update")
|
|
17
|
+
ctx.run("brew upgrade")
|
|
18
|
+
ctx.run("brew cleanup")
|
|
19
|
+
ctx.run("brew list")
|
|
20
|
+
ctx.run("brew leaves")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolInfo(BaseModel):
|
|
24
|
+
version: str | None = None
|
|
25
|
+
expected_paths: list[Path] = Field(default_factory=list, exclude=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Platform(Enum):
|
|
29
|
+
MACOS = "macos"
|
|
30
|
+
LINUX = "linux"
|
|
31
|
+
WINDOWS = "windows"
|
|
32
|
+
OTHER = "other"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PlatformType = Literal["macos", "linux", "windows", "other"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_platform() -> PlatformType:
|
|
39
|
+
if sys.platform == "darwin":
|
|
40
|
+
return Platform.MACOS.value
|
|
41
|
+
elif sys.platform == "linux":
|
|
42
|
+
return Platform.LINUX.value
|
|
43
|
+
elif sys.platform == "win32":
|
|
44
|
+
return Platform.WINDOWS.value
|
|
45
|
+
return Platform.OTHER.value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def setup_uv(ctx: Context) -> None:
|
|
49
|
+
ctx.run("brew install uv")
|
|
50
|
+
ctx.run("uv python upgrade")
|
|
51
|
+
ctx.run("uv tool upgrade --all")
|
|
52
|
+
ctx.run("uv tool update-shell")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def setup_bun(ctx: Context) -> None:
|
|
56
|
+
ctx.run("brew install oven-sh/bun/bun")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def setup_uv_tool(ctx: Context) -> None:
|
|
60
|
+
ctx.run("uv tool install bakefile")
|
|
61
|
+
ctx.run("uv tool install pre-commit")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
HOMWBREW_BIN = Path("/opt/homebrew/bin")
|
|
65
|
+
LOCAL_BIN = Path.home() / ".local" / "bin"
|
|
66
|
+
VENV_BIN = Path.cwd() / ".venv" / "bin"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_expected_paths(tool: str, locations: set[Path]) -> list[Path]:
|
|
70
|
+
return [loc / tool for loc in locations]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _skip_msg(path: Path, suffix: str, dry_run: bool) -> None:
|
|
74
|
+
verb = "Would skip" if dry_run else "Skipping"
|
|
75
|
+
console.echo(f"[yellow]~[/yellow] {verb} {suffix}{path}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _remove_msg(path: Path, dry_run: bool) -> None:
|
|
79
|
+
verb = "Would remove" if dry_run else "Removing"
|
|
80
|
+
console.echo(f"[red]-[/red] [dim]{verb}[/dim] {path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _should_remove_path(path: Path, dry_run: bool) -> None:
|
|
84
|
+
_remove_msg(path, dry_run)
|
|
85
|
+
if dry_run:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if path.is_dir():
|
|
89
|
+
shutil.rmtree(path)
|
|
90
|
+
else:
|
|
91
|
+
path.unlink(missing_ok=True)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def remove_git_clean_candidates(
|
|
95
|
+
git_clean_dry_run_output: str, exclude_patterns: set[str], dry_run: bool
|
|
96
|
+
) -> None:
|
|
97
|
+
spec = pathspec.PathSpec.from_lines(
|
|
98
|
+
GitIgnoreBasicPattern,
|
|
99
|
+
exclude_patterns,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for line in git_clean_dry_run_output.splitlines():
|
|
103
|
+
line = line.strip()
|
|
104
|
+
if not line.startswith("Would remove "):
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
rel_path = line.removeprefix("Would remove ").strip()
|
|
108
|
+
path = Path(rel_path)
|
|
109
|
+
|
|
110
|
+
if spec.match_file(rel_path):
|
|
111
|
+
_skip_msg(path, "", dry_run)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if path.is_dir() and (path / ".git").exists():
|
|
115
|
+
_skip_msg(path, "git repository ", dry_run)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
_should_remove_path(path, dry_run)
|
bakefile/__init__.py
DELETED
bakefile/cli/bake/__init__.py
DELETED