mirror of
https://github.com/Cian-H/I-Form_Server_Node_Deployer.git
synced 2025-12-22 22:22:02 +00:00
All functions are now typed and documented
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -138,6 +138,9 @@ venv.bak/
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# VSCode project settings
|
||||
.vscode/
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
|
||||
47
poetry.lock
generated
47
poetry.lock
generated
@@ -599,6 +599,51 @@ beautifulsoup4 = ">=4.7"
|
||||
lxml = "*"
|
||||
requests = ">=2.22.0"
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.6.1"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"},
|
||||
{file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"},
|
||||
{file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"},
|
||||
{file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"},
|
||||
{file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"},
|
||||
{file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"},
|
||||
{file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"},
|
||||
{file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mypy-extensions = ">=1.0.0"
|
||||
typing-extensions = ">=4.1.0"
|
||||
|
||||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
install-types = ["pip"]
|
||||
reports = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
@@ -1047,4 +1092,4 @@ h11 = ">=0.9.0,<1"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "f4992ac644316ea1b818a9d394644dfa4f16dee7cb6b05fe0853b61b4629c334"
|
||||
content-hash = "c74d1dcf762e5266d41c81b62bef444f065124fede9b3a34176cd9a6b93d9c2b"
|
||||
|
||||
@@ -24,6 +24,7 @@ ruff = "^0.1.1"
|
||||
black = "^23.10.1"
|
||||
snoop = "^0.4.3"
|
||||
pytest = "^7.4.3"
|
||||
mypy = "^1.6.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
node_deployer = "node_deployer.__main__:main"
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
def main() -> None:
|
||||
"""Entry point for the CLI
|
||||
"""
|
||||
from . import config
|
||||
config.update_config("cli")
|
||||
from .node_deployer import app
|
||||
app()
|
||||
|
||||
def debug():
|
||||
def debug() -> None:
|
||||
"""Entry point for the debug CLI
|
||||
"""
|
||||
from . import config
|
||||
config.update_config("debug")
|
||||
from .node_deployer import app
|
||||
|
||||
# Below, we set the default value of the debug flag
|
||||
# for the base function of each command to True
|
||||
def unwrap(f):
|
||||
def unwrap(f): # Not a closure, just here to avoid polluting the namespace
|
||||
if hasattr(f, "__wrapped__"):
|
||||
return unwrap(f.__wrapped__)
|
||||
else:
|
||||
|
||||
@@ -5,6 +5,7 @@ import tarfile
|
||||
import time
|
||||
from typing import Annotated
|
||||
|
||||
import docker
|
||||
import git
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
@@ -18,7 +19,12 @@ from .debug import debug_guard
|
||||
from .utils import ensure_build_dir
|
||||
|
||||
|
||||
def create_driver():
|
||||
def create_driver() -> webdriver.Remote:
|
||||
"""Creates a selenium webdriver instance
|
||||
|
||||
Returns:
|
||||
webdriver.Remote: The created webdriver instance
|
||||
"""
|
||||
driver = webdriver.Remote(
|
||||
"http://127.0.0.1:4444",
|
||||
options=webdriver.FirefoxOptions(),
|
||||
@@ -27,7 +33,20 @@ def create_driver():
|
||||
return driver
|
||||
|
||||
|
||||
def convert_json_via_fuelignition(container, driver, fuelignition_json, img_path):
|
||||
def convert_json_via_fuelignition(
|
||||
container: docker.models.containers.Container,
|
||||
driver: webdriver.Remote,
|
||||
fuelignition_json: Path,
|
||||
img_path: Path,
|
||||
) -> None:
|
||||
"""Converts a fuel-ignition json file to an ignition disk image file
|
||||
|
||||
Args:
|
||||
container (docker.models.containers.Container): The selenium container
|
||||
driver (webdriver.Remote): The selenium webdriver instance
|
||||
fuelignition_json (Path): The path to the fuel-ignition json file
|
||||
img_path (Path): The path to the output ignition disk image file
|
||||
"""
|
||||
driver.get(config.FUELIGNITION_URL)
|
||||
# Navigate to "Load Settings from" and upload the json
|
||||
load_from = driver.find_element(By.NAME, "load_from")
|
||||
@@ -66,7 +85,12 @@ def convert_json_via_fuelignition(container, driver, fuelignition_json, img_path
|
||||
f.write(tar.extractfile(tar.getmembers()[0].name).read())
|
||||
|
||||
|
||||
def build_fuelignition():
|
||||
def build_fuelignition() -> docker.models.images.Image:
|
||||
"""Builds the fuel-ignition docker image
|
||||
|
||||
Returns:
|
||||
docker.models.images.Image: The built docker image
|
||||
"""
|
||||
# Make sure the local fuel-ignition repo is up to date
|
||||
if (not config.FUELIGNITION_BUILD_DIR.exists()) or (
|
||||
len(tuple(config.FUELIGNITION_BUILD_DIR.iterdir())) == 0
|
||||
@@ -149,7 +173,22 @@ def json_to_img(
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Takes a fuel-ignition json file and produces an ignition disk image file"""
|
||||
"""Converts a fuel-ignition json file to an ignition disk image file
|
||||
|
||||
Args:
|
||||
json_path (Annotated[ Path, typer.Option, optional):
|
||||
The path to the fuel-ignition json file.
|
||||
Defaults to Path("fuelignition.json").
|
||||
img_path (Annotated[ Path, typer.Option, optional):
|
||||
The path to the output ignition disk image file.
|
||||
Defaults to Path("ignition.img").
|
||||
debug (Annotated[ bool, typer.Option, optional):
|
||||
Enable debug mode.
|
||||
Defaults to False.
|
||||
|
||||
Raises:
|
||||
e: Any exception raised during execution
|
||||
"""
|
||||
selenium_container = None
|
||||
fuelignition_container = None
|
||||
fuelignition_image = None
|
||||
|
||||
@@ -9,10 +9,20 @@ from .utils import Singleton
|
||||
|
||||
|
||||
class SingletonProgress(Progress, metaclass=Singleton):
|
||||
"""A singleton progress bar"""
|
||||
pass
|
||||
|
||||
|
||||
def cli_spinner(*spinner_args, **spinner_kwargs) -> Callable:
|
||||
"""A decorator that adds a spinner to the CLI while the decorated function is running
|
||||
|
||||
Args:
|
||||
*spinner_args: The arguments to pass to the rich spinner object
|
||||
**spinner_kwargs: The keyword arguments to pass to the rich spinner object
|
||||
|
||||
Returns:
|
||||
Callable: The decorated function
|
||||
"""
|
||||
def decorator(f: Callable) -> Callable:
|
||||
# Indent the spinner to match its nesting level
|
||||
indent = len(inspect.stack()) - 1
|
||||
|
||||
@@ -17,7 +17,17 @@ PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.absolute()
|
||||
type ConfigLabel = str | list[str]
|
||||
|
||||
|
||||
def get_config(config_label: ConfigLabel = ["default"]) -> dict:
|
||||
def get_config(config_label: ConfigLabel = "default") -> dict:
|
||||
"""Gets the specified configuration from config.toml
|
||||
|
||||
Args:
|
||||
config_label (ConfigLabel, optional):
|
||||
The label of the configuration to get.
|
||||
Defaults to "default".
|
||||
|
||||
Returns:
|
||||
dict: The specified configuration
|
||||
"""
|
||||
if isinstance(config_label, str):
|
||||
config_label = [config_label]
|
||||
with open(PROJECT_ROOT / "config.toml", "rb") as f:
|
||||
@@ -29,6 +39,12 @@ def get_config(config_label: ConfigLabel = ["default"]) -> dict:
|
||||
|
||||
|
||||
def finalise_config(config: dict) -> None:
|
||||
"""Finalises the configuration by converting paths to Path objects and
|
||||
appropriately setting secondary parameters such as relative paths
|
||||
|
||||
Args:
|
||||
config (dict): The configuration to finalise
|
||||
"""
|
||||
# First, convert base paths to Path objects
|
||||
for k, v in config.items():
|
||||
match k:
|
||||
@@ -57,17 +73,34 @@ def finalise_config(config: dict) -> None:
|
||||
|
||||
|
||||
def apply_config(config: dict) -> None:
|
||||
"""Applies the specified configuration to this module's globals
|
||||
|
||||
Args:
|
||||
config (dict): The configuration to apply
|
||||
"""
|
||||
finalise_config(config)
|
||||
globals().update(config)
|
||||
|
||||
|
||||
def update_config(config_label: ConfigLabel = "default") -> None:
|
||||
"""Updates the configuration to the specified configuration
|
||||
|
||||
Args:
|
||||
config_label (ConfigLabel, optional):
|
||||
The label of the configuration to update to.
|
||||
Defaults to "default".
|
||||
"""
|
||||
apply_config(get_config(config_label))
|
||||
|
||||
|
||||
def init(config) -> None:
|
||||
globals().update(get_config(config))
|
||||
def init(config_label: ConfigLabel) -> None:
|
||||
"""Initialises the configuration module
|
||||
|
||||
Args:
|
||||
config_label (ConfigLabel): The configuration to initialise with
|
||||
"""
|
||||
globals().update(get_config(config_label))
|
||||
update_config()
|
||||
|
||||
|
||||
init(config="default")
|
||||
init(config_label="default")
|
||||
@@ -4,6 +4,7 @@ from typing import Annotated
|
||||
|
||||
from docker.types import Mount
|
||||
import typer
|
||||
from typing import Tuple
|
||||
|
||||
from . import config
|
||||
from .cli import cli_spinner
|
||||
@@ -16,6 +17,14 @@ type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
|
||||
|
||||
|
||||
def filter_validation_response(response: str) -> str:
|
||||
"""Filters out erroneous warnings from the validation response
|
||||
|
||||
Args:
|
||||
response (str): The response to filter
|
||||
|
||||
Returns:
|
||||
str: The filtered response
|
||||
"""
|
||||
return "\n".join(
|
||||
filter(
|
||||
# Filter out the warning about unused key human_readable, this always exists in
|
||||
@@ -27,6 +36,11 @@ def filter_validation_response(response: str) -> str:
|
||||
|
||||
|
||||
def validation_result() -> str:
|
||||
"""Returns the response resulting from a validation of the ignition image
|
||||
|
||||
Returns:
|
||||
str: The response from the validation
|
||||
"""
|
||||
dockerfile = config.DOCKERFILE_DIR / "validate.dockerfile"
|
||||
image, _ = config.CLIENT.images.build(
|
||||
path=".",
|
||||
@@ -50,7 +64,13 @@ def validation_result() -> str:
|
||||
|
||||
|
||||
@cli_spinner(description="Validating ignition image", total=None)
|
||||
def validate() -> (bool, str):
|
||||
def validate() -> Tuple[bool, str]:
|
||||
"""Validates the ignition image
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: A tuple containing a boolean indicating whether
|
||||
the validation was successful and the response from the validation
|
||||
"""
|
||||
response = validation_result().decode()
|
||||
response = filter_validation_response(response)
|
||||
return (not bool(response), response)
|
||||
@@ -58,6 +78,11 @@ def validate() -> (bool, str):
|
||||
|
||||
@cli_spinner(description="Writing ignition image to disk", total=None)
|
||||
def write_disk(disk: str) -> None:
|
||||
"""Writes the ignition image to the specified disk
|
||||
|
||||
Args:
|
||||
disk (str): The disk to write to
|
||||
"""
|
||||
config.CLIENT.containers.run(
|
||||
"alpine",
|
||||
mounts=[config.CWD_MOUNT, Mount("/ignition_disk", disk, type="bind")],
|
||||
@@ -141,7 +166,34 @@ def create_ignition_disk(
|
||||
)
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Writes an ignition image to the specified disk for easy deployment of new nodes to the swarm""" # noqa
|
||||
"""Creates an ignition image and writes it to the specified disk
|
||||
|
||||
Args:
|
||||
disk (Annotated[ str, typer.Option, optional):
|
||||
The disk to write to.
|
||||
Defaults to None.
|
||||
hostname (Annotated[ str, typer.Option, optional):
|
||||
The hostname for the new node.
|
||||
Defaults to "node".
|
||||
password (Annotated[ str, typer.Option, optional):
|
||||
The password for the root user on the new node.
|
||||
Defaults to None.
|
||||
switch_ip (Annotated[ IPAddress, typer.Option, optional):
|
||||
The IP address of the switch to connect to.
|
||||
Defaults to None.
|
||||
switch_port (Annotated[ int, typer.Option, optional):
|
||||
The port on the switch to connect to.
|
||||
Defaults to 4789.
|
||||
swarm_token (Annotated[ str, typer.Option, optional):
|
||||
The swarm token for connecting to the swarm.
|
||||
Defaults to None.
|
||||
debug (Annotated[ bool, typer.Option, optional):
|
||||
Enable debug mode.
|
||||
Defaults to False.
|
||||
|
||||
Raises:
|
||||
typer.Exit: Exit CLI if the ignition image is invalid
|
||||
"""
|
||||
create_img(
|
||||
hostname = hostname,
|
||||
password = password,
|
||||
|
||||
@@ -16,6 +16,11 @@ type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
|
||||
|
||||
|
||||
def load_template() -> dict:
|
||||
"""Loads the default template for the ignition configuration
|
||||
|
||||
Returns:
|
||||
dict: The default ignition configuration
|
||||
"""
|
||||
with open(config.SRC_DIR / "templates/fuelignition.json", "r") as f:
|
||||
out = json.load(f)
|
||||
return out
|
||||
@@ -27,6 +32,17 @@ def apply_ignition_settings(
|
||||
password: str,
|
||||
swarm_config: str,
|
||||
) -> dict:
|
||||
"""Applies the specified ignition settings to the template
|
||||
|
||||
Args:
|
||||
template (dict): The template to apply the settings to
|
||||
hostname (str): The hostname to set
|
||||
password (str): The password to set for the root user
|
||||
swarm_config (str): The swarm configuration to set
|
||||
|
||||
Returns:
|
||||
dict: The template with the settings applied
|
||||
"""
|
||||
ignition_config = template.copy()
|
||||
ignition_config["hostname"] = hostname
|
||||
ignition_config["login"]["users"][0]["passwd"] = password
|
||||
@@ -144,7 +160,31 @@ def create_img(
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Creates an ignition image for deploying a new node to the swarm"""
|
||||
"""Creates an ignition image for a node that will automatically join a swarm
|
||||
|
||||
Args:
|
||||
hostname (Annotated[ str, typer.Option, optional):
|
||||
The hostname to set for the node.
|
||||
Defaults to "node".
|
||||
password (Annotated[ str, typer.Option, optional):
|
||||
The password to set for the root user on the node.
|
||||
Defaults to None.
|
||||
switch_ip (Annotated[ IPAddress, typer.Option, optional):
|
||||
The IP address of the switch to connect to.
|
||||
Defaults to None.
|
||||
switch_port (Annotated[ int, typer.Option, optional):
|
||||
The port on the switch to connect to.
|
||||
Defaults to 4789.
|
||||
swarm_token (Annotated[ str, typer.Option, optional):
|
||||
The swarm token for connecting to the swarm.
|
||||
Defaults to None.
|
||||
img_path (Annotated[ Path, typer.Option, optional):
|
||||
The path to which the ignition image should be written.
|
||||
Defaults to Path("ignition.img").
|
||||
debug (Annotated[ bool, typer.Option, optional):
|
||||
Enable debug mode.
|
||||
Defaults to False.
|
||||
"""
|
||||
# get swarm configuration as JSON
|
||||
swarm_config = json.dumps(
|
||||
{
|
||||
|
||||
@@ -7,13 +7,18 @@ import typer
|
||||
from . import config
|
||||
|
||||
|
||||
# def merge(f1: Callable) -> Callable:
|
||||
# https://docs.python.org/3/library/functools.html#functools.update_wrapper
|
||||
# wraps, but it combines the signatures of the two functions
|
||||
# This will allow us to add/remove the `debug` arg depending on config context
|
||||
|
||||
|
||||
def debug_guard(f: Callable) -> Callable:
|
||||
"""A decorator that contextually enables debug mode for the decorated function
|
||||
|
||||
Args:
|
||||
f (Callable): The function to decorate
|
||||
|
||||
Raises:
|
||||
typer.Exit: Exit CLI if the dev group is not installed
|
||||
|
||||
Returns:
|
||||
Callable: The decorated function
|
||||
"""
|
||||
if not config.DEBUG:
|
||||
return f
|
||||
try:
|
||||
|
||||
@@ -6,6 +6,14 @@ from . import config
|
||||
|
||||
|
||||
def ensure_build_dir(f: Callable) -> Callable:
|
||||
"""Ensures that the build directory exists before running the decorated function
|
||||
|
||||
Args:
|
||||
f (Callable): The function to decorate
|
||||
|
||||
Returns:
|
||||
Callable: The decorated function
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
Path(config.BUILD_DIR).mkdir(exist_ok=True, parents=True)
|
||||
@@ -15,8 +23,15 @@ def ensure_build_dir(f: Callable) -> Callable:
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
"""A singleton metaclass"""
|
||||
_instance = None
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
"""Creates a new instance of the class if one does not already exist
|
||||
|
||||
Returns:
|
||||
cls._instance: The instance of the class
|
||||
"""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__call__(*args, **kwargs)
|
||||
return cls._instance
|
||||
|
||||
Reference in New Issue
Block a user