conceptkernel 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cklib/__init__.py +29 -0
- cklib/actions.py +277 -0
- cklib/auth.py +98 -0
- cklib/dispatch.py +179 -0
- cklib/entities.py +95 -0
- cklib/events.py +43 -0
- cklib/execution.py +249 -0
- cklib/instance.py +241 -0
- cklib/ledger.py +216 -0
- cklib/processor.py +237 -0
- cklib/prov.py +631 -0
- cklib/serve.py +254 -0
- cklib/urn.py +466 -0
- conceptkernel-1.0.0.dist-info/METADATA +195 -0
- conceptkernel-1.0.0.dist-info/RECORD +18 -0
- conceptkernel-1.0.0.dist-info/WHEEL +5 -0
- conceptkernel-1.0.0.dist-info/licenses/LICENSE +21 -0
- conceptkernel-1.0.0.dist-info/top_level.txt +1 -0
cklib/ledger.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Kernel-wide action ledger — append-only JSONL action log.
|
|
2
|
+
|
|
3
|
+
Extracted from LOCAL.ClaudeCode's ``log_action()`` pattern
|
|
4
|
+
(ref-tg-cli/concepts/LOCAL.ClaudeCode/tool/processor.py lines 79-134).
|
|
5
|
+
|
|
6
|
+
Each kernel invocation writes one JSONL entry to a daily file under
|
|
7
|
+
``storage/ledger/actions-{YYYY-MM-DD}.jsonl``. Files are never rewritten —
|
|
8
|
+
entries are always appended.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from cklib.ledger import log_action, read_ledger
|
|
12
|
+
|
|
13
|
+
log_action(
|
|
14
|
+
ck_dir="concepts/MyKernel",
|
|
15
|
+
action_name="task.create",
|
|
16
|
+
status="completed",
|
|
17
|
+
task_id="T001",
|
|
18
|
+
goal_id="G001",
|
|
19
|
+
detail={"items": 3},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
entries = read_ledger("concepts/MyKernel", limit=20)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import threading
|
|
28
|
+
import time as _time
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from glob import glob
|
|
31
|
+
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
__all__ = ["log_action", "read_ledger"]
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Internal: resolve kernel identity from conceptkernel.yaml
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
# File-level lock — one per process, protects the open-write-flush sequence.
|
|
41
|
+
_write_lock = threading.Lock()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_kernel(ck_dir: str) -> tuple[str, str, str]:
|
|
45
|
+
"""Return (kernel_name, kernel_urn, kernel_version) from conceptkernel.yaml.
|
|
46
|
+
|
|
47
|
+
Falls back to the directory basename if the YAML is absent or malformed.
|
|
48
|
+
"""
|
|
49
|
+
ck_yaml = os.path.join(ck_dir, "conceptkernel.yaml")
|
|
50
|
+
if os.path.isfile(ck_yaml):
|
|
51
|
+
try:
|
|
52
|
+
with open(ck_yaml) as f:
|
|
53
|
+
data = yaml.safe_load(f)
|
|
54
|
+
meta = data.get("metadata", {})
|
|
55
|
+
name = meta.get("name", os.path.basename(os.path.abspath(ck_dir)))
|
|
56
|
+
urn = meta.get("urn", "")
|
|
57
|
+
version = meta.get("version", "v1.0")
|
|
58
|
+
if not urn:
|
|
59
|
+
urn = "ckp://Kernel#%s:%s" % (name, version)
|
|
60
|
+
return name, urn, version
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Fallback
|
|
65
|
+
name = os.path.basename(os.path.abspath(ck_dir))
|
|
66
|
+
return name, "ckp://Kernel#%s:v1.0" % name, "v1.0"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# log_action — append one JSONL entry
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def log_action(
|
|
74
|
+
ck_dir: str,
|
|
75
|
+
action_name: str,
|
|
76
|
+
*,
|
|
77
|
+
status: str = "started",
|
|
78
|
+
target_kernel: str = "",
|
|
79
|
+
task_id: str = "",
|
|
80
|
+
goal_id: str = "",
|
|
81
|
+
instance_id: str = "",
|
|
82
|
+
persona: str = "",
|
|
83
|
+
detail: dict | None = None,
|
|
84
|
+
) -> dict:
|
|
85
|
+
"""Append one action entry to the daily ledger file.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
ck_dir: Root directory of the Concept Kernel.
|
|
89
|
+
action_name: Dotted action name (e.g. ``task.create``).
|
|
90
|
+
status: One of ``started``, ``completed``, ``failed``,
|
|
91
|
+
``timeout``, ``error``.
|
|
92
|
+
target_kernel: Optional target kernel name for cross-kernel actions.
|
|
93
|
+
task_id: Optional task identifier (``T001``).
|
|
94
|
+
goal_id: Optional goal identifier (``G001``).
|
|
95
|
+
instance_id: Optional instance identifier.
|
|
96
|
+
persona: Optional persona label.
|
|
97
|
+
detail: Optional dict of extra payload.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The entry dict that was written.
|
|
101
|
+
"""
|
|
102
|
+
kernel_name, kernel_urn, _version = _resolve_kernel(ck_dir)
|
|
103
|
+
|
|
104
|
+
now = datetime.now(timezone.utc)
|
|
105
|
+
ts_iso = now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
106
|
+
ts_ms = int(now.timestamp() * 1000)
|
|
107
|
+
date_str = now.strftime("%Y-%m-%d")
|
|
108
|
+
|
|
109
|
+
# Build Action URN: ckp://Action#{KernelName}/{action}-{ts_ms}
|
|
110
|
+
action_urn = "ckp://Action#%s/%s-%d" % (kernel_name, action_name, ts_ms)
|
|
111
|
+
|
|
112
|
+
entry: dict = {
|
|
113
|
+
"action_urn": action_urn,
|
|
114
|
+
"action": action_name,
|
|
115
|
+
"target_kernel": target_kernel,
|
|
116
|
+
"task_id": task_id,
|
|
117
|
+
"goal_id": goal_id,
|
|
118
|
+
"instance_id": instance_id,
|
|
119
|
+
"persona": persona,
|
|
120
|
+
"status": status,
|
|
121
|
+
"timestamp": ts_iso,
|
|
122
|
+
"timestamp_ms": ts_ms,
|
|
123
|
+
"prov:wasAssociatedWith": kernel_urn,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Conditional PROV-O fields
|
|
127
|
+
if task_id:
|
|
128
|
+
entry["prov:wasStartedBy"] = "ckp://Task#%s" % task_id
|
|
129
|
+
if goal_id:
|
|
130
|
+
entry["prov:wasInfluencedBy"] = "ckp://Goal#%s" % goal_id
|
|
131
|
+
|
|
132
|
+
# Optional detail payload
|
|
133
|
+
if detail is not None:
|
|
134
|
+
entry["detail"] = detail
|
|
135
|
+
|
|
136
|
+
# Ensure ledger directory exists
|
|
137
|
+
ledger_dir = os.path.join(ck_dir, "storage", "ledger")
|
|
138
|
+
os.makedirs(ledger_dir, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
ledger_file = os.path.join(ledger_dir, "actions-%s.jsonl" % date_str)
|
|
141
|
+
line = json.dumps(entry, separators=(",", ":")) + "\n"
|
|
142
|
+
|
|
143
|
+
# Thread-safe append — hold lock across open+write+flush
|
|
144
|
+
with _write_lock:
|
|
145
|
+
with open(ledger_file, "a") as f:
|
|
146
|
+
f.write(line)
|
|
147
|
+
f.flush()
|
|
148
|
+
os.fsync(f.fileno())
|
|
149
|
+
|
|
150
|
+
return entry
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# read_ledger — read recent entries across daily files
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def read_ledger(ck_dir: str, limit: int = 50) -> list[dict]:
|
|
158
|
+
"""Read recent ledger entries in reverse chronological order.
|
|
159
|
+
|
|
160
|
+
Scans daily ``actions-*.jsonl`` files in ``storage/ledger/``, starting
|
|
161
|
+
from the most recent file and working backwards. Stops once *limit*
|
|
162
|
+
entries have been collected.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
ck_dir: Root directory of the Concept Kernel.
|
|
166
|
+
limit: Maximum number of entries to return. Pass ``0`` for
|
|
167
|
+
unlimited (reads every file).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of entry dicts, newest first.
|
|
171
|
+
"""
|
|
172
|
+
ledger_dir = os.path.join(ck_dir, "storage", "ledger")
|
|
173
|
+
if not os.path.isdir(ledger_dir):
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
# Collect daily files and sort descending by filename (date)
|
|
177
|
+
pattern = os.path.join(ledger_dir, "actions-*.jsonl")
|
|
178
|
+
files = sorted(glob(pattern), reverse=True)
|
|
179
|
+
|
|
180
|
+
entries: list[dict] = []
|
|
181
|
+
for fpath in files:
|
|
182
|
+
if limit > 0 and len(entries) >= limit:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
file_entries = _read_jsonl_file(fpath)
|
|
186
|
+
# Reverse so newest entries in this file come first
|
|
187
|
+
file_entries.reverse()
|
|
188
|
+
|
|
189
|
+
if limit > 0:
|
|
190
|
+
remaining = limit - len(entries)
|
|
191
|
+
entries.extend(file_entries[:remaining])
|
|
192
|
+
else:
|
|
193
|
+
entries.extend(file_entries)
|
|
194
|
+
|
|
195
|
+
return entries
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _read_jsonl_file(path: str) -> list[dict]:
|
|
199
|
+
"""Read all valid JSON lines from a file.
|
|
200
|
+
|
|
201
|
+
Silently skips blank or malformed lines.
|
|
202
|
+
"""
|
|
203
|
+
entries = []
|
|
204
|
+
try:
|
|
205
|
+
with open(path) as f:
|
|
206
|
+
for line in f:
|
|
207
|
+
line = line.strip()
|
|
208
|
+
if not line:
|
|
209
|
+
continue
|
|
210
|
+
try:
|
|
211
|
+
entries.append(json.loads(line))
|
|
212
|
+
except json.JSONDecodeError:
|
|
213
|
+
continue
|
|
214
|
+
except OSError:
|
|
215
|
+
pass
|
|
216
|
+
return entries
|
cklib/processor.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""KernelProcessor — base class for all CK processors.
|
|
2
|
+
|
|
3
|
+
Provides: identity loading, handle_message dispatch via @on decorator,
|
|
4
|
+
status, listen, and CLI argument handling.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from cklib import KernelProcessor, on
|
|
8
|
+
|
|
9
|
+
class MyKernel(KernelProcessor):
|
|
10
|
+
@on("my.action")
|
|
11
|
+
def do_something(self, data):
|
|
12
|
+
return {"result": "done"}
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
MyKernel.run()
|
|
16
|
+
"""
|
|
17
|
+
import argparse
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KernelProcessor:
|
|
28
|
+
"""Base processor for all Concept Kernels."""
|
|
29
|
+
|
|
30
|
+
# Subclass registry of @on handlers
|
|
31
|
+
_handlers = None
|
|
32
|
+
|
|
33
|
+
def __init__(self, ck_dir=None):
|
|
34
|
+
if ck_dir is None:
|
|
35
|
+
# Default: assume tool/processor.py → CK is parent
|
|
36
|
+
frame = sys._getframe(1)
|
|
37
|
+
caller_file = frame.f_globals.get("__file__", "")
|
|
38
|
+
if caller_file:
|
|
39
|
+
ck_dir = os.path.abspath(os.path.join(os.path.dirname(caller_file), ".."))
|
|
40
|
+
else:
|
|
41
|
+
ck_dir = os.getcwd()
|
|
42
|
+
|
|
43
|
+
self.ck_dir = os.path.abspath(ck_dir)
|
|
44
|
+
self.project_root = os.path.abspath(os.path.join(self.ck_dir, "..", ".."))
|
|
45
|
+
self.concepts_dir = os.path.join(self.project_root, "concepts")
|
|
46
|
+
self._identity = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def identity(self):
|
|
50
|
+
"""Load and cache conceptkernel.yaml."""
|
|
51
|
+
if self._identity is None:
|
|
52
|
+
yaml_path = os.path.join(self.ck_dir, "conceptkernel.yaml")
|
|
53
|
+
with open(yaml_path) as f:
|
|
54
|
+
self._identity = yaml.safe_load(f)
|
|
55
|
+
return self._identity
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def name(self):
|
|
59
|
+
return self.identity.get("metadata", {}).get("name", "")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def urn(self):
|
|
63
|
+
return self.identity.get("metadata", {}).get("urn", "")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def kernel_type(self):
|
|
67
|
+
return self.identity.get("qualities", {}).get("type", "unknown")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def governance(self):
|
|
71
|
+
return self.identity.get("qualities", {}).get("governance_mode", "unknown")
|
|
72
|
+
|
|
73
|
+
def handle_message(self, msg):
|
|
74
|
+
"""Dispatch incoming message to @on handler by action name.
|
|
75
|
+
|
|
76
|
+
Resolution order:
|
|
77
|
+
1. @on handlers registered by subclass
|
|
78
|
+
2. Built-in actions (status, ontology)
|
|
79
|
+
3. Composed actions via COMPOSES/EXTENDS edges
|
|
80
|
+
4. Error: unknown action
|
|
81
|
+
"""
|
|
82
|
+
action = msg.get("action", "status") if isinstance(msg, dict) else "status"
|
|
83
|
+
data = msg.get("data", {}) if isinstance(msg, dict) else {}
|
|
84
|
+
|
|
85
|
+
# Check for @on handler
|
|
86
|
+
handlers = self._get_handlers()
|
|
87
|
+
if action in handlers:
|
|
88
|
+
method_name = handlers[action]
|
|
89
|
+
method = getattr(self, method_name)
|
|
90
|
+
return method(data)
|
|
91
|
+
|
|
92
|
+
# Built-in actions
|
|
93
|
+
if action == "status":
|
|
94
|
+
return self._status()
|
|
95
|
+
elif action == "ontology":
|
|
96
|
+
return self._ontology()
|
|
97
|
+
|
|
98
|
+
# Composed action resolution via edges
|
|
99
|
+
composed = self._resolve_composed(action)
|
|
100
|
+
if composed:
|
|
101
|
+
target_kernel, target_action = composed
|
|
102
|
+
return self._forward_to_edge(target_kernel, action, data)
|
|
103
|
+
|
|
104
|
+
return {"status": "error", "message": "unknown action: %s" % action,
|
|
105
|
+
"kernel": self.name}
|
|
106
|
+
|
|
107
|
+
def _resolve_composed(self, action):
|
|
108
|
+
"""Check if action is available via COMPOSES/EXTENDS edges."""
|
|
109
|
+
try:
|
|
110
|
+
from cklib.actions import resolve_composed_actions
|
|
111
|
+
composed = resolve_composed_actions(self.ck_dir)
|
|
112
|
+
if action in composed:
|
|
113
|
+
return composed[action], action
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def _forward_to_edge(self, target_kernel, action, data):
|
|
119
|
+
"""Forward action to a composed kernel via dispatch."""
|
|
120
|
+
try:
|
|
121
|
+
from cklib.dispatch import send_action
|
|
122
|
+
result = send_action(target_kernel, action, data)
|
|
123
|
+
result["_composed_from"] = self.name
|
|
124
|
+
result["_target_kernel"] = target_kernel
|
|
125
|
+
return result
|
|
126
|
+
except Exception as e:
|
|
127
|
+
return {
|
|
128
|
+
"status": "error",
|
|
129
|
+
"message": "edge forward failed: %s" % str(e),
|
|
130
|
+
"kernel": self.name,
|
|
131
|
+
"target_kernel": target_kernel,
|
|
132
|
+
"action": action,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def _get_handlers(self):
|
|
136
|
+
"""Collect @on handlers from this class and all parents."""
|
|
137
|
+
if self.__class__._handlers is not None:
|
|
138
|
+
return self.__class__._handlers
|
|
139
|
+
|
|
140
|
+
handlers = {}
|
|
141
|
+
for cls in reversed(type(self).__mro__):
|
|
142
|
+
for attr_name in dir(cls):
|
|
143
|
+
attr = getattr(cls, attr_name, None)
|
|
144
|
+
if callable(attr) and hasattr(attr, "_on_action"):
|
|
145
|
+
handlers[attr._on_action] = attr_name
|
|
146
|
+
self.__class__._handlers = handlers
|
|
147
|
+
return handlers
|
|
148
|
+
|
|
149
|
+
def _status(self):
|
|
150
|
+
"""Built-in status action."""
|
|
151
|
+
return {
|
|
152
|
+
"status": "ok",
|
|
153
|
+
"kernel": self.name,
|
|
154
|
+
"urn": self.urn,
|
|
155
|
+
"type": self.kernel_type,
|
|
156
|
+
"governance": self.governance,
|
|
157
|
+
"processed_at": datetime.now(timezone.utc).isoformat(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def _ontology(self):
|
|
161
|
+
"""Built-in ontology action — return conceptkernel.yaml."""
|
|
162
|
+
return {
|
|
163
|
+
"status": "ok",
|
|
164
|
+
"kernel": self.name,
|
|
165
|
+
"content_type": "text/yaml",
|
|
166
|
+
"data": self.identity,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def listen(self):
|
|
170
|
+
"""Start NATS listener using NatsKernelLoop."""
|
|
171
|
+
sys.path.insert(0, os.path.join(self.concepts_dir, "CK.Lib", "tool"))
|
|
172
|
+
from nats_kernel import NatsKernelLoop
|
|
173
|
+
loop = NatsKernelLoop(self.ck_dir, self.handle_message)
|
|
174
|
+
asyncio.run(loop.run())
|
|
175
|
+
|
|
176
|
+
def status_cli(self):
|
|
177
|
+
"""Print status to stdout."""
|
|
178
|
+
print("[status] %s" % self.name)
|
|
179
|
+
print(" urn: %s" % self.urn)
|
|
180
|
+
print(" type: %s" % self.kernel_type)
|
|
181
|
+
print(" governance: %s" % self.governance)
|
|
182
|
+
|
|
183
|
+
def serve(self, port=8901, api_token=None):
|
|
184
|
+
"""Start FastAPI HTTP server wrapping this processor's @on handlers."""
|
|
185
|
+
from cklib.serve import KernelServer
|
|
186
|
+
token = api_token or os.environ.get("CK_API_TOKEN")
|
|
187
|
+
server = KernelServer(self, api_token=token, port=port)
|
|
188
|
+
server.run(port=port)
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def run(cls):
|
|
192
|
+
"""CLI entry point — parse args, dispatch."""
|
|
193
|
+
parser = argparse.ArgumentParser(description=cls.__doc__ or cls.__name__)
|
|
194
|
+
parser.add_argument("--status", action="store_true", help="Show kernel status")
|
|
195
|
+
parser.add_argument("--listen", action="store_true", help="NATS listener mode")
|
|
196
|
+
parser.add_argument("--serve", action="store_true", help="HTTP server mode")
|
|
197
|
+
parser.add_argument("--port", type=int, default=8901, help="HTTP port (default: 8901)")
|
|
198
|
+
parser.add_argument("--token", type=str, default=None, help="API token for HTTP auth")
|
|
199
|
+
parser.add_argument("--action", type=str, help="Execute action directly")
|
|
200
|
+
parser.add_argument("--data", type=str, default=None, help="JSON data for action")
|
|
201
|
+
parser.add_argument("--pretty", action="store_true", help="Pretty-print output")
|
|
202
|
+
|
|
203
|
+
# Let subclasses add their own args
|
|
204
|
+
cls._add_args(parser)
|
|
205
|
+
args = parser.parse_args()
|
|
206
|
+
|
|
207
|
+
# Resolve ck_dir from the subclass module (not cklib)
|
|
208
|
+
caller_file = sys.modules[cls.__module__].__file__ if cls.__module__ in sys.modules else None
|
|
209
|
+
ck_dir = os.path.abspath(os.path.join(os.path.dirname(caller_file), "..")) if caller_file else None
|
|
210
|
+
instance = cls(ck_dir=ck_dir)
|
|
211
|
+
|
|
212
|
+
if args.listen:
|
|
213
|
+
return instance.listen()
|
|
214
|
+
elif args.serve:
|
|
215
|
+
return instance.serve(port=args.port, api_token=args.token)
|
|
216
|
+
elif args.status:
|
|
217
|
+
return instance.status_cli()
|
|
218
|
+
elif args.action:
|
|
219
|
+
data = json.loads(args.data) if args.data else {}
|
|
220
|
+
result = instance.handle_message({"action": args.action, "data": data})
|
|
221
|
+
indent = 2 if args.pretty else None
|
|
222
|
+
print(json.dumps(result, indent=indent))
|
|
223
|
+
return 0 if result.get("status") != "error" else 1
|
|
224
|
+
|
|
225
|
+
# Let subclass handle remaining args
|
|
226
|
+
return cls._handle_args(instance, args)
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
def _add_args(cls, parser):
|
|
230
|
+
"""Override to add subclass-specific CLI args."""
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def _handle_args(cls, instance, args):
|
|
235
|
+
"""Override to handle subclass-specific CLI args."""
|
|
236
|
+
instance.status_cli()
|
|
237
|
+
return 0
|