haplohub-cli 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.
Files changed (48) hide show
  1. haplohub_cli/__init__.py +3 -0
  2. haplohub_cli/_version.py +21 -0
  3. haplohub_cli/auth/__init__.py +0 -0
  4. haplohub_cli/auth/auth.py +42 -0
  5. haplohub_cli/auth/auth0_client.py +104 -0
  6. haplohub_cli/auth/auth_web_server.py +43 -0
  7. haplohub_cli/auth/tests/__init__.py +0 -0
  8. haplohub_cli/auth/tests/test_auth_web_server.py +22 -0
  9. haplohub_cli/auth/tests/test_token_storage.py +43 -0
  10. haplohub_cli/auth/token_storage.py +27 -0
  11. haplohub_cli/cli.py +35 -0
  12. haplohub_cli/commands/__init__.py +0 -0
  13. haplohub_cli/commands/cohort.py +33 -0
  14. haplohub_cli/commands/config.py +40 -0
  15. haplohub_cli/commands/file.py +84 -0
  16. haplohub_cli/commands/login.py +17 -0
  17. haplohub_cli/commands/model/__init__.py +0 -0
  18. haplohub_cli/commands/model/model.py +62 -0
  19. haplohub_cli/commands/model/run.py +63 -0
  20. haplohub_cli/commands/version.py +11 -0
  21. haplohub_cli/config/__init__.py +0 -0
  22. haplohub_cli/config/config.py +13 -0
  23. haplohub_cli/config/config_manager.py +57 -0
  24. haplohub_cli/config/environments.py +20 -0
  25. haplohub_cli/core/__init__.py +7 -0
  26. haplohub_cli/core/api/__init__.py +0 -0
  27. haplohub_cli/core/api/client.py +45 -0
  28. haplohub_cli/core/checksum.py +6 -0
  29. haplohub_cli/core/network.py +6 -0
  30. haplohub_cli/core/slug.py +7 -0
  31. haplohub_cli/core/tests/__init__.py +0 -0
  32. haplohub_cli/core/tests/test_network.py +20 -0
  33. haplohub_cli/core/tests/test_slug.py +20 -0
  34. haplohub_cli/formatters/__init__.py +6 -0
  35. haplohub_cli/formatters/cohort.py +39 -0
  36. haplohub_cli/formatters/config.py +15 -0
  37. haplohub_cli/formatters/decorators.py +9 -0
  38. haplohub_cli/formatters/file.py +46 -0
  39. haplohub_cli/formatters/formatter_registry.py +21 -0
  40. haplohub_cli/formatters/generic.py +11 -0
  41. haplohub_cli/formatters/model.py +31 -0
  42. haplohub_cli/formatters/utils.py +13 -0
  43. haplohub_cli/settings.py +24 -0
  44. haplohub_cli-0.0.1.dist-info/METADATA +61 -0
  45. haplohub_cli-0.0.1.dist-info/RECORD +48 -0
  46. haplohub_cli-0.0.1.dist-info/WHEEL +5 -0
  47. haplohub_cli-0.0.1.dist-info/entry_points.txt +2 -0
  48. haplohub_cli-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ from ._version import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.0.1'
21
+ __version_tuple__ = version_tuple = (0, 0, 1)
File without changes
@@ -0,0 +1,42 @@
1
+ import webbrowser
2
+
3
+ import click
4
+
5
+ from haplohub_cli.auth.auth0_client import auth0_client
6
+ from haplohub_cli.auth.auth_web_server import AuthWebServer
7
+ from haplohub_cli.config.config_manager import config_manager
8
+ from haplohub_cli.core import ensure_config_dir
9
+ from haplohub_cli.core.network import check_port_available
10
+
11
+
12
+ def token_login(refresh_token: str):
13
+ credentials = auth0_client.exchange_refresh_token(refresh_token)
14
+
15
+ click.echo("Successfully authenticated with HaploHub.")
16
+ auth0_client.token_storage.store_credentials(credentials)
17
+
18
+
19
+ def interactive_login():
20
+ ensure_config_dir()
21
+
22
+ if not check_port_available(config_manager.config.redirect_port):
23
+ click.echo(
24
+ f"Port {config_manager.config.redirect_port} is already in use. Please ensure it is free and try again.\n"
25
+ f"Use `lsof -i4TCP:{config_manager.config.redirect_port} -sTCP:LISTEN -P -n` to find the process using the port."
26
+ )
27
+ exit(1)
28
+
29
+ auth_request = auth0_client.init_auth_request()
30
+ auth_url = auth_request.auth_url
31
+
32
+ click.echo(
33
+ f"Your browser has been opened to authenticate with HaploHub.\n\n {auth_url[0 : auth_url.find('?')] + '...'}\n"
34
+ )
35
+ webbrowser.open(auth_url)
36
+
37
+ auth_web_server = AuthWebServer(config_manager.config.redirect_port)
38
+ auth_code = auth_web_server.handle_request()
39
+ credentials = auth_request.exchange_code(auth_code)
40
+
41
+ click.echo("Successfully authenticated with HaploHub.")
42
+ auth0_client.token_storage.store_credentials(credentials)
@@ -0,0 +1,104 @@
1
+ import base64
2
+ import hashlib
3
+ import os
4
+
5
+ from requests import Session
6
+
7
+ from haplohub_cli.auth.token_storage import TokenStorage, token_storage
8
+ from haplohub_cli.config.config_manager import config_manager
9
+
10
+
11
+ class AuthRequest:
12
+ def __init__(
13
+ self,
14
+ client: "Auth0Client",
15
+ scopes: list[str],
16
+ ):
17
+ self.client = client
18
+ self.scopes = scopes
19
+ self.code_verifier, self.code_challenge = self.generate_code_verifier()
20
+
21
+ @property
22
+ def auth_url(self):
23
+ return (
24
+ f"https://{self.client.domain}/authorize"
25
+ f"?client_id={self.client.client_id}"
26
+ f"&response_type=code"
27
+ f"&redirect_uri={self.client.redirect_uri}"
28
+ f"&scope={' '.join(self.scopes)}"
29
+ f"&code_challenge={self.code_challenge}"
30
+ f"&code_challenge_method=S256"
31
+ f"&audience={self.client.audience}"
32
+ )
33
+
34
+ def generate_code_verifier(self):
35
+ verifier = base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8").rstrip("=")
36
+ challenge = hashlib.sha256(verifier.encode("utf-8")).digest()
37
+ challenge = base64.urlsafe_b64encode(challenge).decode("utf-8").rstrip("=")
38
+ return verifier, challenge
39
+
40
+ def exchange_code(self, code: str):
41
+ return self.client.exchange_code(code, self.code_verifier)
42
+
43
+
44
+ class Auth0Client:
45
+ http = Session()
46
+
47
+ def __init__(self, token_storage: TokenStorage, domain: str, client_id: str, audience: str, redirect_uri: str):
48
+ self.token_storage = token_storage
49
+ self.domain = domain
50
+ self.client_id = client_id
51
+ self.audience = audience
52
+ self.redirect_uri = redirect_uri
53
+
54
+ def init_auth_request(self, scopes: list[str] = ("openid", "profile", "email")):
55
+ return AuthRequest(client=self, scopes=scopes)
56
+
57
+ def exchange_refresh_token(self, refresh_token: str):
58
+ return self._make_request(
59
+ "oauth/token",
60
+ "POST",
61
+ json={
62
+ "grant_type": "refresh_token",
63
+ "client_id": self.client_id,
64
+ "refresh_token": refresh_token,
65
+ "audience": self.audience,
66
+ },
67
+ )
68
+
69
+ def exchange_code(self, code: str, code_verifier: str):
70
+ return self._make_request(
71
+ "oauth/token",
72
+ "POST",
73
+ json={
74
+ "grant_type": "authorization_code",
75
+ "client_id": self.client_id,
76
+ "code": code,
77
+ "redirect_uri": self.redirect_uri,
78
+ "code_verifier": code_verifier,
79
+ "audience": self.audience,
80
+ },
81
+ )
82
+
83
+ def get_user_info(self):
84
+ return self._make_request("userinfo", "GET")
85
+
86
+ def _make_request(self, path: str, method: str, data: dict = None, json: dict = None):
87
+ url = f"https://{self.domain}/{path}"
88
+
89
+ headers = None
90
+ if self.token_storage.credentials_exist:
91
+ headers = {"Authorization": f"Bearer {self.token_storage.get_access_token()}"}
92
+
93
+ response = self.http.request(method, url, headers=headers, data=data, json=json)
94
+ response.raise_for_status()
95
+ return response.json()
96
+
97
+
98
+ auth0_client = Auth0Client(
99
+ token_storage=token_storage,
100
+ domain=config_manager.config.auth0_domain,
101
+ client_id=config_manager.config.auth0_client_id,
102
+ audience=config_manager.config.auth0_audience,
103
+ redirect_uri=config_manager.config.auth0_redirect_uri,
104
+ )
@@ -0,0 +1,43 @@
1
+ from http.server import BaseHTTPRequestHandler, HTTPServer
2
+ from typing import Any
3
+ from urllib.parse import parse_qs, urlparse
4
+
5
+ HTML_SNIPPET = """
6
+ <html>
7
+ <body>
8
+ <h1>Authentication successful!</h1>
9
+ <p>
10
+ You can now close this window.
11
+ </p>
12
+ </body>
13
+ </html>
14
+ """
15
+
16
+
17
+ class AuthHandler(BaseHTTPRequestHandler):
18
+ def log_message(self, format: str, *args: Any) -> None:
19
+ # Suppress logging
20
+ pass
21
+
22
+ def do_GET(self):
23
+ query = urlparse(self.path).query
24
+ params = parse_qs(query)
25
+ if "code" in params:
26
+ self.server.last_auth_code = params["code"][0]
27
+ self.send_response(200)
28
+ self.send_header("Content-Type", "text/html")
29
+ self.end_headers()
30
+ self.wfile.write(HTML_SNIPPET.encode("utf-8"))
31
+
32
+
33
+ class AuthWebServer:
34
+ def __init__(self, port: int):
35
+ self.port = port
36
+ self.server = HTTPServer(
37
+ ("localhost", self.port),
38
+ AuthHandler,
39
+ )
40
+
41
+ def handle_request(self):
42
+ self.server.handle_request()
43
+ return self.server.last_auth_code
File without changes
@@ -0,0 +1,22 @@
1
+ from threading import Timer
2
+ from unittest import TestCase
3
+
4
+ import requests
5
+
6
+ from haplohub_cli.auth.auth_web_server import AuthWebServer
7
+
8
+
9
+ class AuthWebServerTestCase(TestCase):
10
+ @classmethod
11
+ def setUpClass(cls):
12
+ cls.port = 60000
13
+ cls.auth_code = "qwerty123456"
14
+ cls.instance = AuthWebServer(cls.port)
15
+
16
+ def test_auth_web_server_should_return_auth_code_from_redirect_uri(self):
17
+ Timer(0.01, self._send_request).start()
18
+ auth_code = self.instance.handle_request()
19
+ self.assertEqual(auth_code, self.auth_code)
20
+
21
+ def _send_request(self):
22
+ requests.get(f"http://localhost:{self.port}/?code={self.auth_code}")
@@ -0,0 +1,43 @@
1
+ import json
2
+
3
+ from pyfakefs.fake_filesystem_unittest import TestCase
4
+
5
+ from haplohub_cli.auth.token_storage import TokenStorage
6
+
7
+
8
+ class TokenStorageTestCase(TestCase):
9
+ @classmethod
10
+ def setUpClass(cls):
11
+ cls.setUpClassPyfakefs()
12
+ cls.instance = TokenStorage("/tmp/test_token_storage.json")
13
+
14
+ def test_credentials_exist_should_return_false_when_file_does_not_exist(self):
15
+ self.assertFalse(self.instance.credentials_exist)
16
+
17
+ def test_credentials_exist_should_return_true_when_file_exists(self):
18
+ with open(self.instance.token_file, "w"):
19
+ pass
20
+
21
+ self.assertTrue(self.instance.credentials_exist)
22
+
23
+ def test_store_credentials_should_save_credentials_to_file(self):
24
+ creds = {"test": True}
25
+
26
+ self.instance.store_credentials(creds)
27
+
28
+ with open(self.instance.token_file, "r") as f:
29
+ self.assertEqual(json.load(f), creds)
30
+
31
+ def test_get_credentials_should_return_credentials_from_file(self):
32
+ creds = {"test": True}
33
+
34
+ self.instance.store_credentials(creds)
35
+
36
+ self.assertEqual(self.instance.get_credentials(), creds)
37
+
38
+ def test_get_access_token_should_return_access_token_from_credentials(self):
39
+ creds = {"access_token": "123456"}
40
+
41
+ self.instance.store_credentials(creds)
42
+
43
+ self.assertEqual(self.instance.get_access_token(), "123456")
@@ -0,0 +1,27 @@
1
+ import json
2
+ from genericpath import exists
3
+
4
+ from haplohub_cli import settings
5
+
6
+
7
+ class TokenStorage:
8
+ def __init__(self, token_file: str):
9
+ self.token_file = token_file
10
+
11
+ @property
12
+ def credentials_exist(self):
13
+ return exists(self.token_file)
14
+
15
+ def store_credentials(self, credentials: dict):
16
+ with open(self.token_file, "w") as f:
17
+ json.dump(credentials, f)
18
+
19
+ def get_credentials(self):
20
+ with open(self.token_file, "r") as f:
21
+ return json.load(f)
22
+
23
+ def get_access_token(self):
24
+ return self.get_credentials()["access_token"]
25
+
26
+
27
+ token_storage = TokenStorage(settings.CREDENTIALS_FILE)
haplohub_cli/cli.py ADDED
@@ -0,0 +1,35 @@
1
+ import click
2
+
3
+ from haplohub_cli.commands import cohort, config, file, login, version
4
+ from haplohub_cli.commands.model import model
5
+ from haplohub_cli.formatters.formatter_registry import formatter_registry
6
+
7
+
8
+ @click.group()
9
+ def cli():
10
+ """
11
+ HaploHub CLI
12
+
13
+ To get started, take a look at the documentation:
14
+ https://github.com/haplotypelabs/haplohub-cli
15
+ """
16
+ pass
17
+
18
+
19
+ @cli.result_callback()
20
+ def formatter_callback(result):
21
+ if result is None or not formatter_registry.has_formatter(type(result)):
22
+ return
23
+
24
+ formatter_registry.format(result)
25
+
26
+
27
+ cli.add_command(config.config)
28
+ cli.add_command(login.cmd)
29
+ cli.add_command(version.cmd)
30
+ cli.add_command(cohort.cohort)
31
+ cli.add_command(file.file)
32
+ cli.add_command(model.model)
33
+
34
+ if __name__ == "__main__":
35
+ cli()
File without changes
@@ -0,0 +1,33 @@
1
+ import click
2
+ from haplohub import CreateCohortRequest
3
+
4
+ from haplohub_cli.core.api.client import client
5
+
6
+
7
+ @click.group()
8
+ def cohort():
9
+ """
10
+ Manage cohorts
11
+ """
12
+ pass
13
+
14
+
15
+ @cohort.command()
16
+ def list():
17
+ return client.cohort.list_cohorts()
18
+
19
+
20
+ @cohort.command()
21
+ @click.argument("name")
22
+ @click.option("--description", type=str, required=False)
23
+ def create(name, description=None):
24
+ description = description or ""
25
+
26
+ request = CreateCohortRequest(name=name, description=description)
27
+ return client.cohort.create_cohort(request)
28
+
29
+
30
+ @cohort.command()
31
+ @click.argument("id")
32
+ def delete(id):
33
+ return client.cohort.delete_cohort(id)
@@ -0,0 +1,40 @@
1
+ import click
2
+
3
+ from haplohub_cli.config.config_manager import config_manager
4
+ from haplohub_cli.config.environments import ENVIRONMENTS
5
+
6
+
7
+ @click.group()
8
+ def config():
9
+ """
10
+ Manage configuration
11
+ """
12
+ pass
13
+
14
+
15
+ @config.command()
16
+ def show():
17
+ return config_manager.config
18
+
19
+
20
+ @config.command()
21
+ @click.argument("key")
22
+ @click.argument("value")
23
+ def set(key, value):
24
+ setattr(config_manager.config, key, value)
25
+ config_manager.save()
26
+ return config_manager.config
27
+
28
+
29
+ @config.command()
30
+ @click.argument("environment", type=click.Choice(ENVIRONMENTS.keys()))
31
+ def switch(environment):
32
+ config_manager.switch_environment(environment)
33
+ return config_manager.config
34
+
35
+
36
+ @config.command()
37
+ def reset():
38
+ config_manager.reset()
39
+ config_manager.save()
40
+ return config_manager.config
@@ -0,0 +1,84 @@
1
+ from concurrent.futures import ThreadPoolExecutor, as_completed
2
+ from posixpath import basename
3
+ from uuid import uuid4
4
+
5
+ import click
6
+ import requests
7
+ from haplohub import CreateUploadRequestRequest, FileInfo, UploadType
8
+ from rich.progress import Progress
9
+
10
+ from haplohub_cli.core.api.client import client
11
+ from haplohub_cli.core.checksum import calculate_checksum
12
+
13
+
14
+ @click.group()
15
+ def file():
16
+ """
17
+ Manage files
18
+ """
19
+ pass
20
+
21
+
22
+ @file.command()
23
+ @click.option("--cohort", "-c", type=click.STRING, required=True)
24
+ @click.option("--path", "-p", type=click.STRING, required=False)
25
+ @click.option("--recursive", "-r", is_flag=True, required=False)
26
+ def list(cohort: str, path: str = None, recursive: bool = False):
27
+ return client.file.list_files(
28
+ cohort,
29
+ recursive=recursive,
30
+ path=path,
31
+ )
32
+
33
+
34
+ @file.command()
35
+ @click.argument("filenames", nargs=-1, type=click.Path(exists=True, dir_okay=False), required=True)
36
+ @click.option("--cohort", "-c", type=click.STRING, required=True)
37
+ @click.option("--file-type", "-t", type=click.Choice(UploadType), required=True)
38
+ @click.option("--sample", "-s", type=click.STRING, required=False)
39
+ @click.option("--member", "-m", type=click.STRING, required=False)
40
+ def upload(cohort: str, filenames: tuple[str, ...], file_type: UploadType = None, sample: str = None, member: str = None):
41
+ checksums = {}
42
+ file_map = {}
43
+
44
+ for full_path in filenames:
45
+ file_name = basename(full_path)
46
+ checksums[file_name] = calculate_checksum(full_path)
47
+ file_map[file_name] = full_path
48
+
49
+ request = CreateUploadRequestRequest(
50
+ upload_request_id=str(uuid4()),
51
+ file_type=file_type,
52
+ sample_id=sample,
53
+ member_id=member,
54
+ files=[
55
+ FileInfo(
56
+ file_path=file_name,
57
+ md5_hash=checksums[file_name],
58
+ )
59
+ for file_name in file_map.keys()
60
+ ],
61
+ )
62
+
63
+ response = client.upload.create_upload_request(cohort, request).actual_instance
64
+
65
+ if response.status == "error":
66
+ return response
67
+
68
+ with Progress() as progress:
69
+ uploading = progress.add_task("Uploading files", total=len(response.result.upload_links))
70
+
71
+ with ThreadPoolExecutor(max_workers=8) as executor:
72
+ futures = [
73
+ executor.submit(
74
+ requests.put,
75
+ file.signed_url,
76
+ data=open(file_map[file.original_file_path], "rb").read(),
77
+ headers={"Content-MD5": checksums[file.original_file_path]},
78
+ )
79
+ for file in response.result.upload_links
80
+ ]
81
+
82
+ for future in as_completed(futures):
83
+ future.result().raise_for_status()
84
+ progress.update(uploading, advance=1)
@@ -0,0 +1,17 @@
1
+ from typing import Optional
2
+
3
+ import click
4
+
5
+ from haplohub_cli.auth.auth import interactive_login, token_login
6
+
7
+
8
+ @click.command(name="login")
9
+ @click.option("--token", type=str, help="Token")
10
+ def cmd(token: Optional[str] = None):
11
+ """
12
+ Login to HaploHub
13
+ """
14
+ if token is None:
15
+ interactive_login()
16
+ else:
17
+ token_login(token)
File without changes
@@ -0,0 +1,62 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from haplohub import CreateModelRequest, PushModelRequest
5
+
6
+ from haplohub_cli.core.api.client import client
7
+
8
+ from . import run
9
+
10
+
11
+ @click.group()
12
+ def model():
13
+ """
14
+ Manage models
15
+ """
16
+ pass
17
+
18
+
19
+ @model.command()
20
+ def list():
21
+ """
22
+ List all models.
23
+ """
24
+ return client.model.list_models()
25
+
26
+
27
+ @model.command()
28
+ def build():
29
+ """
30
+ Build a model.
31
+ """
32
+ subprocess.run(["cog", "build"])
33
+
34
+
35
+ @model.command()
36
+ @click.argument("name")
37
+ def push(name, tag: str = None):
38
+ models = client.model.list_models(name=name)
39
+ if models.total_count == 0:
40
+ model = client.model.create_model(CreateModelRequest(name=name))
41
+ else:
42
+ model = models.items[0]
43
+
44
+ response = client.model.push_model(model.id, PushModelRequest(version="latest"))
45
+ push_request = response.result
46
+
47
+ subprocess.run(
48
+ ["docker", "login", "-u", "oauth2accesstoken", "--password-stdin", f"https://{push_request.registry_host}"],
49
+ input=push_request.push_token.encode(),
50
+ check=True,
51
+ )
52
+ subprocess.run(["cog", "push", f"{push_request.registry_host}/{push_request.image_path}"], check=True)
53
+
54
+ # TODO: Push request contains repository URL and temporary credentials.
55
+ # TODO: Authenticate to the customer's repository using temporary credentials.
56
+ # TODO: Use `cog push {repostiroty_url}/{name}` to push the model to the customer's repository.
57
+ # docker login -u oauth2accesstoken --password {push_request.push_token} {push_request.repository_url}
58
+ # cog push {push_request.repository_url}/{name}
59
+ pass
60
+
61
+
62
+ model.add_command(run.run)
@@ -0,0 +1,63 @@
1
+ import click
2
+
3
+ from haplohub_cli.core.api.client import client
4
+
5
+
6
+ @click.group()
7
+ def run():
8
+ """
9
+ Manage model runs
10
+ """
11
+ pass
12
+
13
+
14
+ @run.command()
15
+ @click.argument("model_id")
16
+ @click.option("--input-file", "-i", type=click.STRING, required=False)
17
+ @click.option("--input-data", "-I", type=click.STRING, required=False)
18
+ @click.option("--wait", "-w", is_flag=True)
19
+ def create(model_id, input_file: str = None, input_data: str = None, wait: bool = False):
20
+ """
21
+ 1. Fetches model information.
22
+ 2. Ensures input data is valid.
23
+ 3. Call an API to run the model with input data.
24
+ 4. Pass wait flag to wait for the job to finish.
25
+ 4.1 Poll the job run status until it's finished.
26
+ 5. Return either the job run metadata or the job results.
27
+ """
28
+ pass
29
+
30
+
31
+ @run.command()
32
+ def list():
33
+ return client.run.list_runs()
34
+
35
+
36
+ @run.command()
37
+ @click.argument("run_id")
38
+ def status(run_id):
39
+ """
40
+ 1. Fetches job run information.
41
+ 2. Return the job run status.
42
+ """
43
+ pass
44
+
45
+
46
+ @run.command()
47
+ @click.argument("run_id")
48
+ def logs(run_id):
49
+ """
50
+ 1. Fetches job run logs.
51
+ 2. Return the job run logs.
52
+ """
53
+ pass
54
+
55
+
56
+ @run.command()
57
+ @click.argument("run_id")
58
+ def results(run_id):
59
+ """
60
+ 1. Fetches job run results.
61
+ 2. Return the job run results.
62
+ """
63
+ pass
@@ -0,0 +1,11 @@
1
+ import click
2
+
3
+ from haplohub_cli import __version__
4
+
5
+
6
+ @click.command(name="version")
7
+ def cmd():
8
+ """
9
+ Get the version of the HaploHub CLI
10
+ """
11
+ click.echo(f"HaploHub CLI version {__version__}")
File without changes
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Config(BaseModel):
5
+ api_url: str
6
+ redirect_port: int
7
+ auth0_domain: str
8
+ auth0_client_id: str
9
+ auth0_audience: str
10
+
11
+ @property
12
+ def auth0_redirect_uri(self):
13
+ return f"http://localhost:{self.redirect_port}/"
@@ -0,0 +1,57 @@
1
+ from genericpath import exists
2
+
3
+ from haplohub_cli.config.config import Config
4
+
5
+ from .. import settings
6
+ from .environments import ENVIRONMENTS
7
+
8
+ defaults = Config(
9
+ redirect_port=8088,
10
+ **ENVIRONMENTS["prod"],
11
+ )
12
+
13
+
14
+ class ConfigManager:
15
+ def __init__(self, config_file: str):
16
+ self.config_file = config_file
17
+ self._config = None
18
+ self.read_config()
19
+
20
+ def init_config(self):
21
+ if exists(self.config_file):
22
+ return
23
+
24
+ self._config = defaults
25
+ self.save()
26
+
27
+ def read_config(self):
28
+ if not exists(self.config_file):
29
+ self.init_config()
30
+
31
+ self._config = Config.parse_file(self.config_file)
32
+
33
+ def switch_environment(self, environment: str):
34
+ if environment not in ENVIRONMENTS:
35
+ raise ValueError(f"Invalid environment: {environment}")
36
+
37
+ for key, value in ENVIRONMENTS[environment].items():
38
+ setattr(self._config, key, value)
39
+
40
+ self.save()
41
+
42
+ def save(self):
43
+ with open(self.config_file, "wt") as f:
44
+ f.write(self._config.json(indent=4))
45
+
46
+ def reset(self):
47
+ self._config = defaults
48
+ self.save()
49
+
50
+ @property
51
+ def config(self):
52
+ if self._config is None:
53
+ self.read_config()
54
+ return self._config
55
+
56
+
57
+ config_manager = ConfigManager(config_file=settings.CONFIG_FILE)
@@ -0,0 +1,20 @@
1
+ ENVIRONMENTS = {
2
+ "local": {
3
+ "auth0_domain": "dev-42a7qv136prmsazj.us.auth0.com",
4
+ "auth0_client_id": "QHC6QYwzm9UW9fty7e5mno3AJrcjV5MY",
5
+ "auth0_audience": "http://localhost:3000/api/",
6
+ "api_url": "http://localhost:8000",
7
+ },
8
+ "stage": {
9
+ "auth0_domain": "dev-42a7qv136prmsazj.us.auth0.com",
10
+ "auth0_client_id": "5u4nnf7lwKtzH1sGxBm8AtM25FJMiYGm",
11
+ "auth0_audience": "https://stage.haplohub.com/api/",
12
+ "api_url": "https://stage.haplohub.com",
13
+ },
14
+ "prod": {
15
+ "auth0_domain": "dev-42a7qv136prmsazj.us.auth0.com",
16
+ "auth0_client_id": "TRi894X7yV3e8EOpNVrDdvQbRvYnjibH",
17
+ "auth0_audience": "https://haplohub.com/api/",
18
+ "api_url": "https://haplohub.com",
19
+ },
20
+ }
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from haplohub_cli import settings
4
+
5
+
6
+ def ensure_config_dir():
7
+ os.makedirs(settings.CONFIG_DIR, exist_ok=True)
File without changes
@@ -0,0 +1,45 @@
1
+ from functools import cached_property
2
+
3
+ from haplohub import (
4
+ ApiClient,
5
+ CohortApi,
6
+ Configuration,
7
+ FileApi,
8
+ ModelApi,
9
+ UploadApi,
10
+ )
11
+
12
+ from haplohub_cli import settings
13
+ from haplohub_cli.auth.token_storage import TokenStorage
14
+ from haplohub_cli.config.config_manager import config_manager
15
+
16
+
17
+ class Client:
18
+ token_storage = TokenStorage(settings.CREDENTIALS_FILE)
19
+
20
+ @cached_property
21
+ def client(self):
22
+ config = Configuration(
23
+ host=config_manager.config.api_url,
24
+ access_token=self.token_storage.get_access_token(),
25
+ )
26
+ return ApiClient(config)
27
+
28
+ @cached_property
29
+ def cohort(self):
30
+ return CohortApi(self.client)
31
+
32
+ @cached_property
33
+ def file(self):
34
+ return FileApi(self.client)
35
+
36
+ @cached_property
37
+ def upload(self):
38
+ return UploadApi(self.client)
39
+
40
+ @cached_property
41
+ def model(self):
42
+ return ModelApi(self.client)
43
+
44
+
45
+ client = Client()
@@ -0,0 +1,6 @@
1
+ from base64 import b64encode
2
+ from hashlib import md5
3
+
4
+
5
+ def calculate_checksum(file_path: str) -> str:
6
+ return b64encode(md5(open(file_path, "rb").read()).digest()).decode("utf-8")
@@ -0,0 +1,6 @@
1
+ import socket
2
+
3
+
4
+ def check_port_available(port: int):
5
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
6
+ return s.connect_ex(("localhost", port)) != 0
@@ -0,0 +1,7 @@
1
+ import re
2
+
3
+ non_alphanum_re = re.compile(r"[^a-z0-9]+")
4
+
5
+
6
+ def slugify(text: str) -> str:
7
+ return non_alphanum_re.sub("-", text.lower()).strip("-")
File without changes
@@ -0,0 +1,20 @@
1
+ import socket
2
+ from unittest import TestCase
3
+
4
+ from haplohub_cli.core.network import check_port_available
5
+
6
+
7
+ class NetworkTestCase(TestCase):
8
+ def test_check_port_available_should_return_true_when_port_is_available(self):
9
+ with socket.socket() as sock:
10
+ sock.bind(("localhost", 0))
11
+ port = sock.getsockname()[1]
12
+
13
+ self.assertTrue(check_port_available(port))
14
+
15
+ def test_check_port_available_should_return_false_when_port_is_not_available(self):
16
+ with socket.socket() as sock:
17
+ sock.bind(("localhost", 0))
18
+ port = sock.getsockname()[1]
19
+ sock.listen()
20
+ self.assertFalse(check_port_available(port))
@@ -0,0 +1,20 @@
1
+ from unittest import TestCase
2
+
3
+ from haplohub_cli.core.slug import slugify
4
+
5
+
6
+ class SlugifyTestCase(TestCase):
7
+ def test_slugify_should_return_lowercase_string(self):
8
+ self.assertEqual(slugify("Hello World"), "hello-world")
9
+
10
+ def test_slugify_should_replace_spaces_with_hyphens(self):
11
+ self.assertEqual(slugify("Hello World"), "hello-world")
12
+
13
+ def test_slugify_should_remove_special_characters(self):
14
+ self.assertEqual(slugify("Hello World!"), "hello-world")
15
+
16
+ def test_slugify_should_remove_extra_spaces(self):
17
+ self.assertEqual(slugify("Hello World"), "hello-world")
18
+
19
+ def test_slugify_should_keep_unchanged_when_no_changes_are_needed(self):
20
+ self.assertEqual(slugify("hello-world"), "hello-world")
@@ -0,0 +1,6 @@
1
+ # flake8: noqa
2
+ from .cohort import *
3
+ from .config import *
4
+ from .file import *
5
+ from .generic import *
6
+ from .model import *
@@ -0,0 +1,39 @@
1
+ from haplohub import (
2
+ CohortSchema,
3
+ CreateCohortResponse,
4
+ PaginatedResponseCohortSchema,
5
+ )
6
+ from rich.table import Table
7
+
8
+ from haplohub_cli.formatters import utils
9
+ from haplohub_cli.formatters.decorators import register
10
+
11
+
12
+ @register(CohortSchema)
13
+ def format_cohort(data: CohortSchema):
14
+ table = Table(title="Cohort", caption=f"Id: {data.id}")
15
+ table.add_column("Id")
16
+ table.add_column("Name")
17
+ table.add_column("Description")
18
+ table.add_column("Created")
19
+ table.add_row(str(data.id), data.name, data.description, utils.format_date(data.created))
20
+ return table
21
+
22
+
23
+ @register(PaginatedResponseCohortSchema)
24
+ def format_cohorts(data: PaginatedResponseCohortSchema):
25
+ table = Table(title="Cohorts", caption=f"Total: {data.total_count}")
26
+ table.add_column("Id")
27
+ table.add_column("Name")
28
+ table.add_column("Description")
29
+ table.add_column("Created")
30
+
31
+ for item in data.items:
32
+ table.add_row(str(item.id), item.name, utils.truncate(item.description, 50), utils.format_date(item.created))
33
+
34
+ return table
35
+
36
+
37
+ @register(CreateCohortResponse)
38
+ def format_create_cohort(data: CreateCohortResponse):
39
+ return format_cohort(data.result)
@@ -0,0 +1,15 @@
1
+ from rich.table import Table
2
+
3
+ from haplohub_cli.config.config import Config
4
+ from haplohub_cli.formatters.decorators import register
5
+
6
+
7
+ @register(Config)
8
+ def format_config(data: Config):
9
+ table = Table(title="Config")
10
+ table.add_column("Key")
11
+ table.add_column("Value")
12
+
13
+ for key, value in data.dict().items():
14
+ table.add_row(key, str(value))
15
+ return table
@@ -0,0 +1,9 @@
1
+ from haplohub_cli.formatters.formatter_registry import formatter_registry
2
+
3
+
4
+ def register(model_type):
5
+ def decorator(func):
6
+ formatter_registry.register_formatter(model_type, func)
7
+ return func
8
+
9
+ return decorator
@@ -0,0 +1,46 @@
1
+ from haplohub import (
2
+ FileSchema,
3
+ ResultResponseFileDirSchema,
4
+ ResultResponseFileSchema,
5
+ )
6
+ from rich.table import Table
7
+
8
+ from haplohub_cli.formatters import utils
9
+ from haplohub_cli.formatters.decorators import register
10
+
11
+
12
+ @register(FileSchema)
13
+ def format_file(data: FileSchema):
14
+ table = Table(title="File", caption=f"Id: {data.id}")
15
+ table.add_column("Id")
16
+ table.add_column("File name")
17
+ table.add_column("Size")
18
+ table.add_column("Created")
19
+ table.add_row(str(data.id), data.location, str(data.file_size), utils.format_date(data.created))
20
+ return table
21
+
22
+
23
+ @register(ResultResponseFileDirSchema)
24
+ def format_files(data: ResultResponseFileDirSchema):
25
+ result = data.result
26
+ table = Table(
27
+ title=f"Contents of {result.location or 'ROOT'}",
28
+ caption=f"Total files: {len(result.files)}, total dirs: {len(result.dirs)}",
29
+ )
30
+ table.add_column("Id")
31
+ table.add_column("File name")
32
+ table.add_column("Size")
33
+ table.add_column("Created")
34
+
35
+ for dir in result.dirs:
36
+ table.add_row("DIR", dir.location + "/")
37
+
38
+ for file in result.files:
39
+ table.add_row(str(file.id), file.location, str(file.file_size), utils.format_date(file.created))
40
+
41
+ return table
42
+
43
+
44
+ @register(ResultResponseFileSchema)
45
+ def format_result_response_file(data: ResultResponseFileSchema):
46
+ return format_file(data.result)
@@ -0,0 +1,21 @@
1
+ from rich.console import Console
2
+
3
+
4
+ class FormatterRegistry:
5
+ def __init__(self):
6
+ self.formatters = {}
7
+ self.console = Console()
8
+
9
+ def register_formatter(self, model_type, formatter):
10
+ self.formatters[model_type] = formatter
11
+
12
+ def has_formatter(self, model_type):
13
+ return model_type in self.formatters
14
+
15
+ def format(self, model):
16
+ formatter = self.formatters[type(model)]
17
+ output = formatter(model)
18
+ self.console.print(output)
19
+
20
+
21
+ formatter_registry = FormatterRegistry()
@@ -0,0 +1,11 @@
1
+ from haplohub import (
2
+ ErrorResponse,
3
+ )
4
+ from rich.text import Text
5
+
6
+ from haplohub_cli.formatters.decorators import register
7
+
8
+
9
+ @register(ErrorResponse)
10
+ def format_error_response(data: ErrorResponse):
11
+ return Text(f"Error [{data.error.code}]: {data.error.message}", style="red")
@@ -0,0 +1,31 @@
1
+ from haplohub import (
2
+ MLModelSchema,
3
+ PaginatedResponseMLModelSchema,
4
+ )
5
+ from rich.table import Table
6
+
7
+ from haplohub_cli.formatters import utils
8
+ from haplohub_cli.formatters.decorators import register
9
+
10
+
11
+ @register(MLModelSchema)
12
+ def format_model(data: MLModelSchema):
13
+ table = Table(title="Model", caption=f"Id: {data.id}")
14
+ table.add_column("Id")
15
+ table.add_column("Name")
16
+ table.add_column("Created")
17
+ table.add_row(str(data.id), data.name, utils.format_date(data.created))
18
+ return table
19
+
20
+
21
+ @register(PaginatedResponseMLModelSchema)
22
+ def format_models(data: PaginatedResponseMLModelSchema):
23
+ table = Table(title="Models", caption=f"Total: {data.total_count}")
24
+ table.add_column("Id")
25
+ table.add_column("Name")
26
+ table.add_column("Created")
27
+
28
+ for item in data.items:
29
+ table.add_row(str(item.id), item.name, utils.format_date(item.created))
30
+
31
+ return table
@@ -0,0 +1,13 @@
1
+ from datetime import datetime
2
+
3
+ import pendulum
4
+
5
+
6
+ def format_date(date: datetime) -> str:
7
+ return pendulum.instance(date).to_day_datetime_string()
8
+
9
+
10
+ def truncate(text: str, length: int) -> str:
11
+ if len(text) <= length:
12
+ return text
13
+ return text[:length] + "..."
@@ -0,0 +1,24 @@
1
+ from os import environ
2
+ from posixpath import expanduser, join
3
+
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ # File paths
9
+ CONFIG_DIR = expanduser("~/.haplohub")
10
+ CONFIG_FILE = join(CONFIG_DIR, "config.json")
11
+ CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json")
12
+
13
+ # Authentication
14
+ REDIRECT_PORT = 8088
15
+
16
+ AUTH0_REDIRECT_URI = f"http://localhost:{REDIRECT_PORT}/"
17
+ AUTH0_DOMAIN = environ.get("AUTH0_DOMAIN", "dev-42a7qv136prmsazj.us.auth0.com")
18
+ AUTH0_CLIENT_ID = environ.get("AUTH0_CLIENT_ID", "TRi894X7yV3e8EOpNVrDdvQbRvYnjibH")
19
+ AUTH0_AUDIENCE = environ.get("AUTH0_AUDIENCE", "https://haplohub.com/api/")
20
+
21
+ # API
22
+ API_URL = environ.get("API_URL", "http://localhost:8000")
23
+
24
+ environ["DOCKER_CONFIG"] = join(CONFIG_DIR, "docker")
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: haplohub-cli
3
+ Version: 0.0.1
4
+ Summary: HaploHub Command Line Interface
5
+ Author-email: Mike Polcari <mike@haplotype-labs.com>, Ilya Khrustalev <ilya@haplotype-labs.com>
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: auth0-python>=4.8.0
9
+ Requires-Dist: click>=8.1.8
10
+ Requires-Dist: haplohub>=1.0.4
11
+ Requires-Dist: pendulum>=3.0.0
12
+ Requires-Dist: python-dotenv>=1.0.1
13
+ Requires-Dist: requests>=2.32.0
14
+ Requires-Dist: rich>=13.9.4
15
+ Requires-Dist: haplohub
16
+
17
+ # HaploHub CLI
18
+
19
+ HaploHub is a platform for haplotype data storage and analysis. This CLI provides a way to interact with the HaploHub API.
20
+
21
+ ## Installation
22
+
23
+ To install the CLI, run the following command:
24
+
25
+ ```bash
26
+ python -m venv .venv
27
+ source .venv/bin/activate
28
+ pip install --upgrade pip
29
+ pip install haplohub-cli
30
+ ```
31
+
32
+ You can ensure that the CLI is installed correctly by running the following command:
33
+
34
+ ```bash
35
+ haplohub version
36
+
37
+ # HaploHub CLI version 0.1.0
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ haplohub --help
44
+ ```
45
+
46
+ ### Login
47
+
48
+ The first time you run the CLI, you will be prompted to login.
49
+
50
+ ```bash
51
+ haplohub login
52
+ ```
53
+
54
+ This will open a browser window to the HaploHub login page. Once you login, you will be redirected to the CLI.
55
+ ```bash
56
+ Your browser has been opened to authenticate with HaploHub.
57
+
58
+ https://xxx.us.auth0.com/authorize...
59
+
60
+ Successfully authenticated with HaploHub.
61
+ ```
@@ -0,0 +1,48 @@
1
+ haplohub_cli/__init__.py,sha256=v-rRiDOgZ3sQSMQKq0vgUQZvpeOkoHFXissAx6Ktg84,61
2
+ haplohub_cli/_version.py,sha256=vgltXBYF55vNcC2regxjGN0_cbebmm8VgcDdQaDapWQ,511
3
+ haplohub_cli/cli.py,sha256=dAzULnl-tNtJn2LAzfEU57SixYGHClgMpZm5qkoEeDE,782
4
+ haplohub_cli/settings.py,sha256=oticVH4RzBrIVYUbtXLbHMwHUFutqYecQVI4EY-64Is,723
5
+ haplohub_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ haplohub_cli/auth/auth.py,sha256=GXs2vHpAsBh7aIJJDzNs-cj6Jr_wJx5md8cWhOghKlg,1530
7
+ haplohub_cli/auth/auth0_client.py,sha256=c5jseQTm5trUtyfV4fF81einREb8ni4I8xY2SNJDn64,3457
8
+ haplohub_cli/auth/auth_web_server.py,sha256=QXgCSqmVrqfLcXuaRSjWDzXEogtY5Zs3_2nxINZ_xn8,1115
9
+ haplohub_cli/auth/token_storage.py,sha256=PUD3bjOvcUQQfC6tbrLQd4tQ-5eKcxxc8LvxueeGe3Y,657
10
+ haplohub_cli/auth/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ haplohub_cli/auth/tests/test_auth_web_server.py,sha256=YBJe78CCXN25TPmKY0zFNsXi2ZG7awREy90kS3VWtag,667
12
+ haplohub_cli/auth/tests/test_token_storage.py,sha256=MOy6M1ukCZIHPLDvTPHtqvSscnQQkIttmAdLYFg2r1A,1368
13
+ haplohub_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ haplohub_cli/commands/cohort.py,sha256=jjo4nsonREwVRdcRPixQemMmkLoGLtGaZJ1LC7ccJ7Y,643
15
+ haplohub_cli/commands/config.py,sha256=lVzRbSLgEwxL5OEUp4lr8ceQSz2CIbXph5Hjh5C2_fo,794
16
+ haplohub_cli/commands/file.py,sha256=OI3Y9sgRi-12J2Puba7e13ObabsmF1ozO4nb22ItEZ4,2781
17
+ haplohub_cli/commands/login.py,sha256=Mq_Sc5WhXBiL5DaiqGsCT9obJ3FM4v6AjbWv6UOmpwI,352
18
+ haplohub_cli/commands/version.py,sha256=KivIR_PbALJSQnPyDM03DGac6oKevv9gYvykDVddJ4A,205
19
+ haplohub_cli/commands/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ haplohub_cli/commands/model/model.py,sha256=210wv0VWTqChyaUs57JYwv3cAOISISY05JVrH6X-CvY,1610
21
+ haplohub_cli/commands/model/run.py,sha256=kyGGrvXx9LJDuJTt7pTNdZqj1W21P0oAvqhlBBglOe0,1300
22
+ haplohub_cli/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ haplohub_cli/config/config.py,sha256=ihtV98nX89qUeG6LRrRh5tA2132P-TwIwlH4wMgD13c,275
24
+ haplohub_cli/config/config_manager.py,sha256=y0XH01La3B4nZnnOeiVU7Rb9GXRvkJfJdsjrsM6C9qk,1354
25
+ haplohub_cli/config/environments.py,sha256=ftOyp5EB6ioRX5eUsTiECYm_E93GmYmFBjWF1Q8J9x0,764
26
+ haplohub_cli/core/__init__.py,sha256=yXeki15WeA3_2s1OvMtZ_medNHZsfXQPDVUZ2UK20Uc,124
27
+ haplohub_cli/core/checksum.py,sha256=7bA6UTX9RuYC0wSTKyfjD2wCLUJMrkeDBmTFH9DdL9w,183
28
+ haplohub_cli/core/network.py,sha256=EfHkeG3FiT3zfzTS9vfUJMT2HauOa6wlUOVVxAQ_kgU,172
29
+ haplohub_cli/core/slug.py,sha256=wXzXrQa9F1dDWE3HzXklVicTZWXBNfaG1Qtdm-ZYo0k,149
30
+ haplohub_cli/core/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ haplohub_cli/core/api/client.py,sha256=d_xhz3rgldGf0R-3V5T4HZWGWct351hZlaGHCwNkPr0,958
32
+ haplohub_cli/core/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ haplohub_cli/core/tests/test_network.py,sha256=mqWChARUmhi4s2_mmh-n29huay1tjI6eG7OTkBTRz5g,685
34
+ haplohub_cli/core/tests/test_slug.py,sha256=vufw7ppXY8exalwTcYbfhSHPceFrnLwF7uGlcA9nwvI,751
35
+ haplohub_cli/formatters/__init__.py,sha256=dG5qxS0NnjKsm2zud2KROYRSvnMBPVqJt_zIEu-e8uI,123
36
+ haplohub_cli/formatters/cohort.py,sha256=HSBIknvDLOTOZXSiJn_fQoC52znRCnIzHpkvCrfl2LE,1180
37
+ haplohub_cli/formatters/config.py,sha256=A4CRpsuVx6n5E4CROkGS8jq-mmA7VXBVLN6sI1agIXY,377
38
+ haplohub_cli/formatters/decorators.py,sha256=FQ-l8wt6nfsYxRWE-rMLN_aDyEiCk8zr6WpnkJVCpek,233
39
+ haplohub_cli/formatters/file.py,sha256=yTcMQONhDvl2eMzpw__siy3IRhnheqagFbZxezEUKr4,1369
40
+ haplohub_cli/formatters/formatter_registry.py,sha256=InKpKGJQs46QW4bdVTdRs25k92ufD8pHdK4nxa6JHK4,530
41
+ haplohub_cli/formatters/generic.py,sha256=-kELlDLyzDQOYnrQAXVmFr00JPbBmucxRqZsYzHIU_A,284
42
+ haplohub_cli/formatters/model.py,sha256=COyjwTnI46ZHVKk9KlzdooUj4KbcW4r6tj6qa86eJd0,902
43
+ haplohub_cli/formatters/utils.py,sha256=DKGHRWkfcCE83RX2ftVJ5h_lTPgV4fb-bdcowapopdw,277
44
+ haplohub_cli-0.0.1.dist-info/METADATA,sha256=YgbP86smkVB6YylC-u-QhzgI_uXU8PhzTx0EosnCZIw,1375
45
+ haplohub_cli-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ haplohub_cli-0.0.1.dist-info/entry_points.txt,sha256=dXlR_dnajZR8kZtq_a7nhsQE2--qoogM0InAfLJPn74,50
47
+ haplohub_cli-0.0.1.dist-info/top_level.txt,sha256=aGE0jcVMW4ia9SkOxCcv44UP0jhEQp_tOYScfUtbesU,13
48
+ haplohub_cli-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ haplohub = haplohub_cli.cli:cli
@@ -0,0 +1 @@
1
+ haplohub_cli