cz-cli 0.1.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.
cz_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,265 @@
1
+ """clickzetta profile command — manage connection profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+ from cz_cli import output
12
+ from cz_cli.logger import log_operation
13
+
14
+ try:
15
+ import tomllib
16
+ except ImportError:
17
+ import tomli as tomllib # type: ignore
18
+
19
+
20
+ _PROFILES_DIR = Path.home() / ".clickzetta"
21
+ _PROFILES_FILE = _PROFILES_DIR / "profiles.toml"
22
+
23
+
24
+ def _load_profiles() -> dict[str, Any]:
25
+ """Load profiles from ~/.clickzetta/profiles.toml"""
26
+ if not _PROFILES_FILE.exists():
27
+ return {"profiles": {}}
28
+
29
+ try:
30
+ with open(_PROFILES_FILE, "rb") as f:
31
+ return tomllib.load(f)
32
+ except Exception as exc:
33
+ return {"profiles": {}}
34
+
35
+
36
+ def _save_profiles(data: dict[str, Any]) -> None:
37
+ """Save profiles to ~/.clickzetta/profiles.toml"""
38
+ _PROFILES_DIR.mkdir(parents=True, exist_ok=True)
39
+
40
+ # Convert to TOML format
41
+ lines = []
42
+
43
+ # Add default_profile at the top if it exists
44
+ if "default_profile" in data:
45
+ lines.append(f'default_profile = "{data["default_profile"]}"')
46
+ lines.append("")
47
+
48
+ for profile_name, profile_data in data.get("profiles", {}).items():
49
+ lines.append(f"[profiles.{profile_name}]")
50
+ for key, value in profile_data.items():
51
+ if isinstance(value, str):
52
+ lines.append(f'{key} = "{value}"')
53
+ else:
54
+ lines.append(f"{key} = {value}")
55
+ lines.append("")
56
+
57
+ with open(_PROFILES_FILE, "w", encoding="utf-8") as f:
58
+ f.write("\n".join(lines))
59
+
60
+
61
+ @click.group("profile")
62
+ @click.pass_context
63
+ def profile_cmd(ctx: click.Context) -> None:
64
+ """Manage connection profiles."""
65
+
66
+
67
+ @profile_cmd.command("list")
68
+ @click.pass_context
69
+ def list_profiles(ctx: click.Context) -> None:
70
+ """List all configured profiles."""
71
+ fmt: str = ctx.obj["format"]
72
+
73
+ try:
74
+ data = _load_profiles()
75
+ profiles = data.get("profiles", {})
76
+ default_profile = data.get("default_profile")
77
+
78
+ result = []
79
+ for name, profile_data in profiles.items():
80
+ result.append({
81
+ "name": name,
82
+ "username": profile_data.get("username", ""),
83
+ "service": profile_data.get("service", ""),
84
+ "instance": profile_data.get("instance", ""),
85
+ "workspace": profile_data.get("workspace", ""),
86
+ "is_default": name == default_profile,
87
+ })
88
+
89
+ log_operation("profile list", ok=True)
90
+ output.success(result, fmt=fmt)
91
+ except Exception as exc:
92
+ log_operation("profile list", ok=False, error_code="INTERNAL_ERROR")
93
+ output.error("INTERNAL_ERROR", str(exc), fmt=fmt)
94
+
95
+
96
+ @profile_cmd.command("create")
97
+ @click.argument("name")
98
+ @click.option("--username", required=True, help="Username")
99
+ @click.option("--password", required=True, help="Password")
100
+ @click.option("--service", default="dev-api.clickzetta.com", help="Service endpoint")
101
+ @click.option("--instance", required=True, help="Instance ID")
102
+ @click.option("--workspace", required=True, help="Workspace name")
103
+ @click.option("--schema", default="public", help="Default schema")
104
+ @click.option("--vcluster", default="default", help="Virtual cluster")
105
+ @click.option("--skip-verify", is_flag=True, help="Skip connection verification")
106
+ @click.pass_context
107
+ def create_profile(
108
+ ctx: click.Context,
109
+ name: str,
110
+ username: str,
111
+ password: str,
112
+ service: str,
113
+ instance: str,
114
+ workspace: str,
115
+ schema: str,
116
+ vcluster: str,
117
+ skip_verify: bool,
118
+ ) -> None:
119
+ """Create a new profile."""
120
+ fmt: str = ctx.obj["format"]
121
+
122
+ try:
123
+ data = _load_profiles()
124
+ profiles = data.get("profiles", {})
125
+
126
+ if name in profiles:
127
+ log_operation("profile create", ok=False, error_code="PROFILE_EXISTS")
128
+ output.error("PROFILE_EXISTS", f"Profile '{name}' already exists", fmt=fmt)
129
+ return
130
+
131
+ # Verify connection unless --skip-verify
132
+ if not skip_verify:
133
+ from cz_cli.connection import get_connection
134
+ try:
135
+ conn = get_connection(
136
+ username=username,
137
+ password=password,
138
+ service=service,
139
+ instance=instance,
140
+ workspace=workspace,
141
+ schema=schema,
142
+ vcluster=vcluster,
143
+ )
144
+ # Test connection
145
+ cursor = conn.cursor()
146
+ cursor.execute("SELECT 1")
147
+ cursor.close()
148
+ conn.close()
149
+ except Exception as exc:
150
+ log_operation("profile create", ok=False, error_code="CONNECTION_FAILED")
151
+ output.error(
152
+ "CONNECTION_FAILED",
153
+ f"Failed to connect with provided credentials: {str(exc)}",
154
+ fmt=fmt,
155
+ )
156
+ return
157
+
158
+ profiles[name] = {
159
+ "username": username,
160
+ "password": password,
161
+ "service": service,
162
+ "instance": instance,
163
+ "workspace": workspace,
164
+ "schema": schema,
165
+ "vcluster": vcluster,
166
+ }
167
+
168
+ data["profiles"] = profiles
169
+ _save_profiles(data)
170
+
171
+ log_operation("profile create", ok=True)
172
+ output.success({"message": f"Profile '{name}' created successfully"}, fmt=fmt)
173
+ except Exception as exc:
174
+ log_operation("profile create", ok=False, error_code="INTERNAL_ERROR")
175
+ output.error("INTERNAL_ERROR", str(exc), fmt=fmt)
176
+
177
+
178
+ @profile_cmd.command("update")
179
+ @click.argument("name")
180
+ @click.argument("key")
181
+ @click.argument("value")
182
+ @click.pass_context
183
+ def update_profile(ctx: click.Context, name: str, key: str, value: str) -> None:
184
+ """Update a profile field."""
185
+ fmt: str = ctx.obj["format"]
186
+
187
+ try:
188
+ data = _load_profiles()
189
+ profiles = data.get("profiles", {})
190
+
191
+ if name not in profiles:
192
+ log_operation("profile update", ok=False, error_code="PROFILE_NOT_FOUND")
193
+ output.error("PROFILE_NOT_FOUND", f"Profile '{name}' not found", fmt=fmt)
194
+ return
195
+
196
+ valid_keys = ["username", "password", "service", "instance", "workspace", "schema", "vcluster"]
197
+ if key not in valid_keys:
198
+ log_operation("profile update", ok=False, error_code="INVALID_KEY")
199
+ output.error("INVALID_KEY", f"Invalid key '{key}'. Valid keys: {', '.join(valid_keys)}", fmt=fmt)
200
+ return
201
+
202
+ profiles[name][key] = value
203
+ data["profiles"] = profiles
204
+ _save_profiles(data)
205
+
206
+ log_operation("profile update", ok=True)
207
+ output.success({"message": f"Profile '{name}' updated successfully"}, fmt=fmt)
208
+ except Exception as exc:
209
+ log_operation("profile update", ok=False, error_code="INTERNAL_ERROR")
210
+ output.error("INTERNAL_ERROR", str(exc), fmt=fmt)
211
+
212
+
213
+ @profile_cmd.command("delete")
214
+ @click.argument("name")
215
+ @click.pass_context
216
+ def delete_profile(ctx: click.Context, name: str) -> None:
217
+ """Delete a profile."""
218
+ fmt: str = ctx.obj["format"]
219
+
220
+ try:
221
+ data = _load_profiles()
222
+ profiles = data.get("profiles", {})
223
+
224
+ if name not in profiles:
225
+ log_operation("profile delete", ok=False, error_code="PROFILE_NOT_FOUND")
226
+ output.error("PROFILE_NOT_FOUND", f"Profile '{name}' not found", fmt=fmt)
227
+ return
228
+
229
+ del profiles[name]
230
+ data["profiles"] = profiles
231
+ _save_profiles(data)
232
+
233
+ log_operation("profile delete", ok=True)
234
+ output.success({"message": f"Profile '{name}' deleted successfully"}, fmt=fmt)
235
+ except Exception as exc:
236
+ log_operation("profile delete", ok=False, error_code="INTERNAL_ERROR")
237
+ output.error("INTERNAL_ERROR", str(exc), fmt=fmt)
238
+
239
+
240
+ @profile_cmd.command("use")
241
+ @click.argument("name")
242
+ @click.pass_context
243
+ def use_profile(ctx: click.Context, name: str) -> None:
244
+ """Set a profile as default."""
245
+ fmt: str = ctx.obj["format"]
246
+
247
+ try:
248
+ data = _load_profiles()
249
+ profiles = data.get("profiles", {})
250
+
251
+ if name not in profiles:
252
+ log_operation("profile use", ok=False, error_code="PROFILE_NOT_FOUND")
253
+ output.error("PROFILE_NOT_FOUND", f"Profile '{name}' not found", fmt=fmt)
254
+ return
255
+
256
+ # Set default_profile marker
257
+ data["default_profile"] = name
258
+
259
+ _save_profiles(data)
260
+
261
+ log_operation("profile use", ok=True)
262
+ output.success({"message": f"Profile '{name}' set as default"}, fmt=fmt)
263
+ except Exception as exc:
264
+ log_operation("profile use", ok=False, error_code="INTERNAL_ERROR")
265
+ output.error("INTERNAL_ERROR", str(exc), fmt=fmt)
@@ -0,0 +1,213 @@
1
+ """clickzetta schema command — manage schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import click
8
+
9
+ from cz_cli import output
10
+ from cz_cli.connection import get_connection
11
+ from cz_cli.logger import log_operation
12
+
13
+
14
+ @click.group("schema")
15
+ @click.pass_context
16
+ def schema_cmd(ctx: click.Context) -> None:
17
+ """Manage schemas."""
18
+
19
+
20
+ @schema_cmd.command("list")
21
+ @click.option("--like", help="Filter schemas by pattern (e.g. 'test%').")
22
+ @click.option("--limit", default=100, help="Maximum number of schemas to return (default: 100).")
23
+ @click.pass_context
24
+ def list_schemas(ctx: click.Context, like: str | None, limit: int) -> None:
25
+ """List all schemas in the current workspace."""
26
+ fmt: str = ctx.obj["format"]
27
+ profile: str | None = ctx.obj.get("profile")
28
+ jdbc_url: str | None = ctx.obj.get("jdbc_url")
29
+
30
+ try:
31
+ conn = get_connection(jdbc_url=jdbc_url, profile=profile)
32
+ except Exception as exc:
33
+ log_operation("schema list", ok=False, error_code="CONNECTION_ERROR")
34
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
35
+ return
36
+
37
+ timer = output.Timer()
38
+ try:
39
+ with timer:
40
+ cursor = conn.cursor()
41
+ try:
42
+ sql = "SHOW SCHEMAS"
43
+ if like:
44
+ sql += f" LIKE '{like}'"
45
+ # Note: SHOW SCHEMAS does not support LIMIT clause, we'll limit client-side
46
+
47
+ cursor.execute(sql)
48
+ rows = cursor.fetchall()
49
+
50
+ # Convert tuples to dicts
51
+ columns = [d[0] for d in cursor.description] if cursor.description else []
52
+ rows_dict = [dict(zip(columns, row)) if isinstance(row, tuple) else row for row in rows]
53
+
54
+ result = []
55
+ for row in rows_dict:
56
+ result.append({
57
+ "name": row.get("schema_name", row.get("name", row.get(columns[0], "") if columns else "")),
58
+ "type": row.get("type", ""),
59
+ })
60
+
61
+ # Client-side limit (SHOW SCHEMAS doesn't support LIMIT clause)
62
+ total_count = len(result)
63
+ if total_count > limit:
64
+ result = result[:limit]
65
+
66
+ # Add AI message if results were limited
67
+ ai_msg = None
68
+ if total_count > limit:
69
+ ai_msg = f"Results limited to {limit} of {total_count} schemas. Use --limit to adjust or --like to filter."
70
+
71
+ log_operation("schema list", ok=True, time_ms=timer.elapsed_ms)
72
+ output.success(result, time_ms=timer.elapsed_ms, fmt=fmt, ai_message=ai_msg)
73
+ finally:
74
+ cursor.close()
75
+ except Exception as exc:
76
+ log_operation("schema list", ok=False, error_code="SQL_ERROR")
77
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
78
+ finally:
79
+ conn.close()
80
+
81
+
82
+ @schema_cmd.command("describe")
83
+ @click.argument("name")
84
+ @click.pass_context
85
+ def describe_schema(ctx: click.Context, name: str) -> None:
86
+ """Show schema details including tables."""
87
+ fmt: str = ctx.obj["format"]
88
+ profile: str | None = ctx.obj.get("profile")
89
+ jdbc_url: str | None = ctx.obj.get("jdbc_url")
90
+
91
+ try:
92
+ conn = get_connection(jdbc_url=jdbc_url, profile=profile)
93
+ except Exception as exc:
94
+ log_operation("schema describe", ok=False, error_code="CONNECTION_ERROR")
95
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
96
+ return
97
+
98
+ timer = output.Timer()
99
+ try:
100
+ with timer:
101
+ cursor = conn.cursor()
102
+ try:
103
+ # Get schema info
104
+ cursor.execute(f"SHOW SCHEMAS EXTENDED WHERE schema_name='{name}'")
105
+ schema_rows = cursor.fetchall()
106
+
107
+ if not schema_rows:
108
+ log_operation("schema describe", ok=False, error_code="SCHEMA_NOT_FOUND")
109
+ output.error("SCHEMA_NOT_FOUND", f"Schema '{name}' not found", fmt=fmt)
110
+ return
111
+
112
+ # Convert tuple to dict
113
+ columns = [d[0] for d in cursor.description] if cursor.description else []
114
+ schema_info = dict(zip(columns, schema_rows[0])) if isinstance(schema_rows[0], tuple) else schema_rows[0]
115
+
116
+ # Get tables in schema
117
+ cursor.execute(f"SHOW TABLES IN {name}")
118
+ table_rows = cursor.fetchall()
119
+
120
+ # Convert tuples to dicts
121
+ table_columns = [d[0] for d in cursor.description] if cursor.description else []
122
+ tables = []
123
+ for r in table_rows:
124
+ if isinstance(r, tuple):
125
+ row_dict = dict(zip(table_columns, r))
126
+ tables.append(row_dict.get(table_columns[0], "") if table_columns else "")
127
+ elif isinstance(r, dict):
128
+ tables.append(list(r.values())[0] if r else "")
129
+ else:
130
+ tables.append(str(r))
131
+
132
+ result: dict[str, Any] = {
133
+ "name": name,
134
+ "type": schema_info.get("type", ""),
135
+ "table_count": len(tables),
136
+ "tables": tables,
137
+ }
138
+
139
+ log_operation("schema describe", ok=True, time_ms=timer.elapsed_ms)
140
+ output.success(result, time_ms=timer.elapsed_ms, fmt=fmt)
141
+ finally:
142
+ cursor.close()
143
+ except Exception as exc:
144
+ log_operation("schema describe", ok=False, error_code="SQL_ERROR")
145
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
146
+ finally:
147
+ conn.close()
148
+
149
+
150
+ @schema_cmd.command("create")
151
+ @click.argument("name")
152
+ @click.pass_context
153
+ def create_schema(ctx: click.Context, name: str) -> None:
154
+ """Create a new schema."""
155
+ fmt: str = ctx.obj["format"]
156
+ profile: str | None = ctx.obj.get("profile")
157
+ jdbc_url: str | None = ctx.obj.get("jdbc_url")
158
+
159
+ try:
160
+ conn = get_connection(jdbc_url=jdbc_url, profile=profile)
161
+ except Exception as exc:
162
+ log_operation("schema create", ok=False, error_code="CONNECTION_ERROR")
163
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
164
+ return
165
+
166
+ timer = output.Timer()
167
+ try:
168
+ with timer:
169
+ cursor = conn.cursor()
170
+ try:
171
+ cursor.execute(f"CREATE SCHEMA {name}")
172
+ log_operation("schema create", ok=True, time_ms=timer.elapsed_ms)
173
+ output.success({"message": f"Schema '{name}' created successfully"}, time_ms=timer.elapsed_ms, fmt=fmt)
174
+ finally:
175
+ cursor.close()
176
+ except Exception as exc:
177
+ log_operation("schema create", ok=False, error_code="SQL_ERROR")
178
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
179
+ finally:
180
+ conn.close()
181
+
182
+
183
+ @schema_cmd.command("drop")
184
+ @click.argument("name")
185
+ @click.pass_context
186
+ def drop_schema(ctx: click.Context, name: str) -> None:
187
+ """Drop a schema."""
188
+ fmt: str = ctx.obj["format"]
189
+ profile: str | None = ctx.obj.get("profile")
190
+ jdbc_url: str | None = ctx.obj.get("jdbc_url")
191
+
192
+ try:
193
+ conn = get_connection(jdbc_url=jdbc_url, profile=profile)
194
+ except Exception as exc:
195
+ log_operation("schema drop", ok=False, error_code="CONNECTION_ERROR")
196
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
197
+ return
198
+
199
+ timer = output.Timer()
200
+ try:
201
+ with timer:
202
+ cursor = conn.cursor()
203
+ try:
204
+ cursor.execute(f"DROP SCHEMA {name}")
205
+ log_operation("schema drop", ok=True, time_ms=timer.elapsed_ms)
206
+ output.success({"message": f"Schema '{name}' dropped successfully"}, time_ms=timer.elapsed_ms, fmt=fmt)
207
+ finally:
208
+ cursor.close()
209
+ except Exception as exc:
210
+ log_operation("schema drop", ok=False, error_code="SQL_ERROR")
211
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
212
+ finally:
213
+ conn.close()