Coverage for app/venv/lib/python3.14/site-packages/weblate/utils/tasks.py: 40%
115 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
5from __future__ import annotations
7import gzip
8import os
9import shutil
10import subprocess
11import sys
12import time
13from importlib import import_module
14from shutil import copyfile
15from typing import cast
17from celery.schedules import crontab
18from django.conf import settings
19from django.core.cache import cache
20from django.core.management.commands import diffsettings
21from ruamel.yaml import YAML
23import weblate.utils.version
24from weblate.formats.models import FILE_FORMATS
25from weblate.logger import LOGGER
26from weblate.machinery.models import MACHINERY
27from weblate.trans.models import Component, Project, Translation
28from weblate.trans.util import get_clean_env
29from weblate.utils.backup import backup_lock
30from weblate.utils.celery import app
31from weblate.utils.data import data_dir
32from weblate.utils.db import using_postgresql
33from weblate.utils.errors import add_breadcrumb, report_error
34from weblate.utils.lock import WeblateLockTimeoutError
35from weblate.vcs.models import VCS_REGISTRY
37from .const import HEARTBEAT_FREQUENCY
40@app.task(trail=False)
41def ping():
42 return {
43 "version": weblate.utils.version.GIT_VERSION,
44 "vcs": sorted(VCS_REGISTRY.keys()),
45 "formats": sorted(FILE_FORMATS.keys()),
46 "mt_services": sorted(MACHINERY.keys()),
47 "encoding": [sys.getfilesystemencoding(), sys.getdefaultencoding()],
48 "uid": os.getuid(),
49 "data_dir": settings.DATA_DIR,
50 }
53@app.task(trail=False)
54def heartbeat() -> None:
55 cache.set("celery_loaded", time.time())
56 cache.set("celery_heartbeat", time.time())
57 cache.set(
58 "celery_encoding", [sys.getfilesystemencoding(), sys.getdefaultencoding()]
59 )
62@app.task(trail=False, autoretry_for=(WeblateLockTimeoutError,))
63def settings_backup() -> None:
64 with backup_lock():
65 # Expand settings in case it contains non-trivial code
66 command = diffsettings.Command()
67 kwargs = {"default": None, "all": False, "output": "hash"}
68 with open(data_dir("backups", "settings-expanded.py"), "w") as handle:
69 handle.write(command.handle(**kwargs))
71 # Backup original settings
72 if settings.SETTINGS_MODULE:
73 settings_mod = import_module(settings.SETTINGS_MODULE)
74 if settings_mod.__file__ is not None:
75 copyfile(settings_mod.__file__, data_dir("backups", "settings.py"))
77 # Backup environment (to make restoring Docker easier)
78 with open(data_dir("backups", "environment.yml"), "w") as handle:
79 yaml = YAML()
80 yaml.dump(dict(os.environ), handle)
83@app.task(trail=False)
84def update_translation_stats_parents(pk: int) -> None:
85 translation = Translation.objects.get(pk=pk)
86 translation.stats.update_parents()
89@app.task(trail=False)
90def update_language_stats_parents(pk: int) -> None:
91 component = Component.objects.get(pk=pk)
92 component.stats.update_language_stats_parents()
95@app.task(trail=False)
96def update_project_stats_link(pk: int) -> None:
97 project = Project.objects.get(pk=pk)
98 for language in project.stats.get_language_stats():
99 language.update_stats(update_parents=False)
100 project.stats.update_stats()
103@app.task(trail=False, autoretry_for=(WeblateLockTimeoutError,))
104def database_backup() -> None:
105 if settings.DATABASE_BACKUP == "none":
106 return
107 with backup_lock():
108 database = settings.DATABASES["default"]
109 env = get_clean_env()
110 compress = settings.DATABASE_BACKUP == "compressed"
112 out_compressed = data_dir("backups", "database.sql.gz")
113 out_text = data_dir("backups", "database.sql")
115 if using_postgresql():
116 cmd = [
117 "pg_dump",
118 # Superuser only, crashes on Alibaba Cloud Database PolarDB
119 "--no-subscriptions",
120 "--clean",
121 "--if-exists",
122 "--dbname",
123 database["NAME"],
124 ]
126 if database["HOST"]:
127 cmd.extend(["--host", database["HOST"]])
128 if database["PORT"]:
129 cmd.extend(["--port", database["PORT"]])
130 if database["USER"]:
131 cmd.extend(["--username", database["USER"]])
132 if settings.DATABASE_BACKUP == "compressed":
133 cmd.extend(["--file", out_compressed])
134 cmd.extend(["--compress", "6"])
135 compress = False
136 else:
137 cmd.extend(["--file", out_text])
139 env["PGPASSWORD"] = cast("str", database["PASSWORD"])
140 else:
141 cmd = [
142 "mysqldump",
143 "--result-file",
144 out_text,
145 "--single-transaction",
146 "--skip-lock-tables",
147 ]
149 if database["HOST"]:
150 cmd.extend(["--host", database["HOST"]])
151 if database["PORT"]:
152 cmd.extend(["--port", database["PORT"]])
153 if database["USER"]:
154 cmd.extend(["--user", database["USER"]])
156 cmd.extend(["--databases", database["NAME"]])
158 env["MYSQL_PWD"] = cast("str", database["PASSWORD"])
160 try:
161 subprocess.run(
162 cmd, # type: ignore[arg-type]
163 env=env,
164 capture_output=True,
165 stdin=subprocess.DEVNULL,
166 check=True,
167 text=True,
168 )
169 except subprocess.CalledProcessError as error:
170 add_breadcrumb(
171 category="backup",
172 message="database dump output",
173 stdout=error.stdout,
174 stderr=error.stderr,
175 )
176 LOGGER.error("failed database backup: %s", error.stderr)
177 report_error("Database backup failed")
178 raise
180 if compress:
181 with open(out_text, "rb") as f_in, gzip.open(out_compressed, "wb") as f_out:
182 shutil.copyfileobj(f_in, f_out)
183 os.unlink(out_text)
186@app.on_after_finalize.connect
187def setup_periodic_tasks(sender, **kwargs) -> None:
188 cache.set("celery_loaded", time.time())
189 sender.add_periodic_task(
190 crontab(hour=1, minute=0), settings_backup.s(), name="settings-backup"
191 )
192 sender.add_periodic_task(
193 crontab(hour=1, minute=30), database_backup.s(), name="database-backup"
194 )
195 sender.add_periodic_task(HEARTBEAT_FREQUENCY, heartbeat.s(), name="heartbeat")