feat: introduce AP scheduler for polling vault

This commit is contained in:
ramvignesh-b
2026-04-17 23:37:48 +05:30
parent 3d764703dd
commit 1e7a1c15c9
6 changed files with 150 additions and 12 deletions
+7 -6
View File
@@ -25,7 +25,6 @@ env_file = os.path.join(BASE_DIR.parent, ".env")
if os.path.exists(env_file): if os.path.exists(env_file):
environ.Env.read_env(env_file, overwrite=False) environ.Env.read_env(env_file, overwrite=False)
SSL_ENABLED = env("SSL_ENABLED") == "true" SSL_ENABLED = env("SSL_ENABLED") == "true"
FRONTEND_URL = f"https://{env('FRONTEND_DOMAIN')}" if SSL_ENABLED else f"http://{env('FRONTEND_DOMAIN')}" FRONTEND_URL = f"https://{env('FRONTEND_DOMAIN')}" if SSL_ENABLED else f"http://{env('FRONTEND_DOMAIN')}"
if env("FRONTEND_PORT"): if env("FRONTEND_PORT"):
@@ -42,10 +41,10 @@ DEBUG = env("DEBUG")
ALLOWED_HOSTS = [env("FRONTEND_DOMAIN")] ALLOWED_HOSTS = [env("FRONTEND_DOMAIN")]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"django_apscheduler",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
@@ -73,7 +72,6 @@ ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application" WSGI_APPLICATION = "config.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases # https://docs.djangoproject.com/en/6.0/ref/settings/#databases
@@ -125,7 +123,6 @@ EMAIL_PORT = env("EMAIL_PORT")
EMAIL_USE_TLS = not DEBUG EMAIL_USE_TLS = not DEBUG
FROM_EMAIL = env("FROM_EMAIL") FROM_EMAIL = env("FROM_EMAIL")
# Password validation # Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
@@ -144,7 +141,6 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/ # https://docs.djangoproject.com/en/6.0/topics/i18n/
@@ -156,10 +152,15 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/ # https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = "static/" STATIC_URL = "static/"
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
LOGGING = {
"version": 1,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {"letters": {"handlers": ["console"], "level": "INFO"}},
}
+14
View File
@@ -1,5 +1,19 @@
import os
from django.apps import AppConfig from django.apps import AppConfig
class LettersConfig(AppConfig): class LettersConfig(AppConfig):
name = "letters" name = "letters"
def ready(self):
"""
Start the scheduler only when the server is starting.
NOTE: If we don't check for RUN_MAIN, the scheduler triggers for all django operations (migration, test etc.)
"""
if not (os.environ.get("RUN_MAIN") == "true" or os.environ.get("WERKZEUG_RUN_MAIN") == "true"):
return
from .tasks import start_scheduler
start_scheduler()
+21 -4
View File
@@ -1,6 +1,7 @@
import logging import logging
from datetime import UTC, datetime from datetime import UTC, datetime
from apscheduler.schedulers.background import BackgroundScheduler
from django.core.mail import send_mail from django.core.mail import send_mail
from config import settings from config import settings
@@ -13,8 +14,7 @@ def get_vault_letters_to_notify():
""" """
Identifies the vault letters that have been recently unlocked and not notified Identifies the vault letters that have been recently unlocked and not notified
""" """
letters = Letter.objects.filter(unlock_at__lt=datetime.now(UTC), notified_at=None) return Letter.objects.filter(unlock_at__lt=datetime.now(UTC), notified_at=None)
return letters
def notify_unlocked_letter(letter): def notify_unlocked_letter(letter):
@@ -31,8 +31,25 @@ def notify_unlocked_letter(letter):
def vault_unlock_notification_polling_scheduler(): def vault_unlock_notification_polling_scheduler():
logger.info("Starting vault_unlock_notification_polling_scheduler") """
Orchestrates the vault polling logic.
"""
letters_to_notify = get_vault_letters_to_notify() letters_to_notify = get_vault_letters_to_notify()
print("letters_to_notify", letters_to_notify)
for letter in letters_to_notify: for letter in letters_to_notify:
notify_unlocked_letter(letter) notify_unlocked_letter(letter)
def start_scheduler():
"""
Starts the background scheduler for polling and notifying vault letters.
"""
logger.info("Starting vault polling scheduler...")
scheduler = BackgroundScheduler()
scheduler.add_job(
vault_unlock_notification_polling_scheduler,
trigger="interval",
minutes=1,
id="letter_polling",
replace_existing=True,
)
scheduler.start()
+2 -2
View File
@@ -327,7 +327,7 @@ class LetterTaskTest(TestCase):
letter_to_notify1 = Letter.objects.create( letter_to_notify1 = Letter.objects.create(
user=self.user, type="VAULT", status="SEALED", unlock_at=datetime.now(UTC), notified_at=None user=self.user, type="VAULT", status="SEALED", unlock_at=datetime.now(UTC), notified_at=None
) )
with patch("tasks.send_mail") as mock_send_mail: with patch("letters.tasks.send_mail") as mock_send_mail:
notify_unlocked_letter(letter_to_notify1) notify_unlocked_letter(letter_to_notify1)
mock_send_mail.assert_called_with( mock_send_mail.assert_called_with(
@@ -342,7 +342,7 @@ class LetterTaskTest(TestCase):
letter_to_notify2 = Letter.objects.create( letter_to_notify2 = Letter.objects.create(
user=self.user, type="VAULT", status="SEALED", unlock_at=datetime.now(UTC), notified_at=None user=self.user, type="VAULT", status="SEALED", unlock_at=datetime.now(UTC), notified_at=None
) )
with patch("tasks.send_mail") as mock_send_mail: with patch("letters.tasks.send_mail") as mock_send_mail:
mock_send_mail.side_effect = Exception() mock_send_mail.side_effect = Exception()
notify_unlocked_letter(letter_to_notify2) notify_unlocked_letter(letter_to_notify2)
+3
View File
@@ -5,12 +5,15 @@ description = "Django Rest Framework for handling requests for Pi Ku app"
readme = "README.md" readme = "README.md"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [
"apscheduler>=3.11.2",
"django>=6.0.4", "django>=6.0.4",
"django-apscheduler>=0.7.0",
"django-cors-headers>=4.9.0", "django-cors-headers>=4.9.0",
"django-environ>=0.13.0", "django-environ>=0.13.0",
"django-extensions>=4.1", "django-extensions>=4.1",
"djangorestframework>=3.17.1", "djangorestframework>=3.17.1",
"djangorestframework-simplejwt>=5.5.1", "djangorestframework-simplejwt>=5.5.1",
"djangorestframework-stubs>=3.16.9",
"freezegun>=1.5.5", "freezegun>=1.5.5",
"psycopg2-binary>=2.9.11", "psycopg2-binary>=2.9.11",
"pyopenssl>=26.0.0", "pyopenssl>=26.0.0",
+103
View File
@@ -2,6 +2,18 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.14" requires-python = ">=3.14"
[[package]]
name = "apscheduler"
version = "3.11.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzlocal" },
]
sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
]
[[package]] [[package]]
name = "asgiref" name = "asgiref"
version = "3.11.1" version = "3.11.1"
@@ -111,6 +123,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/47/3d61d611609764aa71a37f7037b870e7bfb22937366974c4fd46cada7bab/django-6.0.4-py3-none-any.whl", hash = "sha256:14359c809fc16e8f81fd2b59d7d348e4d2d799da6840b10522b6edf7b8afc1da", size = 8368342, upload-time = "2026-04-07T13:55:37.999Z" }, { url = "https://files.pythonhosted.org/packages/e9/47/3d61d611609764aa71a37f7037b870e7bfb22937366974c4fd46cada7bab/django-6.0.4-py3-none-any.whl", hash = "sha256:14359c809fc16e8f81fd2b59d7d348e4d2d799da6840b10522b6edf7b8afc1da", size = 8368342, upload-time = "2026-04-07T13:55:37.999Z" },
] ]
[[package]]
name = "django-apscheduler"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "apscheduler" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/6b/873899c2da113187b74f0cccdf4c16660e07bfbbcae72621c4758e0958bf/django_apscheduler-0.7.0.tar.gz", hash = "sha256:30d61a2ba98615922fc2c9782f84bba342ec0c5ed63384d686d71ea90a1a4318", size = 473051, upload-time = "2024-09-28T04:54:09.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/19/c3d2dea21a6afdc93689b9f769ff3694cac810e4a09c24ab423dd1613e6c/django_apscheduler-0.7.0-py3-none-any.whl", hash = "sha256:869d489775420245c9455d55e35f663c856a33ebfc996d92938f786ffb8730ce", size = 24690, upload-time = "2024-09-28T04:54:06.884Z" },
]
[[package]] [[package]]
name = "django-cors-headers" name = "django-cors-headers"
version = "4.9.0" version = "4.9.0"
@@ -145,6 +170,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" }, { url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" },
] ]
[[package]]
name = "django-stubs"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "django-stubs-ext" },
{ name = "types-pyyaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/03/b2/f0214d86180f937c8e3358ff831b20f0634d95bd77436b18861c647e15bc/django_stubs-6.0.2.tar.gz", hash = "sha256:56d43b5e3741563af0063e5b6283f908c625b0439aa06314268673699d1bdccd", size = 274742, upload-time = "2026-04-01T08:27:35.092Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/e7/8f2aaa22eac7fa18db3aca0e7b651ccf5ac79a2021bf67e75a16934a7076/django_stubs-6.0.2-py3-none-any.whl", hash = "sha256:c3bc84d80421758f3b2ad9e1358e001d719388a8eb106e67c873e606216108d4", size = 538234, upload-time = "2026-04-01T08:27:33.411Z" },
]
[[package]]
name = "django-stubs-ext"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/e0/f2e6caf627d176a51fba1ca9c34082c7ea10d3f521ff2c828532ca99fa91/django_stubs_ext-6.0.2.tar.gz", hash = "sha256:70b7b7ae837e7a6036e2facb64435550bf7cf8143c1a6e802864d4824ce6058c", size = 6751, upload-time = "2026-04-01T08:27:01.987Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/d2/9cb93cd1ef94ddc97c26c902ff75a859f5f154051fec98cf8242649b26ce/django_stubs_ext-6.0.2-py3-none-any.whl", hash = "sha256:b35bdec1995bf49765cc39fa89aa7c23f120a23d0cb0152ab7fb4e48ff7d667b", size = 10446, upload-time = "2026-04-01T08:27:00.847Z" },
]
[[package]] [[package]]
name = "djangorestframework" name = "djangorestframework"
version = "3.17.1" version = "3.17.1"
@@ -171,6 +224,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", size = 107674, upload-time = "2025-07-21T16:52:07.493Z" }, { url = "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", size = 107674, upload-time = "2025-07-21T16:52:07.493Z" },
] ]
[[package]]
name = "djangorestframework-stubs"
version = "3.16.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django-stubs" },
{ name = "types-pyyaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/84/fa0e31f763ee35152a418c2a456efdd8047a9da0f5909110147b70382191/djangorestframework_stubs-3.16.9.tar.gz", hash = "sha256:b1abb97490c90c85eabcd09b8ecbadae1b9360f21ad3021abf830227c0129697", size = 32798, upload-time = "2026-03-31T22:40:23.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/be/e53e3b89eaa30c21e036ae4d2ee88a92ef8cb43678400901748ddad870c5/djangorestframework_stubs-3.16.9-py3-none-any.whl", hash = "sha256:27b3e245d5f9c22ff6988d9e54388249f98f88608cc2b365b71e9f39dd096958", size = 57239, upload-time = "2026-03-31T22:40:22.314Z" },
]
[[package]] [[package]]
name = "freezegun" name = "freezegun"
version = "1.5.5" version = "1.5.5"
@@ -218,12 +285,15 @@ name = "piku-backend"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "apscheduler" },
{ name = "django" }, { name = "django" },
{ name = "django-apscheduler" },
{ name = "django-cors-headers" }, { name = "django-cors-headers" },
{ name = "django-environ" }, { name = "django-environ" },
{ name = "django-extensions" }, { name = "django-extensions" },
{ name = "djangorestframework" }, { name = "djangorestframework" },
{ name = "djangorestframework-simplejwt" }, { name = "djangorestframework-simplejwt" },
{ name = "djangorestframework-stubs" },
{ name = "freezegun" }, { name = "freezegun" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "pyopenssl" }, { name = "pyopenssl" },
@@ -233,12 +303,15 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "apscheduler", specifier = ">=3.11.2" },
{ name = "django", specifier = ">=6.0.4" }, { name = "django", specifier = ">=6.0.4" },
{ name = "django-apscheduler", specifier = ">=0.7.0" },
{ name = "django-cors-headers", specifier = ">=4.9.0" }, { name = "django-cors-headers", specifier = ">=4.9.0" },
{ name = "django-environ", specifier = ">=0.13.0" }, { name = "django-environ", specifier = ">=0.13.0" },
{ name = "django-extensions", specifier = ">=4.1" }, { name = "django-extensions", specifier = ">=4.1" },
{ name = "djangorestframework", specifier = ">=3.17.1" }, { name = "djangorestframework", specifier = ">=3.17.1" },
{ name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" },
{ name = "djangorestframework-stubs", specifier = ">=3.16.9" },
{ name = "freezegun", specifier = ">=1.5.5" }, { name = "freezegun", specifier = ">=1.5.5" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pyopenssl", specifier = ">=26.0.0" }, { name = "pyopenssl", specifier = ">=26.0.0" },
@@ -350,6 +423,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
] ]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20260408"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]] [[package]]
name = "tzdata" name = "tzdata"
version = "2026.1" version = "2026.1"
@@ -359,6 +450,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
] ]
[[package]]
name = "tzlocal"
version = "5.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
]
[[package]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "3.1.8" version = "3.1.8"