|
3 | 3 | # |
4 | 4 | # SPDX-License-Identifier: Apache-2.0 |
5 | 5 |
|
6 | | -"""Classes to get information about the execution environment.""" |
| 6 | +"""Extensible environment detection. |
| 7 | +
|
| 8 | +Functions and classes to get information about the execution environment of HERMES and |
| 9 | +the Software CaRD curation plugin. |
| 10 | +
|
| 11 | +This plugin supports GitHub Actions (``GitHubActionsEnvironment``) and GitLab CI |
| 12 | +(``GitLabCIEnvironment``) out of the box. Support for other environments may be added by |
| 13 | +subclassing ``Environment`` and registering the new class in the Python entry point |
| 14 | +``software_card.environment``. Subclasses must make sure that ``.from_env()`` |
| 15 | +returns ``None`` when HERMES is not running in their associated environment. |
| 16 | +""" |
7 | 17 |
|
8 | 18 | import os |
9 | 19 | from dataclasses import dataclass, fields |
10 | 20 | from datetime import datetime |
| 21 | +from importlib import metadata |
11 | 22 | from typing import Self |
12 | 23 | from urllib.parse import urlencode |
13 | 24 |
|
| 25 | +_ENTRY_POINT_GROUP = "software_card.environment" |
| 26 | + |
14 | 27 |
|
15 | 28 | @dataclass(kw_only=True) |
16 | 29 | class Environment: |
17 | 30 | """Base class for representing computing environments.""" |
18 | 31 |
|
19 | 32 | @classmethod |
20 | 33 | def from_env(cls) -> Self | None: |
21 | | - """Create object from environment variables. |
22 | | -
|
23 | | - If not running in GitHub Actions, ``None`` is returned instead. |
24 | | - """ |
| 34 | + """Create object from environment variables.""" |
25 | 35 | env = dict(os.environ) |
26 | 36 | data = {} |
27 | 37 | for field in fields(cls): |
@@ -180,16 +190,39 @@ def url_data(self) -> dict[str, str]: |
180 | 190 | } |
181 | 191 |
|
182 | 192 |
|
| 193 | +def _get_environment_classes() -> dict[str, type]: |
| 194 | + entry_points = {} |
| 195 | + for entry_point in metadata.entry_points(group=_ENTRY_POINT_GROUP): |
| 196 | + name = entry_point.name |
| 197 | + class_ = entry_point.load() |
| 198 | + if not isinstance(class_, type) or not issubclass(class_, Environment): |
| 199 | + message = ( |
| 200 | + f"Entrypoint '{_ENTRY_POINT_GROUP}'.'{name}' " |
| 201 | + f"must be a subclass of '{Environment.__name__}'" |
| 202 | + ) |
| 203 | + raise TypeError(message) |
| 204 | + entry_points[name] = class_ |
| 205 | + return entry_points |
| 206 | + |
| 207 | + |
183 | 208 | def get() -> Environment | None: |
184 | 209 | """Return the CI environment that we are running in, or ``None``.""" |
185 | | - github_actions = GitHubActionsEnvironment.from_env() |
186 | | - gitlab_ci = GitLabCIEnvironment.from_env() |
187 | | - |
188 | | - if github_actions is not None and gitlab_ci is not None: |
189 | | - message = "More than one CI environment detected" |
| 210 | + environment_classes = _get_environment_classes() |
| 211 | + environments = { |
| 212 | + name: environment_class.from_env() |
| 213 | + for name, environment_class in environment_classes.items() |
| 214 | + } |
| 215 | + |
| 216 | + num_found_environments = sum(map(bool, environments.values())) |
| 217 | + if num_found_environments > 1: |
| 218 | + names = [name for name in environments if environments.get(name) is not None] |
| 219 | + message = f"Multiple CI environments detected: {', '.join(names)}" |
190 | 220 | raise RuntimeError(message) |
191 | 221 |
|
192 | | - return github_actions or gitlab_ci |
| 222 | + if num_found_environments == 0: |
| 223 | + return None |
| 224 | + |
| 225 | + return next(env for env in environments.values() if env is not None) |
193 | 226 |
|
194 | 227 |
|
195 | 228 | def format_app_url(base_url: str, environment: Environment) -> str: |
|
0 commit comments