calkit-python 0.10.1__tar.gz → 0.11.1__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.10.1 → calkit_python-0.11.1}/.gitignore +1 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/PKG-INFO +37 -8
- {calkit_python-0.10.1 → calkit_python-0.11.1}/README.md +35 -7
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/__init__.py +3 -1
- calkit_python-0.11.1/calkit/calc.py +255 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/check.py +1 -1
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/config.py +4 -2
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/core.py +6 -2
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/main.py +39 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/new.py +258 -1
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/conda.py +37 -30
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/core.py +10 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/dvc.py +13 -7
- calkit_python-0.11.1/calkit/git.py +22 -0
- calkit_python-0.11.1/calkit/tests/test_calc.py +69 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/test_conda.py +38 -1
- {calkit_python-0.10.1 → calkit_python-0.11.1}/pyproject.toml +1 -0
- calkit_python-0.10.1/calkit/git.py +0 -17
- {calkit_python-0.10.1 → calkit_python-0.11.1}/.github/FUNDING.yml +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/LICENSE +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/check.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/import_.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/list.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/office.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cli/update.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/cloud.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/config.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/data.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/docker.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/gui.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/jupyter.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/magics.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/models.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/office.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/server.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/core.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/core.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/cli/test_main.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/cli/test_new.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/test_check.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/test_dvc.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/docs/img/calkit-no-bg.png +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/docs/tutorials/conda-envs.md +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.10.1 → calkit_python-0.11.1}/test/pipeline.ipynb +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: calkit-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: Reproducibility simplified.
|
|
5
5
|
Project-URL: Homepage, https://github.com/calkit/calkit
|
|
6
6
|
Project-URL: Issues, https://github.com/calkit/calkit/issues
|
|
@@ -10,6 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.8
|
|
13
|
+
Requires-Dist: arithmeval
|
|
13
14
|
Requires-Dist: docx2pdf
|
|
14
15
|
Requires-Dist: dvc
|
|
15
16
|
Requires-Dist: eval-type-backport; python_version < '3.10'
|
|
@@ -90,23 +91,51 @@ management interface and a DVC remote for easily storing all versions of your
|
|
|
90
91
|
data/code/figures/publications, interacting with your collaborators,
|
|
91
92
|
reusing others' research artifacts, etc.
|
|
92
93
|
|
|
93
|
-
After signing up, visit the
|
|
94
|
-
|
|
94
|
+
After signing up, visit the
|
|
95
|
+
[settings](https://calkit.io/settings?tab=tokens)
|
|
96
|
+
page and create a token for use with the API.
|
|
95
97
|
Then run
|
|
96
98
|
|
|
97
99
|
```sh
|
|
98
100
|
calkit config set token ${YOUR_TOKEN_HERE}
|
|
99
101
|
```
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
## Quickstart
|
|
104
|
+
|
|
105
|
+
After installing Calkit and setting your token as described above, run:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
calkit new project calkit-project-1 \
|
|
109
|
+
--title "My first Calkit project" \
|
|
110
|
+
--template calkit/example-basic \
|
|
111
|
+
--cloud \
|
|
112
|
+
--public
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This will create a new project from the
|
|
116
|
+
[`calkit/example-basic`](https://github.com/calkit/example-basic)
|
|
117
|
+
template,
|
|
118
|
+
creating it in the cloud and cloning to `calkit-project-1`.
|
|
119
|
+
You should now be able to run:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
cd calkit-project-1
|
|
123
|
+
calkit run
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This will reproduce the project's pipeline.
|
|
127
|
+
Next, you can start adding stages to the pipeline,
|
|
128
|
+
modifying the Python environments and scripts,
|
|
129
|
+
and editing the paper.
|
|
130
|
+
All will be kept in sync with the `calkit run` command.
|
|
131
|
+
|
|
132
|
+
To back up all of your work, execute:
|
|
102
133
|
|
|
103
134
|
```sh
|
|
104
|
-
calkit
|
|
135
|
+
calkit save -am "Run pipeline"
|
|
105
136
|
```
|
|
106
137
|
|
|
107
|
-
This will
|
|
108
|
-
allow you to push versions of your data or pipeline outputs to the cloud
|
|
109
|
-
for safe storage and sharing with your collaborators.
|
|
138
|
+
This will commit and push to both GitHub and the Calkit Cloud.
|
|
110
139
|
|
|
111
140
|
## Tutorials
|
|
112
141
|
|
|
@@ -58,23 +58,51 @@ management interface and a DVC remote for easily storing all versions of your
|
|
|
58
58
|
data/code/figures/publications, interacting with your collaborators,
|
|
59
59
|
reusing others' research artifacts, etc.
|
|
60
60
|
|
|
61
|
-
After signing up, visit the
|
|
62
|
-
|
|
61
|
+
After signing up, visit the
|
|
62
|
+
[settings](https://calkit.io/settings?tab=tokens)
|
|
63
|
+
page and create a token for use with the API.
|
|
63
64
|
Then run
|
|
64
65
|
|
|
65
66
|
```sh
|
|
66
67
|
calkit config set token ${YOUR_TOKEN_HERE}
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
## Quickstart
|
|
71
|
+
|
|
72
|
+
After installing Calkit and setting your token as described above, run:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
calkit new project calkit-project-1 \
|
|
76
|
+
--title "My first Calkit project" \
|
|
77
|
+
--template calkit/example-basic \
|
|
78
|
+
--cloud \
|
|
79
|
+
--public
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This will create a new project from the
|
|
83
|
+
[`calkit/example-basic`](https://github.com/calkit/example-basic)
|
|
84
|
+
template,
|
|
85
|
+
creating it in the cloud and cloning to `calkit-project-1`.
|
|
86
|
+
You should now be able to run:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
cd calkit-project-1
|
|
90
|
+
calkit run
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This will reproduce the project's pipeline.
|
|
94
|
+
Next, you can start adding stages to the pipeline,
|
|
95
|
+
modifying the Python environments and scripts,
|
|
96
|
+
and editing the paper.
|
|
97
|
+
All will be kept in sync with the `calkit run` command.
|
|
98
|
+
|
|
99
|
+
To back up all of your work, execute:
|
|
70
100
|
|
|
71
101
|
```sh
|
|
72
|
-
calkit
|
|
102
|
+
calkit save -am "Run pipeline"
|
|
73
103
|
```
|
|
74
104
|
|
|
75
|
-
This will
|
|
76
|
-
allow you to push versions of your data or pipeline outputs to the cloud
|
|
77
|
-
for safe storage and sharing with your collaborators.
|
|
105
|
+
This will commit and push to both GitHub and the Calkit Cloud.
|
|
78
106
|
|
|
79
107
|
## Tutorials
|
|
80
108
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Functionality for calculations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import arithmetic_eval
|
|
8
|
+
import requests
|
|
9
|
+
from pydantic import BaseModel, model_validator
|
|
10
|
+
|
|
11
|
+
DTYPES = {"int": int, "float": float, "str": str}
|
|
12
|
+
DEFAULT_IN_TYPE = "float"
|
|
13
|
+
DEFAULT_OUT_TYPE = "float"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Input(BaseModel):
|
|
17
|
+
name: str
|
|
18
|
+
description: str | None = None
|
|
19
|
+
dtype: Literal["int", "float", "str"] = DEFAULT_IN_TYPE
|
|
20
|
+
min: int | float | None = None
|
|
21
|
+
max: int | float | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Output(BaseModel):
|
|
25
|
+
name: str
|
|
26
|
+
description: str | None = None
|
|
27
|
+
dtype: Literal["int", "float", "str"] = DEFAULT_OUT_TYPE
|
|
28
|
+
template: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Calculation(BaseModel):
|
|
32
|
+
kind: str
|
|
33
|
+
params: dict = {}
|
|
34
|
+
name: str | None = None
|
|
35
|
+
description: str | None = None
|
|
36
|
+
inputs: list[Input] | list[str]
|
|
37
|
+
output: Output | str
|
|
38
|
+
|
|
39
|
+
@model_validator(mode="after")
|
|
40
|
+
def validate_model(self) -> Calculation:
|
|
41
|
+
input_names = self.input_names
|
|
42
|
+
if self.output_name in input_names:
|
|
43
|
+
raise ValueError("Output name must not overlap with input names")
|
|
44
|
+
if len(set(input_names)) != len(input_names):
|
|
45
|
+
raise ValueError("Input names must be unique")
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def input_names(self) -> list[str]:
|
|
50
|
+
input_names = []
|
|
51
|
+
for i in self.inputs:
|
|
52
|
+
if isinstance(i, Input):
|
|
53
|
+
input_names.append(i.name)
|
|
54
|
+
else:
|
|
55
|
+
input_names.append(i)
|
|
56
|
+
return input_names
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def inputs_dict(self) -> dict[str, Input]:
|
|
60
|
+
res = {}
|
|
61
|
+
for i in self.inputs:
|
|
62
|
+
if isinstance(i, str):
|
|
63
|
+
res[i] = Input(name=i)
|
|
64
|
+
else:
|
|
65
|
+
res[i.name] = i
|
|
66
|
+
return res
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def output_name(self) -> str:
|
|
70
|
+
if isinstance(self.output, Output):
|
|
71
|
+
return self.output.name
|
|
72
|
+
return self.output
|
|
73
|
+
|
|
74
|
+
def check_inputs(self, **inputs) -> dict:
|
|
75
|
+
"""Check that the supplied inputs match those declared and do type
|
|
76
|
+
coercion.
|
|
77
|
+
"""
|
|
78
|
+
inputs_dict = self.inputs_dict
|
|
79
|
+
for k in inputs_dict:
|
|
80
|
+
if k not in inputs:
|
|
81
|
+
raise ValueError(f"Missing input {k}")
|
|
82
|
+
for k, v in inputs.items():
|
|
83
|
+
if k not in inputs_dict:
|
|
84
|
+
raise ValueError(f"{k} is not in declared inputs")
|
|
85
|
+
input_def = inputs_dict[k]
|
|
86
|
+
v = DTYPES[input_def.dtype](v)
|
|
87
|
+
inputs[k] = v
|
|
88
|
+
if input_def.min is not None and v < input_def.min:
|
|
89
|
+
raise ValueError(f"Input value {k} = {v} it too small")
|
|
90
|
+
if input_def.max is not None and v > input_def.max:
|
|
91
|
+
raise ValueError(f"Input value {k} = {v} is too large")
|
|
92
|
+
return inputs
|
|
93
|
+
|
|
94
|
+
def calculate(self, **inputs):
|
|
95
|
+
"""This is the method to override to implement custom logic.
|
|
96
|
+
|
|
97
|
+
Input and output type coercion will be handled outside.
|
|
98
|
+
"""
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
|
|
101
|
+
def evaluate(self, **inputs):
|
|
102
|
+
inputs = self.check_inputs(**inputs)
|
|
103
|
+
out = self.calculate(**inputs)
|
|
104
|
+
return self.coerce_output(out)
|
|
105
|
+
|
|
106
|
+
def coerce_output(self, val):
|
|
107
|
+
if isinstance(self.output, Output):
|
|
108
|
+
return DTYPES[self.output.dtype](val)
|
|
109
|
+
else:
|
|
110
|
+
return DTYPES[DEFAULT_OUT_TYPE](val)
|
|
111
|
+
|
|
112
|
+
def evaluate_and_format(self, **inputs) -> str:
|
|
113
|
+
res = self.evaluate(**inputs)
|
|
114
|
+
if isinstance(self.output, Output):
|
|
115
|
+
out_name = self.output.name
|
|
116
|
+
template = self.output.template
|
|
117
|
+
else:
|
|
118
|
+
out_name = self.output
|
|
119
|
+
template = None
|
|
120
|
+
if template is None:
|
|
121
|
+
template = "For input "
|
|
122
|
+
for input_name in inputs:
|
|
123
|
+
template += input_name + "={" + input_name + "}, "
|
|
124
|
+
template += "the output is "
|
|
125
|
+
template += out_name + "={" + out_name + "}."
|
|
126
|
+
return template.format(**(inputs | {out_name: res}))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class FormulaParams(BaseModel):
|
|
130
|
+
formula: str
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Formula(Calculation):
|
|
134
|
+
kind: str = "formula"
|
|
135
|
+
params: FormulaParams
|
|
136
|
+
|
|
137
|
+
def calculate(self, **inputs):
|
|
138
|
+
inputs = self.check_inputs(**inputs)
|
|
139
|
+
return arithmetic_eval.evaluate(self.params.formula, inputs)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class LinearParams(BaseModel):
|
|
143
|
+
coeffs: dict[str, float]
|
|
144
|
+
offset: float = 0.0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Linear(Calculation):
|
|
148
|
+
"""Calculation for a simple linear relationship."""
|
|
149
|
+
|
|
150
|
+
kind: str = "linear"
|
|
151
|
+
params: LinearParams
|
|
152
|
+
|
|
153
|
+
@model_validator(mode="after")
|
|
154
|
+
def validate_model(self) -> Linear:
|
|
155
|
+
if set(self.input_names) != set(self.params.coeffs.keys()):
|
|
156
|
+
raise ValueError("Coefficients must have same keys as input names")
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def calculate(self, **inputs):
|
|
160
|
+
val = self.params.offset
|
|
161
|
+
for input_name, input_val in inputs.items():
|
|
162
|
+
val += self.params.coeffs[input_name] * input_val
|
|
163
|
+
return val
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class LookupTableParams(BaseModel):
|
|
167
|
+
x_values: list[float]
|
|
168
|
+
y_values: list[float]
|
|
169
|
+
method: Literal["floor", "ceil", "round", "interpolate"] = "interpolate"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class LookupTable(Calculation):
|
|
173
|
+
"""A 1-D lookup table."""
|
|
174
|
+
|
|
175
|
+
kind: str = "lookup-table"
|
|
176
|
+
params: LookupTableParams
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class HttpRequestParams(BaseModel):
|
|
180
|
+
url: str
|
|
181
|
+
inputs_as_params: bool = True # Otherwise, use body
|
|
182
|
+
method: Literal["get", "post", "put"] = "get"
|
|
183
|
+
as_json: bool = True # Otherwise, return raw text
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class HttpRequest(Calculation):
|
|
187
|
+
"""Make an HTTP request and return the result.
|
|
188
|
+
|
|
189
|
+
This should not be run on a web server since it can be insecure.
|
|
190
|
+
For example, it could make requests to private services and return
|
|
191
|
+
sensitive data.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
kind: str = "http"
|
|
195
|
+
params: HttpRequestParams
|
|
196
|
+
|
|
197
|
+
def calculate(self, **inputs):
|
|
198
|
+
func = getattr(requests, self.params.method)
|
|
199
|
+
if self.params.inputs_as_params:
|
|
200
|
+
kws = {"params": inputs}
|
|
201
|
+
else:
|
|
202
|
+
kws = {"json", inputs}
|
|
203
|
+
resp: requests.Response = func(url=self.params.url, **kws)
|
|
204
|
+
resp.raise_for_status()
|
|
205
|
+
if self.params.as_json:
|
|
206
|
+
return resp.json()
|
|
207
|
+
else:
|
|
208
|
+
return resp.text
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class XGBoostModelParams(BaseModel):
|
|
212
|
+
path: str
|
|
213
|
+
type: Literal["classifier", "regressor"]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class XGBoostModel(Calculation):
|
|
217
|
+
"""Make predictions with an XGBoost model saved as JSON.
|
|
218
|
+
|
|
219
|
+
This is currently just a prototype and should not be expected to work.
|
|
220
|
+
|
|
221
|
+
One input, ``data``, should be defined to be passed to the model's
|
|
222
|
+
``predict`` method.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
kind: str = "xgboost"
|
|
226
|
+
params: XGBoostModelParams
|
|
227
|
+
|
|
228
|
+
def calculate(self, **inputs):
|
|
229
|
+
# Load model from JSON
|
|
230
|
+
import xgboost
|
|
231
|
+
|
|
232
|
+
# Convert model path to something that can be loaded if running on the
|
|
233
|
+
# Calkit Cloud
|
|
234
|
+
types = {
|
|
235
|
+
"classifier": xgboost.XGBClassifier,
|
|
236
|
+
"regressor": xgboost.XGBRegressor,
|
|
237
|
+
}
|
|
238
|
+
model = types[self.params.type]().load_model(self.params.path)
|
|
239
|
+
return model.predict(**inputs)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def parse(data: dict) -> Calculation:
|
|
243
|
+
if isinstance(data, BaseModel):
|
|
244
|
+
data = data.model_dump()
|
|
245
|
+
# Automatically take keys not in the `kind` and move them into `params`?
|
|
246
|
+
kinds = {"formula": Formula, "lookup-table": LookupTable, "linear": Linear}
|
|
247
|
+
return kinds[data["kind"]].model_validate(data)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def evaluate(calc_def: dict | Calculation, **inputs) -> dict:
|
|
251
|
+
return parse(calc_def).evaluate(**inputs)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def evaluate_and_format(calc_def: dict | Calculation, **inputs) -> str:
|
|
255
|
+
return parse(calc_def).evaluate_and_format(**inputs)
|
|
@@ -68,7 +68,7 @@ class ReproCheck(BaseModel):
|
|
|
68
68
|
if not self.n_dvc_remotes:
|
|
69
69
|
return (
|
|
70
70
|
"No DVC remotes have been defined. "
|
|
71
|
-
"Run `calkit config
|
|
71
|
+
"Run `calkit config remote` or `dvc remote add` next."
|
|
72
72
|
)
|
|
73
73
|
if not self.has_pipeline:
|
|
74
74
|
return (
|
|
@@ -42,7 +42,8 @@ def get_config_value(key: str) -> None:
|
|
|
42
42
|
print()
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
@config_app.command(name="setup-remote")
|
|
45
|
+
@config_app.command(name="setup-remote", help="Alias for 'remote'.")
|
|
46
|
+
@config_app.command(name="remote")
|
|
46
47
|
def setup_remote():
|
|
47
48
|
"""Setup the Calkit cloud as the default DVC remote and store a token in
|
|
48
49
|
the local config.
|
|
@@ -56,7 +57,8 @@ def setup_remote():
|
|
|
56
57
|
raise_error("Current directory is not a Git repository")
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
@config_app.command(name="setup-remote-auth")
|
|
60
|
+
@config_app.command(name="setup-remote-auth", help="Alias for 'remote-auth'.")
|
|
61
|
+
@config_app.command(name="remote-auth")
|
|
60
62
|
def setup_remote_auth():
|
|
61
63
|
"""Store a Calkit cloud token in the local DVC config for all Calkit
|
|
62
64
|
remotes.
|
|
@@ -23,6 +23,10 @@ def run_cmd(cmd: list[str]):
|
|
|
23
23
|
pty.spawn(cmd, lambda fd: os.read(fd, 1024))
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def raise_error(txt):
|
|
27
|
-
typer.echo(typer.style(txt, fg="red"), err=txt)
|
|
26
|
+
def raise_error(txt: str):
|
|
27
|
+
typer.echo(typer.style("Error: " + txt, fg="red"), err=txt)
|
|
28
28
|
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def warn(txt: str):
|
|
32
|
+
typer.echo(typer.style("Warning: " + txt, fg="yellow"))
|
|
@@ -978,3 +978,42 @@ def check_conda_env(
|
|
|
978
978
|
log_func=log_func,
|
|
979
979
|
relaxed=relaxed,
|
|
980
980
|
)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
@app.command(name="calc")
|
|
984
|
+
def run_calculation(
|
|
985
|
+
name: Annotated[str, typer.Argument(help="Calculation name.")],
|
|
986
|
+
inputs: Annotated[
|
|
987
|
+
list[str],
|
|
988
|
+
typer.Option(
|
|
989
|
+
"--input", "-i", help="Inputs defined like x=1 (with no spaces.)"
|
|
990
|
+
),
|
|
991
|
+
] = [],
|
|
992
|
+
no_formatting: Annotated[
|
|
993
|
+
bool,
|
|
994
|
+
typer.Option(
|
|
995
|
+
"--no-format", help="Do not format output before printing"
|
|
996
|
+
),
|
|
997
|
+
] = False,
|
|
998
|
+
):
|
|
999
|
+
"""Run a project's calculation."""
|
|
1000
|
+
ck_info = calkit.load_calkit_info()
|
|
1001
|
+
calcs = ck_info.get("calculations", {})
|
|
1002
|
+
if name not in calcs:
|
|
1003
|
+
raise_error(f"Calculation '{name}' not defined in calkit.yaml")
|
|
1004
|
+
try:
|
|
1005
|
+
calc = calkit.calc.parse(calcs[name])
|
|
1006
|
+
except Exception as e:
|
|
1007
|
+
raise_error(f"Invalid calculation: {e}")
|
|
1008
|
+
# Parse inputs
|
|
1009
|
+
parsed_inputs = {}
|
|
1010
|
+
for i in inputs:
|
|
1011
|
+
iname, ival = i.split("=")
|
|
1012
|
+
parsed_inputs[iname] = ival
|
|
1013
|
+
try:
|
|
1014
|
+
if no_formatting:
|
|
1015
|
+
typer.echo(calc.evaluate(**parsed_inputs))
|
|
1016
|
+
else:
|
|
1017
|
+
typer.echo(calc.evaluate_and_format(**parsed_inputs))
|
|
1018
|
+
except Exception as e:
|
|
1019
|
+
raise_error(f"Calculation failed: {e}")
|
|
@@ -3,21 +3,277 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
8
9
|
|
|
9
10
|
import git
|
|
10
11
|
import typer
|
|
12
|
+
from git.exc import InvalidGitRepositoryError
|
|
11
13
|
from typing_extensions import Annotated
|
|
12
14
|
|
|
13
15
|
import calkit
|
|
14
|
-
from calkit.cli import raise_error
|
|
16
|
+
from calkit.cli import raise_error, warn
|
|
17
|
+
from calkit.cli.update import update_devcontainer
|
|
15
18
|
from calkit.core import ryaml
|
|
16
19
|
from calkit.docker import LAYERS
|
|
17
20
|
|
|
18
21
|
new_app = typer.Typer(no_args_is_help=True)
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
@new_app.command(name="project")
|
|
25
|
+
def new_project(
|
|
26
|
+
path: Annotated[str, typer.Argument(help="Where to create the project.")],
|
|
27
|
+
name: Annotated[
|
|
28
|
+
str,
|
|
29
|
+
typer.Option(
|
|
30
|
+
"--name",
|
|
31
|
+
"-n",
|
|
32
|
+
help=(
|
|
33
|
+
"Project name. Will be inferred as kebab-cased directory "
|
|
34
|
+
"name if not provided."
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
] = None,
|
|
38
|
+
title: Annotated[
|
|
39
|
+
str, typer.Option("--title", help="Project title.")
|
|
40
|
+
] = None,
|
|
41
|
+
description: Annotated[
|
|
42
|
+
str, typer.Option("--description", help="Project description.")
|
|
43
|
+
] = None,
|
|
44
|
+
cloud: Annotated[
|
|
45
|
+
bool,
|
|
46
|
+
typer.Option(
|
|
47
|
+
"--cloud",
|
|
48
|
+
help=("Create this project in the cloud (Calkit and GitHub.)"),
|
|
49
|
+
),
|
|
50
|
+
] = False,
|
|
51
|
+
public: Annotated[
|
|
52
|
+
bool,
|
|
53
|
+
typer.Option(
|
|
54
|
+
"--public",
|
|
55
|
+
help="Create as a public project if --cloud is selected.",
|
|
56
|
+
),
|
|
57
|
+
] = False,
|
|
58
|
+
git_repo_url: Annotated[
|
|
59
|
+
str,
|
|
60
|
+
typer.Option(
|
|
61
|
+
"--git-url",
|
|
62
|
+
help=(
|
|
63
|
+
"Git repo URL. "
|
|
64
|
+
"Usually https://github.com/{your_name}/{project_name}."
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
] = None,
|
|
68
|
+
template: Annotated[
|
|
69
|
+
str,
|
|
70
|
+
typer.Option(
|
|
71
|
+
"--template",
|
|
72
|
+
"-t",
|
|
73
|
+
help=(
|
|
74
|
+
"Template from which to derive the project, e.g., "
|
|
75
|
+
"'calkit/example-basic'."
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
] = None,
|
|
79
|
+
no_commit: Annotated[
|
|
80
|
+
bool, typer.Option("--no-commit", help="Do not commit changes to Git.")
|
|
81
|
+
] = None,
|
|
82
|
+
overwrite: Annotated[
|
|
83
|
+
bool,
|
|
84
|
+
typer.Option(
|
|
85
|
+
"--overwrite",
|
|
86
|
+
"-f",
|
|
87
|
+
help="Overwrite project if one already exists.",
|
|
88
|
+
),
|
|
89
|
+
] = False,
|
|
90
|
+
):
|
|
91
|
+
"""Create a new project."""
|
|
92
|
+
# TODO: Update this when there is a real docs site up
|
|
93
|
+
docs_url = "https://github.com/calkit/calkit?tab=readme-ov-file#tutorials"
|
|
94
|
+
success_message = (
|
|
95
|
+
"\nCongrats on creating your new Calkit project!\n\n"
|
|
96
|
+
"Next, you'll probably want to start building your pipeline.\n\n"
|
|
97
|
+
f"Check out the docs at {docs_url}."
|
|
98
|
+
)
|
|
99
|
+
abs_path = os.path.abspath(path)
|
|
100
|
+
if (cloud or template) and os.path.exists(abs_path):
|
|
101
|
+
raise_error(
|
|
102
|
+
"Must specify a new directory if using --cloud or --template"
|
|
103
|
+
)
|
|
104
|
+
ck_info_fpath = os.path.join(abs_path, "calkit.yaml")
|
|
105
|
+
if os.path.isfile(ck_info_fpath) and not overwrite:
|
|
106
|
+
raise_error(
|
|
107
|
+
"Destination is already a Calkit project; "
|
|
108
|
+
"use --overwrite to continue"
|
|
109
|
+
)
|
|
110
|
+
if os.path.isdir(abs_path) and os.listdir(abs_path):
|
|
111
|
+
warn(f"{abs_path} is not empty")
|
|
112
|
+
if name is None:
|
|
113
|
+
name = re.sub(r"[-_,\.\ ]", "-", os.path.basename(abs_path).lower())
|
|
114
|
+
if " " in name:
|
|
115
|
+
warn("Invalid name; replacing spaces with hyphens")
|
|
116
|
+
name = name.replace(" ", "-")
|
|
117
|
+
typer.echo(f"Creating project {name}")
|
|
118
|
+
if title is None:
|
|
119
|
+
title = typer.prompt("Enter a title (ex: 'My research project')")
|
|
120
|
+
typer.echo(f"Using title: {title}")
|
|
121
|
+
if cloud:
|
|
122
|
+
# Cloud should allow None, which will allow us to post just the name
|
|
123
|
+
# NOTE: This will fail if the user hasn't logged into GitHub in a
|
|
124
|
+
# while, and their token stored in the Calkit cloud is expired
|
|
125
|
+
try:
|
|
126
|
+
resp = calkit.cloud.post(
|
|
127
|
+
"/projects",
|
|
128
|
+
json=dict(
|
|
129
|
+
name=name,
|
|
130
|
+
title=title,
|
|
131
|
+
description=description,
|
|
132
|
+
git_repo_url=git_repo_url,
|
|
133
|
+
is_public=public,
|
|
134
|
+
template=template,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise_error(f"Posting new project to cloud failed: {e}")
|
|
139
|
+
# Now clone here and that's about
|
|
140
|
+
subprocess.run(["git", "clone", resp["git_repo_url"], abs_path])
|
|
141
|
+
try:
|
|
142
|
+
calkit.dvc.set_remote_auth(wdir=abs_path)
|
|
143
|
+
except Exception:
|
|
144
|
+
warn("Failed to setup Calkit DVC remote auth")
|
|
145
|
+
prj = calkit.git.detect_project_name(path=abs_path)
|
|
146
|
+
add_msg = f"\n\nYou can view your project at https://calkit.io/{prj}"
|
|
147
|
+
typer.echo(success_message + add_msg)
|
|
148
|
+
return
|
|
149
|
+
# If using a template, clone it first
|
|
150
|
+
if template:
|
|
151
|
+
# TODO: If the template is not a Git repo URL, make a request to the
|
|
152
|
+
# Calkit Cloud to get it?
|
|
153
|
+
# For now, assume consistency between Calkit Cloud projects and
|
|
154
|
+
# GitHub repo URLs
|
|
155
|
+
if "github.com" in template:
|
|
156
|
+
template_git_url = template
|
|
157
|
+
template_name = template.split("github.com")[-1][1:].removesuffix(
|
|
158
|
+
".git"
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
template_name = template
|
|
162
|
+
template_git_url = f"https://github.com/{template}"
|
|
163
|
+
# Now clone it
|
|
164
|
+
subprocess.run(["git", "clone", template_git_url, abs_path])
|
|
165
|
+
# Templates should always have DVC initialized, so no need to do that
|
|
166
|
+
repo = git.Repo(abs_path)
|
|
167
|
+
git_rev = repo.git.rev_parse("HEAD")
|
|
168
|
+
# Rename origin remote as upstream
|
|
169
|
+
typer.echo("Renaming template remote as upstream")
|
|
170
|
+
repo.git.remote(["rename", "origin", "upstream"])
|
|
171
|
+
# Set git repo URL if provided
|
|
172
|
+
if git_repo_url:
|
|
173
|
+
typer.echo("Setting origin remote URL")
|
|
174
|
+
repo.git.remote(["add", "origin", git_repo_url])
|
|
175
|
+
# Update Calkit info in this project
|
|
176
|
+
ck_info = calkit.load_calkit_info(wdir=abs_path)
|
|
177
|
+
ck_info = ck_info | dict(
|
|
178
|
+
name=name,
|
|
179
|
+
title=title,
|
|
180
|
+
description=description,
|
|
181
|
+
git_repo_url=git_repo_url,
|
|
182
|
+
derived_from=dict(
|
|
183
|
+
project=template_name,
|
|
184
|
+
git_repo_url=template_git_url,
|
|
185
|
+
git_rev=git_rev,
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
# Remove questions and owner if they're there
|
|
189
|
+
_ = ck_info.pop("questions", None)
|
|
190
|
+
_ = ck_info.pop("owner", None)
|
|
191
|
+
# Write Calkit info
|
|
192
|
+
with open(os.path.join(abs_path, "calkit.yaml"), "w") as f:
|
|
193
|
+
ryaml.dump(ck_info, f)
|
|
194
|
+
# Update README
|
|
195
|
+
readme_fpath = os.path.join(abs_path, "README.md")
|
|
196
|
+
typer.echo("Generating README.md")
|
|
197
|
+
readme_txt = calkit.make_readme_content(
|
|
198
|
+
project_name=name,
|
|
199
|
+
project_title=title,
|
|
200
|
+
project_description=description,
|
|
201
|
+
)
|
|
202
|
+
with open(readme_fpath, "w") as f:
|
|
203
|
+
f.write(readme_txt)
|
|
204
|
+
# Update DVC remote
|
|
205
|
+
# TODO: This will fail because we don't know this user's account name
|
|
206
|
+
typer.echo("Updating Calkit DVC remote")
|
|
207
|
+
try:
|
|
208
|
+
calkit.dvc.configure_remote(wdir=abs_path)
|
|
209
|
+
except ValueError:
|
|
210
|
+
warn(
|
|
211
|
+
"Could not update Calkit DVC remote since "
|
|
212
|
+
"no Git repo URL was provided"
|
|
213
|
+
)
|
|
214
|
+
warn(
|
|
215
|
+
"You will need to manually run `git remote add origin` "
|
|
216
|
+
"and `calkit config remote`"
|
|
217
|
+
)
|
|
218
|
+
subprocess.call(
|
|
219
|
+
["dvc", "remote", "remove", "calkit", "-q"], cwd=abs_path
|
|
220
|
+
)
|
|
221
|
+
try:
|
|
222
|
+
calkit.dvc.set_remote_auth(wdir=abs_path)
|
|
223
|
+
except Exception:
|
|
224
|
+
warn("Could not set authentication for Calkit DVC remote")
|
|
225
|
+
# Commit this stuff to Git
|
|
226
|
+
repo.git.add(".")
|
|
227
|
+
if repo.git.diff("--staged"):
|
|
228
|
+
repo.git.commit(["-m", f"Create new project from {template_name}"])
|
|
229
|
+
typer.echo(success_message)
|
|
230
|
+
return
|
|
231
|
+
os.makedirs(abs_path, exist_ok=True)
|
|
232
|
+
try:
|
|
233
|
+
repo = git.Repo(abs_path)
|
|
234
|
+
except InvalidGitRepositoryError:
|
|
235
|
+
typer.echo("Initializing Git repository")
|
|
236
|
+
subprocess.run(["git", "init", "-q"], cwd=abs_path)
|
|
237
|
+
repo = git.Repo(abs_path)
|
|
238
|
+
if not os.path.isfile(os.path.join(abs_path, ".dvc", "config")):
|
|
239
|
+
typer.echo("Initializing DVC repository")
|
|
240
|
+
subprocess.run(["dvc", "init", "-q"], cwd=abs_path)
|
|
241
|
+
# Create calkit.yaml file
|
|
242
|
+
ck_info = calkit.load_calkit_info(wdir=abs_path)
|
|
243
|
+
ck_info = dict(name=name, title=title, description=description) | ck_info
|
|
244
|
+
with open(os.path.join(abs_path, "calkit.yaml"), "w") as f:
|
|
245
|
+
ryaml.dump(ck_info, f)
|
|
246
|
+
# Create dev container spec
|
|
247
|
+
update_devcontainer(wdir=abs_path)
|
|
248
|
+
# Create README
|
|
249
|
+
readme_fpath = os.path.join(abs_path, "README.md")
|
|
250
|
+
if os.path.isfile(readme_fpath) and not overwrite:
|
|
251
|
+
warn("README.md already exists; not modifying")
|
|
252
|
+
else:
|
|
253
|
+
typer.echo("Generating README.md")
|
|
254
|
+
readme_txt = calkit.make_readme_content(
|
|
255
|
+
project_name=name,
|
|
256
|
+
project_title=title,
|
|
257
|
+
project_description=description,
|
|
258
|
+
)
|
|
259
|
+
with open(readme_fpath, "w") as f:
|
|
260
|
+
f.write(readme_txt)
|
|
261
|
+
if git_repo_url and not repo.remotes:
|
|
262
|
+
typer.echo(f"Adding Git remote {git_repo_url}")
|
|
263
|
+
repo.git.remote(["add", "origin", git_repo_url])
|
|
264
|
+
elif not git_repo_url and not repo.remotes:
|
|
265
|
+
warn("No Git remotes are configured")
|
|
266
|
+
# Setup Calkit Cloud DVC remote
|
|
267
|
+
if repo.remotes:
|
|
268
|
+
typer.echo("Setting up Calkit Cloud DVC remote")
|
|
269
|
+
calkit.dvc.configure_remote(wdir=abs_path)
|
|
270
|
+
calkit.dvc.set_remote_auth(wdir=abs_path)
|
|
271
|
+
repo.git.add(".")
|
|
272
|
+
if repo.git.diff("--staged") and not no_commit:
|
|
273
|
+
repo.git.commit(["-m", "Initialize Calkit project"])
|
|
274
|
+
typer.echo(success_message)
|
|
275
|
+
|
|
276
|
+
|
|
21
277
|
@new_app.command(name="figure")
|
|
22
278
|
def new_figure(
|
|
23
279
|
path: str,
|
|
@@ -533,6 +789,7 @@ def new_publication(
|
|
|
533
789
|
str,
|
|
534
790
|
typer.Option(
|
|
535
791
|
"--template",
|
|
792
|
+
"-t",
|
|
536
793
|
help=(
|
|
537
794
|
"Template with which to create the source files. "
|
|
538
795
|
"Should be in the format {type}/{name}."
|
|
@@ -2,14 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import subprocess
|
|
6
7
|
|
|
8
|
+
from packaging.specifiers import SpecifierSet
|
|
9
|
+
from packaging.version import Version
|
|
7
10
|
from pydantic import BaseModel
|
|
8
11
|
|
|
9
12
|
import calkit
|
|
10
13
|
from calkit import ryaml
|
|
11
14
|
|
|
12
15
|
|
|
16
|
+
def _check_single(req: str, actual: str, conda: bool = False) -> bool:
|
|
17
|
+
"""Helper function for checking actual versions against requirements."""
|
|
18
|
+
req_name = re.split("[=<>]", req)[0]
|
|
19
|
+
req_spec = req.removeprefix(req_name)
|
|
20
|
+
if conda and req_spec.startswith("="):
|
|
21
|
+
req_spec = "=" + req_spec
|
|
22
|
+
if not req_spec.endswith(".*"):
|
|
23
|
+
req_spec += ".*"
|
|
24
|
+
actual_name, actual_vers = re.split("[=<>]+", actual, maxsplit=1)
|
|
25
|
+
if actual_name != req_name:
|
|
26
|
+
return False
|
|
27
|
+
actual_spec = actual.removeprefix(actual_name)
|
|
28
|
+
if conda and actual_spec.startswith("="):
|
|
29
|
+
actual_spec = "=" + actual_spec
|
|
30
|
+
version = Version(actual_vers)
|
|
31
|
+
spec = SpecifierSet(req_spec)
|
|
32
|
+
return spec.contains(version)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _check_list(req: str, actual: list[str], conda: bool = False) -> bool:
|
|
36
|
+
for installed in actual:
|
|
37
|
+
if _check_single(req, installed, conda=conda):
|
|
38
|
+
return True
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
13
42
|
class EnvCheckResult(BaseModel):
|
|
14
43
|
env_exists: bool | None = None
|
|
15
44
|
env_needs_export: bool | None = None
|
|
@@ -127,45 +156,23 @@ def check_env(
|
|
|
127
156
|
required_conda_deps.append(dep.replace("==", "="))
|
|
128
157
|
log_func("Checking conda dependencies")
|
|
129
158
|
for dep in required_conda_deps:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
else:
|
|
135
|
-
version = None
|
|
136
|
-
if version is not None and dep not in existing_conda_deps:
|
|
159
|
+
is_okay = _check_list(
|
|
160
|
+
req=dep, actual=existing_conda_deps, conda=True
|
|
161
|
+
)
|
|
162
|
+
if not is_okay:
|
|
137
163
|
log_func(f"Found missing dependency: {dep}")
|
|
138
164
|
env_needs_rebuild = True
|
|
139
165
|
break
|
|
140
|
-
elif version is None:
|
|
141
|
-
# TODO: This does not handle specification of only major or
|
|
142
|
-
# major+minor version
|
|
143
|
-
if package not in [
|
|
144
|
-
d.split("=")[0] for d in existing_conda_deps
|
|
145
|
-
]:
|
|
146
|
-
log_func(f"Found missing dependency: {dep}")
|
|
147
|
-
env_needs_rebuild = True
|
|
148
|
-
break
|
|
149
166
|
if not env_needs_rebuild and not relaxed:
|
|
150
167
|
log_func("Checking pip dependencies")
|
|
151
168
|
for dep in required_pip_deps:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
else:
|
|
157
|
-
version = None
|
|
158
|
-
if version is not None and dep not in existing_pip_deps:
|
|
169
|
+
is_okay = _check_list(
|
|
170
|
+
req=dep, actual=existing_pip_deps, conda=False
|
|
171
|
+
)
|
|
172
|
+
if not is_okay:
|
|
159
173
|
env_needs_rebuild = True
|
|
160
174
|
log_func(f"Found missing dependency: {dep}")
|
|
161
175
|
break
|
|
162
|
-
elif version is None:
|
|
163
|
-
if package not in [
|
|
164
|
-
d.split("==")[0] for d in existing_pip_deps
|
|
165
|
-
]:
|
|
166
|
-
log_func(f"Found missing dependency: {dep}")
|
|
167
|
-
env_needs_rebuild = True
|
|
168
|
-
break
|
|
169
176
|
if env_needs_rebuild:
|
|
170
177
|
res.env_needs_rebuild = True
|
|
171
178
|
log_func(f"Rebuilding {env_name} since it does not match spec")
|
|
@@ -209,3 +209,13 @@ def save_notebook_stage_out(
|
|
|
209
209
|
obj.write_parquet(fpath)
|
|
210
210
|
else:
|
|
211
211
|
raise ValueError(f"Unsupported format '{fmt}' for engine '{engine}'")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def make_readme_content(
|
|
215
|
+
project_name: str, project_title: str, project_description: str | None
|
|
216
|
+
) -> str:
|
|
217
|
+
"""Create Markdown content for a Calkit project README."""
|
|
218
|
+
txt = f"# {project_title}\n\n"
|
|
219
|
+
if project_description is not None:
|
|
220
|
+
txt += f"\n{project_description}\n"
|
|
221
|
+
return txt
|
|
@@ -13,19 +13,23 @@ logger = logging.getLogger(__package__)
|
|
|
13
13
|
logger.setLevel(logging.INFO)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def configure_remote():
|
|
17
|
-
project_name = calkit.git.detect_project_name()
|
|
16
|
+
def configure_remote(wdir: str = None):
|
|
17
|
+
project_name = calkit.git.detect_project_name(path=wdir)
|
|
18
18
|
base_url = calkit.cloud.get_base_url()
|
|
19
19
|
remote_url = f"{base_url}/projects/{project_name}/dvc"
|
|
20
20
|
subprocess.check_call(
|
|
21
|
-
["dvc", "remote", "add", "-d", "-f", get_app_name(), remote_url]
|
|
21
|
+
["dvc", "remote", "add", "-d", "-f", get_app_name(), remote_url],
|
|
22
|
+
cwd=wdir,
|
|
22
23
|
)
|
|
23
24
|
subprocess.check_call(
|
|
24
|
-
["dvc", "remote", "modify", get_app_name(), "auth", "custom"]
|
|
25
|
+
["dvc", "remote", "modify", get_app_name(), "auth", "custom"],
|
|
26
|
+
cwd=wdir,
|
|
25
27
|
)
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
def set_remote_auth(
|
|
30
|
+
def set_remote_auth(
|
|
31
|
+
remote_name: str = None, always_auth: bool = False, wdir: str = None
|
|
32
|
+
):
|
|
29
33
|
"""Get a token and set it in the local DVC config so we can interact with
|
|
30
34
|
the cloud as an HTTP remote.
|
|
31
35
|
"""
|
|
@@ -48,7 +52,8 @@ def set_remote_auth(remote_name: str = None, always_auth: bool = False):
|
|
|
48
52
|
remote_name,
|
|
49
53
|
"custom_auth_header",
|
|
50
54
|
"Authorization",
|
|
51
|
-
]
|
|
55
|
+
],
|
|
56
|
+
cwd=wdir,
|
|
52
57
|
)
|
|
53
58
|
subprocess.check_call(
|
|
54
59
|
[
|
|
@@ -59,7 +64,8 @@ def set_remote_auth(remote_name: str = None, always_auth: bool = False):
|
|
|
59
64
|
remote_name,
|
|
60
65
|
"password",
|
|
61
66
|
f"Bearer {settings.dvc_token}",
|
|
62
|
-
]
|
|
67
|
+
],
|
|
68
|
+
cwd=wdir,
|
|
63
69
|
)
|
|
64
70
|
|
|
65
71
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Git-related functionality."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import git
|
|
6
|
+
|
|
7
|
+
import calkit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def detect_project_name(path: str = None) -> str:
|
|
11
|
+
"""Read the project owner and name from the remote."""
|
|
12
|
+
ck_info = calkit.load_calkit_info(wdir=path)
|
|
13
|
+
name = ck_info.get("name")
|
|
14
|
+
owner = ck_info.get("owner")
|
|
15
|
+
url = git.Repo(path=path).remote().url
|
|
16
|
+
from_url = url.split("github.com")[-1][1:].removesuffix(".git")
|
|
17
|
+
owner_name, project_name = from_url.split("/")
|
|
18
|
+
if name is None:
|
|
19
|
+
name = project_name
|
|
20
|
+
if owner is None:
|
|
21
|
+
owner = owner_name
|
|
22
|
+
return f"{owner}/{name}"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Tests for ``calkit.calc``."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import calkit
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_formula():
|
|
9
|
+
calc = calkit.calc.Formula(
|
|
10
|
+
params=dict(formula="0.2151 * x + y**2"),
|
|
11
|
+
inputs=["x", "y"],
|
|
12
|
+
output=calkit.calc.Output(
|
|
13
|
+
name="z", description="The value", template="The value is {z:.1f}."
|
|
14
|
+
),
|
|
15
|
+
)
|
|
16
|
+
assert calc.evaluate(x=10.2, y=0.1) == 2.20402
|
|
17
|
+
res = calkit.calc.evaluate_and_format(calc, x=5, y=1)
|
|
18
|
+
assert res == "The value is 2.1."
|
|
19
|
+
with pytest.raises(ValueError):
|
|
20
|
+
calc.evaluate(x=5)
|
|
21
|
+
with pytest.raises(ValueError):
|
|
22
|
+
calc.evaluate(z=5)
|
|
23
|
+
with pytest.raises(ValueError):
|
|
24
|
+
calc = calkit.calc.Formula(
|
|
25
|
+
params=dict(formula="0.2151 * x + y**2"),
|
|
26
|
+
inputs=["x", "x"],
|
|
27
|
+
output=calkit.calc.Output(
|
|
28
|
+
name="z",
|
|
29
|
+
description="The value",
|
|
30
|
+
template="The value is {z:.1f}.",
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
with pytest.raises(ValueError):
|
|
34
|
+
calc = calkit.calc.Formula(
|
|
35
|
+
params=dict(formula="0.2151 * x + y**2"),
|
|
36
|
+
inputs=["x", "y"],
|
|
37
|
+
output=calkit.calc.Output(
|
|
38
|
+
name="x",
|
|
39
|
+
description="The value",
|
|
40
|
+
template="The value is {z:.1f}.",
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_lookuptable():
|
|
46
|
+
calc = calkit.calc.LookupTable(
|
|
47
|
+
inputs=["x"],
|
|
48
|
+
output="something",
|
|
49
|
+
params=calkit.calc.LookupTableParams(
|
|
50
|
+
x_values=[1, 2, 3], y_values=[4, 5, 6]
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
# TODO: Implement this
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_linear():
|
|
57
|
+
calc = calkit.calc.Linear(
|
|
58
|
+
params=calkit.calc.LinearParams(
|
|
59
|
+
coeffs=dict(input_voltage=1.1), offset=0.01
|
|
60
|
+
),
|
|
61
|
+
inputs=[{"name": "input_voltage", "dtype": "float"}],
|
|
62
|
+
output="load_lbf",
|
|
63
|
+
)
|
|
64
|
+
res = calc.evaluate(input_voltage=1.534)
|
|
65
|
+
assert round(res, ndigits=3) == 1.697
|
|
66
|
+
res2 = calkit.calc.evaluate_and_format(calc, input_voltage=-0.5)
|
|
67
|
+
assert (
|
|
68
|
+
res2 == "For input input_voltage=-0.5, the output is load_lbf=-0.54."
|
|
69
|
+
)
|
|
@@ -5,7 +5,25 @@ import uuid
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
-
from calkit.conda import check_env
|
|
8
|
+
from calkit.conda import _check_list, _check_single, check_env
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_check_single():
|
|
12
|
+
assert _check_single("python=3.12", "python=3.12.18", conda=True)
|
|
13
|
+
assert _check_single("python=3", "python=3.12.18", conda=True)
|
|
14
|
+
assert _check_single("python=3.12.18", "python=3.12.18", conda=True)
|
|
15
|
+
assert _check_single("python>=3.12,<3.13", "python==3.12.18", conda=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_check_list():
|
|
19
|
+
installed = ["python=3.12.1", "numpy=1.0.11"]
|
|
20
|
+
assert _check_list("python=3", installed, conda=True)
|
|
21
|
+
assert _check_list("numpy", installed, conda=True)
|
|
22
|
+
assert not _check_list("pandas", installed, conda=True)
|
|
23
|
+
installed = ["python==3.12.1", "numpy==1.0.11"]
|
|
24
|
+
assert _check_list("python>=3", installed, conda=False)
|
|
25
|
+
assert _check_list("numpy", installed, conda=False)
|
|
26
|
+
assert not _check_list("pandas", installed, conda=False)
|
|
9
27
|
|
|
10
28
|
|
|
11
29
|
def delete_env(name: str):
|
|
@@ -124,3 +142,22 @@ def test_check_env(tmp_dir, env_name):
|
|
|
124
142
|
)
|
|
125
143
|
res = check_env(relaxed=True)
|
|
126
144
|
assert not res.env_needs_rebuild
|
|
145
|
+
# Make sure we can handle other ways of specifying versions
|
|
146
|
+
subprocess.check_call(
|
|
147
|
+
[
|
|
148
|
+
"calkit",
|
|
149
|
+
"new",
|
|
150
|
+
"conda-env",
|
|
151
|
+
"--overwrite",
|
|
152
|
+
"-n",
|
|
153
|
+
env_name,
|
|
154
|
+
"python=3.12",
|
|
155
|
+
"--pip",
|
|
156
|
+
"numpy>=1",
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
res = check_env()
|
|
160
|
+
assert res.env_needs_rebuild
|
|
161
|
+
res = check_env()
|
|
162
|
+
assert not res.env_needs_export
|
|
163
|
+
assert not res.env_needs_rebuild
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"""Git-related functionality."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import git
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def detect_project_name(path=None) -> str:
|
|
9
|
-
"""Read the project owner and name from the remote.
|
|
10
|
-
|
|
11
|
-
TODO: Currently only works with GitHub remotes where the GitHub repo
|
|
12
|
-
name is identical to the Calkit project name, which is not guaranteed.
|
|
13
|
-
We should probably look inside ``calkit.yaml`` at ``name``
|
|
14
|
-
first, and fallback to the GitHub remote URL if we can't find that.
|
|
15
|
-
"""
|
|
16
|
-
url = git.Repo(path=path).remote().url
|
|
17
|
-
return url.split("github.com")[-1][1:].removesuffix(".git")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|