cmem-cmemc 25.5.0rc1__py3-none-any.whl → 26.1.0rc1__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.
- cmem_cmemc/cli.py +11 -6
- cmem_cmemc/command.py +1 -1
- cmem_cmemc/command_group.py +59 -31
- cmem_cmemc/commands/acl.py +403 -26
- cmem_cmemc/commands/admin.py +10 -10
- cmem_cmemc/commands/client.py +12 -5
- cmem_cmemc/commands/config.py +106 -12
- cmem_cmemc/commands/dataset.py +163 -172
- cmem_cmemc/commands/file.py +509 -0
- cmem_cmemc/commands/graph.py +200 -72
- cmem_cmemc/commands/graph_imports.py +12 -5
- cmem_cmemc/commands/graph_insights.py +157 -53
- cmem_cmemc/commands/metrics.py +15 -9
- cmem_cmemc/commands/migration.py +12 -4
- cmem_cmemc/commands/package.py +548 -0
- cmem_cmemc/commands/project.py +157 -22
- cmem_cmemc/commands/python.py +9 -5
- cmem_cmemc/commands/query.py +119 -25
- cmem_cmemc/commands/scheduler.py +6 -4
- cmem_cmemc/commands/store.py +2 -1
- cmem_cmemc/commands/user.py +124 -24
- cmem_cmemc/commands/validation.py +15 -10
- cmem_cmemc/commands/variable.py +264 -61
- cmem_cmemc/commands/vocabulary.py +31 -17
- cmem_cmemc/commands/workflow.py +21 -11
- cmem_cmemc/completion.py +126 -109
- cmem_cmemc/context.py +40 -10
- cmem_cmemc/exceptions.py +8 -2
- cmem_cmemc/manual_helper/graph.py +2 -2
- cmem_cmemc/manual_helper/multi_page.py +5 -7
- cmem_cmemc/object_list.py +234 -7
- cmem_cmemc/placeholder.py +2 -2
- cmem_cmemc/string_processor.py +153 -4
- cmem_cmemc/title_helper.py +50 -0
- cmem_cmemc/utils.py +9 -8
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/METADATA +7 -6
- cmem_cmemc-26.1.0rc1.dist-info/RECORD +62 -0
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/WHEEL +1 -1
- cmem_cmemc/commands/resource.py +0 -220
- cmem_cmemc-25.5.0rc1.dist-info/RECORD +0 -61
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/entry_points.txt +0 -0
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/licenses/LICENSE +0 -0
cmem_cmemc/string_processor.py
CHANGED
|
@@ -4,8 +4,11 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from urllib.parse import quote
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
from cmem.cmempy.
|
|
7
|
+
from cmem.cmempy.config import get_cmem_base_uri, get_di_api_endpoint
|
|
8
|
+
from cmem.cmempy.workflow.workflow import get_workflow_editor_uri
|
|
9
|
+
from cmem.cmempy.workspace import get_task_plugins
|
|
10
|
+
from cmem.cmempy.workspace.search import list_items
|
|
11
|
+
from humanize import naturalsize, naturaltime
|
|
9
12
|
|
|
10
13
|
from cmem_cmemc.title_helper import TitleHelper
|
|
11
14
|
from cmem_cmemc.utils import get_graphs_as_dict
|
|
@@ -19,6 +22,17 @@ class StringProcessor(ABC):
|
|
|
19
22
|
"""Process a single string content and output the processed string."""
|
|
20
23
|
|
|
21
24
|
|
|
25
|
+
class FileSize(StringProcessor):
|
|
26
|
+
"""Create a human-readable file size string."""
|
|
27
|
+
|
|
28
|
+
def process(self, text: str) -> str:
|
|
29
|
+
"""Process a single string content and output the processed string."""
|
|
30
|
+
try:
|
|
31
|
+
return "" if text is None else naturalsize(value=text, gnu=True)
|
|
32
|
+
except ValueError:
|
|
33
|
+
return text
|
|
34
|
+
|
|
35
|
+
|
|
22
36
|
class TimeAgo(StringProcessor):
|
|
23
37
|
"""Create a string similar to 'x minutes ago' from a timestamp or iso-formated string."""
|
|
24
38
|
|
|
@@ -28,13 +42,13 @@ class TimeAgo(StringProcessor):
|
|
|
28
42
|
return ""
|
|
29
43
|
try:
|
|
30
44
|
stamp = datetime.fromisoformat(str(text))
|
|
31
|
-
return str(
|
|
45
|
+
return str(naturaltime(stamp, when=datetime.now(tz=timezone.utc)))
|
|
32
46
|
except (ValueError, TypeError):
|
|
33
47
|
pass
|
|
34
48
|
try:
|
|
35
49
|
text_as_int = int(text)
|
|
36
50
|
stamp = datetime.fromtimestamp(text_as_int / 1000, tz=timezone.utc)
|
|
37
|
-
return str(
|
|
51
|
+
return str(naturaltime(stamp, when=datetime.now(tz=timezone.utc)))
|
|
38
52
|
except ValueError:
|
|
39
53
|
return text
|
|
40
54
|
|
|
@@ -59,6 +73,28 @@ class GraphLink(StringProcessor):
|
|
|
59
73
|
return f"[link={link}]{label}[/link]" if label else text
|
|
60
74
|
|
|
61
75
|
|
|
76
|
+
class QueryLink(StringProcessor):
|
|
77
|
+
"""Create a query link from a query IRI cell
|
|
78
|
+
|
|
79
|
+
"Visit my [link=https://www.willmcgugan.com]blog[/link]!"
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, catalog_graph: str, queries: dict):
|
|
83
|
+
self.catalog_graph = catalog_graph
|
|
84
|
+
self.queries = queries
|
|
85
|
+
|
|
86
|
+
def process(self, text: str) -> str:
|
|
87
|
+
"""Process a single string content and output the processed string."""
|
|
88
|
+
# Find the query entry from the queries dict
|
|
89
|
+
query_entry = self.queries.get(text)
|
|
90
|
+
if query_entry:
|
|
91
|
+
# Use the SparqlQuery's get_editor_url method for consistent URL generation
|
|
92
|
+
link = query_entry.get_editor_url(graph=self.catalog_graph)
|
|
93
|
+
label = query_entry.label
|
|
94
|
+
return f"[link={link}]{label}[/link]"
|
|
95
|
+
return text
|
|
96
|
+
|
|
97
|
+
|
|
62
98
|
class ResourceLink(StringProcessor):
|
|
63
99
|
"""Create a resource link from an IRI cell
|
|
64
100
|
|
|
@@ -77,6 +113,119 @@ class ResourceLink(StringProcessor):
|
|
|
77
113
|
return f"[link={link}]{label}[/link]"
|
|
78
114
|
|
|
79
115
|
|
|
116
|
+
class ProjectLink(StringProcessor):
|
|
117
|
+
"""Create a project link from a project ID cell
|
|
118
|
+
|
|
119
|
+
"Visit my [link=https://www.willmcgugan.com]blog[/link]!"
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, projects: dict):
|
|
123
|
+
self.projects = projects
|
|
124
|
+
self.base = get_di_api_endpoint() + "/workbench/projects/"
|
|
125
|
+
|
|
126
|
+
def process(self, text: str) -> str:
|
|
127
|
+
"""Process a single string content and output the processed string."""
|
|
128
|
+
project = self.projects.get(text)
|
|
129
|
+
if project:
|
|
130
|
+
link = self.base + text
|
|
131
|
+
label = project["metaData"].get("label", text)
|
|
132
|
+
return f"[link={link}]{label}[/link]"
|
|
133
|
+
return text
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class WorkflowLink(StringProcessor):
|
|
137
|
+
"""Create a workflow link from a workflow ID cell
|
|
138
|
+
|
|
139
|
+
"Visit my [link=https://www.willmcgugan.com]blog[/link]!"
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, workflows: dict):
|
|
143
|
+
self.workflows = workflows
|
|
144
|
+
self.base_uri = get_workflow_editor_uri()
|
|
145
|
+
|
|
146
|
+
def process(self, text: str) -> str:
|
|
147
|
+
"""Process a single string content and output the processed string."""
|
|
148
|
+
workflow = self.workflows.get(text)
|
|
149
|
+
if workflow:
|
|
150
|
+
project_id, task_id = text.split(":")
|
|
151
|
+
link = self.base_uri.format(project_id, task_id)
|
|
152
|
+
label = workflow["label"]
|
|
153
|
+
return f"[link={link}]{label}[/link]"
|
|
154
|
+
return text
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class DatasetTypeLink(StringProcessor):
|
|
158
|
+
"""Create a documentation link from a dataset type (pluginId) cell
|
|
159
|
+
|
|
160
|
+
Links to the Corporate Memory dataset documentation.
|
|
161
|
+
Example: "json" -> "[link=https://documentation.eccenca.com/latest/build/reference/dataset/json/]JSON[/link]"
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self, base_url: str = "https://documentation.eccenca.com/latest/build/reference/dataset/"
|
|
166
|
+
):
|
|
167
|
+
self.base_url = base_url
|
|
168
|
+
self.type_labels: dict[str, str] = {}
|
|
169
|
+
# Dataset types that don't have documentation pages
|
|
170
|
+
self.undocumented_types = {"variableDataset"}
|
|
171
|
+
# Import here to avoid circular imports
|
|
172
|
+
|
|
173
|
+
plugins = get_task_plugins()
|
|
174
|
+
for plugin_id, plugin in plugins.items():
|
|
175
|
+
if plugin["taskType"] == "Dataset":
|
|
176
|
+
self.type_labels[plugin_id] = plugin["title"]
|
|
177
|
+
|
|
178
|
+
def process(self, text: str) -> str:
|
|
179
|
+
"""Process a dataset type and create a documentation link if available."""
|
|
180
|
+
if not text:
|
|
181
|
+
return text
|
|
182
|
+
|
|
183
|
+
# Use the title if available, otherwise fall back to the text
|
|
184
|
+
label = self.type_labels.get(text, text)
|
|
185
|
+
|
|
186
|
+
# Only create link if this type is not in the undocumented list
|
|
187
|
+
if text not in self.undocumented_types:
|
|
188
|
+
link = f"{self.base_url}{text}/"
|
|
189
|
+
return f"[link={link}]{label}[/link]"
|
|
190
|
+
|
|
191
|
+
return label
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class DatasetLink(StringProcessor):
|
|
195
|
+
"""Create a workspace link from a dataset ID cell
|
|
196
|
+
|
|
197
|
+
Links to the Corporate Memory workspace dataset page and displays the dataset label.
|
|
198
|
+
Example: "project:dataset" ->
|
|
199
|
+
"[link=https://cmem.example.com/workspaces/datasets/...]Dataset Label[/link]"
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self) -> None:
|
|
203
|
+
self.cmem_base_uri = get_cmem_base_uri()
|
|
204
|
+
self.dataset_urls: dict[str, str] = {}
|
|
205
|
+
self.dataset_labels: dict[str, str] = {}
|
|
206
|
+
# Build a mapping of dataset_id -> URL path and label
|
|
207
|
+
datasets = list_items(item_type="dataset")["results"]
|
|
208
|
+
for dataset in datasets:
|
|
209
|
+
dataset_id = dataset["projectId"] + ":" + dataset["id"]
|
|
210
|
+
url_path = dataset["itemLinks"][0]["path"]
|
|
211
|
+
self.dataset_urls[dataset_id] = url_path
|
|
212
|
+
self.dataset_labels[dataset_id] = dataset["label"]
|
|
213
|
+
|
|
214
|
+
def process(self, text: str) -> str:
|
|
215
|
+
"""Process a dataset ID and create a workspace link with label if available."""
|
|
216
|
+
if not text:
|
|
217
|
+
return text
|
|
218
|
+
|
|
219
|
+
# Check if we have a URL for this dataset ID
|
|
220
|
+
if text in self.dataset_urls:
|
|
221
|
+
full_url = self.cmem_base_uri + self.dataset_urls[text]
|
|
222
|
+
label = self.dataset_labels.get(text, text)
|
|
223
|
+
return f"[link={full_url}]{label}[/link]"
|
|
224
|
+
|
|
225
|
+
# If no URL found, return the text as-is
|
|
226
|
+
return text
|
|
227
|
+
|
|
228
|
+
|
|
80
229
|
def process_row(row: list[str], hints: dict[int, StringProcessor]) -> list[str]:
|
|
81
230
|
"""Process all cells in a row according to the StringProcessors"""
|
|
82
231
|
processed_row = []
|
cmem_cmemc/title_helper.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""Title helper functions."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from typing import ClassVar
|
|
4
5
|
|
|
5
6
|
from cmem.cmempy.api import get_json
|
|
6
7
|
from cmem.cmempy.config import get_dp_api_endpoint
|
|
8
|
+
from cmem.cmempy.workspace import get_task_plugins
|
|
9
|
+
from cmem.cmempy.workspace.projects.project import get_projects
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class TitleHelper:
|
|
@@ -39,3 +42,50 @@ class TitleHelper:
|
|
|
39
42
|
output[title["iri"]] = title["title"]
|
|
40
43
|
|
|
41
44
|
return output[iri] if isinstance(iri, str) else output
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ProjectTitleHelper(TitleHelper):
|
|
48
|
+
"""Title helper for project IDs with class-level caching."""
|
|
49
|
+
|
|
50
|
+
_labels_cache: ClassVar[dict[str, str]] = {}
|
|
51
|
+
_cache_initialized: ClassVar[bool] = False
|
|
52
|
+
|
|
53
|
+
def get(self, project_id: str | list[str]) -> str | dict[str, str]:
|
|
54
|
+
"""Get the label of a project (or list of projects)."""
|
|
55
|
+
# Fetch all project labels once at class level
|
|
56
|
+
if not ProjectTitleHelper._cache_initialized:
|
|
57
|
+
projects = get_projects()
|
|
58
|
+
for project in projects:
|
|
59
|
+
ProjectTitleHelper._labels_cache[project["name"]] = project["metaData"].get(
|
|
60
|
+
"label", ""
|
|
61
|
+
)
|
|
62
|
+
ProjectTitleHelper._cache_initialized = True
|
|
63
|
+
|
|
64
|
+
# Build and return output
|
|
65
|
+
if isinstance(project_id, str):
|
|
66
|
+
return ProjectTitleHelper._labels_cache.get(project_id, "")
|
|
67
|
+
return {pid: ProjectTitleHelper._labels_cache.get(pid, "") for pid in project_id}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DatasetTypeTitleHelper(TitleHelper):
|
|
71
|
+
"""Title helper for dataset types with class-level caching."""
|
|
72
|
+
|
|
73
|
+
_labels_cache: ClassVar[dict[str, str]] = {}
|
|
74
|
+
_cache_initialized: ClassVar[bool] = False
|
|
75
|
+
|
|
76
|
+
def get(self, plugin_id: str | list[str]) -> str | dict[str, str]:
|
|
77
|
+
"""Get the description of a dataset type (or list of types)."""
|
|
78
|
+
# Fetch all plugin descriptions once at class level
|
|
79
|
+
if not DatasetTypeTitleHelper._cache_initialized:
|
|
80
|
+
plugins = get_task_plugins()
|
|
81
|
+
for pid, plugin in plugins.items():
|
|
82
|
+
if plugin["taskType"] == "Dataset":
|
|
83
|
+
title = plugin["title"]
|
|
84
|
+
description = plugin["description"].partition("\n")[0]
|
|
85
|
+
DatasetTypeTitleHelper._labels_cache[pid] = f"{title}: {description}"
|
|
86
|
+
DatasetTypeTitleHelper._cache_initialized = True
|
|
87
|
+
|
|
88
|
+
# Build and return output
|
|
89
|
+
if isinstance(plugin_id, str):
|
|
90
|
+
return DatasetTypeTitleHelper._labels_cache.get(plugin_id, plugin_id)
|
|
91
|
+
return {pid: DatasetTypeTitleHelper._labels_cache.get(pid, pid) for pid in plugin_id}
|
cmem_cmemc/utils.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING
|
|
|
12
12
|
from zipfile import BadZipFile, ZipFile
|
|
13
13
|
|
|
14
14
|
import requests
|
|
15
|
-
from click import Argument
|
|
15
|
+
from click import Argument
|
|
16
16
|
from cmem.cmempy.dp.proxy.graph import get_graphs_list
|
|
17
17
|
from cmem.cmempy.queries import QueryCatalog
|
|
18
18
|
from cmem.cmempy.workspace.projects.project import get_projects
|
|
@@ -20,6 +20,7 @@ from prometheus_client import Metric
|
|
|
20
20
|
|
|
21
21
|
from cmem_cmemc.config_parser import PureSectionConfigParser
|
|
22
22
|
from cmem_cmemc.constants import NAMESPACES
|
|
23
|
+
from cmem_cmemc.exceptions import CmemcError
|
|
23
24
|
from cmem_cmemc.smart_path import SmartPath
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -35,7 +36,7 @@ def check_python_version(ctx: type["ApplicationContext"]) -> None:
|
|
|
35
36
|
"""Check the runtime python version and warn or error."""
|
|
36
37
|
version = sys.version_info
|
|
37
38
|
major_expected = [3]
|
|
38
|
-
minor_expected = [
|
|
39
|
+
minor_expected = [13]
|
|
39
40
|
if version.major not in major_expected:
|
|
40
41
|
ctx.echo_error(f"Error: cmemc can not be executed with Python {version.major}.")
|
|
41
42
|
sys.exit(1)
|
|
@@ -246,7 +247,7 @@ def split_task_id(task_id: str) -> tuple[str, str]:
|
|
|
246
247
|
project_part = task_id.split(":")[0]
|
|
247
248
|
task_part = task_id.split(":")[1]
|
|
248
249
|
except IndexError as error:
|
|
249
|
-
raise
|
|
250
|
+
raise CmemcError(f"{task_id} is not a valid task ID.") from error
|
|
250
251
|
return project_part, task_part
|
|
251
252
|
|
|
252
253
|
|
|
@@ -303,13 +304,13 @@ def check_or_select_project(app: "ApplicationContext", project_id: str | None =
|
|
|
303
304
|
return project_name
|
|
304
305
|
|
|
305
306
|
if len(projects) == 0:
|
|
306
|
-
raise
|
|
307
|
+
raise CmemcError(
|
|
307
308
|
"There are no projects available. "
|
|
308
309
|
"Please create a project with 'cmemc project create'."
|
|
309
310
|
)
|
|
310
311
|
|
|
311
312
|
# more than one project
|
|
312
|
-
raise
|
|
313
|
+
raise CmemcError(
|
|
313
314
|
"There is more than one project available so you need to "
|
|
314
315
|
"specify the project with '--project'."
|
|
315
316
|
)
|
|
@@ -391,10 +392,10 @@ def get_query_text(file_or_uri: str, required_projections: set) -> str:
|
|
|
391
392
|
"""
|
|
392
393
|
sparql_query = QueryCatalog().get_query(file_or_uri)
|
|
393
394
|
if sparql_query is None:
|
|
394
|
-
raise
|
|
395
|
+
raise CmemcError(f"{file_or_uri} is neither a readable file nor a query URI.")
|
|
395
396
|
|
|
396
397
|
if sparql_query.get_placeholder_keys():
|
|
397
|
-
raise
|
|
398
|
+
raise CmemcError("Placeholder queries are not supported.")
|
|
398
399
|
|
|
399
400
|
result = sparql_query.get_json_results()
|
|
400
401
|
projected_vars = set(result["head"]["vars"])
|
|
@@ -402,7 +403,7 @@ def get_query_text(file_or_uri: str, required_projections: set) -> str:
|
|
|
402
403
|
missing_projections = required_projections - projected_vars
|
|
403
404
|
if missing_projections:
|
|
404
405
|
missing = ", ".join(missing_projections)
|
|
405
|
-
raise
|
|
406
|
+
raise CmemcError(f"Select query must include projections for: {missing}")
|
|
406
407
|
txt: str = sparql_query.text
|
|
407
408
|
return txt
|
|
408
409
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmem-cmemc
|
|
3
|
-
Version:
|
|
3
|
+
Version: 26.1.0rc1
|
|
4
4
|
Summary: Command line client for eccenca Corporate Memory
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Author: eccenca
|
|
8
8
|
Author-email: cmempy-developer@eccenca.com
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.13,<4
|
|
10
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Intended Audience :: Customer Service
|
|
@@ -18,12 +18,12 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
18
18
|
Classifier: Natural Language :: English
|
|
19
19
|
Classifier: Operating System :: OS Independent
|
|
20
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
26
23
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
27
|
Classifier: Topic :: Database
|
|
28
28
|
Classifier: Topic :: Software Development :: Testing
|
|
29
29
|
Classifier: Topic :: Utilities
|
|
@@ -32,8 +32,10 @@ Requires-Dist: certifi (>=2024.2.2)
|
|
|
32
32
|
Requires-Dist: click (>=8.3.0,<9.0.0)
|
|
33
33
|
Requires-Dist: click-didyoumean (>=0.3.1,<0.4.0)
|
|
34
34
|
Requires-Dist: click-help-colors (>=0.9.4,<0.10.0)
|
|
35
|
+
Requires-Dist: cmem-client (>=0.5.0,<0.6.0)
|
|
35
36
|
Requires-Dist: cmem-cmempy (==25.4.0)
|
|
36
37
|
Requires-Dist: configparser (>=7.2.0,<8.0.0)
|
|
38
|
+
Requires-Dist: humanize (>=4.14.0,<5.0.0)
|
|
37
39
|
Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
|
|
38
40
|
Requires-Dist: junit-xml (>=1.9,<2.0)
|
|
39
41
|
Requires-Dist: natsort (>=8.4.0,<9.0.0)
|
|
@@ -46,7 +48,6 @@ Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
|
46
48
|
Requires-Dist: rich (>=14.0.0,<15.0.0)
|
|
47
49
|
Requires-Dist: six (>=1.17.0,<2.0.0)
|
|
48
50
|
Requires-Dist: smart-open (>=7.1.0,<8.0.0)
|
|
49
|
-
Requires-Dist: timeago (>=1.0.16,<2.0.0)
|
|
50
51
|
Requires-Dist: treelib (>=1.7.1,<2.0.0)
|
|
51
52
|
Requires-Dist: urllib3 (>=2.3.0,<3.0.0)
|
|
52
53
|
Project-URL: Homepage, https://eccenca.com/go/cmemc
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
cmem_cmemc/__init__.py,sha256=-RPEVweA-fcmEAynszDDMKwArJgxZpGW61UBiV7O4Og,24
|
|
2
|
+
cmem_cmemc/_cmemc.zsh,sha256=fmkrBHIQxus8cp2AgO1tzZ5mNZdGL_83cYz3a9uAdsg,1326
|
|
3
|
+
cmem_cmemc/cli.py,sha256=VGfHqSIPPTvYpPHrjwbnLYLB6O0u8kwiJDXeXMlI1xU,4855
|
|
4
|
+
cmem_cmemc/command.py,sha256=CuaskaqD12soZLhDP1prgXOT4cRFu1CzuJm6LBp4zLM,1949
|
|
5
|
+
cmem_cmemc/command_group.py,sha256=0I2Jg1lCdoozcMg7d0g9sk0zVJ8_LiKvWWwqM6tZGI0,4793
|
|
6
|
+
cmem_cmemc/commands/__init__.py,sha256=NaGM5jOzf0S_-4UIAwlVDOf2AZ3mliGPoRLXQJfTyZs,22
|
|
7
|
+
cmem_cmemc/commands/acl.py,sha256=IHLEmrbz3r9SVEsaEWxpGVRjoT0oXWhYJ4d3y-_402Y,31421
|
|
8
|
+
cmem_cmemc/commands/admin.py,sha256=XyJVk9CuxLh60GdWNtMkGPpZ3BOftDF4T04M8gRaOQ0,10193
|
|
9
|
+
cmem_cmemc/commands/client.py,sha256=4Ke9Pl6-KQ-6_uSpRbVhB7spAi8tP0r39y99fwFRSWE,5304
|
|
10
|
+
cmem_cmemc/commands/config.py,sha256=tRB7YK6vzkaasmyZVWGLOw1_x0SM2x-coYMBRIJwEJs,8392
|
|
11
|
+
cmem_cmemc/commands/dataset.py,sha256=OCRBcDFVBBpap4bnjSIOfrWgFNRU9m0V_QlSobvObkY,30472
|
|
12
|
+
cmem_cmemc/commands/file.py,sha256=FBmi0D8bZs5NoKcOWEYIuaJM0x9kWQAaIQ2IyL8sFiE,17038
|
|
13
|
+
cmem_cmemc/commands/graph.py,sha256=HzddrKcoxq0w2xWMIhVft5p5Oacq0TSrmhebLngJdMs,36623
|
|
14
|
+
cmem_cmemc/commands/graph_imports.py,sha256=VcOisHSvNYJPxMiRb6sHEswYIQHwlIamLwQD1jgr_e0,14368
|
|
15
|
+
cmem_cmemc/commands/graph_insights.py,sha256=sx-Lvv4FWK9-eB8X4R5fDU63lrErAhc8W3a2fEQAjDI,14473
|
|
16
|
+
cmem_cmemc/commands/manual.py,sha256=-sZWeFL92Kj8gL3VYsbpKh2ZaVTyM3LgKaUcpNn9u3A,2179
|
|
17
|
+
cmem_cmemc/commands/metrics.py,sha256=t5I6VjBzjp_bQEoGkU9cdqNu_sa_WQwiIeJA3f9KNWc,12385
|
|
18
|
+
cmem_cmemc/commands/migration.py,sha256=FibmYpvZD2mrutjyRBhs7xDAZ-sjPiH9Kn3CI-zUPm0,9861
|
|
19
|
+
cmem_cmemc/commands/package.py,sha256=mJ9vDj34YdGySGK_Nh7Yl3O2tyuT-FhV8p4taRNLU_c,17762
|
|
20
|
+
cmem_cmemc/commands/project.py,sha256=ovn9zre7L0EzthMITQHGF9dffiP44UQUWNjG8Is6l8k,24938
|
|
21
|
+
cmem_cmemc/commands/python.py,sha256=lcbBAYZN5NB37HLSmVPs0SXJV7Ey4xVMYQiSiuyGkvc,12225
|
|
22
|
+
cmem_cmemc/commands/query.py,sha256=1cj1QbvwL98YbBGSCO0Zazbzscts_kiv0A7k75KwJXw,32231
|
|
23
|
+
cmem_cmemc/commands/scheduler.py,sha256=3wk3BF6Z3uRb0e5pphOYBusbXgs7C6Lz-D9wi7Nlohc,8855
|
|
24
|
+
cmem_cmemc/commands/store.py,sha256=zKz8FTtVSvFU6gMm6An7Jja9Bu9dZKbI1GW7UCq034s,10655
|
|
25
|
+
cmem_cmemc/commands/user.py,sha256=F0JSRkrj274Hi0i4nBIUkWFm-ItC40VwuqvLYETJBdM,15844
|
|
26
|
+
cmem_cmemc/commands/validation.py,sha256=v9_cXGzaexemuz6xBA358XY1_vP42SBfOD3PEZLcqbw,29731
|
|
27
|
+
cmem_cmemc/commands/variable.py,sha256=ZcvrQDb0CztWGkFIX-jnvqBTLp3qpQEZ7ZpBmKV16SI,18841
|
|
28
|
+
cmem_cmemc/commands/vocabulary.py,sha256=sv7hDZOeRPrPlc5RJfpAKzKH5JRyKjB93D-Jl4eLNqI,18359
|
|
29
|
+
cmem_cmemc/commands/workflow.py,sha256=UdKAsY3chxfrIpkjL1K9MqLVyu7jTdFYkOXfGVvJISI,26369
|
|
30
|
+
cmem_cmemc/commands/workspace.py,sha256=IcZgBsvtulLRFofS70qpln6oKQIZunrVLfSAUeiFhCA,4579
|
|
31
|
+
cmem_cmemc/completion.py,sha256=JbMZmTLjgu_nrIS9NuuFHqfqAFwHE1dCvFNk0g2c6d0,44805
|
|
32
|
+
cmem_cmemc/config_parser.py,sha256=NduwOT-BB_uAk3pz1Y-ex18RQJW-jjHzkQKCEUUK6Hc,1276
|
|
33
|
+
cmem_cmemc/constants.py,sha256=pzZYbSaTDUiWmE-VOAHB20oivHew5_FP9UTejySsVK4,550
|
|
34
|
+
cmem_cmemc/context.py,sha256=oCcd6dFl6BdYqKsueVqzQhSEwTNW7b1MjrE4CRznxt8,23220
|
|
35
|
+
cmem_cmemc/exceptions.py,sha256=c4Z6CKgymu0a7gD8MtHxzK_7WCsb9I2Zl-EgEkwu-YY,760
|
|
36
|
+
cmem_cmemc/manual_helper/__init__.py,sha256=G3Lqw2aPxo8x63Tg7L0aa5VD9BMaRzZDmhrog7IuEPg,43
|
|
37
|
+
cmem_cmemc/manual_helper/graph.py,sha256=dTkFXgU9fgySn54rE93t79v1MjWjQkprKRIfJhc7Jps,3655
|
|
38
|
+
cmem_cmemc/manual_helper/multi_page.py,sha256=I1gTCDETlCli2k-G7Mkdpw_MCqey60HxFl35wTmvYFU,12279
|
|
39
|
+
cmem_cmemc/manual_helper/single_page.py,sha256=0mMn_IJwFCe-WPKAmxGEStb8IINLpQRxAx_F1pIxg1E,1526
|
|
40
|
+
cmem_cmemc/migrations/__init__.py,sha256=i6Ri7qN58ou_MwOzm2KibPkXOD7u-1ELky-nUE5LjAA,24
|
|
41
|
+
cmem_cmemc/migrations/abc.py,sha256=UGJzrvMzUFdp2-sosp49ObRI-SrUSzLJqLEhvB4QTzg,3564
|
|
42
|
+
cmem_cmemc/migrations/access_conditions_243.py,sha256=IXcvSuo9pLaTTo4XNBB6_ln-2TzOV5PU5ugti0BWbxA,5083
|
|
43
|
+
cmem_cmemc/migrations/bootstrap_data.py,sha256=RF0vyFTGUQ_RcpTTWZmm3XLAJAJX2gSYcGwcBmRmU8A,963
|
|
44
|
+
cmem_cmemc/migrations/remove_noop_triple_251.py,sha256=392FZV5ipUMeqqc2QJWfupFeNRR4ceKPmyak17I5xVk,1451
|
|
45
|
+
cmem_cmemc/migrations/shapes_widget_integrations_243.py,sha256=8lQTOlEJvlrDvdvKkl5OAjnyx1jMRgQnU6Bk_fEORYQ,8543
|
|
46
|
+
cmem_cmemc/migrations/sparql_query_texts_242.py,sha256=K_GbxaX5-kkQKDZMq8UvT1vHazde53htwdDHGyB0b9s,1568
|
|
47
|
+
cmem_cmemc/migrations/workspace_configurations.py,sha256=tFmCdfEL10ICjqMXQEIf-9fveE41HBQ_jaWNQJENz50,998
|
|
48
|
+
cmem_cmemc/object_list.py,sha256=RII_nhRIRThzVdrtABELXW-Li-O6x970C69Vy2T3ruE,23364
|
|
49
|
+
cmem_cmemc/parameter_types/__init__.py,sha256=Jqhwnw5a2oPNMClzUyovWiieK60RCl3rvSNr-t3wP84,36
|
|
50
|
+
cmem_cmemc/parameter_types/path.py,sha256=M56PGdjploN2pEYaNAk6_qomAX54crLW8E9XZsFvRuI,2270
|
|
51
|
+
cmem_cmemc/placeholder.py,sha256=Rf20OqwDjISnVPJsYlvuSgzeUbfJ2sklE2PWnZ5TSYg,2409
|
|
52
|
+
cmem_cmemc/smart_path/__init__.py,sha256=zDgm1kDrzLyCuIcNb8VXSdnb_CcVNjGkjgiIDVlsh74,3023
|
|
53
|
+
cmem_cmemc/smart_path/clients/__init__.py,sha256=YFOm69BfTCRvAcJjN_CoUmCv3kzEciyYOPUG337p_pA,1696
|
|
54
|
+
cmem_cmemc/smart_path/clients/http.py,sha256=3clZu2v4uuOvPY4MY_8SVSy7hIXJDNooahFRBRpy0ok,2347
|
|
55
|
+
cmem_cmemc/string_processor.py,sha256=19YSLUF9PIbfTmsTm2bZslsNhFUAYx0MerWYwC3BVEo,8616
|
|
56
|
+
cmem_cmemc/title_helper.py,sha256=8Cyes2U4lHTQbzYwBSYqCrZbq29_oBg6uibe7xZ6DEg,3486
|
|
57
|
+
cmem_cmemc/utils.py,sha256=jmXjbEQ2MDIz031A81kivGnir7HG5XUkLbOd1OCoV6s,14662
|
|
58
|
+
cmem_cmemc-26.1.0rc1.dist-info/METADATA,sha256=-8H1iGcueln3Y31sViwBpY-g2hBleljk2Vm-YSsRL2Y,5761
|
|
59
|
+
cmem_cmemc-26.1.0rc1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
60
|
+
cmem_cmemc-26.1.0rc1.dist-info/entry_points.txt,sha256=2G0AWAyz501EHpFTjIxccdlCTsHt80NT0pdUGP1QkPA,45
|
|
61
|
+
cmem_cmemc-26.1.0rc1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
62
|
+
cmem_cmemc-26.1.0rc1.dist-info/RECORD,,
|
cmem_cmemc/commands/resource.py
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
"""Build dataset resource commands for cmemc."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
from click import ClickException, UsageError
|
|
7
|
-
from cmem.cmempy.workspace.projects.resources import get_all_resources
|
|
8
|
-
from cmem.cmempy.workspace.projects.resources.resource import (
|
|
9
|
-
delete_resource,
|
|
10
|
-
get_resource_metadata,
|
|
11
|
-
get_resource_usage_data,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
from cmem_cmemc import completion
|
|
15
|
-
from cmem_cmemc.command import CmemcCommand
|
|
16
|
-
from cmem_cmemc.command_group import CmemcGroup
|
|
17
|
-
from cmem_cmemc.context import ApplicationContext
|
|
18
|
-
from cmem_cmemc.utils import split_task_id, struct_to_table
|
|
19
|
-
|
|
20
|
-
RESOURCE_FILTER_TYPES = ["project", "regex"]
|
|
21
|
-
RESOURCE_FILTER_TYPES_HIDDEN = ["ids"]
|
|
22
|
-
RESOURCE_FILTER_TEXT = (
|
|
23
|
-
"Filter file resources based on metadata. "
|
|
24
|
-
f"First parameter CHOICE can be one of {RESOURCE_FILTER_TYPES!s}"
|
|
25
|
-
". The second parameter is based on CHOICE, e.g. a project "
|
|
26
|
-
"ID or a regular expression string."
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _get_resources_filtered(
|
|
31
|
-
resources: list[dict], filter_name: str, filter_value: str | tuple[str, ...]
|
|
32
|
-
) -> list[dict]:
|
|
33
|
-
"""Get file resources but filtered according to name and value."""
|
|
34
|
-
# check for correct filter names (filter ids is used internally only)
|
|
35
|
-
if filter_name not in RESOURCE_FILTER_TYPES + RESOURCE_FILTER_TYPES_HIDDEN:
|
|
36
|
-
raise UsageError(
|
|
37
|
-
f"{filter_name} is an unknown filter name. " f"Use one of {RESOURCE_FILTER_TYPES}."
|
|
38
|
-
)
|
|
39
|
-
# filter by ID list
|
|
40
|
-
if filter_name == "ids":
|
|
41
|
-
return [_ for _ in resources if _["id"] in filter_value]
|
|
42
|
-
# filter by project
|
|
43
|
-
if filter_name == "project":
|
|
44
|
-
return [_ for _ in resources if _["project"] == str(filter_value)]
|
|
45
|
-
# filter by regex
|
|
46
|
-
if filter_name == "regex":
|
|
47
|
-
return [_ for _ in resources if re.search(str(filter_value), _["name"])]
|
|
48
|
-
# return unfiltered list
|
|
49
|
-
return resources
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@click.command(cls=CmemcCommand, name="list")
|
|
53
|
-
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
54
|
-
@click.option(
|
|
55
|
-
"--id-only",
|
|
56
|
-
is_flag=True,
|
|
57
|
-
help="Lists only resource names and no other metadata. "
|
|
58
|
-
"This is useful for piping the IDs into other commands.",
|
|
59
|
-
)
|
|
60
|
-
@click.option(
|
|
61
|
-
"--filter",
|
|
62
|
-
"filters_",
|
|
63
|
-
multiple=True,
|
|
64
|
-
type=(str, str),
|
|
65
|
-
shell_complete=completion.resource_list_filter,
|
|
66
|
-
help=RESOURCE_FILTER_TEXT,
|
|
67
|
-
)
|
|
68
|
-
@click.pass_obj
|
|
69
|
-
def list_command(
|
|
70
|
-
app: ApplicationContext, raw: bool, id_only: bool, filters_: tuple[tuple[str, str], ...]
|
|
71
|
-
) -> None:
|
|
72
|
-
"""List available file resources.
|
|
73
|
-
|
|
74
|
-
Outputs a table or a list of dataset resources (files).
|
|
75
|
-
"""
|
|
76
|
-
resources = get_all_resources()
|
|
77
|
-
for _ in filters_:
|
|
78
|
-
filter_name, filter_value = _
|
|
79
|
-
resources = _get_resources_filtered(resources, filter_name, filter_value)
|
|
80
|
-
if raw:
|
|
81
|
-
app.echo_info_json(resources)
|
|
82
|
-
return
|
|
83
|
-
if id_only:
|
|
84
|
-
for _ in sorted(_["id"] for _ in resources):
|
|
85
|
-
app.echo_result(_)
|
|
86
|
-
return
|
|
87
|
-
# output a user table
|
|
88
|
-
table = []
|
|
89
|
-
headers = ["ID", "Modified", "Size"]
|
|
90
|
-
for _ in resources:
|
|
91
|
-
row = [
|
|
92
|
-
_["id"],
|
|
93
|
-
_["modified"],
|
|
94
|
-
_["size"],
|
|
95
|
-
]
|
|
96
|
-
table.append(row)
|
|
97
|
-
app.echo_info_table(
|
|
98
|
-
table,
|
|
99
|
-
headers=headers,
|
|
100
|
-
sort_column=0,
|
|
101
|
-
empty_table_message="No dataset resources found. "
|
|
102
|
-
"Use the `dataset create` command to create a new file based dataset.",
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@click.command(cls=CmemcCommand, name="delete")
|
|
107
|
-
@click.argument("resource_ids", nargs=-1, type=click.STRING, shell_complete=completion.resource_ids)
|
|
108
|
-
@click.option("--force", is_flag=True, help="Delete resource even if in use by a task.")
|
|
109
|
-
@click.option(
|
|
110
|
-
"-a",
|
|
111
|
-
"--all",
|
|
112
|
-
"all_",
|
|
113
|
-
is_flag=True,
|
|
114
|
-
help="Delete all resources. " "This is a dangerous option, so use it with care.",
|
|
115
|
-
)
|
|
116
|
-
@click.option(
|
|
117
|
-
"--filter",
|
|
118
|
-
"filters_",
|
|
119
|
-
multiple=True,
|
|
120
|
-
type=(str, str),
|
|
121
|
-
shell_complete=completion.resource_list_filter,
|
|
122
|
-
help=RESOURCE_FILTER_TEXT,
|
|
123
|
-
)
|
|
124
|
-
@click.pass_obj
|
|
125
|
-
def delete_command(
|
|
126
|
-
app: ApplicationContext,
|
|
127
|
-
resource_ids: tuple[str, ...],
|
|
128
|
-
force: bool,
|
|
129
|
-
all_: bool,
|
|
130
|
-
filters_: tuple[tuple[str, str], ...],
|
|
131
|
-
) -> None:
|
|
132
|
-
"""Delete file resources.
|
|
133
|
-
|
|
134
|
-
There are three selection mechanisms: with specific IDs, only those
|
|
135
|
-
specified resources will be deleted; by using --filter, resources based
|
|
136
|
-
on the filter type and value will be deleted; using --all will delete
|
|
137
|
-
all resources.
|
|
138
|
-
"""
|
|
139
|
-
if resource_ids == () and not all_ and filters_ == ():
|
|
140
|
-
raise UsageError(
|
|
141
|
-
"Either specify at least one resource ID or use the --all or "
|
|
142
|
-
"--filter options to specify resources for deletion."
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
resources = get_all_resources()
|
|
146
|
-
if len(resource_ids) > 0:
|
|
147
|
-
for resource_id in resource_ids:
|
|
148
|
-
if resource_id not in [_["id"] for _ in resources]:
|
|
149
|
-
raise ClickException(f"Resource {resource_id} not available.")
|
|
150
|
-
# "filter" by id
|
|
151
|
-
resources = _get_resources_filtered(resources, "ids", resource_ids)
|
|
152
|
-
for _ in filters_:
|
|
153
|
-
resources = _get_resources_filtered(resources, _[0], _[1])
|
|
154
|
-
|
|
155
|
-
# avoid double removal as well as sort IDs
|
|
156
|
-
processed_ids = sorted({_["id"] for _ in resources}, key=lambda v: v.lower())
|
|
157
|
-
count = len(processed_ids)
|
|
158
|
-
for current, resource_id in enumerate(processed_ids, start=1):
|
|
159
|
-
current_string = str(current).zfill(len(str(count)))
|
|
160
|
-
app.echo_info(f"Delete resource {current_string}/{count}: {resource_id} ... ", nl=False)
|
|
161
|
-
project_id, resource_local_id = split_task_id(resource_id)
|
|
162
|
-
usage = get_resource_usage_data(project_id, resource_local_id)
|
|
163
|
-
if len(usage) > 0:
|
|
164
|
-
app.echo_error(f"in use by {len(usage)} task(s)", nl=False)
|
|
165
|
-
if force:
|
|
166
|
-
app.echo_info(" ... ", nl=False)
|
|
167
|
-
else:
|
|
168
|
-
app.echo_info("")
|
|
169
|
-
continue
|
|
170
|
-
delete_resource(project_name=project_id, resource_name=resource_local_id)
|
|
171
|
-
app.echo_success("deleted")
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
@click.command(cls=CmemcCommand, name="inspect")
|
|
175
|
-
@click.argument("resource_id", type=click.STRING, shell_complete=completion.resource_ids)
|
|
176
|
-
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
177
|
-
@click.pass_obj
|
|
178
|
-
def inspect_command(app: ApplicationContext, resource_id: str, raw: bool) -> None:
|
|
179
|
-
"""Display all metadata of a file resource."""
|
|
180
|
-
project_id, resource_id = split_task_id(resource_id)
|
|
181
|
-
resource_data = get_resource_metadata(project_id, resource_id)
|
|
182
|
-
if raw:
|
|
183
|
-
app.echo_info_json(resource_data)
|
|
184
|
-
else:
|
|
185
|
-
table = struct_to_table(resource_data)
|
|
186
|
-
app.echo_info_table(table, headers=["Key", "Value"], sort_column=0)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
@click.command(cls=CmemcCommand, name="usage")
|
|
190
|
-
@click.argument("resource_id", type=click.STRING, shell_complete=completion.resource_ids)
|
|
191
|
-
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
192
|
-
@click.pass_obj
|
|
193
|
-
def usage_command(app: ApplicationContext, resource_id: str, raw: bool) -> None:
|
|
194
|
-
"""Display all usage data of a file resource."""
|
|
195
|
-
project_id, resource_id = split_task_id(resource_id)
|
|
196
|
-
usage = get_resource_usage_data(project_id, resource_id)
|
|
197
|
-
if raw:
|
|
198
|
-
app.echo_info_json(usage)
|
|
199
|
-
return
|
|
200
|
-
# output a user table
|
|
201
|
-
table = []
|
|
202
|
-
headers = ["Task ID", "Type", "Label"]
|
|
203
|
-
for _ in usage:
|
|
204
|
-
row = [project_id + ":" + _["id"], _["taskType"], _["label"]]
|
|
205
|
-
table.append(row)
|
|
206
|
-
app.echo_info_table(table, headers=headers, sort_column=2)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@click.group(cls=CmemcGroup)
|
|
210
|
-
def resource() -> CmemcGroup: # type: ignore[empty-body]
|
|
211
|
-
"""List, inspect or delete dataset file resources.
|
|
212
|
-
|
|
213
|
-
File resources are identified by their paths and project IDs.
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
resource.add_command(list_command)
|
|
218
|
-
resource.add_command(delete_command)
|
|
219
|
-
resource.add_command(inspect_command)
|
|
220
|
-
resource.add_command(usage_command)
|