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.
- haplohub_cli/__init__.py +3 -0
- haplohub_cli/_version.py +21 -0
- haplohub_cli/auth/__init__.py +0 -0
- haplohub_cli/auth/auth.py +42 -0
- haplohub_cli/auth/auth0_client.py +104 -0
- haplohub_cli/auth/auth_web_server.py +43 -0
- haplohub_cli/auth/tests/__init__.py +0 -0
- haplohub_cli/auth/tests/test_auth_web_server.py +22 -0
- haplohub_cli/auth/tests/test_token_storage.py +43 -0
- haplohub_cli/auth/token_storage.py +27 -0
- haplohub_cli/cli.py +35 -0
- haplohub_cli/commands/__init__.py +0 -0
- haplohub_cli/commands/cohort.py +33 -0
- haplohub_cli/commands/config.py +40 -0
- haplohub_cli/commands/file.py +84 -0
- haplohub_cli/commands/login.py +17 -0
- haplohub_cli/commands/model/__init__.py +0 -0
- haplohub_cli/commands/model/model.py +62 -0
- haplohub_cli/commands/model/run.py +63 -0
- haplohub_cli/commands/version.py +11 -0
- haplohub_cli/config/__init__.py +0 -0
- haplohub_cli/config/config.py +13 -0
- haplohub_cli/config/config_manager.py +57 -0
- haplohub_cli/config/environments.py +20 -0
- haplohub_cli/core/__init__.py +7 -0
- haplohub_cli/core/api/__init__.py +0 -0
- haplohub_cli/core/api/client.py +45 -0
- haplohub_cli/core/checksum.py +6 -0
- haplohub_cli/core/network.py +6 -0
- haplohub_cli/core/slug.py +7 -0
- haplohub_cli/core/tests/__init__.py +0 -0
- haplohub_cli/core/tests/test_network.py +20 -0
- haplohub_cli/core/tests/test_slug.py +20 -0
- haplohub_cli/formatters/__init__.py +6 -0
- haplohub_cli/formatters/cohort.py +39 -0
- haplohub_cli/formatters/config.py +15 -0
- haplohub_cli/formatters/decorators.py +9 -0
- haplohub_cli/formatters/file.py +46 -0
- haplohub_cli/formatters/formatter_registry.py +21 -0
- haplohub_cli/formatters/generic.py +11 -0
- haplohub_cli/formatters/model.py +31 -0
- haplohub_cli/formatters/utils.py +13 -0
- haplohub_cli/settings.py +24 -0
- haplohub_cli-0.0.1.dist-info/METADATA +61 -0
- haplohub_cli-0.0.1.dist-info/RECORD +48 -0
- haplohub_cli-0.0.1.dist-info/WHEEL +5 -0
- haplohub_cli-0.0.1.dist-info/entry_points.txt +2 -0
- haplohub_cli-0.0.1.dist-info/top_level.txt +1 -0
haplohub_cli/__init__.py
ADDED
haplohub_cli/_version.py
ADDED
|
@@ -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
|
|
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
|
+
}
|
|
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()
|
|
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,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,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] + "..."
|
haplohub_cli/settings.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
haplohub_cli
|