calkit-python 0.47.4__py3-none-any.whl → 0.47.6__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.
- calkit/agent_skills/build-paper-pipeline/SKILL.md +5 -2
- calkit/agent_skills/check-questions/SKILL.md +44 -4
- calkit/cli/list.py +57 -15
- calkit/cli/main/core.py +2 -1
- calkit/cli/new.py +20 -11
- calkit/config.py +50 -10
- calkit/core.py +15 -1
- calkit/environments.py +3 -1
- calkit/install.py +267 -29
- calkit/models/core.py +9 -1
- calkit/models/pipeline.py +8 -1
- calkit/questions.py +230 -18
- calkit/resources/devcontainer/base.json +1 -1
- calkit/resources/devcontainer/devcontainer.json +1 -1
- calkit/templates/core.py +129 -12
- calkit/tests/cli/test_check.py +34 -17
- calkit/tests/cli/test_list.py +28 -2
- calkit/tests/cli/test_new.py +31 -0
- calkit/tests/test_config.py +28 -0
- calkit/tests/test_core.py +25 -0
- calkit/tests/test_install.py +124 -0
- calkit/tests/test_questions.py +155 -1
- calkit/tests/test_templates.py +50 -0
- {calkit_python-0.47.4.dist-info → calkit_python-0.47.6.dist-info}/METADATA +132 -31
- {calkit_python-0.47.4.dist-info → calkit_python-0.47.6.dist-info}/RECORD +41 -41
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.47.4.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.47.4.dist-info → calkit_python-0.47.6.dist-info}/WHEEL +0 -0
- {calkit_python-0.47.4.dist-info → calkit_python-0.47.6.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.47.4.dist-info → calkit_python-0.47.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -84,8 +84,11 @@ building a pipeline from scratch.
|
|
|
84
84
|
`calkit.yaml`: the `question` as the paper poses it, the `answer` as the
|
|
85
85
|
paper states it with `{name}` placeholders where numbers go, and
|
|
86
86
|
`evidence` naming the results file and key, the figure, and the
|
|
87
|
-
publication section that carries the argument.
|
|
88
|
-
|
|
87
|
+
publication section that carries the argument. The results file must be
|
|
88
|
+
one a stage from step 3 writes, never one written by hand. Where the
|
|
89
|
+
claim hinges on a threshold, write a conditional answer; the
|
|
90
|
+
`check-questions` skill covers both. Run `calkit check questions` and
|
|
91
|
+
fix what it reports.
|
|
89
92
|
|
|
90
93
|
6. **Run the pipeline, then check your own work.** Run `calkit run`, then
|
|
91
94
|
`calkit check repro`, which reads the manuscript back and reports any
|
|
@@ -21,12 +21,17 @@ Deterministic, done by `calkit check questions` — never re-derive by hand:
|
|
|
21
21
|
- every evidence path exists;
|
|
22
22
|
- every `value` key resolves in its file, and every `{name}` placeholder in
|
|
23
23
|
the prose resolves and formats;
|
|
24
|
+
- every clause of a conditional answer parses, names only evidence, and
|
|
25
|
+
renders, including the clauses the current values don't select;
|
|
24
26
|
- every publication `label` still exists in the LaTeX source;
|
|
25
27
|
- no evidence has changed (Git history for Git-tracked outputs, `dvc.lock`
|
|
26
28
|
for DVC-tracked ones) since the commit that last edited the question;
|
|
27
|
-
- each
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
- each `value` entry reads a file a pipeline stage produces, or one
|
|
30
|
+
declared with `imported_from`; anything else is an error, since a number
|
|
31
|
+
nothing computes is a magic number;
|
|
32
|
+
- each other evidence path is produced by a pipeline stage, or declared
|
|
33
|
+
with `imported_from` or `created_by`. This one is advisory: it is
|
|
34
|
+
reported as `unattributed` and does not fail the check.
|
|
30
35
|
|
|
31
36
|
Judgment, done here — the check reads paths and hashes, and cannot read a
|
|
32
37
|
sentence:
|
|
@@ -35,6 +40,8 @@ sentence:
|
|
|
35
40
|
this skill exists;
|
|
36
41
|
- does the answer still follow from the evidence, given what changed;
|
|
37
42
|
- are numbers retyped into the prose that should be `{name}` placeholders;
|
|
43
|
+
- does a claim resting on a threshold use a conditional answer, with a
|
|
44
|
+
threshold chosen before the value was known;
|
|
38
45
|
- is the answer concise, and does it point at the publication section that
|
|
39
46
|
carries the argument rather than repeating it.
|
|
40
47
|
|
|
@@ -104,7 +111,8 @@ evidence without showing them both.
|
|
|
104
111
|
from. If a stage should produce it, that is a pipeline gap worth
|
|
105
112
|
reporting. If it was imported or made by hand, declare it under
|
|
106
113
|
`figures`, `datasets`, or `publications` with `imported_from` or
|
|
107
|
-
`created_by` so the project says so.
|
|
114
|
+
`created_by` so the project says so. A `value` entry with no stage is
|
|
115
|
+
reported as an error rather than `unattributed`: give it a stage.
|
|
108
116
|
4. For each **error**, fix the reference: a missing path means the pipeline
|
|
109
117
|
has not been run or pulled; a bad key or placeholder means a results
|
|
110
118
|
file was restructured; a missing label means the publication was
|
|
@@ -128,6 +136,38 @@ project, not a tidy-up.
|
|
|
128
136
|
what it means; leave the reasoning to the publication.
|
|
129
137
|
- Numbers come from `value` evidence via placeholders, formatted to the
|
|
130
138
|
precision the claim needs: `{ratio:.1f}x`, `{error:.0%}`.
|
|
139
|
+
- A `value` entry must read a file a pipeline stage writes. A placeholder
|
|
140
|
+
over a results file written by hand, including one you write, is a
|
|
141
|
+
retyped number with extra steps, and the check fails it. If no stage
|
|
142
|
+
produces the number, add one (`/calkit:add-pipeline-stage`) instead of
|
|
143
|
+
writing the file. Never declare a file `imported_from` or `created_by`
|
|
144
|
+
to clear that error unless it really came from there, and never edit a
|
|
145
|
+
results file to change what an answer says.
|
|
146
|
+
- When the claim itself depends on a value, not just the number in it,
|
|
147
|
+
e.g., significant or not, which method wins, write a conditional answer
|
|
148
|
+
so the wording follows the evidence on a rerun:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
answer:
|
|
152
|
+
if p < 0.05: "The closure cuts error by {improvement:.1f}x."
|
|
153
|
+
else: "The closure does not measurably reduce error."
|
|
154
|
+
evidence:
|
|
155
|
+
- kind: value
|
|
156
|
+
path: results/closure.json
|
|
157
|
+
key: p-value
|
|
158
|
+
name: p
|
|
159
|
+
- kind: value
|
|
160
|
+
path: results/closure.json
|
|
161
|
+
key: improvement
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Conditions name `value` evidence, so those names must be valid Python
|
|
165
|
+
identifiers; give the entry a `name` otherwise. Every branch must be a
|
|
166
|
+
claim the evidence would support if it held. The threshold is part of
|
|
167
|
+
the claim: take it from the field's convention or the user, never pick
|
|
168
|
+
it to make the current branch hold, and don't use branches to hedge a
|
|
169
|
+
claim that should simply be weakened.
|
|
170
|
+
|
|
131
171
|
- Point at the publication with a `publication` evidence entry carrying
|
|
132
172
|
`section` (for the reader) and `label` (for the check), instead of an
|
|
133
173
|
`explanation` that restates the argument.
|
calkit/cli/list.py
CHANGED
|
@@ -243,7 +243,7 @@ def list_questions(
|
|
|
243
243
|
render_question,
|
|
244
244
|
)
|
|
245
245
|
|
|
246
|
-
def _texts(question: dict) -> list[str]:
|
|
246
|
+
def _texts(question: dict) -> list[str | dict]:
|
|
247
247
|
evidence = question.get("evidence") or []
|
|
248
248
|
return [question.get(f) or "" for f in TEMPLATED_FIELDS] + [
|
|
249
249
|
ev.get("explanation") or ""
|
|
@@ -260,16 +260,18 @@ def list_questions(
|
|
|
260
260
|
# let a fresh clone read as a project that types its braces. Any
|
|
261
261
|
# placeholder left standing counts, whether it names evidence that
|
|
262
262
|
# could not be read or names nothing at all: a brace meant to stay
|
|
263
|
-
# in the text is written '{{' and never reaches here.
|
|
263
|
+
# in the text is written '{{' and never reaches here. A conditional
|
|
264
|
+
# answer still in clauses is one whose conditions could not be read.
|
|
264
265
|
unfilled = any(
|
|
265
|
-
placeholders(text)
|
|
266
|
+
isinstance(text, dict) or placeholders(text)
|
|
266
267
|
for q in rendered
|
|
267
268
|
if isinstance(q, dict)
|
|
268
269
|
for text in _texts(q)
|
|
269
270
|
)
|
|
270
271
|
if unfilled:
|
|
271
272
|
warn(
|
|
272
|
-
"Some placeholders could not be filled from
|
|
273
|
+
"Some placeholders or conditions could not be filled from "
|
|
274
|
+
"the evidence. "
|
|
273
275
|
"Run 'calkit check questions' to see why; 'calkit pull' if "
|
|
274
276
|
"the results files are not here yet.",
|
|
275
277
|
err=json_output,
|
|
@@ -333,21 +335,44 @@ def list_environments(
|
|
|
333
335
|
|
|
334
336
|
@list_app.command(name="templates")
|
|
335
337
|
def list_templates(
|
|
338
|
+
kind: Annotated[
|
|
339
|
+
str | None,
|
|
340
|
+
typer.Option("--kind", "-k", help="Only show templates of one kind."),
|
|
341
|
+
] = None,
|
|
336
342
|
json_output: Annotated[
|
|
337
343
|
bool, typer.Option("--json", help="Output result as JSON.")
|
|
338
344
|
] = False,
|
|
339
345
|
):
|
|
340
|
-
"""List all available Calkit templates.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
+
"""List all available Calkit templates, grouped by kind.
|
|
347
|
+
|
|
348
|
+
A template is named by its kind and name, except a project template,
|
|
349
|
+
which names a project on a hub and so is ``owner/project``.
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
templates = calkit.templates.get_templates(kind=kind)
|
|
353
|
+
except ValueError as e:
|
|
354
|
+
raise_error(str(e))
|
|
355
|
+
groups: dict[str, list[dict]] = {}
|
|
356
|
+
for template in templates:
|
|
357
|
+
groups.setdefault(template.kind, []).append(
|
|
358
|
+
{
|
|
359
|
+
"name": template.ref,
|
|
360
|
+
"title": template.title,
|
|
361
|
+
"description": template.description,
|
|
362
|
+
}
|
|
363
|
+
)
|
|
346
364
|
if json_output:
|
|
347
|
-
echo_json(
|
|
365
|
+
echo_json(groups)
|
|
348
366
|
return
|
|
349
|
-
for
|
|
350
|
-
|
|
367
|
+
for i, (group, entries) in enumerate(groups.items()):
|
|
368
|
+
if i:
|
|
369
|
+
typer.echo()
|
|
370
|
+
typer.echo(f"{group}:")
|
|
371
|
+
for entry in entries:
|
|
372
|
+
typer.echo(f" {entry['name']}")
|
|
373
|
+
for key in ("title", "description"):
|
|
374
|
+
if entry[key]:
|
|
375
|
+
typer.echo(f" {entry[key]}")
|
|
351
376
|
|
|
352
377
|
|
|
353
378
|
@list_app.command(name="installers")
|
|
@@ -368,17 +393,30 @@ def list_installers(
|
|
|
368
393
|
groups: dict[int, list[str]] = {}
|
|
369
394
|
for name, entry in calkit.install.INSTALLERS.items():
|
|
370
395
|
groups.setdefault(id(entry), []).append(name)
|
|
396
|
+
# What Calkit has already installed here, so the listing doubles as a
|
|
397
|
+
# record of changes it made to this machine
|
|
398
|
+
installed = {
|
|
399
|
+
rec["app"]: rec["installed_at"]
|
|
400
|
+
for rec in calkit.install.read_install_log()
|
|
401
|
+
}
|
|
371
402
|
result: list[dict] = []
|
|
372
403
|
for names in groups.values():
|
|
373
404
|
names.sort()
|
|
374
405
|
entry = calkit.install.INSTALLERS[names[0]]
|
|
375
406
|
scripts = {}
|
|
376
|
-
for platform in ("unix", "windows"):
|
|
407
|
+
for platform in ("unix", "mac", "linux", "windows"):
|
|
377
408
|
ins = entry.get(platform) # type: ignore[call-overload]
|
|
378
409
|
if ins is not None:
|
|
379
410
|
scripts[platform] = ins["script"]
|
|
380
411
|
result.append(
|
|
381
|
-
{
|
|
412
|
+
{
|
|
413
|
+
"name": names[0],
|
|
414
|
+
"aliases": names[1:],
|
|
415
|
+
"scripts": scripts,
|
|
416
|
+
"installed_by_calkit": next(
|
|
417
|
+
(installed[n] for n in names if n in installed), None
|
|
418
|
+
),
|
|
419
|
+
}
|
|
382
420
|
)
|
|
383
421
|
if json_output:
|
|
384
422
|
echo_json(result)
|
|
@@ -388,6 +426,10 @@ def list_installers(
|
|
|
388
426
|
header = installer["name"] + (
|
|
389
427
|
f" (aliases: {aliases})" if aliases else ""
|
|
390
428
|
)
|
|
429
|
+
if installer["installed_by_calkit"]:
|
|
430
|
+
header += (
|
|
431
|
+
f" [installed by Calkit {installer['installed_by_calkit']}]"
|
|
432
|
+
)
|
|
391
433
|
typer.echo(header)
|
|
392
434
|
for platform, script in installer["scripts"].items():
|
|
393
435
|
typer.echo(f" {platform}: {script}")
|
calkit/cli/main/core.py
CHANGED
|
@@ -2936,10 +2936,11 @@ def run(
|
|
|
2936
2936
|
in_main_thread = threading.current_thread() is threading.main_thread()
|
|
2937
2937
|
old_handler = None
|
|
2938
2938
|
handler_set = False
|
|
2939
|
-
with open(log_fpath, "a", encoding="utf-8") as log_f:
|
|
2939
|
+
with open(log_fpath, "a", encoding="utf-8", errors="replace") as log_f:
|
|
2940
2940
|
log_f.write(STAGE_OUTPUT_START + "\n")
|
|
2941
2941
|
log_f.flush()
|
|
2942
2942
|
try:
|
|
2943
|
+
kwargs.setdefault("errors", "replace")
|
|
2943
2944
|
p = subprocess.Popen(exec_cmd, **kwargs)
|
|
2944
2945
|
if in_main_thread:
|
|
2945
2946
|
old_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
|
calkit/cli/new.py
CHANGED
|
@@ -434,17 +434,26 @@ def new_project(
|
|
|
434
434
|
)
|
|
435
435
|
if template_git_url is None:
|
|
436
436
|
project = "/".join(template_name.split("/")[:2])
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
437
|
+
# A template this package knows about carries its own repo URL,
|
|
438
|
+
# so the common case needs no hub: no request, no login, and it
|
|
439
|
+
# still works offline once the repo is reachable.
|
|
440
|
+
known = calkit.templates.find_project_template(project)
|
|
441
|
+
if known is not None:
|
|
442
|
+
if verbose:
|
|
443
|
+
typer.echo(f"Using known template {project}")
|
|
444
|
+
template_git_url = known.git_repo_url
|
|
445
|
+
else:
|
|
446
|
+
typer.echo(f"Fetching Git repo URL for {project} from the hub")
|
|
447
|
+
try:
|
|
448
|
+
template_git_url = calkit.hub.get(f"/projects/{project}")[
|
|
449
|
+
"git_repo_url"
|
|
450
|
+
]
|
|
451
|
+
except Exception as e:
|
|
452
|
+
raise_error(
|
|
453
|
+
f"Could not fetch project {project} from the hub "
|
|
454
|
+
f"({e}); for a repo not on the hub, pass its URL, "
|
|
455
|
+
f"e.g., https://github.com/{template_name}"
|
|
456
|
+
)
|
|
448
457
|
if template_subdir is None:
|
|
449
458
|
# Now clone it
|
|
450
459
|
subprocess.run(["git", "clone", template_git_url, abs_path])
|
calkit/config.py
CHANGED
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import platform
|
|
7
7
|
import warnings
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from contextlib import contextmanager
|
|
8
10
|
from typing import Any, Literal
|
|
9
11
|
from typing import get_args as get_type_args
|
|
10
12
|
|
|
@@ -26,7 +28,8 @@ def _probe_keyring() -> bool:
|
|
|
26
28
|
"""
|
|
27
29
|
try:
|
|
28
30
|
# Attempt to get a password (this will trigger backend initialization)
|
|
29
|
-
|
|
31
|
+
with _keyring_home():
|
|
32
|
+
keyring.get_password("test_service", "test_user")
|
|
30
33
|
return True
|
|
31
34
|
except keyring.errors.NoKeyringError:
|
|
32
35
|
return False
|
|
@@ -142,6 +145,40 @@ def _get_project_hub() -> str | None:
|
|
|
142
145
|
return hub
|
|
143
146
|
|
|
144
147
|
|
|
148
|
+
# Where set_env_vars keeps the user's own home directory when a project's
|
|
149
|
+
# env_vars change it, e.g., to /tmp for a tool a stage runs, so Calkit still
|
|
150
|
+
# finds its config, credentials, and caches
|
|
151
|
+
USER_HOME_ENV_VAR = "CALKIT_USER_HOME"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_user_home() -> str:
|
|
155
|
+
"""The home directory Calkit keeps its own files in."""
|
|
156
|
+
return os.environ.get(USER_HOME_ENV_VAR) or os.path.expanduser("~")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@contextmanager
|
|
160
|
+
def _keyring_home() -> Iterator[None]:
|
|
161
|
+
"""Point HOME at the user's own home for a keyring call.
|
|
162
|
+
|
|
163
|
+
The macOS keychain finds the login keychain through HOME when it's
|
|
164
|
+
called, so a project that points HOME elsewhere would otherwise hide
|
|
165
|
+
every stored secret, including hub credentials.
|
|
166
|
+
"""
|
|
167
|
+
user_home = os.environ.get(USER_HOME_ENV_VAR)
|
|
168
|
+
old_home = os.environ.get("HOME")
|
|
169
|
+
if user_home is None or user_home == old_home:
|
|
170
|
+
yield
|
|
171
|
+
return
|
|
172
|
+
os.environ["HOME"] = user_home
|
|
173
|
+
try:
|
|
174
|
+
yield
|
|
175
|
+
finally:
|
|
176
|
+
if old_home is None:
|
|
177
|
+
os.environ.pop("HOME", None)
|
|
178
|
+
else:
|
|
179
|
+
os.environ["HOME"] = old_home
|
|
180
|
+
|
|
181
|
+
|
|
145
182
|
def _get_default_hub() -> str | None:
|
|
146
183
|
"""Read ``default_hub`` from the base (unsuffixed) config file.
|
|
147
184
|
|
|
@@ -150,7 +187,7 @@ def _get_default_hub() -> str | None:
|
|
|
150
187
|
"""
|
|
151
188
|
import yaml
|
|
152
189
|
|
|
153
|
-
fpath = os.path.join(
|
|
190
|
+
fpath = os.path.join(get_user_home(), ".calkit", "config.yaml")
|
|
154
191
|
try:
|
|
155
192
|
with open(fpath) as f:
|
|
156
193
|
data = yaml.safe_load(f) or {}
|
|
@@ -275,7 +312,7 @@ def get_local_config_path() -> str:
|
|
|
275
312
|
|
|
276
313
|
def get_config_yaml_fpath() -> str:
|
|
277
314
|
return os.path.join(
|
|
278
|
-
|
|
315
|
+
get_user_home(),
|
|
279
316
|
".calkit",
|
|
280
317
|
f"config{get_env_suffix()}.yaml",
|
|
281
318
|
)
|
|
@@ -291,11 +328,12 @@ def set_secret(key: str, value: str) -> None:
|
|
|
291
328
|
"""Sets a secret using keyring, handling byte conversion for Linux."""
|
|
292
329
|
service_name = get_app_name()
|
|
293
330
|
username = _keyring_username(key)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
331
|
+
with _keyring_home():
|
|
332
|
+
if platform.system() == "Linux":
|
|
333
|
+
value_bytes = value.encode("utf-8")
|
|
334
|
+
keyring.set_password(service_name, username, value_bytes) # type: ignore
|
|
335
|
+
else:
|
|
336
|
+
keyring.set_password(service_name, username, value)
|
|
299
337
|
_secret_cache[(service_name, username)] = value
|
|
300
338
|
|
|
301
339
|
|
|
@@ -306,7 +344,8 @@ def get_secret(key: str) -> str | None:
|
|
|
306
344
|
cache_key = (service_name, username)
|
|
307
345
|
if cache_key in _secret_cache:
|
|
308
346
|
return _secret_cache[cache_key]
|
|
309
|
-
|
|
347
|
+
with _keyring_home():
|
|
348
|
+
password = keyring.get_password(service_name, username)
|
|
310
349
|
if platform.system() == "Linux" and isinstance(password, bytes):
|
|
311
350
|
password = password.decode("utf-8")
|
|
312
351
|
_secret_cache[cache_key] = password
|
|
@@ -318,7 +357,8 @@ def delete_secret(key: str) -> None:
|
|
|
318
357
|
service_name = get_app_name()
|
|
319
358
|
username = _keyring_username(key)
|
|
320
359
|
_secret_cache.pop((service_name, username), None)
|
|
321
|
-
|
|
360
|
+
with _keyring_home():
|
|
361
|
+
keyring.delete_password(service_name, username)
|
|
322
362
|
|
|
323
363
|
|
|
324
364
|
class KeyringOptionalSecret(str):
|
calkit/core.py
CHANGED
|
@@ -945,7 +945,13 @@ def check_requirements(
|
|
|
945
945
|
described_as=described_as,
|
|
946
946
|
)
|
|
947
947
|
continue
|
|
948
|
-
|
|
948
|
+
# Some apps have no installer here but a known way in, e.g., a
|
|
949
|
+
# system package manager on Linux
|
|
950
|
+
hint = _install.get_unsupported_message(dep_name)
|
|
951
|
+
raise ValueError(
|
|
952
|
+
f"app '{dep_name}' not found on {described_as}"
|
|
953
|
+
+ (f"; {hint}" if hint else "")
|
|
954
|
+
)
|
|
949
955
|
for dep in buckets.get("_other", []):
|
|
950
956
|
dep_name = dep["name"]
|
|
951
957
|
dep_kind = dep["kind"]
|
|
@@ -1449,5 +1455,13 @@ def set_env_vars(ck_info: dict, cli: bool = True) -> None:
|
|
|
1449
1455
|
raise_error(msg)
|
|
1450
1456
|
else:
|
|
1451
1457
|
raise ValueError(msg)
|
|
1458
|
+
# A project that points the home directory elsewhere, e.g., at /tmp for
|
|
1459
|
+
# a tool a stage runs, would otherwise hide the user's Calkit config,
|
|
1460
|
+
# and with it their credentials, from every Calkit command it runs
|
|
1461
|
+
import calkit.config
|
|
1462
|
+
|
|
1463
|
+
home = os.path.expanduser("~")
|
|
1452
1464
|
for k, v in env_vars.items():
|
|
1453
1465
|
os.environ[str(k)] = str(v)
|
|
1466
|
+
if os.path.expanduser("~") != home:
|
|
1467
|
+
os.environ.setdefault(calkit.config.USER_HOME_ENV_VAR, home)
|
calkit/environments.py
CHANGED
|
@@ -930,8 +930,10 @@ def write_system_env_lock(
|
|
|
930
930
|
|
|
931
931
|
|
|
932
932
|
def get_cache_db(name="cache") -> SqliteDict:
|
|
933
|
+
from calkit.config import get_user_home
|
|
934
|
+
|
|
933
935
|
env_check_cache_dir = os.path.join(
|
|
934
|
-
|
|
936
|
+
get_user_home(), ".calkit", "env-checks"
|
|
935
937
|
)
|
|
936
938
|
os.makedirs(env_check_cache_dir, exist_ok=True)
|
|
937
939
|
env_check_cache_path = os.path.join(env_check_cache_dir, f"{name}.sqlite")
|