calkit-python 0.17.6__tar.gz → 0.19.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {calkit_python-0.17.6 → calkit_python-0.19.0}/PKG-INFO +1 -1
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/__init__.py +1 -1
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/config.py +4 -3
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/core.py +4 -1
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/main.py +28 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/new.py +212 -9
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/core.py +32 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/dvc.py +26 -1
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/git.py +7 -3
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/models.py +7 -1
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/cli/test_main.py +22 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/cli/test_new.py +175 -0
- calkit_python-0.19.0/docs/tutorials/existing-project.md +552 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/mkdocs.yml +1 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/.github/FUNDING.yml +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/.github/workflows/docs.yml +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/.gitignore +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/LICENSE +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/README.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/__main__.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/calc.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/check.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/check.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/import_.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/list.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/office.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cli/update.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/cloud.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/conda.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/config.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/datasets.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/docker.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/gui.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/jupyter.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/magics.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/office.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/ops.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/server.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/core.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/core.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_calc.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_check.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_conda.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_dvc.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/CNAME +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/apps.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/calculations.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/calkit-yaml.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/cli-reference.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/cloud-integration.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/datasets.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/environments.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/examples.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/help.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/img/c-to-the-k-white.svg +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/img/calkit-no-bg.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/index.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/installation.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/local-server.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/pipeline/index.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/pipeline/manual-steps.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/references.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/conda-envs.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/first-project.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/push.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/chart-more-rows.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/create-project.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/excel-chart.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/excel-data.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/needs-clone.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/new-stage.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/status-more-rows.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/untracked-data.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/updated-publication.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/office/workflow-page.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/clone.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/create-project.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/new-token.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/reclone.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/latex-codespaces.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/matlab.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/office.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/openfoam.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/docs/version-control.md +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/pyproject.toml +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/test/pipeline.ipynb +0 -0
- {calkit_python-0.17.6 → calkit_python-0.19.0}/uv.lock +0 -0
|
@@ -65,13 +65,14 @@ def setup_remote(
|
|
|
65
65
|
raise_error("DVC remote config failed; have you run `dvc init`?")
|
|
66
66
|
except InvalidGitRepositoryError:
|
|
67
67
|
raise_error("Current directory is not a Git repository")
|
|
68
|
+
except ValueError as e:
|
|
69
|
+
raise_error(e)
|
|
68
70
|
if not no_commit:
|
|
69
71
|
repo = git.Repo()
|
|
70
|
-
repo.git.reset()
|
|
71
72
|
repo.git.add(".dvc/config")
|
|
72
|
-
if calkit.git.get_staged_files():
|
|
73
|
+
if ".dvc/config" in calkit.git.get_staged_files():
|
|
73
74
|
typer.echo("Committing changes to DVC config")
|
|
74
|
-
repo.git.commit(["-m", "Set DVC remote"])
|
|
75
|
+
repo.git.commit([".dvc/config", "-m", "Set DVC remote"])
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
@config_app.command(name="setup-remote-auth", help="Alias for 'remote-auth'.")
|
|
@@ -11,7 +11,10 @@ def print_sep(name: str):
|
|
|
11
11
|
txt_width = len(name) + 2
|
|
12
12
|
buffer_width = (width - txt_width) // 2
|
|
13
13
|
buffer = "-" * buffer_width
|
|
14
|
-
|
|
14
|
+
line = f"{buffer} {name} {buffer}"
|
|
15
|
+
if len(line) == (width - 1):
|
|
16
|
+
line += "-"
|
|
17
|
+
typer.echo(line)
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def run_cmd(cmd: list[str]):
|
|
@@ -208,6 +208,23 @@ def clone(
|
|
|
208
208
|
@app.command(name="status")
|
|
209
209
|
def get_status():
|
|
210
210
|
"""Get a unified Git and DVC status."""
|
|
211
|
+
print_sep("Project")
|
|
212
|
+
# Print latest status
|
|
213
|
+
status = calkit.get_latest_project_status()
|
|
214
|
+
if status is not None:
|
|
215
|
+
ts = status.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
216
|
+
colors = {
|
|
217
|
+
"in-progress": "blue",
|
|
218
|
+
"on-hold": "yellow",
|
|
219
|
+
"completed": "green",
|
|
220
|
+
}
|
|
221
|
+
status_txt = typer.style(status.status, fg=colors.get(status.status))
|
|
222
|
+
typer.echo(f"Current status: {status_txt} (updated {ts} UTC)")
|
|
223
|
+
else:
|
|
224
|
+
typer.echo(
|
|
225
|
+
'Project status not set. Use "calkit new status" to update.'
|
|
226
|
+
)
|
|
227
|
+
typer.echo()
|
|
211
228
|
print_sep("Code (Git)")
|
|
212
229
|
run_cmd(["git", "status"])
|
|
213
230
|
typer.echo()
|
|
@@ -1047,6 +1064,17 @@ def run_in_env(
|
|
|
1047
1064
|
jobs[job_key] = job
|
|
1048
1065
|
with open(jobs_fpath, "w") as f:
|
|
1049
1066
|
calkit.ryaml.dump(jobs, f)
|
|
1067
|
+
elif env["kind"] == "renv":
|
|
1068
|
+
try:
|
|
1069
|
+
subprocess.check_call(
|
|
1070
|
+
["Rscript", "-e", "'renv::restore()'"], cwd=wdir
|
|
1071
|
+
)
|
|
1072
|
+
except subprocess.CalledProcessError:
|
|
1073
|
+
raise_error("Failed to check renv")
|
|
1074
|
+
try:
|
|
1075
|
+
subprocess.check_call(cmd, cwd=wdir)
|
|
1076
|
+
except subprocess.CalledProcessError:
|
|
1077
|
+
raise_error("Failed to run in renv")
|
|
1050
1078
|
else:
|
|
1051
1079
|
raise_error("Environment kind not supported")
|
|
1052
1080
|
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import csv
|
|
5
6
|
import os
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
9
|
+
from enum import Enum
|
|
8
10
|
|
|
9
11
|
import git
|
|
10
12
|
import typer
|
|
11
|
-
from git.exc import InvalidGitRepositoryError
|
|
13
|
+
from git.exc import GitCommandError, InvalidGitRepositoryError
|
|
12
14
|
from typing_extensions import Annotated
|
|
13
15
|
|
|
14
16
|
import calkit
|
|
@@ -95,10 +97,10 @@ def new_project(
|
|
|
95
97
|
f"Check out the docs at {docs_url}."
|
|
96
98
|
)
|
|
97
99
|
abs_path = os.path.abspath(path)
|
|
98
|
-
if
|
|
99
|
-
raise_error(
|
|
100
|
-
|
|
101
|
-
)
|
|
100
|
+
if template and os.path.exists(abs_path):
|
|
101
|
+
raise_error("Must specify a new directory if using --template")
|
|
102
|
+
if cloud and os.path.isdir(os.path.join(abs_path, ".git")):
|
|
103
|
+
raise_error("Must not already be a Git repo to use --cloud")
|
|
102
104
|
ck_info_fpath = os.path.join(abs_path, "calkit.yaml")
|
|
103
105
|
if os.path.isfile(ck_info_fpath) and not overwrite:
|
|
104
106
|
raise_error(
|
|
@@ -118,8 +120,8 @@ def new_project(
|
|
|
118
120
|
typer.echo(f"Using title: {title}")
|
|
119
121
|
if cloud:
|
|
120
122
|
# Cloud should allow None, which will allow us to post just the name
|
|
121
|
-
# NOTE: This will fail if the user hasn't logged into
|
|
122
|
-
#
|
|
123
|
+
# NOTE: This will fail if the user hasn't logged into the Calkit Cloud
|
|
124
|
+
# in 6 months, since their GitHub refresh token stored is expired
|
|
123
125
|
try:
|
|
124
126
|
resp = calkit.cloud.post(
|
|
125
127
|
"/projects",
|
|
@@ -134,8 +136,21 @@ def new_project(
|
|
|
134
136
|
)
|
|
135
137
|
except Exception as e:
|
|
136
138
|
raise_error(f"Posting new project to cloud failed: {e}")
|
|
137
|
-
# Now clone here
|
|
138
|
-
|
|
139
|
+
# Now clone here
|
|
140
|
+
if not os.path.isdir(abs_path):
|
|
141
|
+
subprocess.run(["git", "clone", resp["git_repo_url"], abs_path])
|
|
142
|
+
else:
|
|
143
|
+
typer.echo("Fetching from newly create Git repo")
|
|
144
|
+
repo = git.Repo.init(abs_path, initial_branch="main")
|
|
145
|
+
repo.git.remote(["add", "origin", resp["git_repo_url"]])
|
|
146
|
+
repo.git.fetch()
|
|
147
|
+
checkout_cmd = ["-t", "origin/main"]
|
|
148
|
+
if overwrite:
|
|
149
|
+
checkout_cmd.append("--force")
|
|
150
|
+
try:
|
|
151
|
+
repo.git.checkout(checkout_cmd)
|
|
152
|
+
except GitCommandError as e:
|
|
153
|
+
raise_error(f"Failed to check out main branch: {e}")
|
|
139
154
|
try:
|
|
140
155
|
calkit.dvc.set_remote_auth(wdir=abs_path)
|
|
141
156
|
except Exception:
|
|
@@ -1231,3 +1246,191 @@ def new_pixi_env(
|
|
|
1231
1246
|
repo.git.add("calkit.yaml")
|
|
1232
1247
|
if not no_commit and repo.git.diff("--staged"):
|
|
1233
1248
|
repo.git.commit(["-m", f"Add pixi env {name}"])
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
class Status(str, Enum):
|
|
1252
|
+
in_progress = "in-progress"
|
|
1253
|
+
on_hold = "on-hold"
|
|
1254
|
+
completed = "completed"
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
@new_app.command(name="status")
|
|
1258
|
+
def new_status(
|
|
1259
|
+
status: Annotated[
|
|
1260
|
+
Status,
|
|
1261
|
+
typer.Argument(help="Current status of the project."),
|
|
1262
|
+
],
|
|
1263
|
+
message: Annotated[
|
|
1264
|
+
str,
|
|
1265
|
+
typer.Option(
|
|
1266
|
+
"--message",
|
|
1267
|
+
"-m",
|
|
1268
|
+
help="Optional message describing the status.",
|
|
1269
|
+
),
|
|
1270
|
+
] = "",
|
|
1271
|
+
no_commit: Annotated[
|
|
1272
|
+
bool,
|
|
1273
|
+
typer.Option(
|
|
1274
|
+
"--no-commit", help="Do not commit changes to the status log."
|
|
1275
|
+
),
|
|
1276
|
+
] = False,
|
|
1277
|
+
):
|
|
1278
|
+
"""Add a new project status to the log."""
|
|
1279
|
+
typer.echo(f"Adding {status.value} status log entry")
|
|
1280
|
+
fpath = os.path.join(".calkit", "status.csv")
|
|
1281
|
+
os.makedirs(".calkit", exist_ok=True)
|
|
1282
|
+
now = calkit.utcnow(remove_tz=False)
|
|
1283
|
+
# Append to end of CSV
|
|
1284
|
+
write_header = not os.path.isfile(fpath)
|
|
1285
|
+
with open(fpath, "a") as f:
|
|
1286
|
+
writer = csv.writer(f)
|
|
1287
|
+
if write_header:
|
|
1288
|
+
writer.writerow(["timestamp", "status", "message"])
|
|
1289
|
+
writer.writerow([now.isoformat(), status.value, message])
|
|
1290
|
+
if not no_commit:
|
|
1291
|
+
typer.echo("Committing")
|
|
1292
|
+
repo = git.Repo()
|
|
1293
|
+
repo.git.add(fpath)
|
|
1294
|
+
repo.git.commit([fpath, "-m", f"Add {status.value} status log entry"])
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
class StageKind(str, Enum):
|
|
1298
|
+
python_script = "python-script"
|
|
1299
|
+
latex = "latex"
|
|
1300
|
+
r_script = "r-script"
|
|
1301
|
+
sh_script = "sh-script"
|
|
1302
|
+
bash_script = "bash-script"
|
|
1303
|
+
zsh_script = "zsh-script"
|
|
1304
|
+
matlab_script = "matlab-script"
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
@new_app.command(name="stage")
|
|
1308
|
+
def new_stage(
|
|
1309
|
+
name: Annotated[
|
|
1310
|
+
str,
|
|
1311
|
+
typer.Option("--name", "-n", help="Stage name, typically kebab-case."),
|
|
1312
|
+
],
|
|
1313
|
+
kind: Annotated[
|
|
1314
|
+
StageKind, typer.Option("--kind", help="What kind of stage to create.")
|
|
1315
|
+
],
|
|
1316
|
+
target: Annotated[
|
|
1317
|
+
str,
|
|
1318
|
+
typer.Option(
|
|
1319
|
+
"--target", "-t", help="Target file, e.g., the script to run."
|
|
1320
|
+
),
|
|
1321
|
+
],
|
|
1322
|
+
environment: Annotated[
|
|
1323
|
+
str,
|
|
1324
|
+
typer.Option(
|
|
1325
|
+
"--environment", "-e", help="Environment to use to run the stage."
|
|
1326
|
+
),
|
|
1327
|
+
] = None,
|
|
1328
|
+
deps: Annotated[
|
|
1329
|
+
list[str],
|
|
1330
|
+
typer.Option("--dep", "-d", help="A path on which the stage depends."),
|
|
1331
|
+
] = [],
|
|
1332
|
+
outs: Annotated[
|
|
1333
|
+
list[str],
|
|
1334
|
+
typer.Option(
|
|
1335
|
+
"--out", "-o", help="A path that is produced by the stage."
|
|
1336
|
+
),
|
|
1337
|
+
] = [],
|
|
1338
|
+
outs_persist: Annotated[
|
|
1339
|
+
list[str],
|
|
1340
|
+
typer.Option(
|
|
1341
|
+
"--out-persist",
|
|
1342
|
+
help="An output that should not be deleted before running.",
|
|
1343
|
+
),
|
|
1344
|
+
] = [],
|
|
1345
|
+
outs_no_cache: Annotated[
|
|
1346
|
+
list[str],
|
|
1347
|
+
typer.Option(
|
|
1348
|
+
"--out-git",
|
|
1349
|
+
help="An output that should be tracked with Git instead of DVC.",
|
|
1350
|
+
),
|
|
1351
|
+
] = [],
|
|
1352
|
+
outs_persist_no_cache: Annotated[
|
|
1353
|
+
list[str],
|
|
1354
|
+
typer.Option(
|
|
1355
|
+
"--out-git-persist",
|
|
1356
|
+
help=(
|
|
1357
|
+
"An output that should be tracked with Git instead of DVC, "
|
|
1358
|
+
"and also should not be deleted before running stage."
|
|
1359
|
+
),
|
|
1360
|
+
),
|
|
1361
|
+
] = [],
|
|
1362
|
+
overwrite: Annotated[
|
|
1363
|
+
bool,
|
|
1364
|
+
typer.Option(
|
|
1365
|
+
"--overwrite",
|
|
1366
|
+
"--force",
|
|
1367
|
+
"-f",
|
|
1368
|
+
help="Overwrite an existing stage with this name if necessary.",
|
|
1369
|
+
),
|
|
1370
|
+
] = False,
|
|
1371
|
+
no_commit: Annotated[
|
|
1372
|
+
bool, typer.Option("--no-commit", help="Do not commit changes to Git.")
|
|
1373
|
+
] = False,
|
|
1374
|
+
):
|
|
1375
|
+
"""Create a new pipeline stage."""
|
|
1376
|
+
ck_info = calkit.load_calkit_info()
|
|
1377
|
+
if environment is None:
|
|
1378
|
+
warn("No environment is specified")
|
|
1379
|
+
cmd = ""
|
|
1380
|
+
else:
|
|
1381
|
+
if environment not in ck_info["environments"]:
|
|
1382
|
+
raise_error(f"Environment '{environment}' does not exist")
|
|
1383
|
+
cmd = f"calkit xenv -n {environment} -- "
|
|
1384
|
+
# Add environment path as a dependency
|
|
1385
|
+
env_path = ck_info["environments"][environment].get("path")
|
|
1386
|
+
if env_path is not None:
|
|
1387
|
+
deps = [env_path] + deps
|
|
1388
|
+
if not os.path.exists(target):
|
|
1389
|
+
raise_error(f"Target '{target}' does not exist")
|
|
1390
|
+
if kind.value == "python-script":
|
|
1391
|
+
cmd += f"python {target}"
|
|
1392
|
+
elif kind.value == "latex":
|
|
1393
|
+
cmd += f"latexmk -cd -interaction=nonstopmode -pdf {target}"
|
|
1394
|
+
out_target = target.removesuffix(".tex") + ".pdf"
|
|
1395
|
+
if out_target not in (
|
|
1396
|
+
outs + outs_no_cache + outs_persist + outs_persist_no_cache
|
|
1397
|
+
):
|
|
1398
|
+
outs = [out_target] + outs
|
|
1399
|
+
elif kind.value == "matlab-script":
|
|
1400
|
+
cmd += f"matlab -noFigureWindows -batch \"run('{target}');\""
|
|
1401
|
+
elif kind.value == "sh-script":
|
|
1402
|
+
cmd += f"sh {target}"
|
|
1403
|
+
elif kind.value == "bash-script":
|
|
1404
|
+
cmd += f"bash {target}"
|
|
1405
|
+
elif kind.value == "zsh-script":
|
|
1406
|
+
cmd += f"zsh {target}"
|
|
1407
|
+
elif kind.value == "r-script":
|
|
1408
|
+
cmd += f"Rscript {target}"
|
|
1409
|
+
add_cmd = ["dvc", "stage", "add", "-n", name]
|
|
1410
|
+
for dep in [target] + deps:
|
|
1411
|
+
add_cmd += ["-d", dep]
|
|
1412
|
+
for out in outs:
|
|
1413
|
+
add_cmd += ["-o", out]
|
|
1414
|
+
for out in outs_no_cache:
|
|
1415
|
+
add_cmd += ["--outs-no-cache", out]
|
|
1416
|
+
for out in outs_persist:
|
|
1417
|
+
add_cmd += ["--outs-persist", out]
|
|
1418
|
+
for out in outs_persist_no_cache:
|
|
1419
|
+
add_cmd += ["--outs-persist-no-cache", out]
|
|
1420
|
+
if overwrite:
|
|
1421
|
+
add_cmd.append("-f")
|
|
1422
|
+
add_cmd.append(cmd)
|
|
1423
|
+
try:
|
|
1424
|
+
subprocess.check_call(add_cmd)
|
|
1425
|
+
except subprocess.CalledProcessError:
|
|
1426
|
+
raise_error("Failed to create stage")
|
|
1427
|
+
if not no_commit:
|
|
1428
|
+
try:
|
|
1429
|
+
repo = git.Repo()
|
|
1430
|
+
except InvalidGitRepositoryError:
|
|
1431
|
+
raise_error("Can't commit because this is not a Git repo")
|
|
1432
|
+
repo.git.add("dvc.yaml")
|
|
1433
|
+
if "dvc.yaml" in calkit.git.get_staged_files():
|
|
1434
|
+
repo.git.commit(
|
|
1435
|
+
["dvc.yaml", "-m", f"Add {kind.value} pipeline stage '{name}'"]
|
|
1436
|
+
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
|
+
import csv
|
|
6
7
|
import glob
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
@@ -11,6 +12,8 @@ import pickle
|
|
|
11
12
|
import re
|
|
12
13
|
import subprocess
|
|
13
14
|
|
|
15
|
+
from calkit.models import ProjectStatus
|
|
16
|
+
|
|
14
17
|
import requests
|
|
15
18
|
|
|
16
19
|
try:
|
|
@@ -385,3 +388,32 @@ def get_size(path: str):
|
|
|
385
388
|
def to_kebab_case(str) -> str:
|
|
386
389
|
"""Convert a string to kebab-case."""
|
|
387
390
|
return re.sub(r"[-_,\.\ ]", "-", str.lower())
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_project_status_history(
|
|
394
|
+
wdir: str = None, as_pydantic=True
|
|
395
|
+
) -> list[ProjectStatus] | list[dict]:
|
|
396
|
+
statuses = []
|
|
397
|
+
fpath = os.path.join(".calkit", "status.csv")
|
|
398
|
+
if wdir is not None:
|
|
399
|
+
fpath = os.path.join(wdir, fpath)
|
|
400
|
+
if os.path.isfile(fpath):
|
|
401
|
+
with open(fpath) as f:
|
|
402
|
+
reader = csv.reader(f)
|
|
403
|
+
next(reader, None) # Skip header row
|
|
404
|
+
for line in reader:
|
|
405
|
+
ts, status, message = line
|
|
406
|
+
ts = datetime.fromisoformat(ts)
|
|
407
|
+
obj = ProjectStatus(
|
|
408
|
+
timestamp=ts, status=status, message=message
|
|
409
|
+
)
|
|
410
|
+
if not as_pydantic:
|
|
411
|
+
obj = obj.model_dump()
|
|
412
|
+
statuses.append(obj)
|
|
413
|
+
return statuses
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def get_latest_project_status(wdir: str = None) -> ProjectStatus | None:
|
|
417
|
+
statuses = get_project_status_history(wdir=wdir)
|
|
418
|
+
if statuses:
|
|
419
|
+
return statuses[-1]
|
|
@@ -7,8 +7,10 @@ import os
|
|
|
7
7
|
import subprocess
|
|
8
8
|
|
|
9
9
|
import dvc.repo
|
|
10
|
+
import git
|
|
10
11
|
|
|
11
12
|
import calkit
|
|
13
|
+
from calkit.cli import warn
|
|
12
14
|
from calkit.config import get_app_name
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__package__)
|
|
@@ -16,7 +18,25 @@ logger.setLevel(logging.INFO)
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def configure_remote(wdir: str = None):
|
|
19
|
-
|
|
21
|
+
try:
|
|
22
|
+
project_name = calkit.git.detect_project_name(path=wdir)
|
|
23
|
+
except ValueError as e:
|
|
24
|
+
raise ValueError(f"Can't detect project name: {e}")
|
|
25
|
+
# If Git origin is not set, set that
|
|
26
|
+
repo = git.Repo(wdir)
|
|
27
|
+
try:
|
|
28
|
+
repo.remote()
|
|
29
|
+
except ValueError:
|
|
30
|
+
warn("No Git remote defined; querying Calkit Cloud")
|
|
31
|
+
# Try to fetch Git repo URL from Calkit cloud
|
|
32
|
+
try:
|
|
33
|
+
project = calkit.cloud.get(f"/projects/{project_name}")
|
|
34
|
+
url = project["git_repo_url"]
|
|
35
|
+
except Exception as e:
|
|
36
|
+
raise ValueError(f"Could not fetch project info: {e}")
|
|
37
|
+
if not url.endswith(".git"):
|
|
38
|
+
url += ".git"
|
|
39
|
+
repo.git.remote(["add", "origin", url])
|
|
20
40
|
base_url = calkit.cloud.get_base_url()
|
|
21
41
|
remote_url = f"{base_url}/projects/{project_name}/dvc"
|
|
22
42
|
subprocess.check_call(
|
|
@@ -110,3 +130,8 @@ def list_paths(wdir: str = None) -> list[str]:
|
|
|
110
130
|
"""List paths tracked with DVC."""
|
|
111
131
|
dvc_repo = dvc.repo.Repo(wdir)
|
|
112
132
|
return [p.get("path") for p in dvc_repo.ls(".", dvc_only=True)]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_output_revisions(path: str):
|
|
136
|
+
"""Get all revisions of a pipeline output."""
|
|
137
|
+
pass
|
|
@@ -12,9 +12,13 @@ def detect_project_name(path: str = None) -> str:
|
|
|
12
12
|
ck_info = calkit.load_calkit_info(wdir=path)
|
|
13
13
|
name = ck_info.get("name")
|
|
14
14
|
owner = ck_info.get("owner")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
if name is None or owner is None:
|
|
16
|
+
try:
|
|
17
|
+
url = git.Repo(path=path).remote().url
|
|
18
|
+
except ValueError:
|
|
19
|
+
raise ValueError("No Git remote set with name 'origin'")
|
|
20
|
+
from_url = url.split("github.com")[-1][1:].removesuffix(".git")
|
|
21
|
+
owner_name, project_name = from_url.split("/")
|
|
18
22
|
if name is None:
|
|
19
23
|
name = project_name
|
|
20
24
|
if owner is None:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import timedelta
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
6
|
from typing import Literal
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
@@ -225,6 +225,12 @@ class DerivedFromProject(BaseModel):
|
|
|
225
225
|
git_rev: str
|
|
226
226
|
|
|
227
227
|
|
|
228
|
+
class ProjectStatus(BaseModel):
|
|
229
|
+
timestamp: datetime
|
|
230
|
+
status: Literal["in-progress", "on-hold", "completed"]
|
|
231
|
+
message: str | None = None
|
|
232
|
+
|
|
233
|
+
|
|
228
234
|
class ProjectInfo(BaseModel):
|
|
229
235
|
"""All of the project's information or metadata, written to the
|
|
230
236
|
``calkit.yaml`` file.
|
|
@@ -296,3 +296,25 @@ def test_add(tmp_dir):
|
|
|
296
296
|
f.write(os.urandom(2_000_000))
|
|
297
297
|
subprocess.check_call(["calkit", "add", "data2", "-M"])
|
|
298
298
|
assert repo.head.commit.message.strip() == "Add data2"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_status(tmp_dir):
|
|
302
|
+
subprocess.check_call(["calkit", "status"])
|
|
303
|
+
subprocess.check_call(["calkit", "init"])
|
|
304
|
+
subprocess.check_call(["calkit", "new", "status", "in-progress"])
|
|
305
|
+
subprocess.check_call(["calkit", "status"])
|
|
306
|
+
status = calkit.get_latest_project_status()
|
|
307
|
+
assert status.status == "in-progress"
|
|
308
|
+
assert not status.message
|
|
309
|
+
subprocess.check_call(
|
|
310
|
+
["calkit", "new", "status", "completed", "-m", "We're done."]
|
|
311
|
+
)
|
|
312
|
+
subprocess.check_call(["calkit", "status"])
|
|
313
|
+
status = calkit.get_latest_project_status()
|
|
314
|
+
assert status.status == "completed"
|
|
315
|
+
assert status.message == "We're done."
|
|
316
|
+
history = calkit.get_project_status_history()
|
|
317
|
+
assert history[-1] == status
|
|
318
|
+
calkit.get_project_status_history(as_pydantic=False)
|
|
319
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
320
|
+
subprocess.check_call(["calkit", "new", "status", "very-cool"])
|
|
@@ -228,3 +228,178 @@ def test_new_project(tmp_dir):
|
|
|
228
228
|
assert repo.git.ls_files(".devcontainer")
|
|
229
229
|
ck_info = calkit.load_calkit_info()
|
|
230
230
|
assert ck_info["title"] == "My new project"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_new_stage(tmp_dir):
|
|
234
|
+
subprocess.check_call(["calkit", "init"])
|
|
235
|
+
subprocess.check_call(
|
|
236
|
+
[
|
|
237
|
+
"calkit",
|
|
238
|
+
"new",
|
|
239
|
+
"docker-env",
|
|
240
|
+
"--name",
|
|
241
|
+
"tex",
|
|
242
|
+
"--image",
|
|
243
|
+
"texlive/texlive:latest-full",
|
|
244
|
+
]
|
|
245
|
+
)
|
|
246
|
+
subprocess.check_call(
|
|
247
|
+
[
|
|
248
|
+
"calkit",
|
|
249
|
+
"new",
|
|
250
|
+
"uv-venv",
|
|
251
|
+
"--name",
|
|
252
|
+
"py",
|
|
253
|
+
"requests",
|
|
254
|
+
]
|
|
255
|
+
)
|
|
256
|
+
with open("plot.py", "w") as f:
|
|
257
|
+
f.write("print('hi')")
|
|
258
|
+
with open("paper.tex", "w") as f:
|
|
259
|
+
f.write("Hello")
|
|
260
|
+
with open("data.csv", "w") as f:
|
|
261
|
+
f.write("data")
|
|
262
|
+
# Create a Python script stage
|
|
263
|
+
subprocess.check_call(
|
|
264
|
+
[
|
|
265
|
+
"calkit",
|
|
266
|
+
"new",
|
|
267
|
+
"stage",
|
|
268
|
+
"--name",
|
|
269
|
+
"plot",
|
|
270
|
+
"--kind",
|
|
271
|
+
"python-script",
|
|
272
|
+
"--environment",
|
|
273
|
+
"py",
|
|
274
|
+
"--target",
|
|
275
|
+
"plot.py",
|
|
276
|
+
"--dep",
|
|
277
|
+
"data.csv",
|
|
278
|
+
"--out",
|
|
279
|
+
"plot1.png",
|
|
280
|
+
"-o",
|
|
281
|
+
"plot2.png",
|
|
282
|
+
]
|
|
283
|
+
)
|
|
284
|
+
pipeline = calkit.dvc.read_pipeline()
|
|
285
|
+
assert (
|
|
286
|
+
pipeline["stages"]["plot"]["cmd"]
|
|
287
|
+
== "calkit xenv -n py -- python plot.py"
|
|
288
|
+
)
|
|
289
|
+
assert set(pipeline["stages"]["plot"]["deps"]) == set(
|
|
290
|
+
["plot.py", "data.csv", "requirements.txt"]
|
|
291
|
+
)
|
|
292
|
+
assert set(pipeline["stages"]["plot"]["outs"]) == set(
|
|
293
|
+
["plot1.png", "plot2.png"]
|
|
294
|
+
)
|
|
295
|
+
# Create a LaTeX stage
|
|
296
|
+
subprocess.check_call(
|
|
297
|
+
[
|
|
298
|
+
"calkit",
|
|
299
|
+
"new",
|
|
300
|
+
"stage",
|
|
301
|
+
"--name",
|
|
302
|
+
"build-paper",
|
|
303
|
+
"--kind",
|
|
304
|
+
"latex",
|
|
305
|
+
"--environment",
|
|
306
|
+
"tex",
|
|
307
|
+
"--target",
|
|
308
|
+
"paper.tex",
|
|
309
|
+
"--dep",
|
|
310
|
+
"plot1.png",
|
|
311
|
+
"-d",
|
|
312
|
+
"plot2.png",
|
|
313
|
+
"--out",
|
|
314
|
+
"paper.pdf",
|
|
315
|
+
]
|
|
316
|
+
)
|
|
317
|
+
pipeline = calkit.dvc.read_pipeline()
|
|
318
|
+
assert pipeline["stages"]["build-paper"]["cmd"] == (
|
|
319
|
+
"calkit xenv -n tex -- "
|
|
320
|
+
"latexmk -cd -interaction=nonstopmode -pdf paper.tex"
|
|
321
|
+
)
|
|
322
|
+
assert set(pipeline["stages"]["build-paper"]["deps"]) == set(
|
|
323
|
+
[
|
|
324
|
+
"paper.tex",
|
|
325
|
+
"plot1.png",
|
|
326
|
+
"plot2.png",
|
|
327
|
+
]
|
|
328
|
+
)
|
|
329
|
+
assert pipeline["stages"]["build-paper"]["outs"] == ["paper.pdf"]
|
|
330
|
+
# Check that we can create a MATLAB script with no environment
|
|
331
|
+
with open("script.m", "w") as f:
|
|
332
|
+
f.write("script")
|
|
333
|
+
subprocess.check_call(
|
|
334
|
+
[
|
|
335
|
+
"calkit",
|
|
336
|
+
"new",
|
|
337
|
+
"stage",
|
|
338
|
+
"--name",
|
|
339
|
+
"plot",
|
|
340
|
+
"-f",
|
|
341
|
+
"--kind",
|
|
342
|
+
"matlab-script",
|
|
343
|
+
"--target",
|
|
344
|
+
"script.m",
|
|
345
|
+
"--dep",
|
|
346
|
+
"data.csv",
|
|
347
|
+
"--out",
|
|
348
|
+
"plot1.png",
|
|
349
|
+
"-o",
|
|
350
|
+
"plot2.png",
|
|
351
|
+
]
|
|
352
|
+
)
|
|
353
|
+
pipeline = calkit.dvc.read_pipeline()
|
|
354
|
+
assert (
|
|
355
|
+
pipeline["stages"]["plot"]["cmd"]
|
|
356
|
+
== "matlab -noFigureWindows -batch \"run('script.m');\""
|
|
357
|
+
)
|
|
358
|
+
assert set(pipeline["stages"]["plot"]["deps"]) == set(
|
|
359
|
+
["script.m", "data.csv"]
|
|
360
|
+
)
|
|
361
|
+
assert set(pipeline["stages"]["plot"]["outs"]) == set(
|
|
362
|
+
["plot1.png", "plot2.png"]
|
|
363
|
+
)
|
|
364
|
+
# Check that we fail for a nonexistent target
|
|
365
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
366
|
+
subprocess.check_call(
|
|
367
|
+
[
|
|
368
|
+
"calkit",
|
|
369
|
+
"new",
|
|
370
|
+
"stage",
|
|
371
|
+
"--name",
|
|
372
|
+
"plot",
|
|
373
|
+
"-f",
|
|
374
|
+
"--kind",
|
|
375
|
+
"matlab-script",
|
|
376
|
+
"--target",
|
|
377
|
+
"script2.m",
|
|
378
|
+
"--out",
|
|
379
|
+
"plot1.png",
|
|
380
|
+
"-o",
|
|
381
|
+
"plot2.png",
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
# Check that we fail to create a stage with a non-existent environment
|
|
385
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
386
|
+
subprocess.check_call(
|
|
387
|
+
[
|
|
388
|
+
"calkit",
|
|
389
|
+
"new",
|
|
390
|
+
"stage",
|
|
391
|
+
"--name",
|
|
392
|
+
"plot",
|
|
393
|
+
"-f",
|
|
394
|
+
"--kind",
|
|
395
|
+
"python-script",
|
|
396
|
+
"--target",
|
|
397
|
+
"plot.py",
|
|
398
|
+
"--out",
|
|
399
|
+
"plot1.png",
|
|
400
|
+
"-o",
|
|
401
|
+
"plot2.png",
|
|
402
|
+
"-e",
|
|
403
|
+
"nonexistent-env",
|
|
404
|
+
]
|
|
405
|
+
)
|