calkit-python 0.1.1__tar.gz → 0.2.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.
Files changed (43) hide show
  1. {calkit_python-0.1.1 → calkit_python-0.2.1}/PKG-INFO +1 -1
  2. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/__init__.py +1 -1
  3. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/core.py +5 -0
  4. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/main.py +87 -35
  5. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/new.py +14 -26
  6. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/docker.py +5 -3
  7. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/dvc.py +15 -3
  8. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/models.py +17 -0
  9. calkit_python-0.2.1/calkit/tests/test_dvc.py +15 -0
  10. {calkit_python-0.1.1 → calkit_python-0.2.1}/.github/FUNDING.yml +0 -0
  11. {calkit_python-0.1.1 → calkit_python-0.2.1}/.github/workflows/publish-test.yml +0 -0
  12. {calkit_python-0.1.1 → calkit_python-0.2.1}/.github/workflows/publish.yml +0 -0
  13. {calkit_python-0.1.1 → calkit_python-0.2.1}/.gitignore +0 -0
  14. {calkit_python-0.1.1 → calkit_python-0.2.1}/LICENSE +0 -0
  15. {calkit_python-0.1.1 → calkit_python-0.2.1}/README.md +0 -0
  16. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/__init__.py +0 -0
  17. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/config.py +0 -0
  18. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/import_.py +0 -0
  19. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/list.py +0 -0
  20. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cli/notebooks.py +0 -0
  21. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/cloud.py +0 -0
  22. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/config.py +0 -0
  23. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/core.py +0 -0
  24. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/data.py +0 -0
  25. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/git.py +0 -0
  26. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/gui.py +0 -0
  27. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/jupyter.py +0 -0
  28. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/server.py +0 -0
  29. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/tests/__init__.py +0 -0
  30. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/tests/cli/__init__.py +0 -0
  31. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/tests/cli/test_list.py +0 -0
  32. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/tests/cli/test_main.py +0 -0
  33. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/tests/cli/test_new.py +0 -0
  34. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/tests/test_core.py +0 -0
  35. {calkit_python-0.1.1 → calkit_python-0.2.1}/calkit/tests/test_jupyter.py +0 -0
  36. {calkit_python-0.1.1 → calkit_python-0.2.1}/examples/cfd-study/README.md +0 -0
  37. {calkit_python-0.1.1 → calkit_python-0.2.1}/examples/cfd-study/calkit.yaml +0 -0
  38. {calkit_python-0.1.1 → calkit_python-0.2.1}/examples/cfd-study/config/simulations/runs.csv +0 -0
  39. {calkit_python-0.1.1 → calkit_python-0.2.1}/examples/cfd-study/notebook.ipynb +0 -0
  40. {calkit_python-0.1.1 → calkit_python-0.2.1}/examples/ms-office/.gitignore +0 -0
  41. {calkit_python-0.1.1 → calkit_python-0.2.1}/examples/ms-office/README.md +0 -0
  42. {calkit_python-0.1.1 → calkit_python-0.2.1}/examples/ms-office/calkit.yaml +0 -0
  43. {calkit_python-0.1.1 → calkit_python-0.2.1}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: calkit-python
3
- Version: 0.1.1
3
+ Version: 0.2.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
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.1"
1
+ __version__ = "0.2.1"
2
2
 
3
3
  from .core import *
4
4
  from . import git
@@ -21,3 +21,8 @@ def run_cmd(cmd: list[str]):
21
21
  import pty
22
22
 
23
23
  pty.spawn(cmd, lambda fd: os.read(fd, 1024))
24
+
25
+
26
+ def raise_error(txt):
27
+ typer.echo(txt, err=txt)
28
+ raise typer.Exit(1)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import hashlib
5
6
  import json
6
7
  import os
7
8
  import subprocess
@@ -12,7 +13,7 @@ import typer
12
13
  from typing_extensions import Annotated, Optional
13
14
 
14
15
  import calkit
15
- from calkit.cli import print_sep, run_cmd
16
+ from calkit.cli import print_sep, raise_error, run_cmd
16
17
  from calkit.cli.config import config_app
17
18
  from calkit.cli.import_ import import_app
18
19
  from calkit.cli.list import list_app
@@ -26,8 +27,11 @@ app = typer.Typer(
26
27
  pretty_exceptions_show_locals=False,
27
28
  )
28
29
  app.add_typer(config_app, name="config", help="Configure Calkit.")
30
+ app.add_typer(new_app, name="new", help="Create a new Calkit object.")
29
31
  app.add_typer(
30
- new_app, name="new", help="Add new Calkit object (to calkit.yaml)."
32
+ new_app,
33
+ name="create",
34
+ help="Create a new Calkit object (alias for 'new').",
31
35
  )
32
36
  app.add_typer(notebooks_app, name="nb", help="Work with Jupyter notebooks.")
33
37
  app.add_typer(list_app, name="list", help="List Calkit objects.")
@@ -46,6 +50,53 @@ def main(
46
50
  raise typer.Exit()
47
51
 
48
52
 
53
+ @app.command(name="clone")
54
+ def clone(
55
+ url: Annotated[str, typer.Argument(help="Repo URL.")],
56
+ location: Annotated[
57
+ str,
58
+ typer.Argument(
59
+ help="Location to clone to (default will be ./{repo_name})"
60
+ ),
61
+ ] = None,
62
+ no_config_remote: Annotated[
63
+ bool,
64
+ typer.Option(
65
+ "--no-config-remote",
66
+ help="Do not automatically configure Calkit DVC remote.",
67
+ ),
68
+ ] = False,
69
+ no_dvc_pull: Annotated[
70
+ bool, typer.Option("--no-dvc-pull", help="Do not pull DVC objects.")
71
+ ] = False,
72
+ ):
73
+ """Clone a Git repo and by default configure and pull from the DVC
74
+ remote.
75
+ """
76
+ # Git clone
77
+ cmd = ["git", "clone", url]
78
+ if location is not None:
79
+ cmd.append(location)
80
+ try:
81
+ subprocess.call(cmd)
82
+ except Exception as e:
83
+ raise_error(str(e))
84
+ if location is None:
85
+ location = url.split("/")[-1].removesuffix(".git")
86
+ typer.echo(f"Moving into repo dir: {location}")
87
+ os.chdir(location)
88
+ # Setup auth for any Calkit remotes
89
+ if not no_config_remote:
90
+ remotes = calkit.dvc.get_remotes()
91
+ for name, url in remotes.items():
92
+ if name == "calkit" or name.startswith("calkit:"):
93
+ typer.echo(f"Setting up authentication for DVC remote: {name}")
94
+ calkit.dvc.set_remote_auth(remote_name=name)
95
+ # DVC pull
96
+ if not no_dvc_pull:
97
+ subprocess.call(["dvc", "pull"])
98
+
99
+
49
100
  @app.command(name="status")
50
101
  def get_status():
51
102
  """Get a unified Git and DVC status."""
@@ -97,8 +148,7 @@ def add(
97
148
  adding any .dvc files to Git when adding to DVC.
98
149
  """
99
150
  if to is not None and to not in ["git", "dvc"]:
100
- typer.echo(f"Invalid option for 'to': {to}")
101
- raise typer.Exit(1)
151
+ raise_error(f"Invalid option for 'to': {to}")
102
152
  # Ensure autostage is enabled for DVC
103
153
  subprocess.call(["dvc", "config", "core.autostage", "true"])
104
154
  subprocess.call(["git", "add", ".dvc/config"])
@@ -117,13 +167,11 @@ def add(
117
167
  ]
118
168
  dvc_size_thresh_bytes = 1_000_000
119
169
  if "." in paths and to is None:
120
- typer.echo("Cannot add '.' with calkit; use git or dvc")
121
- raise typer.Exit(1)
170
+ raise_error("Cannot add '.' with calkit; use git or dvc")
122
171
  if to is None:
123
172
  for path in paths:
124
173
  if os.path.isdir(path):
125
- typer.echo("Cannot auto-add directories; use git or dvc")
126
- raise typer.Exit(1)
174
+ raise_error("Cannot auto-add directories; use git or dvc")
127
175
  repo = git.Repo()
128
176
  for path in paths:
129
177
  # Detect if this file should be tracked with Git or DVC
@@ -323,8 +371,7 @@ def run_dvc_repro(
323
371
  subprocess.call(["dvc", "repro"] + args)
324
372
  # Now parse stage metadata for calkit objects
325
373
  if not os.path.isfile("dvc.yaml"):
326
- typer.echo("No dvc.yaml file found")
327
- raise typer.Exit(1)
374
+ raise_error("No dvc.yaml file found")
328
375
  objects = []
329
376
  with open("dvc.yaml") as f:
330
377
  pipeline = calkit.ryaml.load(f)
@@ -332,21 +379,18 @@ def run_dvc_repro(
332
379
  ckmeta = stage_info.get("meta", {}).get("calkit")
333
380
  if ckmeta is not None:
334
381
  if not isinstance(ckmeta, dict):
335
- typer.echo(
382
+ raise_error(
336
383
  f"Calkit metadata for {stage_name} is not a dictionary"
337
384
  )
338
- typer.Exit(1)
339
385
  # Stage must have a single output
340
386
  outs = stage_info.get("outs", [])
341
387
  if len(outs) != 1:
342
- typer.echo(
388
+ raise_error(
343
389
  f"Stage {stage_name} does not have exactly one output"
344
390
  )
345
- raise typer.Exit(1)
346
391
  cktype = ckmeta.get("type")
347
392
  if cktype not in ["figure", "dataset", "publication"]:
348
- typer.echo(f"Invalid Calkit output type '{cktype}'")
349
- raise typer.Exit(1)
393
+ raise_error(f"Invalid Calkit output type '{cktype}'")
350
394
  objects.append(
351
395
  dict(path=outs[0]) | ckmeta | dict(stage=stage_name)
352
396
  )
@@ -433,21 +477,24 @@ def run_in_env(
433
477
  ck_info = calkit.load_calkit_info()
434
478
  envs = ck_info.get("environments", {})
435
479
  if not envs:
436
- typer.echo("No environments defined in calkit.yaml", err=True)
437
- raise typer.Exit(1)
480
+ raise_error("No environments defined in calkit.yaml")
438
481
  if isinstance(envs, list):
439
- typer.echo(
440
- "Error: Environments should be a dict, not a list", err=True
441
- )
442
- raise typer.Exit(1)
443
- if len(envs) > 1 and env_name is None:
444
- typer.echo(
445
- "Environment must be specified if there are multiple",
446
- err=True,
447
- )
448
- raise typer.Exit(1)
482
+ raise_error("Error: Environments should be a dict, not a list")
483
+ if env_name is None:
484
+ # See if there's a default env, or only one env defined
485
+ default_env_name = None
486
+ for n, e in envs.items():
487
+ if e.get("default"):
488
+ if default_env_name is not None:
489
+ raise_error(
490
+ "Only one default environment can be specified"
491
+ )
492
+ default_env_name = n
493
+ if default_env_name is None and len(envs) == 1:
494
+ default_env_name = list(envs.keys())[0]
495
+ env_name = default_env_name
449
496
  if env_name is None:
450
- env_name = list(envs.keys())[0]
497
+ raise_error("Environment must be specified if there are multiple")
451
498
  env = envs[env_name]
452
499
  cwd = os.getcwd()
453
500
  image_name = env.get("image", env_name)
@@ -477,8 +524,7 @@ def run_in_env(
477
524
  typer.echo(f"Running command: {cmd}")
478
525
  subprocess.call(cmd)
479
526
  else:
480
- typer.echo("Environment kind not supported", err=True)
481
- raise typer.Exit(1)
527
+ raise_error("Environment kind not supported")
482
528
 
483
529
 
484
530
  @app.command(
@@ -507,8 +553,7 @@ def check_call(
507
553
  subprocess.check_call(if_error, shell=True)
508
554
  typer.echo("Fallback call succeeded")
509
555
  except subprocess.CalledProcessError:
510
- typer.echo("Fallback call failed", err=True)
511
- raise typer.Exit(1)
556
+ raise_error("Fallback call failed")
512
557
 
513
558
 
514
559
  @app.command(
@@ -539,6 +584,10 @@ def build_docker(
539
584
  except subprocess.CalledProcessError:
540
585
  typer.echo(f"No image with tag {tag} found locally")
541
586
  inspect = []
587
+ typer.echo(f"Reading Dockerfile from {fpath}")
588
+ with open(fpath) as f:
589
+ dockerfile = f.read()
590
+ dockerfile_md5 = hashlib.md5(dockerfile.encode()).hexdigest()
542
591
  lock_fpath = fpath + "-lock.json"
543
592
  rebuild = True
544
593
  if os.path.isfile(lock_fpath):
@@ -549,11 +598,14 @@ def build_docker(
549
598
  typer.echo(f"Lock file ({lock_fpath}) does not exist")
550
599
  lock = None
551
600
  if inspect and lock:
552
- typer.echo("Checking image against lock file")
553
- rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"]["Layers"]
601
+ typer.echo("Checking image and Dockerfile against lock file")
602
+ rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"][
603
+ "Layers"
604
+ ] or dockerfile_md5 != lock[0].get("DockerfileMD5")
554
605
  if rebuild:
555
606
  subprocess.check_call(["docker", "build", "-t", tag, "-f", fpath, "."])
556
607
  # Write the lock file
557
608
  inspect = get_docker_inspect()
609
+ inspect[0]["DockerfileMD5"] = dockerfile_md5
558
610
  with open(lock_fpath, "w") as f:
559
611
  json.dump(inspect, f, indent=4)
@@ -10,17 +10,13 @@ import typer
10
10
  from typing_extensions import Annotated
11
11
 
12
12
  import calkit
13
+ from calkit.cli import raise_error
13
14
  from calkit.core import ryaml
14
15
  from calkit.docker import LAYERS
15
16
 
16
17
  new_app = typer.Typer(no_args_is_help=True)
17
18
 
18
19
 
19
- def _error(txt):
20
- typer.echo(txt, err=txt)
21
- raise typer.Exit(1)
22
-
23
-
24
20
  @new_app.command(name="figure")
25
21
  def new_figure(
26
22
  path: str,
@@ -69,18 +65,18 @@ def new_figure(
69
65
  ),
70
66
  ] = False,
71
67
  ):
72
- """Add a new figure."""
68
+ """Create a new figure."""
73
69
  ck_info = calkit.load_calkit_info()
74
70
  figures = ck_info.get("figures", [])
75
71
  paths = [f.get("path") for f in figures]
76
72
  if not overwrite and path in paths:
77
- _error(f"Figure at path {path} already exists")
73
+ raise_error(f"Figure at path {path} already exists")
78
74
  if cmd is not None and stage_name is None:
79
- _error("Stage name must be provided if command is specified")
75
+ raise_error("Stage name must be provided if command is specified")
80
76
  if (deps or outs or outs_from_stage) and not cmd:
81
- _error("Command must be provided")
77
+ raise_error("Command must be provided")
82
78
  if (deps or outs or outs_from_stage) and not stage_name:
83
- _error("Stage name must be provided")
79
+ raise_error("Stage name must be provided")
84
80
  obj = dict(path=path, title=title)
85
81
  if description is not None:
86
82
  obj["description"] = description
@@ -91,7 +87,7 @@ def new_figure(
91
87
  pipeline = calkit.dvc.read_pipeline()
92
88
  stages = pipeline.get("stages", {})
93
89
  if outs_from_stage not in stages:
94
- _error(f"Stage {outs_from_stage} does not exist")
90
+ raise_error(f"Stage {outs_from_stage} does not exist")
95
91
  deps += stages[outs_from_stage].get("outs", [])
96
92
  if path not in outs:
97
93
  outs.append(path)
@@ -232,15 +228,9 @@ def new_docker_env(
232
228
  ):
233
229
  """Create a new Docker environment."""
234
230
  if base and os.path.isfile(path) and not overwrite:
235
- typer.echo(
236
- "Output path already exists (use -f to overwrite)", err=True
237
- )
238
- raise typer.Exit(1)
231
+ raise_error("Output path already exists (use -f to overwrite)")
239
232
  if stage and not base:
240
- typer.echo(
241
- "--from must be specified when creating a build stage", err=True
242
- )
243
- raise typer.Exit(1)
233
+ raise_error("--from must be specified when creating a build stage")
244
234
  if image_name is None:
245
235
  typer.echo("No image name specified; using environment name")
246
236
  image_name = name
@@ -249,8 +239,7 @@ def new_docker_env(
249
239
  txt = "FROM " + base + "\n\n"
250
240
  for layer in layers:
251
241
  if layer not in LAYERS:
252
- typer.echo(f"Unknown layer type '{layer}'")
253
- raise typer.Exit(1)
242
+ raise_error(f"Unknown layer type '{layer}'")
254
243
  txt += LAYERS[layer] + "\n\n"
255
244
  txt += f"RUN mkdir {wdir}\n"
256
245
  txt += f"WORKDIR {wdir}\n"
@@ -264,11 +253,10 @@ def new_docker_env(
264
253
  typer.echo("Converting environments from list to dict")
265
254
  envs = {env.pop("name"): env for env in envs}
266
255
  if name in envs and not overwrite:
267
- typer.echo(
256
+ raise_error(
268
257
  f"Environment with name {name} already exists "
269
258
  "(use -f to overwrite)"
270
259
  )
271
- raise typer.Exit(1)
272
260
  if base:
273
261
  repo.git.add(path)
274
262
  typer.echo("Adding environment to calkit.yaml")
@@ -309,7 +297,8 @@ def new_docker_env(
309
297
  ]
310
298
  )
311
299
  repo.git.add("calkit.yaml")
312
- repo.git.add("dvc.yaml")
300
+ if stage:
301
+ repo.git.add("dvc.yaml")
313
302
  if not no_commit and repo.git.diff("--staged"):
314
303
  repo.git.commit(["-m", f"Add Docker environment {name}"])
315
304
 
@@ -348,8 +337,7 @@ def new_foreach_stage(
348
337
  """
349
338
  pipeline = calkit.dvc.read_pipeline()
350
339
  if name in pipeline and not overwrite:
351
- typer.echo("Stage already exists; use -f to overwrite", err=True)
352
- raise typer.Exit(1)
340
+ raise_error("Stage already exists; use -f to overwrite")
353
341
  if "stages" not in pipeline:
354
342
  pipeline["stages"] = {}
355
343
  pipeline["stages"][name] = dict(
@@ -1,6 +1,6 @@
1
1
  """Functionality for working with Docker."""
2
2
 
3
- MAMBAFORGE_LAYER_TXT = """
3
+ MAMBAFORGE_LAYER_TXT = r"""
4
4
  # Install Miniforge
5
5
  ARG MINIFORGE_NAME=Mambaforge
6
6
  ARG MINIFORGE_VERSION=24.3.0-0
@@ -40,8 +40,10 @@ RUN apt-get update > /dev/null && \
40
40
  echo ". ${CONDA_DIR}/etc/profile.d/conda.sh && conda activate base" >> ~/.bashrc
41
41
  """.strip()
42
42
 
43
- FOAMPY_LAYER_TEXT = """
44
- RUN pip install numpy pandas matplotlib && pip install scipy && pip install foampy
43
+ FOAMPY_LAYER_TEXT = r"""
44
+ RUN pip install --no-cache-dir numpy pandas matplotlib h5py \
45
+ && pip install --no-cache-dir scipy \
46
+ && pip install --no-cache-dir foampy
45
47
  """.strip()
46
48
 
47
49
  LAYERS = {
@@ -67,9 +67,7 @@ def add_external_remote(owner_name: str, project_name: str):
67
67
  base_url = calkit.cloud.get_base_url()
68
68
  remote_url = f"{base_url}/projects/{owner_name}/{project_name}/dvc"
69
69
  remote_name = f"{get_app_name()}:{owner_name}/{project_name}"
70
- subprocess.call(
71
- ["dvc", "remote", "add", "-f", remote_name, remote_url]
72
- )
70
+ subprocess.call(["dvc", "remote", "add", "-f", remote_name, remote_url])
73
71
  subprocess.call(["dvc", "remote", "modify", remote_name, "auth", "custom"])
74
72
  set_remote_auth(remote_name)
75
73
 
@@ -79,3 +77,17 @@ def read_pipeline() -> dict:
79
77
  return {}
80
78
  with open("dvc.yaml") as f:
81
79
  return calkit.ryaml.load(f)
80
+
81
+
82
+ def get_remotes() -> dict[str, str]:
83
+ """Get a dictionary of DVC remotes, keyed by name, with URL as the
84
+ value.
85
+ """
86
+ out = subprocess.check_output(["dvc", "remote", "list"]).decode().strip()
87
+ if not out:
88
+ return {}
89
+ resp = {}
90
+ for line in out.split("\n"):
91
+ name, url = line.split("\t")
92
+ resp[name] = url
93
+ return resp
@@ -66,6 +66,7 @@ class Environment(BaseModel):
66
66
  path: str | None = None
67
67
  description: str | None = None
68
68
  stage: str | None = None
69
+ default: bool | None = None
69
70
 
70
71
 
71
72
  class DockerEnvironment(Environment):
@@ -89,8 +90,24 @@ class Notebook(_CalkitObject):
89
90
  class ProjectInfo(BaseModel):
90
91
  """All of the project's information or metadata, written to the
91
92
  ``calkit.yaml`` file.
93
+
94
+ Attributes
95
+ ----------
96
+ parent : str
97
+ The project's parent project, if applicable. This should be set if
98
+ the project was created as a copy of another. This is similar to the
99
+ concept of forking, but unlike a fork, a child project's changes
100
+ are not meant to be merged back into the parent.
101
+ The format of this field should be something like
102
+ {owner_name}/{project_name}, e.g., 'someuser/some-project-name'.
103
+ Note that individual objects can be imported from other projects, but
104
+ that doesn't necessarily make them parent projects.
105
+ This is probably not that important of a distinction.
106
+ The real use case is being able to trace where things came from and
107
+ distinguish what has been newly created here.
92
108
  """
93
109
 
110
+ parent: str | None = None
94
111
  questions: list[str] = []
95
112
  datasets: list[Dataset] = []
96
113
  figures: list[Figure] = []
@@ -0,0 +1,15 @@
1
+ """Tests for the ``dvc`` module."""
2
+
3
+ import subprocess
4
+
5
+ import calkit
6
+
7
+
8
+ def test_get_remotes(tmp_dir):
9
+ subprocess.call(["git", "init"])
10
+ assert not calkit.dvc.get_remotes()
11
+ subprocess.call(["dvc", "init"])
12
+ assert not calkit.dvc.get_remotes()
13
+ subprocess.call(["dvc", "remote", "add", "something", "https://sup.com"])
14
+ resp = calkit.dvc.get_remotes()
15
+ assert resp == {"something": "https://sup.com"}
File without changes
File without changes
File without changes