inthub-cli 0.1.4__tar.gz → 0.1.6__tar.gz

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 (34) hide show
  1. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/PKG-INFO +1 -1
  2. inthub_cli-0.1.6/inthub/bulk/gcp_secrets.py +52 -0
  3. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/cli.py +8 -0
  4. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/bulk/__init__.py +8 -0
  5. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/bulk/manage.py +19 -8
  6. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/bulk/progress.py +6 -1
  7. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/bulk/src_sqlserver_sink_snowflake/__init__.py +9 -7
  8. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/bulk/src_sqlserver_sink_snowflake/orchestrator.py +2 -2
  9. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/connectors.py +25 -10
  10. inthub_cli-0.1.6/inthub/state.py +1 -0
  11. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/pyproject.toml +1 -1
  12. inthub_cli-0.1.4/inthub/bulk/gcp_secrets.py +0 -23
  13. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/.gitignore +0 -0
  14. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/Makefile +0 -0
  15. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/README.md +0 -0
  16. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/__init__.py +0 -0
  17. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/__main__.py +0 -0
  18. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/bulk/__init__.py +0 -0
  19. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/bulk/csv_state.py +0 -0
  20. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/bulk/ddl_converter.py +0 -0
  21. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/bulk/snowflake_client.py +0 -0
  22. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/bulk/sqlserver.py +0 -0
  23. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/client.py +0 -0
  24. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/__init__.py +0 -0
  25. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/auth.py +0 -0
  26. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/commands/bulk/src_sqlserver_sink_snowflake/connectors.py +0 -0
  27. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/inthub/config.py +0 -0
  28. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/plan.md +0 -0
  29. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/plan_bulk.md +0 -0
  30. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/tests/__init__.py +0 -0
  31. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/tests/bulk/__init__.py +0 -0
  32. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/tests/bulk/test_csv_state.py +0 -0
  33. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/tests/bulk/test_ddl_converter.py +0 -0
  34. {inthub_cli-0.1.4 → inthub_cli-0.1.6}/tests/test_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: inthub-cli
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: inthub CLI — manage your inthub.io resources from the terminal
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: google-cloud-secret-manager==2.21.0
@@ -0,0 +1,52 @@
1
+ import http.client
2
+ import json
3
+ import logging
4
+
5
+ from google.cloud import secretmanager
6
+
7
+ from inthub import state
8
+
9
+
10
+ def _enable_gcp_debug_logging() -> None:
11
+ logging.basicConfig(
12
+ level=logging.DEBUG,
13
+ format="%(asctime)s %(name)s %(levelname)s %(message)s",
14
+ )
15
+ http.client.HTTPConnection.debuglevel = 2
16
+ for name in ("urllib3", "urllib3.connectionpool", "google.auth", "google.api_core"):
17
+ logging.getLogger(name).setLevel(logging.DEBUG)
18
+
19
+
20
+ def _make_client() -> secretmanager.SecretManagerServiceClient:
21
+ if state.debug:
22
+ _enable_gcp_debug_logging()
23
+ return secretmanager.SecretManagerServiceClient(transport="rest")
24
+ return secretmanager.SecretManagerServiceClient()
25
+
26
+
27
+ def fetch_secret(project_id: str, secret_name: str) -> dict[str, str]:
28
+ client = _make_client()
29
+ secret_path = f"projects/{project_id}/secrets/{secret_name}"
30
+ versions = list(
31
+ client.list_secret_versions(
32
+ request={"parent": secret_path, "filter": "state=ENABLED"}
33
+ )
34
+ )
35
+ if not versions:
36
+ raise ValueError(f"No enabled versions found for secret '{secret_name}'")
37
+ latest = max(versions, key=lambda v: int(v.name.rsplit("/", 1)[-1]))
38
+ response = client.access_secret_version(request={"name": latest.name})
39
+ payload = response.payload.data.decode("UTF-8")
40
+
41
+ try:
42
+ data: dict[str, str] = json.loads(payload)
43
+ except json.JSONDecodeError as exc:
44
+ raise ValueError(f"Secret '{secret_name}' is not valid JSON") from exc
45
+
46
+ missing = {"username", "password"} - set(data.keys())
47
+ if missing:
48
+ raise ValueError(
49
+ f"Secret '{secret_name}' missing required fields: {', '.join(sorted(missing))}"
50
+ )
51
+
52
+ return {"username": data["username"], "password": data["password"]}
@@ -3,6 +3,7 @@ from importlib.metadata import version as pkg_version
3
3
 
4
4
  import typer
5
5
 
6
+ from inthub import state
6
7
  from inthub.commands import connectors
7
8
  from inthub.commands.auth import login
8
9
  from inthub.commands.bulk import app as bulk_app
@@ -15,6 +16,13 @@ app.add_typer(connectors.app, name="connectors")
15
16
  app.add_typer(bulk_app, name="bulk")
16
17
 
17
18
 
19
+ @app.callback()
20
+ def _root(
21
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
22
+ ) -> None:
23
+ state.debug = state.debug or debug
24
+
25
+
18
26
  @app.command("version")
19
27
  def version() -> None:
20
28
  """Show the CLI version."""
@@ -1,5 +1,6 @@
1
1
  import typer
2
2
 
3
+ from inthub import state
3
4
  from inthub.commands.bulk import manage, progress
4
5
  from inthub.commands.bulk.src_sqlserver_sink_snowflake import app as _src_sf_app
5
6
 
@@ -9,3 +10,10 @@ app.command("list")(manage.list_bulks)
9
10
  app.command("delete")(manage.delete_bulk)
10
11
  app.command("cleanup")(manage.cleanup)
11
12
  app.add_typer(_src_sf_app, name="src-sqlserver-sink-snowflake")
13
+
14
+
15
+ @app.callback()
16
+ def _bulk(
17
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
18
+ ) -> None:
19
+ state.debug = state.debug or debug
@@ -5,6 +5,7 @@ import typer
5
5
  from rich.console import Console
6
6
  from rich.table import Table
7
7
 
8
+ from inthub import state
8
9
  from inthub.bulk.csv_state import BULK_DIR, _progress_path, _state_path, load_rows
9
10
 
10
11
  console = Console()
@@ -36,8 +37,11 @@ def _delete_bulk(ulid: str) -> bool:
36
37
  return found
37
38
 
38
39
 
39
- def list_bulks() -> None:
40
+ def list_bulks(
41
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
42
+ ) -> None:
40
43
  """List all bulk runs."""
44
+ state.debug = state.debug or debug
41
45
  entries = _state_files()
42
46
  if not entries:
43
47
  console.print("No bulk runs found.")
@@ -53,7 +57,7 @@ def list_bulks() -> None:
53
57
  table.add_column("ERROR", justify="right")
54
58
  table.add_column("PENDING", justify="right")
55
59
 
56
- for ulid, state in entries:
60
+ for ulid, entry in entries:
57
61
  counts: Counter[str] = Counter()
58
62
  csv_file = _progress_path(ulid)
59
63
  if csv_file.exists():
@@ -64,10 +68,10 @@ def list_bulks() -> None:
64
68
 
65
69
  table.add_row(
66
70
  ulid,
67
- state.get("gcp_project", ""),
68
- state.get("sqlserver_source_plugin", ""),
69
- state.get("snowflake_sink_plugin", ""),
70
- state.get("tags", ""),
71
+ entry.get("gcp_project", ""),
72
+ entry.get("sqlserver_source_plugin", ""),
73
+ entry.get("snowflake_sink_plugin", ""),
74
+ entry.get("tags", ""),
71
75
  f"[green]{counts['done']}[/]" if counts["done"] else "0",
72
76
  f"[red]{counts['error']}[/]" if counts["error"] else "0",
73
77
  str(counts["pending"]),
@@ -76,8 +80,12 @@ def list_bulks() -> None:
76
80
  console.print(table)
77
81
 
78
82
 
79
- def delete_bulk(ulid: str = typer.Argument(..., help="ULID of the bulk run to delete.")) -> None:
83
+ def delete_bulk(
84
+ ulid: str = typer.Argument(..., help="ULID of the bulk run to delete."),
85
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
86
+ ) -> None:
80
87
  """Delete a bulk run (CSV + state)."""
88
+ state.debug = state.debug or debug
81
89
  if _delete_bulk(ulid):
82
90
  console.print(f"Deleted bulk run [bold]{ulid}[/].")
83
91
  else:
@@ -85,8 +93,11 @@ def delete_bulk(ulid: str = typer.Argument(..., help="ULID of the bulk run to de
85
93
  raise typer.Exit(1)
86
94
 
87
95
 
88
- def cleanup() -> None:
96
+ def cleanup(
97
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
98
+ ) -> None:
89
99
  """Delete all bulk runs."""
100
+ state.debug = state.debug or debug
90
101
  entries = _state_files()
91
102
  if not entries:
92
103
  console.print("Nothing to clean up.")
@@ -2,6 +2,7 @@ import typer
2
2
  from rich.console import Console
3
3
  from rich.table import Table
4
4
 
5
+ from inthub import state
5
6
  from inthub.bulk.csv_state import _progress_path, load_rows
6
7
 
7
8
  console = Console()
@@ -14,8 +15,12 @@ _STATUS_STYLE = {
14
15
  }
15
16
 
16
17
 
17
- def progress(ulid: str = typer.Argument(..., help="ULID of the bulk run.")) -> None:
18
+ def progress(
19
+ ulid: str = typer.Argument(..., help="ULID of the bulk run."),
20
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
21
+ ) -> None:
18
22
  """Show the current status of a bulk provisioning run."""
23
+ state.debug = state.debug or debug
19
24
  path = _progress_path(ulid)
20
25
  if not path.exists():
21
26
  console.print(f"[bold red]File not found:[/] {path}")
@@ -4,7 +4,7 @@ from typing import Annotated
4
4
  import typer
5
5
  from ulid import ULID
6
6
 
7
- from inthub import config
7
+ from inthub import config, state
8
8
  from inthub.bulk import csv_state
9
9
  from inthub.client import InthubClient
10
10
  from inthub.commands.bulk.src_sqlserver_sink_snowflake import orchestrator
@@ -16,12 +16,13 @@ app = typer.Typer(
16
16
 
17
17
 
18
18
  def _client() -> InthubClient:
19
- return InthubClient(config.require_token())
19
+ return InthubClient(config.require_token(), debug=state.debug)
20
20
 
21
21
 
22
22
  @app.callback(invoke_without_command=True)
23
23
  def src_sqlserver_sink_snowflake(
24
24
  ctx: typer.Context,
25
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
25
26
  csv_file: Annotated[Path | None, typer.Option("--csv", help="Path to input CSV.")] = None,
26
27
  sqlserver_source_plugin: Annotated[
27
28
  str | None, typer.Option("--sqlserver-source-plugin")
@@ -40,6 +41,7 @@ def src_sqlserver_sink_snowflake(
40
41
  ] = None,
41
42
  ) -> None:
42
43
  """Provision SQL Server source and Snowflake sink connectors from a CSV."""
44
+ state.debug = state.debug or debug
43
45
  if ctx.invoked_subcommand is not None:
44
46
  return
45
47
 
@@ -54,7 +56,7 @@ def src_sqlserver_sink_snowflake(
54
56
 
55
57
  if resume:
56
58
  try:
57
- state = csv_state.load_state(resume)
59
+ saved = csv_state.load_state(resume)
58
60
  except FileNotFoundError:
59
61
  typer.echo(f"Error: state for ULID '{resume}' not found in ~/.inthub/.", err=True)
60
62
  raise typer.Exit(1)
@@ -62,10 +64,10 @@ def src_sqlserver_sink_snowflake(
62
64
  if not progress_path.exists():
63
65
  typer.echo(f"Error: progress file '{progress_path}' not found.", err=True)
64
66
  raise typer.Exit(1)
65
- source_plugin = state["sqlserver_source_plugin"]
66
- sink_plugin = state["snowflake_sink_plugin"]
67
- tag_list = [t for t in state.get("tags", "").split(",") if t]
68
- resolved_gcp_project = state["gcp_project"]
67
+ source_plugin = saved["sqlserver_source_plugin"]
68
+ sink_plugin = saved["snowflake_sink_plugin"]
69
+ tag_list = [t for t in saved.get("tags", "").split(",") if t]
70
+ resolved_gcp_project = saved["gcp_project"]
69
71
  else:
70
72
  missing = [
71
73
  name for name, val in [
@@ -143,14 +143,14 @@ def process_row(
143
143
  _msg2 = "Fetching GCP secrets"
144
144
  try:
145
145
  ss_creds = gcp_secrets.fetch_secret(
146
- gcp_project, f"{row['sqlserver_gcp_secret']}-{src_cluster_slug}"
146
+ gcp_project, row["sqlserver_gcp_secret"]
147
147
  )
148
148
  except Exception as exc:
149
149
  fail(2, _msg2, str(exc))
150
150
  return
151
151
  try:
152
152
  sf_creds = gcp_secrets.fetch_secret(
153
- gcp_project, f"{row['snowflake_gcp_secret']}-{snk_cluster_slug}"
153
+ gcp_project, row["snowflake_gcp_secret"]
154
154
  )
155
155
  except Exception as exc:
156
156
  fail(2, _msg2, str(exc))
@@ -8,7 +8,7 @@ import typer
8
8
  from rich.console import Console
9
9
  from rich.table import Table
10
10
 
11
- from inthub import config
11
+ from inthub import config, state
12
12
  from inthub.client import InthubClient
13
13
 
14
14
  app = typer.Typer(help="Manage connectors.", add_completion=False)
@@ -20,8 +20,15 @@ class OutputFormat(str, Enum):
20
20
  json = "json"
21
21
 
22
22
 
23
- def _client(debug: bool = False) -> InthubClient:
24
- return InthubClient(config.require_token(), debug=debug)
23
+ @app.callback()
24
+ def _connectors(
25
+ debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
26
+ ) -> None:
27
+ state.debug = state.debug or debug
28
+
29
+
30
+ def _client() -> InthubClient:
31
+ return InthubClient(config.require_token(), debug=state.debug)
25
32
 
26
33
 
27
34
  @app.command("list")
@@ -34,11 +41,12 @@ def list_connectors(
34
41
  debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
35
42
  ) -> None:
36
43
  """List connectors, optionally filtered by tags."""
44
+ state.debug = state.debug or debug
37
45
  path = "/connectors"
38
46
  if tag:
39
47
  path += "?tags=" + ",".join(tag)
40
48
 
41
- body = _client(debug).get(path).json()
49
+ body = _client().get(path).json()
42
50
  connectors: list[dict[str, object]] = (
43
51
  body.get("connectors", []) if isinstance(body, dict) else body
44
52
  )
@@ -63,8 +71,11 @@ def list_connectors(
63
71
  table.add_column("UPDATED")
64
72
 
65
73
  for c in connectors:
66
- state = str(c.get("state", ""))
67
- state_cell = f"[green]{state}[/]" if state == "DEPLOYED" else f"[yellow]{state}[/]"
74
+ connector_state = str(c.get("state", ""))
75
+ if connector_state == "DEPLOYED":
76
+ state_cell = f"[green]{connector_state}[/]"
77
+ else:
78
+ state_cell = f"[yellow]{connector_state}[/]"
68
79
 
69
80
  health = str(c.get("argoHealth", "") or "")
70
81
  health_cell = (
@@ -117,7 +128,8 @@ def create_connector(
117
128
  debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
118
129
  ) -> None:
119
130
  """Create a new connector."""
120
- body = _client(debug).post("/connectors", json={
131
+ state.debug = state.debug or debug
132
+ body = _client().post("/connectors", json={
121
133
  "name": name,
122
134
  "description": description,
123
135
  "kafkaConnectCluster": kafka_connect_cluster,
@@ -142,7 +154,8 @@ def clone_connector(
142
154
  debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
143
155
  ) -> None:
144
156
  """Clone an existing connector."""
145
- body = _client(debug).post(f"/connectors/{id}/clone").json()
157
+ state.debug = state.debug or debug
158
+ body = _client().post(f"/connectors/{id}/clone").json()
146
159
 
147
160
  if output == OutputFormat.json:
148
161
  print(json_mod.dumps(body))
@@ -157,7 +170,8 @@ def delete_connector(
157
170
  debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
158
171
  ) -> None:
159
172
  """Delete a connector."""
160
- _client(debug).delete(f"/connectors/{id}")
173
+ state.debug = state.debug or debug
174
+ _client().delete(f"/connectors/{id}")
161
175
  console.print(f"Connector {id} deleted.")
162
176
 
163
177
 
@@ -173,7 +187,8 @@ def publish_connector(
173
187
  debug: bool = typer.Option(False, "--debug", help="Print HTTP request/response details."),
174
188
  ) -> None:
175
189
  """Publish (apply) a connector to git/ArgoCD."""
176
- resp = _client(debug).post(f"/connectors/{id}/apply", json={
190
+ state.debug = state.debug or debug
191
+ resp = _client().post(f"/connectors/{id}/apply", json={
177
192
  "branch": branch,
178
193
  "commitMessage": commit_message,
179
194
  "authorName": author,
@@ -0,0 +1 @@
1
+ debug: bool = False
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "inthub-cli"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "inthub CLI — manage your inthub.io resources from the terminal"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -1,23 +0,0 @@
1
- import json
2
-
3
- from google.cloud import secretmanager
4
-
5
-
6
- def fetch_secret(project_id: str, secret_name: str) -> dict[str, str]:
7
- client = secretmanager.SecretManagerServiceClient()
8
- resource = f"projects/{project_id}/secrets/{secret_name}/versions/latest"
9
- response = client.access_secret_version(request={"name": resource})
10
- payload = response.payload.data.decode("UTF-8")
11
-
12
- try:
13
- data: dict[str, str] = json.loads(payload)
14
- except json.JSONDecodeError as exc:
15
- raise ValueError(f"Secret '{secret_name}' is not valid JSON") from exc
16
-
17
- missing = {"username", "password"} - set(data.keys())
18
- if missing:
19
- raise ValueError(
20
- f"Secret '{secret_name}' missing required fields: {', '.join(sorted(missing))}"
21
- )
22
-
23
- return {"username": data["username"], "password": data["password"]}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes