entropy-data 0.3.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.
@@ -0,0 +1,90 @@
1
+ """Connection management commands."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.table import Table
7
+
8
+ from entropy_data import config as cfg
9
+ from entropy_data.output import console, print_error, print_success
10
+
11
+ connection_app = typer.Typer(no_args_is_help=True)
12
+
13
+
14
+ @connection_app.command("list")
15
+ def list_connections() -> None:
16
+ """List all configured connections."""
17
+ connections = cfg.list_connections()
18
+ if not connections:
19
+ console.print("No connections configured. Run: entropy-data connection add <name>")
20
+ return
21
+
22
+ table = Table(show_header=True)
23
+ table.add_column("Name")
24
+ table.add_column("Host")
25
+ table.add_column("API Key")
26
+ table.add_column("Default")
27
+ for conn in connections:
28
+ table.add_row(
29
+ conn["name"],
30
+ conn["host"],
31
+ conn["api_key"],
32
+ "*" if conn["default"] else "",
33
+ )
34
+ console.print(table)
35
+
36
+
37
+ @connection_app.command("add")
38
+ def add_connection(
39
+ name: Annotated[str, typer.Argument(help="Connection name.")],
40
+ api_key: Annotated[str, typer.Option("--api-key", prompt="API key", help="The API key.")] = None,
41
+ host: Annotated[
42
+ str, typer.Option("--host", prompt="Host", prompt_required=False, help="API host URL.")
43
+ ] = cfg.DEFAULT_HOST,
44
+ ) -> None:
45
+ """Add or update a named connection."""
46
+ try:
47
+ cfg.add_connection(name, api_key, host)
48
+ print_success(f"Connection '{name}' saved.")
49
+ except cfg.ConfigurationError as e:
50
+ print_error(str(e))
51
+ raise typer.Exit(1)
52
+
53
+
54
+ @connection_app.command("remove")
55
+ def remove_connection(
56
+ name: Annotated[str, typer.Argument(help="Connection name to remove.")],
57
+ ) -> None:
58
+ """Remove a named connection."""
59
+ try:
60
+ cfg.remove_connection(name)
61
+ print_success(f"Connection '{name}' removed.")
62
+ except cfg.ConfigurationError as e:
63
+ print_error(str(e))
64
+ raise typer.Exit(1)
65
+
66
+
67
+ @connection_app.command("set-default")
68
+ def set_default(
69
+ name: Annotated[str, typer.Argument(help="Connection name to set as default.")],
70
+ ) -> None:
71
+ """Set the default connection."""
72
+ try:
73
+ cfg.set_default_connection(name)
74
+ print_success(f"Default connection set to '{name}'.")
75
+ except cfg.ConfigurationError as e:
76
+ print_error(str(e))
77
+ raise typer.Exit(1)
78
+
79
+
80
+ @connection_app.command("test")
81
+ def test_connection() -> None:
82
+ """Test the current connection by calling the API."""
83
+ from entropy_data.cli import get_client, handle_error
84
+
85
+ try:
86
+ client = get_client()
87
+ client.list_resources("teams", params={"p": "0"})
88
+ print_success("Connection successful.")
89
+ except Exception as e:
90
+ handle_error(e)
@@ -0,0 +1,61 @@
1
+ """Costs commands."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from entropy_data.output import OutputFormat, print_resource_list, print_success
9
+ from entropy_data.util import read_body
10
+
11
+ costs_app = typer.Typer(no_args_is_help=True)
12
+ RESOURCE_PATH = "costs"
13
+ RESOURCE_TYPE = "costs"
14
+
15
+
16
+ @costs_app.command("list")
17
+ def list_costs(
18
+ data_product_id: Annotated[str, typer.Option("--data-product-id", help="Data product ID to list costs for.")],
19
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
20
+ ) -> None:
21
+ """List all costs of a data product."""
22
+ from entropy_data.cli import get_client, get_output_format, handle_error
23
+
24
+ fmt = output or get_output_format()
25
+ try:
26
+ client = get_client()
27
+ data, has_next = client.list_resources(RESOURCE_PATH, params={"dataProductId": data_product_id})
28
+ print_resource_list(data, RESOURCE_TYPE, fmt)
29
+ except Exception as e:
30
+ handle_error(e)
31
+
32
+
33
+ @costs_app.command("add")
34
+ def add_cost(
35
+ file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
36
+ ) -> None:
37
+ """Add a cost."""
38
+ from entropy_data.cli import get_client, handle_error
39
+
40
+ try:
41
+ body = read_body(file)
42
+ client = get_client()
43
+ client.post_resource(RESOURCE_PATH, body)
44
+ print_success("Cost added.")
45
+ except Exception as e:
46
+ handle_error(e)
47
+
48
+
49
+ @costs_app.command("delete")
50
+ def delete_cost(
51
+ id: Annotated[str, typer.Argument(help="Cost ID.")],
52
+ ) -> None:
53
+ """Delete a cost."""
54
+ from entropy_data.cli import get_client, handle_error
55
+
56
+ try:
57
+ client = get_client()
58
+ client.delete_resource(RESOURCE_PATH, id)
59
+ print_success(f"Cost '{id}' deleted.")
60
+ except Exception as e:
61
+ handle_error(e)
@@ -0,0 +1,115 @@
1
+ """Data contracts commands."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from entropy_data.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
9
+ from entropy_data.util import read_body
10
+
11
+ datacontracts_app = typer.Typer(no_args_is_help=True)
12
+ RESOURCE_PATH = "datacontracts"
13
+ RESOURCE_TYPE = "datacontracts"
14
+
15
+
16
+ @datacontracts_app.command("list")
17
+ def list_datacontracts(
18
+ page: Annotated[int, typer.Option("--page", "-p", help="Page number (0-indexed).")] = 0,
19
+ query: Annotated[Optional[str], typer.Option("--query", "-q", help="Search term.")] = None,
20
+ owner: Annotated[Optional[str], typer.Option("--owner", help="Filter by owner.")] = None,
21
+ tag: Annotated[Optional[str], typer.Option("--tag", help="Filter by tag.")] = None,
22
+ sort: Annotated[Optional[str], typer.Option("--sort", help="Sort field.")] = None,
23
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
24
+ ) -> None:
25
+ """List all data contracts."""
26
+ from entropy_data.cli import get_client, get_output_format, handle_error
27
+
28
+ fmt = output or get_output_format()
29
+ try:
30
+ client = get_client()
31
+ params = {"p": page}
32
+ if query:
33
+ params["q"] = query
34
+ if owner:
35
+ params["owner"] = owner
36
+ if tag:
37
+ params["tag"] = tag
38
+ if sort:
39
+ params["sort"] = sort
40
+ data, has_next = client.list_resources(RESOURCE_PATH, params=params)
41
+ print_resource_list(data, RESOURCE_TYPE, fmt, has_next_page=has_next, page=page)
42
+ except Exception as e:
43
+ handle_error(e)
44
+
45
+
46
+ @datacontracts_app.command("get")
47
+ def get_datacontract(
48
+ id: Annotated[str, typer.Argument(help="Data contract ID.")],
49
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
50
+ ) -> None:
51
+ """Get a data contract by ID."""
52
+ from entropy_data.cli import get_client, get_output_format, handle_error
53
+
54
+ fmt = output or get_output_format()
55
+ try:
56
+ client = get_client()
57
+ data = client.get_resource(RESOURCE_PATH, id)
58
+ print_resource(data, RESOURCE_TYPE, fmt)
59
+ except Exception as e:
60
+ handle_error(e)
61
+
62
+
63
+ @datacontracts_app.command("put")
64
+ def put_datacontract(
65
+ id: Annotated[str, typer.Argument(help="Data contract ID.")],
66
+ file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
67
+ ) -> None:
68
+ """Create or update a data contract."""
69
+ from entropy_data.cli import get_client, handle_error
70
+
71
+ try:
72
+ body = read_body(file)
73
+ client = get_client()
74
+ location = client.put_resource(RESOURCE_PATH, id, body)
75
+ print_success(f"Data contract '{id}' saved.")
76
+ print_link(location)
77
+ except Exception as e:
78
+ handle_error(e)
79
+
80
+
81
+ @datacontracts_app.command("test")
82
+ def test_datacontract(
83
+ id: Annotated[str, typer.Argument(help="Data contract ID.")],
84
+ server: Annotated[Optional[str], typer.Option("--server", "-s", help="Server name to test against.")] = None,
85
+ ) -> None:
86
+ """Run a data contract test."""
87
+ import json
88
+
89
+ from entropy_data.cli import get_client, handle_error
90
+
91
+ try:
92
+ client = get_client()
93
+ params = {}
94
+ if server:
95
+ params["server"] = server
96
+ # Data contract tests can take a long time (up to 30 minutes)
97
+ data = client.post_action_json(RESOURCE_PATH, id, "test", params=params, timeout=1800)
98
+ print(json.dumps(data, indent=2))
99
+ except Exception as e:
100
+ handle_error(e)
101
+
102
+
103
+ @datacontracts_app.command("delete")
104
+ def delete_datacontract(
105
+ id: Annotated[str, typer.Argument(help="Data contract ID.")],
106
+ ) -> None:
107
+ """Delete a data contract."""
108
+ from entropy_data.cli import get_client, handle_error
109
+
110
+ try:
111
+ client = get_client()
112
+ client.delete_resource(RESOURCE_PATH, id)
113
+ print_success(f"Data contract '{id}' deleted.")
114
+ except Exception as e:
115
+ handle_error(e)
@@ -0,0 +1,93 @@
1
+ """Data products commands."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from entropy_data.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
9
+ from entropy_data.util import read_body
10
+
11
+ dataproducts_app = typer.Typer(no_args_is_help=True)
12
+ RESOURCE_PATH = "dataproducts"
13
+ RESOURCE_TYPE = "dataproducts"
14
+
15
+
16
+ @dataproducts_app.command("list")
17
+ def list_dataproducts(
18
+ page: Annotated[int, typer.Option("--page", "-p", help="Page number (0-indexed).")] = 0,
19
+ query: Annotated[Optional[str], typer.Option("--query", "-q", help="Search term.")] = None,
20
+ status: Annotated[Optional[str], typer.Option("--status", help="Filter by status.")] = None,
21
+ tag: Annotated[Optional[str], typer.Option("--tag", help="Filter by tag.")] = None,
22
+ sort: Annotated[Optional[str], typer.Option("--sort", help="Sort field.")] = None,
23
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
24
+ ) -> None:
25
+ """List all data products."""
26
+ from entropy_data.cli import get_client, get_output_format, handle_error
27
+
28
+ fmt = output or get_output_format()
29
+ try:
30
+ client = get_client()
31
+ params = {"p": page}
32
+ if query:
33
+ params["q"] = query
34
+ if status:
35
+ params["status"] = status
36
+ if tag:
37
+ params["tag"] = tag
38
+ if sort:
39
+ params["sort"] = sort
40
+ data, has_next = client.list_resources(RESOURCE_PATH, params=params)
41
+ print_resource_list(data, RESOURCE_TYPE, fmt, has_next_page=has_next, page=page)
42
+ except Exception as e:
43
+ handle_error(e)
44
+
45
+
46
+ @dataproducts_app.command("get")
47
+ def get_dataproduct(
48
+ id: Annotated[str, typer.Argument(help="Data product ID.")],
49
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
50
+ ) -> None:
51
+ """Get a data product by ID."""
52
+ from entropy_data.cli import get_client, get_output_format, handle_error
53
+
54
+ fmt = output or get_output_format()
55
+ try:
56
+ client = get_client()
57
+ data = client.get_resource(RESOURCE_PATH, id)
58
+ print_resource(data, RESOURCE_TYPE, fmt)
59
+ except Exception as e:
60
+ handle_error(e)
61
+
62
+
63
+ @dataproducts_app.command("put")
64
+ def put_dataproduct(
65
+ id: Annotated[str, typer.Argument(help="Data product ID.")],
66
+ file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
67
+ ) -> None:
68
+ """Create or update a data product."""
69
+ from entropy_data.cli import get_client, handle_error
70
+
71
+ try:
72
+ body = read_body(file)
73
+ client = get_client()
74
+ location = client.put_resource(RESOURCE_PATH, id, body)
75
+ print_success(f"Data product '{id}' saved.")
76
+ print_link(location)
77
+ except Exception as e:
78
+ handle_error(e)
79
+
80
+
81
+ @dataproducts_app.command("delete")
82
+ def delete_dataproduct(
83
+ id: Annotated[str, typer.Argument(help="Data product ID.")],
84
+ ) -> None:
85
+ """Delete a data product."""
86
+ from entropy_data.cli import get_client, handle_error
87
+
88
+ try:
89
+ client = get_client()
90
+ client.delete_resource(RESOURCE_PATH, id)
91
+ print_success(f"Data product '{id}' deleted.")
92
+ except Exception as e:
93
+ handle_error(e)
@@ -0,0 +1,80 @@
1
+ """Definitions commands."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from entropy_data.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
9
+ from entropy_data.util import read_body
10
+
11
+ definitions_app = typer.Typer(no_args_is_help=True)
12
+ RESOURCE_PATH = "definitions"
13
+ RESOURCE_TYPE = "definitions"
14
+
15
+
16
+ @definitions_app.command("list")
17
+ def list_definitions(
18
+ page: Annotated[int, typer.Option("--page", "-p", help="Page number (0-indexed).")] = 0,
19
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
20
+ ) -> None:
21
+ """List all definitions."""
22
+ from entropy_data.cli import get_client, get_output_format, handle_error
23
+
24
+ fmt = output or get_output_format()
25
+ try:
26
+ client = get_client()
27
+ data, has_next = client.list_resources(RESOURCE_PATH, params={"p": page})
28
+ print_resource_list(data, RESOURCE_TYPE, fmt, has_next_page=has_next, page=page)
29
+ except Exception as e:
30
+ handle_error(e)
31
+
32
+
33
+ @definitions_app.command("get")
34
+ def get_definition(
35
+ id: Annotated[str, typer.Argument(help="Definition ID.")],
36
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
37
+ ) -> None:
38
+ """Get a definition by ID."""
39
+ from entropy_data.cli import get_client, get_output_format, handle_error
40
+
41
+ fmt = output or get_output_format()
42
+ try:
43
+ client = get_client()
44
+ data = client.get_resource(RESOURCE_PATH, id)
45
+ print_resource(data, RESOURCE_TYPE, fmt)
46
+ except Exception as e:
47
+ handle_error(e)
48
+
49
+
50
+ @definitions_app.command("put")
51
+ def put_definition(
52
+ id: Annotated[str, typer.Argument(help="Definition ID.")],
53
+ file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
54
+ ) -> None:
55
+ """Create or update a definition."""
56
+ from entropy_data.cli import get_client, handle_error
57
+
58
+ try:
59
+ body = read_body(file)
60
+ client = get_client()
61
+ location = client.put_resource(RESOURCE_PATH, id, body)
62
+ print_success(f"Definition '{id}' saved.")
63
+ print_link(location)
64
+ except Exception as e:
65
+ handle_error(e)
66
+
67
+
68
+ @definitions_app.command("delete")
69
+ def delete_definition(
70
+ id: Annotated[str, typer.Argument(help="Definition ID.")],
71
+ ) -> None:
72
+ """Delete a definition."""
73
+ from entropy_data.cli import get_client, handle_error
74
+
75
+ try:
76
+ client = get_client()
77
+ client.delete_resource(RESOURCE_PATH, id)
78
+ print_success(f"Definition '{id}' deleted.")
79
+ except Exception as e:
80
+ handle_error(e)
@@ -0,0 +1,32 @@
1
+ """Events commands."""
2
+
3
+ import json
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from entropy_data.output import OutputFormat, console, print_resource_list
9
+
10
+ events_app = typer.Typer(no_args_is_help=True)
11
+
12
+
13
+ @events_app.command("poll")
14
+ def poll_events(
15
+ last_event_id: Annotated[
16
+ Optional[str], typer.Option("--last-event-id", help="Last event ID for incremental polling.")
17
+ ] = None,
18
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
19
+ ) -> None:
20
+ """Poll for events."""
21
+ from entropy_data.cli import get_client, get_output_format, handle_error
22
+
23
+ fmt = output or get_output_format()
24
+ try:
25
+ client = get_client()
26
+ data = client.get_events(last_event_id=last_event_id)
27
+ if fmt == OutputFormat.json:
28
+ console.print_json(json.dumps(data))
29
+ else:
30
+ print_resource_list(data, "events", fmt)
31
+ except Exception as e:
32
+ handle_error(e)
@@ -0,0 +1,83 @@
1
+ """Example data commands."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from entropy_data.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
9
+ from entropy_data.util import read_body
10
+
11
+ example_data_app = typer.Typer(no_args_is_help=True)
12
+ RESOURCE_PATH = "example-data"
13
+ RESOURCE_TYPE = "example-data"
14
+
15
+
16
+ @example_data_app.command("list")
17
+ def list_example_data(
18
+ data_product_id: Annotated[Optional[str], typer.Option("--data-product-id", help="Filter by data product.")] = None,
19
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
20
+ ) -> None:
21
+ """List all example data entries."""
22
+ from entropy_data.cli import get_client, get_output_format, handle_error
23
+
24
+ fmt = output or get_output_format()
25
+ try:
26
+ client = get_client()
27
+ params = {}
28
+ if data_product_id:
29
+ params["dataProductId"] = data_product_id
30
+ data, has_next = client.list_resources(RESOURCE_PATH, params=params or None)
31
+ print_resource_list(data, RESOURCE_TYPE, fmt, has_next_page=has_next)
32
+ except Exception as e:
33
+ handle_error(e)
34
+
35
+
36
+ @example_data_app.command("get")
37
+ def get_example_data(
38
+ id: Annotated[str, typer.Argument(help="Example data ID.")],
39
+ output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
40
+ ) -> None:
41
+ """Get example data by ID."""
42
+ from entropy_data.cli import get_client, get_output_format, handle_error
43
+
44
+ fmt = output or get_output_format()
45
+ try:
46
+ client = get_client()
47
+ data = client.get_resource(RESOURCE_PATH, id)
48
+ print_resource(data, RESOURCE_TYPE, fmt)
49
+ except Exception as e:
50
+ handle_error(e)
51
+
52
+
53
+ @example_data_app.command("put")
54
+ def put_example_data(
55
+ id: Annotated[str, typer.Argument(help="Example data ID.")],
56
+ file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
57
+ ) -> None:
58
+ """Create or update example data."""
59
+ from entropy_data.cli import get_client, handle_error
60
+
61
+ try:
62
+ body = read_body(file)
63
+ client = get_client()
64
+ location = client.put_resource(RESOURCE_PATH, id, body)
65
+ print_success(f"Example data '{id}' saved.")
66
+ print_link(location)
67
+ except Exception as e:
68
+ handle_error(e)
69
+
70
+
71
+ @example_data_app.command("delete")
72
+ def delete_example_data(
73
+ id: Annotated[str, typer.Argument(help="Example data ID.")],
74
+ ) -> None:
75
+ """Delete example data."""
76
+ from entropy_data.cli import get_client, handle_error
77
+
78
+ try:
79
+ client = get_client()
80
+ client.delete_resource(RESOURCE_PATH, id)
81
+ print_success(f"Example data '{id}' deleted.")
82
+ except Exception as e:
83
+ handle_error(e)
@@ -0,0 +1,134 @@
1
+ """Import command for organization exports."""
2
+
3
+ import tempfile
4
+ import zipfile
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ import yaml
10
+
11
+ from entropy_data.client import ApiError
12
+ from entropy_data.output import console, error_console
13
+
14
+ import_app = typer.Typer(no_args_is_help=True)
15
+
16
+ # Import order respects resource dependencies:
17
+ # teams (parent→child), tags (→teams), definitions (→teams),
18
+ # assets (→teams), datacontracts (→teams, assets),
19
+ # dataproducts (→teams, datacontracts, assets), access (→dataproducts)
20
+ RESOURCE_ORDER = [
21
+ ("teams", "teams"),
22
+ ("tags", "tags"),
23
+ ("definitions", "definitions"),
24
+ ("assets", "assets"),
25
+ ("datacontracts", "datacontracts"),
26
+ ("dataproducts", "dataproducts"),
27
+ ("access", "access"),
28
+ ]
29
+
30
+
31
+ def _import_teams(teams_dir: Path, client) -> tuple[int, int]:
32
+ """Import teams in topological order (parents before children), stripping members."""
33
+ teams = {}
34
+ for f in sorted(teams_dir.glob("*.yaml")):
35
+ data = yaml.safe_load(f.read_text())
36
+ data["members"] = []
37
+ teams[data["id"]] = {"data": data, "parent": data.get("parent")}
38
+
39
+ imported: set[str] = set()
40
+ success_count = 0
41
+ error_count = 0
42
+
43
+ while len(imported) < len(teams):
44
+ progress = False
45
+ for tid, t in teams.items():
46
+ if tid in imported:
47
+ continue
48
+ if t["parent"] is None or t["parent"] in imported:
49
+ try:
50
+ client.put_resource("teams", tid, t["data"])
51
+ console.print(f" [green]OK[/green] {tid}")
52
+ success_count += 1
53
+ except ApiError as e:
54
+ error_console.print(f" [red]FAIL[/red] {tid}: {e}")
55
+ error_count += 1
56
+ imported.add(tid)
57
+ progress = True
58
+
59
+ if not progress:
60
+ remaining = set(teams.keys()) - imported
61
+ error_console.print(f" [red]ERROR: circular or broken parent references: {remaining}[/red]")
62
+ error_count += len(remaining)
63
+ break
64
+
65
+ return success_count, error_count
66
+
67
+
68
+ def _import_simple(resource_dir: Path, api_path: str, client) -> tuple[int, int]:
69
+ """Import resources from a directory using PUT."""
70
+ success_count = 0
71
+ error_count = 0
72
+
73
+ for f in sorted(resource_dir.glob("*.yaml")):
74
+ data = yaml.safe_load(f.read_text())
75
+ resource_id = data["id"]
76
+ try:
77
+ client.put_resource(api_path, resource_id, data)
78
+ console.print(f" [green]OK[/green] {resource_id}")
79
+ success_count += 1
80
+ except ApiError as e:
81
+ error_console.print(f" [red]FAIL[/red] {resource_id}: {e}")
82
+ error_count += 1
83
+
84
+ return success_count, error_count
85
+
86
+
87
+ @import_app.command("zip")
88
+ def import_zip(
89
+ file: Annotated[Path, typer.Argument(help="Path to the export zip file.")],
90
+ ) -> None:
91
+ """Import an organization export zip file."""
92
+ from entropy_data.cli import get_client, handle_error
93
+
94
+ if not file.is_file():
95
+ error_console.print(f"[red]Error: {file} not found[/red]")
96
+ raise typer.Exit(1)
97
+
98
+ if not zipfile.is_zipfile(file):
99
+ error_console.print(f"[red]Error: {file} is not a valid zip file[/red]")
100
+ raise typer.Exit(1)
101
+
102
+ try:
103
+ client = get_client()
104
+ except Exception as e:
105
+ handle_error(e)
106
+ return
107
+
108
+ with tempfile.TemporaryDirectory() as tmpdir:
109
+ export_dir = Path(tmpdir)
110
+ console.print(f"Extracting {file}...")
111
+ with zipfile.ZipFile(file) as zf:
112
+ zf.extractall(export_dir)
113
+
114
+ total_ok = 0
115
+ total_fail = 0
116
+
117
+ for directory, api_path in RESOURCE_ORDER:
118
+ resource_dir = export_dir / directory
119
+ if not resource_dir.is_dir():
120
+ continue
121
+
122
+ console.print(f"\n[bold]{api_path}[/bold]")
123
+
124
+ if directory == "teams":
125
+ ok, fail = _import_teams(resource_dir, client)
126
+ else:
127
+ ok, fail = _import_simple(resource_dir, api_path, client)
128
+
129
+ total_ok += ok
130
+ total_fail += fail
131
+
132
+ console.print(f"\n[bold]Summary:[/bold] {total_ok} succeeded, {total_fail} failed")
133
+ if total_fail > 0:
134
+ raise typer.Exit(1)