From bfd38adea56e8840917dbd8dd7d33b68df9d2021 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 17 Feb 2026 21:41:33 -0500 Subject: [PATCH] Add standalone Python entrypoint for EasyHAProxy and pip-based installation - Introduced `/scripts/.venv/bin/easy-haproxy` as the default entrypoint for standalone environments. - Added `_build_parser` and `start` functions to enable CLI-driven functionality configuration. - Updated packaging to reflect new `easyhaproxy.main` module and adapted project name from `easymapping` to `easyhaproxy`. - Modified `Dockerfile` to align with new script paths and entrypoints for better portability. - Enhanced documentation with dedicated sections for pip-based installation, CLI references, and systemd setup for non-Docker environments. - Adjusted helm, plugin, and environment-related docs for accurate sidebar ordering. - Added a new GitHub workflow to automate package publishing to PyPI. --- .github/workflows/build.yml | 23 ++++ build/Dockerfile | 4 +- docs/acme.md | 2 +- docs/container-labels.md | 2 +- docs/digitalocean.md | 2 +- docs/dokku.md | 2 +- docs/environment-variable.md | 2 +- docs/helm.md | 2 +- docs/microk8s.md | 2 +- docs/pip.md | 211 +++++++++++++++++++++++++++++++ docs/plugin-development.md | 2 +- docs/plugins.md | 2 +- docs/ssl.md | 2 +- docs/volumes.md | 2 +- pyproject.toml | 6 +- src/easyhaproxy/__init__.py | 0 src/easyhaproxy/main.py | 234 +++++++++++++++++++++++++++++++++++ src/functions/__init__.py | 15 ++- uv.lock | 2 +- 19 files changed, 497 insertions(+), 20 deletions(-) create mode 100644 docs/pip.md create mode 100644 src/easyhaproxy/__init__.py create mode 100644 src/easyhaproxy/main.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddc6084..433a875 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -220,6 +220,29 @@ jobs: # chmod +x $HOME/.docker/cli-plugins/docker-pushrm # docker pushrm ${{ env.IMAGE_NAME }} + Publish-PyPI: + runs-on: ubuntu-latest + needs: [Test, Tests-E2E-Docker, Tests-E2E-Kubernetes, Tests-E2E-Additional] + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + id-token: write # Required for OIDC trusted publisher + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Build package + run: | + export PATH="$HOME/.local/bin:$PATH" + uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + Helm: runs-on: 'ubuntu-latest' needs: Build diff --git a/build/Dockerfile b/build/Dockerfile index 55d225a..feb3676 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -27,7 +27,7 @@ WORKDIR /scripts COPY build/assets / COPY pyproject.toml uv.lock LICENSE README.md /scripts/ -COPY src/ /scripts/ +COPY src/ /scripts/src/ COPY tests/ /scripts/tests/ RUN cd /scripts && uv sync --frozen @@ -36,4 +36,4 @@ RUN apk del .build-deps RUN cd /scripts && uv run pytest -s -vv tests/ && uv sync --no-dev -CMD ["uv", "run", "python", "-u", "/scripts/main.py"] +CMD ["/scripts/.venv/bin/easy-haproxy"] diff --git a/docs/acme.md b/docs/acme.md index f290306..b75dbc5 100644 --- a/docs/acme.md +++ b/docs/acme.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 11 --- # SSL - Automatic Certificate Management Environment (ACME) diff --git a/docs/container-labels.md b/docs/container-labels.md index a87eea7..c2ab3ac 100644 --- a/docs/container-labels.md +++ b/docs/container-labels.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 12 --- # Container Labels diff --git a/docs/digitalocean.md b/docs/digitalocean.md index de48e9f..aab938f 100644 --- a/docs/digitalocean.md +++ b/docs/digitalocean.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 --- # DigitalOcean diff --git a/docs/dokku.md b/docs/dokku.md index 9b5490c..c995b5d 100644 --- a/docs/dokku.md +++ b/docs/dokku.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 --- # Dokku diff --git a/docs/environment-variable.md b/docs/environment-variable.md index 5b4a7e5..16dabc1 100644 --- a/docs/environment-variable.md +++ b/docs/environment-variable.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 13 --- # Docker environment variables diff --git a/docs/helm.md b/docs/helm.md index 9e61d5e..055f938 100644 --- a/docs/helm.md +++ b/docs/helm.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 --- # Helm 3 diff --git a/docs/microk8s.md b/docs/microk8s.md index 316cf1a..7326a33 100644 --- a/docs/microk8s.md +++ b/docs/microk8s.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 --- # Microk8s Add-ons diff --git a/docs/pip.md b/docs/pip.md new file mode 100644 index 0000000..f0b9b64 --- /dev/null +++ b/docs/pip.md @@ -0,0 +1,211 @@ +--- +sidebar_position: 5 +--- + +# Install via pip / uv + +EasyHAProxy can run directly on any Linux or macOS host without Docker, using the `easyhaproxy` Python package. + +## Prerequisites + +HAProxy must be installed and available in your system `PATH` before running `easy-haproxy`. EasyHAProxy will refuse to start with a clear error message if HAProxy is not found. + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```bash +sudo apt install haproxy +``` + + + + +```bash +sudo dnf install haproxy +``` + + + + +```bash +brew install haproxy +``` + + + + +## Installation + +### Recommended: `uv tool` (system-wide, isolated) + +[`uv`](https://docs.astral.sh/uv/) installs `easyhaproxy` into its own isolated environment and exposes the `easy-haproxy` binary in `~/.local/bin/`, similar to `pipx`. + +```bash +# Install uv (if not already installed) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install easyhaproxy as a tool +uv tool install easyhaproxy + +# Make sure ~/.local/bin is in PATH (one-time setup) +uv tool update-shell +``` + +After installation: + +```bash +easy-haproxy --help +``` + +### Alternative: `pip` + +```bash +pip install easyhaproxy +``` + +:::note Virtual environments +When installing inside a virtual environment, `easy-haproxy` is only available while the environment is activated. For system-wide use, prefer `uv tool install` or install with `pip` at the system/user level. +::: + +## CLI Reference + +Every configuration option can be set via a CLI flag **or** an environment variable. CLI flags take precedence over environment variables. + +``` +easy-haproxy [OPTIONS] +``` + +### Core + +| Flag | Environment Variable | Default | Description | +|--------------------------|----------------------------|------------------------------------------------------|-----------------------------------------------------------| +| `--discover MODE` | `EASYHAPROXY_DISCOVER` | **required** | Discovery mode: `static`, `docker`, `swarm`, `kubernetes` | +| `--base-path PATH` | `EASYHAPROXY_BASE_PATH` | `/etc/easyhaproxy` (root) `~/easyhaproxy` (non-root) | Base directory for all EasyHAProxy files | +| `--label-prefix PREFIX` | `EASYHAPROXY_LABEL_PREFIX` | `easyhaproxy` | Label/annotation prefix used to discover services | +| `--ssl-mode MODE` | `EASYHAPROXY_SSL_MODE` | `default` | TLS policy: `strict`, `default`, or `loose` | +| `--refresh-conf SECONDS` | `EASYHAPROXY_REFRESH_CONF` | `10` | Polling interval for configuration changes | +| `--customer-errors BOOL` | `HAPROXY_CUSTOMERRORS` | `false` | Enable custom HAProxy HTML error pages | + +### Logging + +| Flag | Environment Variable | Default | Description | +|-----------------------------|-------------------------|----------|---------------------------| +| `--log-level LEVEL` | `EASYHAPROXY_LOG_LEVEL` | `DEBUG` | EasyHAProxy log level | +| `--haproxy-log-level LEVEL` | `HAPROXY_LOG_LEVEL` | `INFO` | HAProxy process log level | +| `--certbot-log-level LEVEL` | `CERTBOT_LOG_LEVEL` | `DEBUG` | Certbot log level | + +Valid levels: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL` + +### Stats Dashboard + +| Flag | Environment Variable | Default | Description | +|--------------------------------------|-----------------------------|--------------|---------------------------------------------| +| `--haproxy-password PASSWORD` | `HAPROXY_PASSWORD` | *(disabled)* | Enable stats dashboard with this password | +| `--haproxy-username USERNAME` | `HAPROXY_USERNAME` | `admin` | Stats dashboard username | +| `--haproxy-stats-port PORT` | `HAPROXY_STATS_PORT` | `1936` | Stats dashboard port | +| `--haproxy-stats-cors-origin ORIGIN` | `HAPROXY_STATS_CORS_ORIGIN` | *(none)* | Allowed CORS origin for the stats dashboard | + +:::tip +The stats dashboard is only enabled when `--haproxy-password` (or `HAPROXY_PASSWORD`) is set. +::: + +### ACME / Certbot (SSL certificates) + +| Flag | Environment Variable | Default | Description | +|---------------------------------------|--------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--certbot-email EMAIL` | `EASYHAPROXY_CERTBOT_EMAIL` | *(none)* | Contact email — enables ACME when set | +| `--certbot-autoconfig CA` | `EASYHAPROXY_CERTBOT_AUTOCONFIG` | *(none)* | Well-known CA shorthand: `letsencrypt`, `letsencrypt_test`, `buypass`, `buypass_test`, `sslcom_rca`, `sslcom_ecc`, `google`, `google_test`, `zerossl` | +| `--certbot-server URL` | `EASYHAPROXY_CERTBOT_SERVER` | *(none)* | Custom ACME server directory URL | +| `--certbot-eab-kid KID` | `EASYHAPROXY_CERTBOT_EAB_KID` | *(none)* | External Account Binding key ID | +| `--certbot-eab-hmac-key KEY` | `EASYHAPROXY_CERTBOT_EAB_HMAC_KEY` | *(none)* | External Account Binding HMAC key | +| `--certbot-retry-count N` | `EASYHAPROXY_CERTBOT_RETRY_COUNT` | `60` | Iterations before retrying after a rate limit | +| `--certbot-preferred-challenges TYPE` | `EASYHAPROXY_CERTBOT_PREFERRED_CHALLENGES` | `http` | ACME challenge type | +| `--certbot-manual-auth-hook SCRIPT` | `EASYHAPROXY_CERTBOT_MANUAL_AUTH_HOOK` | *(none)* | Path to a manual auth hook script for certbot | + +See the full [ACME documentation](acme.md) for details. + +### Plugins + +| Flag | Environment Variable | Default | Description | +|---------------------------------|--------------------------------------|----------|-------------------------------------------| +| `--plugins-enabled LIST` | `EASYHAPROXY_PLUGINS_ENABLED` | *(none)* | Comma-separated list of plugins to enable | +| `--plugins-abort-on-error BOOL` | `EASYHAPROXY_PLUGINS_ABORT_ON_ERROR` | `false` | Abort startup if a plugin fails to load | + +See the [plugins documentation](plugins.md) for available plugins. + +### Kubernetes + +| Flag | Environment Variable | Default | Description | +|--------------------------------------------|--------------------------------------|----------|----------------------------------------------| +| `--update-ingress-status BOOL` | `EASYHAPROXY_UPDATE_INGRESS_STATUS` | `true` | Update Ingress status with load-balancer IP | +| `--deployment-mode MODE` | `EASYHAPROXY_DEPLOYMENT_MODE` | `auto` | Deployment mode: `auto`, `single`, `cluster` | +| `--external-hostname HOSTNAME` | `EASYHAPROXY_EXTERNAL_HOSTNAME` | *(none)* | External hostname reported in Ingress status | +| `--ingress-status-update-interval SECONDS` | `EASYHAPROXY_STATUS_UPDATE_INTERVAL` | `30` | Interval to update Ingress status | + +## Quick-start examples + +### Static mode (bare-metal / VM) + +```bash +mkdir -p ~/easyhaproxy/static + +cat > ~/easyhaproxy/static/config.yml < argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="easy-haproxy", + description="HAProxy label-based routing with service discovery for Docker, Swarm, and Kubernetes.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # Core + parser.add_argument("--discover", metavar="MODE", + choices=["static", "docker", "swarm", "kubernetes"], + help="Service discovery mode. Also set by EASYHAPROXY_DISCOVER.") + parser.add_argument("--base-path", metavar="PATH", + help="Base directory for all EasyHAProxy files. Also set by EASYHAPROXY_BASE_PATH.") + parser.add_argument("--label-prefix", metavar="PREFIX", + help="Label/annotation prefix used to discover services. Also set by EASYHAPROXY_LABEL_PREFIX.") + parser.add_argument("--ssl-mode", metavar="MODE", + choices=["strict", "default", "loose"], + help="TLS policy: strict (TLS 1.3 only), default, or loose (all). Also set by EASYHAPROXY_SSL_MODE.") + parser.add_argument("--refresh-conf", metavar="SECONDS", type=int, + help="Interval in seconds to poll for configuration changes. Also set by EASYHAPROXY_REFRESH_CONF.") + parser.add_argument("--customer-errors", metavar="BOOL", + choices=["true", "false"], + help="Enable custom HAProxy HTML error pages. Also set by HAPROXY_CUSTOMERRORS.") + + # Logging + log_levels = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"] + parser.add_argument("--log-level", metavar="LEVEL", choices=log_levels, + help="EasyHAProxy log level. Also set by EASYHAPROXY_LOG_LEVEL.") + parser.add_argument("--haproxy-log-level", metavar="LEVEL", choices=log_levels, + help="HAProxy process log level. Also set by HAPROXY_LOG_LEVEL.") + parser.add_argument("--certbot-log-level", metavar="LEVEL", choices=log_levels, + help="Certbot log level. Also set by CERTBOT_LOG_LEVEL.") + + # Stats + parser.add_argument("--haproxy-password", metavar="PASSWORD", + help="Enable HAProxy stats dashboard with this password. Also set by HAPROXY_PASSWORD.") + parser.add_argument("--haproxy-username", metavar="USERNAME", + help="HAProxy stats dashboard username. Also set by HAPROXY_USERNAME.") + parser.add_argument("--haproxy-stats-port", metavar="PORT", + help="HAProxy stats dashboard port. Also set by HAPROXY_STATS_PORT.") + parser.add_argument("--haproxy-stats-cors-origin", metavar="ORIGIN", + help="Allowed CORS origin for the stats dashboard. Also set by HAPROXY_STATS_CORS_ORIGIN.") + + # ACME / Certbot + parser.add_argument("--certbot-email", metavar="EMAIL", + help="Contact email for ACME/Let's Encrypt. Enables certbot when set. Also set by EASYHAPROXY_CERTBOT_EMAIL.") + parser.add_argument("--certbot-autoconfig", metavar="CA", + choices=["letsencrypt", "letsencrypt_test", "buypass", "buypass_test", + "sslcom_rca", "sslcom_ecc", "google", "google_test", "zerossl"], + help="Shorthand to configure a well-known ACME CA. Also set by EASYHAPROXY_CERTBOT_AUTOCONFIG.") + parser.add_argument("--certbot-server", metavar="URL", + help="Custom ACME server directory URL. Also set by EASYHAPROXY_CERTBOT_SERVER.") + parser.add_argument("--certbot-eab-kid", metavar="KID", + help="External Account Binding key ID (required by some CAs). Also set by EASYHAPROXY_CERTBOT_EAB_KID.") + parser.add_argument("--certbot-eab-hmac-key", metavar="KEY", + help="External Account Binding HMAC key. Also set by EASYHAPROXY_CERTBOT_EAB_HMAC_KEY.") + parser.add_argument("--certbot-retry-count", metavar="N", type=int, + help="Iterations before retrying after a rate limit. Also set by EASYHAPROXY_CERTBOT_RETRY_COUNT.") + parser.add_argument("--certbot-preferred-challenges", metavar="TYPE", + help="ACME challenge type (default: http). Also set by EASYHAPROXY_CERTBOT_PREFERRED_CHALLENGES.") + parser.add_argument("--certbot-manual-auth-hook", metavar="SCRIPT", + help="Path to manual auth hook script for certbot. Also set by EASYHAPROXY_CERTBOT_MANUAL_AUTH_HOOK.") + + # Plugins + parser.add_argument("--plugins-enabled", metavar="LIST", + help="Comma-separated list of plugins to enable. Also set by EASYHAPROXY_PLUGINS_ENABLED.") + parser.add_argument("--plugins-abort-on-error", metavar="BOOL", + choices=["true", "false"], + help="Abort startup if a plugin fails to load. Also set by EASYHAPROXY_PLUGINS_ABORT_ON_ERROR.") + + # Kubernetes + parser.add_argument("--update-ingress-status", metavar="BOOL", + choices=["true", "false"], + help="Update Kubernetes Ingress status with load-balancer IP. Also set by EASYHAPROXY_UPDATE_INGRESS_STATUS.") + parser.add_argument("--deployment-mode", metavar="MODE", + choices=["auto", "single", "cluster"], + help="Kubernetes deployment mode. Also set by EASYHAPROXY_DEPLOYMENT_MODE.") + parser.add_argument("--external-hostname", metavar="HOSTNAME", + help="External hostname reported in Ingress status. Also set by EASYHAPROXY_EXTERNAL_HOSTNAME.") + parser.add_argument("--ingress-status-update-interval", metavar="SECONDS", type=int, + help="Interval in seconds to update Ingress status. Also set by EASYHAPROXY_STATUS_UPDATE_INTERVAL.") + + return parser + + +def _apply_args_to_env(args: argparse.Namespace) -> None: + """Write non-None CLI arguments into os.environ so the rest of the code reads them.""" + mapping = { + "discover": "EASYHAPROXY_DISCOVER", + "base_path": "EASYHAPROXY_BASE_PATH", + "label_prefix": "EASYHAPROXY_LABEL_PREFIX", + "ssl_mode": "EASYHAPROXY_SSL_MODE", + "refresh_conf": "EASYHAPROXY_REFRESH_CONF", + "customer_errors": "HAPROXY_CUSTOMERRORS", + "log_level": "EASYHAPROXY_LOG_LEVEL", + "haproxy_log_level": "HAPROXY_LOG_LEVEL", + "certbot_log_level": "CERTBOT_LOG_LEVEL", + "haproxy_password": "HAPROXY_PASSWORD", + "haproxy_username": "HAPROXY_USERNAME", + "haproxy_stats_port": "HAPROXY_STATS_PORT", + "haproxy_stats_cors_origin": "HAPROXY_STATS_CORS_ORIGIN", + "certbot_email": "EASYHAPROXY_CERTBOT_EMAIL", + "certbot_autoconfig": "EASYHAPROXY_CERTBOT_AUTOCONFIG", + "certbot_server": "EASYHAPROXY_CERTBOT_SERVER", + "certbot_eab_kid": "EASYHAPROXY_CERTBOT_EAB_KID", + "certbot_eab_hmac_key": "EASYHAPROXY_CERTBOT_EAB_HMAC_KEY", + "certbot_retry_count": "EASYHAPROXY_CERTBOT_RETRY_COUNT", + "certbot_preferred_challenges": "EASYHAPROXY_CERTBOT_PREFERRED_CHALLENGES", + "certbot_manual_auth_hook": "EASYHAPROXY_CERTBOT_MANUAL_AUTH_HOOK", + "plugins_enabled": "EASYHAPROXY_PLUGINS_ENABLED", + "plugins_abort_on_error": "EASYHAPROXY_PLUGINS_ABORT_ON_ERROR", + "update_ingress_status": "EASYHAPROXY_UPDATE_INGRESS_STATUS", + "deployment_mode": "EASYHAPROXY_DEPLOYMENT_MODE", + "external_hostname": "EASYHAPROXY_EXTERNAL_HOSTNAME", + "ingress_status_update_interval": "EASYHAPROXY_STATUS_UPDATE_INTERVAL", + } + for arg_name, env_name in mapping.items(): + value = getattr(args, arg_name, None) + if value is not None: + os.environ[env_name] = str(value) + + +def start(): + processor_obj = ProcessorInterface.factory(os.getenv("EASYHAPROXY_DISCOVER")) + if processor_obj is None: + exit(1) + + os.makedirs(Consts.certs_certbot, exist_ok=True) + os.makedirs(Consts.certs_haproxy, exist_ok=True) + + processor_obj.save_config(Consts.haproxy_config) + processor_obj.save_certs(Consts.certs_haproxy) + certbot_certs_found = processor_obj.get_certbot_hosts() + logger_easyhaproxy.info(f'Found hosts: {", ".join(processor_obj.get_hosts())}') # Needs to run after save_config + logger_easyhaproxy.debug(f'Object Found: {processor_obj.get_parsed_object()}') + + old_haproxy = None + haproxy = DaemonizeHAProxy() + current_custom_config_files = haproxy.get_custom_config_files() + haproxy.haproxy(DaemonizeHAProxy.HAPROXY_START) + haproxy.sleep() + + certbot = Certbot(Consts.certs_certbot) + + # Check ACME environment readiness if Certbot is configured + if certbot.email != "": + is_ready, error_msg = Certbot.check_acme_environment_ready(certbot.email, certbot.acme_server) + if not is_ready: + logger_easyhaproxy.warning(f"ACME environment not ready: {error_msg}") + logger_easyhaproxy.warning("Certificate auto-renewal may fail. Verify ACME server configuration.") + else: + logger_easyhaproxy.info("ACME environment validated and ready") + + while True: + if old_haproxy is not None: + old_haproxy.kill() + old_haproxy = None + try: + old_parsed = processor_obj.get_parsed_object() + processor_obj.refresh() + if certbot.check_certificates(certbot_certs_found) or DeepDiff(old_parsed, processor_obj.get_parsed_object()) != {} or not haproxy.is_alive() or DeepDiff(current_custom_config_files, haproxy.get_custom_config_files()) != {}: + logger_easyhaproxy.info('New configuration found. Reloading...') + logger_easyhaproxy.debug(f'Object Found: {processor_obj.get_parsed_object()}') + processor_obj.save_config(Consts.haproxy_config) + processor_obj.save_certs(Consts.certs_haproxy) + certbot_certs_found = processor_obj.get_certbot_hosts() + logger_easyhaproxy.info(f'Found hosts: {", ".join(processor_obj.get_hosts())}') # Needs to after save_config + old_haproxy = haproxy + haproxy = DaemonizeHAProxy() + current_custom_config_files = haproxy.get_custom_config_files() + haproxy.haproxy(DaemonizeHAProxy.HAPROXY_RELOAD) + old_haproxy.terminate() + + except Exception as e: + logger_easyhaproxy.fatal(f"Err: {e}") + + logger_easyhaproxy.info('Heartbeat') + haproxy.sleep() + + +def main(): + haproxy_bin = shutil.which('haproxy') + if haproxy_bin is None: + print("ERROR: HAProxy is not installed or not in PATH.") + print("Please install HAProxy before running easy-haproxy.") + print(" Debian/Ubuntu: sudo apt install haproxy") + print(" RHEL/Fedora: sudo dnf install haproxy") + print(" macOS: brew install haproxy") + sys.exit(1) + + args = _build_parser().parse_args() + _apply_args_to_env(args) + + # Reset cached base_path so it re-evaluates after --base-path may have been applied + Consts.reset() + + Functions.run_bash(logger_init, f'{haproxy_bin} -v') + + logger_init.info(r".........................__.....................................") + logger_init.info(r"..___ ____ ________ __/ /_ ____ _____ _________ _ ____ __") + logger_init.info(r"./ _ \/ __ `/ ___/ / / / __ \/ __ `/ __ \/ ___/ __ \| |/_/ / / /") + logger_init.info(r"/ __/ /_/ (__ ) /_/ / / / / /_/ / /_/ / / / /_/ />