clarifai 11.2.2__py3-none-any.whl → 11.2.3rc1__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.
Files changed (103) hide show
  1. clarifai/__init__.py +1 -1
  2. clarifai/__pycache__/__init__.cpython-312.pyc +0 -0
  3. clarifai/__pycache__/errors.cpython-312.pyc +0 -0
  4. clarifai/__pycache__/versions.cpython-312.pyc +0 -0
  5. clarifai/cli/__pycache__/__init__.cpython-312.pyc +0 -0
  6. clarifai/cli/__pycache__/base.cpython-312.pyc +0 -0
  7. clarifai/cli/__pycache__/compute_cluster.cpython-312.pyc +0 -0
  8. clarifai/cli/__pycache__/deployment.cpython-312.pyc +0 -0
  9. clarifai/cli/__pycache__/model.cpython-312.pyc +0 -0
  10. clarifai/cli/__pycache__/nodepool.cpython-312.pyc +0 -0
  11. clarifai/cli/base.py +225 -89
  12. clarifai/cli/compute_cluster.py +21 -20
  13. clarifai/cli/deployment.py +63 -41
  14. clarifai/cli/model.py +1 -1
  15. clarifai/cli/nodepool.py +56 -40
  16. clarifai/client/__pycache__/__init__.cpython-312.pyc +0 -0
  17. clarifai/client/__pycache__/app.cpython-312.pyc +0 -0
  18. clarifai/client/__pycache__/base.cpython-312.pyc +0 -0
  19. clarifai/client/__pycache__/compute_cluster.cpython-312.pyc +0 -0
  20. clarifai/client/__pycache__/dataset.cpython-312.pyc +0 -0
  21. clarifai/client/__pycache__/deployment.cpython-312.pyc +0 -0
  22. clarifai/client/__pycache__/input.cpython-312.pyc +0 -0
  23. clarifai/client/__pycache__/lister.cpython-312.pyc +0 -0
  24. clarifai/client/__pycache__/model.cpython-312.pyc +0 -0
  25. clarifai/client/__pycache__/model_client.cpython-312.pyc +0 -0
  26. clarifai/client/__pycache__/module.cpython-312.pyc +0 -0
  27. clarifai/client/__pycache__/nodepool.cpython-312.pyc +0 -0
  28. clarifai/client/__pycache__/search.cpython-312.pyc +0 -0
  29. clarifai/client/__pycache__/user.cpython-312.pyc +0 -0
  30. clarifai/client/__pycache__/workflow.cpython-312.pyc +0 -0
  31. clarifai/client/auth/__pycache__/__init__.cpython-312.pyc +0 -0
  32. clarifai/client/auth/__pycache__/helper.cpython-312.pyc +0 -0
  33. clarifai/client/auth/__pycache__/register.cpython-312.pyc +0 -0
  34. clarifai/client/auth/__pycache__/stub.cpython-312.pyc +0 -0
  35. clarifai/constants/__pycache__/base.cpython-312.pyc +0 -0
  36. clarifai/constants/__pycache__/dataset.cpython-312.pyc +0 -0
  37. clarifai/constants/__pycache__/input.cpython-312.pyc +0 -0
  38. clarifai/constants/__pycache__/model.cpython-312.pyc +0 -0
  39. clarifai/constants/__pycache__/search.cpython-312.pyc +0 -0
  40. clarifai/constants/__pycache__/workflow.cpython-312.pyc +0 -0
  41. clarifai/datasets/__pycache__/__init__.cpython-312.pyc +0 -0
  42. clarifai/datasets/export/__pycache__/__init__.cpython-312.pyc +0 -0
  43. clarifai/datasets/export/__pycache__/inputs_annotations.cpython-312.pyc +0 -0
  44. clarifai/datasets/upload/__pycache__/__init__.cpython-312.pyc +0 -0
  45. clarifai/datasets/upload/__pycache__/base.cpython-312.pyc +0 -0
  46. clarifai/datasets/upload/__pycache__/features.cpython-312.pyc +0 -0
  47. clarifai/datasets/upload/__pycache__/image.cpython-312.pyc +0 -0
  48. clarifai/datasets/upload/__pycache__/multimodal.cpython-312.pyc +0 -0
  49. clarifai/datasets/upload/__pycache__/text.cpython-312.pyc +0 -0
  50. clarifai/datasets/upload/__pycache__/utils.cpython-312.pyc +0 -0
  51. clarifai/datasets/upload/loaders/__pycache__/__init__.cpython-312.pyc +0 -0
  52. clarifai/datasets/upload/loaders/__pycache__/coco_detection.cpython-312.pyc +0 -0
  53. clarifai/modules/__pycache__/__init__.cpython-312.pyc +0 -0
  54. clarifai/modules/__pycache__/css.cpython-312.pyc +0 -0
  55. clarifai/runners/__pycache__/__init__.cpython-312.pyc +0 -0
  56. clarifai/runners/__pycache__/server.cpython-312.pyc +0 -0
  57. clarifai/runners/models/__pycache__/__init__.cpython-312.pyc +0 -0
  58. clarifai/runners/models/__pycache__/base_typed_model.cpython-312.pyc +0 -0
  59. clarifai/runners/models/__pycache__/model_builder.cpython-312.pyc +0 -0
  60. clarifai/runners/models/__pycache__/model_class.cpython-312.pyc +0 -0
  61. clarifai/runners/models/__pycache__/model_run_locally.cpython-312.pyc +0 -0
  62. clarifai/runners/models/__pycache__/model_runner.cpython-312.pyc +0 -0
  63. clarifai/runners/models/__pycache__/model_servicer.cpython-312.pyc +0 -0
  64. clarifai/runners/models/model_builder.py +17 -7
  65. clarifai/runners/models/model_run_locally.py +1 -0
  66. clarifai/runners/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  67. clarifai/runners/utils/__pycache__/const.cpython-312.pyc +0 -0
  68. clarifai/runners/utils/__pycache__/data_handler.cpython-312.pyc +0 -0
  69. clarifai/runners/utils/__pycache__/data_types.cpython-312.pyc +0 -0
  70. clarifai/runners/utils/__pycache__/data_utils.cpython-312.pyc +0 -0
  71. clarifai/runners/utils/__pycache__/loader.cpython-312.pyc +0 -0
  72. clarifai/runners/utils/__pycache__/method_signatures.cpython-312.pyc +0 -0
  73. clarifai/runners/utils/__pycache__/serializers.cpython-312.pyc +0 -0
  74. clarifai/runners/utils/__pycache__/url_fetcher.cpython-312.pyc +0 -0
  75. clarifai/runners/utils/const.py +9 -8
  76. clarifai/schema/__pycache__/search.cpython-312.pyc +0 -0
  77. clarifai/urls/__pycache__/helper.cpython-312.pyc +0 -0
  78. clarifai/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  79. clarifai/utils/__pycache__/cli.cpython-312.pyc +0 -0
  80. clarifai/utils/__pycache__/config.cpython-312.pyc +0 -0
  81. clarifai/utils/__pycache__/constants.cpython-312.pyc +0 -0
  82. clarifai/utils/__pycache__/logging.cpython-312.pyc +0 -0
  83. clarifai/utils/__pycache__/misc.cpython-312.pyc +0 -0
  84. clarifai/utils/__pycache__/model_train.cpython-312.pyc +0 -0
  85. clarifai/utils/cli.py +124 -34
  86. clarifai/utils/config.py +105 -0
  87. clarifai/utils/config.py~ +145 -0
  88. clarifai/utils/constants.py +4 -0
  89. clarifai/utils/evaluation/__pycache__/__init__.cpython-312.pyc +0 -0
  90. clarifai/utils/evaluation/__pycache__/helpers.cpython-312.pyc +0 -0
  91. clarifai/utils/evaluation/__pycache__/main.cpython-312.pyc +0 -0
  92. clarifai/utils/misc.py +2 -0
  93. clarifai/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  94. clarifai/workflows/__pycache__/export.cpython-312.pyc +0 -0
  95. clarifai/workflows/__pycache__/utils.cpython-312.pyc +0 -0
  96. clarifai/workflows/__pycache__/validate.cpython-312.pyc +0 -0
  97. {clarifai-11.2.2.dist-info → clarifai-11.2.3rc1.dist-info}/METADATA +3 -15
  98. clarifai-11.2.3rc1.dist-info/RECORD +185 -0
  99. {clarifai-11.2.2.dist-info → clarifai-11.2.3rc1.dist-info}/WHEEL +1 -1
  100. clarifai-11.2.2.dist-info/RECORD +0 -101
  101. {clarifai-11.2.2.dist-info/licenses → clarifai-11.2.3rc1.dist-info}/LICENSE +0 -0
  102. {clarifai-11.2.2.dist-info → clarifai-11.2.3rc1.dist-info}/entry_points.txt +0 -0
  103. {clarifai-11.2.2.dist-info → clarifai-11.2.3rc1.dist-info}/top_level.txt +0 -0
clarifai/cli/nodepool.py CHANGED
@@ -1,30 +1,27 @@
1
1
  import click
2
+
2
3
  from clarifai.cli.base import cli
3
4
  from clarifai.client.compute_cluster import ComputeCluster
4
- from clarifai.utils.cli import display_co_resources, dump_yaml, from_yaml, validate_context
5
+ from clarifai.client.user import User
6
+ from clarifai.utils.cli import (AliasedGroup, display_co_resources, dump_yaml, from_yaml,
7
+ validate_context)
5
8
 
6
9
 
7
- @cli.group(['nodepool', 'np'])
10
+ @cli.group(['nodepool', 'np'], cls=AliasedGroup)
8
11
  def nodepool():
9
12
  """Manage Nodepools: create, delete, list"""
10
- pass
11
13
 
12
14
 
13
- @nodepool.command()
14
- @click.option(
15
- '-cc_id',
16
- '--compute_cluster_id',
17
- required=False,
18
- help='Compute Cluster ID for the compute cluster to interact with.')
15
+ @nodepool.command(['c'])
16
+ @click.argument('compute_cluster_id')
17
+ @click.argument('nodepool_id')
19
18
  @click.option(
20
19
  '--config',
21
20
  type=click.Path(exists=True),
22
21
  required=True,
23
22
  help='Path to the nodepool config file.')
24
- @click.option(
25
- '-np_id', '--nodepool_id', required=False, help='New Nodepool ID for the nodepool to create.')
26
23
  @click.pass_context
27
- def create(ctx, compute_cluster_id, config, nodepool_id):
24
+ def create(ctx, compute_cluster_id, nodepool_id, config):
28
25
  """Create a new Nodepool with the given config file."""
29
26
 
30
27
  validate_context(ctx)
@@ -43,44 +40,63 @@ def create(ctx, compute_cluster_id, config, nodepool_id):
43
40
 
44
41
  compute_cluster = ComputeCluster(
45
42
  compute_cluster_id=compute_cluster_id,
46
- user_id=ctx.obj['user_id'],
47
- pat=ctx.obj['pat'],
48
- base_url=ctx.obj['base_url'])
43
+ user_id=ctx.obj.current.user_id,
44
+ pat=ctx.obj.current.pat,
45
+ base_url=ctx.obj.current.api_base)
49
46
  if nodepool_id:
50
47
  compute_cluster.create_nodepool(config, nodepool_id=nodepool_id)
51
48
  else:
52
49
  compute_cluster.create_nodepool(config)
53
50
 
54
51
 
55
- @nodepool.command()
56
- @click.option(
57
- '-cc_id',
58
- '--compute_cluster_id',
59
- required=True,
60
- help='Compute Cluster ID for the compute cluster to interact with.')
52
+ @nodepool.command(['ls'])
53
+ @click.argument('compute_cluster_id', default="")
61
54
  @click.option('--page_no', required=False, help='Page number to list.', default=1)
62
- @click.option('--per_page', required=False, help='Number of items per page.', default=16)
55
+ @click.option('--per_page', required=False, help='Number of items per page.', default=128)
63
56
  @click.pass_context
64
57
  def list(ctx, compute_cluster_id, page_no, per_page):
65
- """List all nodepools for the user."""
58
+ """List all nodepools for the user across all compute clusters. If compute_cluster_id is provided
59
+ it will list only within that compute cluster. """
66
60
 
67
61
  validate_context(ctx)
68
- compute_cluster = ComputeCluster(
69
- compute_cluster_id=compute_cluster_id,
70
- user_id=ctx.obj['user_id'],
71
- pat=ctx.obj['pat'],
72
- base_url=ctx.obj['base_url'])
73
- response = compute_cluster.list_nodepools(page_no, per_page)
74
- display_co_resources(response, "Nodepool")
75
62
 
63
+ cc_id = compute_cluster_id
76
64
 
77
- @nodepool.command()
78
- @click.option(
79
- '-cc_id',
80
- '--compute_cluster_id',
81
- required=True,
82
- help='Compute Cluster ID for the compute cluster to interact with.')
83
- @click.option('-np_id', '--nodepool_id', help='Nodepool ID of the user to delete.')
65
+ if cc_id:
66
+ compute_cluster = ComputeCluster(
67
+ compute_cluster_id=cc_id,
68
+ user_id=ctx.obj.current.user_id,
69
+ pat=ctx.obj.current.pat,
70
+ base_url=ctx.obj.current.api_base)
71
+ response = compute_cluster.list_nodepools(page_no, per_page)
72
+ else:
73
+ user = User(
74
+ user_id=ctx.obj.current.user_id,
75
+ pat=ctx.obj.current.pat,
76
+ base_url=ctx.obj.current.api_base)
77
+ ccs = user.list_compute_clusters(page_no, per_page)
78
+ response = []
79
+ for cc in ccs:
80
+ compute_cluster = ComputeCluster(
81
+ compute_cluster_id=cc.id,
82
+ user_id=ctx.obj.current.user_id,
83
+ pat=ctx.obj.current.pat,
84
+ base_url=ctx.obj.current.api_base)
85
+ response.extend([i for i in compute_cluster.list_nodepools(page_no, per_page)])
86
+
87
+ display_co_resources(
88
+ response,
89
+ custom_columns={
90
+ 'ID': lambda c: c.id,
91
+ 'USER_ID': lambda c: c.compute_cluster.user_id,
92
+ 'COMPUTE_CLUSTER_ID': lambda c: c.compute_cluster.id,
93
+ 'DESCRIPTION': lambda c: c.description,
94
+ })
95
+
96
+
97
+ @nodepool.command(['rm'])
98
+ @click.argument('compute_cluster_id')
99
+ @click.argument('nodepool_id')
84
100
  @click.pass_context
85
101
  def delete(ctx, compute_cluster_id, nodepool_id):
86
102
  """Deletes a nodepool for the user."""
@@ -88,7 +104,7 @@ def delete(ctx, compute_cluster_id, nodepool_id):
88
104
  validate_context(ctx)
89
105
  compute_cluster = ComputeCluster(
90
106
  compute_cluster_id=compute_cluster_id,
91
- user_id=ctx.obj['user_id'],
92
- pat=ctx.obj['pat'],
93
- base_url=ctx.obj['base_url'])
107
+ user_id=ctx.obj.current.user_id,
108
+ pat=ctx.obj.current.pat,
109
+ base_url=ctx.obj.current.api_base)
94
110
  compute_cluster.delete_nodepools([nodepool_id])
@@ -142,17 +142,21 @@ class ModelBuilder:
142
142
  def _validate_config_checkpoints(self):
143
143
  """
144
144
  Validates the checkpoints section in the config file.
145
+ return loader_type, repo_id, hf_token, when, allowed_file_patterns, ignore_file_patterns
145
146
  :return: loader_type the type of loader or None if no checkpoints.
146
147
  :return: repo_id location of checkpoint.
147
148
  :return: hf_token token to access checkpoint.
149
+ :return: when one of ['upload', 'build', 'runtime'] to download checkpoint
150
+ :return: allowed_file_patterns patterns to allow in downloaded checkpoint
151
+ :return: ignore_file_patterns patterns to ignore in downloaded checkpoint
148
152
  """
149
153
  if "checkpoints" not in self.config:
150
- return None, None, None, DEFAULT_DOWNLOAD_CHECKPOINT_WHEN
154
+ return None, None, None, DEFAULT_DOWNLOAD_CHECKPOINT_WHEN, None, None
151
155
  assert "type" in self.config.get("checkpoints"), "No loader type specified in the config file"
152
156
  loader_type = self.config.get("checkpoints").get("type")
153
157
  if not loader_type:
154
158
  logger.info("No loader type specified in the config file for checkpoints")
155
- return None, None, None
159
+ return None, None, None, DEFAULT_DOWNLOAD_CHECKPOINT_WHEN, None, None
156
160
  checkpoints = self.config.get("checkpoints")
157
161
  if 'when' not in checkpoints:
158
162
  logger.warn(
@@ -184,9 +188,17 @@ class ModelBuilder:
184
188
  resp = self.client.STUB.GetApp(service_pb2.GetAppRequest(user_app_id=self.client.user_app_id))
185
189
  if resp.status.code == status_code_pb2.SUCCESS:
186
190
  return True
191
+ if resp.status.code == status_code_pb2.CONN_KEY_INVALID:
192
+ logger.error(
193
+ f"Invalid PAT provided for user {self.client.user_app_id.user_id}. Please check your PAT and try again."
194
+ )
195
+ return False
187
196
  logger.error(
188
197
  f"Error checking API {self._base_api} for user app {self.client.user_app_id.user_id}/{self.client.user_app_id.app_id}. Error code: {resp.status.code}"
189
198
  )
199
+ logger.error(
200
+ f"App {self.client.user_app_id.app_id} not found for user {self.client.user_app_id.user_id}. Please create the app first and try again."
201
+ )
190
202
  return False
191
203
 
192
204
  def _validate_config_model(self):
@@ -207,9 +219,6 @@ class ModelBuilder:
207
219
  assert model.get('id') != "", "model_id cannot be empty in the config file"
208
220
 
209
221
  if not self._check_app_exists():
210
- logger.error(
211
- f"App {self.client.user_app_id.app_id} not found for user {self.client.user_app_id.user_id}"
212
- )
213
222
  sys.exit(1)
214
223
 
215
224
  def _validate_config(self):
@@ -406,11 +415,12 @@ class ModelBuilder:
406
415
  # Sort in reverse so that newer cuda versions come first and are preferred.
407
416
  for image in sorted(AVAILABLE_TORCH_IMAGES, reverse=True):
408
417
  if torch_version in image and f'py{python_version}' in image:
409
- cuda_version = image.split('-')[-1].replace('cuda', '')
418
+ # like cu124, rocm6.3, etc.
419
+ gpu_version = image.split('-')[-1]
410
420
  final_image = TORCH_BASE_IMAGE.format(
411
421
  torch_version=torch_version,
412
422
  python_version=python_version,
413
- cuda_version=cuda_version,
423
+ gpu_version=gpu_version,
414
424
  )
415
425
  logger.info(f"Using Torch version {torch_version} base image to build the Docker image")
416
426
  break
@@ -108,6 +108,7 @@ class ModelRunLocally:
108
108
 
109
109
  def _build_stream_request(self):
110
110
  request = self._build_request()
111
+ ensure_urls_downloaded(request)
111
112
  for i in range(1):
112
113
  yield request
113
114
 
@@ -2,10 +2,10 @@ import os
2
2
 
3
3
  registry = os.environ.get('CLARIFAI_BASE_IMAGE_REGISTRY', 'public.ecr.aws/clarifai-models')
4
4
 
5
- GIT_SHA = "df565436eea93efb3e8d1eb558a0a46df29523ec"
5
+ GIT_SHA = "b8ae56bf3b7c95e686ca002b07ca83d259c716eb"
6
6
 
7
7
  PYTHON_BASE_IMAGE = registry + '/python-base:{python_version}-' + GIT_SHA
8
- TORCH_BASE_IMAGE = registry + '/torch:{torch_version}-py{python_version}-cuda{cuda_version}-' + GIT_SHA
8
+ TORCH_BASE_IMAGE = registry + '/torch:{torch_version}-py{python_version}-{gpu_version}-' + GIT_SHA
9
9
 
10
10
  # List of available python base images
11
11
  AVAILABLE_PYTHON_IMAGES = ['3.11', '3.12']
@@ -21,12 +21,13 @@ DEFAULT_RUNTIME_DOWNLOAD_PATH = os.path.join(os.sep, "tmp", ".cache")
21
21
  # List of available torch images
22
22
  # Keep sorted by most recent cuda version.
23
23
  AVAILABLE_TORCH_IMAGES = [
24
- '2.4.1-py3.11-cuda124',
25
- '2.5.1-py3.11-cuda124',
26
- '2.4.1-py3.12-cuda124',
27
- '2.5.1-py3.12-cuda124',
28
- # '2.4.1-py3.13-cuda124',
29
- # '2.5.1-py3.13-cuda124',
24
+ '2.4.1-py3.11-cu124',
25
+ '2.5.1-py3.11-cu124',
26
+ '2.4.1-py3.12-cu124',
27
+ '2.5.1-py3.12-cu124',
28
+ '2.6.0-py3.12-cu126',
29
+ '2.7.0-py3.12-cu128',
30
+ '2.7.0-py3.12-rocm6.3',
30
31
  ]
31
32
  CONCEPTS_REQUIRED_MODEL_TYPE = [
32
33
  'visual-classifier', 'visual-detector', 'visual-segmenter', 'text-classifier'
clarifai/utils/cli.py CHANGED
@@ -2,14 +2,13 @@ import importlib
2
2
  import os
3
3
  import pkgutil
4
4
  import sys
5
+ import typing as t
6
+ from collections import defaultdict
7
+ from typing import OrderedDict
5
8
 
6
9
  import click
7
10
  import yaml
8
-
9
- from rich.console import Console
10
- from rich.panel import Panel
11
- from rich.style import Style
12
- from rich.text import Text
11
+ from tabulate import tabulate
13
12
 
14
13
  from clarifai.utils.logging import logger
15
14
 
@@ -31,19 +30,6 @@ def dump_yaml(data, filename: str):
31
30
  click.echo(f"Error writing YAML file: {e}", err=True)
32
31
 
33
32
 
34
- def set_base_url(env):
35
- environments = {
36
- 'prod': 'https://api.clarifai.com',
37
- 'staging': 'https://api-staging.clarifai.com',
38
- 'dev': 'https://api-dev.clarifai.com'
39
- }
40
-
41
- if env in environments:
42
- return environments[env]
43
- else:
44
- raise ValueError("Invalid environment. Please choose from 'prod', 'staging', 'dev'.")
45
-
46
-
47
33
  # Dynamically find and import all command modules from the cli directory
48
34
  def load_command_modules():
49
35
  package_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'cli')
@@ -53,24 +39,128 @@ def load_command_modules():
53
39
  importlib.import_module(f'clarifai.cli.{module_name}')
54
40
 
55
41
 
56
- def display_co_resources(response, resource_type):
42
+ def display_co_resources(response,
43
+ custom_columns={
44
+ 'ID': lambda c: c.id,
45
+ 'USER_ID': lambda c: c.user_id,
46
+ 'DESCRIPTION': lambda c: c.description,
47
+ }):
57
48
  """Display compute orchestration resources listing results using rich."""
58
49
 
59
- console = Console()
60
- panel = Panel(
61
- Text(f"List of {resource_type}s", justify="center"),
62
- title="",
63
- style=Style(color="blue", bold=True),
64
- border_style="green",
65
- width=60)
66
- console.print(panel)
67
- for indx, item in enumerate(list(response)):
68
- panel = Panel(
69
- "\n".join([f"{'ID'}: {item.id}", f"{'Description'}: {item.description}"]),
70
- title=f"{resource_type} {(indx + 1)}",
71
- border_style="green",
72
- width=60)
73
- console.print(panel)
50
+ formatter = TableFormatter(custom_columns)
51
+ print(formatter.format(list(response), fmt="plain"))
52
+
53
+
54
+ class TableFormatter:
55
+
56
+ def __init__(self, custom_columns: OrderedDict):
57
+ """
58
+ Initializes the TableFormatter with column headers and custom column mappings.
59
+
60
+ :param headers: List of column headers for the table.
61
+ """
62
+ self.custom_columns = custom_columns
63
+
64
+ def format(self, objects, fmt='plain'):
65
+ """
66
+ Formats a list of objects into a table with custom columns.
67
+
68
+ :param objects: List of objects to format into a table.
69
+ :return: A string representing the table.
70
+ """
71
+ # Prepare the rows by applying the custom column functions to each object
72
+ rows = []
73
+ for obj in objects:
74
+ # row = [self.custom_columns[header](obj) for header in self.headers]
75
+ row = [f(obj) for f in self.custom_columns.values()]
76
+ rows.append(row)
77
+
78
+ # Create the table
79
+ table = tabulate(rows, headers=self.custom_columns.keys(), tablefmt=fmt)
80
+ return table
81
+
82
+
83
+ class AliasedGroup(click.Group):
84
+
85
+ def __init__(self,
86
+ name: t.Optional[str] = None,
87
+ commands: t.Optional[t.Union[t.MutableMapping[str, click.Command], t.Sequence[
88
+ click.Command]]] = None,
89
+ **attrs: t.Any) -> None:
90
+ super().__init__(name, commands, **attrs)
91
+ self.alias_map = {}
92
+ self.command_to_aliases = defaultdict(list)
93
+
94
+ def add_alias(self, cmd: click.Command, alias: str) -> None:
95
+ self.alias_map[alias] = cmd
96
+ if alias != cmd.name:
97
+ self.command_to_aliases[cmd].append(alias)
98
+
99
+ def command(self, aliases=None, *args,
100
+ **kwargs) -> t.Callable[[t.Callable[..., t.Any]], click.Command]:
101
+ cmd_decorator = super().command(*args, **kwargs)
102
+ if aliases is None:
103
+ aliases = []
104
+
105
+ def aliased_decorator(f):
106
+ cmd = cmd_decorator(f)
107
+ if cmd.name:
108
+ self.add_alias(cmd, cmd.name)
109
+ for alias in aliases:
110
+ self.add_alias(cmd, alias)
111
+ return cmd
112
+
113
+ f = None
114
+ if args and callable(args[0]):
115
+ (f,) = args
116
+ if f is not None:
117
+ return aliased_decorator(f)
118
+ return aliased_decorator
119
+
120
+ def group(self, aliases=None, *args,
121
+ **kwargs) -> t.Callable[[t.Callable[..., t.Any]], click.Group]:
122
+ cmd_decorator = super().group(*args, **kwargs)
123
+ if aliases is None:
124
+ aliases = []
125
+
126
+ def aliased_decorator(f):
127
+ cmd = cmd_decorator(f)
128
+ if cmd.name:
129
+ self.add_alias(cmd, cmd.name)
130
+ for alias in aliases:
131
+ self.add_alias(cmd, alias)
132
+ return cmd
133
+
134
+ f = None
135
+ if args and callable(args[0]):
136
+ (f,) = args
137
+ if f is not None:
138
+ return aliased_decorator(f)
139
+ return aliased_decorator
140
+
141
+ def get_command(self, ctx: click.Context, cmd_name: str) -> t.Optional[click.Command]:
142
+ rv = click.Group.get_command(self, ctx, cmd_name)
143
+ if rv is not None:
144
+ return rv
145
+ return self.alias_map.get(cmd_name)
146
+
147
+ def format_commands(self, ctx, formatter):
148
+ sub_commands = self.list_commands(ctx)
149
+
150
+ rows = []
151
+ for sub_command in sub_commands:
152
+ cmd = self.get_command(ctx, sub_command)
153
+ if cmd is None or getattr(cmd, 'hidden', False):
154
+ continue
155
+ if cmd in self.command_to_aliases:
156
+ aliases = ', '.join(self.command_to_aliases[cmd])
157
+ sub_command = f'{sub_command} ({aliases})'
158
+ cmd_help = cmd.help
159
+ rows.append((sub_command, cmd_help))
160
+
161
+ if rows:
162
+ with formatter.section("Commands"):
163
+ formatter.write_dl(rows)
74
164
 
75
165
 
76
166
  def validate_context(ctx):