bakefile 0.0.10__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/ui/run/run.py CHANGED
@@ -68,7 +68,8 @@ def _run_with_temp_file(
68
68
  stream: bool,
69
69
  keep_temp_file: bool = False,
70
70
  env: dict[str, str] | None = None,
71
- _encoding: str = "utf-8",
71
+ _encoding: str | None = None,
72
+ echo_cmd: str | None = None,
72
73
  **kwargs,
73
74
  ) -> subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None]:
74
75
  """Run multi-line script using temp file with shebang support.
@@ -83,6 +84,9 @@ def _run_with_temp_file(
83
84
  _encoding : str, optional
84
85
  Encoding to use for subprocess output. Defaults to "utf-8" to ensure
85
86
  cross-platform UTF-8 support for temp file scripts.
87
+ echo_cmd : str | None, optional
88
+ Override the command string displayed in logs and console output.
89
+ Default is None (show actual command).
86
90
 
87
91
  Notes
88
92
  -----
@@ -122,6 +126,7 @@ def _run_with_temp_file(
122
126
  cwd=cwd,
123
127
  stream=stream,
124
128
  echo=False,
129
+ echo_cmd=echo_cmd,
125
130
  env=env,
126
131
  _encoding=_encoding,
127
132
  **kwargs,
@@ -147,6 +152,7 @@ def run(
147
152
  stream: bool = True,
148
153
  shell: bool | None = None,
149
154
  echo: bool = True,
155
+ echo_cmd: str | None = None,
150
156
  dry_run: bool = False,
151
157
  keep_temp_file: bool = False,
152
158
  env: dict[str, str] | None = None,
@@ -165,6 +171,7 @@ def run(
165
171
  stream: bool = True,
166
172
  shell: bool | None = None,
167
173
  echo: bool = True,
174
+ echo_cmd: str | None = None,
168
175
  dry_run: bool = False,
169
176
  keep_temp_file: bool = False,
170
177
  env: dict[str, str] | None = None,
@@ -182,6 +189,7 @@ def run(
182
189
  stream: bool = True,
183
190
  shell: bool | None = None,
184
191
  echo: bool = True,
192
+ echo_cmd: str | None = None,
185
193
  dry_run: bool = False,
186
194
  keep_temp_file: bool = False,
187
195
  env: dict[str, str] | None = None,
@@ -215,6 +223,11 @@ def run(
215
223
  echo : bool, optional
216
224
  Display command before execution using console.cmd().
217
225
  Default is True. Set to False for silent execution.
226
+ echo_cmd : str | None, optional
227
+ Override the command string displayed in logs and console output.
228
+ The actual command is still executed, but this string is shown instead.
229
+ Useful for hiding complex binary paths or secrets in commands.
230
+ Default is None (show actual command).
218
231
  dry_run : bool, optional
219
232
  Display command without executing (dry-run mode).
220
233
  Default is False. Does NOT auto-echo; combine with echo=True
@@ -249,16 +262,18 @@ def run(
249
262
  >>> run("echo hello", echo=True, dry_run=True) # Show but don't run
250
263
  >>> run("ls *.py | wc -l") # Pipes and wildcards
251
264
  >>> run(["echo", "hello"]) # List for direct execution
265
+ >>> run("/path/to/binary arg", echo_cmd="binary arg") # Override display
252
266
  """
253
267
  _validate_params(stream=stream, capture_output=capture_output)
254
268
  shell = _detect_shell(cmd=cmd, shell=shell)
255
269
  cmd_str = _format_cmd_str(cmd=cmd)
270
+ cmd_str_for_display = echo_cmd if echo_cmd is not None else cmd_str
256
271
 
257
272
  if echo:
258
- console.cmd(cmd_str)
273
+ console.cmd(cmd_str_for_display)
259
274
 
260
275
  if dry_run:
261
- return _dry_run_result(cmd=cmd, capture_output=capture_output, cwd=cwd)
276
+ return _dry_run_result(cmd=cmd, capture_output=capture_output, cwd=cwd, echo_cmd=echo_cmd)
262
277
 
263
278
  # Handle multi-line scripts that require temp file approach:
264
279
  # - Windows: Any multi-line script with shell=True (cmd.exe limitation)
@@ -287,10 +302,12 @@ def run(
287
302
  stream=stream,
288
303
  keep_temp_file=keep_temp_file,
289
304
  env=env,
305
+ _encoding=_encoding,
306
+ echo_cmd=echo_cmd,
290
307
  **kwargs,
291
308
  )
292
309
 
293
- logger.debug(f"[run] {cmd_str}", extra={"cwd": cwd})
310
+ logger.debug(f"[run] {cmd_str_for_display}", extra={"cwd": cwd})
294
311
  start = time.perf_counter()
295
312
 
296
313
  _run = _run_with_stream if stream else _run_without_stream
@@ -305,9 +322,9 @@ def run(
305
322
  **kwargs,
306
323
  )
307
324
 
308
- _check_exit_code(returncode=result.returncode, check=check, cmd_str=cmd_str)
325
+ _check_exit_code(result=result, check=check, cmd_str_for_display=cmd_str_for_display)
309
326
 
310
- _log_completion(cmd_str=cmd_str, result=result, start=start)
327
+ _log_completion(cmd_str_for_display=cmd_str_for_display, result=result, start=start)
311
328
  return result
312
329
 
313
330
 
@@ -330,9 +347,11 @@ def _dry_run_result(
330
347
  cmd: str | list[str] | tuple[str, ...],
331
348
  capture_output: bool,
332
349
  cwd: Path | str | None,
350
+ echo_cmd: str | None = None,
333
351
  ) -> subprocess.CompletedProcess[str]:
334
352
  cmd_str = _format_cmd_str(cmd)
335
- logger.debug(f"[dry-run] {cmd_str}", extra={"cwd": cwd})
353
+ cmd_str_for_display = echo_cmd if echo_cmd is not None else cmd_str
354
+ logger.debug(f"[dry-run] {cmd_str_for_display}", extra={"cwd": cwd})
336
355
  return subprocess.CompletedProcess(
337
356
  args=cmd,
338
357
  returncode=0,
@@ -341,10 +360,21 @@ def _dry_run_result(
341
360
  )
342
361
 
343
362
 
344
- def _check_exit_code(returncode: int, check: bool, cmd_str: str) -> None:
345
- if check and returncode != 0:
346
- logger.debug(f"[error] {cmd_str}", extra={"returncode": returncode})
347
- raise typer.Exit(returncode)
363
+ def _check_exit_code(
364
+ result: subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None],
365
+ check: bool,
366
+ cmd_str_for_display: str,
367
+ ) -> None:
368
+ if check and result.returncode != 0:
369
+ logger.debug(
370
+ f"[error] {cmd_str_for_display}",
371
+ extra={
372
+ "returncode": result.returncode,
373
+ "stdout": result.stdout,
374
+ "stderr": result.stderr,
375
+ },
376
+ )
377
+ raise typer.Exit(result.returncode)
348
378
 
349
379
 
350
380
  def _process_stream_output(
@@ -533,10 +563,12 @@ def _run_without_stream(
533
563
  return result
534
564
 
535
565
 
536
- def _log_completion(cmd_str: str, result: subprocess.CompletedProcess, start: float) -> None:
566
+ def _log_completion(
567
+ cmd_str_for_display: str, result: subprocess.CompletedProcess, start: float
568
+ ) -> None:
537
569
  elapsed_seconds = time.perf_counter() - start
538
570
  logger.debug(
539
- f"[done] {cmd_str}",
571
+ f"[done] {cmd_str_for_display}",
540
572
  extra={
541
573
  "returncode": result.returncode,
542
574
  "stdout": result.stdout,
bake/ui/run/uv.py CHANGED
@@ -4,7 +4,6 @@ from typing import Literal, overload
4
4
 
5
5
  from uv import find_uv_bin
6
6
 
7
- from bake.ui import console
8
7
  from bake.ui.run.run import run
9
8
 
10
9
 
@@ -59,14 +58,6 @@ def run_uv(
59
58
  ) -> subprocess.CompletedProcess[str] | subprocess.CompletedProcess[None]:
60
59
  uv_bin = find_uv_bin()
61
60
 
62
- # Build display string: "uv" + command parts (no full binary path)
63
- display_cmd = "uv " + " ".join(cmd)
64
-
65
- # Echo command to console if requested
66
- if echo:
67
- console.cmd(display_cmd)
68
-
69
- # Call run with full uv binary path, echo=False (already displayed), pass through options
70
61
  return run(
71
62
  [uv_bin, *cmd],
72
63
  capture_output=capture_output,
@@ -74,7 +65,8 @@ def run_uv(
74
65
  cwd=cwd,
75
66
  stream=stream,
76
67
  shell=False,
77
- echo=False,
68
+ echo=echo,
69
+ echo_cmd="uv " + " ".join(cmd) if echo else None,
78
70
  dry_run=dry_run,
79
71
  keep_temp_file=keep_temp_file,
80
72
  env=env,
bake/utils/__init__.py CHANGED
@@ -1,11 +1,17 @@
1
1
  from bake.utils.constants import DEFAULT_BAKEBOOK_NAME, DEFAULT_FILE_NAME
2
- from bake.utils.env import should_use_colors
3
2
  from bake.utils.exceptions import BakebookError, BaseBakefileError
3
+ from bake.utils.settings import (
4
+ ENV__BAKE_REINVOKED,
5
+ ENV_NO_COLOR,
6
+ bake_settings,
7
+ )
4
8
 
5
9
  __all__ = [
6
10
  "DEFAULT_BAKEBOOK_NAME",
7
11
  "DEFAULT_FILE_NAME",
12
+ "ENV_NO_COLOR",
13
+ "ENV__BAKE_REINVOKED",
8
14
  "BakebookError",
9
15
  "BaseBakefileError",
10
- "should_use_colors",
16
+ "bake_settings",
11
17
  ]
bake/utils/settings.py ADDED
@@ -0,0 +1,25 @@
1
+ from pydantic import Field
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+ ENV_NO_COLOR = "NO_COLOR"
5
+ ENV__BAKE_REINVOKED = "_BAKE_REINVOKED"
6
+
7
+
8
+ class BakeSettings(BaseSettings):
9
+ model_config = SettingsConfigDict(
10
+ env_file=".env",
11
+ env_file_encoding="utf-8",
12
+ extra="ignore",
13
+ populate_by_name=True,
14
+ )
15
+
16
+ ci: bool = False
17
+ github_actions: bool = False
18
+ no_color: bool = False
19
+ bake_reinvoked: bool = Field(default=False, alias="_BAKE_REINVOKED")
20
+
21
+ def should_use_colors(self) -> bool:
22
+ return self.no_color
23
+
24
+
25
+ bake_settings = BakeSettings()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bakefile
3
- Version: 0.0.10
3
+ Version: 0.0.11
4
4
  Summary: Add your description here
5
5
  Author: Wisaroot Lertthaweedech
6
6
  Author-email: Wisaroot Lertthaweedech <l.wisaroot@gmail.com>
@@ -18,7 +18,9 @@ Requires-Dist: tomli>=2.0.0 ; python_full_version < '3.11'
18
18
  Requires-Dist: ty>=0.0.8
19
19
  Requires-Dist: typer>=0.21.0
20
20
  Requires-Dist: uv>=0.9.20
21
+ Requires-Dist: keyring>=25.7.0 ; extra == 'lib'
21
22
  Requires-Dist: pathspec>=1.0.3 ; extra == 'lib'
23
+ Requires-Dist: tenacity>=9.1.2 ; extra == 'lib'
22
24
  Requires-Python: >=3.10
23
25
  Provides-Extra: lib
24
26
  Description-Content-Type: text/markdown
@@ -6,8 +6,8 @@ bake/bakebook/get.py,sha256=fx5WV66OIBrywPN0thePXAJk2gt7wvTVAxYQqZ_PMao,5807
6
6
  bake/cli/__init__.py,sha256=da1PTClDMl-IBkrSvq6JC1lnS-K_BASzCvxVhNxN5Ls,13
7
7
  bake/cli/bake/__init__.py,sha256=CJokSP1t1KXaIqjkjFQ8_gbaSZ9RRB4YdemIoSTqRcI,56
8
8
  bake/cli/bake/__main__.py,sha256=5Ui5_OD1-oG1ou6boak63EPQDmRsW9sGOVVDgXJaNec,133
9
- bake/cli/bake/main.py,sha256=Lr6eyfaf4-zDRrF0GK9bjjFWop66ydewV5tNUluDd9M,2262
10
- bake/cli/bake/reinvocation.py,sha256=Ifqc8ZAM7NMyrKU2jkmw1agCEI-Sx7yvLLBiFRi23_0,1985
9
+ bake/cli/bake/main.py,sha256=286zCeRpxq2sGm7_vbX55YVm13dHrrCBtVNEXeZcNzg,3630
10
+ bake/cli/bake/reinvocation.py,sha256=Df_rRylarejI0nj2dV7s9YAhgN0B5iJnclzU0EcfN5Q,2100
11
11
  bake/cli/bakefile/__init__.py,sha256=_zD3rXQHLr6EWHADdPLAmnc2A5C3dhmBuvP5uJ-_A58,60
12
12
  bake/cli/bakefile/__main__.py,sha256=FVntzkZdzdygSWjMzyneXCXsM-MDTPmC3GUk4JZiYFU,137
13
13
  bake/cli/bakefile/add_inline.py,sha256=V98T50SLMPqnWVtyEO_6hL17r4n3ZtkSC8NSEqdyHzc,919
@@ -15,54 +15,58 @@ bake/cli/bakefile/export.py,sha256=m9X0u6FgbjUzneQuh39H1CaFUT444jOPTFBNjnjs_Dg,6
15
15
  bake/cli/bakefile/find_python.py,sha256=J2HDs_nfNODqCHBZCNM64ESB4kVZK-C04i-KNmVUoSs,539
16
16
  bake/cli/bakefile/init.py,sha256=0QuvADFOZZUBN2BUJfK90aEY1oUzoSNVRiljlUSjLu0,1825
17
17
  bake/cli/bakefile/lint.py,sha256=DJkIJNBOef6JvgwQ3iL9jTrLqgUyn66Mhv6cuAgqXk0,2509
18
- bake/cli/bakefile/main.py,sha256=jbpzNQa55thbzhpcmEtys1M1CvNUJBvi5UmgVzSbOM8,1414
18
+ bake/cli/bakefile/main.py,sha256=pxDIefHx-_PrbGbE133eODHwC7UQC7NkjMUm_7EGFKg,2435
19
19
  bake/cli/bakefile/uv.py,sha256=PMFG3BdofzGWkor4fMEi3GE4G7hGtclCgPm2xlaPDso,4013
20
20
  bake/cli/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- bake/cli/common/app.py,sha256=_XzBOy0OK2GB14E-cyfLV6CBCn70G62QqjUscj_DHfA,1443
22
- bake/cli/common/callback.py,sha256=NmrZUl5eRr95nluomTwcKjTU7dSKjWcQVli5VEdZk-4,439
23
- bake/cli/common/context.py,sha256=RtFHUDCZLcD88Ys17u_zXoHUq-12jkoXc9f_D4jh_7M,3871
21
+ bake/cli/common/app.py,sha256=6tWCyMKHwSynYI3qzc1AfSfLS4p2P25AilOsThqLN7k,1296
22
+ bake/cli/common/context.py,sha256=miyA87yaZLeuTsBnjIQdo4f5QKsI3CtOByTrzAWtweQ,4013
24
23
  bake/cli/common/exception_handler.py,sha256=2vLbqMeZlLxKqNWUkTs3cA-8l6IjK0dU3SyZlRb96YI,1759
25
- bake/cli/common/obj.py,sha256=ShDsQtHCxex17IrKb9kSdC1t459qBsam53SFCUB_DSA,7094
26
- bake/cli/common/params.py,sha256=rhLa34SY92nXfUaKo0SQMKK__xRnrmHejHa25tRyKdg,2002
24
+ bake/cli/common/obj.py,sha256=IcURtAHXRrwFkXZsVTTruEMeLddwowtBvpa5lBknpEk,7181
25
+ bake/cli/common/params.py,sha256=229B4PPTv84InlDSA0sjlU3ToCjyzvhjNpRIp2iFano,2278
27
26
  bake/cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
27
  bake/cli/utils/version.py,sha256=aiweLD0vDezBlJAcCC99oMms71WGD9CWSJuZ4i3VLHA,390
29
28
  bake/manage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
29
  bake/manage/add_inline.py,sha256=yefHmF33ghCB8NZ-v61ybeVsaeE8iDFvfRGeTAKg4I8,2245
31
30
  bake/manage/find_python.py,sha256=67PAFPDA3fdSsDGjYfPcXOVUcgC63wg5mipjVIt1VUQ,7730
32
- bake/manage/lint.py,sha256=OqwYFF8GGvzHGVPuJcWMRAv5esXEIX4nQXdGcChnkqA,2394
31
+ bake/manage/lint.py,sha256=4opV1EanzPUz5tmdzvNclM-qK4TmL85IixbzP5z-zRY,2472
33
32
  bake/manage/run_uv.py,sha256=QzlKeVpr20dXNDcwUgyJqnXT4MofRqK-6XkWpzBbUhE,3234
34
33
  bake/manage/write_bakefile.py,sha256=efGViLk7sh-QX9Mox7yQw_A1Tp7EOuc_vmSTbFmXUm0,736
35
34
  bake/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
35
  bake/samples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
36
  bake/samples/simple.py,sha256=hP2TW-D7BQBGJseqRPpilxkoQ8ScTuZZePICupyvFKA,155
38
37
  bake/ui/__init__.py,sha256=6OhZVKjfC9aumbhraxkGtx7KLpV1ouepeHRA2dUVoSo,209
39
- bake/ui/console.py,sha256=C5wrbsOc-wwcx0hGmCozHvGCNTWgGhsh-5vxl880xS4,1689
38
+ bake/ui/console.py,sha256=y5pZ5i_Iqh4bzaHGJQFDMnqRILsIClr8004EkLydLr4,3289
40
39
  bake/ui/logger/__init__.py,sha256=bup2cssTHhergh47s6uYbGtY2dJNxlKKH6otBc4ECFM,728
41
40
  bake/ui/logger/capsys.py,sha256=KZL6k7Werp_8styfJKfIvQyv0-gJq54vY3hSJFIacEM,5267
42
41
  bake/ui/logger/setup.py,sha256=OrX9UiY0iBGfWWfhMJCdfqCRJsL5yC3rIdIEOn7rveo,1377
43
42
  bake/ui/logger/utils.py,sha256=dcppxoS_pX92AFcHIerJGI2_JBHBNghRQmQqlZmmj2Q,7218
44
43
  bake/ui/params.py,sha256=yNDChJQkbeZSxQzXTSBrAPCbwsJ5zOK4s4sFHQPSnHs,140
45
44
  bake/ui/run/__init__.py,sha256=A671l5YVTRAtS47ewvaMCNwPRim_Wkof1am0WibxA2I,205
46
- bake/ui/run/run.py,sha256=qfDgy-YqcexJyHhSjnQ5IXipBDoK-umwKq-wAn8ZITU,17504
45
+ bake/ui/run/run.py,sha256=t06v1E2TD8PGymnUsIHfZxwiBGeqFZ7Mt9W3IwowsMc,18842
47
46
  bake/ui/run/script.py,sha256=fk7KiDklYDYpFGkH3wu-hZGI4OnvgcB8z5jtNt41Hg0,2263
48
47
  bake/ui/run/splitter.py,sha256=L6uCU3bzpoMgj891Q1BZnOtiWF07QFDcCOx9RyUqHKk,9198
49
- bake/ui/run/uv.py,sha256=3NpnjgAwQNijJiUT_H6U-3mTHQgBZPlJbNWEeYCZY1g,2077
48
+ bake/ui/run/uv.py,sha256=LC1Eo3rbmRug8_MsmXxZYVGWcyr79WGzFfXQrEwtWH0,1810
50
49
  bake/ui/style.py,sha256=v9dferzV317Acb0GHpVK_niCj_s2HtL-yiToBZtXky4,70
51
- bake/utils/__init__.py,sha256=GUu_xlJy3RAHo6UcZXu2x4khxGqLHMA9Zos4hDiQIY8,326
50
+ bake/utils/__init__.py,sha256=1XKY-YbhTIwkWM-goTwek82mcQhwKqMW_bNTirUSrfI,422
52
51
  bake/utils/constants.py,sha256=mRq5IpgOTdlHOTWPq5dx0A-LwhiFkWgYHfr8cLWG7rY,471
53
- bake/utils/env.py,sha256=bzNdH_2bTJebQaw7D0uVJv-vzZ-uYl0pCAS8oQONVsA,190
54
52
  bake/utils/exceptions.py,sha256=pwsQnKH5ljMNxmqEREutXa7TohiBHATHg_D5kQUPT30,519
55
- bakelib/__init__.py,sha256=7lWfLpL82AVkot3BXodJpyuEzEAiythFevMtzxzd_i4,455
53
+ bake/utils/settings.py,sha256=SRFU9WVHRMB1cE0TjReKEgL1nbIFM9NBLJWcGiODdOk,612
54
+ bakelib/__init__.py,sha256=yj72sp5MbSebIxIGHh183gbNE5oIdv-OG-8IYnoJ6AE,529
56
55
  bakelib/environ/__init__.py,sha256=XIFVtu8SQySjPetu9WR_Q8HgqxUzenMNm1K24pYSfNo,356
57
56
  bakelib/environ/bakebook.py,sha256=gnOvi3t5Ww0_6N5wozXLVvLe8KAK-u-6_v0UYCl_iKY,749
58
57
  bakelib/environ/base.py,sha256=azPUdc9C5zVU8iyXJrEm3uDfe39nG48DRAZiF9rTdHI,3753
59
58
  bakelib/environ/get_bakebook.py,sha256=LihxB3VDcVq81KJy1HT-N-beyt2C7ReWm2kMPrd7VlA,1563
60
59
  bakelib/environ/presets.py,sha256=IwHGeDeQe4e57k9_u_vBQ61bjMS5Z2Jj9zkXw6YJHAE,1943
60
+ bakelib/refreshable_cache/__init__.py,sha256=hNylzA9X9GpAiuG3WYBvgGRY-z8Gcu_BxsyHOIwtTOQ,347
61
+ bakelib/refreshable_cache/cache.py,sha256=P8gX63FP1vgdwNOwKLf-U0ly1P1LvNsB7KqDS9XdG5Q,7629
62
+ bakelib/refreshable_cache/exceptions.py,sha256=d15dd0RbapXneWZpDwfvDt7k49cB6jZBMxULi5DLU9Q,97
61
63
  bakelib/space/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
- bakelib/space/base.py,sha256=4afkB2Cmq3kP-xBeL45lvmmOhARsA-m5F1-rEPLAgEc,5871
63
- bakelib/space/python.py,sha256=0qxjIQ1SYIvDkaeQjkykE2t8doBUTl1MONK8cuG5juo,2793
64
- bakelib/space/utils.py,sha256=xx4X_txhDH_p97CKJ-KuvFpgfNBC0y_din1IBlUVusU,2983
65
- bakefile-0.0.10.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
66
- bakefile-0.0.10.dist-info/entry_points.txt,sha256=Ecvvh7BYHCPJ0UdntrDc3Od6AZdRPXN5Z7o_7ok_0Qw,107
67
- bakefile-0.0.10.dist-info/METADATA,sha256=sb-rcoJxanUjkn2dA-9ZfOg0aaXdlVF4ys-ATVVjuuk,2368
68
- bakefile-0.0.10.dist-info/RECORD,,
64
+ bakelib/space/base.py,sha256=7vwVw19hkPimvamvTVySkc7Ndk8san3EF_5pg-8jf7w,6260
65
+ bakelib/space/lib.py,sha256=MZnS_vP9zxalRE_eFb-fkW5NU5-6l8w8Rsz63PMCVS8,5617
66
+ bakelib/space/python.py,sha256=o7t46DnBOcHf_bcJ8HOhiisjMf-UQsWCcykgLYxffu4,3400
67
+ bakelib/space/python_lib.py,sha256=kkfdoMOWMX_qP3uGafQbLXy5NFbvv4ZKwmOXrgzH6Cs,2729
68
+ bakelib/space/utils.py,sha256=Mp82CgpNMeG76slXaDs9GXa1r_ugoiJICvpsOaH_2tg,3206
69
+ bakefile-0.0.11.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
70
+ bakefile-0.0.11.dist-info/entry_points.txt,sha256=Ecvvh7BYHCPJ0UdntrDc3Od6AZdRPXN5Z7o_7ok_0Qw,107
71
+ bakefile-0.0.11.dist-info/METADATA,sha256=sDQXFSpH61wMHVp8mBsWgNzXLJWMR1upvBP3wsEMu9E,2464
72
+ bakefile-0.0.11.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.28
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
bakelib/__init__.py CHANGED
@@ -9,6 +9,7 @@ from bakelib.environ import (
9
9
  )
10
10
  from bakelib.space.base import BaseSpace
11
11
  from bakelib.space.python import PythonSpace
12
+ from bakelib.space.python_lib import PythonLibSpace
12
13
 
13
14
  __all__ = [
14
15
  "BaseEnv",
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "EnvBakebook",
18
19
  "GcpLandingZoneEnv",
19
20
  "ProdEnvBakebook",
21
+ "PythonLibSpace",
20
22
  "PythonSpace",
21
23
  "StagingEnvBakebook",
22
24
  "get_bakebook",
@@ -0,0 +1,17 @@
1
+ from bakelib.refreshable_cache.cache import (
2
+ ChainedCache,
3
+ KeyringCache,
4
+ MemoryCache,
5
+ NullCache,
6
+ RefreshableCache,
7
+ )
8
+ from bakelib.refreshable_cache.exceptions import RefreshNeededError
9
+
10
+ __all__ = [
11
+ "ChainedCache",
12
+ "KeyringCache",
13
+ "MemoryCache",
14
+ "NullCache",
15
+ "RefreshNeededError",
16
+ "RefreshableCache",
17
+ ]
@@ -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
@@ -0,0 +1,2 @@
1
+ class RefreshNeededError(Exception):
2
+ """Raised when a cached value needs to be refreshed."""