calkit-python 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
calkit/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ __version__ = "0.0.1"
2
+
3
+ from .core import *
4
+ from . import git
5
+ from . import dvc
6
+ from . import cloud
7
+ from . import jupyter
8
+ from . import config
calkit/cli.py ADDED
@@ -0,0 +1,117 @@
1
+ """The command line interface."""
2
+
3
+ import os
4
+ import pty
5
+ import subprocess
6
+ import sys
7
+
8
+ import typer
9
+ from typing_extensions import Annotated, Optional
10
+
11
+ import calkit
12
+
13
+ from . import config
14
+ from .dvc import configure_remote, set_remote_auth
15
+
16
+ app = typer.Typer()
17
+ config_app = typer.Typer()
18
+ app.add_typer(config_app, name="config", help="Configure Calkit.")
19
+
20
+
21
+ @config_app.command(name="set")
22
+ def set_config_value(key: str, value: str):
23
+ try:
24
+ cfg = config.read()
25
+ cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
26
+ # Kind of a hack for setting the password computed field
27
+ # Types have been validated above, so this won't hurt to do again
28
+ setattr(cfg, key, value)
29
+ except FileNotFoundError:
30
+ # TODO: This fails if we try to set password before any config has
31
+ # been written
32
+ # Username is fine
33
+ cfg = config.Settings.model_validate({key: value})
34
+ cfg.write()
35
+
36
+
37
+ @config_app.command(name="get")
38
+ def get_config_value(key: str) -> None:
39
+ cfg = config.read()
40
+ val = getattr(cfg, key)
41
+ if val is not None:
42
+ print(val)
43
+ else:
44
+ print()
45
+
46
+
47
+ @config_app.command(name="setup-remote")
48
+ def setup_remote():
49
+ configure_remote()
50
+ set_remote_auth()
51
+
52
+
53
+ @app.command(name="status")
54
+ def get_status():
55
+ """Get a unified Git and DVC status."""
56
+
57
+ def print_sep(name: str):
58
+ print(f"------------ {name} ------------")
59
+
60
+ print_sep("Code")
61
+ if os.name == "nt":
62
+ subprocess.call(["git", "status"])
63
+ print()
64
+ print_sep("data")
65
+ subprocess.call(["dvc", "status"])
66
+ else:
67
+ pty.spawn(["git", "status"], lambda fd: os.read(fd, 1024))
68
+ print()
69
+ print_sep("Data")
70
+ pty.spawn(["dvc", "status"], lambda fd: os.read(fd, 1024))
71
+
72
+
73
+ @app.command(name="add")
74
+ def add(paths: list[str]):
75
+ """Add paths to the repo.
76
+
77
+ Code will be added to Git and data will be added to DVC.
78
+ """
79
+ dvc_extensions = [".png", ".h5", ".parquet", ".pickle"]
80
+ dvc_size_thresh_bytes = 1_000_000
81
+ dvc_folders = ["data", "figures"]
82
+ if "." in paths:
83
+ print("ERROR: Cannot add '.' with calkit; use git or dvc")
84
+ sys.exit(1)
85
+ for path in paths:
86
+ if os.path.isdir(path):
87
+ print("ERROR: Cannot add directories with calkit; use git or dvc")
88
+ sys.exit(1)
89
+ # Detect if this file should be tracked with Git or DVC
90
+ # TODO: Add to whatever
91
+
92
+
93
+ @app.command(name="commit")
94
+ def commit(
95
+ all: Annotated[Optional[bool], typer.Option("--all", "-a")] = False,
96
+ message: Annotated[Optional[str], typer.Option("--message", "-m")] = None,
97
+ ):
98
+ """Commit a change to the repo."""
99
+ print(all, message)
100
+ raise NotImplementedError
101
+
102
+
103
+ @app.command(name="server")
104
+ def run_server():
105
+ import uvicorn
106
+
107
+ uvicorn.run(
108
+ "calkit.server:app",
109
+ port=8866,
110
+ host="localhost",
111
+ reload=True,
112
+ reload_dirs=[os.path.dirname(__file__)],
113
+ )
114
+
115
+
116
+ def run() -> None:
117
+ app()
calkit/cloud.py ADDED
@@ -0,0 +1,95 @@
1
+ """The REST API client."""
2
+
3
+ import os
4
+ from functools import partial
5
+ from typing import Literal
6
+
7
+ import requests
8
+
9
+ from . import config
10
+
11
+ # A dictionary of tokens keyed by base URL
12
+ _tokens = {}
13
+
14
+
15
+ def get_base_url() -> str:
16
+ """Get the API base URL."""
17
+ urls = {
18
+ "local": "http://api.localhost",
19
+ "staging": "https://api.staging.calkit.io",
20
+ "production": "https://api.calkit.io",
21
+ }
22
+ return urls[config.get_env()]
23
+
24
+
25
+ def get_token() -> str:
26
+ """Get a token.
27
+
28
+ Automatically reauthenticate if the token doesn't exist or has expired.
29
+ """
30
+ token = _tokens.get(get_base_url())
31
+ if token is None:
32
+ token = config.read().token
33
+ _tokens[get_base_url()] = token
34
+ # TODO: Check for expiration
35
+ if token is None:
36
+ return auth()
37
+ return token
38
+
39
+
40
+ def get_headers(headers: dict | None = None) -> dict:
41
+ base_headers = {"Authorization": f"Bearer {get_token()}"}
42
+ if headers is not None:
43
+ return base_headers | headers
44
+ else:
45
+ return base_headers
46
+
47
+
48
+ def auth() -> str:
49
+ """Authenticate with the server and save a token."""
50
+ cfg = config.read()
51
+ base_url = get_base_url()
52
+ resp = requests.post(
53
+ base_url + "/login/access-token",
54
+ data=dict(username=cfg.username, password=cfg.password),
55
+ )
56
+ token = resp.json()["access_token"]
57
+ _tokens[base_url] = token
58
+ return token
59
+
60
+
61
+ def _request(
62
+ kind: Literal["get", "post", "put", "patch", "delete"],
63
+ path: str,
64
+ params: dict | None = None,
65
+ json: dict | None = None,
66
+ data: dict | None = None,
67
+ headers: dict | None = None,
68
+ as_json=True,
69
+ **kwargs,
70
+ ):
71
+ func = getattr(requests, kind)
72
+ resp = func(
73
+ get_base_url() + path,
74
+ params=params,
75
+ json=json,
76
+ data=data,
77
+ headers=get_headers(headers),
78
+ **kwargs,
79
+ )
80
+ resp.raise_for_status()
81
+ if as_json:
82
+ return resp.json()
83
+ else:
84
+ return resp
85
+
86
+
87
+ get = partial(_request, "get")
88
+ post = partial(_request, "post")
89
+ patch = partial(_request, "patch")
90
+ put = partial(_request, "put")
91
+ delete = partial(_request, "delete")
92
+
93
+
94
+ def get_current_user() -> dict:
95
+ return get("/user")
calkit/config.py ADDED
@@ -0,0 +1,71 @@
1
+ """Configuration."""
2
+
3
+ import os
4
+ from typing import Literal
5
+
6
+ import keyring
7
+ import yaml
8
+ from pydantic import EmailStr, computed_field
9
+ from pydantic_settings import BaseSettings, SettingsConfigDict
10
+
11
+
12
+ def get_env() -> Literal["local", "staging", "production"]:
13
+ return os.getenv(f"{__package__.upper()}_ENV", "production")
14
+
15
+
16
+ def set_env(name: Literal["local", "staging", "production"]) -> None:
17
+ if name not in ["local", "staging", "production"]:
18
+ raise ValueError(f"{name} is not a valid environment name")
19
+ os.environ[f"{__package__.upper()}_ENV"] = name
20
+
21
+
22
+ def get_env_suffix() -> str:
23
+ if get_env() != "production":
24
+ return "-" + get_env()
25
+ return ""
26
+
27
+
28
+ def get_app_name() -> str:
29
+ return __package__ + get_env_suffix()
30
+
31
+
32
+ class Settings(BaseSettings):
33
+ model_config = SettingsConfigDict(
34
+ yaml_file=os.path.join(
35
+ os.path.expanduser("~"),
36
+ "." + __package__,
37
+ f"config{get_env_suffix()}.yaml",
38
+ ),
39
+ extra="ignore",
40
+ )
41
+ username: EmailStr | None = None
42
+ token: str | None = None
43
+ dvc_token: str | None = None
44
+ dataframe_engine: Literal["pandas", "polars"] = "pandas"
45
+
46
+ @computed_field
47
+ @property
48
+ def password(self) -> str:
49
+ return keyring.get_password(get_app_name(), self.username)
50
+
51
+ @password.setter
52
+ def password(self, value: str) -> None:
53
+ keyring.set_password(get_app_name(), self.username, value)
54
+
55
+ def write(self) -> None:
56
+ base_dir = os.path.dirname(self.model_config["yaml_file"])
57
+ os.makedirs(base_dir, exist_ok=True)
58
+ cfg = self.model_dump()
59
+ # Remove password
60
+ _ = cfg.pop("password")
61
+ with open(self.model_config["yaml_file"], "w") as f:
62
+ yaml.safe_dump(cfg, f)
63
+
64
+
65
+ def read() -> Settings:
66
+ """Read the config."""
67
+ fpath = Settings.model_config["yaml_file"]
68
+ if not os.path.isfile(fpath):
69
+ return Settings()
70
+ with open(fpath) as f:
71
+ return Settings.model_validate(yaml.safe_load(f))
calkit/core.py ADDED
@@ -0,0 +1,34 @@
1
+ """Core functionality."""
2
+
3
+ import glob
4
+ import os
5
+
6
+ from git import Repo
7
+ from git.exc import InvalidGitRepositoryError
8
+
9
+
10
+ def find_project_dirs(relative=False, max_depth=3) -> list[str]:
11
+ """Find all Calkit project directories."""
12
+ if relative:
13
+ start = ""
14
+ else:
15
+ start = os.path.expanduser("~")
16
+ res = []
17
+ for i in range(max_depth):
18
+ pattern = os.path.join(start, *["*"] * (i + 1), "calkit.yaml")
19
+ res += glob.glob(pattern)
20
+ # Check GitHub documents for users who use GitHub Desktop
21
+ pattern = os.path.join(
22
+ start, "*", "GitHub", *["*"] * (i + 1), "calkit.yaml"
23
+ )
24
+ res += glob.glob(pattern)
25
+ final_res = []
26
+ for ck_fpath in res:
27
+ path = os.path.dirname(ck_fpath)
28
+ # Make sure this path is a Git repo
29
+ try:
30
+ Repo(path)
31
+ except InvalidGitRepositoryError:
32
+ continue
33
+ final_res.append(path)
34
+ return final_res
calkit/data.py ADDED
@@ -0,0 +1,53 @@
1
+ """Functionality for working with datasets."""
2
+
3
+ from typing import Literal, Union
4
+
5
+ import pandas as pd
6
+ import polars as pl
7
+
8
+ import calkit.config
9
+
10
+ DEFAULT_ENGINE = calkit.config.read().dataframe_engine
11
+
12
+
13
+ def list_data():
14
+ """Read the Calkit metadata file and list out our datasets."""
15
+ pass
16
+
17
+
18
+ def read_data(
19
+ path: str, engine: Literal["pandas", "polars"] = DEFAULT_ENGINE
20
+ ) -> Union[pd.DataFrame, pl.DataFrame]:
21
+ """Read (tabular) data from dataset with path ``path`` and return a
22
+ DataFrame.
23
+
24
+ If the dataset doesn't exist locally, but is a DVC object, download it
25
+ first.
26
+
27
+ If the dataset path includes a user and project name, we add it to the
28
+ project as an imported dataset, and therefore DVC import it?
29
+
30
+ For example: someuser/someproject:data/somefile.parquet
31
+
32
+ We can run a DVC import command if it needs to be imported. We will need to
33
+ find the Git repo and path within it? Maybe we should require an explicit
34
+ import of the data.
35
+ """
36
+ pass
37
+
38
+
39
+ def write_data(
40
+ data: Union[pd.DataFrame, pl.DataFrame],
41
+ path: str,
42
+ filename: str | None = None,
43
+ commit=False,
44
+ ):
45
+ """Write ``data`` to the dataset with path ``path``.
46
+
47
+ If the dataset path is a directory, the filename must be specified.
48
+
49
+ If the path is not a Calkit dataset, it will be created.
50
+
51
+ If ``commit`` is specified, create a commit for the dataset update.
52
+ """
53
+ pass
calkit/dvc.py ADDED
@@ -0,0 +1,58 @@
1
+ """Working with DVC."""
2
+
3
+ import logging
4
+ import subprocess
5
+
6
+ import calkit
7
+ from calkit.config import get_app_name
8
+
9
+ logger = logging.getLogger(__package__)
10
+ logger.setLevel(logging.INFO)
11
+
12
+
13
+ def configure_remote():
14
+ project_name = calkit.git.detect_project_name()
15
+ base_url = calkit.cloud.get_base_url()
16
+ remote_url = f"{base_url}/projects/{project_name}/dvc"
17
+ subprocess.call(
18
+ ["dvc", "remote", "add", "-d", "-f", get_app_name(), remote_url]
19
+ )
20
+ subprocess.call(
21
+ ["dvc", "remote", "modify", get_app_name(), "auth", "custom"]
22
+ )
23
+
24
+
25
+ def set_remote_auth():
26
+ """Get a token and set it in the local DVC config so we can interact with
27
+ the cloud as an HTTP remote.
28
+ """
29
+ settings = calkit.config.read()
30
+ if settings.dvc_token is None:
31
+ logger.info("Creating token for DVC scope")
32
+ token = calkit.cloud.post(
33
+ "/user/tokens", json=dict(expires_days=365, scope="dvc")
34
+ )["access_token"]
35
+ settings.dvc_token = token
36
+ settings.write()
37
+ subprocess.call(
38
+ [
39
+ "dvc",
40
+ "remote",
41
+ "modify",
42
+ "--local",
43
+ get_app_name(),
44
+ "custom_auth_header",
45
+ "Authorization",
46
+ ]
47
+ )
48
+ subprocess.call(
49
+ [
50
+ "dvc",
51
+ "remote",
52
+ "modify",
53
+ "--local",
54
+ get_app_name(),
55
+ "password",
56
+ f"Bearer {settings.dvc_token}",
57
+ ]
58
+ )
calkit/git.py ADDED
@@ -0,0 +1,12 @@
1
+ """Git-related functionality."""
2
+
3
+ import git
4
+
5
+
6
+ def detect_project_name(path=None) -> str:
7
+ """Read the project owner and name from the remote.
8
+
9
+ TODO: Currently only works with GitHub remotes.
10
+ """
11
+ url = git.Repo(path=path).remote().url
12
+ return url.split("github.com")[-1][1:].removesuffix(".git")
calkit/gui.py ADDED
@@ -0,0 +1 @@
1
+ """An optional GUI."""
calkit/jupyter.py ADDED
@@ -0,0 +1,46 @@
1
+ """Functionality for working with Jupyter."""
2
+
3
+ import subprocess
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class Server(BaseModel):
9
+ url: str
10
+ wdir: str
11
+ token: str
12
+
13
+
14
+ def get_servers() -> list[Server]:
15
+ out = (
16
+ subprocess.check_output(["jupyter", "server", "list"])
17
+ .decode()
18
+ .strip()
19
+ .split("\n")
20
+ )
21
+ resp = []
22
+ for line in out:
23
+ if line.startswith("http://"):
24
+ url, wdir = line.split(" :: ")
25
+ token = url.split("?token=")[-1]
26
+ resp.append(Server(url=url, wdir=wdir, token=token))
27
+ return resp
28
+
29
+
30
+ def start_server(wdir=None, no_browser=False):
31
+ """Start a Jupyter server."""
32
+ subprocess.Popen(
33
+ [
34
+ "jupyter",
35
+ "lab",
36
+ "-y",
37
+ "--no-browser" if no_browser else "",
38
+ ],
39
+ cwd=wdir,
40
+ )
41
+
42
+
43
+ def stop_server(url: str):
44
+ # Extract the port from the URL
45
+ port = url.split(":")[2][:4]
46
+ subprocess.call(["jupyter", "server", "stop", port])
calkit/server.py ADDED
@@ -0,0 +1,201 @@
1
+ """A local server for interacting with project repos."""
2
+
3
+ import os
4
+
5
+ import dvc
6
+ import dvc.repo
7
+ import git
8
+ from fastapi import FastAPI, HTTPException
9
+ from pydantic import BaseModel
10
+ from starlette.middleware.cors import CORSMiddleware
11
+
12
+ import calkit
13
+ import calkit.jupyter
14
+
15
+ app = FastAPI(title="calkit-server")
16
+
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=[
20
+ "http://localhost:5173",
21
+ "http://localhost",
22
+ "https://calkit.io",
23
+ "https://staging.calkit.io",
24
+ ],
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+
31
+ @app.get("/health")
32
+ def get_health() -> str:
33
+ return "All good!"
34
+
35
+
36
+ class LocalProject(BaseModel):
37
+ owner_name: str
38
+ project_name: str
39
+ wdir: str
40
+ jupyter_url: str | None
41
+ jupyter_token: str | None
42
+
43
+
44
+ @app.get("/")
45
+ def get_root() -> list[LocalProject]:
46
+ """Return information about the current running server.
47
+
48
+ - The project owner
49
+ - The project name
50
+ - The current working directory
51
+ - A Jupyter server running here, if applicable
52
+ """
53
+ resp = []
54
+ project_dirs = calkit.find_project_dirs()
55
+ servers = calkit.jupyter.get_servers()
56
+ for pdir in project_dirs:
57
+ project = calkit.git.detect_project_name(path=pdir)
58
+ owner, name = project.split("/")
59
+ url = token = None
60
+ for server in servers:
61
+ if server.wdir == pdir:
62
+ url = server.url
63
+ token = server.token
64
+ break
65
+ resp.append(
66
+ LocalProject(
67
+ owner_name=owner,
68
+ project_name=name,
69
+ wdir=os.path.abspath(pdir),
70
+ jupyter_token=token,
71
+ jupyter_url=url,
72
+ )
73
+ )
74
+ return resp
75
+
76
+
77
+ @app.get("/projects/{owner_name}/{project_name}")
78
+ def get_local_project(owner_name: str, project_name: str) -> LocalProject:
79
+ all_projects = get_root()
80
+ for project in all_projects:
81
+ if (
82
+ project.owner_name == owner_name
83
+ and project.project_name == project_name
84
+ ):
85
+ return project
86
+ raise HTTPException(404)
87
+
88
+
89
+ @app.get("/projects/{owner_name}/{project_name}/jupyter-server")
90
+ def get_project_jupyter_server(
91
+ owner_name: str,
92
+ project_name: str,
93
+ autostart=False,
94
+ no_browser=False,
95
+ ) -> calkit.jupyter.Server | None:
96
+ project = get_local_project(
97
+ owner_name=owner_name, project_name=project_name
98
+ )
99
+ if project is None:
100
+ return
101
+ if project.jupyter_url is not None:
102
+ return calkit.jupyter.Server(
103
+ url=project.jupyter_url,
104
+ token=project.jupyter_token,
105
+ wdir=project.wdir,
106
+ )
107
+ if autostart:
108
+ calkit.jupyter.start_server(project.wdir, no_browser=no_browser)
109
+ servers = calkit.jupyter.get_servers()
110
+ for server in servers:
111
+ if server.wdir == project.wdir:
112
+ return server
113
+
114
+
115
+ @app.delete("/projects/{owner_name}/{project_name}/jupyter-server")
116
+ def stop_project_jupyter_server(owner_name: str, project_name: str) -> None:
117
+ project = get_local_project(
118
+ owner_name=owner_name, project_name=project_name
119
+ )
120
+ if project is None:
121
+ return
122
+ if project.jupyter_url is not None:
123
+ calkit.jupyter.stop_server(url=project.jupyter_url)
124
+
125
+
126
+ @app.get("/cwd")
127
+ def get_cwd() -> str:
128
+ return os.getcwd()
129
+
130
+
131
+ @app.get("/projects/{owner_name}/{project_name}/ls")
132
+ def get_ls(owner_name: str, project_name: str) -> list[dict]:
133
+ project = get_local_project(owner_name, project_name)
134
+ repo = git.Repo(project.wdir)
135
+ contents = os.listdir(project.wdir)
136
+ resp = []
137
+ for item in contents:
138
+ if item == ".git" or repo.ignored(item):
139
+ continue
140
+ if os.path.isfile(os.path.join(project.wdir, item)):
141
+ kind = "file"
142
+ else:
143
+ kind = "dir"
144
+ resp.append(dict(name=item, type=kind))
145
+ return sorted(resp, key=lambda item: (item["type"], item["name"]))
146
+
147
+
148
+ @app.post("/projects/{owner_name}/{project_name}/git/{command}")
149
+ def run_git_command(
150
+ owner_name: str, project_name: str, command: str, params: dict = {}
151
+ ):
152
+ project = get_local_project(owner_name, project_name)
153
+ func = getattr(git.Repo(project.wdir).git, command)
154
+ return func(**params)
155
+
156
+
157
+ @app.get("/diff")
158
+ def get_diff():
159
+ """Get differences in working directory, from both Git and DVC."""
160
+ raise HTTPException(501)
161
+
162
+
163
+ @app.get("/projects/{owner_name}/{project_name}/status")
164
+ def get_status(owner_name: str, project_name: str):
165
+ """Get status in working directory, from both Git and DVC."""
166
+ project = get_local_project(owner_name, project_name)
167
+ git_repo = git.Repo(project.wdir)
168
+ untracked_git_files = git_repo.untracked_files
169
+ # Get a list of diffs of the working tree to the index
170
+ git_diff = git_repo.index.diff(None)
171
+ git_diff_files = [d.a_path for d in git_diff]
172
+ dvc_repo = dvc.repo.Repo(project.wdir)
173
+ # Get a dictionary of DVC artifacts that have changed, keyed by the DVC
174
+ # file, where values are a list, which may include a dict with a
175
+ # 'changed outs' key, e.g.,
176
+ # {
177
+ # "data/jhtdb-transitional-bl/all-stats.h5.dvc": [
178
+ # {
179
+ # "changed outs": {
180
+ # "data/jhtdb-transitional-bl/all-stats.h5": "modified"
181
+ # }
182
+ # }
183
+ # ]
184
+ # }
185
+ dvc_status = dvc.repo.status.status(dvc_repo)
186
+ # TODO: Structure this in an intelligent way
187
+ return {
188
+ "dvc": dvc_status,
189
+ "git": {"untracked": untracked_git_files, "diff": git_diff_files},
190
+ }
191
+
192
+
193
+ @app.post("/projects/{owner_name}/{project_name}/open/vscode")
194
+ def open_vscode(owner_name: str, project_name: str) -> int:
195
+ project = get_local_project(owner_name, project_name)
196
+ return os.system(f"code {project.wdir}")
197
+
198
+
199
+ @app.get("/jupyter/servers")
200
+ def get_jupyter_servers() -> list[calkit.jupyter.Server]:
201
+ return calkit.jupyter.get_servers()
File without changes
@@ -0,0 +1,8 @@
1
+ """Tests for the ``core`` module."""
2
+
3
+ import calkit
4
+
5
+
6
+ def test_find_project_dirs():
7
+ calkit.find_project_dirs()
8
+ assert calkit.find_project_dirs(relative=False)
@@ -0,0 +1,7 @@
1
+ """Tests for the ``jupyter`` module."""
2
+
3
+ import calkit
4
+
5
+
6
+ def test_get_servers():
7
+ calkit.jupyter.get_servers()
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.3
2
+ Name: calkit-python
3
+ Version: 0.0.1
4
+ Summary: Reproducibility made easy.
5
+ Project-URL: Homepage, https://github.com/calkit/calkit
6
+ Project-URL: Issues, https://github.com/calkit/calkit/issues
7
+ Author-email: Pete Bachant <petebachant@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Requires-Dist: dvc
14
+ Requires-Dist: fastapi
15
+ Requires-Dist: gitpython
16
+ Requires-Dist: keyring
17
+ Requires-Dist: pandas
18
+ Requires-Dist: polars
19
+ Requires-Dist: pydantic-settings
20
+ Requires-Dist: pydantic[email]
21
+ Requires-Dist: pyjwt
22
+ Requires-Dist: requests
23
+ Requires-Dist: typer
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Calkit
27
+
28
+ [Calkit](https://calkit.io) makes reproducible research easy,
29
+ acting as a layer on top of
30
+ [Git](https://git-scm.com/) and [DVC](https://dvc.org/), such that all
31
+ all materials involved in the research process can be fully described in a
32
+ single repository.
33
+
34
+ ## Installation
35
+
36
+ Simply run
37
+
38
+ ```sh
39
+ pip install calkit-python
40
+ ```
41
+
42
+ ## Cloud integration
43
+
44
+ The Calkit cloud platform (https://calkit.io) serves as a project
45
+ management interface and a DVC remote for easily storing all versions of your
46
+ data/code/figures/publications, interacting with your collaborators,
47
+ reusing others' research artifacts, etc.
48
+
49
+ After signing up, visit the [settings](https://calkit.io/settings) page
50
+ and create a token.
51
+ Then run
52
+
53
+ ```sh
54
+ calkit config set token ${YOUR_TOKEN_HERE}
55
+ ```
56
+
57
+ Then, inside a project repo you'd like to connect to the cloud, run
58
+
59
+ ```sh
60
+ calkit config setup-remote
61
+ ```
62
+
63
+ This will setup the Calkit DVC remote, such that commands like `dvc push` will
64
+ allow you to push versions of your data or pipeline outputs to the cloud
65
+ for safe storage and sharing with your collaborators.
66
+
67
+ ## How it works
68
+
69
+ Calkit creates a simple human-readable "database" inside the `calkit.yaml`
70
+ file, which serves as a way to store important information about the project,
71
+ e.g., what question(s) it seeks to answer,
72
+ what files should be considered datasets, figures, publications, etc.
73
+ The Calkit cloud reads this database and registers the various entities
74
+ as part of the entire ecosystem such that if a project is made public,
75
+ other researchers can find and reuse your work to accelerate their own.
@@ -0,0 +1,19 @@
1
+ calkit/__init__.py,sha256=3QZKwXecsbiUn-TsKW90NGAxGaVKOjG1q_EhB9lF_7Q,142
2
+ calkit/cli.py,sha256=G23OBAAEHt-gE9lEDQFJ45RGobPyC-tt_xHnCh8GNvs,2993
3
+ calkit/cloud.py,sha256=DLhds45nFFuXE9k6VpWMh4toxKZdy4yp2jfdAkl6Nzg,2173
4
+ calkit/config.py,sha256=UqgWHrsQgM_bODCrmOEJZY6gxpBs_jFbEdT-ZQLjLSQ,2008
5
+ calkit/core.py,sha256=44S3Ql3x6uJraoss9ha-FWbZeSfW2YOxU81p16k80h0,954
6
+ calkit/data.py,sha256=G_dFLUCE7tMqGfg7NV7-197NrOYATSgI1eTdcz2HCyI,1411
7
+ calkit/dvc.py,sha256=a5X8MeAr9rDmY70MZo4BHh2LfHocj0u4WAiqPXGGEos,1470
8
+ calkit/git.py,sha256=2BcfdyGaCxoN25S5aFGMRMmIfWHCrCoFY3hNrGsu2yM,314
9
+ calkit/gui.py,sha256=2UCrMyosc5v4CLlDcHO7oDwW9eLn5_WbNaLaG5bjMTc,23
10
+ calkit/jupyter.py,sha256=VYONwhc_2orwjlki0oNYxy1dYkDBFWvi02Tl8-Tnw-c,998
11
+ calkit/server.py,sha256=NgJuUcc8tZOY5wIH86xFA7Bs1Ji61JYcMUEB7VXlwkw,5923
12
+ calkit/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ calkit/tests/test_core.py,sha256=CdRqvYnq3MidyTknvfIbkmmhMfydpyKtTW9hakAvzsc,167
14
+ calkit/tests/test_jupyter.py,sha256=YTL6zI740UM2KUjskSipzvSKxsyQ8rVPFQIX5cb2tMQ,114
15
+ calkit_python-0.0.1.dist-info/METADATA,sha256=ryO4XQ0MNWArJsunUIEAWjM_oFcqUJ3VO7BIToD8jrk,2334
16
+ calkit_python-0.0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
+ calkit_python-0.0.1.dist-info/entry_points.txt,sha256=tiPfbDDZNB-YStw5YVO_4Gww0W3Dcdy8Mh6muX_GWnY,42
18
+ calkit_python-0.0.1.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
19
+ calkit_python-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ calkit = calkit.cli:run
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2024 Pete Bachant
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.