BTHLABS-64: Support for customized environments

Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
This commit is contained in:
2025-10-27 19:04:48 +00:00
committed by Tomek Wójcik
parent 168657bd14
commit d8bbe57b17
25 changed files with 291 additions and 173 deletions

View File

@@ -13,4 +13,5 @@ backend/hotpocket_backend/settings/metal/
backend/hotpocket_backend/static/
extension/node_modules/
extension/dist/
vendor/
.envrc*

View File

@@ -1,12 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.core.conf import settings
LOGGER = logging.getLogger(__name__)
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
label = 'core'
name = 'hotpocket_backend.apps.core'
verbose_name = _('Core')
def ready(self):
LOGGER.info(
'HotPocket Backend ready: env=`%s` app=`%s`',
settings.ENV.name,
settings.APP.name,
)

View File

@@ -6,7 +6,7 @@ import typing
from hotpocket_backend.secrets.admin import AdminSecrets
from hotpocket_backend.secrets.webapp import WebAppSecrets
from hotpocket_common.constants import App, Env
from hotpocket_common.constants import App, Environment
class PSettings(typing.Protocol):
@@ -16,7 +16,7 @@ class PSettings(typing.Protocol):
SECRET_KEY: str
APP: App
ENV: Env
ENV: Environment
SECRETS: AdminSecrets | WebAppSecrets
@@ -32,3 +32,9 @@ class PSettings(typing.Protocol):
UPLOADS_PATH: pathlib.Path
AUTH_KEY_TTL: int
UI_BASE_HEAD_INCLUDES: list
UI_BASE_SCRIPT_INCLUDES: list
UI_PAGE_HEAD_INCLUDES: list
UI_PAGE_SCRIPT_INCLUDES: list

View File

@@ -82,3 +82,21 @@ def appearance_settings(request: HttpRequest) -> dict:
return {
'APPEARANCE_SETTINGS': result,
}
def base_includes(request: HttpRequest) -> dict:
return {
'BASE_INCLUDES': {
'head': settings.UI_BASE_HEAD_INCLUDES,
'script': settings.UI_BASE_SCRIPT_INCLUDES,
},
}
def page_includes(request: HttpRequest) -> dict:
return {
'PAGE_INCLUDES': {
'head': settings.UI_PAGE_HEAD_INCLUDES,
'script': settings.UI_PAGE_SCRIPT_INCLUDES,
},
}

View File

@@ -31,11 +31,17 @@
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'ui/img/icon-32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'ui/img/icon-16.png' %}">
<link rel="manifest" href="{% url 'ui.meta.manifest_json' %}">
{% for head_include in BASE_INCLUDES.head %}
{% include head_include %}
{% endfor %}
{% block page_head %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}" hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'>
{% block body %}
{% endblock %}
{% block scripts %}{% endblock %}
{% for script_include in BASE_INCLUDES.script %}
{% include script_include %}
{% endfor %}
</body>
</html>

View File

@@ -2,6 +2,12 @@
{% load i18n static ui %}
{% block page_head %}
{% for head_include in PAGE_INCLUDES.head %}
{% include head_include %}
{% endfor %}
{% endblock %}
{% block body %}
<nav id="navbar" class="navbar navbar-expand-sm bg-body-tertiary fixed-top">
<div class="container">
@@ -153,7 +159,11 @@
<script src="{% static 'ui/js/hotpocket-backend.ui.BrowseAccountAppsView.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.InlineCreateSaveForm.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.PasteboardLink.js' %}" type="text/javascript"></script>
{% block page_scripts %}{% endblock %}
{% for script_include in PAGE_INCLUDES.script %}
{% include script_include %}
{% endfor %}
{% block page_scripts %}
{% endblock %}
<script type="text/javascript">
(() => {
window.HotPocket.run({

View File

@@ -19,8 +19,12 @@ ALLOWED_HOSTS = []
ENV = Env(os.getenv('HOTPOCKET_BACKEND_ENV', None))
APP = App(os.getenv('HOTPOCKET_BACKEND_APP', None))
HOTPOCKET_BACKEND_SECRETS_PACKAGE = os.getenv(
'HOTPOCKET_BACKEND_SECRETS_PACKAGE', 'hotpocket_backend.secrets',
)
SECRETS: BaseSecrets = load_secrets(
'hotpocket_backend.secrets', ENV.value, APP.value,
HOTPOCKET_BACKEND_SECRETS_PACKAGE, ENV.value, APP.value,
)
SECRET_KEY = SECRETS.SECRET_KEY

View File

@@ -4,13 +4,15 @@ from __future__ import annotations
from pathlib import Path
from hotpocket_common.constants import App, Environment
BASE_DIR = Path(__file__).resolve().parent.parent.parent
DEBUG = False
ALLOWED_HOSTS = []
ENV = 'build'
APP = 'backend'
ENV = Environment('BUILD', 'build')
APP = App.WEBAPP
INSTALLED_APPS = [
'django.contrib.auth',

View File

@@ -31,6 +31,11 @@ MIDDLEWARE = [
'django_htmx.middleware.HtmxMiddleware',
]
TEMPLATES[0]['OPTIONS']['context_processors'] += [ # noqa: F405
'hotpocket_backend.apps.ui.context_processors.base_includes',
'hotpocket_backend.apps.ui.context_processors.page_includes',
]
ROOT_URLCONF = 'hotpocket_backend.urls.webapp'
LOGIN_REDIRECT_URL = '/accounts/post-login/'
@@ -81,3 +86,9 @@ CORS_ALLOW_HEADERS = (
)
AUTH_KEY_TTL = 30
UI_BASE_HEAD_INCLUDES = []
UI_BASE_SCRIPT_INCLUDES = []
UI_PAGE_HEAD_INCLUDES = []
UI_PAGE_SCRIPT_INCLUDES = []

View File

@@ -1190,14 +1190,14 @@ files = [
[[package]]
name = "keep-it-secret"
version = "1.2.0"
version = "1.3.0"
description = "Keep It Secret by BTHLabs"
optional = false
python-versions = ">=3.10,<4.0"
groups = ["main"]
files = [
{file = "keep_it_secret-1.2.0-py3-none-any.whl", hash = "sha256:aae8f3d3c1db93223ad3afb5c35c2c7202068b082b8bf7d199ed5bc610e63449"},
{file = "keep_it_secret-1.2.0.tar.gz", hash = "sha256:2585591d451674e30c08fcdbba95de2180904069da149bca628cc51261720839"},
{file = "keep_it_secret-1.3.0-py3-none-any.whl", hash = "sha256:9efe032bd1efbf18fa0cc15b73e38e17e23ee8a8690ffab948f16368f8eb9126"},
{file = "keep_it_secret-1.3.0.tar.gz", hash = "sha256:80d6907b296e1f520c1ad30f9748c75ed007bb91ec4db7a83b0cb1b200804f6c"},
]
[package.dependencies]
@@ -1208,6 +1208,11 @@ hvac = {version = ">=2.1.0", optional = true, markers = "extra == \"vault\""}
aws = ["boto3 (>=1.34.0)"]
vault = ["hvac (>=2.1.0)"]
[package.source]
type = "legacy"
url = "https://nexus.bthlabs.pl/repository/pypi/simple"
reference = "nexus"
[[package]]
name = "kombu"
version = "5.5.4"
@@ -2572,4 +2577,4 @@ brotli = ["brotli"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "bdbe000a5510f8f6eadb468eaa66df87c65df006c05dbfdde0a660316efbb120"
content-hash = "9ea38ee8174f679163c0b46cde1cb4eff4b7330db8a3b4649a7a910bdf5fe15d"

View File

@@ -22,7 +22,7 @@ django-crispy-forms = "2.4"
django-htmx = "1.26.0"
hotpocket-common = {path = "../packages/common", develop = true}
hotpocket-soa = {path = "../packages/soa", develop = true}
keep-it-secret = {version = "1.2.0", extras = ["aws", "vault"]}
keep-it-secret = {version = "1.3.0", extras = ["aws", "vault"]}
psycopg = {version = "3.2.10", extras = ["binary"]}
pydantic = "2.12.2"
pyquery = "2.0.1"

View File

@@ -1,3 +1,3 @@
from .accounts import AccessTokenOriginApp # noqa: F401
from .associations import AssociationsSearchMode # noqa: F401
from .core import NULL_UUID, App, Env # noqa: F401
from .core import NULL_UUID, App, Env, Environment # noqa: F401

View File

@@ -2,16 +2,69 @@
from __future__ import annotations
import enum
import typing
import uuid
NULL_UUID = uuid.UUID('00000000-0000-0000-0000-000000000000')
class Env(enum.Enum):
METAL = 'metal'
DOCKER = 'docker'
DEPLOYMENT = 'deployment'
AIO = 'aio'
class Environment:
def __init__(self, name: str, value: str):
self._name = name
self._value = value
@property
def name(self) -> str:
return self._name
@property
def value(self) -> str:
return self._value
def __repr__(self) -> str:
return f'<Env {self.name}: {self.value}>'
def __str__(self) -> str:
return self.name
def to_rpc(self) -> str:
return self.value
class Environments:
METAL = Environment('METAL', 'metal')
DOCKER = Environment('DOCKER', 'docker')
DEPLOYMENT = Environment('DEPLOYMENT', 'deployment')
AIO = Environment('AIO', 'aio')
def _current_envs(self) -> typing.Generator[Environment, None, None]:
for attribute in dir(self):
candidate = getattr(self, attribute)
if isinstance(candidate, Environment):
yield candidate
def __call__(self, value: str) -> Environment:
result: Environment | None = None
for candidate in self._current_envs():
if candidate.value == value:
result = candidate
break
if result is None:
raise ValueError(f'Could not resolve env: `{value}`')
return result
def choices(self) -> list[tuple[str, str]]:
result = []
for env in self._current_envs():
result.append((env.value, env.name))
return result
Env = Environments()
class App(enum.Enum):