calkit-python 0.48.1__py3-none-any.whl → 0.48.3__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.
Files changed (50) hide show
  1. calkit/calc.py +15 -9
  2. calkit/cli/check.py +92 -2
  3. calkit/cli/describe.py +4 -1
  4. calkit/cli/latex.py +477 -1
  5. calkit/cli/main/core.py +59 -17
  6. calkit/cli/update.py +233 -0
  7. calkit/docx.py +7 -1
  8. calkit/dvc/core.py +210 -0
  9. calkit/environments.py +6 -0
  10. calkit/git.py +83 -8
  11. calkit/latex.py +260 -10
  12. calkit/markdown.py +156 -5
  13. calkit/models/base.py +13 -0
  14. calkit/models/core.py +44 -44
  15. calkit/models/io.py +8 -6
  16. calkit/models/iteration.py +6 -4
  17. calkit/models/pipeline.py +114 -10
  18. calkit/pipeline.py +53 -4
  19. calkit/questions.py +61 -34
  20. calkit/resources/devcontainer/devcontainer.json +1 -1
  21. calkit/resources/vscode/settings.json +1 -1
  22. calkit/tests/cli/main/test_core.py +112 -1
  23. calkit/tests/cli/test_check.py +85 -0
  24. calkit/tests/cli/test_latex.py +241 -0
  25. calkit/tests/cli/test_update.py +170 -0
  26. calkit/tests/dvc/test_core.py +93 -0
  27. calkit/tests/test_docx.py +106 -9
  28. calkit/tests/test_environments.py +10 -0
  29. calkit/tests/test_git.py +29 -0
  30. calkit/tests/test_markdown.py +88 -0
  31. calkit/tests/test_pipeline.py +10 -0
  32. calkit/tests/test_questions.py +60 -0
  33. {calkit_python-0.48.1.dist-info → calkit_python-0.48.3.dist-info}/METADATA +3 -1
  34. {calkit_python-0.48.1.dist-info → calkit_python-0.48.3.dist-info}/RECORD +50 -49
  35. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
  36. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
  37. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
  38. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
  39. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
  40. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
  41. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
  42. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
  43. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
  44. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
  45. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
  46. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
  47. {calkit_python-0.48.1.data → calkit_python-0.48.3.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
  48. {calkit_python-0.48.1.dist-info → calkit_python-0.48.3.dist-info}/WHEEL +0 -0
  49. {calkit_python-0.48.1.dist-info → calkit_python-0.48.3.dist-info}/entry_points.txt +0 -0
  50. {calkit_python-0.48.1.dist-info → calkit_python-0.48.3.dist-info}/licenses/LICENSE +0 -0
calkit/calc.py CHANGED
@@ -4,14 +4,20 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Annotated, Literal
6
6
 
7
- from pydantic import BaseModel, Discriminator, model_validator
7
+ from pydantic import BaseModel, ConfigDict, Discriminator, model_validator
8
8
 
9
9
  DTYPES = {"int": int, "float": float, "str": str}
10
10
  DEFAULT_IN_TYPE = "float"
11
11
  DEFAULT_OUT_TYPE = "float"
12
12
 
13
13
 
14
- class Input(BaseModel):
14
+ class _Model(BaseModel):
15
+ # As calkit.models.base.Model, which importing here would import
16
+ # calkit.models, which imports this
17
+ model_config = ConfigDict(defer_build=True)
18
+
19
+
20
+ class Input(_Model):
15
21
  name: str
16
22
  description: str | None = None
17
23
  dtype: Literal["int", "float", "str"] = DEFAULT_IN_TYPE
@@ -19,14 +25,14 @@ class Input(BaseModel):
19
25
  max: int | float | None = None
20
26
 
21
27
 
22
- class Output(BaseModel):
28
+ class Output(_Model):
23
29
  name: str
24
30
  description: str | None = None
25
31
  dtype: Literal["int", "float", "str"] = DEFAULT_OUT_TYPE
26
32
  template: str | None = None
27
33
 
28
34
 
29
- class Calculation(BaseModel):
35
+ class Calculation(_Model):
30
36
  kind: str
31
37
  params: dict = {}
32
38
  name: str | None = None
@@ -124,7 +130,7 @@ class Calculation(BaseModel):
124
130
  return template.format(**(inputs | {out_name: res}))
125
131
 
126
132
 
127
- class FormulaParams(BaseModel):
133
+ class FormulaParams(_Model):
128
134
  formula: str
129
135
 
130
136
 
@@ -139,7 +145,7 @@ class Formula(Calculation):
139
145
  return arithmetic_eval.evaluate(self.params.formula, inputs)
140
146
 
141
147
 
142
- class LinearParams(BaseModel):
148
+ class LinearParams(_Model):
143
149
  coeffs: dict[str, float]
144
150
  offset: float = 0.0
145
151
 
@@ -163,7 +169,7 @@ class Linear(Calculation):
163
169
  return val
164
170
 
165
171
 
166
- class LookupTableParams(BaseModel):
172
+ class LookupTableParams(_Model):
167
173
  x_values: list[float]
168
174
  y_values: list[float]
169
175
  method: Literal["floor", "ceil", "round", "interpolate"] = "interpolate"
@@ -176,7 +182,7 @@ class LookupTable(Calculation):
176
182
  params: LookupTableParams
177
183
 
178
184
 
179
- class HttpRequestParams(BaseModel):
185
+ class HttpRequestParams(_Model):
180
186
  url: str
181
187
  inputs_as_params: bool = True # Otherwise, use body
182
188
  method: Literal["get", "post", "put"] = "get"
@@ -210,7 +216,7 @@ class HttpRequest(Calculation):
210
216
  return resp.text
211
217
 
212
218
 
213
- class XGBoostModelParams(BaseModel):
219
+ class XGBoostModelParams(_Model):
214
220
  path: str
215
221
  type: Literal["classifier", "regressor"]
216
222
 
calkit/cli/check.py CHANGED
@@ -2026,6 +2026,95 @@ def check_pipeline(
2026
2026
  )
2027
2027
 
2028
2028
 
2029
+ @check_app.command(name="storage")
2030
+ def check_storage(
2031
+ fix: Annotated[
2032
+ bool,
2033
+ typer.Option(
2034
+ "--fix", help="Move each path to where it's declared to be stored."
2035
+ ),
2036
+ ] = False,
2037
+ as_json: Annotated[
2038
+ bool, typer.Option("--json", help="Output the problems as JSON.")
2039
+ ] = False,
2040
+ ) -> None:
2041
+ """Check that paths are tracked where they're declared to be stored.
2042
+
2043
+ That is, nothing is tracked by both Git and DVC, no pipeline output
2044
+ stored in Git is ignored by it, and nothing in Git is too large for it.
2045
+ With --fix, the pipeline's declarations, or a file's .dvc file, decide
2046
+ where each path belongs, and the changes are staged.
2047
+ """
2048
+ import calkit.dvc
2049
+ import calkit.git
2050
+ import calkit.pipeline
2051
+
2052
+ if fix and as_json:
2053
+ raise_error("--json can't be used with --fix")
2054
+ problems = calkit.dvc.get_storage_problems()
2055
+ if as_json:
2056
+ typer.echo(json.dumps(problems, indent=2))
2057
+ if not fix:
2058
+ if not as_json:
2059
+ for problem in problems:
2060
+ warn(problem["message"])
2061
+ if any(p["kind"] != "large" for p in problems):
2062
+ if not as_json:
2063
+ typer.echo("Run 'calkit check storage --fix' to fix these")
2064
+ raise typer.Exit(1)
2065
+ if not problems and not as_json:
2066
+ calkit.echo("✅ Every path is stored where it's declared to be")
2067
+ return
2068
+ repo = calkit.git.get_repo()
2069
+ gitignores = calkit.git.get_dirty_gitignores(repo)
2070
+ fixed = []
2071
+ unfixed = []
2072
+ # Pipeline outputs are recorded in DVC, which records their stages'
2073
+ # inputs too, so only for stages that are up to date
2074
+ to_record = []
2075
+ for problem in problems:
2076
+ path = os.path.abspath(problem["path"])
2077
+ if problem["store_in"] == "dvc":
2078
+ repo.git.rm("-r", "-q", "--cached", "--", path)
2079
+ calkit.git.ensure_path_is_ignored(repo, path)
2080
+ if problem["kind"] == "both":
2081
+ to_record.append(problem)
2082
+ elif problem["store_in"] == "git":
2083
+ calkit.git.ensure_path_is_not_ignored(repo, path)
2084
+ repo.git.add("--", path)
2085
+ else:
2086
+ unfixed.append(problem)
2087
+ continue
2088
+ fixed.append(problem)
2089
+ typer.echo(f"Fixed: {problem['message']}")
2090
+ # None when it can't be told which are stale, so none are recorded
2091
+ stale: set[str] | None = set()
2092
+ if any(p["stage"] for p in to_record):
2093
+ status = calkit.pipeline.get_status(
2094
+ check_environments=False, clean_notebooks=False
2095
+ )
2096
+ if not status.errors:
2097
+ stale = {n.split("@")[0] for n in status.stale_stage_names}
2098
+ else:
2099
+ stale = None
2100
+ for problem in to_record:
2101
+ stage = problem["stage"]
2102
+ if stage and (stale is None or stage.split("@")[0] in stale):
2103
+ typer.echo(f"Run '{stage}' to record {problem['path']} in DVC")
2104
+ continue
2105
+ target = problem["dvc_file"] or stage
2106
+ if calkit.dvc.run_dvc_command(["commit", "-f", "-q", target]) != 0:
2107
+ warn(f"Couldn't record {problem['path']} in DVC")
2108
+ for message in calkit.git.stage_gitignores(repo, gitignores):
2109
+ warn(message)
2110
+ for problem in unfixed:
2111
+ warn(problem["message"])
2112
+ if fixed:
2113
+ typer.echo("Commit the staged changes")
2114
+ if any(p["kind"] != "large" for p in unfixed):
2115
+ raise typer.Exit(1)
2116
+
2117
+
2029
2118
  @check_app.command(name="call")
2030
2119
  def check_call(
2031
2120
  cmd: Annotated[str, typer.Argument(help="Command to check.")],
@@ -2082,8 +2171,9 @@ def check_questions(
2082
2171
  pushed, or pinned to a Git ref that doesn't exist); broken references
2083
2172
  (a key that doesn't resolve, a placeholder that names no evidence, a
2084
2173
  label missing from the LaTeX); evidence the pipeline would rebuild;
2085
- and evidence from a frozen stage, or downstream of one, which nothing
2086
- will ever report out of date unless the citation pins a git_ref.
2174
+ and evidence from a frozen stage, or downstream of one, that no longer
2175
+ matches dvc.lock, which won't be rerun, unless the citation pins a
2176
+ git_ref. Subprojects' questions are checked too.
2087
2177
 
2088
2178
  Evidence pinned with a git_ref is checked at that ref rather than in
2089
2179
  the working tree. Exits with an error if any answered question is
calkit/cli/describe.py CHANGED
@@ -9,7 +9,6 @@ import typer
9
9
 
10
10
  import calkit
11
11
  from calkit.cli import AliasGroup, echo_json, raise_error
12
- from calkit.environments import get_env_lock_fpath
13
12
 
14
13
  describe_app = typer.Typer(cls=AliasGroup, no_args_is_help=True)
15
14
 
@@ -20,6 +19,10 @@ def _describe_env(env: dict, env_name: str) -> dict:
20
19
  The keys here are a stable contract for tools consuming ``--json``, so
21
20
  they are always present, even if their value is null.
22
21
  """
22
+ # Here rather than at the top, since every command imports this module;
23
+ # what that saves is a question in decisions/calkit.yaml
24
+ from calkit.environments import get_env_lock_fpath
25
+
23
26
  return {
24
27
  "kind": env.get("kind"),
25
28
  "spec_path": env.get("path"),