kctl-opencloud 0.5.0__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 (27) hide show
  1. kctl_opencloud-0.5.0/.gitignore +8 -0
  2. kctl_opencloud-0.5.0/PKG-INFO +15 -0
  3. kctl_opencloud-0.5.0/README.md +94 -0
  4. kctl_opencloud-0.5.0/pyproject.toml +43 -0
  5. kctl_opencloud-0.5.0/skills/opencloud-admin/SKILL.md +71 -0
  6. kctl_opencloud-0.5.0/src/kctl_opencloud/__init__.py +3 -0
  7. kctl_opencloud-0.5.0/src/kctl_opencloud/__main__.py +5 -0
  8. kctl_opencloud-0.5.0/src/kctl_opencloud/cli.py +124 -0
  9. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/__init__.py +0 -0
  10. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/config_cmd.py +191 -0
  11. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/dashboard.py +80 -0
  12. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/doctor_cmd.py +110 -0
  13. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/groups.py +130 -0
  14. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/health.py +135 -0
  15. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/shares.py +109 -0
  16. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/skill_cmd.py +50 -0
  17. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/spaces.py +173 -0
  18. kctl_opencloud-0.5.0/src/kctl_opencloud/commands/users.py +133 -0
  19. kctl_opencloud-0.5.0/src/kctl_opencloud/core/__init__.py +0 -0
  20. kctl_opencloud-0.5.0/src/kctl_opencloud/core/callbacks.py +34 -0
  21. kctl_opencloud-0.5.0/src/kctl_opencloud/core/client.py +96 -0
  22. kctl_opencloud-0.5.0/src/kctl_opencloud/core/config.py +131 -0
  23. kctl_opencloud-0.5.0/src/kctl_opencloud/core/exceptions.py +23 -0
  24. kctl_opencloud-0.5.0/src/kctl_opencloud/core/output.py +7 -0
  25. kctl_opencloud-0.5.0/tests/__init__.py +0 -0
  26. kctl_opencloud-0.5.0/tests/conftest.py +67 -0
  27. kctl_opencloud-0.5.0/tests/test_cli.py +69 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ dist/
5
+ *.egg-info/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ .pytest_cache/
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: kctl-opencloud
3
+ Version: 0.5.0
4
+ Summary: Kodemeio OpenCloud CLI — manage OpenCloud file platform
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: httpx>=0.28.0
7
+ Requires-Dist: kctl-lib>=0.5.0
8
+ Requires-Dist: pydantic>=2.10.0
9
+ Requires-Dist: rich>=13.9.0
10
+ Requires-Dist: typer>=0.15.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy>=1.14.0; extra == 'dev'
13
+ Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
14
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
15
+ Requires-Dist: ruff>=0.9.0; extra == 'dev'
@@ -0,0 +1,94 @@
1
+ # kctl-opencloud
2
+
3
+ Kodemeio OpenCloud CLI — manage users, groups, spaces, and shares on your OpenCloud file storage platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv tool install kctl-opencloud
9
+ ```
10
+
11
+ Requires `kctl-lib>=0.4.0` (installed automatically).
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Configure a profile
17
+ kctl-opencloud config init
18
+
19
+ # Check platform health
20
+ kctl-opencloud health check
21
+
22
+ # View system overview
23
+ kctl-opencloud dashboard show
24
+
25
+ # List all users
26
+ kctl-opencloud users list
27
+
28
+ # List spaces with quota info
29
+ kctl-opencloud spaces list
30
+ kctl-opencloud spaces quota <space-id>
31
+
32
+ # Manage sharing
33
+ kctl-opencloud shares list <space-id> <item-id>
34
+ kctl-opencloud shares create-link <space-id> <item-id>
35
+ ```
36
+
37
+ ## Command Groups
38
+
39
+ | Group | Description | Key Commands |
40
+ |-------|-------------|--------------|
41
+ | `health` | Health checks and monitoring | `check`, `detailed` |
42
+ | `dashboard` | System overview and statistics | `show` |
43
+ | `users` | User management | `list`, `get`, `create`, `update`, `delete` |
44
+ | `groups` | Group management | `list`, `get`, `create`, `update`, `delete`, `add-member`, `remove-member` |
45
+ | `spaces` | Drive and space management | `list`, `get`, `create`, `update`, `delete`, `quota` |
46
+ | `shares` | Sharing and permissions | `list`, `create-link`, `invite`, `delete` |
47
+ | `config` | Profile management | `init`, `add`, `use`, `show`, `validate`, `remove`, `set`, `profiles`, `current` |
48
+ | `doctor` | Diagnostic checks | API connectivity, auth validation |
49
+
50
+ ## Global Options
51
+
52
+ | Option | Short | Description |
53
+ |--------|-------|-------------|
54
+ | `--profile` | `-p` | Config profile name |
55
+ | `--url` | | API URL override |
56
+ | `--token` | | API token override |
57
+ | `--json` | | Output as JSON |
58
+ | `--quiet` | `-q` | Suppress info messages |
59
+ | `--version` | `-V` | Show version and exit |
60
+
61
+ ## Configuration
62
+
63
+ Config lives in `~/.config/kodemeio/config.yaml` under the `opencloud` service key.
64
+
65
+ ```bash
66
+ # Interactive setup
67
+ kctl-opencloud config init
68
+
69
+ # Add a named profile
70
+ kctl-opencloud config add --profile production \
71
+ --url https://opencloud.kodeme.io \
72
+ --token $OPENCLOUD_TOKEN
73
+
74
+ # Switch active profile
75
+ kctl-opencloud config use production
76
+
77
+ # Show current config (secrets masked)
78
+ kctl-opencloud config show
79
+ ```
80
+
81
+ Environment variable overrides:
82
+ - `KCTL_OPENCLOUD_URL` -- API URL
83
+ - `KCTL_OPENCLOUD_TOKEN` -- API token (machine auth key)
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ cd packages/kctl-opencloud
89
+ uv sync --all-extras
90
+ uv run pytest tests/ -v
91
+ uv run mypy src/
92
+ uv run ruff check src/
93
+ uv build
94
+ ```
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kctl-opencloud"
7
+ version = "0.5.0"
8
+ description = "Kodemeio OpenCloud CLI — manage OpenCloud file platform"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "kctl-lib>=0.5.0",
12
+ "typer>=0.15.0",
13
+ "rich>=13.9.0",
14
+ "pydantic>=2.10.0",
15
+ "httpx>=0.28.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.3.0",
21
+ "pytest-httpx>=0.35.0",
22
+ "ruff>=0.9.0",
23
+ "mypy>=1.14.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ kctl-opencloud = "kctl_opencloud.cli:_run"
28
+
29
+ [tool.uv.sources]
30
+ kctl-lib = { workspace = true }
31
+
32
+ [project.entry-points."kctl_opencloud.plugins"]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/kctl_opencloud"]
36
+
37
+ [tool.ruff]
38
+ target-version = "py312"
39
+ line-length = 120
40
+
41
+ [tool.mypy]
42
+ python_version = "3.12"
43
+ strict = true
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: opencloud-admin
3
+ description: OpenCloud file platform administration via kctl-opencloud CLI (8 groups, ~35 commands). MUST use for ANY kctl-opencloud operation.
4
+ triggers:
5
+ - kctl-opencloud
6
+ - opencloud
7
+ - cloud.kodeme.io
8
+ - file storage
9
+ - spaces
10
+ - drives
11
+ ---
12
+
13
+ # OpenCloud Administration
14
+
15
+ Use the `kctl-opencloud` CLI for all OpenCloud management tasks.
16
+
17
+ ## Command Groups
18
+
19
+ | Group | Description |
20
+ |-------|-------------|
21
+ | `health` | Health checks with 5-probe scoring |
22
+ | `dashboard` | System overview and statistics |
23
+ | `config` | Profile and connection management |
24
+ | `users` | User CRUD and search |
25
+ | `groups` | Group management and membership |
26
+ | `spaces` | Drive/space lifecycle and quota |
27
+ | `shares` | Sharing links and permissions |
28
+ | `doctor` | Diagnostic checks |
29
+
30
+ ## Common Patterns
31
+
32
+ ```bash
33
+ # Health
34
+ kctl-opencloud health check
35
+ kctl-opencloud health check --watch
36
+
37
+ # Users
38
+ kctl-opencloud users list
39
+ kctl-opencloud users list --search "john"
40
+ kctl-opencloud users get <id>
41
+ kctl-opencloud users create user@kodeme.io --name "User Name"
42
+ kctl-opencloud users delete <id> --force
43
+
44
+ # Groups
45
+ kctl-opencloud groups list
46
+ kctl-opencloud groups create "Team Name"
47
+ kctl-opencloud groups add-member <group-id> <user-id>
48
+
49
+ # Spaces
50
+ kctl-opencloud spaces list
51
+ kctl-opencloud spaces list --type project
52
+ kctl-opencloud spaces create "Project Name" --description "..."
53
+ kctl-opencloud spaces quota <id>
54
+
55
+ # Dashboard
56
+ kctl-opencloud dashboard show
57
+ kctl-opencloud dashboard show --json
58
+
59
+ # Configuration
60
+ kctl-opencloud config init
61
+ kctl-opencloud config show
62
+ kctl-opencloud config test
63
+ ```
64
+
65
+ ## JSON Output
66
+
67
+ All commands support `--json` for machine-readable output:
68
+ ```bash
69
+ kctl-opencloud --json users list
70
+ kctl-opencloud --json spaces list
71
+ ```
@@ -0,0 +1,3 @@
1
+ """kctl-opencloud: Kodemeio OpenCloud CLI."""
2
+
3
+ __version__ = "0.5.0"
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m kctl_opencloud."""
2
+
3
+ from kctl_opencloud.cli import app
4
+
5
+ app()
@@ -0,0 +1,124 @@
1
+ """Main CLI entry point for kctl-opencloud."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from kctl_lib import KctlError, handle_cli_error
9
+
10
+ from kctl_opencloud import __version__
11
+ from kctl_opencloud.commands.config_cmd import app as config_app
12
+ from kctl_opencloud.commands.dashboard import app as dashboard_app
13
+ from kctl_opencloud.commands.doctor_cmd import app as doctor_app
14
+ from kctl_opencloud.commands.groups import app as groups_app
15
+ from kctl_opencloud.commands.health import app as health_app
16
+ from kctl_opencloud.commands.shares import app as shares_app
17
+ from kctl_opencloud.commands.skill_cmd import app as skill_app
18
+ from kctl_opencloud.commands.spaces import app as spaces_app
19
+ from kctl_opencloud.commands.users import app as users_app
20
+ from kctl_opencloud.core.callbacks import AppContext
21
+ from kctl_lib.self_update import notify_if_outdated
22
+ from kctl_lib.tui import add_tui_command
23
+
24
+
25
+ def version_callback(value: bool) -> None:
26
+ if value:
27
+ typer.echo(f"kctl-opencloud {__version__}")
28
+ raise typer.Exit()
29
+
30
+
31
+ app = typer.Typer(
32
+ name="kctl-opencloud",
33
+ help="Kodemeio OpenCloud CLI - manage your OpenCloud file platform.",
34
+ no_args_is_help=True,
35
+ rich_markup_mode="rich",
36
+ pretty_exceptions_enable=False,
37
+ )
38
+
39
+
40
+ @app.callback()
41
+ def main(
42
+ ctx: typer.Context,
43
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
44
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
45
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
46
+ url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
47
+ token: Annotated[str | None, typer.Option("--token", help="API token override")] = None,
48
+ version: Annotated[
49
+ bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
50
+ ] = False,
51
+ ) -> None:
52
+ """Kodemeio OpenCloud CLI."""
53
+ ctx.ensure_object(dict)
54
+ ctx.obj = AppContext(
55
+ json_mode=json_output,
56
+ quiet=quiet,
57
+ profile=profile,
58
+ url_override=url,
59
+ token_override=token,
60
+ )
61
+ notify_if_outdated(ctx.obj.output, "kctl-opencloud", __version__)
62
+
63
+
64
+ # Register command groups
65
+ app.add_typer(health_app, name="health")
66
+ app.add_typer(dashboard_app, name="dashboard")
67
+ app.add_typer(config_app, name="config")
68
+ app.add_typer(users_app, name="users")
69
+ app.add_typer(groups_app, name="groups")
70
+ app.add_typer(spaces_app, name="spaces")
71
+ app.add_typer(shares_app, name="shares")
72
+ app.add_typer(doctor_app, name="doctor")
73
+ app.add_typer(skill_app, name="skill", hidden=True)
74
+ add_tui_command(app, service_key="opencloud", version=__version__)
75
+
76
+
77
+ @app.command("self-update")
78
+ def self_update_cmd(ctx: typer.Context) -> None:
79
+ """Check for updates and upgrade kctl-opencloud."""
80
+ actx: AppContext = ctx.obj
81
+ out = actx.output
82
+
83
+ from kctl_lib.self_update import check_update
84
+ from kctl_lib.self_update import update as do_update
85
+
86
+ latest = check_update("kctl-opencloud", __version__)
87
+ if latest:
88
+ out.info(f"Updating to {latest}...")
89
+ do_update("kctl-opencloud")
90
+ out.success(f"Updated to {latest}")
91
+ else:
92
+ out.success("Already up to date")
93
+
94
+
95
+ @app.command()
96
+ def completions(
97
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
98
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
99
+ ) -> None:
100
+ """Generate or install shell completions."""
101
+ from kctl_lib.completions import get_completion_script, install_completions
102
+
103
+ if install:
104
+ path = install_completions("kctl-opencloud", shell)
105
+ if path:
106
+ typer.echo(f"Completions installed to {path}")
107
+ else:
108
+ typer.echo(f"Could not install completions for {shell}", err=True)
109
+ raise typer.Exit(code=1)
110
+ else:
111
+ script = get_completion_script("kctl-opencloud", shell)
112
+ typer.echo(script)
113
+
114
+
115
+ def _run() -> None:
116
+ """Entry point with error handling."""
117
+ try:
118
+ app()
119
+ except KctlError as e:
120
+ handle_cli_error(e)
121
+
122
+
123
+ if __name__ == "__main__":
124
+ _run()
@@ -0,0 +1,191 @@
1
+ """Configuration management commands for kctl-opencloud."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_opencloud.core.callbacks import AppContext
10
+ from kctl_opencloud.core.config import (
11
+ SERVICE_KEY,
12
+ ServiceConfig,
13
+ get_default_profile,
14
+ get_profile_names,
15
+ get_service_config,
16
+ remove_profile,
17
+ resolve_active_profile_name,
18
+ set_default_profile,
19
+ set_service_config,
20
+ )
21
+
22
+ app = typer.Typer(help="Configuration and profile management.")
23
+
24
+
25
+ def _mask_token(token: str) -> str:
26
+ """Mask a token for display."""
27
+ if not token or len(token) < 8:
28
+ return "****" if token else ""
29
+ return token[:4] + "****" + token[-4:]
30
+
31
+
32
+ @app.command()
33
+ def init(
34
+ ctx: typer.Context,
35
+ url: Annotated[str, typer.Option("--url", help="OpenCloud API URL")] = "",
36
+ token: Annotated[str, typer.Option("--token", help="Machine auth API key")] = "",
37
+ profile_name: Annotated[str, typer.Option("--profile", "-p", help="Profile name")] = "default",
38
+ ) -> None:
39
+ """Initialize configuration with API credentials."""
40
+ c: AppContext = ctx.obj
41
+
42
+ if not url:
43
+ url = typer.prompt("OpenCloud URL", default="https://cloud.kodeme.io")
44
+ if not token:
45
+ token = typer.prompt("Machine Auth API Key", hide_input=True)
46
+
47
+ svc = ServiceConfig(url=url, token=token)
48
+ set_service_config(profile_name, svc)
49
+ set_default_profile(profile_name)
50
+
51
+ # Test connection
52
+ try:
53
+ from kctl_opencloud.core.client import OpenCloudClient
54
+
55
+ client = OpenCloudClient(base_url=url, credential=token)
56
+ status = client.check_health()
57
+ if status == 200:
58
+ c.output.success(f"Connected to {url}")
59
+ elif status == 0:
60
+ c.output.warn(f"Could not reach {url} (connection error)")
61
+ else:
62
+ c.output.warn(f"Connection returned HTTP {status}")
63
+ except Exception as e:
64
+ c.output.warn(f"Could not verify connection: {e}")
65
+
66
+ c.output.success(f"Configuration saved to profile '{profile_name}'")
67
+
68
+
69
+ @app.command()
70
+ def show(ctx: typer.Context) -> None:
71
+ """Show current configuration."""
72
+ c: AppContext = ctx.obj
73
+ pname = resolve_active_profile_name(c.profile)
74
+ svc = get_service_config(pname)
75
+
76
+ data = {
77
+ "profile": pname,
78
+ "service_key": SERVICE_KEY,
79
+ "url": svc.url or "(not set)",
80
+ "token": _mask_token(svc.token),
81
+ "container_name": svc.container_name or "(default)",
82
+ }
83
+
84
+ sections = [
85
+ (
86
+ "Configuration",
87
+ [
88
+ ("Profile", data["profile"]),
89
+ ("Service Key", data["service_key"]),
90
+ ("URL", data["url"]),
91
+ ("Token", data["token"]),
92
+ ("Container", data["container_name"]),
93
+ ],
94
+ ),
95
+ ]
96
+ c.output.detail("OpenCloud Config", sections, data_for_json=data)
97
+
98
+
99
+ @app.command()
100
+ def current(ctx: typer.Context) -> None:
101
+ """Show the active profile name."""
102
+ c: AppContext = ctx.obj
103
+ pname = resolve_active_profile_name(c.profile)
104
+ c.output.text(pname)
105
+
106
+
107
+ @app.command("set")
108
+ def set_(
109
+ ctx: typer.Context,
110
+ key: Annotated[str, typer.Argument(help="Config key (url, token, container_name)")],
111
+ value: Annotated[str, typer.Argument(help="Config value")],
112
+ ) -> None:
113
+ """Set a configuration value."""
114
+ c: AppContext = ctx.obj
115
+ valid_fields = set(ServiceConfig.model_fields.keys())
116
+ if key not in valid_fields:
117
+ c.output.error(f"Invalid key '{key}'. Valid keys: {', '.join(sorted(valid_fields))}")
118
+ raise typer.Exit(code=1)
119
+
120
+ pname = resolve_active_profile_name(c.profile)
121
+ svc = get_service_config(pname)
122
+ setattr(svc, key, value)
123
+ set_service_config(pname, svc)
124
+ display_value = _mask_token(value) if "token" in key else value
125
+ c.output.success(f"Set {key} = {display_value}")
126
+
127
+
128
+ @app.command()
129
+ def profiles(ctx: typer.Context) -> None:
130
+ """List all configuration profiles."""
131
+ c: AppContext = ctx.obj
132
+ names = get_profile_names()
133
+ default = get_default_profile()
134
+
135
+ rows = []
136
+ for name in names:
137
+ svc = get_service_config(name)
138
+ marker = "*" if name == default else ""
139
+ rows.append([marker, name, svc.url or "(not set)"])
140
+
141
+ c.output.table(
142
+ "Profiles",
143
+ [("", "cyan"), ("Name", "green"), ("URL", "white")],
144
+ rows,
145
+ data_for_json=[{"name": n, "default": n == default} for n in names],
146
+ )
147
+
148
+
149
+ @app.command("use")
150
+ def use_(
151
+ ctx: typer.Context,
152
+ name: Annotated[str, typer.Argument(help="Profile name to activate")],
153
+ ) -> None:
154
+ """Switch to a different profile."""
155
+ c: AppContext = ctx.obj
156
+ names = get_profile_names()
157
+ if name not in names:
158
+ c.output.error(f"Profile '{name}' not found. Available: {', '.join(names)}")
159
+ raise typer.Exit(code=1)
160
+ set_default_profile(name)
161
+ c.output.success(f"Switched to profile '{name}'")
162
+
163
+
164
+ @app.command()
165
+ def remove(
166
+ ctx: typer.Context,
167
+ name: Annotated[str, typer.Argument(help="Profile name to remove")],
168
+ ) -> None:
169
+ """Remove a configuration profile."""
170
+ c: AppContext = ctx.obj
171
+ remove_profile(name)
172
+ c.output.success(f"Removed profile '{name}'")
173
+
174
+
175
+ @app.command()
176
+ def test(ctx: typer.Context) -> None:
177
+ """Test the current connection."""
178
+ c: AppContext = ctx.obj
179
+ try:
180
+ status = c.client.check_health()
181
+ if status == 200:
182
+ c.output.success("Connection OK")
183
+ version = c.client.get_version()
184
+ if version:
185
+ c.output.info(f"OpenCloud {version}")
186
+ else:
187
+ c.output.error(f"Connection failed (HTTP {status})")
188
+ raise typer.Exit(code=1)
189
+ except Exception as e:
190
+ c.output.error(f"Connection failed: {e}")
191
+ raise typer.Exit(code=1)
@@ -0,0 +1,80 @@
1
+ """Dashboard commands for kctl-opencloud."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Annotated, Any
7
+
8
+ import typer
9
+ from kctl_lib.exceptions import AuthenticationError, ConfigError
10
+
11
+ from kctl_opencloud.core.callbacks import AppContext
12
+
13
+ app = typer.Typer(help="System overview and statistics.")
14
+
15
+
16
+ def _fetch_dashboard(c: AppContext) -> dict[str, Any]:
17
+ """Fetch dashboard data."""
18
+ client = c.client
19
+ data: dict[str, Any] = {}
20
+
21
+ # Version
22
+ data["version"] = client.get_version() or "unknown"
23
+
24
+ # Resource counts
25
+ for resource, endpoint in [("users", "users"), ("groups", "groups"), ("spaces", "drives")]:
26
+ try:
27
+ result = client.get(endpoint)
28
+ data[resource] = len(result.get("value", []))
29
+ except (AuthenticationError, ConfigError):
30
+ raise
31
+ except Exception:
32
+ data[resource] = 0
33
+
34
+ return data
35
+
36
+
37
+ def _display_dashboard(c: AppContext, data: dict[str, Any]) -> None:
38
+ """Display dashboard."""
39
+ out = c.output
40
+
41
+ if c.json_mode:
42
+ out.raw_json(data)
43
+ return
44
+
45
+ out.header("OpenCloud Dashboard")
46
+ out.kv("URL", c.client.root_url)
47
+ out.kv("Version", data["version"])
48
+ out.text("")
49
+ out.kv("Users", str(data.get("users", 0)))
50
+ out.kv("Groups", str(data.get("groups", 0)))
51
+ out.kv("Spaces", str(data.get("spaces", 0)))
52
+
53
+
54
+ @app.callback(invoke_without_command=True)
55
+ def show(
56
+ ctx: typer.Context,
57
+ watch: Annotated[bool, typer.Option("--watch", "-w", help="Continuous monitoring")] = False,
58
+ interval: Annotated[int, typer.Option("--interval", "-i", help="Watch interval")] = 10,
59
+ compact: Annotated[bool, typer.Option("--compact", "-c", help="Compact output")] = False,
60
+ ) -> None:
61
+ """Show system overview."""
62
+ c: AppContext = ctx.obj
63
+
64
+ if watch:
65
+ try:
66
+ while True:
67
+ data = _fetch_dashboard(c)
68
+ if compact:
69
+ c.output.text(f"Users: {data['users']} | Groups: {data['groups']} | Spaces: {data['spaces']}")
70
+ else:
71
+ _display_dashboard(c, data)
72
+ time.sleep(interval)
73
+ except KeyboardInterrupt:
74
+ pass
75
+ else:
76
+ data = _fetch_dashboard(c)
77
+ if compact:
78
+ c.output.text(f"Users: {data['users']} | Groups: {data['groups']} | Spaces: {data['spaces']}")
79
+ else:
80
+ _display_dashboard(c, data)