kctl-api 0.7.3__tar.gz → 0.8.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.
- {kctl_api-0.7.3 → kctl_api-0.8.0}/.gitignore +3 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/PKG-INFO +2 -2
- {kctl_api-0.7.3 → kctl_api-0.8.0}/pyproject.toml +2 -2
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/__init__.py +1 -1
- kctl_api-0.8.0/src/kctl_api/_register.py +127 -0
- kctl_api-0.8.0/src/kctl_api/cli.py +130 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/doctor_cmd.py +76 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_config_cmd.py +1 -3
- kctl_api-0.8.0/tests/test_doctor_ai_summary.py +16 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_users.py +0 -1
- kctl_api-0.7.3/src/kctl_api/cli.py +0 -245
- {kctl_api-0.7.3 → kctl_api-0.8.0}/README.md +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/skills/api-admin/SKILL.md +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/__main__.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/__init__.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/ai.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/aliases.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/apps.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/auth.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/automation.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/build_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/clean.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/config_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/dashboard.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/db.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/deploy.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/deps.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/dev.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/docker_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/env.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/files.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/fmt_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/health.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/jobs.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/lint_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/logs.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/marketplace.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/monitor_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/notifications.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/odoo_proxy.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/openapi.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/perf.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/rate_limit.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/realtime.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/redis_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/routes_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/saas.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/scaffold.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/security_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/services.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/shell.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/skill_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/streams.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/stripe_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/tenant_ai.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/test_cmd.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/users.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/webhooks.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/workflows.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/commands/ws.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/__init__.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/async_client.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/callbacks.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/client.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/config.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/db.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/exceptions.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/output.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/plugins.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/redis.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/resolve.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/src/kctl_api/core/utils.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/conftest.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_auth.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_cli.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_core.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_deploy.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_logs.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_smoke.py +0 -0
- {kctl_api-0.7.3 → kctl_api-0.8.0}/tests/test_standard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kctl-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Kodemeio API CLI - manage your FastAPI platform
|
|
5
5
|
Author-email: Kodemeio <dev@kodeme.io>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
14
14
|
Classifier: Topic :: System :: Systems Administration
|
|
15
15
|
Requires-Python: >=3.12
|
|
16
16
|
Requires-Dist: httpx>=0.28.0
|
|
17
|
-
Requires-Dist: kctl-lib>=0.
|
|
17
|
+
Requires-Dist: kctl-lib>=0.12.0
|
|
18
18
|
Provides-Extra: all
|
|
19
19
|
Requires-Dist: asyncpg>=0.30.0; extra == 'all'
|
|
20
20
|
Requires-Dist: redis>=5.0.0; extra == 'all'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kctl-api"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Kodemeio API CLI - manage your FastAPI platform"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -20,7 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Topic :: System :: Systems Administration",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"kctl-lib>=0.
|
|
23
|
+
"kctl-lib>=0.12.0",
|
|
24
24
|
"httpx>=0.28.0",
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Command-group registration for kctl-api (extracted from cli.py to keep it thin)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from kctl_lib.ai_summary import add_ai_summary_command
|
|
7
|
+
|
|
8
|
+
from kctl_api import __version__
|
|
9
|
+
from kctl_api.commands.ai import app as ai_app
|
|
10
|
+
from kctl_api.commands.aliases import register_aliases
|
|
11
|
+
from kctl_api.commands.apps import app as apps_app
|
|
12
|
+
from kctl_api.commands.auth import app as auth_app
|
|
13
|
+
from kctl_api.commands.automation import app as automation_app
|
|
14
|
+
from kctl_api.commands.build_cmd import app as build_app
|
|
15
|
+
from kctl_api.commands.clean import app as clean_app
|
|
16
|
+
from kctl_api.commands.config_cmd import app as config_app
|
|
17
|
+
from kctl_api.commands.dashboard import app as dashboard_app
|
|
18
|
+
from kctl_api.commands.db import app as db_app
|
|
19
|
+
from kctl_api.commands.deploy import app as deploy_app
|
|
20
|
+
from kctl_api.commands.deps import app as deps_app
|
|
21
|
+
from kctl_api.commands.dev import app as dev_app
|
|
22
|
+
from kctl_api.commands.docker_cmd import app as docker_app
|
|
23
|
+
from kctl_api.commands.doctor_cmd import app as doctor_app
|
|
24
|
+
from kctl_api.commands.doctor_cmd import build_checks as _doctor_build_checks
|
|
25
|
+
from kctl_api.commands.env import app as env_app
|
|
26
|
+
from kctl_api.commands.files import app as files_app
|
|
27
|
+
from kctl_api.commands.fmt_cmd import app as fmt_app
|
|
28
|
+
from kctl_api.commands.health import app as health_app
|
|
29
|
+
from kctl_api.commands.jobs import app as jobs_app
|
|
30
|
+
from kctl_api.commands.lint_cmd import app as lint_app
|
|
31
|
+
from kctl_api.commands.logs import app as logs_app
|
|
32
|
+
from kctl_api.commands.marketplace import app as marketplace_app
|
|
33
|
+
from kctl_api.commands.monitor_cmd import app as monitor_app
|
|
34
|
+
from kctl_api.commands.notifications import app as notifications_app
|
|
35
|
+
from kctl_api.commands.odoo_proxy import app as odoo_app
|
|
36
|
+
from kctl_api.commands.openapi import app as openapi_app
|
|
37
|
+
from kctl_api.commands.perf import app as perf_app
|
|
38
|
+
from kctl_api.commands.rate_limit import app as rate_limit_app
|
|
39
|
+
from kctl_api.commands.realtime import app as realtime_app
|
|
40
|
+
from kctl_api.commands.redis_cmd import app as redis_app
|
|
41
|
+
from kctl_api.commands.routes_cmd import app as routes_app
|
|
42
|
+
from kctl_api.commands.saas import app as saas_app
|
|
43
|
+
from kctl_api.commands.scaffold import app as scaffold_app
|
|
44
|
+
from kctl_api.commands.security_cmd import app as security_app
|
|
45
|
+
from kctl_api.commands.services import app as services_app
|
|
46
|
+
from kctl_api.commands.shell import app as shell_app
|
|
47
|
+
from kctl_api.commands.skill_cmd import app as skill_app
|
|
48
|
+
from kctl_api.commands.streams import app as streams_app
|
|
49
|
+
from kctl_api.commands.stripe_cmd import app as stripe_app
|
|
50
|
+
from kctl_api.commands.tenant_ai import app as tenant_ai_app
|
|
51
|
+
from kctl_api.commands.test_cmd import app as test_app
|
|
52
|
+
from kctl_api.commands.users import app as users_app
|
|
53
|
+
from kctl_api.commands.webhooks import app as webhooks_app
|
|
54
|
+
from kctl_api.commands.workflows import app as workflows_app
|
|
55
|
+
from kctl_api.commands.ws import app as ws_app
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register_all(app: typer.Typer) -> None:
|
|
59
|
+
"""Register all command groups on *app* in the canonical order."""
|
|
60
|
+
# Authentication & access
|
|
61
|
+
app.add_typer(config_app, name="config")
|
|
62
|
+
app.add_typer(auth_app, name="auth")
|
|
63
|
+
app.add_typer(health_app, name="health")
|
|
64
|
+
|
|
65
|
+
# API resources
|
|
66
|
+
app.add_typer(users_app, name="users")
|
|
67
|
+
app.add_typer(files_app, name="files")
|
|
68
|
+
app.add_typer(jobs_app, name="jobs")
|
|
69
|
+
app.add_typer(workflows_app, name="workflows")
|
|
70
|
+
app.add_typer(automation_app, name="automation")
|
|
71
|
+
app.add_typer(notifications_app, name="notifications")
|
|
72
|
+
app.add_typer(webhooks_app, name="webhooks")
|
|
73
|
+
app.add_typer(marketplace_app, name="marketplace")
|
|
74
|
+
app.add_typer(saas_app, name="saas")
|
|
75
|
+
app.add_typer(stripe_app, name="stripe")
|
|
76
|
+
app.add_typer(odoo_app, name="odoo")
|
|
77
|
+
app.add_typer(realtime_app, name="realtime")
|
|
78
|
+
|
|
79
|
+
# AI platform
|
|
80
|
+
app.add_typer(ai_app, name="ai")
|
|
81
|
+
app.add_typer(tenant_ai_app, name="tenant-ai")
|
|
82
|
+
|
|
83
|
+
# Infrastructure
|
|
84
|
+
app.add_typer(db_app, name="db")
|
|
85
|
+
app.add_typer(redis_app, name="redis")
|
|
86
|
+
app.add_typer(streams_app, name="streams")
|
|
87
|
+
app.add_typer(services_app, name="services")
|
|
88
|
+
app.add_typer(deploy_app, name="deploy")
|
|
89
|
+
app.add_typer(apps_app, name="apps")
|
|
90
|
+
app.add_typer(docker_app, name="docker")
|
|
91
|
+
|
|
92
|
+
# Development tools
|
|
93
|
+
app.add_typer(dev_app, name="dev")
|
|
94
|
+
app.add_typer(test_app, name="test")
|
|
95
|
+
app.add_typer(lint_app, name="lint")
|
|
96
|
+
app.add_typer(fmt_app, name="fmt")
|
|
97
|
+
app.add_typer(build_app, name="build")
|
|
98
|
+
app.add_typer(scaffold_app, name="scaffold")
|
|
99
|
+
app.add_typer(shell_app, name="shell")
|
|
100
|
+
|
|
101
|
+
# API analysis & testing
|
|
102
|
+
app.add_typer(openapi_app, name="openapi")
|
|
103
|
+
app.add_typer(routes_app, name="routes")
|
|
104
|
+
app.add_typer(rate_limit_app, name="rate-limit")
|
|
105
|
+
app.add_typer(ws_app, name="ws")
|
|
106
|
+
app.add_typer(perf_app, name="perf")
|
|
107
|
+
|
|
108
|
+
# Environment & security
|
|
109
|
+
app.add_typer(doctor_app, name="doctor")
|
|
110
|
+
add_ai_summary_command(
|
|
111
|
+
doctor_app, cli_name="kctl-api", version=__version__, app=app, get_checks=_doctor_build_checks
|
|
112
|
+
)
|
|
113
|
+
app.add_typer(env_app, name="env")
|
|
114
|
+
app.add_typer(security_app, name="security")
|
|
115
|
+
app.add_typer(deps_app, name="deps")
|
|
116
|
+
app.add_typer(clean_app, name="clean")
|
|
117
|
+
|
|
118
|
+
# Observability & monitoring
|
|
119
|
+
app.add_typer(logs_app, name="logs")
|
|
120
|
+
app.add_typer(dashboard_app, name="dashboard")
|
|
121
|
+
app.add_typer(monitor_app, name="monitor")
|
|
122
|
+
|
|
123
|
+
# Skill generation
|
|
124
|
+
app.add_typer(skill_app, name="skill")
|
|
125
|
+
|
|
126
|
+
# Short aliases
|
|
127
|
+
register_aliases(app)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Main CLI entry point for kctl-api."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from kctl_lib import KctlError, cli_entrypoint, handle_cli_error, register_introspection_commands
|
|
9
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
10
|
+
from kctl_lib.tui import add_tui_command
|
|
11
|
+
|
|
12
|
+
from kctl_api import __version__
|
|
13
|
+
from kctl_api._register import register_all
|
|
14
|
+
from kctl_api.core.callbacks import AppContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def version_callback(value: bool) -> None:
|
|
18
|
+
if value:
|
|
19
|
+
typer.echo(f"kctl-api {__version__}")
|
|
20
|
+
raise typer.Exit()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name="kctl-api",
|
|
25
|
+
help="Kodemeio API CLI - manage your FastAPI platform.",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
rich_markup_mode="rich",
|
|
28
|
+
pretty_exceptions_enable=False,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.callback()
|
|
33
|
+
def main(
|
|
34
|
+
ctx: typer.Context,
|
|
35
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
36
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
37
|
+
output_format: Annotated[
|
|
38
|
+
str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")
|
|
39
|
+
] = "pretty",
|
|
40
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Omit table headers (for scripting)")] = False,
|
|
41
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
42
|
+
url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
|
|
43
|
+
ai_url: Annotated[str | None, typer.Option("--ai-url", help="AI API URL override")] = None,
|
|
44
|
+
api_key: Annotated[str | None, typer.Option("--api-key", help="API key override")] = None,
|
|
45
|
+
database_url: Annotated[str | None, typer.Option("--database-url", help="Database URL override")] = None,
|
|
46
|
+
redis_url: Annotated[str | None, typer.Option("--redis-url", help="Redis URL override")] = None,
|
|
47
|
+
version: Annotated[
|
|
48
|
+
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
49
|
+
] = False,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Kodemeio API CLI."""
|
|
52
|
+
ctx.ensure_object(dict)
|
|
53
|
+
ctx.obj = AppContext(
|
|
54
|
+
json_mode=json_output or output_format == "json",
|
|
55
|
+
quiet=quiet,
|
|
56
|
+
format=output_format,
|
|
57
|
+
no_header=no_header,
|
|
58
|
+
profile=profile,
|
|
59
|
+
url_override=url,
|
|
60
|
+
ai_url_override=ai_url,
|
|
61
|
+
api_key_override=api_key,
|
|
62
|
+
database_url_override=database_url,
|
|
63
|
+
redis_url_override=redis_url,
|
|
64
|
+
)
|
|
65
|
+
notify_if_outdated(ctx.obj.output, "kctl-api", __version__)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
register_all(app)
|
|
69
|
+
|
|
70
|
+
# Load plugins via entry points
|
|
71
|
+
from kctl_api.core.plugins import discover_and_load_plugins # noqa: E402
|
|
72
|
+
|
|
73
|
+
discover_and_load_plugins(app)
|
|
74
|
+
|
|
75
|
+
add_tui_command(app, service_key="api", version=__version__)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("self-update")
|
|
79
|
+
def self_update_cmd(ctx: typer.Context) -> None:
|
|
80
|
+
"""Check for updates and upgrade kctl-api."""
|
|
81
|
+
actx = ctx.obj
|
|
82
|
+
out = actx.output
|
|
83
|
+
|
|
84
|
+
from kctl_lib.self_update import check_update
|
|
85
|
+
from kctl_lib.self_update import update as do_update
|
|
86
|
+
|
|
87
|
+
latest = check_update("kctl-api", __version__)
|
|
88
|
+
if latest:
|
|
89
|
+
out.info(f"Updating to {latest}...")
|
|
90
|
+
do_update("kctl-api")
|
|
91
|
+
out.success(f"Updated to {latest}")
|
|
92
|
+
else:
|
|
93
|
+
out.success("Already up to date")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command()
|
|
97
|
+
def completions(
|
|
98
|
+
shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
|
|
99
|
+
install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Generate or install shell completions."""
|
|
102
|
+
from kctl_lib.completions import get_completion_script, install_completions
|
|
103
|
+
|
|
104
|
+
if install:
|
|
105
|
+
path = install_completions("kctl-api", shell)
|
|
106
|
+
if path:
|
|
107
|
+
typer.echo(f"Completions installed to {path}")
|
|
108
|
+
else:
|
|
109
|
+
typer.echo(f"Could not install completions for {shell}", err=True)
|
|
110
|
+
raise typer.Exit(code=1)
|
|
111
|
+
else:
|
|
112
|
+
script = get_completion_script("kctl-api", shell)
|
|
113
|
+
typer.echo(script)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Wrap app so KctlError subclasses surface as clean user-facing messages.
|
|
117
|
+
app = cli_entrypoint(app)
|
|
118
|
+
register_introspection_commands(app)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _run() -> None:
|
|
122
|
+
"""Entry point with error handling."""
|
|
123
|
+
try:
|
|
124
|
+
app()
|
|
125
|
+
except KctlError as e:
|
|
126
|
+
handle_cli_error(e)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
_run()
|
|
@@ -8,8 +8,10 @@ from __future__ import annotations
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import shutil
|
|
10
10
|
import subprocess
|
|
11
|
+
from dataclasses import dataclass
|
|
11
12
|
|
|
12
13
|
import typer
|
|
14
|
+
from kctl_lib.doctor_base import CheckResult, DoctorCheck
|
|
13
15
|
|
|
14
16
|
from kctl_api.core.callbacks import AppContext
|
|
15
17
|
from kctl_api.core.utils import find_project_root
|
|
@@ -136,6 +138,80 @@ def _run_all_checks(actx: AppContext) -> list[dict]:
|
|
|
136
138
|
return checks
|
|
137
139
|
|
|
138
140
|
|
|
141
|
+
@dataclass
|
|
142
|
+
class _DictCheck:
|
|
143
|
+
"""Adapter: wraps a no-arg callable returning a dict into DoctorCheck."""
|
|
144
|
+
|
|
145
|
+
_fn: object # callable[[], dict]
|
|
146
|
+
name: str = ""
|
|
147
|
+
|
|
148
|
+
def __post_init__(self) -> None:
|
|
149
|
+
if not self.name:
|
|
150
|
+
|
|
151
|
+
self.name = getattr(self._fn, "__name__", "check").replace("_check_", "").replace("_", " ").title()
|
|
152
|
+
|
|
153
|
+
def run(self) -> CheckResult:
|
|
154
|
+
try:
|
|
155
|
+
result = self._fn() # type: ignore[operator]
|
|
156
|
+
status = "ok" if result.get("ok") else "fail"
|
|
157
|
+
msg_parts = []
|
|
158
|
+
if result.get("version"):
|
|
159
|
+
msg_parts.append(result["version"])
|
|
160
|
+
if result.get("issue"):
|
|
161
|
+
msg_parts.append(result["issue"])
|
|
162
|
+
message = "; ".join(msg_parts) or ("OK" if status == "ok" else "failed")
|
|
163
|
+
return CheckResult(name=self.name, status=status, message=message)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
return CheckResult(name=self.name, status="warn", message=str(exc))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class _AsyncDictCheck:
|
|
170
|
+
"""Adapter: wraps an async callable returning a dict into DoctorCheck."""
|
|
171
|
+
|
|
172
|
+
_fn: object # async callable[[AppContext], dict]
|
|
173
|
+
_actx: AppContext
|
|
174
|
+
name: str = ""
|
|
175
|
+
|
|
176
|
+
def __post_init__(self) -> None:
|
|
177
|
+
if not self.name:
|
|
178
|
+
self.name = (
|
|
179
|
+
getattr(self._fn, "__name__", "check")
|
|
180
|
+
.replace("_check_", "")
|
|
181
|
+
.replace("_async", "")
|
|
182
|
+
.replace("_", " ")
|
|
183
|
+
.title()
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def run(self) -> CheckResult:
|
|
187
|
+
try:
|
|
188
|
+
result = asyncio.run(self._fn(self._actx)) # type: ignore[operator]
|
|
189
|
+
status = "ok" if result.get("ok") else "fail"
|
|
190
|
+
msg_parts = []
|
|
191
|
+
if result.get("version"):
|
|
192
|
+
msg_parts.append(result["version"])
|
|
193
|
+
if result.get("issue"):
|
|
194
|
+
msg_parts.append(result["issue"])
|
|
195
|
+
message = "; ".join(msg_parts) or ("OK" if status == "ok" else "failed")
|
|
196
|
+
return CheckResult(name=self.name, status=status, message=message)
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
return CheckResult(name=self.name, status="warn", message=str(exc))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_checks(ctx: typer.Context) -> list[DoctorCheck]:
|
|
202
|
+
"""Return the standard list of doctor checks (no side effects)."""
|
|
203
|
+
actx: AppContext = ctx.obj
|
|
204
|
+
return [
|
|
205
|
+
_DictCheck(_fn=_check_python, name="Python"),
|
|
206
|
+
_DictCheck(_fn=lambda: _check_binary("uv", "--version"), name="uv"),
|
|
207
|
+
_DictCheck(_fn=lambda: _check_binary("docker", "--version"), name="Docker"),
|
|
208
|
+
_DictCheck(_fn=lambda: _check_binary("git", "--version"), name="git"),
|
|
209
|
+
_DictCheck(_fn=_check_env_file, name=".env"),
|
|
210
|
+
_AsyncDictCheck(_fn=_check_postgres_async, _actx=actx, name="PostgreSQL"),
|
|
211
|
+
_AsyncDictCheck(_fn=_check_redis_async, _actx=actx, name="Redis"),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
|
|
139
215
|
# ---------------------------------------------------------------------------
|
|
140
216
|
# check
|
|
141
217
|
# ---------------------------------------------------------------------------
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from unittest.mock import
|
|
6
|
+
from unittest.mock import patch
|
|
7
7
|
|
|
8
|
-
import pytest
|
|
9
8
|
from typer.testing import CliRunner
|
|
10
9
|
|
|
11
10
|
from kctl_api.cli import app
|
|
12
|
-
from kctl_api.core.config import ServiceConfig
|
|
13
11
|
|
|
14
12
|
runner = CliRunner()
|
|
15
13
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from typer.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from kctl_api.cli import app
|
|
6
|
+
|
|
7
|
+
runner = CliRunner()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_doctor_ai_summary_json_runs():
|
|
11
|
+
result = runner.invoke(app, ["--json", "doctor", "ai-summary"])
|
|
12
|
+
assert result.exit_code == 0
|
|
13
|
+
out = result.output
|
|
14
|
+
data = json.loads(out[out.index("{"):]) # tolerate a leading notify/update line
|
|
15
|
+
assert data["cli"] == "kctl-api"
|
|
16
|
+
assert "health" in data and "checks" in data
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
"""Main CLI entry point for kctl-api."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Annotated
|
|
6
|
-
|
|
7
|
-
import typer
|
|
8
|
-
from kctl_lib import KctlError, cli_entrypoint, handle_cli_error, register_introspection_commands
|
|
9
|
-
from kctl_lib.self_update import notify_if_outdated
|
|
10
|
-
from kctl_lib.tui import add_tui_command
|
|
11
|
-
|
|
12
|
-
from kctl_api import __version__
|
|
13
|
-
from kctl_api.core.callbacks import AppContext
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def version_callback(value: bool) -> None:
|
|
17
|
-
if value:
|
|
18
|
-
typer.echo(f"kctl-api {__version__}")
|
|
19
|
-
raise typer.Exit()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
app = typer.Typer(
|
|
23
|
-
name="kctl-api",
|
|
24
|
-
help="Kodemeio API CLI - manage your FastAPI platform.",
|
|
25
|
-
no_args_is_help=True,
|
|
26
|
-
rich_markup_mode="rich",
|
|
27
|
-
pretty_exceptions_enable=False,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@app.callback()
|
|
32
|
-
def main(
|
|
33
|
-
ctx: typer.Context,
|
|
34
|
-
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
35
|
-
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
36
|
-
output_format: Annotated[
|
|
37
|
-
str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")
|
|
38
|
-
] = "pretty",
|
|
39
|
-
no_header: Annotated[bool, typer.Option("--no-header", help="Omit table headers (for scripting)")] = False,
|
|
40
|
-
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
41
|
-
url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
|
|
42
|
-
ai_url: Annotated[str | None, typer.Option("--ai-url", help="AI API URL override")] = None,
|
|
43
|
-
api_key: Annotated[str | None, typer.Option("--api-key", help="API key override")] = None,
|
|
44
|
-
database_url: Annotated[str | None, typer.Option("--database-url", help="Database URL override")] = None,
|
|
45
|
-
redis_url: Annotated[str | None, typer.Option("--redis-url", help="Redis URL override")] = None,
|
|
46
|
-
version: Annotated[
|
|
47
|
-
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
48
|
-
] = False,
|
|
49
|
-
) -> None:
|
|
50
|
-
"""Kodemeio API CLI."""
|
|
51
|
-
ctx.ensure_object(dict)
|
|
52
|
-
ctx.obj = AppContext(
|
|
53
|
-
json_mode=json_output or output_format == "json",
|
|
54
|
-
quiet=quiet,
|
|
55
|
-
format=output_format,
|
|
56
|
-
no_header=no_header,
|
|
57
|
-
profile=profile,
|
|
58
|
-
url_override=url,
|
|
59
|
-
ai_url_override=ai_url,
|
|
60
|
-
api_key_override=api_key,
|
|
61
|
-
database_url_override=database_url,
|
|
62
|
-
redis_url_override=redis_url,
|
|
63
|
-
)
|
|
64
|
-
notify_if_outdated(ctx.obj.output, "kctl-api", __version__)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _register_commands() -> None:
|
|
68
|
-
"""Register all command groups (lazy imports for fast startup)."""
|
|
69
|
-
from kctl_api.commands.ai import app as ai_app
|
|
70
|
-
from kctl_api.commands.aliases import register_aliases
|
|
71
|
-
from kctl_api.commands.apps import app as apps_app
|
|
72
|
-
from kctl_api.commands.auth import app as auth_app
|
|
73
|
-
from kctl_api.commands.automation import app as automation_app
|
|
74
|
-
from kctl_api.commands.build_cmd import app as build_app
|
|
75
|
-
from kctl_api.commands.clean import app as clean_app
|
|
76
|
-
from kctl_api.commands.config_cmd import app as config_app
|
|
77
|
-
from kctl_api.commands.dashboard import app as dashboard_app
|
|
78
|
-
from kctl_api.commands.db import app as db_app
|
|
79
|
-
from kctl_api.commands.deploy import app as deploy_app
|
|
80
|
-
from kctl_api.commands.deps import app as deps_app
|
|
81
|
-
from kctl_api.commands.dev import app as dev_app
|
|
82
|
-
from kctl_api.commands.docker_cmd import app as docker_app
|
|
83
|
-
from kctl_api.commands.doctor_cmd import app as doctor_app
|
|
84
|
-
from kctl_api.commands.env import app as env_app
|
|
85
|
-
from kctl_api.commands.files import app as files_app
|
|
86
|
-
from kctl_api.commands.fmt_cmd import app as fmt_app
|
|
87
|
-
from kctl_api.commands.health import app as health_app
|
|
88
|
-
from kctl_api.commands.jobs import app as jobs_app
|
|
89
|
-
from kctl_api.commands.lint_cmd import app as lint_app
|
|
90
|
-
from kctl_api.commands.logs import app as logs_app
|
|
91
|
-
from kctl_api.commands.marketplace import app as marketplace_app
|
|
92
|
-
from kctl_api.commands.monitor_cmd import app as monitor_app
|
|
93
|
-
from kctl_api.commands.notifications import app as notifications_app
|
|
94
|
-
from kctl_api.commands.odoo_proxy import app as odoo_app
|
|
95
|
-
from kctl_api.commands.openapi import app as openapi_app
|
|
96
|
-
from kctl_api.commands.perf import app as perf_app
|
|
97
|
-
from kctl_api.commands.rate_limit import app as rate_limit_app
|
|
98
|
-
from kctl_api.commands.realtime import app as realtime_app
|
|
99
|
-
from kctl_api.commands.redis_cmd import app as redis_app
|
|
100
|
-
from kctl_api.commands.routes_cmd import app as routes_app
|
|
101
|
-
from kctl_api.commands.saas import app as saas_app
|
|
102
|
-
from kctl_api.commands.scaffold import app as scaffold_app
|
|
103
|
-
from kctl_api.commands.security_cmd import app as security_app
|
|
104
|
-
from kctl_api.commands.services import app as services_app
|
|
105
|
-
from kctl_api.commands.shell import app as shell_app
|
|
106
|
-
from kctl_api.commands.streams import app as streams_app
|
|
107
|
-
from kctl_api.commands.stripe_cmd import app as stripe_app
|
|
108
|
-
from kctl_api.commands.tenant_ai import app as tenant_ai_app
|
|
109
|
-
from kctl_api.commands.test_cmd import app as test_app
|
|
110
|
-
from kctl_api.commands.users import app as users_app
|
|
111
|
-
from kctl_api.commands.webhooks import app as webhooks_app
|
|
112
|
-
from kctl_api.commands.workflows import app as workflows_app
|
|
113
|
-
from kctl_api.commands.ws import app as ws_app
|
|
114
|
-
|
|
115
|
-
# Authentication & access
|
|
116
|
-
app.add_typer(config_app, name="config")
|
|
117
|
-
app.add_typer(auth_app, name="auth")
|
|
118
|
-
app.add_typer(health_app, name="health")
|
|
119
|
-
|
|
120
|
-
# API resources
|
|
121
|
-
app.add_typer(users_app, name="users")
|
|
122
|
-
app.add_typer(files_app, name="files")
|
|
123
|
-
app.add_typer(jobs_app, name="jobs")
|
|
124
|
-
app.add_typer(workflows_app, name="workflows")
|
|
125
|
-
app.add_typer(automation_app, name="automation")
|
|
126
|
-
app.add_typer(notifications_app, name="notifications")
|
|
127
|
-
app.add_typer(webhooks_app, name="webhooks")
|
|
128
|
-
app.add_typer(marketplace_app, name="marketplace")
|
|
129
|
-
app.add_typer(saas_app, name="saas")
|
|
130
|
-
app.add_typer(stripe_app, name="stripe")
|
|
131
|
-
app.add_typer(odoo_app, name="odoo")
|
|
132
|
-
app.add_typer(realtime_app, name="realtime")
|
|
133
|
-
|
|
134
|
-
# AI platform
|
|
135
|
-
app.add_typer(ai_app, name="ai")
|
|
136
|
-
app.add_typer(tenant_ai_app, name="tenant-ai")
|
|
137
|
-
|
|
138
|
-
# Infrastructure
|
|
139
|
-
app.add_typer(db_app, name="db")
|
|
140
|
-
app.add_typer(redis_app, name="redis")
|
|
141
|
-
app.add_typer(streams_app, name="streams")
|
|
142
|
-
app.add_typer(services_app, name="services")
|
|
143
|
-
app.add_typer(deploy_app, name="deploy")
|
|
144
|
-
app.add_typer(apps_app, name="apps")
|
|
145
|
-
app.add_typer(docker_app, name="docker")
|
|
146
|
-
|
|
147
|
-
# Development tools
|
|
148
|
-
app.add_typer(dev_app, name="dev")
|
|
149
|
-
app.add_typer(test_app, name="test")
|
|
150
|
-
app.add_typer(lint_app, name="lint")
|
|
151
|
-
app.add_typer(fmt_app, name="fmt")
|
|
152
|
-
app.add_typer(build_app, name="build")
|
|
153
|
-
app.add_typer(scaffold_app, name="scaffold")
|
|
154
|
-
app.add_typer(shell_app, name="shell")
|
|
155
|
-
|
|
156
|
-
# API analysis & testing
|
|
157
|
-
app.add_typer(openapi_app, name="openapi")
|
|
158
|
-
app.add_typer(routes_app, name="routes")
|
|
159
|
-
app.add_typer(rate_limit_app, name="rate-limit")
|
|
160
|
-
app.add_typer(ws_app, name="ws")
|
|
161
|
-
app.add_typer(perf_app, name="perf")
|
|
162
|
-
|
|
163
|
-
# Environment & security
|
|
164
|
-
app.add_typer(doctor_app, name="doctor")
|
|
165
|
-
app.add_typer(env_app, name="env")
|
|
166
|
-
app.add_typer(security_app, name="security")
|
|
167
|
-
app.add_typer(deps_app, name="deps")
|
|
168
|
-
app.add_typer(clean_app, name="clean")
|
|
169
|
-
|
|
170
|
-
# Observability & monitoring
|
|
171
|
-
app.add_typer(logs_app, name="logs")
|
|
172
|
-
app.add_typer(dashboard_app, name="dashboard")
|
|
173
|
-
app.add_typer(monitor_app, name="monitor")
|
|
174
|
-
|
|
175
|
-
# Skill generation
|
|
176
|
-
from kctl_api.commands.skill_cmd import app as skill_app
|
|
177
|
-
|
|
178
|
-
app.add_typer(skill_app, name="skill")
|
|
179
|
-
|
|
180
|
-
# Short aliases
|
|
181
|
-
register_aliases(app)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
_register_commands()
|
|
185
|
-
|
|
186
|
-
# Load plugins via entry points
|
|
187
|
-
from kctl_api.core.plugins import discover_and_load_plugins # noqa: E402
|
|
188
|
-
|
|
189
|
-
discover_and_load_plugins(app)
|
|
190
|
-
|
|
191
|
-
add_tui_command(app, service_key="api", version=__version__)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@app.command("self-update")
|
|
195
|
-
def self_update_cmd(ctx: typer.Context) -> None:
|
|
196
|
-
"""Check for updates and upgrade kctl-api."""
|
|
197
|
-
actx = ctx.obj
|
|
198
|
-
out = actx.output
|
|
199
|
-
|
|
200
|
-
from kctl_lib.self_update import check_update
|
|
201
|
-
from kctl_lib.self_update import update as do_update
|
|
202
|
-
|
|
203
|
-
latest = check_update("kctl-api", __version__)
|
|
204
|
-
if latest:
|
|
205
|
-
out.info(f"Updating to {latest}...")
|
|
206
|
-
do_update("kctl-api")
|
|
207
|
-
out.success(f"Updated to {latest}")
|
|
208
|
-
else:
|
|
209
|
-
out.success("Already up to date")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@app.command()
|
|
213
|
-
def completions(
|
|
214
|
-
shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
|
|
215
|
-
install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
|
|
216
|
-
) -> None:
|
|
217
|
-
"""Generate or install shell completions."""
|
|
218
|
-
from kctl_lib.completions import get_completion_script, install_completions
|
|
219
|
-
|
|
220
|
-
if install:
|
|
221
|
-
path = install_completions("kctl-api", shell)
|
|
222
|
-
if path:
|
|
223
|
-
typer.echo(f"Completions installed to {path}")
|
|
224
|
-
else:
|
|
225
|
-
typer.echo(f"Could not install completions for {shell}", err=True)
|
|
226
|
-
raise typer.Exit(code=1)
|
|
227
|
-
else:
|
|
228
|
-
script = get_completion_script("kctl-api", shell)
|
|
229
|
-
typer.echo(script)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# Wrap app so KctlError subclasses surface as clean user-facing messages.
|
|
233
|
-
app = cli_entrypoint(app)
|
|
234
|
-
register_introspection_commands(app)
|
|
235
|
-
|
|
236
|
-
def _run() -> None:
|
|
237
|
-
"""Entry point with error handling."""
|
|
238
|
-
try:
|
|
239
|
-
app()
|
|
240
|
-
except KctlError as e:
|
|
241
|
-
handle_cli_error(e)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if __name__ == "__main__":
|
|
245
|
-
_run()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|