kop-cli 0.2.0b1__tar.gz → 0.2.0b2__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.
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/PKG-INFO +1 -1
- kop_cli-0.2.0b2/scripts/set_release_version.py +52 -0
- kop_cli-0.2.0b2/src/kop/__init__.py +2 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/fields.py +1 -1
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/ResourceView.py +26 -13
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Panel.py +28 -2
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_resource_view.py +112 -7
- kop_cli-0.2.0b1/src/kop/__init__.py +0 -2
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/.gitignore +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/.python-version +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/CODE_OF_CONDUCT.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/LICENSE +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/NOTICE +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/README.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/__init__.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/faq.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/getting_started.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ClusterRoleBindings.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ClusterRoles.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Clusters.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ConfigMaps.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/CronJobs.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/DaemonSets.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Deployments.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/EndpointSlices.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Endpoints.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/IngressClasses.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Ingresses.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Jobs.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Namespaces.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/NetworkPolicies.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Nodes.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/PersistentVolumeClaims.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/PersistentVolumes.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Pods.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/RoleBindings.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Roles.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Secrets.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/ServiceAccounts.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/Services.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/StatefulSets.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/StorageClasses.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/index.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide/zh/pods-zh.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/guide.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/create_resource.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/edit_resource.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/forward_port.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/pod_logs.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/resource_detail.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/scale_resource.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/search_kinds.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/guide/select_namespace.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/sample.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/action_workspace.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/add_new_cluster.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/cluster_area.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/detail_view.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/keys.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/resource_view.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/startup.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/startup_without_cluster.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/images/tutorial/themes.png +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/index.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/readme_ch.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/docs/tutorial.md +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/mkdocs.yml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/pyproject.toml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/event.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/forward.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/kube_client.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/mock_resource_view_deployment.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/mock_resource_view_pod.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/multiple_select.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/pixels.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/pod_logs.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/rich_detail.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/scripts/view_pod_logs.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/app/__init__.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/app/main.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/controllers/handler.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/factory.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/models.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/attach.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/client.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/config.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/events.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/exec.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/forward.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/logs.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/provider/utils.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/registry.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/details.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/formatter.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/forms.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/renderers/table.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/clusterrolebindings.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/clusterroles.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/configmaps.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/cronjobs.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/daemonsets.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/deployments.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/endpointslices.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/ingressclasses.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/ingresses.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/jobs.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/namespaces.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/networkpolicies.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/node-shell-pod.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/persistentvolumeclaims.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/persistentvolumes.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/pods.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/rolebindings.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/roles.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/secrets.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/serviceaccounts.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/services.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/statefulsets.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/templates/resource/storageclasses.yaml +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/validations.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/ActionWorkspace.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/EditView.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/PodAttach.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/PodLog.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/PodTerminal.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/views/StartupView.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Actions.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Attach.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Columns.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Detail.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Directory.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Dynamic.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Edit.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Events.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Expandable.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Focusable.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Forward.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Log.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Modals.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/MultipleSelect.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Pty.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/RichDetail.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/Rules.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/SideMenu.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/src/kop/widgets/__init__.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_action_workspace.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_detail_modal_renderer.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_edit_view.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_event_service.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_log_controller.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_pagination_integration.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_pod_logs.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_startup_view.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/tests/test_table_renderer.py +0 -0
- {kop_cli-0.2.0b1 → kop_cli-0.2.0b2}/uv.lock +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Set the source package version using PEP 440 normalization."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from packaging.version import Version
|
|
12
|
+
except ModuleNotFoundError:
|
|
13
|
+
from pip._vendor.packaging.version import Version
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
VERSION_PATTERN = re.compile(r'^__version__\s*=\s*["\']([^"\']+)["\']\s*$')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_version(path: Path) -> str:
|
|
20
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
21
|
+
match = VERSION_PATTERN.match(line)
|
|
22
|
+
if match:
|
|
23
|
+
return match.group(1)
|
|
24
|
+
raise SystemExit(f"Unable to find __version__ in {path}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write_version(path: Path, version: str) -> None:
|
|
28
|
+
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
29
|
+
for index, line in enumerate(lines):
|
|
30
|
+
if VERSION_PATTERN.match(line.strip()):
|
|
31
|
+
newline = "\n" if line.endswith("\n") else ""
|
|
32
|
+
lines[index] = f'__version__ = "{version}"{newline}'
|
|
33
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
34
|
+
return
|
|
35
|
+
raise SystemExit(f"Unable to find __version__ in {path}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> None:
|
|
39
|
+
parser = argparse.ArgumentParser()
|
|
40
|
+
parser.add_argument("version", nargs="?")
|
|
41
|
+
parser.add_argument("--path", default="src/kop/__init__.py")
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
version_path = Path(args.path)
|
|
45
|
+
if args.version:
|
|
46
|
+
write_version(version_path, str(Version(args.version)))
|
|
47
|
+
|
|
48
|
+
print(read_version(version_path))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -38,7 +38,7 @@ def container_status_renderer(value):
|
|
|
38
38
|
if ephemeral_container_statuses is None:
|
|
39
39
|
ephemeral_container_statuses = []
|
|
40
40
|
|
|
41
|
-
all_container_status = value.container_statuses + init_container_statuses + ephemeral_container_statuses
|
|
41
|
+
all_container_status = value.container_statuses or [] + init_container_statuses + ephemeral_container_statuses
|
|
42
42
|
status_texts = []
|
|
43
43
|
for cs in all_container_status:
|
|
44
44
|
if cs.ready and cs.started:
|
|
@@ -94,7 +94,7 @@ class ResourceView(Screen):
|
|
|
94
94
|
self.resource_type: Optional[str] = None
|
|
95
95
|
self.resource_kind_name: Optional[str] = None
|
|
96
96
|
self.page_index: int = 0
|
|
97
|
-
self.resource_pages: dict[str, list[tuple[object, list, Optional[str]]]] = {}
|
|
97
|
+
self.resource_pages: dict[str, list[tuple[object, list, Optional[str], int]]] = {}
|
|
98
98
|
|
|
99
99
|
def compose(self) -> ComposeResult:
|
|
100
100
|
yield Header()
|
|
@@ -179,10 +179,10 @@ class ResourceView(Screen):
|
|
|
179
179
|
namespace: Optional[str],
|
|
180
180
|
keyword: Optional[str],
|
|
181
181
|
continue_token: Optional[str] = None,
|
|
182
|
-
) -> Tuple[Optional[BaseFactory], Optional[object], list]:
|
|
182
|
+
) -> Tuple[Optional[BaseFactory], Optional[object], list, int]:
|
|
183
183
|
factory_cls = ResourceRegistry.get_factory(resource_type)
|
|
184
184
|
if not factory_cls:
|
|
185
|
-
return None, None, []
|
|
185
|
+
return None, None, [], 0
|
|
186
186
|
factory = factory_cls(self.endpoint)
|
|
187
187
|
try:
|
|
188
188
|
data = factory.fetch(
|
|
@@ -192,11 +192,10 @@ class ResourceView(Screen):
|
|
|
192
192
|
)
|
|
193
193
|
except TypeError:
|
|
194
194
|
data = factory.fetch(namespace=namespace)
|
|
195
|
-
if keyword
|
|
196
|
-
|
|
197
|
-
cleaned = factory.clean(data)
|
|
195
|
+
filtered = factory.filter(data, keyword) if keyword else data
|
|
196
|
+
cleaned = factory.clean(filtered)
|
|
198
197
|
cleaned.sort(key=lambda vm: vm.name)
|
|
199
|
-
return factory, data, cleaned
|
|
198
|
+
return factory, data, cleaned, len(filtered.items)
|
|
200
199
|
|
|
201
200
|
def _fetch_resource_with_timeout(
|
|
202
201
|
self,
|
|
@@ -205,7 +204,7 @@ class ResourceView(Screen):
|
|
|
205
204
|
keyword: Optional[str],
|
|
206
205
|
timeout: float,
|
|
207
206
|
continue_token: Optional[str] = None,
|
|
208
|
-
) -> Tuple[Optional[BaseFactory], Optional[object], list]:
|
|
207
|
+
) -> Tuple[Optional[BaseFactory], Optional[object], list, int]:
|
|
209
208
|
"""Run kubernetes fetch in a daemon thread so app shutdown isn't blocked by stuck API calls."""
|
|
210
209
|
result: Queue[Tuple[str, object]] = Queue(maxsize=1)
|
|
211
210
|
|
|
@@ -264,7 +263,9 @@ class ResourceView(Screen):
|
|
|
264
263
|
page_index: int,
|
|
265
264
|
factory: Optional[BaseFactory],
|
|
266
265
|
data: Optional[object],
|
|
266
|
+
keyword: Optional[str],
|
|
267
267
|
cleaned: list,
|
|
268
|
+
resource_count: int,
|
|
268
269
|
) -> None:
|
|
269
270
|
if request_id != self._resource_request_id:
|
|
270
271
|
return
|
|
@@ -274,10 +275,15 @@ class ResourceView(Screen):
|
|
|
274
275
|
|
|
275
276
|
self.FACTORY_CACHE = factory
|
|
276
277
|
self.data = data
|
|
278
|
+
if keyword != self.keyword:
|
|
279
|
+
filtered = factory.filter(data, self.keyword) if self.keyword else data
|
|
280
|
+
cleaned = factory.clean(filtered)
|
|
281
|
+
cleaned.sort(key=lambda vm: vm.name)
|
|
282
|
+
resource_count = len(filtered.items)
|
|
277
283
|
next_token = getattr(getattr(data, "metadata", None), "_continue", None)
|
|
278
284
|
cache_key = self._resource_cache_key(resource_type, namespace)
|
|
279
285
|
pages = self.resource_pages.setdefault(cache_key, [])
|
|
280
|
-
page_entry = (data, cleaned, next_token)
|
|
286
|
+
page_entry = (data, cleaned, next_token, resource_count)
|
|
281
287
|
if page_index < len(pages):
|
|
282
288
|
pages[page_index] = page_entry
|
|
283
289
|
del pages[page_index + 1 :]
|
|
@@ -296,7 +302,7 @@ class ResourceView(Screen):
|
|
|
296
302
|
self.table.raw_data = data.items
|
|
297
303
|
self.table.data = cleaned
|
|
298
304
|
|
|
299
|
-
self.panel.resource_count =
|
|
305
|
+
self.panel.resource_count = resource_count
|
|
300
306
|
|
|
301
307
|
def _handle_resource_error(self, request_id: int, exc: Exception) -> None:
|
|
302
308
|
if request_id != self._resource_request_id:
|
|
@@ -316,7 +322,7 @@ class ResourceView(Screen):
|
|
|
316
322
|
) -> None:
|
|
317
323
|
worker = get_current_worker()
|
|
318
324
|
try:
|
|
319
|
-
factory, data, cleaned = self._fetch_resource_with_timeout(
|
|
325
|
+
factory, data, cleaned, resource_count = self._fetch_resource_with_timeout(
|
|
320
326
|
resource_type,
|
|
321
327
|
namespace,
|
|
322
328
|
keyword,
|
|
@@ -337,7 +343,9 @@ class ResourceView(Screen):
|
|
|
337
343
|
page_index,
|
|
338
344
|
factory,
|
|
339
345
|
data,
|
|
346
|
+
keyword,
|
|
340
347
|
cleaned,
|
|
348
|
+
resource_count,
|
|
341
349
|
)
|
|
342
350
|
|
|
343
351
|
def _load_resource(
|
|
@@ -367,14 +375,19 @@ class ResourceView(Screen):
|
|
|
367
375
|
pages = self.resource_pages.get(cache_key, [])
|
|
368
376
|
if page_index < 0 or page_index >= len(pages):
|
|
369
377
|
return False
|
|
370
|
-
|
|
378
|
+
if not self.FACTORY_CACHE:
|
|
379
|
+
return False
|
|
380
|
+
data, _cleaned, _continue_token, _resource_count = pages[page_index]
|
|
371
381
|
self.data = data
|
|
372
382
|
self.page_index = page_index
|
|
383
|
+
filtered = self.FACTORY_CACHE.filter(data, self.keyword) if self.keyword else data
|
|
384
|
+
cleaned = self.FACTORY_CACHE.clean(filtered)
|
|
385
|
+
cleaned.sort(key=lambda vm: vm.name)
|
|
373
386
|
if self.table:
|
|
374
387
|
self.table.raw_data = data.items
|
|
375
388
|
self.table.data = cleaned
|
|
376
389
|
if self.panel:
|
|
377
|
-
self.panel.resource_count = len(
|
|
390
|
+
self.panel.resource_count = len(filtered.items)
|
|
378
391
|
self.refresh_bindings()
|
|
379
392
|
return True
|
|
380
393
|
|
|
@@ -2,7 +2,8 @@ from textual.message import Message
|
|
|
2
2
|
from textual.events import Mount
|
|
3
3
|
from textual.reactive import Reactive
|
|
4
4
|
from textual.app import ComposeResult
|
|
5
|
-
from textual.
|
|
5
|
+
from textual.css.query import NoMatches
|
|
6
|
+
from textual.widgets import Static, Input, Label, Select, ListView
|
|
6
7
|
from textual.containers import Grid, Horizontal
|
|
7
8
|
from textual.timer import Timer
|
|
8
9
|
from textual.binding import Binding
|
|
@@ -66,6 +67,7 @@ class ResourcePanel(Static):
|
|
|
66
67
|
debounce_time: float = 0.3
|
|
67
68
|
|
|
68
69
|
BINDINGS = [
|
|
70
|
+
Binding(key="tab", action="focus_table", show=False),
|
|
69
71
|
Binding(key="escape", action="clear", show=False),
|
|
70
72
|
]
|
|
71
73
|
|
|
@@ -128,6 +130,30 @@ class ResourcePanel(Static):
|
|
|
128
130
|
def _on_mount(self, event: Mount) -> None:
|
|
129
131
|
self.post_message(self.RequireNamespace().set_sender(self))
|
|
130
132
|
|
|
133
|
+
def check_action(self, action: str, parameters: tuple[object, ...]) -> Optional[bool]:
|
|
134
|
+
if action != "focus_table":
|
|
135
|
+
return True
|
|
136
|
+
focused = self.app.focused
|
|
137
|
+
if not isinstance(focused, (Input, Select)):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
parent = focused.parent
|
|
141
|
+
while parent is not None:
|
|
142
|
+
if parent is self:
|
|
143
|
+
break
|
|
144
|
+
parent = parent.parent
|
|
145
|
+
else:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
self.screen.query_one("#list_view", ListView)
|
|
150
|
+
except NoMatches:
|
|
151
|
+
return False
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
def action_focus_table(self) -> None:
|
|
155
|
+
self.screen.query_one("#list_view", ListView).focus()
|
|
156
|
+
|
|
131
157
|
def action_clear(self) -> None:
|
|
132
158
|
"""
|
|
133
159
|
clear search input
|
|
@@ -147,4 +173,4 @@ class ResourcePanel(Static):
|
|
|
147
173
|
def __init__(self, query: str) -> None:
|
|
148
174
|
super().__init__()
|
|
149
175
|
self.query = query
|
|
150
|
-
|
|
176
|
+
|
|
@@ -6,8 +6,11 @@ from unittest.mock import MagicMock
|
|
|
6
6
|
|
|
7
7
|
from textual.app import App
|
|
8
8
|
from textual.screen import Screen
|
|
9
|
+
from textual.widgets import ListView
|
|
9
10
|
|
|
11
|
+
from kop.models import ColumnModel
|
|
10
12
|
from kop.registry import ResourceRegistry
|
|
13
|
+
from kop.renderers.table import TableRenderer
|
|
11
14
|
from kop.views.ResourceView import ResourceView
|
|
12
15
|
from kop.widgets.Panel import ResourcePanel
|
|
13
16
|
from kop.controllers.handler import BaseActionHandlerMixin
|
|
@@ -36,6 +39,46 @@ class _FakeActiveTimer:
|
|
|
36
39
|
self.reset_called = True
|
|
37
40
|
|
|
38
41
|
|
|
42
|
+
class _Row(dict):
|
|
43
|
+
def __getattr__(self, item):
|
|
44
|
+
try:
|
|
45
|
+
return self[item]
|
|
46
|
+
except KeyError as exc:
|
|
47
|
+
raise AttributeError(item) from exc
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_resource_panel_tab_focuses_table_list_view() -> None:
|
|
51
|
+
app = ResourceHarnessApp()
|
|
52
|
+
|
|
53
|
+
async def _run() -> None:
|
|
54
|
+
async with app.run_test(size=(120, 40)) as pilot:
|
|
55
|
+
await pilot.pause()
|
|
56
|
+
view = app.view
|
|
57
|
+
assert view is not None
|
|
58
|
+
|
|
59
|
+
table = TableRenderer(
|
|
60
|
+
columns=[ColumnModel(title="Name", width=1, field="name")],
|
|
61
|
+
data=[_Row(name="pod-a")],
|
|
62
|
+
raw_data=[SimpleNamespace(metadata=SimpleNamespace(name="pod-a"), name="pod-a")],
|
|
63
|
+
)
|
|
64
|
+
await view.query_one("#resource_container").mount(table, after=view.panel)
|
|
65
|
+
await pilot.pause()
|
|
66
|
+
|
|
67
|
+
for selector in ("#namespace_select", "#search_input"):
|
|
68
|
+
widget = view.query_one(selector)
|
|
69
|
+
widget.focus()
|
|
70
|
+
await pilot.pause()
|
|
71
|
+
|
|
72
|
+
await pilot.press("tab")
|
|
73
|
+
await pilot.pause()
|
|
74
|
+
|
|
75
|
+
focused = app.focused
|
|
76
|
+
assert isinstance(focused, ListView)
|
|
77
|
+
assert focused.id == "list_view"
|
|
78
|
+
|
|
79
|
+
asyncio.run(_run())
|
|
80
|
+
|
|
81
|
+
|
|
39
82
|
def test_fetch_resource_returns_none_when_factory_missing(monkeypatch) -> None:
|
|
40
83
|
app = ResourceHarnessApp()
|
|
41
84
|
monkeypatch.setattr(ResourceRegistry, "get_factory", lambda _resource_type: None)
|
|
@@ -45,10 +88,11 @@ def test_fetch_resource_returns_none_when_factory_missing(monkeypatch) -> None:
|
|
|
45
88
|
view = app.view
|
|
46
89
|
assert view is not None
|
|
47
90
|
|
|
48
|
-
factory, data, cleaned = view._fetch_resource("pods", None, None)
|
|
91
|
+
factory, data, cleaned, resource_count = view._fetch_resource("pods", None, None)
|
|
49
92
|
assert factory is None
|
|
50
93
|
assert data is None
|
|
51
94
|
assert cleaned == []
|
|
95
|
+
assert resource_count == 0
|
|
52
96
|
|
|
53
97
|
asyncio.run(_run())
|
|
54
98
|
|
|
@@ -86,10 +130,62 @@ def test_fetch_resource_filters_and_sorts(monkeypatch) -> None:
|
|
|
86
130
|
view = app.view
|
|
87
131
|
assert view is not None
|
|
88
132
|
|
|
89
|
-
factory, data, cleaned = view._fetch_resource("pods", None, "a")
|
|
133
|
+
factory, data, cleaned, resource_count = view._fetch_resource("pods", None, "a")
|
|
90
134
|
assert isinstance(factory, FakeFactory)
|
|
91
|
-
assert [item.name for item in data.items] == ["a"]
|
|
135
|
+
assert [item.name for item in data.items] == ["b", "a"]
|
|
92
136
|
assert [item.name for item in cleaned] == ["a"]
|
|
137
|
+
assert resource_count == 1
|
|
138
|
+
|
|
139
|
+
asyncio.run(_run())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_apply_resource_recomputes_display_when_keyword_changes() -> None:
|
|
143
|
+
app = ResourceHarnessApp()
|
|
144
|
+
|
|
145
|
+
class FakeFactory:
|
|
146
|
+
resource_type = "pods"
|
|
147
|
+
|
|
148
|
+
def filter(self, raw, keyword):
|
|
149
|
+
return SimpleNamespace(
|
|
150
|
+
items=[item for item in raw.items if keyword in item.name],
|
|
151
|
+
metadata=raw.metadata,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def clean(self, raw):
|
|
155
|
+
return [SimpleNamespace(name=item.name) for item in raw.items]
|
|
156
|
+
|
|
157
|
+
async def _run() -> None:
|
|
158
|
+
async with app.run_test(size=(120, 40)):
|
|
159
|
+
view = app.view
|
|
160
|
+
assert view is not None
|
|
161
|
+
view._resource_request_id = 1
|
|
162
|
+
view.keyword = ""
|
|
163
|
+
view._table_resource_type = "pods"
|
|
164
|
+
view.table = SimpleNamespace(raw_data=[], data=[])
|
|
165
|
+
view.panel = SimpleNamespace(resource_count=0)
|
|
166
|
+
|
|
167
|
+
data = SimpleNamespace(
|
|
168
|
+
items=[SimpleNamespace(name="a"), SimpleNamespace(name="b")],
|
|
169
|
+
metadata=SimpleNamespace(_continue=None),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
view._set_loading = lambda _is_loading: None
|
|
173
|
+
view.refresh_bindings = lambda: None
|
|
174
|
+
view._apply_resource(
|
|
175
|
+
1,
|
|
176
|
+
"pods",
|
|
177
|
+
None,
|
|
178
|
+
0,
|
|
179
|
+
FakeFactory(),
|
|
180
|
+
data,
|
|
181
|
+
"a",
|
|
182
|
+
[SimpleNamespace(name="a")],
|
|
183
|
+
1,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
assert view.data is data
|
|
187
|
+
assert [item.name for item in view.table.data] == ["a", "b"]
|
|
188
|
+
assert view.panel.resource_count == 2
|
|
93
189
|
|
|
94
190
|
asyncio.run(_run())
|
|
95
191
|
|
|
@@ -128,7 +224,7 @@ def test_fetch_resource_uses_dynamic_page_size_from_screen_height(monkeypatch) -
|
|
|
128
224
|
async with app.run_test(size=(120, 40)):
|
|
129
225
|
view = app.view
|
|
130
226
|
assert view is not None
|
|
131
|
-
_factory, _data, _cleaned = view._fetch_resource("pods", None, None)
|
|
227
|
+
_factory, _data, _cleaned, _resource_count = view._fetch_resource("pods", None, None)
|
|
132
228
|
|
|
133
229
|
asyncio.run(_run())
|
|
134
230
|
|
|
@@ -442,19 +538,22 @@ def test_mock_pods_over_200_items_with_pagination_tokens(monkeypatch) -> None:
|
|
|
442
538
|
assert view is not None
|
|
443
539
|
monkeypatch.setattr(view, "_get_page_size", lambda: 100)
|
|
444
540
|
|
|
445
|
-
_, page1, cleaned1 = view._fetch_resource("pods", None, None)
|
|
541
|
+
_, page1, cleaned1, count1 = view._fetch_resource("pods", None, None)
|
|
446
542
|
assert len(page1.items) == 100
|
|
447
543
|
assert len(cleaned1) == 100
|
|
544
|
+
assert count1 == 100
|
|
448
545
|
assert page1.metadata._continue == "100"
|
|
449
546
|
|
|
450
|
-
_, page2, cleaned2 = view._fetch_resource("pods", None, None, continue_token=page1.metadata._continue)
|
|
547
|
+
_, page2, cleaned2, count2 = view._fetch_resource("pods", None, None, continue_token=page1.metadata._continue)
|
|
451
548
|
assert len(page2.items) == 100
|
|
452
549
|
assert len(cleaned2) == 100
|
|
550
|
+
assert count2 == 100
|
|
453
551
|
assert page2.metadata._continue == "200"
|
|
454
552
|
|
|
455
|
-
_, page3, cleaned3 = view._fetch_resource("pods", None, None, continue_token=page2.metadata._continue)
|
|
553
|
+
_, page3, cleaned3, count3 = view._fetch_resource("pods", None, None, continue_token=page2.metadata._continue)
|
|
456
554
|
assert len(page3.items) == 50
|
|
457
555
|
assert len(cleaned3) == 50
|
|
556
|
+
assert count3 == 50
|
|
458
557
|
assert page3.metadata._continue is None
|
|
459
558
|
|
|
460
559
|
asyncio.run(_run())
|
|
@@ -469,6 +568,7 @@ def test_mock_pod_pagination_next_and_prev_from_cached_pages() -> None:
|
|
|
469
568
|
SimpleNamespace(items=rows, metadata=SimpleNamespace(_continue=next_token)),
|
|
470
569
|
rows,
|
|
471
570
|
next_token,
|
|
571
|
+
len(rows),
|
|
472
572
|
)
|
|
473
573
|
|
|
474
574
|
async def _run() -> None:
|
|
@@ -488,6 +588,10 @@ def test_mock_pod_pagination_next_and_prev_from_cached_pages() -> None:
|
|
|
488
588
|
view.data = view.resource_pages[key][0][0]
|
|
489
589
|
view.table = SimpleNamespace(raw_data=[], data=[])
|
|
490
590
|
view.panel = SimpleNamespace(resource_count=0)
|
|
591
|
+
view.FACTORY_CACHE = SimpleNamespace(
|
|
592
|
+
filter=lambda data, _keyword: data,
|
|
593
|
+
clean=lambda data: data.items,
|
|
594
|
+
)
|
|
491
595
|
|
|
492
596
|
view.action_next_page()
|
|
493
597
|
assert view.page_index == 1
|
|
@@ -516,6 +620,7 @@ def test_pagination_bindings_visibility_changes_by_page() -> None:
|
|
|
516
620
|
SimpleNamespace(items=rows, metadata=SimpleNamespace(_continue=next_token)),
|
|
517
621
|
rows,
|
|
518
622
|
next_token,
|
|
623
|
+
len(rows),
|
|
519
624
|
)
|
|
520
625
|
|
|
521
626
|
async def _run() -> None:
|
|
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
|
|
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
|
|
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
|