From a7026445d1c7108d56d36629afcd4b73b29d8faa Mon Sep 17 00:00:00 2001 From: Vlad Grigorescu Date: Tue, 26 Nov 2024 09:39:45 -0600 Subject: [PATCH 1/5] PoC for self-documenting environment variables. --- config/__init__.py | 19 +++++++++++++- config/asgi.py | 26 +++----------------- docs/document_env_vars.py | 28 +++++++++++++++++++++ docs/reference/index.md | 6 +++-- mkdocs.yml | 3 +++ pyproject.toml | 1 + requirements/base.txt | 1 + requirements/local.txt | 6 ----- scram/utils/debug.py | 52 +++++++++++++++++++++++++++++++++++++++ translator/translator.py | 50 +++---------------------------------- 10 files changed, 113 insertions(+), 79 deletions(-) create mode 100644 docs/document_env_vars.py create mode 100644 scram/utils/debug.py diff --git a/config/__init__.py b/config/__init__.py index 69052b7a..9050e3ed 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1 +1,18 @@ -"""Holds the setings and entrypoints.""" +"""Define the configuration scheme using environ-config.""" + +import environ +from attrs.validators import in_ + + +@environ.config(prefix="SCRAM") +class AppConfig: + """Top level configuration (i.e. SCRAM_).""" + + debugger = environ.var( + default="", + help='Debug SCRAM with a debugger. Set to either "pycharm-pydevd" or "debugpy".', + validator=in_(["pycharm-pydevd", "debugpy", ""]), + ) + + +cfg = environ.to_config(AppConfig) diff --git a/config/asgi.py b/config/asgi.py index 5e11d1fa..28d92ef5 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -18,30 +18,10 @@ # TODO: from channels.security.websocket import AllowedHostsOriginValidator from django.core.asgi import get_asgi_application -logger = logging.getLogger(__name__) - -# Here we setup a debugger if this is desired. This obviously should not be run in production. -debug = os.environ.get("DEBUG") -if debug: - logger.info("Django is set to use a debugger. Provided debug mode: %s", debug) - if debug == "pycharm-pydevd": - logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!") - - import pydevd_pycharm - - pydevd_pycharm.settrace("host.docker.internal", port=56783, stdoutToServer=True, stderrToServer=True) +from scram.utils import debug - logger.info("Debugger started.") - elif debug == "debugpy": - logger.info("Entering debug mode for debugpy (VSCode)") - - import debugpy - - debugpy.listen(("0.0.0.0", 56780)) # noqa S104 (doesn't like binding to all interfaces) - - logger.info("Debugger listening on port 56780.") - else: - logger.warning("Invalid debug mode given: %s. Debugger not started", debug) +logger = logging.getLogger(__name__) +debug.setup(base_port=56730) # This allows easy placement of apps within the interior # scram directory. diff --git a/docs/document_env_vars.py b/docs/document_env_vars.py new file mode 100644 index 00000000..71ec1e4c --- /dev/null +++ b/docs/document_env_vars.py @@ -0,0 +1,28 @@ +"""Converts environ-config's documentation to Markdown and writes it to the docs folder.""" + +import sys +from pathlib import Path + +sys.path.append(".") + +from config import cfg + + +def formatter(options): + """Formats the help text to Markdown. + + Returns: + str: A simple Markdown table with the env vars + """ + result = "| Name | Description | Required | Default |\n" + result += "|-|-|-|-|\n" + for o in options: + result += f"|{o['var_name']}|{o['help_str']}|{o['required']}|{o['default']!r}|\n" + + return result + + +def on_pre_build(config): + """Handler from mkdocs hook.""" + with Path("docs/environment_variables.md").open("w", encoding="utf-8") as f: + f.write(cfg.generate_help(formatter=formatter)) diff --git a/docs/reference/index.md b/docs/reference/index.md index fc771185..4318bd24 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -2,6 +2,8 @@ The SCRAM ecosystem consists of two parts: -A Django app, [route_manager](/reference/django) +A Django app, [route_manager](/reference/django.md) -A translator service, [translator](/reference/translator) +A translator service, [translator](/reference/translator.md) + +Environment variable configuration [envvars](/reference/envvars.md) diff --git a/mkdocs.yml b/mkdocs.yml index 704db80c..3068815d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,3 +14,6 @@ plugins: show_submodules: true - search - section-index + +hooks: + - docs/document_env_vars.py \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5c332104..549bd79c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ data_file = "coverage.coverage" [tool.coverage.report] exclude_also = [ + "if cfg.debugger:", "if debug:", "if self.debug:", "if settings.DEBUG", diff --git a/requirements/base.txt b/requirements/base.txt index b43b582a..a4b43109 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,6 +1,7 @@ argon2-cffi==21.3.0 # https://github.com/hynek/argon2_cffi django-celery-beat # https://github.com/celery/django-celery-beat django-netfields # https://pypi.org/project/django-netfields/ +environ-config # https://pypi.org/project/environ-config/ flower==1.0.0 # https://github.com/mher/flower python-slugify==6.1.2 # https://github.com/un33k/python-slugify uvicorn[standard]==0.17.6 # https://github.com/encode/uvicorn diff --git a/requirements/local.txt b/requirements/local.txt index 2712cf7e..37b27421 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -41,9 +41,3 @@ django-debug-toolbar==3.4.0 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin pytest-django==4.5.2 # https://github.com/pytest-dev/pytest-django - -# Debugging -# ------------------------------------------------------------------------------ -debugpy -# Pycharm might force you to be on the same version IDE as library, use caution. -pydevd-pycharm diff --git a/scram/utils/debug.py b/scram/utils/debug.py new file mode 100644 index 00000000..98c75817 --- /dev/null +++ b/scram/utils/debug.py @@ -0,0 +1,52 @@ +"""Enable either the PyCharm or debugpy debugger.""" + +import logging +import subprocess # noqa: S404 +import sys + +from config import cfg + +logger = logging.getLogger(__name__) + + +def setup(base_port=56780): + """Setup a debugger if this is desired. This obviously should not be run in production. + + If using PyCharm, will start it on base_port + 1. + If using debugpy, will start it on base_port. + + """ + if cfg.debugger: + logger.info("Django is set to use a debugger. Provided debug mode: %s", cfg.debugger) + if cfg.debugger == "pycharm-pydevd": + logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!") + + try: + import pydevd_pycharm # noqa: PLC0415 + except ImportError: + logger.info("Installing pydevd_pycharm...") + subprocess.check_call([sys.executable, "-m", "pip", "install", "pydevd-pycharm"]) # noqa: S603 TODO: add this to the container build + import pydevd_pycharm # noqa: PLC0415 + + logger.info("Done installing pydevd_pycharm") + + pydevd_pycharm.settrace( + "host.docker.internal", port=base_port + 1, stdoutToServer=True, stderrToServer=True + ) + + logger.info("pycharm-pydevd debugger started on host=host.docker.internal port=%d.", base_port + 1) + elif cfg.debugger == "debugpy": + logger.info("Entering debug mode for debugpy (VSCode)") + + try: + import debugpy # noqa: PLC0415 + except ImportError: + logger.info("Installing debugpy...") + subprocess.check_call([sys.executable, "-m", "pip", "install", "debugpy"]) # noqa: S603 TODO: add this to the container build + import debugpy # noqa: PLC0415 + + logger.info("Done installing debugpy") + + debugpy.listen(("0.0.0.0", base_port)) # noqa S104 (doesn't like binding to all interfaces) + + logger.info("VScode debugpy debugger started on host=0.0.0.0 port=%d.", base_port) diff --git a/translator/translator.py b/translator/translator.py index d2c34018..0280e451 100644 --- a/translator/translator.py +++ b/translator/translator.py @@ -11,7 +11,10 @@ import websockets from gobgp import GoBGP +from scram.utils import debug + logger = logging.getLogger(__name__) +debug.setup(base_port=56732) KNOWN_MESSAGES = { "translator_add", @@ -20,53 +23,6 @@ "translator_check", } -# Here we setup a debugger if this is desired. This obviously should not be run in production. -debug_mode = os.environ.get("DEBUG") -if debug_mode: - - def install_deps(): - """Install necessary dependencies for debuggers. - - Because of how we build translator currently, we don't have a great way to selectively - install things at build, so we just do it here! Right now this also includes base.txt, - which is unecessary, but in the future when we build a little better, it'll already be - setup. - """ - logger.info("Installing dependencies for debuggers") - - import subprocess # noqa: S404, PLC0415 - import sys # noqa: PLC0415 - - subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "/requirements/local.txt"]) # noqa: S603 TODO: add this to the container build - - logger.info("Done installing dependencies for debuggers") - - logger.info("Translator is set to use a debugger. Provided debug mode: %s", debug_mode) - # We have to setup the debugger appropriately for various IDEs. It'd be nice if they all used the same thing but - # sadly, we live in a fallen world. - if debug_mode == "pycharm-pydevd": - logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!") - - install_deps() - - import pydevd_pycharm - - pydevd_pycharm.settrace("host.docker.internal", port=56782, stdoutToServer=True, stderrToServer=True) - - logger.info("Debugger started.") - elif debug_mode == "debugpy": - logger.info("Entering debug mode for debugpy (VSCode)") - - install_deps() - - import debugpy - - debugpy.listen(("0.0.0.0", 56781)) # noqa S104 (doesn't like binding to all interfaces) - - logger.info("Debugger listening on port 56781.") - else: - logger.warning("Invalid debug mode given: %s. Debugger not started", debug_mode) - # Must match the URL in asgi.py, and needs a trailing slash hostname = os.environ.get("SCRAM_HOSTNAME", "scram_hostname_not_set") url = os.environ.get("SCRAM_EVENTS_URL", "ws://django:8000/ws/route_manager/translator_block/") From 6ad3b355b5c29e79822eeb04425229b3de232f6b Mon Sep 17 00:00:00 2001 From: Vlad Grigorescu Date: Tue, 26 Nov 2024 09:54:30 -0600 Subject: [PATCH 2/5] Change the validator to a warning. --- config/__init__.py | 13 ++++++++++--- docs/reference/index.md | 6 ++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index 9050e3ed..c03c4248 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,8 +1,12 @@ """Define the configuration scheme using environ-config.""" +import logging + import environ from attrs.validators import in_ +logger = logging.getLogger(__name__) + @environ.config(prefix="SCRAM") class AppConfig: @@ -10,9 +14,12 @@ class AppConfig: debugger = environ.var( default="", - help='Debug SCRAM with a debugger. Set to either "pycharm-pydevd" or "debugpy".', - validator=in_(["pycharm-pydevd", "debugpy", ""]), - ) + help='Will launch the appropriate debugger if set to either "pycharm-pydevd" or "debugpy".') + + @debugger.validator + def _warn_on_unknown_debugger(self, var, debugger): + if debugger and debugger not in {"pycharm-pydevd", "debugpy"}: + logger.warning("Unknown debugger value: %s", debugger) cfg = environ.to_config(AppConfig) diff --git a/docs/reference/index.md b/docs/reference/index.md index 4318bd24..517dbe31 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -2,8 +2,6 @@ The SCRAM ecosystem consists of two parts: -A Django app, [route_manager](/reference/django.md) +A Django app, [route_manager](django.md) -A translator service, [translator](/reference/translator.md) - -Environment variable configuration [envvars](/reference/envvars.md) +A translator service, [translator](translator.md) From 2353d83d4c6189fa2e6f673a9c251401e7275c03 Mon Sep 17 00:00:00 2001 From: Vlad Grigorescu Date: Tue, 26 Nov 2024 09:55:17 -0600 Subject: [PATCH 3/5] Ruff fixes --- config/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index c03c4248..17d10dd7 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -3,7 +3,6 @@ import logging import environ -from attrs.validators import in_ logger = logging.getLogger(__name__) @@ -13,9 +12,9 @@ class AppConfig: """Top level configuration (i.e. SCRAM_).""" debugger = environ.var( - default="", - help='Will launch the appropriate debugger if set to either "pycharm-pydevd" or "debugpy".') - + default="", help='Will launch the appropriate debugger if set to either "pycharm-pydevd" or "debugpy".' + ) + @debugger.validator def _warn_on_unknown_debugger(self, var, debugger): if debugger and debugger not in {"pycharm-pydevd", "debugpy"}: From cfc41b06a194458447e8bbccd061d852caf4dc6a Mon Sep 17 00:00:00 2001 From: Vlad Grigorescu Date: Tue, 26 Nov 2024 09:55:55 -0600 Subject: [PATCH 4/5] Spacing [noci] --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3068815d..ee762ea5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,4 +16,4 @@ plugins: - section-index hooks: - - docs/document_env_vars.py \ No newline at end of file + - docs/document_env_vars.py From 6d3eafdbf1beb66b636509883145f3edb237e81c Mon Sep 17 00:00:00 2001 From: Vlad Grigorescu Date: Tue, 26 Nov 2024 10:06:28 -0600 Subject: [PATCH 5/5] Roll back the debug modifications --- config/asgi.py | 25 +++++++++++++++++-- scram/utils/debug.py | 52 ---------------------------------------- translator/translator.py | 49 +++++++++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 56 deletions(-) delete mode 100644 scram/utils/debug.py diff --git a/config/asgi.py b/config/asgi.py index 28d92ef5..24095fe5 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -18,10 +18,31 @@ # TODO: from channels.security.websocket import AllowedHostsOriginValidator from django.core.asgi import get_asgi_application -from scram.utils import debug +from config import cfg logger = logging.getLogger(__name__) -debug.setup(base_port=56730) + +# Here we setup a debugger if this is desired. This obviously should not be run in production. +if cfg.debugger: + logger.info("Django is set to use a debugger. Provided debug mode: %s", cfg.debugger) + if cfg.debugger == "pycharm-pydevd": + logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!") + + import pydevd_pycharm + + pydevd_pycharm.settrace("host.docker.internal", port=56783, stdoutToServer=True, stderrToServer=True) + + logger.info("Debugger started.") + elif cfg.debugger == "debugpy": + logger.info("Entering debug mode for debugpy (VSCode)") + + import debugpy + + debugpy.listen(("0.0.0.0", 56780)) # noqa S104 (doesn't like binding to all interfaces) + + logger.info("Debugger listening on port 56780.") + else: + logger.warning("Invalid debug mode given: %s. Debugger not started", cfg.debugger) # This allows easy placement of apps within the interior # scram directory. diff --git a/scram/utils/debug.py b/scram/utils/debug.py deleted file mode 100644 index 98c75817..00000000 --- a/scram/utils/debug.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Enable either the PyCharm or debugpy debugger.""" - -import logging -import subprocess # noqa: S404 -import sys - -from config import cfg - -logger = logging.getLogger(__name__) - - -def setup(base_port=56780): - """Setup a debugger if this is desired. This obviously should not be run in production. - - If using PyCharm, will start it on base_port + 1. - If using debugpy, will start it on base_port. - - """ - if cfg.debugger: - logger.info("Django is set to use a debugger. Provided debug mode: %s", cfg.debugger) - if cfg.debugger == "pycharm-pydevd": - logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!") - - try: - import pydevd_pycharm # noqa: PLC0415 - except ImportError: - logger.info("Installing pydevd_pycharm...") - subprocess.check_call([sys.executable, "-m", "pip", "install", "pydevd-pycharm"]) # noqa: S603 TODO: add this to the container build - import pydevd_pycharm # noqa: PLC0415 - - logger.info("Done installing pydevd_pycharm") - - pydevd_pycharm.settrace( - "host.docker.internal", port=base_port + 1, stdoutToServer=True, stderrToServer=True - ) - - logger.info("pycharm-pydevd debugger started on host=host.docker.internal port=%d.", base_port + 1) - elif cfg.debugger == "debugpy": - logger.info("Entering debug mode for debugpy (VSCode)") - - try: - import debugpy # noqa: PLC0415 - except ImportError: - logger.info("Installing debugpy...") - subprocess.check_call([sys.executable, "-m", "pip", "install", "debugpy"]) # noqa: S603 TODO: add this to the container build - import debugpy # noqa: PLC0415 - - logger.info("Done installing debugpy") - - debugpy.listen(("0.0.0.0", base_port)) # noqa S104 (doesn't like binding to all interfaces) - - logger.info("VScode debugpy debugger started on host=0.0.0.0 port=%d.", base_port) diff --git a/translator/translator.py b/translator/translator.py index 0280e451..1591bf05 100644 --- a/translator/translator.py +++ b/translator/translator.py @@ -11,10 +11,9 @@ import websockets from gobgp import GoBGP -from scram.utils import debug +from config import cfg logger = logging.getLogger(__name__) -debug.setup(base_port=56732) KNOWN_MESSAGES = { "translator_add", @@ -23,6 +22,52 @@ "translator_check", } +# Here we setup a debugger if this is desired. This obviously should not be run in production. +if cfg.debugger: + + def install_deps(): + """Install necessary dependencies for debuggers. + + Because of how we build translator currently, we don't have a great way to selectively + install things at build, so we just do it here! Right now this also includes base.txt, + which is unecessary, but in the future when we build a little better, it'll already be + setup. + """ + logger.info("Installing dependencies for debuggers") + + import subprocess # noqa: S404, PLC0415 + import sys # noqa: PLC0415 + + subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "/requirements/local.txt"]) # noqa: S603 TODO: add this to the container build + + logger.info("Done installing dependencies for debuggers") + + logger.info("Translator is set to use a debugger. Provided debug mode: %s", cfg.debugger) + # We have to setup the debugger appropriately for various IDEs. It'd be nice if they all used the same thing but + # sadly, we live in a fallen world. + if cfg.debugger == "pycharm-pydevd": + logger.info("Entering debug mode for pycharm, make sure the debug server is running in PyCharm!") + + install_deps() + + import pydevd_pycharm + + pydevd_pycharm.settrace("host.docker.internal", port=56782, stdoutToServer=True, stderrToServer=True) + + logger.info("Debugger started.") + elif cfg.debugger == "debugpy": + logger.info("Entering debug mode for debugpy (VSCode)") + + install_deps() + + import debugpy + + debugpy.listen(("0.0.0.0", 56781)) # noqa S104 (doesn't like binding to all interfaces) + + logger.info("Debugger listening on port 56781.") + else: + logger.warning("Invalid debug mode given: %s. Debugger not started", cfg.debugger) + # Must match the URL in asgi.py, and needs a trailing slash hostname = os.environ.get("SCRAM_HOSTNAME", "scram_hostname_not_set") url = os.environ.get("SCRAM_EVENTS_URL", "ws://django:8000/ws/route_manager/translator_block/")