kctl-payload 0.6.1__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.
- kctl_payload/__init__.py +3 -0
- kctl_payload/__main__.py +5 -0
- kctl_payload/cli.py +97 -0
- kctl_payload/commands/__init__.py +0 -0
- kctl_payload/commands/collections.py +155 -0
- kctl_payload/commands/config_cmd.py +105 -0
- kctl_payload/commands/doctor.py +82 -0
- kctl_payload/commands/globals_cmd.py +48 -0
- kctl_payload/commands/media.py +85 -0
- kctl_payload/commands/users.py +58 -0
- kctl_payload/core/__init__.py +0 -0
- kctl_payload/core/callbacks.py +39 -0
- kctl_payload/core/client.py +62 -0
- kctl_payload/core/config.py +134 -0
- kctl_payload-0.6.1.dist-info/METADATA +17 -0
- kctl_payload-0.6.1.dist-info/RECORD +18 -0
- kctl_payload-0.6.1.dist-info/WHEEL +4 -0
- kctl_payload-0.6.1.dist-info/entry_points.txt +2 -0
kctl_payload/__init__.py
ADDED
kctl_payload/__main__.py
ADDED
kctl_payload/cli.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Main CLI entry point for kctl-payload."""
|
|
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_payload import __version__
|
|
11
|
+
from kctl_payload.commands.collections import app as collections_app
|
|
12
|
+
from kctl_payload.commands.config_cmd import app as config_app
|
|
13
|
+
from kctl_payload.commands.doctor import app as doctor_app
|
|
14
|
+
from kctl_payload.commands.globals_cmd import app as globals_app
|
|
15
|
+
from kctl_payload.commands.media import app as media_app
|
|
16
|
+
from kctl_payload.commands.users import app as users_app
|
|
17
|
+
from kctl_payload.core.callbacks import AppContext
|
|
18
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
19
|
+
from kctl_lib.tui import add_tui_command
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def version_callback(value: bool) -> None:
|
|
23
|
+
if value:
|
|
24
|
+
typer.echo(f"kctl-payload {__version__}")
|
|
25
|
+
raise typer.Exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(
|
|
29
|
+
name="kctl-payload",
|
|
30
|
+
help="Kodemeio Payload CMS CLI - manage your Payload CMS instance.",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
rich_markup_mode="rich",
|
|
33
|
+
pretty_exceptions_enable=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.callback()
|
|
38
|
+
def main(
|
|
39
|
+
ctx: typer.Context,
|
|
40
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON (shortcut for --format json)")] = False,
|
|
41
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
42
|
+
output_format: Annotated[
|
|
43
|
+
str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")
|
|
44
|
+
] = "pretty",
|
|
45
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Omit headers in CSV output")] = False,
|
|
46
|
+
debug: Annotated[bool, typer.Option("--debug", help="Enable debug logging")] = False,
|
|
47
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
48
|
+
url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
|
|
49
|
+
api_key: Annotated[str | None, typer.Option("--api-key", help="API key override")] = None,
|
|
50
|
+
version: Annotated[
|
|
51
|
+
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
52
|
+
] = False,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Kodemeio Payload CMS CLI."""
|
|
55
|
+
import os
|
|
56
|
+
|
|
57
|
+
if debug:
|
|
58
|
+
os.environ["KCTL_DEBUG"] = "1"
|
|
59
|
+
|
|
60
|
+
effective_format = "json" if json_output else output_format
|
|
61
|
+
|
|
62
|
+
ctx.ensure_object(dict)
|
|
63
|
+
ctx.obj = AppContext(
|
|
64
|
+
json_mode=json_output or effective_format == "json",
|
|
65
|
+
quiet=quiet,
|
|
66
|
+
format=effective_format,
|
|
67
|
+
no_header=no_header,
|
|
68
|
+
debug=debug,
|
|
69
|
+
profile=profile,
|
|
70
|
+
url_override=url,
|
|
71
|
+
api_key_override=api_key,
|
|
72
|
+
)
|
|
73
|
+
notify_if_outdated(ctx.obj.output, "kctl-payload", __version__)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Command groups
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
app.add_typer(config_app, name="config")
|
|
80
|
+
app.add_typer(collections_app, name="collections")
|
|
81
|
+
app.add_typer(media_app, name="media")
|
|
82
|
+
app.add_typer(globals_app, name="globals")
|
|
83
|
+
app.add_typer(users_app, name="users")
|
|
84
|
+
app.add_typer(doctor_app, name="doctor")
|
|
85
|
+
add_tui_command(app, service_key="payload", version=__version__)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _run() -> None:
|
|
89
|
+
"""Entry point with error handling."""
|
|
90
|
+
try:
|
|
91
|
+
app()
|
|
92
|
+
except KctlError as e:
|
|
93
|
+
handle_cli_error(e)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
_run()
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Collection management commands for Payload CMS.
|
|
2
|
+
|
|
3
|
+
Supports CRUD operations on any Payload collection via REST API.
|
|
4
|
+
Known collections: blog-posts, testimonials, faqs, pages, campaigns, media
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from kctl_payload.core.callbacks import AppContext
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Manage Payload CMS collections.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("list")
|
|
20
|
+
def list_cmd(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
collection: Annotated[str, typer.Argument(help="Collection slug (e.g. blog-posts, pages)")],
|
|
23
|
+
status: Annotated[str | None, typer.Option("--status", "-s", help="Filter by _status field")] = None,
|
|
24
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Number of items to return")] = 10,
|
|
25
|
+
page: Annotated[int, typer.Option("--page", help="Page number")] = 1,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""List documents in a collection."""
|
|
28
|
+
c: AppContext = ctx.obj
|
|
29
|
+
out = c.output
|
|
30
|
+
params: dict[str, str | int] = {"limit": limit, "page": page}
|
|
31
|
+
if status:
|
|
32
|
+
params["where[_status][equals]"] = status
|
|
33
|
+
|
|
34
|
+
result = c.client.get(f"/{collection}", params=params)
|
|
35
|
+
docs = result.get("docs", []) if isinstance(result, dict) else result
|
|
36
|
+
out.table(
|
|
37
|
+
docs,
|
|
38
|
+
columns=["id", "title", "slug", "_status", "createdAt", "updatedAt"],
|
|
39
|
+
title=f"{collection} (page {page})",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def get(
|
|
45
|
+
ctx: typer.Context,
|
|
46
|
+
collection: Annotated[str, typer.Argument(help="Collection slug")],
|
|
47
|
+
doc_id: Annotated[str, typer.Argument(help="Document ID")],
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Get a single document by ID."""
|
|
50
|
+
c: AppContext = ctx.obj
|
|
51
|
+
out = c.output
|
|
52
|
+
result = c.client.get(f"/{collection}/{doc_id}")
|
|
53
|
+
out.raw_json(result)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command()
|
|
57
|
+
def create(
|
|
58
|
+
ctx: typer.Context,
|
|
59
|
+
collection: Annotated[str, typer.Argument(help="Collection slug")],
|
|
60
|
+
data: Annotated[str, typer.Option("--data", "-d", help="JSON string of document data")],
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Create a new document in a collection."""
|
|
63
|
+
c: AppContext = ctx.obj
|
|
64
|
+
out = c.output
|
|
65
|
+
try:
|
|
66
|
+
payload = json.loads(data)
|
|
67
|
+
except json.JSONDecodeError as e:
|
|
68
|
+
out.error(f"Invalid JSON: {e}")
|
|
69
|
+
raise typer.Exit(1) from e
|
|
70
|
+
|
|
71
|
+
result = c.client.post(f"/{collection}", json=payload)
|
|
72
|
+
doc = result.get("doc", result) if isinstance(result, dict) else result
|
|
73
|
+
out.success(f"Created document in {collection}")
|
|
74
|
+
out.raw_json(doc)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command()
|
|
78
|
+
def update(
|
|
79
|
+
ctx: typer.Context,
|
|
80
|
+
collection: Annotated[str, typer.Argument(help="Collection slug")],
|
|
81
|
+
doc_id: Annotated[str, typer.Argument(help="Document ID")],
|
|
82
|
+
data: Annotated[str, typer.Option("--data", "-d", help="JSON string of fields to update")],
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Update an existing document."""
|
|
85
|
+
c: AppContext = ctx.obj
|
|
86
|
+
out = c.output
|
|
87
|
+
try:
|
|
88
|
+
payload = json.loads(data)
|
|
89
|
+
except json.JSONDecodeError as e:
|
|
90
|
+
out.error(f"Invalid JSON: {e}")
|
|
91
|
+
raise typer.Exit(1) from e
|
|
92
|
+
|
|
93
|
+
result = c.client.patch(f"/{collection}/{doc_id}", json=payload)
|
|
94
|
+
doc = result.get("doc", result) if isinstance(result, dict) else result
|
|
95
|
+
out.success(f"Updated document {doc_id} in {collection}")
|
|
96
|
+
out.raw_json(doc)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command()
|
|
100
|
+
def delete(
|
|
101
|
+
ctx: typer.Context,
|
|
102
|
+
collection: Annotated[str, typer.Argument(help="Collection slug")],
|
|
103
|
+
doc_id: Annotated[str, typer.Argument(help="Document ID")],
|
|
104
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation")] = False,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Delete a document from a collection."""
|
|
107
|
+
c: AppContext = ctx.obj
|
|
108
|
+
out = c.output
|
|
109
|
+
if not force:
|
|
110
|
+
typer.confirm(f"Delete document {doc_id} from {collection}?", abort=True)
|
|
111
|
+
|
|
112
|
+
c.client.delete(f"/{collection}/{doc_id}")
|
|
113
|
+
out.success(f"Deleted document {doc_id} from {collection}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command()
|
|
117
|
+
def query(
|
|
118
|
+
ctx: typer.Context,
|
|
119
|
+
collection: Annotated[str, typer.Argument(help="Collection slug")],
|
|
120
|
+
where: Annotated[str | None, typer.Option("--where", "-w", help="Payload where query as JSON")] = None,
|
|
121
|
+
sort: Annotated[str | None, typer.Option("--sort", help="Sort field (prefix with - for desc)")] = None,
|
|
122
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Number of items to return")] = 10,
|
|
123
|
+
page: Annotated[int, typer.Option("--page", help="Page number")] = 1,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Query a collection with advanced filters."""
|
|
126
|
+
c: AppContext = ctx.obj
|
|
127
|
+
out = c.output
|
|
128
|
+
params: dict[str, str | int] = {"limit": limit, "page": page}
|
|
129
|
+
if sort:
|
|
130
|
+
params["sort"] = sort
|
|
131
|
+
|
|
132
|
+
# Parse where clause from JSON into Payload query params
|
|
133
|
+
if where:
|
|
134
|
+
try:
|
|
135
|
+
where_obj = json.loads(where)
|
|
136
|
+
# Flatten where object into query params
|
|
137
|
+
for field_name, conditions in where_obj.items():
|
|
138
|
+
if isinstance(conditions, dict):
|
|
139
|
+
for op, val in conditions.items():
|
|
140
|
+
params[f"where[{field_name}][{op}]"] = val
|
|
141
|
+
else:
|
|
142
|
+
params[f"where[{field_name}][equals]"] = conditions
|
|
143
|
+
except json.JSONDecodeError as e:
|
|
144
|
+
out.error(f"Invalid JSON in --where: {e}")
|
|
145
|
+
raise typer.Exit(1) from e
|
|
146
|
+
|
|
147
|
+
result = c.client.get(f"/{collection}", params=params)
|
|
148
|
+
docs = result.get("docs", []) if isinstance(result, dict) else result
|
|
149
|
+
total = result.get("totalDocs", "?") if isinstance(result, dict) else "?"
|
|
150
|
+
out.info(f"Found {total} documents (showing page {page})")
|
|
151
|
+
out.table(
|
|
152
|
+
docs,
|
|
153
|
+
columns=["id", "title", "slug", "_status", "createdAt"],
|
|
154
|
+
title=f"{collection} query results",
|
|
155
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Configuration management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from kctl_payload.core.callbacks import AppContext
|
|
10
|
+
from kctl_payload.core.config import (
|
|
11
|
+
CONFIG_FILE,
|
|
12
|
+
SERVICE_KEY,
|
|
13
|
+
ServiceConfig,
|
|
14
|
+
get_all_services_in_profile,
|
|
15
|
+
get_default_profile,
|
|
16
|
+
get_profile_names,
|
|
17
|
+
resolve_active_profile_name,
|
|
18
|
+
set_default_profile,
|
|
19
|
+
set_service_config,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(help="Manage CLI configuration and profiles.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _mask(val: str) -> str:
|
|
26
|
+
if not val:
|
|
27
|
+
return "[dim]not set[/dim]"
|
|
28
|
+
return f"{val[:4]}{'*' * max(0, len(val) - 8)}{val[-4:]}" if len(val) > 10 else "****"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def init(
|
|
33
|
+
ctx: typer.Context,
|
|
34
|
+
url: Annotated[str | None, typer.Option("--url")] = None,
|
|
35
|
+
api_key: Annotated[str | None, typer.Option("--api-key")] = None,
|
|
36
|
+
name: Annotated[str | None, typer.Option("--name", "-n")] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Initialize CLI configuration."""
|
|
39
|
+
c: AppContext = ctx.obj
|
|
40
|
+
out = c.output
|
|
41
|
+
profile_name = name or typer.prompt("Profile name", default="kodemeio")
|
|
42
|
+
api_url = url or typer.prompt("Payload CMS URL (e.g. https://cms.example.com)")
|
|
43
|
+
key = api_key or typer.prompt("API key", hide_input=True)
|
|
44
|
+
|
|
45
|
+
svc = ServiceConfig(url=api_url, api_key=key)
|
|
46
|
+
set_service_config(profile_name, svc)
|
|
47
|
+
if len(get_profile_names()) <= 1:
|
|
48
|
+
set_default_profile(profile_name)
|
|
49
|
+
out.success(f"Configuration saved to {CONFIG_FILE}")
|
|
50
|
+
out.kv("Profile", profile_name)
|
|
51
|
+
out.kv("URL", api_url)
|
|
52
|
+
out.kv("API Key", _mask(key))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def show(ctx: typer.Context) -> None:
|
|
57
|
+
"""Show configuration (keys masked)."""
|
|
58
|
+
c: AppContext = ctx.obj
|
|
59
|
+
out = c.output
|
|
60
|
+
default = get_default_profile()
|
|
61
|
+
sections = [
|
|
62
|
+
(
|
|
63
|
+
"General",
|
|
64
|
+
[
|
|
65
|
+
("Config file", str(CONFIG_FILE)),
|
|
66
|
+
("Default profile", default),
|
|
67
|
+
("Service key", SERVICE_KEY),
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
]
|
|
71
|
+
for pname in get_profile_names():
|
|
72
|
+
marker = " [green](default)[/green]" if pname == default else ""
|
|
73
|
+
services = get_all_services_in_profile(pname)
|
|
74
|
+
kvs = []
|
|
75
|
+
for svc_name, svc_data in services.items():
|
|
76
|
+
if not isinstance(svc_data, dict):
|
|
77
|
+
continue
|
|
78
|
+
indicator = "[green]●[/green]" if svc_name == SERVICE_KEY else "[dim]○[/dim]"
|
|
79
|
+
kvs.append(
|
|
80
|
+
(
|
|
81
|
+
f"{indicator} {svc_name}",
|
|
82
|
+
f"{svc_data.get('url', '')} key: {_mask(svc_data.get('api_key', ''))}",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
sections.append((f"Profile: {pname}{marker}", kvs or [("(empty)", "")]))
|
|
86
|
+
out.detail("Configuration", sections)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command()
|
|
90
|
+
def test(ctx: typer.Context) -> None:
|
|
91
|
+
"""Test API connection."""
|
|
92
|
+
c: AppContext = ctx.obj
|
|
93
|
+
out = c.output
|
|
94
|
+
active = resolve_active_profile_name(c.profile)
|
|
95
|
+
out.info(f"Testing profile '{active}' → {SERVICE_KEY}")
|
|
96
|
+
try:
|
|
97
|
+
status = c.client.check_health()
|
|
98
|
+
if status == 200:
|
|
99
|
+
out.success("Connected to Payload CMS API")
|
|
100
|
+
else:
|
|
101
|
+
out.error(f"Connection failed: HTTP {status}")
|
|
102
|
+
raise typer.Exit(1)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
out.error(f"Connection failed: {e}")
|
|
105
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Doctor diagnostic checks for kctl-payload."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from kctl_payload.core.callbacks import AppContext
|
|
10
|
+
from kctl_lib.doctor_base import CheckResult, DoctorCheck, run_doctor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class APIConnectivityCheck:
|
|
15
|
+
"""Check that the configured API endpoint is reachable."""
|
|
16
|
+
|
|
17
|
+
name: str = "API Connectivity"
|
|
18
|
+
|
|
19
|
+
def run(self) -> CheckResult:
|
|
20
|
+
try:
|
|
21
|
+
from kctl_payload.core.config import get_service_config, resolve_active_profile_name
|
|
22
|
+
|
|
23
|
+
profile = resolve_active_profile_name()
|
|
24
|
+
cfg = get_service_config(profile)
|
|
25
|
+
url = cfg.url or ""
|
|
26
|
+
if not url:
|
|
27
|
+
return CheckResult(
|
|
28
|
+
name=self.name,
|
|
29
|
+
status="fail",
|
|
30
|
+
message="No URL configured",
|
|
31
|
+
fix_command="kctl-payload config init",
|
|
32
|
+
)
|
|
33
|
+
return CheckResult(name=self.name, status="ok", message=f"URL: {url}")
|
|
34
|
+
except Exception as e:
|
|
35
|
+
return CheckResult(name=self.name, status="warn", message=str(e))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AuthCheck:
|
|
40
|
+
"""Check that authentication credentials are configured."""
|
|
41
|
+
|
|
42
|
+
name: str = "Authentication"
|
|
43
|
+
|
|
44
|
+
def run(self) -> CheckResult:
|
|
45
|
+
try:
|
|
46
|
+
from kctl_payload.core.config import get_service_config, resolve_active_profile_name
|
|
47
|
+
|
|
48
|
+
profile = resolve_active_profile_name()
|
|
49
|
+
cfg = get_service_config(profile)
|
|
50
|
+
token = cfg.api_key or ""
|
|
51
|
+
if not token:
|
|
52
|
+
return CheckResult(
|
|
53
|
+
name=self.name,
|
|
54
|
+
status="fail",
|
|
55
|
+
message="No API key configured",
|
|
56
|
+
fix_command="kctl-payload config init",
|
|
57
|
+
)
|
|
58
|
+
masked = token[:4] + "****" + token[-4:] if len(token) > 8 else "****"
|
|
59
|
+
return CheckResult(name=self.name, status="ok", message=f"API key configured ({masked})")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return CheckResult(name=self.name, status="warn", message=str(e))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
app = typer.Typer(help="Run diagnostic checks.", no_args_is_help=False, invoke_without_command=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.callback(invoke_without_command=True)
|
|
68
|
+
def doctor(ctx: typer.Context) -> None:
|
|
69
|
+
"""Run all diagnostic checks."""
|
|
70
|
+
if ctx.invoked_subcommand is not None:
|
|
71
|
+
return
|
|
72
|
+
actx: AppContext = ctx.obj
|
|
73
|
+
out = actx.output
|
|
74
|
+
|
|
75
|
+
checks: list[DoctorCheck] = [
|
|
76
|
+
APIConnectivityCheck(),
|
|
77
|
+
AuthCheck(),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
all_passed = run_doctor(checks, out) # type: ignore[arg-type]
|
|
81
|
+
if not all_passed:
|
|
82
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Globals management commands for Payload CMS.
|
|
2
|
+
|
|
3
|
+
Supports get and update operations on global documents.
|
|
4
|
+
Known globals: site-settings, navigation
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from kctl_payload.core.callbacks import AppContext
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Manage Payload CMS globals.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def get(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
slug: Annotated[str, typer.Argument(help="Global slug (e.g. site-settings, navigation)")],
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Get a global document."""
|
|
25
|
+
c: AppContext = ctx.obj
|
|
26
|
+
out = c.output
|
|
27
|
+
result = c.client.get(f"/globals/{slug}")
|
|
28
|
+
out.raw_json(result)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def update(
|
|
33
|
+
ctx: typer.Context,
|
|
34
|
+
slug: Annotated[str, typer.Argument(help="Global slug (e.g. site-settings, navigation)")],
|
|
35
|
+
data: Annotated[str, typer.Option("--data", "-d", help="JSON string of fields to update")],
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Update a global document."""
|
|
38
|
+
c: AppContext = ctx.obj
|
|
39
|
+
out = c.output
|
|
40
|
+
try:
|
|
41
|
+
payload = json.loads(data)
|
|
42
|
+
except json.JSONDecodeError as e:
|
|
43
|
+
out.error(f"Invalid JSON: {e}")
|
|
44
|
+
raise typer.Exit(1) from e
|
|
45
|
+
|
|
46
|
+
result = c.client.patch(f"/globals/{slug}", json=payload)
|
|
47
|
+
out.success(f"Updated global '{slug}'")
|
|
48
|
+
out.raw_json(result)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Media management commands for Payload CMS.
|
|
2
|
+
|
|
3
|
+
Supports listing, uploading, and deleting media files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from kctl_payload.core.callbacks import AppContext
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Manage Payload CMS media files.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def list_cmd(
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Number of items to return")] = 10,
|
|
22
|
+
page: Annotated[int, typer.Option("--page", help="Page number")] = 1,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""List media files."""
|
|
25
|
+
c: AppContext = ctx.obj
|
|
26
|
+
out = c.output
|
|
27
|
+
params: dict[str, str | int] = {"limit": limit, "page": page}
|
|
28
|
+
|
|
29
|
+
result = c.client.get("/media", params=params)
|
|
30
|
+
docs = result.get("docs", []) if isinstance(result, dict) else result
|
|
31
|
+
out.table(
|
|
32
|
+
docs,
|
|
33
|
+
columns=["id", "filename", "mimeType", "filesize", "createdAt"],
|
|
34
|
+
title=f"Media (page {page})",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def upload(
|
|
40
|
+
ctx: typer.Context,
|
|
41
|
+
file_path: Annotated[Path, typer.Argument(help="Path to file to upload")],
|
|
42
|
+
alt: Annotated[str | None, typer.Option("--alt", help="Alt text for the media")] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Upload a media file (multipart form)."""
|
|
45
|
+
c: AppContext = ctx.obj
|
|
46
|
+
out = c.output
|
|
47
|
+
|
|
48
|
+
if not file_path.exists():
|
|
49
|
+
out.error(f"File not found: {file_path}")
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
import httpx
|
|
53
|
+
|
|
54
|
+
# Build multipart upload using raw httpx since APIClient.post is JSON-oriented
|
|
55
|
+
files = {"file": (file_path.name, file_path.open("rb"))}
|
|
56
|
+
data: dict[str, str] = {}
|
|
57
|
+
if alt:
|
|
58
|
+
data["alt"] = alt
|
|
59
|
+
|
|
60
|
+
url = f"{c.client._base_url}/media"
|
|
61
|
+
headers = {c.client.AUTH_HEADER: f"{c.client.AUTH_PREFIX}{c.client._credential}"}
|
|
62
|
+
|
|
63
|
+
response = httpx.post(url, files=files, data=data, headers=headers, timeout=60)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
result = response.json()
|
|
66
|
+
|
|
67
|
+
doc = result.get("doc", result) if isinstance(result, dict) else result
|
|
68
|
+
out.success(f"Uploaded {file_path.name}")
|
|
69
|
+
out.raw_json(doc)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def delete(
|
|
74
|
+
ctx: typer.Context,
|
|
75
|
+
media_id: Annotated[str, typer.Argument(help="Media document ID")],
|
|
76
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation")] = False,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Delete a media file."""
|
|
79
|
+
c: AppContext = ctx.obj
|
|
80
|
+
out = c.output
|
|
81
|
+
if not force:
|
|
82
|
+
typer.confirm(f"Delete media {media_id}?", abort=True)
|
|
83
|
+
|
|
84
|
+
c.client.delete(f"/media/{media_id}")
|
|
85
|
+
out.success(f"Deleted media {media_id}")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""User management commands for Payload CMS."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kctl_payload.core.callbacks import AppContext
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Manage Payload CMS users.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("list")
|
|
16
|
+
def list_cmd(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Number of items to return")] = 10,
|
|
19
|
+
page: Annotated[int, typer.Option("--page", help="Page number")] = 1,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""List users."""
|
|
22
|
+
c: AppContext = ctx.obj
|
|
23
|
+
out = c.output
|
|
24
|
+
params: dict[str, str | int] = {"limit": limit, "page": page}
|
|
25
|
+
|
|
26
|
+
result = c.client.get("/users", params=params)
|
|
27
|
+
docs = result.get("docs", []) if isinstance(result, dict) else result
|
|
28
|
+
out.table(
|
|
29
|
+
docs,
|
|
30
|
+
columns=["id", "email", "name", "role", "createdAt"],
|
|
31
|
+
title=f"Users (page {page})",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
def create(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
email: Annotated[str, typer.Option("--email", "-e", help="User email")],
|
|
39
|
+
password: Annotated[str, typer.Option("--password", "-p", help="User password", hide_input=True)],
|
|
40
|
+
data: Annotated[str | None, typer.Option("--data", "-d", help="Additional user data as JSON")] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Create a new user."""
|
|
43
|
+
c: AppContext = ctx.obj
|
|
44
|
+
out = c.output
|
|
45
|
+
payload: dict = {"email": email, "password": password}
|
|
46
|
+
|
|
47
|
+
if data:
|
|
48
|
+
try:
|
|
49
|
+
extra = json.loads(data)
|
|
50
|
+
payload.update(extra)
|
|
51
|
+
except json.JSONDecodeError as e:
|
|
52
|
+
out.error(f"Invalid JSON in --data: {e}")
|
|
53
|
+
raise typer.Exit(1) from e
|
|
54
|
+
|
|
55
|
+
result = c.client.post("/users", json=payload)
|
|
56
|
+
doc = result.get("doc", result) if isinstance(result, dict) else result
|
|
57
|
+
out.success(f"Created user {email}")
|
|
58
|
+
out.raw_json(doc)
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Typer global callback and shared context for kctl-payload.
|
|
2
|
+
|
|
3
|
+
Subclasses AppContextBase from kctl-lib with Payload CMS-specific fields.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
from kctl_lib.callbacks import AppContextBase
|
|
11
|
+
|
|
12
|
+
from kctl_payload.core.client import PayloadClient
|
|
13
|
+
from kctl_payload.core.config import resolve_connection
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AppContext(AppContextBase):
|
|
18
|
+
"""Payload CMS-specific application context."""
|
|
19
|
+
|
|
20
|
+
debug: bool = False
|
|
21
|
+
url_override: str | None = None
|
|
22
|
+
api_key_override: str | None = None
|
|
23
|
+
_client: PayloadClient | None = field(default=None, repr=False, init=False)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def client(self) -> PayloadClient:
|
|
27
|
+
if self._client is None:
|
|
28
|
+
url, api_key = resolve_connection(
|
|
29
|
+
profile_name=self.profile,
|
|
30
|
+
url_override=self.url_override,
|
|
31
|
+
api_key_override=self.api_key_override,
|
|
32
|
+
)
|
|
33
|
+
self._client = PayloadClient(base_url=url, api_key=api_key)
|
|
34
|
+
return self._client
|
|
35
|
+
|
|
36
|
+
def close(self) -> None:
|
|
37
|
+
"""Close underlying HTTP client."""
|
|
38
|
+
if self._client is not None:
|
|
39
|
+
self._client.close()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Payload CMS API client, subclassing kctl-lib's APIClient.
|
|
2
|
+
|
|
3
|
+
Provides Payload-specific auth (Authorization: users API-Key <key>),
|
|
4
|
+
retry support, and health check functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from kctl_lib.api_client import APIClient
|
|
13
|
+
from kctl_lib.exceptions import ConfigError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PayloadClient(APIClient):
|
|
17
|
+
"""Synchronous httpx client for Payload CMS API with retry support."""
|
|
18
|
+
|
|
19
|
+
AUTH_HEADER = "Authorization"
|
|
20
|
+
AUTH_PREFIX = "users API-Key "
|
|
21
|
+
API_PREFIX = "/api"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
base_url: str = "",
|
|
26
|
+
api_key: str = "",
|
|
27
|
+
timeout: float = 30.0,
|
|
28
|
+
max_retries: int = 3,
|
|
29
|
+
retry_base_delay: float = 2.0,
|
|
30
|
+
retry_max_delay: float = 60.0,
|
|
31
|
+
**kwargs: Any,
|
|
32
|
+
):
|
|
33
|
+
if not base_url:
|
|
34
|
+
raise ConfigError("No URL configured. Run: kctl-payload config init")
|
|
35
|
+
|
|
36
|
+
super().__init__(
|
|
37
|
+
base_url=base_url,
|
|
38
|
+
credential=api_key or "unset",
|
|
39
|
+
timeout=timeout,
|
|
40
|
+
retry_enabled=True,
|
|
41
|
+
max_retries=max_retries,
|
|
42
|
+
retry_base_delay=retry_base_delay,
|
|
43
|
+
retry_max_delay=retry_max_delay,
|
|
44
|
+
**kwargs,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def root_url(self) -> str:
|
|
49
|
+
"""Public accessor for the root URL (without /api)."""
|
|
50
|
+
return self._base_url.rsplit("/api", 1)[0]
|
|
51
|
+
|
|
52
|
+
def check_health(self) -> int:
|
|
53
|
+
"""Check health by hitting /api/globals/site-settings, returns HTTP status code."""
|
|
54
|
+
try:
|
|
55
|
+
r = httpx.get(
|
|
56
|
+
f"{self._base_url}/globals/site-settings",
|
|
57
|
+
headers={self.AUTH_HEADER: f"{self.AUTH_PREFIX}{self._credential}"},
|
|
58
|
+
timeout=5,
|
|
59
|
+
)
|
|
60
|
+
return r.status_code
|
|
61
|
+
except httpx.HTTPError:
|
|
62
|
+
return 0
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Profile management and configuration resolution for kctl-payload.
|
|
2
|
+
|
|
3
|
+
Delegates to kctl-lib's config framework with Payload CMS-specific settings.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from kctl_lib.config import (
|
|
11
|
+
CONFIG_DIR,
|
|
12
|
+
CONFIG_FILE,
|
|
13
|
+
ConfigFile,
|
|
14
|
+
expand_env,
|
|
15
|
+
get_all_services_in_profile,
|
|
16
|
+
get_default_profile,
|
|
17
|
+
get_profile_names,
|
|
18
|
+
is_service_scoped,
|
|
19
|
+
load_config,
|
|
20
|
+
load_raw_config,
|
|
21
|
+
remove_profile,
|
|
22
|
+
save_raw_config,
|
|
23
|
+
set_default_profile,
|
|
24
|
+
)
|
|
25
|
+
from kctl_lib.config import get_service_config as _get_service_config
|
|
26
|
+
from kctl_lib.config import (
|
|
27
|
+
resolve_active_profile_name as _resolve_active_profile_name,
|
|
28
|
+
)
|
|
29
|
+
from kctl_lib.config import set_service_config as _set_service_config
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
|
|
32
|
+
# This CLI's service key within a profile
|
|
33
|
+
SERVICE_KEY = "payload"
|
|
34
|
+
|
|
35
|
+
# Environment variable prefix for this CLI
|
|
36
|
+
ENV_PREFIX = "KCTL_PAYLOAD"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"CONFIG_DIR",
|
|
40
|
+
"CONFIG_FILE",
|
|
41
|
+
"ConfigFile",
|
|
42
|
+
"SERVICE_KEY",
|
|
43
|
+
"ServiceConfig",
|
|
44
|
+
"get_all_services_in_profile",
|
|
45
|
+
"get_default_profile",
|
|
46
|
+
"get_profile_names",
|
|
47
|
+
"get_service_config",
|
|
48
|
+
"is_service_scoped",
|
|
49
|
+
"load_config",
|
|
50
|
+
"load_raw_config",
|
|
51
|
+
"remove_profile",
|
|
52
|
+
"resolve_active_profile_name",
|
|
53
|
+
"resolve_connection",
|
|
54
|
+
"save_raw_config",
|
|
55
|
+
"set_default_profile",
|
|
56
|
+
"set_service_config",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ServiceConfig(BaseModel):
|
|
61
|
+
"""Payload CMS service-specific config within a profile."""
|
|
62
|
+
|
|
63
|
+
url: str = ""
|
|
64
|
+
api_key: str = ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_service_config(profile_name: str) -> ServiceConfig:
|
|
68
|
+
"""Get Payload service config from a profile."""
|
|
69
|
+
raw = _get_service_config(
|
|
70
|
+
profile_name,
|
|
71
|
+
SERVICE_KEY,
|
|
72
|
+
valid_fields=list(ServiceConfig.model_fields.keys()),
|
|
73
|
+
)
|
|
74
|
+
if not raw:
|
|
75
|
+
return ServiceConfig()
|
|
76
|
+
return ServiceConfig(**raw)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_service_config(profile_name: str, svc_config: ServiceConfig) -> None:
|
|
80
|
+
"""Write Payload service config into a profile."""
|
|
81
|
+
svc_data = svc_config.model_dump(exclude_defaults=False)
|
|
82
|
+
# Remove empty values
|
|
83
|
+
cleaned = {k: v for k, v in svc_data.items() if v}
|
|
84
|
+
_set_service_config(profile_name, SERVICE_KEY, cleaned)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _expand_key(api_key: str) -> str:
|
|
88
|
+
"""Expand ${ENV_VAR} references in API key values."""
|
|
89
|
+
return expand_env(api_key)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve_active_profile_name(
|
|
93
|
+
profile_name: str | None = None,
|
|
94
|
+
) -> str:
|
|
95
|
+
"""Resolve the active profile name from all sources."""
|
|
96
|
+
return _resolve_active_profile_name(profile_name, ENV_PREFIX)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_connection(
|
|
100
|
+
profile_name: str | None = None,
|
|
101
|
+
url_override: str | None = None,
|
|
102
|
+
api_key_override: str | None = None,
|
|
103
|
+
) -> tuple[str, str]:
|
|
104
|
+
"""Resolve API URL and API key from all sources.
|
|
105
|
+
|
|
106
|
+
Priority:
|
|
107
|
+
1. CLI flags (url_override, api_key_override)
|
|
108
|
+
2. KCTL_PAYLOAD_URL / KCTL_PAYLOAD_API_KEY env vars
|
|
109
|
+
3. Profile's payload service config
|
|
110
|
+
"""
|
|
111
|
+
url = ""
|
|
112
|
+
api_key = ""
|
|
113
|
+
|
|
114
|
+
# 3. Config file profile (service-scoped)
|
|
115
|
+
pname = resolve_active_profile_name(profile_name)
|
|
116
|
+
svc = get_service_config(pname)
|
|
117
|
+
if svc.url:
|
|
118
|
+
url = svc.url
|
|
119
|
+
if svc.api_key:
|
|
120
|
+
api_key = svc.api_key
|
|
121
|
+
|
|
122
|
+
# 2. KCTL env vars
|
|
123
|
+
if env_url := os.environ.get("KCTL_PAYLOAD_URL"):
|
|
124
|
+
url = env_url
|
|
125
|
+
if env_key := os.environ.get("KCTL_PAYLOAD_API_KEY"):
|
|
126
|
+
api_key = env_key
|
|
127
|
+
|
|
128
|
+
# 1. CLI flags
|
|
129
|
+
if url_override:
|
|
130
|
+
url = url_override
|
|
131
|
+
if api_key_override:
|
|
132
|
+
api_key = api_key_override
|
|
133
|
+
|
|
134
|
+
return url, api_key
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kctl-payload
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Summary: Kodemeio Payload CMS CLI — manage Payload CMS via REST API
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: httpx>=0.28.0
|
|
7
|
+
Requires-Dist: kctl-lib>=0.7.0
|
|
8
|
+
Requires-Dist: pydantic>=2.10.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
10
|
+
Requires-Dist: rich>=13.9.0
|
|
11
|
+
Requires-Dist: typer>=0.15.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: mypy>=1.14.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
kctl_payload/__init__.py,sha256=QbWQ0X7dS6LvBSmFMreLpBzh9z9PPhOaBe2NkS9ewSI,69
|
|
2
|
+
kctl_payload/__main__.py,sha256=xnNd3_koRg-eJmSDUSXk7Ersz1Ng7wzGUbthiSFpeJo,91
|
|
3
|
+
kctl_payload/cli.py,sha256=1sBSnEl4AZhkCZpqBp0UogP-7p9QNBab-D7-UR5m2Dw,3345
|
|
4
|
+
kctl_payload/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
kctl_payload/commands/collections.py,sha256=RnNjzRkcLvgaJGUxgxGg9AmFc2-k5_ABnjusTdI5g2k,5575
|
|
6
|
+
kctl_payload/commands/config_cmd.py,sha256=fkgxvjjsgdgLKEQSXxWCuKmzPwlNT5eMiv8FD3ZDkvI,3317
|
|
7
|
+
kctl_payload/commands/doctor.py,sha256=yTnHrwJOEbp4V-PBkDI6fTJqXfjm8KDjwCSw9ObfGMI,2644
|
|
8
|
+
kctl_payload/commands/globals_cmd.py,sha256=LbDkDsDdtRuwTGNMGIXWXRK0FQrsIvOfNiGnrnk_1go,1283
|
|
9
|
+
kctl_payload/commands/media.py,sha256=0e4OkpJS-FnH-uyxy8d2dXacmFXIwv-p0vKaRJJKyPA,2544
|
|
10
|
+
kctl_payload/commands/users.py,sha256=sF207EwGlSQSdg2j4Z5p0cm6fEmLa1-ZQBIrik6w9nI,1794
|
|
11
|
+
kctl_payload/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
kctl_payload/core/callbacks.py,sha256=4Bhz1mFckN2VKMFnY1uXxp4CybyGd_N5rr_z8Ct3stA,1193
|
|
13
|
+
kctl_payload/core/client.py,sha256=zIurmFYkNSj_nj8jRBVtSsGiAMLn2_A9YDoe_M2mr60,1831
|
|
14
|
+
kctl_payload/core/config.py,sha256=WIWGGE0miDKUNLBgftb3hsMMoVayYkBbNV3a9TdWQ-Q,3486
|
|
15
|
+
kctl_payload-0.6.1.dist-info/METADATA,sha256=lMH5kSKVMITHXqZwpND2eNb4oP3s5R6qWV3PH2003dk,583
|
|
16
|
+
kctl_payload-0.6.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
17
|
+
kctl_payload-0.6.1.dist-info/entry_points.txt,sha256=Eee2H_2Zs01yuh6LIy3-PMaiZvhR_gTTScGF2e-00Ls,55
|
|
18
|
+
kctl_payload-0.6.1.dist-info/RECORD,,
|