omicslab 1.0.0__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.
omicslab/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from .client import Client
2
+ from .config import Config
3
+ from .exceptions import (
4
+ OmicslabError,
5
+ AuthError,
6
+ APIError,
7
+ NotFoundError,
8
+ ForbiddenError,
9
+ )
10
+ from ._version import __version__
11
+
12
+ __all__ = [
13
+ "Client",
14
+ "Config",
15
+ "OmicslabError",
16
+ "AuthError",
17
+ "APIError",
18
+ "NotFoundError",
19
+ "ForbiddenError",
20
+ "__version__",
21
+ ]
omicslab/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
File without changes
@@ -0,0 +1,141 @@
1
+ from typing import Optional
2
+
3
+ import typer
4
+
5
+ from .utils import success, error, output, get_client
6
+
7
+ app = typer.Typer(help="Analysis tool catalog")
8
+
9
+
10
+ @app.command(name="list")
11
+ def list_analysis(
12
+ json_output: bool = typer.Option(False, "--json"),
13
+ ):
14
+ """List analysis tools."""
15
+ client = get_client()
16
+ try:
17
+ result = client.analysis.list()
18
+ output(result, format="json" if json_output else "table")
19
+ except Exception as e:
20
+ error(f"Failed: {e}")
21
+
22
+
23
+ @app.command()
24
+ def create(
25
+ url: str = typer.Option(
26
+ ..., "--url", "-u", help="Git repository URL of the analysis tool"
27
+ ),
28
+ ):
29
+ """Register a new analysis tool from a Git repository."""
30
+ client = get_client()
31
+ try:
32
+ result = client.analysis.create(url=url)
33
+ success(
34
+ f"Analysis tool '{result.get('name', result.get('id', 'unknown'))}' registered."
35
+ )
36
+ output(result)
37
+ except Exception as e:
38
+ error(f"Failed: {e}")
39
+
40
+
41
+ @app.command()
42
+ def get(
43
+ analysis_id: str = typer.Argument(..., help="Analysis tool ID"),
44
+ ):
45
+ """Get analysis tool details."""
46
+ client = get_client()
47
+ try:
48
+ result = client.analysis.get(analysis_id)
49
+ output(result)
50
+ except Exception as e:
51
+ error(f"Failed: {e}")
52
+
53
+
54
+ @app.command()
55
+ def update(
56
+ analysis_id: str = typer.Argument(..., help="Analysis tool ID"),
57
+ url: Optional[str] = typer.Option(None, "--url", "-u", help="New Git URL"),
58
+ allow_access: Optional[bool] = typer.Option(
59
+ None, "--allow-access/--deny-access", help="Allow/deny access"
60
+ ),
61
+ ):
62
+ """Update an analysis tool."""
63
+ client = get_client()
64
+ try:
65
+ result = client.analysis.update(analysis_id, url=url, allow_access=allow_access)
66
+ success("Analysis tool updated.")
67
+ output(result)
68
+ except Exception as e:
69
+ error(f"Failed: {e}")
70
+
71
+
72
+ @app.command()
73
+ def delete(
74
+ analysis_id: str = typer.Argument(..., help="Analysis tool ID"),
75
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
76
+ ):
77
+ """Remove an analysis tool."""
78
+ if not force:
79
+ confirm = typer.confirm(f"Delete analysis tool {analysis_id}?")
80
+ if not confirm:
81
+ raise typer.Abort()
82
+ client = get_client()
83
+ try:
84
+ client.analysis.delete(analysis_id)
85
+ success("Analysis tool deleted.")
86
+ except Exception as e:
87
+ error(f"Failed: {e}")
88
+
89
+
90
+ @app.command()
91
+ def pin(
92
+ analysis_id: str = typer.Argument(..., help="Analysis tool ID"),
93
+ ):
94
+ """Pin an analysis tool to favorites."""
95
+ client = get_client()
96
+ try:
97
+ client.analysis.pin(analysis_id)
98
+ success("Analysis tool pinned.")
99
+ except Exception as e:
100
+ error(f"Failed: {e}")
101
+
102
+
103
+ @app.command()
104
+ def unpin(
105
+ analysis_id: str = typer.Argument(..., help="Analysis tool ID"),
106
+ ):
107
+ """Unpin an analysis tool from favorites."""
108
+ client = get_client()
109
+ try:
110
+ client.analysis.unpin(analysis_id)
111
+ success("Analysis tool unpinned.")
112
+ except Exception as e:
113
+ error(f"Failed: {e}")
114
+
115
+
116
+ @app.command()
117
+ def schema(
118
+ analysis_id: str = typer.Argument(..., help="Analysis tool ID"),
119
+ json_output: bool = typer.Option(False, "--json"),
120
+ ):
121
+ """Show the parameter schema of an analysis tool."""
122
+ client = get_client()
123
+ try:
124
+ result = client.analysis.get_schema(analysis_id)
125
+ output(result, format="json" if json_output else "table")
126
+ except Exception as e:
127
+ error(f"Failed: {e}")
128
+
129
+
130
+ @app.command()
131
+ def tags(
132
+ analysis_id: str = typer.Argument(..., help="Analysis tool ID"),
133
+ json_output: bool = typer.Option(False, "--json"),
134
+ ):
135
+ """Show tags of an analysis tool."""
136
+ client = get_client()
137
+ try:
138
+ result = client.analysis.get_tags(analysis_id)
139
+ output(result, format="json" if json_output else "table")
140
+ except Exception as e:
141
+ error(f"Failed: {e}")
@@ -0,0 +1,45 @@
1
+ import typer
2
+
3
+ from ..config import Config
4
+ from .utils import console, success, error, get_client
5
+
6
+ app = typer.Typer(help="Authentication management")
7
+
8
+
9
+ @app.command()
10
+ def login(
11
+ token: str = typer.Option(..., "--token", "-t", help="API token (omx_...)"),
12
+ base_url: str = typer.Option(
13
+ "https://platform.omicslab.io/api", "--base-url", help="Platform API base URL"
14
+ ),
15
+ ):
16
+ """Log in with an API token."""
17
+ if not token.startswith("omx_"):
18
+ error("Token must start with 'omx_'")
19
+ raise typer.Exit(1)
20
+
21
+ config = Config()
22
+ config.set_token(token)
23
+ config.set_base_url(base_url)
24
+ success(f"Logged in. Token stored in {config._config_path}")
25
+
26
+
27
+ @app.command()
28
+ def logout():
29
+ """Log out by removing the stored token."""
30
+ config = Config()
31
+ config.clear_token()
32
+ success("Logged out. Token removed.")
33
+
34
+
35
+ @app.command()
36
+ def whoami():
37
+ """Show the currently authenticated user (requires a valid token)."""
38
+ client = get_client()
39
+ try:
40
+ res = client._request("GET", "/auth/check_access_token")
41
+ console.print(
42
+ f"[green]Authenticated as:[/green] [bold]{res.get('email', 'unknown')}[/bold]"
43
+ )
44
+ except Exception as e:
45
+ error(f"Not authenticated: {e}")
@@ -0,0 +1,137 @@
1
+ from typing import Optional
2
+
3
+ import typer
4
+
5
+ from ..config import Config
6
+ from .utils import (
7
+ console,
8
+ info,
9
+ success,
10
+ get_client,
11
+ select_org,
12
+ select_workspace,
13
+ select_prefix,
14
+ )
15
+
16
+ app = typer.Typer(help="Set active context (org, workspace, folder)")
17
+
18
+
19
+ @app.callback(invoke_without_command=True)
20
+ def use(
21
+ ctx: typer.Context,
22
+ org_id: Optional[str] = typer.Option(
23
+ None, "--org-id", "-o", help="Organization ID"
24
+ ),
25
+ workspace_id: Optional[str] = typer.Option(
26
+ None, "--workspace-id", "-w", help="Workspace ID"
27
+ ),
28
+ prefix: Optional[str] = typer.Option(
29
+ None, "--prefix", "-p", help="Default storage prefix"
30
+ ),
31
+ ):
32
+ """Set the active org, workspace, and folder.
33
+
34
+ Run without arguments for interactive selection.
35
+ """
36
+ if ctx.invoked_subcommand is not None:
37
+ return
38
+ if org_id or workspace_id or prefix:
39
+ _set_from_args(org_id, workspace_id, prefix)
40
+ else:
41
+ _set_interactive()
42
+
43
+
44
+ def _set_from_args(
45
+ org_id: Optional[str],
46
+ workspace_id: Optional[str],
47
+ prefix: Optional[str],
48
+ ):
49
+ config = Config()
50
+ if org_id:
51
+ config.set_org(org_id)
52
+ info(f"Org set to {org_id}")
53
+ if workspace_id:
54
+ config.set_workspace(workspace_id)
55
+ info(f"Workspace set to {workspace_id}")
56
+ if prefix is not None:
57
+ config.set_prefix(prefix)
58
+ info(f"Prefix set to {prefix or '(none)'}")
59
+ _show_context(config)
60
+
61
+
62
+ def _set_interactive():
63
+ client = get_client()
64
+ config = Config()
65
+
66
+ console.print("[bold]Select organization:[/bold]")
67
+ org_id, org_name = select_org(client)
68
+ config.set_org(org_id, org_name)
69
+ success(f"Org set to {org_name}")
70
+
71
+ console.print("[bold]Select workspace:[/bold]")
72
+ ws_id, ws_name = select_workspace(client)
73
+ config.set_workspace(ws_id, ws_name)
74
+ success(f"Workspace set to {ws_name}")
75
+
76
+ console.print("[bold]Select folder (optional):[/bold]")
77
+ chosen_prefix = select_prefix(client, ws_id)
78
+ if chosen_prefix:
79
+ config.set_prefix(chosen_prefix)
80
+ info(f"Prefix set to {chosen_prefix}")
81
+ else:
82
+ config.clear_prefix()
83
+
84
+ _show_context(config)
85
+
86
+
87
+ @app.command()
88
+ def show():
89
+ """Show current context."""
90
+ _show_context(Config())
91
+
92
+
93
+ def _show_context(config: Config):
94
+ org_id = config.get_org_id()
95
+ ws_id = config.get_workspace_id()
96
+ prefix = config.get_prefix()
97
+ profile = config.current_profile
98
+
99
+ from .utils import print_panel
100
+
101
+ print_panel(
102
+ f"Active Context (profile: {profile})",
103
+ {
104
+ "org_id": org_id or "[dim]not set[/dim]",
105
+ "workspace_id": ws_id or "[dim]not set[/dim]",
106
+ "prefix": "/" if prefix == "" else (prefix or "[dim]not set[/dim]"),
107
+ },
108
+ )
109
+
110
+
111
+ @app.command()
112
+ def clear(
113
+ org: bool = typer.Option(False, "--org", help="Clear org context"),
114
+ workspace: bool = typer.Option(
115
+ False, "--workspace", "-w", help="Clear workspace context"
116
+ ),
117
+ prefix: bool = typer.Option(False, "--prefix", "-p", help="Clear prefix"),
118
+ all: bool = typer.Option(False, "--all", "-a", help="Clear all context"),
119
+ ):
120
+ """Clear part or all of the active context."""
121
+ config = Config()
122
+ if all:
123
+ config.clear_org()
124
+ config.clear_workspace()
125
+ config.clear_prefix()
126
+ success("All context cleared.")
127
+ else:
128
+ if org:
129
+ config.clear_org()
130
+ success("Org context cleared.")
131
+ if workspace:
132
+ config.clear_workspace()
133
+ success("Workspace context cleared.")
134
+ if prefix:
135
+ config.clear_prefix()
136
+ success("Prefix cleared.")
137
+ _show_context(config)
@@ -0,0 +1,168 @@
1
+ import json
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from ..config import Config
7
+ from .utils import success, error, output, get_client, resolve_workspace
8
+
9
+ app = typer.Typer(help="Job management (analysis and studio)")
10
+
11
+
12
+ @app.command(name="launch")
13
+ def launch(
14
+ workspace_id: Optional[str] = typer.Option(
15
+ None, "--workspace", "-w", help="Workspace ID (uses stored context if omitted)"
16
+ ),
17
+ analysis_id: Optional[str] = typer.Option(
18
+ None, "--analysis", "-a", help="Analysis tool ID"
19
+ ),
20
+ studio_id: Optional[str] = typer.Option(
21
+ None, "--studio", "-s", help="Studio tool ID"
22
+ ),
23
+ compute_id: str = typer.Option(
24
+ ..., "--compute", "-c", help="Compute credential ID"
25
+ ),
26
+ params: Optional[str] = typer.Option(
27
+ "{}", "--params", "-p", help="Job params as JSON"
28
+ ),
29
+ ):
30
+ """Launch an analysis or studio job."""
31
+ if not analysis_id and not studio_id:
32
+ error("Must specify --analysis or --studio")
33
+ raise typer.Exit(1)
34
+ if analysis_id and studio_id:
35
+ error("Cannot specify both --analysis and --studio")
36
+ raise typer.Exit(1)
37
+
38
+ client = get_client()
39
+ ws = resolve_workspace(client, Config(), workspace_id)
40
+ try:
41
+ params_dict = json.loads(params) if params else {}
42
+
43
+ if analysis_id:
44
+ result = client.jobs.launch_analysis(
45
+ workspace_id=ws,
46
+ analysis_id=analysis_id,
47
+ compute_id=compute_id,
48
+ params=params_dict,
49
+ )
50
+ job_type = "analysis"
51
+ else:
52
+ result = client.jobs.launch_studio(
53
+ workspace_id=ws,
54
+ studio_id=studio_id,
55
+ compute_id=compute_id,
56
+ params=params_dict,
57
+ )
58
+ job_type = "studio"
59
+
60
+ success(f"{job_type.capitalize()} job launched: {result.get('id', result)}")
61
+ output(result, format="table")
62
+ except Exception as e:
63
+ error(f"Failed: {e}")
64
+
65
+
66
+ @app.command(name="list")
67
+ def list_jobs(
68
+ workspace_id: Optional[str] = typer.Option(
69
+ None, "--workspace", "-w", help="Workspace ID (uses stored context if omitted)"
70
+ ),
71
+ job_type: str = typer.Option(
72
+ "analysis", "--type", "-t", help="Job type: analysis or studio"
73
+ ),
74
+ json_output: bool = typer.Option(False, "--json"),
75
+ ):
76
+ """List jobs in a workspace."""
77
+ client = get_client()
78
+ ws = resolve_workspace(client, Config(), workspace_id)
79
+ try:
80
+ if job_type == "analysis":
81
+ result = client.jobs.list_analysis(ws)
82
+ elif job_type == "studio":
83
+ result = client.jobs.list_studio(ws)
84
+ else:
85
+ error(f"Invalid job type: {job_type}")
86
+ raise typer.Exit(1)
87
+ output(result, format="json" if json_output else "table")
88
+ except Exception as e:
89
+ error(f"Failed: {e}")
90
+
91
+
92
+ @app.command(name="get")
93
+ def get_job(
94
+ workspace_id: Optional[str] = typer.Option(
95
+ None, "--workspace", "-w", help="Workspace ID (uses stored context if omitted)"
96
+ ),
97
+ job_id: str = typer.Argument(..., help="Job ID"),
98
+ job_type: str = typer.Option(
99
+ "analysis", "--type", "-t", help="Job type: analysis or studio"
100
+ ),
101
+ ):
102
+ """Get job details."""
103
+ client = get_client()
104
+ ws = resolve_workspace(client, Config(), workspace_id)
105
+ try:
106
+ if job_type == "analysis":
107
+ result = client.jobs.get_analysis(ws, job_id)
108
+ elif job_type == "studio":
109
+ result = client.jobs.get_studio(ws, job_id)
110
+ else:
111
+ error(f"Invalid job type: {job_type}")
112
+ raise typer.Exit(1)
113
+ output(result, format="table")
114
+ except Exception as e:
115
+ error(f"Failed: {e}")
116
+
117
+
118
+ @app.command(name="logs")
119
+ def get_logs(
120
+ workspace_id: Optional[str] = typer.Option(
121
+ None, "--workspace", "-w", help="Workspace ID (uses stored context if omitted)"
122
+ ),
123
+ job_id: str = typer.Argument(..., help="Job ID"),
124
+ job_type: str = typer.Option(
125
+ "analysis", "--type", "-t", help="Job type: analysis or studio"
126
+ ),
127
+ ):
128
+ """Get job logs."""
129
+ client = get_client()
130
+ ws = resolve_workspace(client, Config(), workspace_id)
131
+ try:
132
+ if job_type == "analysis":
133
+ result = client.jobs.get_analysis_log(ws, job_id)
134
+ else:
135
+ error("Studio job logs are not yet supported via REST API")
136
+ raise typer.Exit(1)
137
+ if isinstance(result, dict) and "log" in result:
138
+ print(result["log"])
139
+ else:
140
+ output(result)
141
+ except Exception as e:
142
+ error(f"Failed: {e}")
143
+
144
+
145
+ @app.command()
146
+ def terminate(
147
+ workspace_id: Optional[str] = typer.Option(
148
+ None, "--workspace", "-w", help="Workspace ID (uses stored context if omitted)"
149
+ ),
150
+ job_id: str = typer.Argument(..., help="Job ID"),
151
+ job_type: str = typer.Option(
152
+ "analysis", "--type", "-t", help="Job type: analysis or studio"
153
+ ),
154
+ ):
155
+ """Terminate a running job."""
156
+ client = get_client()
157
+ ws = resolve_workspace(client, Config(), workspace_id)
158
+ try:
159
+ if job_type == "analysis":
160
+ client.jobs.terminate_analysis(ws, job_id)
161
+ elif job_type == "studio":
162
+ client.jobs.terminate_studio(ws, job_id)
163
+ else:
164
+ error(f"Invalid job type: {job_type}")
165
+ raise typer.Exit(1)
166
+ success(f"{job_type.capitalize()} job {job_id} terminated.")
167
+ except Exception as e:
168
+ error(f"Failed: {e}")