-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsettings_override.py
More file actions
195 lines (155 loc) · 7.01 KB
/
Copy pathsettings_override.py
File metadata and controls
195 lines (155 loc) · 7.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0
"""Docker ``settings-override.py`` fragment for QuickBook and the Boost endpoint app.
Weblate's official image runs this file with ``exec()`` in the same namespace as
``weblate.settings_docker`` (see upstream ``ADDITIONAL_CONFIG``). Copy this module to
``/app/data/settings-override.py`` (hyphen on disk) or keep it on ``PYTHONPATH`` and
point your image at the same content.
``WEBLATE_FORMATS`` is built by **reading** ``weblate/formats/models.py`` and
AST-parsing ``FormatsConf.FORMATS``. That avoids ``import weblate.formats.models``,
which pulls in Django ORM classes during settings import and raises
``AppRegistryNotReady``. If upstream restructures ``FormatsConf`` (e.g. renames the
class or moves ``FORMATS`` off a simple tuple assignment), update the AST helpers
below.
When this file is ``exec``'d into Weblate's settings namespace (Docker),
``INSTALLED_APPS`` is taken from ``globals()`` and extended. Upstream
``weblate.settings_docker`` uses a **list**; the override appends in place with
``+=``. Settings that use an **immutable tuple** instead get a new tuple assigned
back to ``globals()["INSTALLED_APPS"]``. Importing this module without
``INSTALLED_APPS`` in the namespace (typical unit tests) still defines
``WEBLATE_FORMATS`` and skips the apps mutation.
"""
from __future__ import annotations
import ast
import os
from pathlib import Path
from typing import Any
# Package ``__init__`` loads the format registry; does not import ``formats.models``.
import weblate.formats
from boost_weblate.formats import registry # noqa: F401 — register plugin formats
_ENDPOINT_APP_CONFIG = "boost_weblate.endpoint.apps.BoostEndpointConfig"
def _parse_formatsconf_formats_ast(models_text: str) -> list[str]:
tree = ast.parse(models_text)
for node in tree.body:
if isinstance(node, ast.ClassDef) and node.name == "FormatsConf":
return _formats_assignment_to_strings(node.body)
msg = "Class FormatsConf not found in weblate formats models source"
raise RuntimeError(msg)
def _formats_assignment_to_strings(class_body: list[ast.stmt]) -> list[str]:
for node in class_body:
if not isinstance(node, ast.Assign):
continue
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "FORMATS":
return _string_tuple_or_list(node.value)
msg = "FORMATS assignment not found on FormatsConf"
raise RuntimeError(msg)
def _string_tuple_or_list(node: ast.expr) -> list[str]:
if isinstance(node, (ast.Tuple, ast.List)):
out: list[str] = []
for elt in node.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
out.append(elt.value)
else:
msg = f"Unexpected literal in FormatsConf.FORMATS: {ast.dump(elt)}"
raise RuntimeError(msg)
return out
msg = f"Unexpected FormatsConf.FORMATS value: {ast.dump(node)}"
raise RuntimeError(msg)
def weblate_formats_with_plugin_formats() -> tuple[str, ...]:
"""Upstream ``FormatsConf.FORMATS`` paths plus plugin formats from the registry.
Avoids importing ``weblate.formats.models``.
"""
models_py = Path(weblate.formats.__file__).resolve().parent / "models.py"
src = models_py.read_text(encoding="utf-8")
try:
core = tuple(_parse_formatsconf_formats_ast(src))
except RuntimeError:
raise
except (SyntaxError, ValueError) as exc:
msg = f"boost_weblate: could not parse FormatsConf.FORMATS from {models_py}"
raise RuntimeError(msg) from exc
if not core:
msg = f"boost_weblate: no format paths parsed from {models_py}"
raise RuntimeError(msg)
plugin_paths = registry.weblate_class_paths()
extra = tuple(path for path in plugin_paths if path not in core)
return core + extra
WEBLATE_FORMATS = weblate_formats_with_plugin_formats()
_DEFAULT_BOOST_ENDPOINT_THROTTLE_RATES = {
"info": "60/minute",
"add-or-update": "10/hour",
}
def boost_endpoint_throttle_rates() -> dict[str, str]:
"""Scoped throttle rates for Boost endpoint views (env overrides optional)."""
return {
"info": os.environ.get(
"BOOST_ENDPOINT_THROTTLE_INFO",
_DEFAULT_BOOST_ENDPOINT_THROTTLE_RATES["info"],
),
"add-or-update": os.environ.get(
"BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE",
_DEFAULT_BOOST_ENDPOINT_THROTTLE_RATES["add-or-update"],
),
}
BOOST_ENDPOINT_THROTTLE_RATES = boost_endpoint_throttle_rates()
_DEFAULT_ALLOWED_CLONE_HOSTS = ("github.com",)
def allowed_clone_hosts() -> list[str]:
"""Hostnames permitted for git clone URLs (env override optional)."""
raw = os.environ.get("BOOST_ALLOWED_CLONE_HOSTS")
if raw is None:
return list(_DEFAULT_ALLOWED_CLONE_HOSTS)
if raw.strip() == "":
return []
return [host.strip().lower() for host in raw.split(",") if host.strip()]
ALLOWED_CLONE_HOSTS = allowed_clone_hosts()
_DEFAULT_BOOST_TASK_LOCK_TIMEOUT = 1800
_DEFAULT_BOOST_TASK_LOCK_ON_CONFLICT = "skip"
_DEFAULT_BOOST_TASK_LOCK_WAIT_TIMEOUT = 300
def boost_task_lock_settings() -> dict[str, Any]:
"""Redis task-lock settings for Celery deduplication (env overrides optional)."""
return {
"timeout": int(
os.environ.get(
"BOOST_TASK_LOCK_TIMEOUT",
str(_DEFAULT_BOOST_TASK_LOCK_TIMEOUT),
)
),
"on_conflict": os.environ.get(
"BOOST_TASK_LOCK_ON_CONFLICT",
_DEFAULT_BOOST_TASK_LOCK_ON_CONFLICT,
)
.lower()
.strip(),
"wait_timeout": int(
os.environ.get(
"BOOST_TASK_LOCK_WAIT_TIMEOUT",
str(_DEFAULT_BOOST_TASK_LOCK_WAIT_TIMEOUT),
)
),
}
_task_lock_settings = boost_task_lock_settings()
BOOST_TASK_LOCK_TIMEOUT = _task_lock_settings["timeout"]
BOOST_TASK_LOCK_ON_CONFLICT = _task_lock_settings["on_conflict"]
BOOST_TASK_LOCK_WAIT_TIMEOUT = _task_lock_settings["wait_timeout"]
def merge_boost_endpoint_throttle_rates(
rest_framework: dict[str, Any],
) -> dict[str, Any]:
"""Merge Boost endpoint scoped rates into ``REST_FRAMEWORK``."""
merged = dict(rest_framework)
existing = dict(merged.get("DEFAULT_THROTTLE_RATES", {}))
existing.update(BOOST_ENDPOINT_THROTTLE_RATES)
merged["DEFAULT_THROTTLE_RATES"] = existing
return merged
_REST_FRAMEWORK = globals().get("REST_FRAMEWORK")
if _REST_FRAMEWORK is not None:
globals()["REST_FRAMEWORK"] = merge_boost_endpoint_throttle_rates(_REST_FRAMEWORK)
_INSTALLED_APPS = globals().get("INSTALLED_APPS")
if _INSTALLED_APPS is not None:
# Tuple += creates a new object; assign back so exec namespace / settings see it.
# List += mutates in place, matching Weblate/Docker settings namespaces.
if isinstance(_INSTALLED_APPS, tuple):
globals()["INSTALLED_APPS"] = _INSTALLED_APPS + (_ENDPOINT_APP_CONFIG,)
else:
_INSTALLED_APPS += (_ENDPOINT_APP_CONFIG,)