backend.ai-plugin 24.3.9b1__py3-none-any.whl → 25.15.5__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.

Potentially problematic release.


This version of backend.ai-plugin might be problematic. Click here for more details.

ai/backend/plugin/VERSION CHANGED
@@ -1 +1 @@
1
- 24.03.9b1
1
+ 25.15.5
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import itertools
5
+ import json
6
+ import logging
7
+ from collections import defaultdict
8
+ from typing import Self
9
+
10
+ import click
11
+ import colorama
12
+ import tabulate
13
+ from colorama import Fore, Style
14
+
15
+ from ai.backend.logging import AbstractLogger, LocalLogger, LogLevel
16
+
17
+ from .entrypoint import (
18
+ prepare_wheelhouse,
19
+ scan_entrypoint_from_buildscript,
20
+ scan_entrypoint_from_package_metadata,
21
+ scan_entrypoint_from_plugin_checkouts,
22
+ )
23
+
24
+ log = logging.getLogger(__spec__.name)
25
+
26
+
27
+ class FormatOptions(enum.StrEnum):
28
+ CONSOLE = "console"
29
+ JSON = "json"
30
+
31
+
32
+ class CLIContext:
33
+ _logger: AbstractLogger
34
+
35
+ def __init__(self, log_level: LogLevel) -> None:
36
+ self.log_level = log_level
37
+
38
+ def __enter__(self) -> Self:
39
+ self._logger = LocalLogger(log_level=self.log_level)
40
+ self._logger.__enter__()
41
+ return self
42
+
43
+ def __exit__(self, *exc_info) -> None:
44
+ self._logger.__exit__()
45
+
46
+
47
+ @click.group()
48
+ @click.option(
49
+ "--debug",
50
+ is_flag=True,
51
+ help="Set the logging level to DEBUG",
52
+ )
53
+ @click.pass_context
54
+ def main(
55
+ ctx: click.Context,
56
+ debug: bool,
57
+ ) -> None:
58
+ """The root entrypoint for unified CLI of the plugin subsystem"""
59
+ log_level = LogLevel.DEBUG if debug else LogLevel.NOTSET
60
+ ctx.obj = ctx.with_resource(CLIContext(log_level))
61
+
62
+
63
+ @main.command()
64
+ @click.argument("group_name")
65
+ @click.option(
66
+ "--format",
67
+ type=click.Choice([*FormatOptions]),
68
+ default=FormatOptions.CONSOLE,
69
+ show_default=True,
70
+ help="Set the output format.",
71
+ )
72
+ def scan(
73
+ group_name: str,
74
+ format: FormatOptions,
75
+ ) -> None:
76
+ sources: dict[str, set[str]] = defaultdict(set)
77
+ rows = []
78
+
79
+ prepare_wheelhouse()
80
+ for source, entrypoint in itertools.chain(
81
+ (("buildscript", item) for item in scan_entrypoint_from_buildscript(group_name)),
82
+ (("plugin-checkout", item) for item in scan_entrypoint_from_plugin_checkouts(group_name)),
83
+ (("python-package", item) for item in scan_entrypoint_from_package_metadata(group_name)),
84
+ ):
85
+ sources[entrypoint.name].add(source)
86
+ rows.append((source, entrypoint.name, entrypoint.module))
87
+ rows.sort(key=lambda row: (row[2], row[1], row[0]))
88
+
89
+ match format:
90
+ case FormatOptions.CONSOLE:
91
+ if not rows:
92
+ print(f"No plugins found for the entrypoint {group_name!r}")
93
+ return
94
+ colorama.init(autoreset=True)
95
+ ITALIC = colorama.ansi.code_to_chars(3)
96
+ STRIKETHR = colorama.ansi.code_to_chars(9)
97
+ src_style = {
98
+ "buildscript": Fore.LIGHTYELLOW_EX,
99
+ "plugin-checkout": Fore.LIGHTGREEN_EX,
100
+ "python-package": Fore.LIGHTBLUE_EX,
101
+ }
102
+ display_headers = (
103
+ f"{ITALIC}Source{Style.RESET_ALL}",
104
+ f"{ITALIC}Name{Style.RESET_ALL}",
105
+ f"{ITALIC}Module Path{Style.RESET_ALL}",
106
+ f"{ITALIC}Note{Style.RESET_ALL}",
107
+ )
108
+ display_rows = []
109
+ duplicates = set()
110
+ warnings: dict[str, str] = dict()
111
+ for source, name, module_path in rows:
112
+ note = ""
113
+ name_style = Style.BRIGHT
114
+ has_plugin_checkout = "plugin-checkout" in sources[name]
115
+ duplication_threshold = 2 if has_plugin_checkout else 1
116
+ if len(sources[name]) > duplication_threshold:
117
+ duplicates.add(name)
118
+ name_style = Fore.RED + Style.BRIGHT
119
+ if source == "plugin-checkout":
120
+ name_style = Style.DIM + STRIKETHR
121
+ if "python-package" in sources[name]:
122
+ note = "Loaded via the python-package source"
123
+ else:
124
+ note = "Ignored when loading plugins unless installed as editable"
125
+ display_rows.append((
126
+ f"{src_style[source]}{source}{Style.RESET_ALL}",
127
+ f"{name_style}{name}{Style.RESET_ALL}",
128
+ module_path,
129
+ note,
130
+ ))
131
+ print(tabulate.tabulate(display_rows, display_headers))
132
+ for name, msg in warnings.items():
133
+ print(msg)
134
+ if duplicates:
135
+ duplicate_list = ", ".join(duplicates)
136
+ print(
137
+ f"\n{Fore.LIGHTRED_EX}\u26a0 Detected duplicated entrypoint(s): {Style.BRIGHT}{duplicate_list}{Style.RESET_ALL}"
138
+ )
139
+ if "accelerator" in group_name:
140
+ print(
141
+ f"{Fore.LIGHTRED_EX} You should check [agent].allow-compute-plugins in "
142
+ f"agent.toml to activate only one accelerator implementation for each name.{Style.RESET_ALL}"
143
+ )
144
+ case FormatOptions.JSON:
145
+ output_rows = []
146
+ for source, name, module_path in rows:
147
+ output_rows.append({
148
+ "source": source,
149
+ "name": name,
150
+ "module_path": module_path,
151
+ })
152
+ print(json.dumps(output_rows, indent=2))
@@ -1,9 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import ast
2
4
  import collections
3
5
  import configparser
4
6
  import itertools
5
7
  import logging
6
8
  import os
9
+ import sys
10
+ import zipfile
7
11
  from importlib.metadata import EntryPoint, entry_points
8
12
  from pathlib import Path
9
13
  from typing import Iterable, Iterator, Optional
@@ -19,9 +23,10 @@ def scan_entrypoints(
19
23
  if blocklist is None:
20
24
  blocklist = set()
21
25
  existing_names: dict[str, EntryPoint] = {}
26
+
27
+ prepare_wheelhouse()
22
28
  for entrypoint in itertools.chain(
23
29
  scan_entrypoint_from_buildscript(group_name),
24
- scan_entrypoint_from_plugin_checkouts(group_name),
25
30
  scan_entrypoint_from_package_metadata(group_name),
26
31
  ):
27
32
  if allowlist is not None and not match_plugin_list(entrypoint.value, allowlist):
@@ -62,6 +67,8 @@ def match_plugin_list(entry_path: str, plugin_list: set[str]) -> bool:
62
67
 
63
68
 
64
69
  def scan_entrypoint_from_package_metadata(group_name: str) -> Iterator[EntryPoint]:
70
+ log.debug("scan_entrypoint_from_package_metadata(%r)", group_name)
71
+
65
72
  yield from entry_points().select(group=group_name)
66
73
 
67
74
 
@@ -70,27 +77,82 @@ _default_glob_excluded_patterns = [
70
77
  "ai/backend/web/static",
71
78
  "ai/backend/runner",
72
79
  "ai/backend/kernel",
80
+ "wheelhouse",
81
+ "tools",
73
82
  ]
74
83
 
84
+ _optimized_glob_search_patterns = {
85
+ # These patterns only apply to scanning BUILD files in dev setups and pex distributions.
86
+ # They do not affect standard package entrypoint searches.
87
+ # NOTE: most entrypoint declaration in BUILD files are in the package's top-level only!
88
+ "backendai_cli_v10": ["ai/backend/*", "ai/backend/appproxy/*"],
89
+ "backendai_network_manager_v1": ["ai/backend/*"],
90
+ "backendai_event_dispatcher_v20": ["ai/backend/*", "ai/backend/appproxy/*"],
91
+ "backendai_stats_monitor_v20": ["ai/backend/*", "ai/backend/appproxy/*"],
92
+ "backendai_error_monitor_v20": ["ai/backend/*", "ai/backend/appproxy/*"],
93
+ "backendai_hook_v20": ["ai/backend/*"],
94
+ "backendai_webapp_v20": ["ai/backend/*"],
95
+ "backendai_scheduler_v10": ["ai/backend/manager"],
96
+ "backendai_agentselector_v10": ["ai/backend/manager"],
97
+ }
75
98
 
76
- def _glob(base_path: Path, filename: str, excluded_patterns: Iterable[str]) -> Iterator[Path]:
77
- q: collections.deque[Path] = collections.deque()
78
- q.append(base_path)
99
+
100
+ def _glob(
101
+ base_path: Path,
102
+ filename: str,
103
+ excluded_patterns: Iterable[str],
104
+ match_patterns: Iterable[str] | None = None,
105
+ ) -> Iterator[Path]:
106
+ q: collections.deque[tuple[Path, bool]] = collections.deque()
107
+ assert base_path.is_dir()
108
+ q.append((base_path, False))
79
109
  while q:
80
- search_path = q.pop()
81
- assert search_path.is_dir()
110
+ search_path, suffix_match = q.pop()
111
+
112
+ # Check if current directory matches any pattern and we should yield files from it
113
+ current_matches = False
114
+ if match_patterns is not None:
115
+ current_matches = any(search_path.match(pattern) for pattern in match_patterns)
116
+
82
117
  for item in search_path.iterdir():
83
118
  if item.is_dir():
84
- if search_path.name == "__pycache__":
119
+ if item.name == "__pycache__":
85
120
  continue
86
- if search_path.name.startswith("."):
121
+ if item.name.startswith("."):
87
122
  continue
88
- if any(search_path.match(pattern) for pattern in excluded_patterns):
123
+ if any(item.match(pattern) for pattern in excluded_patterns):
89
124
  continue
90
- q.append(item)
125
+
126
+ # Determine if we should queue this directory
127
+ should_queue = False
128
+ new_suffix_match = False
129
+
130
+ if match_patterns is None:
131
+ # No patterns specified - queue all non-excluded directories
132
+ should_queue = True
133
+ new_suffix_match = False
134
+ elif not suffix_match:
135
+ # Haven't found a matching directory yet - check if this one matches
136
+ if any(item.match(pattern) for pattern in match_patterns):
137
+ should_queue = True
138
+ new_suffix_match = True
139
+ else:
140
+ # Keep searching - queue without suffix match
141
+ should_queue = True
142
+ new_suffix_match = False
143
+ else:
144
+ # Already found a matching directory - only queue if this also matches
145
+ if any(item.match(pattern) for pattern in match_patterns):
146
+ should_queue = True
147
+ new_suffix_match = True
148
+
149
+ if should_queue:
150
+ q.append((item, new_suffix_match))
91
151
  else:
92
152
  if item.name == filename:
93
- yield item
153
+ # Yield file if no patterns or current directory matches
154
+ if match_patterns is None or current_matches:
155
+ yield item
94
156
 
95
157
 
96
158
  def scan_entrypoint_from_buildscript(group_name: str) -> Iterator[EntryPoint]:
@@ -100,20 +162,36 @@ def scan_entrypoint_from_buildscript(group_name: str) -> Iterator[EntryPoint]:
100
162
  log.debug(
101
163
  "scan_entrypoint_from_buildscript(%r): Namespace path: %s", group_name, ai_backend_ns_path
102
164
  )
103
- for buildscript_path in _glob(ai_backend_ns_path, "BUILD", _default_glob_excluded_patterns):
165
+ match_patterns = _optimized_glob_search_patterns.get(group_name, None)
166
+ # First, it is invoked in PEX or temporary test environment generated by Pantsbuild.
167
+ # In the test environment, BUILD files are NOT copied, so the plugin discovery will rely on the
168
+ # followed build-root search below.
169
+ for buildscript_path in _glob(
170
+ ai_backend_ns_path, "BUILD", _default_glob_excluded_patterns, match_patterns
171
+ ):
104
172
  for entrypoint in extract_entrypoints_from_buildscript(group_name, buildscript_path):
105
173
  entrypoints[entrypoint.name] = entrypoint
106
- # Override with the entrypoints found in the current source directories,
107
- try:
108
- build_root = find_build_root()
109
- except ValueError:
110
- pass
174
+ if os.environ.get("SCIE", None) is None:
175
+ # Override with the entrypoints found in the current build-root directory.
176
+ try:
177
+ build_root = find_build_root()
178
+ except ValueError:
179
+ pass
180
+ else:
181
+ src_path = build_root / "src"
182
+ log.debug("scan_entrypoint_from_buildscript(%r): current src: %s", group_name, src_path)
183
+ for buildscript_path in _glob(
184
+ src_path, "BUILD", _default_glob_excluded_patterns, match_patterns
185
+ ):
186
+ for entrypoint in extract_entrypoints_from_buildscript(
187
+ group_name, buildscript_path
188
+ ):
189
+ entrypoints[entrypoint.name] = entrypoint
111
190
  else:
112
- src_path = build_root / "src"
113
- log.debug("scan_entrypoint_from_buildscript(%r): current src: %s", group_name, src_path)
114
- for buildscript_path in _glob(src_path, "BUILD", _default_glob_excluded_patterns):
115
- for entrypoint in extract_entrypoints_from_buildscript(group_name, buildscript_path):
116
- entrypoints[entrypoint.name] = entrypoint
191
+ log.debug(
192
+ "scan_entrypoint_from_buildscript(%r): skipping 'src' when executed inside the SCIE environment",
193
+ group_name,
194
+ )
117
195
  yield from entrypoints.values()
118
196
 
119
197
 
@@ -143,6 +221,20 @@ def scan_entrypoint_from_plugin_checkouts(group_name: str) -> Iterator[EntryPoin
143
221
  yield from entrypoints.values()
144
222
 
145
223
 
224
+ def prepare_wheelhouse(base_dir: Path | None = None) -> None:
225
+ if base_dir is None:
226
+ base_dir = Path.cwd()
227
+ for whl_path in (base_dir / "wheelhouse").glob("*.whl"):
228
+ extracted_path = whl_path.with_suffix("") # strip the extension
229
+ log.debug("prepare_wheelhouse(): loading %s", whl_path)
230
+ if not extracted_path.exists():
231
+ with zipfile.ZipFile(whl_path, "r") as z:
232
+ z.extractall(extracted_path)
233
+ decoded_path = os.fsdecode(extracted_path)
234
+ if decoded_path not in sys.path:
235
+ sys.path.append(decoded_path)
236
+
237
+
146
238
  def find_build_root(path: Optional[Path] = None) -> Path:
147
239
  if env_build_root := os.environ.get("BACKEND_BUILD_ROOT", None):
148
240
  return Path(env_build_root)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: backend.ai-plugin
3
- Version: 24.3.9b1
3
+ Version: 25.15.5
4
4
  Summary: Backend.AI Plugin Subsystem
5
5
  Home-page: https://github.com/lablup/backend.ai
6
6
  Author: Lablup Inc. and contributors
@@ -15,11 +15,28 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Environment :: No Input/Output (Daemon)
16
16
  Classifier: Topic :: Scientific/Engineering
17
17
  Classifier: Topic :: Software Development
18
- Classifier: Development Status :: 4 - Beta
19
- Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Development Status :: 5 - Production/Stable
19
+ Classifier: Programming Language :: Python :: 3.13
20
20
  Classifier: License :: OSI Approved :: MIT License
21
- Requires-Python: >=3.12,<3.13
21
+ Requires-Python: >=3.13,<3.14
22
22
  Description-Content-Type: text/markdown
23
+ Requires-Dist: backend.ai-common==25.15.5
24
+ Requires-Dist: backend.ai-logging==25.15.5
25
+ Requires-Dist: click~=8.1.7
26
+ Requires-Dist: colorama>=0.4.6
27
+ Requires-Dist: tabulate~=0.8.9
28
+ Requires-Dist: types-colorama
29
+ Requires-Dist: types-tabulate
30
+ Dynamic: author
31
+ Dynamic: classifier
32
+ Dynamic: description
33
+ Dynamic: description-content-type
34
+ Dynamic: home-page
35
+ Dynamic: license
36
+ Dynamic: project-url
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
23
40
 
24
41
  Backend.AI Plugin Subsystem
25
42
  ===========================
@@ -0,0 +1,11 @@
1
+ ai/backend/plugin/VERSION,sha256=Q0grptxKHmGs0zbrjO9Pd6vBscOXL2Wus3WF9nw9xXI,8
2
+ ai/backend/plugin/__init__.py,sha256=HKBIEWtrpEk2KY3fB3Xb72N0xz5zoYUyn2HfZ75bTsc,96
3
+ ai/backend/plugin/cli.py,sha256=7SLUJy-8uPd_tSBOEZPYw1566QwrJJmCngoVyqrPAWE,5182
4
+ ai/backend/plugin/entrypoint.py,sha256=lFqz-QNcWfYBlFoDTGEU5sY0CgYD0NWEHhhtmXs4840,12083
5
+ ai/backend/plugin/py.typed,sha256=L3M0nPxGMCVTGcbI38G0aomWrOnRTY4HVjsWWRWRjsI,12
6
+ backend_ai_plugin-25.15.5.dist-info/METADATA,sha256=T3J6Ia6qIrr4bjQi-S2ZFUq6sxQXAG5nBcFL35uSROs,1528
7
+ backend_ai_plugin-25.15.5.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
8
+ backend_ai_plugin-25.15.5.dist-info/entry_points.txt,sha256=XxdR8AJRnWYCT-BgkqvFySRw_WjL0r9M43fRAVszaqY,56
9
+ backend_ai_plugin-25.15.5.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
+ backend_ai_plugin-25.15.5.dist-info/top_level.txt,sha256=TJAp5TUfTUztZSUatbygths7CWRrFfnOMCtZ-DIcw6c,3
11
+ backend_ai_plugin-25.15.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (80.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [backendai_cli_v10]
2
+ plugin = ai.backend.plugin.cli:main
@@ -1,9 +0,0 @@
1
- ai/backend/plugin/VERSION,sha256=nbYJZmVl6PFYyLMBzO0xWpgVCdtDsJkASxzXmRELNaM,10
2
- ai/backend/plugin/__init__.py,sha256=HKBIEWtrpEk2KY3fB3Xb72N0xz5zoYUyn2HfZ75bTsc,96
3
- ai/backend/plugin/entrypoint.py,sha256=7VU_dl8QnrR7n2kTRsmdy0VCIqwtrUcrTMti2hWM6xk,8172
4
- ai/backend/plugin/py.typed,sha256=L3M0nPxGMCVTGcbI38G0aomWrOnRTY4HVjsWWRWRjsI,12
5
- backend.ai_plugin-24.3.9b1.dist-info/METADATA,sha256=ZvGGReukg1pHCSdFxLbDdbQHyhqA1N_vsRf4ruyhj-8,1068
6
- backend.ai_plugin-24.3.9b1.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
7
- backend.ai_plugin-24.3.9b1.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
- backend.ai_plugin-24.3.9b1.dist-info/top_level.txt,sha256=TJAp5TUfTUztZSUatbygths7CWRrFfnOMCtZ-DIcw6c,3
9
- backend.ai_plugin-24.3.9b1.dist-info/RECORD,,