From dcb0dd1c49d401a08b02c53776014adf4649a3e4 Mon Sep 17 00:00:00 2001 From: Bonicki Wojciech Date: Sat, 24 May 2025 13:54:50 +0200 Subject: [PATCH 1/4] cache API exercises task added --- wger/exercises/cache.py | 46 +++++++++++++++++++ .../commands/warmup-exercise-api-cache.py | 19 ++------ wger/exercises/tasks.py | 33 ++++++++++--- wger/settings_global.py | 2 + 4 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 wger/exercises/cache.py diff --git a/wger/exercises/cache.py b/wger/exercises/cache.py new file mode 100644 index 000000000..deda67bd3 --- /dev/null +++ b/wger/exercises/cache.py @@ -0,0 +1,46 @@ +# This file is part of wger Workout Manager. +# +# wger Workout Manager is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# wger Workout Manager is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License + +from typing import Callable +# Standard Library +from wger.exercises.models import ( + Exercise, +) +from wger.utils.cache import reset_exercise_api_cache +from wger.exercises.api.serializers import ExerciseInfoSerializer + + +def cache_exercise(exercise: Exercise, force=False, print_fn: Callable=print, style_fn: Callable=lambda x: x): + """ + Caches a provided exercise. + """ + if force: + print_fn(f"Force updating cache for exercise base {exercise.uuid}") + reset_exercise_api_cache(exercise.uuid) + else: + print_fn(f"Warming cache for exercise base {exercise.uuid}") + + serializer = ExerciseInfoSerializer(exercise) + serializer.data + + +def cache_api_exercises( + print_fn: Callable, + force: bool, + style_fn: Callable=lambda x: x, +): + print_fn("*** Caching API exercises ***") + for exercise in Exercise.with_translations.all(): + cache_exercise(exercise, force, print_fn, style_fn) + print_fn(style_fn("Exercises cached!\n")) diff --git a/wger/exercises/management/commands/warmup-exercise-api-cache.py b/wger/exercises/management/commands/warmup-exercise-api-cache.py index 2483e5fa3..a73200cb8 100644 --- a/wger/exercises/management/commands/warmup-exercise-api-cache.py +++ b/wger/exercises/management/commands/warmup-exercise-api-cache.py @@ -16,10 +16,8 @@ from django.core.management.base import BaseCommand # wger -from wger.exercises.api.serializers import ExerciseInfoSerializer from wger.exercises.models import Exercise -from wger.utils.cache import reset_exercise_api_cache - +from wger.exercises.cache import cache_exercise class Command(BaseCommand): """ @@ -48,20 +46,9 @@ def handle(self, **options): if exercise_id: exercise = Exercise.objects.get(pk=exercise_id) - self.handle_cache(exercise, force) + cache_exercise(exercise, force, self.stdout.write) return for exercise in Exercise.with_translations.all(): - self.handle_cache(exercise, force) - - def handle_cache(self, exercise: Exercise, force: bool): - if force: - self.stdout.write(f'Force updating cache for exercise base {exercise.uuid}') - else: - self.stdout.write(f'Warming cache for exercise base {exercise.uuid}') - - if force: - reset_exercise_api_cache(exercise.uuid) + cache_exercise(exercise, force, self.stdout.write) - serializer = ExerciseInfoSerializer(exercise) - serializer.data diff --git a/wger/exercises/tasks.py b/wger/exercises/tasks.py index de5d25683..6113f1668 100644 --- a/wger/exercises/tasks.py +++ b/wger/exercises/tasks.py @@ -35,6 +35,7 @@ sync_licenses, sync_muscles, ) +from wger.exercises.cache import cache_api_exercises logger = logging.getLogger(__name__) @@ -70,9 +71,18 @@ def sync_videos_task(): download_exercise_videos(logger.info) +@app.task +def cache_api_exercises_task(): + """ + Fetches all exercises from database and caches them. + """ + force = settings.WGER_SETTINGS["CACHE_API_EXERCISES_CELERY_FORCE_UPDATE"] + cache_api_exercises(logger.info, force) + + @app.on_after_finalize.connect def setup_periodic_tasks(sender, **kwargs): - if settings.WGER_SETTINGS['SYNC_EXERCISES_CELERY']: + if settings.WGER_SETTINGS["SYNC_EXERCISES_CELERY"]: sender.add_periodic_task( crontab( hour=str(random.randint(0, 23)), @@ -80,10 +90,10 @@ def setup_periodic_tasks(sender, **kwargs): day_of_week=str(random.randint(0, 6)), ), sync_exercises_task.s(), - name='Sync exercises', + name="Sync exercises", ) - if settings.WGER_SETTINGS['SYNC_EXERCISE_IMAGES_CELERY']: + if settings.WGER_SETTINGS["SYNC_EXERCISE_IMAGES_CELERY"]: sender.add_periodic_task( crontab( hour=str(random.randint(0, 23)), @@ -91,10 +101,10 @@ def setup_periodic_tasks(sender, **kwargs): day_of_week=str(random.randint(0, 6)), ), sync_images_task.s(), - name='Sync exercise images', + name="Sync exercise images", ) - if settings.WGER_SETTINGS['SYNC_EXERCISE_VIDEOS_CELERY']: + if settings.WGER_SETTINGS["SYNC_EXERCISE_VIDEOS_CELERY"]: sender.add_periodic_task( crontab( hour=str(random.randint(0, 23)), @@ -102,5 +112,16 @@ def setup_periodic_tasks(sender, **kwargs): day_of_week=str(random.randint(0, 6)), ), sync_videos_task.s(), - name='Sync exercise videos', + name="Sync exercise videos", + ) + + if settings.WGER_SETTINGS["CACHE_API_EXERCISES_CELERY"]: + sender.add_periodic_task( + crontab( + hour=str(random.randint(0, 23)), + minute=str(random.randint(0, 59)), + day_of_week=str(random.randint(0, 6)), + ), + cache_api_exercises_task.s(), + name="Cache API exercises", ) diff --git a/wger/settings_global.py b/wger/settings_global.py index 9041763c1..59d6b814a 100644 --- a/wger/settings_global.py +++ b/wger/settings_global.py @@ -549,6 +549,8 @@ 'SYNC_EXERCISE_VIDEOS_CELERY': False, 'SYNC_INGREDIENTS_CELERY': False, 'SYNC_OFF_DAILY_DELTA_CELERY': False, + 'CACHE_API_EXERCISES_CELERY': True, + 'CACHE_API_EXERCISES_CELERY_FORCE_UPDATE': True, 'TWITTER': False, 'MASTODON': 'https://fosstodon.org/@wger', 'USE_CELERY': False, From 03dcc6a9c0f356a09355b7015e47743f0abfb357 Mon Sep 17 00:00:00 2001 From: Bonicki Wojciech Date: Sun, 25 May 2025 11:25:06 +0200 Subject: [PATCH 2/4] unittest added for new wger/exercises/cache.py file --- wger/exercises/tests/test_cache.py | 50 ++++++++++++++++++++++++++++++ wger/settings_global.py | 4 +-- 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 wger/exercises/tests/test_cache.py diff --git a/wger/exercises/tests/test_cache.py b/wger/exercises/tests/test_cache.py new file mode 100644 index 000000000..3ff8fd89c --- /dev/null +++ b/wger/exercises/tests/test_cache.py @@ -0,0 +1,50 @@ +from unittest.mock import patch, MagicMock +from wger.core.tests.base_testcase import WgerTestCase +from wger.exercises.models import Exercise +from wger.exercises.cache import cache_exercise, cache_api_exercises + + +class TestCacheExercise(WgerTestCase): + + @patch('wger.exercises.cache.reset_exercise_api_cache') + @patch('wger.exercises.cache.ExerciseInfoSerializer') + def test_cache_exercise_force_true(self, mock_serializer, mock_reset_cache): + exercise = Exercise.objects.first() + output = [] + + serializer_instance = MagicMock() + serializer_instance.data = {'mocked': True} + mock_serializer.return_value = serializer_instance + + cache_exercise(exercise, force=True, print_fn=output.append) + + mock_reset_cache.assert_called_once_with(exercise.uuid) + mock_serializer.assert_called_once_with(exercise) + self.assertTrue(any("Force updating cache" in msg for msg in output)) + + @patch('wger.exercises.cache.reset_exercise_api_cache') + @patch('wger.exercises.cache.ExerciseInfoSerializer') + def test_cache_exercise_force_false(self, mock_serializer, mock_reset_cache): + exercise = Exercise.objects.first() + output = [] + + serializer_instance = MagicMock() + serializer_instance.data = {'mocked': True} + mock_serializer.return_value = serializer_instance + + cache_exercise(exercise, force=False, print_fn=output.append) + + mock_reset_cache.assert_not_called() + mock_serializer.assert_called_once_with(exercise) + self.assertTrue(any("Warming cache" in msg for msg in output)) + + @patch('wger.exercises.cache.cache_exercise') + def test_cache_api_exercises_calls_all(self, mock_cache_exercise): + output = [] + called_exercises = [] + def fake_cache(exercise, force, print_fn, style_fn): + called_exercises.append(exercise) + mock_cache_exercise.side_effect = fake_cache + cache_api_exercises(print_fn=output.append, force=True) + + self.assertTrue(len(called_exercises) > 0) \ No newline at end of file diff --git a/wger/settings_global.py b/wger/settings_global.py index 59d6b814a..1898d42b4 100644 --- a/wger/settings_global.py +++ b/wger/settings_global.py @@ -549,8 +549,8 @@ 'SYNC_EXERCISE_VIDEOS_CELERY': False, 'SYNC_INGREDIENTS_CELERY': False, 'SYNC_OFF_DAILY_DELTA_CELERY': False, - 'CACHE_API_EXERCISES_CELERY': True, - 'CACHE_API_EXERCISES_CELERY_FORCE_UPDATE': True, + 'CACHE_API_EXERCISES_CELERY': False, + 'CACHE_API_EXERCISES_CELERY_FORCE_UPDATE': False, 'TWITTER': False, 'MASTODON': 'https://fosstodon.org/@wger', 'USE_CELERY': False, From 70903b383822d1fdc404d7bcb9dfb9b7c16e6af5 Mon Sep 17 00:00:00 2001 From: Bonicki Wojciech Date: Sun, 25 May 2025 11:29:28 +0200 Subject: [PATCH 3/4] ruff and isort commands --- wger/exercises/cache.py | 24 ++++++++++--------- .../commands/warmup-exercise-api-cache.py | 4 ++-- wger/exercises/tasks.py | 20 ++++++++-------- wger/exercises/tests/test_cache.py | 22 ++++++++++++----- 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/wger/exercises/cache.py b/wger/exercises/cache.py index deda67bd3..cd1fc70a7 100644 --- a/wger/exercises/cache.py +++ b/wger/exercises/cache.py @@ -12,24 +12,26 @@ # # You should have received a copy of the GNU Affero General Public License -from typing import Callable # Standard Library -from wger.exercises.models import ( - Exercise, -) -from wger.utils.cache import reset_exercise_api_cache +from typing import Callable + +# wger from wger.exercises.api.serializers import ExerciseInfoSerializer +from wger.exercises.models import Exercise +from wger.utils.cache import reset_exercise_api_cache -def cache_exercise(exercise: Exercise, force=False, print_fn: Callable=print, style_fn: Callable=lambda x: x): +def cache_exercise( + exercise: Exercise, force=False, print_fn: Callable = print, style_fn: Callable = lambda x: x +): """ Caches a provided exercise. """ if force: - print_fn(f"Force updating cache for exercise base {exercise.uuid}") + print_fn(f'Force updating cache for exercise base {exercise.uuid}') reset_exercise_api_cache(exercise.uuid) else: - print_fn(f"Warming cache for exercise base {exercise.uuid}") + print_fn(f'Warming cache for exercise base {exercise.uuid}') serializer = ExerciseInfoSerializer(exercise) serializer.data @@ -38,9 +40,9 @@ def cache_exercise(exercise: Exercise, force=False, print_fn: Callable=print, st def cache_api_exercises( print_fn: Callable, force: bool, - style_fn: Callable=lambda x: x, + style_fn: Callable = lambda x: x, ): - print_fn("*** Caching API exercises ***") + print_fn('*** Caching API exercises ***') for exercise in Exercise.with_translations.all(): cache_exercise(exercise, force, print_fn, style_fn) - print_fn(style_fn("Exercises cached!\n")) + print_fn(style_fn('Exercises cached!\n')) diff --git a/wger/exercises/management/commands/warmup-exercise-api-cache.py b/wger/exercises/management/commands/warmup-exercise-api-cache.py index a73200cb8..fd5a7d96b 100644 --- a/wger/exercises/management/commands/warmup-exercise-api-cache.py +++ b/wger/exercises/management/commands/warmup-exercise-api-cache.py @@ -16,8 +16,9 @@ from django.core.management.base import BaseCommand # wger -from wger.exercises.models import Exercise from wger.exercises.cache import cache_exercise +from wger.exercises.models import Exercise + class Command(BaseCommand): """ @@ -51,4 +52,3 @@ def handle(self, **options): for exercise in Exercise.with_translations.all(): cache_exercise(exercise, force, self.stdout.write) - diff --git a/wger/exercises/tasks.py b/wger/exercises/tasks.py index 6113f1668..2659bc9f1 100644 --- a/wger/exercises/tasks.py +++ b/wger/exercises/tasks.py @@ -24,6 +24,7 @@ # wger from wger.celery_configuration import app +from wger.exercises.cache import cache_api_exercises from wger.exercises.sync import ( download_exercise_images, download_exercise_videos, @@ -35,7 +36,6 @@ sync_licenses, sync_muscles, ) -from wger.exercises.cache import cache_api_exercises logger = logging.getLogger(__name__) @@ -76,13 +76,13 @@ def cache_api_exercises_task(): """ Fetches all exercises from database and caches them. """ - force = settings.WGER_SETTINGS["CACHE_API_EXERCISES_CELERY_FORCE_UPDATE"] + force = settings.WGER_SETTINGS['CACHE_API_EXERCISES_CELERY_FORCE_UPDATE'] cache_api_exercises(logger.info, force) @app.on_after_finalize.connect def setup_periodic_tasks(sender, **kwargs): - if settings.WGER_SETTINGS["SYNC_EXERCISES_CELERY"]: + if settings.WGER_SETTINGS['SYNC_EXERCISES_CELERY']: sender.add_periodic_task( crontab( hour=str(random.randint(0, 23)), @@ -90,10 +90,10 @@ def setup_periodic_tasks(sender, **kwargs): day_of_week=str(random.randint(0, 6)), ), sync_exercises_task.s(), - name="Sync exercises", + name='Sync exercises', ) - if settings.WGER_SETTINGS["SYNC_EXERCISE_IMAGES_CELERY"]: + if settings.WGER_SETTINGS['SYNC_EXERCISE_IMAGES_CELERY']: sender.add_periodic_task( crontab( hour=str(random.randint(0, 23)), @@ -101,10 +101,10 @@ def setup_periodic_tasks(sender, **kwargs): day_of_week=str(random.randint(0, 6)), ), sync_images_task.s(), - name="Sync exercise images", + name='Sync exercise images', ) - if settings.WGER_SETTINGS["SYNC_EXERCISE_VIDEOS_CELERY"]: + if settings.WGER_SETTINGS['SYNC_EXERCISE_VIDEOS_CELERY']: sender.add_periodic_task( crontab( hour=str(random.randint(0, 23)), @@ -112,10 +112,10 @@ def setup_periodic_tasks(sender, **kwargs): day_of_week=str(random.randint(0, 6)), ), sync_videos_task.s(), - name="Sync exercise videos", + name='Sync exercise videos', ) - if settings.WGER_SETTINGS["CACHE_API_EXERCISES_CELERY"]: + if settings.WGER_SETTINGS['CACHE_API_EXERCISES_CELERY']: sender.add_periodic_task( crontab( hour=str(random.randint(0, 23)), @@ -123,5 +123,5 @@ def setup_periodic_tasks(sender, **kwargs): day_of_week=str(random.randint(0, 6)), ), cache_api_exercises_task.s(), - name="Cache API exercises", + name='Cache API exercises', ) diff --git a/wger/exercises/tests/test_cache.py b/wger/exercises/tests/test_cache.py index 3ff8fd89c..7903a6ae9 100644 --- a/wger/exercises/tests/test_cache.py +++ b/wger/exercises/tests/test_cache.py @@ -1,11 +1,19 @@ -from unittest.mock import patch, MagicMock +# Standard Library +from unittest.mock import ( + MagicMock, + patch, +) + +# wger from wger.core.tests.base_testcase import WgerTestCase +from wger.exercises.cache import ( + cache_api_exercises, + cache_exercise, +) from wger.exercises.models import Exercise -from wger.exercises.cache import cache_exercise, cache_api_exercises class TestCacheExercise(WgerTestCase): - @patch('wger.exercises.cache.reset_exercise_api_cache') @patch('wger.exercises.cache.ExerciseInfoSerializer') def test_cache_exercise_force_true(self, mock_serializer, mock_reset_cache): @@ -20,7 +28,7 @@ def test_cache_exercise_force_true(self, mock_serializer, mock_reset_cache): mock_reset_cache.assert_called_once_with(exercise.uuid) mock_serializer.assert_called_once_with(exercise) - self.assertTrue(any("Force updating cache" in msg for msg in output)) + self.assertTrue(any('Force updating cache' in msg for msg in output)) @patch('wger.exercises.cache.reset_exercise_api_cache') @patch('wger.exercises.cache.ExerciseInfoSerializer') @@ -36,15 +44,17 @@ def test_cache_exercise_force_false(self, mock_serializer, mock_reset_cache): mock_reset_cache.assert_not_called() mock_serializer.assert_called_once_with(exercise) - self.assertTrue(any("Warming cache" in msg for msg in output)) + self.assertTrue(any('Warming cache' in msg for msg in output)) @patch('wger.exercises.cache.cache_exercise') def test_cache_api_exercises_calls_all(self, mock_cache_exercise): output = [] called_exercises = [] + def fake_cache(exercise, force, print_fn, style_fn): called_exercises.append(exercise) + mock_cache_exercise.side_effect = fake_cache cache_api_exercises(print_fn=output.append, force=True) - self.assertTrue(len(called_exercises) > 0) \ No newline at end of file + self.assertTrue(len(called_exercises) > 0) From bf2a32f788c4be4adfca685c9150877d469bbe68 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 30 Sep 2025 12:26:44 +0200 Subject: [PATCH 4/4] Adds exercise cache settings to docker --- extras/docker/production/settings.py | 2 ++ wger/exercises/cache.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extras/docker/production/settings.py b/extras/docker/production/settings.py index b3bc64862..39b07c1cf 100644 --- a/extras/docker/production/settings.py +++ b/extras/docker/production/settings.py @@ -101,6 +101,8 @@ WGER_SETTINGS["SYNC_OFF_DAILY_DELTA_CELERY"] = env.bool("SYNC_OFF_DAILY_DELTA_CELERY", False) WGER_SETTINGS["USE_RECAPTCHA"] = env.bool("USE_RECAPTCHA", False) WGER_SETTINGS["USE_CELERY"] = env.bool("USE_CELERY", False) +WGER_SETTINGS["CACHE_API_EXERCISES_CELERY"] = env.bool("CACHE_API_EXERCISES_CELERY", False) +WGER_SETTINGS["CACHE_API_EXERCISES_CELERY_FORCE_UPDATE"] = env.bool("CACHE_API_EXERCISES_CELERY_FORCE_UPDATE", False) # # Auth Proxy Authentication diff --git a/wger/exercises/cache.py b/wger/exercises/cache.py index cd1fc70a7..6c8ab26f6 100644 --- a/wger/exercises/cache.py +++ b/wger/exercises/cache.py @@ -28,10 +28,10 @@ def cache_exercise( Caches a provided exercise. """ if force: - print_fn(f'Force updating cache for exercise base {exercise.uuid}') + print_fn(f'Force updating cache for exercise {exercise.uuid}') reset_exercise_api_cache(exercise.uuid) else: - print_fn(f'Warming cache for exercise base {exercise.uuid}') + print_fn(f'Warming cache for exercise {exercise.uuid}') serializer = ExerciseInfoSerializer(exercise) serializer.data