bakefile 0.0.9__py3-none-any.whl → 0.0.11__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/cli/bake/main.py +51 -9
- bake/cli/bake/reinvocation.py +6 -3
- bake/cli/bakefile/main.py +38 -4
- bake/cli/common/app.py +35 -33
- bake/cli/common/context.py +4 -0
- bake/cli/common/obj.py +4 -3
- bake/cli/common/params.py +10 -2
- bake/manage/find_python.py +26 -9
- bake/manage/lint.py +10 -7
- bake/ui/console.py +73 -9
- bake/ui/run/run.py +45 -13
- bake/ui/run/splitter.py +42 -31
- bake/ui/run/uv.py +2 -10
- bake/utils/__init__.py +8 -2
- bake/utils/settings.py +25 -0
- {bakefile-0.0.9.dist-info → bakefile-0.0.11.dist-info}/METADATA +3 -1
- {bakefile-0.0.9.dist-info → bakefile-0.0.11.dist-info}/RECORD +28 -24
- {bakefile-0.0.9.dist-info → bakefile-0.0.11.dist-info}/WHEEL +1 -1
- bakelib/__init__.py +2 -0
- bakelib/refreshable_cache/__init__.py +17 -0
- bakelib/refreshable_cache/cache.py +250 -0
- bakelib/refreshable_cache/exceptions.py +2 -0
- bakelib/space/base.py +29 -20
- bakelib/space/lib.py +161 -0
- bakelib/space/python.py +17 -0
- bakelib/space/python_lib.py +77 -0
- bakelib/space/utils.py +10 -0
- bake/cli/common/callback.py +0 -13
- bake/utils/env.py +0 -10
- {bakefile-0.0.9.dist-info → bakefile-0.0.11.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, ParamSpec, TypeVar
|
|
9
|
+
|
|
10
|
+
import keyring as kr
|
|
11
|
+
from keyring.errors import PasswordDeleteError
|
|
12
|
+
from pydantic import BaseModel, TypeAdapter
|
|
13
|
+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_none
|
|
14
|
+
|
|
15
|
+
from bakelib.refreshable_cache.exceptions import RefreshNeededError
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from tenacity.stop import StopBaseT
|
|
19
|
+
from tenacity.wait import WaitBaseT
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
P = ParamSpec("P")
|
|
25
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
CachedT = TypeVar("CachedT", covariant=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CacheEntry(BaseModel, Generic[CachedT]):
|
|
31
|
+
"""Cache entry containing the cached value and timestamp."""
|
|
32
|
+
|
|
33
|
+
value: CachedT
|
|
34
|
+
timestamp: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
DEFAULT_NAMESPACE = "bakelib.refreshable_cache"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RefreshableCache(ABC, Generic[CachedT]):
|
|
41
|
+
"""Cache that can be refreshed when values expire or become invalid."""
|
|
42
|
+
|
|
43
|
+
RefreshNeededError: type[RefreshNeededError] = RefreshNeededError
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
key: str,
|
|
48
|
+
fetch_fn: Callable[[], CachedT],
|
|
49
|
+
ttl: float | None = None,
|
|
50
|
+
namespace: str | None = None,
|
|
51
|
+
stop: "StopBaseT | None" = None,
|
|
52
|
+
wait: "WaitBaseT | None" = None,
|
|
53
|
+
cached_type: Any = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._key = key
|
|
56
|
+
self._fetch_fn = fetch_fn
|
|
57
|
+
self._ttl = ttl
|
|
58
|
+
self._namespace = namespace if namespace is not None else DEFAULT_NAMESPACE
|
|
59
|
+
self._stop = stop if stop is not None else stop_after_attempt(2)
|
|
60
|
+
self._wait = wait if wait is not None else wait_none()
|
|
61
|
+
|
|
62
|
+
# Determine cached type: explicit cached_type or infer from fetch_fn
|
|
63
|
+
if cached_type is not None:
|
|
64
|
+
return_type = cached_type
|
|
65
|
+
else:
|
|
66
|
+
return_type = inspect.signature(fetch_fn).return_annotation
|
|
67
|
+
if return_type is inspect.Parameter.empty:
|
|
68
|
+
msg = "fetch_fn must have a return type annotation or cached_type must be provided"
|
|
69
|
+
raise TypeError(msg)
|
|
70
|
+
|
|
71
|
+
self._adapter = TypeAdapter(CacheEntry[return_type])
|
|
72
|
+
|
|
73
|
+
def _get_full_key(self) -> str:
|
|
74
|
+
return f"{self._namespace}:{self._key}"
|
|
75
|
+
|
|
76
|
+
def _serialize_entry(self, value: CachedT) -> bytes:
|
|
77
|
+
entry = CacheEntry(value=value, timestamp=time.time())
|
|
78
|
+
return self._adapter.dump_json(entry)
|
|
79
|
+
|
|
80
|
+
def _deserialize_entry(self, data: bytes) -> CacheEntry[CachedT]:
|
|
81
|
+
return self._adapter.validate_json(data)
|
|
82
|
+
|
|
83
|
+
def _is_expired(self, timestamp: float) -> bool:
|
|
84
|
+
if self._ttl is None:
|
|
85
|
+
return False
|
|
86
|
+
return time.time() - timestamp > self._ttl
|
|
87
|
+
|
|
88
|
+
def get_value(self) -> CachedT:
|
|
89
|
+
cached = self._get_entry()
|
|
90
|
+
if cached is None:
|
|
91
|
+
logger.debug(f"Cache miss for key '{self._key}', fetching value")
|
|
92
|
+
return self._refresh()
|
|
93
|
+
if self._is_expired(cached.timestamp):
|
|
94
|
+
logger.debug(f"Cache expired for key '{self._key}', fetching fresh value")
|
|
95
|
+
return self._refresh()
|
|
96
|
+
logger.debug(f"Cache hit for key '{self._key}'")
|
|
97
|
+
return cached.value
|
|
98
|
+
|
|
99
|
+
def _refresh(self) -> CachedT:
|
|
100
|
+
logger.debug(f"Refreshing value for key '{self._key}'")
|
|
101
|
+
value = self._fetch_fn()
|
|
102
|
+
self.set(value)
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
def catch_refresh(self, func: Callable[P, T]) -> Callable[P, T]:
|
|
106
|
+
@functools.wraps(func)
|
|
107
|
+
@retry(
|
|
108
|
+
stop=self._stop,
|
|
109
|
+
wait=self._wait,
|
|
110
|
+
retry=retry_if_exception_type(self.RefreshNeededError),
|
|
111
|
+
reraise=True,
|
|
112
|
+
)
|
|
113
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
114
|
+
try:
|
|
115
|
+
return func(*args, **kwargs)
|
|
116
|
+
except self.RefreshNeededError:
|
|
117
|
+
self.delete()
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
return wrapper
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def _get_entry(self) -> CacheEntry[CachedT] | None: ...
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def set(self, value: CachedT) -> None: ...
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def delete(self) -> None: ...
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class KeyringCache(RefreshableCache[CachedT]):
|
|
133
|
+
"""Cache using system keyring for persistent storage."""
|
|
134
|
+
|
|
135
|
+
def _get_entry(self) -> CacheEntry[CachedT] | None:
|
|
136
|
+
data = kr.get_password(self._namespace, self._key)
|
|
137
|
+
if data is None:
|
|
138
|
+
return None
|
|
139
|
+
return self._deserialize_entry(data.encode())
|
|
140
|
+
|
|
141
|
+
def set(self, value: CachedT) -> None:
|
|
142
|
+
data = self._serialize_entry(value).decode()
|
|
143
|
+
kr.set_password(self._namespace, self._key, data)
|
|
144
|
+
|
|
145
|
+
def delete(self) -> None:
|
|
146
|
+
with contextlib.suppress(PasswordDeleteError):
|
|
147
|
+
kr.delete_password(self._namespace, self._key)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MemoryCache(RefreshableCache[CachedT]):
|
|
151
|
+
"""In-memory cache for ephemeral storage."""
|
|
152
|
+
|
|
153
|
+
_storage: ClassVar[dict[str, CacheEntry[CachedT]]] = {}
|
|
154
|
+
|
|
155
|
+
def _get_entry(self) -> CacheEntry[CachedT] | None:
|
|
156
|
+
entry = self._storage.get(self._get_full_key())
|
|
157
|
+
if entry is None:
|
|
158
|
+
return None
|
|
159
|
+
return entry
|
|
160
|
+
|
|
161
|
+
def set(self, value: CachedT) -> None:
|
|
162
|
+
self._storage[self._get_full_key()] = CacheEntry(value=value, timestamp=time.time())
|
|
163
|
+
|
|
164
|
+
def delete(self) -> None:
|
|
165
|
+
self._storage.pop(self._get_full_key(), None)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class NullCache(RefreshableCache[CachedT]):
|
|
169
|
+
"""Cache that doesn't cache anything (Null Object pattern).
|
|
170
|
+
|
|
171
|
+
Useful as a final fallback when you want to explicitly disable caching.
|
|
172
|
+
Reads always return None (triggering fetch), writes/deletes do nothing.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def _get_entry(self) -> CacheEntry[CachedT] | None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def set(self, value: CachedT) -> None:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
def delete(self) -> None:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class ChainedCache(RefreshableCache[CachedT]):
|
|
186
|
+
"""Tries multiple backends in order.
|
|
187
|
+
|
|
188
|
+
Reads from the first backend that has data.
|
|
189
|
+
Writes to all backends (stops on first success).
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
_backends: list[RefreshableCache[CachedT]]
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self,
|
|
196
|
+
backends: list[type[RefreshableCache[CachedT]]],
|
|
197
|
+
key: str,
|
|
198
|
+
fetch_fn: Callable[[], CachedT],
|
|
199
|
+
ttl: float | None = None,
|
|
200
|
+
namespace: str | None = None,
|
|
201
|
+
stop: "StopBaseT | None" = None,
|
|
202
|
+
wait: "WaitBaseT | None" = None,
|
|
203
|
+
cached_type: Any = None,
|
|
204
|
+
) -> None:
|
|
205
|
+
super().__init__(
|
|
206
|
+
key=key,
|
|
207
|
+
fetch_fn=fetch_fn,
|
|
208
|
+
ttl=ttl,
|
|
209
|
+
namespace=namespace,
|
|
210
|
+
stop=stop,
|
|
211
|
+
wait=wait,
|
|
212
|
+
cached_type=cached_type,
|
|
213
|
+
)
|
|
214
|
+
self._backends = [
|
|
215
|
+
backend(
|
|
216
|
+
key=key,
|
|
217
|
+
fetch_fn=fetch_fn,
|
|
218
|
+
ttl=ttl,
|
|
219
|
+
namespace=namespace,
|
|
220
|
+
stop=stop,
|
|
221
|
+
wait=wait,
|
|
222
|
+
cached_type=cached_type,
|
|
223
|
+
)
|
|
224
|
+
for backend in backends
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
def _get_entry(self) -> CacheEntry[CachedT] | None:
|
|
228
|
+
for backend in self._backends:
|
|
229
|
+
try:
|
|
230
|
+
entry = backend._get_entry()
|
|
231
|
+
if entry is not None:
|
|
232
|
+
return entry
|
|
233
|
+
except Exception:
|
|
234
|
+
continue
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def set(self, value: CachedT) -> None:
|
|
238
|
+
for backend in self._backends:
|
|
239
|
+
try:
|
|
240
|
+
backend.set(value)
|
|
241
|
+
return
|
|
242
|
+
except Exception:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
def delete(self) -> None:
|
|
246
|
+
for backend in self._backends:
|
|
247
|
+
try:
|
|
248
|
+
backend.delete()
|
|
249
|
+
except Exception:
|
|
250
|
+
continue
|
bakelib/space/base.py
CHANGED
|
@@ -45,6 +45,28 @@ class BaseSpace(Bakebook):
|
|
|
45
45
|
def test_all(self, ctx: Context) -> None:
|
|
46
46
|
self._no_implementation(ctx)
|
|
47
47
|
|
|
48
|
+
def _clean(
|
|
49
|
+
self,
|
|
50
|
+
ctx: Context,
|
|
51
|
+
exclude_patterns: list[str] | None,
|
|
52
|
+
default_excludes: bool,
|
|
53
|
+
default_exclude_patterns: set[str],
|
|
54
|
+
):
|
|
55
|
+
results = ctx.run("git clean -fdX -n", stream=False, dry_run=False, echo=True)
|
|
56
|
+
|
|
57
|
+
exclude_patterns: set[str] = set(exclude_patterns if exclude_patterns else [])
|
|
58
|
+
|
|
59
|
+
if default_excludes:
|
|
60
|
+
exclude_patterns |= default_exclude_patterns
|
|
61
|
+
|
|
62
|
+
console.err.print(f"Exclude pattens: {exclude_patterns}")
|
|
63
|
+
|
|
64
|
+
remove_git_clean_candidates(
|
|
65
|
+
git_clean_dry_run_output=results.stdout,
|
|
66
|
+
exclude_patterns=exclude_patterns,
|
|
67
|
+
dry_run=ctx.dry_run,
|
|
68
|
+
)
|
|
69
|
+
|
|
48
70
|
@command(help="Clean gitignored files with optional exclusions")
|
|
49
71
|
def clean(
|
|
50
72
|
self,
|
|
@@ -57,28 +79,16 @@ class BaseSpace(Bakebook):
|
|
|
57
79
|
help="Patterns to exclude",
|
|
58
80
|
),
|
|
59
81
|
] = None,
|
|
60
|
-
|
|
82
|
+
default_excludes: Annotated[
|
|
61
83
|
bool,
|
|
62
|
-
typer.Option(
|
|
63
|
-
|
|
64
|
-
help="Do not apply default exclude patterns",
|
|
65
|
-
is_flag=True,
|
|
66
|
-
),
|
|
67
|
-
] = False,
|
|
84
|
+
typer.Option(help="Apply default exclude patterns (.env, .cache)"),
|
|
85
|
+
] = True,
|
|
68
86
|
) -> None:
|
|
69
|
-
|
|
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,
|
|
87
|
+
self._clean(
|
|
88
|
+
ctx=ctx,
|
|
80
89
|
exclude_patterns=exclude_patterns,
|
|
81
|
-
|
|
90
|
+
default_excludes=default_excludes,
|
|
91
|
+
default_exclude_patterns={".env", ".cache"},
|
|
82
92
|
)
|
|
83
93
|
|
|
84
94
|
@command(help="Clean all gitignored files")
|
|
@@ -175,7 +185,6 @@ class BaseSpace(Bakebook):
|
|
|
175
185
|
"--skip-test",
|
|
176
186
|
"-s",
|
|
177
187
|
help="Skip running tests",
|
|
178
|
-
is_flag=True,
|
|
179
188
|
),
|
|
180
189
|
] = False,
|
|
181
190
|
) -> None:
|
bakelib/space/lib.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pydantic import SecretStr
|
|
9
|
+
from tenacity import stop_after_attempt
|
|
10
|
+
|
|
11
|
+
from bake import Context, command, console
|
|
12
|
+
from bake.ui.logger import strip_ansi
|
|
13
|
+
from bakelib.refreshable_cache import ChainedCache, KeyringCache, NullCache
|
|
14
|
+
|
|
15
|
+
from .base import BaseSpace, ToolInfo
|
|
16
|
+
from .utils import CARGO_BIN, PlatformType, get_expected_paths, setup_rustup, setup_zerv
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PublishResult:
|
|
21
|
+
result: subprocess.CompletedProcess[str] | None
|
|
22
|
+
is_dry_run: bool
|
|
23
|
+
is_auth_failed: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseLibSpace(BaseSpace):
|
|
27
|
+
bake_publish_token: SecretStr | None = None
|
|
28
|
+
_dummy_publish_token: str = "dummy-token-for-dry-run"
|
|
29
|
+
|
|
30
|
+
def setup_tools(self, ctx: Context, platform: PlatformType) -> None:
|
|
31
|
+
_ = platform
|
|
32
|
+
super().setup_tools(ctx, platform=platform)
|
|
33
|
+
setup_rustup(ctx)
|
|
34
|
+
setup_zerv(ctx)
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def _get_publish_token_from_remote(self, registry: str) -> str | None: ...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def package_name(self, ctx: Context) -> str: ...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def _build_for_publish(self, ctx: Context): ...
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def _publish_with_token(
|
|
47
|
+
self, ctx: Context, token: str | None, registry: str
|
|
48
|
+
) -> PublishResult: ...
|
|
49
|
+
|
|
50
|
+
def _get_cached_publish_token(
|
|
51
|
+
self, ctx: Context, token: str | None, registry: str
|
|
52
|
+
) -> ChainedCache[str | None]:
|
|
53
|
+
token_from_local = self._get_token_from_local(token)
|
|
54
|
+
key = f"publish-token-{registry}"
|
|
55
|
+
namespace = self.package_name(ctx)
|
|
56
|
+
|
|
57
|
+
def get_publish_token() -> str | None:
|
|
58
|
+
return token_from_local or self._get_publish_token_from_remote(registry)
|
|
59
|
+
|
|
60
|
+
stop = stop_after_attempt(1) if token_from_local else None
|
|
61
|
+
|
|
62
|
+
cached_publish_token = ChainedCache(
|
|
63
|
+
backends=[KeyringCache, NullCache],
|
|
64
|
+
namespace=namespace,
|
|
65
|
+
key=key,
|
|
66
|
+
fetch_fn=get_publish_token,
|
|
67
|
+
stop=stop,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if token_from_local is not None:
|
|
71
|
+
cached_publish_token.set(token_from_local)
|
|
72
|
+
|
|
73
|
+
return cached_publish_token
|
|
74
|
+
|
|
75
|
+
def _get_token_from_local(self, token: str | None) -> str | None:
|
|
76
|
+
if token:
|
|
77
|
+
return token
|
|
78
|
+
|
|
79
|
+
if self.bake_publish_token:
|
|
80
|
+
return self.bake_publish_token.get_secret_value()
|
|
81
|
+
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
@contextmanager
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def _version_bump_context(self, _ctx: Context, _version: str): ...
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def _pre_publish_cleanup(self, _ctx: Context): ...
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def _version_schema(self) -> str | None:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def _is_auth_failure(self, result: subprocess.CompletedProcess[str]) -> bool:
|
|
96
|
+
return result.returncode != 0
|
|
97
|
+
|
|
98
|
+
def _determine_version(self, ctx: Context, version: str | None) -> str:
|
|
99
|
+
return version if version else self.zerv_versioning(ctx, schema=self._version_schema)
|
|
100
|
+
|
|
101
|
+
@command(help="Build and publish the package")
|
|
102
|
+
def publish(
|
|
103
|
+
self,
|
|
104
|
+
ctx: Context,
|
|
105
|
+
registry: Annotated[str, typer.Option(help="Publish registry")] = "default",
|
|
106
|
+
token: Annotated[str | None, typer.Option(help="Publish token")] = None,
|
|
107
|
+
version: Annotated[str | None, typer.Option(help="Version to publish")] = None,
|
|
108
|
+
):
|
|
109
|
+
cached_publish_token = self._get_cached_publish_token(
|
|
110
|
+
ctx=ctx, token=token, registry=registry
|
|
111
|
+
)
|
|
112
|
+
version = self._determine_version(ctx, version)
|
|
113
|
+
|
|
114
|
+
self._pre_publish_cleanup(ctx)
|
|
115
|
+
|
|
116
|
+
with self._version_bump_context(ctx, version):
|
|
117
|
+
self._build_for_publish(ctx)
|
|
118
|
+
publish_result = self._execute_publish(
|
|
119
|
+
ctx=ctx, cached_publish_token=cached_publish_token, registry=registry
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self._handle_publish_result(ctx, publish_result=publish_result)
|
|
123
|
+
|
|
124
|
+
def _execute_publish(
|
|
125
|
+
self, ctx: Context, cached_publish_token: ChainedCache[str | None], registry: str
|
|
126
|
+
) -> PublishResult:
|
|
127
|
+
@cached_publish_token.catch_refresh
|
|
128
|
+
def _publish() -> PublishResult:
|
|
129
|
+
token_value = cached_publish_token.get_value()
|
|
130
|
+
publish_result = self._publish_with_token(ctx=ctx, token=token_value, registry=registry)
|
|
131
|
+
|
|
132
|
+
if publish_result.result is not None and self._is_auth_failure(publish_result.result):
|
|
133
|
+
raise cached_publish_token.RefreshNeededError
|
|
134
|
+
|
|
135
|
+
return publish_result
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
return _publish()
|
|
139
|
+
except cached_publish_token.RefreshNeededError:
|
|
140
|
+
return PublishResult(result=None, is_dry_run=False, is_auth_failed=True)
|
|
141
|
+
|
|
142
|
+
def _handle_publish_result(self, ctx: Context, publish_result: PublishResult) -> None:
|
|
143
|
+
if publish_result.is_auth_failed:
|
|
144
|
+
console.error("Authentication failed. Please check your publish token.")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
if publish_result.is_dry_run and not ctx.dry_run:
|
|
148
|
+
console.warning(
|
|
149
|
+
"This was a dry-run. To actually publish, "
|
|
150
|
+
"set the BAKE_PUBLISH_TOKEN environment variable"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def zerv_versioning(self, ctx: Context, *, schema: str | None = None) -> str:
|
|
154
|
+
schema_flag = f"--schema {schema}" if schema else ""
|
|
155
|
+
result = ctx.run(f"zerv flow {schema_flag}", dry_run=False)
|
|
156
|
+
return strip_ansi(result.stdout.strip())
|
|
157
|
+
|
|
158
|
+
def _get_tools(self) -> dict[str, ToolInfo]:
|
|
159
|
+
tools = super()._get_tools()
|
|
160
|
+
tools["zerv"] = ToolInfo(expected_paths=get_expected_paths("zerv", {CARGO_BIN}))
|
|
161
|
+
return tools
|
bakelib/space/python.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from bake import Context, params
|
|
5
|
+
from bake.ui.logger import strip_ansi
|
|
4
6
|
|
|
5
7
|
from .base import BaseSpace, ToolInfo
|
|
6
8
|
from .utils import VENV_BIN, get_expected_paths
|
|
@@ -14,6 +16,10 @@ def _get_python_version() -> str | None:
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class PythonSpace(BaseSpace):
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
super().__init__()
|
|
21
|
+
os.environ["UV_NO_PROGRESS"] = "true"
|
|
22
|
+
|
|
17
23
|
def _get_tools(self) -> dict[str, ToolInfo]:
|
|
18
24
|
tools = super()._get_tools()
|
|
19
25
|
tools["python"] = ToolInfo(
|
|
@@ -87,3 +93,14 @@ class PythonSpace(BaseSpace):
|
|
|
87
93
|
super().update(ctx=ctx)
|
|
88
94
|
ctx.run("uv lock --upgrade")
|
|
89
95
|
ctx.run("uv sync --all-extras --all-groups")
|
|
96
|
+
|
|
97
|
+
def _uv_version(self, ctx: Context) -> tuple[str, str]:
|
|
98
|
+
result = ctx.run("uv version", stream=False, dry_run=False, echo=False)
|
|
99
|
+
package_name, original_version = strip_ansi(result.stdout.strip()).split()
|
|
100
|
+
return package_name, original_version
|
|
101
|
+
|
|
102
|
+
def package_name(self, ctx: Context) -> str:
|
|
103
|
+
return self._uv_version(ctx)[0]
|
|
104
|
+
|
|
105
|
+
def current_version(self, ctx: Context) -> str:
|
|
106
|
+
return self._uv_version(ctx)[1]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Annotated, Literal, cast, get_args
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from bake import Context
|
|
9
|
+
|
|
10
|
+
from .lib import BaseLibSpace, PublishResult
|
|
11
|
+
from .python import PythonSpace
|
|
12
|
+
|
|
13
|
+
PublishIndex = Literal["testpypi", "pypi"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PythonLibSpace(PythonSpace, BaseLibSpace):
|
|
17
|
+
@property
|
|
18
|
+
def _version_schema(self) -> str | None:
|
|
19
|
+
return "standard-base-prerelease-post-dev"
|
|
20
|
+
|
|
21
|
+
def _registry_to_index(self, registry: str) -> PublishIndex:
|
|
22
|
+
valid_indices = get_args(PublishIndex)
|
|
23
|
+
if registry not in valid_indices:
|
|
24
|
+
raise ValueError(f"Invalid registry: {registry!r}. Expected one of {valid_indices}.")
|
|
25
|
+
return cast(PublishIndex, registry)
|
|
26
|
+
|
|
27
|
+
def _get_publish_token_from_remote(self, registry: str) -> str | None:
|
|
28
|
+
index = self._registry_to_index(registry)
|
|
29
|
+
_ = index
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def _build_for_publish(self, ctx: Context):
|
|
33
|
+
ctx.run("uv build")
|
|
34
|
+
|
|
35
|
+
def _publish_with_token(self, ctx: Context, token: str | None, registry: str) -> PublishResult:
|
|
36
|
+
index = self._registry_to_index(registry)
|
|
37
|
+
index_flag = f"--index {index} " if index == "testpypi" else ""
|
|
38
|
+
dry_run_flag = "" if token is not None else "--dry-run "
|
|
39
|
+
is_dry_run = token is None
|
|
40
|
+
|
|
41
|
+
env: dict[str, str] = {
|
|
42
|
+
"UV_PUBLISH_TOKEN": token if token is not None else self._dummy_publish_token
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
result = ctx.run(
|
|
46
|
+
f"uv publish {dry_run_flag}{index_flag}",
|
|
47
|
+
stream=True,
|
|
48
|
+
env=env,
|
|
49
|
+
check=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return PublishResult(result=result, is_dry_run=is_dry_run, is_auth_failed=False)
|
|
53
|
+
|
|
54
|
+
def _is_auth_failure(self, result: subprocess.CompletedProcess[str]) -> bool:
|
|
55
|
+
auth_error_message = "403 Invalid or non-existent authentication information"
|
|
56
|
+
return result.returncode != 0 and auth_error_message in result.stderr
|
|
57
|
+
|
|
58
|
+
@contextmanager
|
|
59
|
+
def _version_bump_context(self, ctx: Context, version: str):
|
|
60
|
+
original_version = self.current_version(ctx)
|
|
61
|
+
ctx.run(f"uv version {version}")
|
|
62
|
+
try:
|
|
63
|
+
yield
|
|
64
|
+
finally:
|
|
65
|
+
ctx.run(f"uv version {original_version}")
|
|
66
|
+
|
|
67
|
+
def _pre_publish_cleanup(self, _ctx: Context):
|
|
68
|
+
shutil.rmtree("dist", ignore_errors=True)
|
|
69
|
+
|
|
70
|
+
def publish(
|
|
71
|
+
self,
|
|
72
|
+
ctx: Context,
|
|
73
|
+
index: Annotated[PublishIndex, typer.Option(help="Publish index")] = "testpypi",
|
|
74
|
+
token: Annotated[str | None, typer.Option(help="Publish token")] = None,
|
|
75
|
+
version: Annotated[str | None, typer.Option(help="Version to publish")] = None,
|
|
76
|
+
):
|
|
77
|
+
return super().publish(ctx=ctx, registry=index, token=token, version=version)
|
bakelib/space/utils.py
CHANGED
|
@@ -52,6 +52,15 @@ def setup_uv(ctx: Context) -> None:
|
|
|
52
52
|
ctx.run("uv tool update-shell")
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
def setup_rustup(ctx: Context) -> None:
|
|
56
|
+
ctx.run("brew install rustup")
|
|
57
|
+
ctx.run("rustup update")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setup_zerv(ctx: Context) -> None:
|
|
61
|
+
ctx.run("cargo install zerv")
|
|
62
|
+
|
|
63
|
+
|
|
55
64
|
def setup_bun(ctx: Context) -> None:
|
|
56
65
|
ctx.run("brew install oven-sh/bun/bun")
|
|
57
66
|
|
|
@@ -62,6 +71,7 @@ def setup_uv_tool(ctx: Context) -> None:
|
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
HOMWBREW_BIN = Path("/opt/homebrew/bin")
|
|
74
|
+
CARGO_BIN = Path.home() / ".cargo" / "bin"
|
|
65
75
|
LOCAL_BIN = Path.home() / ".local" / "bin"
|
|
66
76
|
VENV_BIN = Path.cwd() / ".venv" / "bin"
|
|
67
77
|
|
bake/cli/common/callback.py
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def validate_file_name(file_name: str) -> str:
|
|
5
|
-
if "/" in file_name or "\\" in file_name:
|
|
6
|
-
raise typer.BadParameter(f"File name must not contain path separators: {file_name}")
|
|
7
|
-
if not file_name.endswith(".py"):
|
|
8
|
-
raise typer.BadParameter(f"File name must end with .py: {file_name}")
|
|
9
|
-
return file_name
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def validate_file_name_callback(value: str) -> str:
|
|
13
|
-
return validate_file_name(file_name=value)
|
bake/utils/env.py
DELETED
|
File without changes
|