Skip to content

Commit

Permalink
Merge pull request #521 from openedx/saleem-latif/ENT-9622
Browse files Browse the repository at this point in the history
ENT-9622: Added caching for API endpoints related to advanced analytics.
  • Loading branch information
saleem-latif authored Oct 15, 2024
2 parents c8dd881 + 49c0048 commit 8d04c12
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Unreleased

=========================

[9.6.0] - 2024-10-14
---------------------
* feat: Added caching for API endpoints related to advanced analytics.

[9.5.2] - 2024-10-14
---------------------
* feat: Transform extensions_requested field to return 0 if None
Expand Down
2 changes: 1 addition & 1 deletion enterprise_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Enterprise data api application. This Django app exposes API endpoints used by enterprises.
"""

__version__ = "9.5.2"
__version__ = "9.6.0"
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from datetime import date
from uuid import UUID

from enterprise_data.cache.decorators import cache_it

from ..queries import FactEngagementAdminDashQueries
from ..utils import run_query
from .base import BaseTable
Expand All @@ -15,6 +17,7 @@ class FactEngagementAdminDashTable(BaseTable):
"""
queries = FactEngagementAdminDashQueries()

@cache_it()
def get_learning_hours_and_daily_sessions(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the learning hours and daily sessions for the given enterprise customer.
Expand All @@ -40,6 +43,7 @@ def get_learning_hours_and_daily_sessions(self, enterprise_customer_uuid: UUID,

return tuple(results[0])

@cache_it()
def get_engagement_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the total number of engagements for the given enterprise customer.
Expand All @@ -64,6 +68,7 @@ def get_engagement_count(self, enterprise_customer_uuid: UUID, start_date: date,
return 0
return results[0][0]

@cache_it()
def get_all_engagements(
self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
):
Expand Down Expand Up @@ -92,6 +97,7 @@ def get_all_engagements(
as_dict=True,
)

@cache_it()
def get_top_courses_by_engagement(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the top courses by user engagement for the given enterprise customer.
Expand All @@ -114,6 +120,7 @@ def get_top_courses_by_engagement(self, enterprise_customer_uuid: UUID, start_da
as_dict=True,
)

@cache_it()
def get_top_subjects_by_engagement(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the top subjects by user engagement for the given enterprise customer.
Expand All @@ -136,6 +143,7 @@ def get_top_subjects_by_engagement(self, enterprise_customer_uuid: UUID, start_d
as_dict=True,
)

@cache_it()
def get_engagement_time_series_data(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the engagement time series data.
Expand All @@ -158,6 +166,7 @@ def get_engagement_time_series_data(self, enterprise_customer_uuid: UUID, start_
as_dict=True,
)

@cache_it()
def _get_engagement_data_for_leaderboard(
self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
):
Expand Down Expand Up @@ -189,6 +198,7 @@ def _get_engagement_data_for_leaderboard(
as_dict=True,
)

@cache_it()
def _get_completion_data_for_leaderboard_query(
self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, email_list: list
):
Expand Down Expand Up @@ -257,6 +267,7 @@ def get_all_leaderboard_data(

return list(engagement_data_dict.values())

@cache_it()
def get_leaderboard_data_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the total number of leaderboard records for the given enterprise customer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from datetime import date, datetime
from uuid import UUID

from enterprise_data.cache.decorators import cache_it

from ..queries import FactEnrollmentAdminDashQueries
from ..utils import run_query
from .base import BaseTable
Expand All @@ -15,6 +17,7 @@ class FactEnrollmentAdminDashTable(BaseTable):
"""
queries = FactEnrollmentAdminDashQueries()

@cache_it()
def get_enrollment_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the total number of enrollments for the given enterprise customer.
Expand All @@ -39,6 +42,7 @@ def get_enrollment_count(self, enterprise_customer_uuid: UUID, start_date: date,
return 0
return int(results[0][0] or 0)

@cache_it()
def get_all_enrollments(
self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
):
Expand Down Expand Up @@ -67,6 +71,7 @@ def get_all_enrollments(
as_dict=True,
)

@cache_it()
def get_enrollment_date_range(self, enterprise_customer_uuid: UUID):
"""
Get the enrollment date range for the given enterprise customer.
Expand Down Expand Up @@ -94,6 +99,7 @@ def get_enrollment_date_range(self, enterprise_customer_uuid: UUID):

return min_date, max_date

@cache_it()
def get_enrollment_and_course_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the enrollment and course count for the given enterprise customer.
Expand All @@ -118,6 +124,7 @@ def get_enrollment_and_course_count(self, enterprise_customer_uuid: UUID, start_
return 0, 0
return tuple(results[0])

@cache_it()
def get_completion_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the completion count for the given enterprise customer.
Expand All @@ -143,6 +150,7 @@ def get_completion_count(self, enterprise_customer_uuid: UUID, start_date: date,

return int(results[0][0] or 0)

@cache_it()
def get_top_courses_by_enrollments(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the top courses enrollments for the given enterprise customer.
Expand All @@ -165,6 +173,7 @@ def get_top_courses_by_enrollments(self, enterprise_customer_uuid: UUID, start_d
as_dict=True,
)

@cache_it()
def get_top_subjects_by_enrollments(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the top subjects by enrollments for the given enterprise customer.
Expand All @@ -187,6 +196,7 @@ def get_top_subjects_by_enrollments(self, enterprise_customer_uuid: UUID, start_
as_dict=True,
)

@cache_it()
def get_enrolment_time_series_data(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the enrollment time series data for the given enterprise customer.
Expand All @@ -209,6 +219,7 @@ def get_enrolment_time_series_data(self, enterprise_customer_uuid: UUID, start_d
as_dict=True,
)

@cache_it()
def get_all_completions(
self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
):
Expand Down Expand Up @@ -237,6 +248,7 @@ def get_all_completions(
as_dict=True,
)

@cache_it()
def get_top_courses_by_completions(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the top courses by completion for the given enterprise customer.
Expand All @@ -259,6 +271,7 @@ def get_top_courses_by_completions(self, enterprise_customer_uuid: UUID, start_d
as_dict=True,
)

@cache_it()
def get_top_subjects_by_completions(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the top subjects by completions for the given enterprise customer.
Expand All @@ -281,6 +294,7 @@ def get_top_subjects_by_completions(self, enterprise_customer_uuid: UUID, start_
as_dict=True,
)

@cache_it()
def get_completions_time_series_data(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the completions time series data for the given enterprise customer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)
from enterprise_data.admin_analytics.database.tables.base import BaseTable
from enterprise_data.admin_analytics.database.utils import run_query
from enterprise_data.cache.decorators import cache_it


class SkillsDailyRollupAdminDashTable(BaseTable):
Expand All @@ -17,6 +18,7 @@ class SkillsDailyRollupAdminDashTable(BaseTable):
"""
queries = SkillsDailyRollupAdminDashQueries()

@cache_it()
def get_top_skills(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
"""
Get the top skills for the given enterprise customer.
Expand All @@ -39,6 +41,7 @@ def get_top_skills(self, enterprise_customer_uuid: UUID, start_date: date, end_d
as_dict=True,
)

@cache_it()
def get_top_skills_by_enrollment(
self,
enterprise_customer_uuid: UUID,
Expand Down Expand Up @@ -66,6 +69,7 @@ def get_top_skills_by_enrollment(
as_dict=True,
)

@cache_it()
def get_top_skills_by_completion(
self,
enterprise_customer_uuid: UUID,
Expand Down
59 changes: 59 additions & 0 deletions enterprise_data/cache/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Caching related utility classes and functions.
"""
import hashlib

from edx_django_utils.cache import TieredCache

DEFAULT_TIMEOUT = 60 * 60 # 1 hour


def get_key(*args, **kwargs):
"""
Get MD5 encoded cache key for given positional and keyword arguments.
MD5 encrytion is applied to a key that is generated by concatenating the positional and keyword arguments.
Following is the format of the generated key from arguments before applying the MD5 encryption.
arg1__arg2__key1:value1__key2:value2 ...
Example:
>>> get_key('ecommerce', site_domain='example.com', resource='catalogs')
1892cd85a30b8fc9180369c17b472c38
>>> # The generated key for the above call before applying MD5 encryption will be as follows
>>> # "ecommerce__site_domain:example.com__resource:catalogs"
Arguments:
*args: Arguments that need to be present in cache key.
**kwargs: Key word arguments that need to be present in cache key.
Returns:
(str): An MD5 encoded key uniquely identified by the key word arguments.
"""
key = '{}__{}'.format(
'__'.join(map(str, args)),
'__'.join(['{}:{}'.format(item, str(value)) for item, value in kwargs.items()])
)

return hashlib.md5(key.encode('utf-8')).hexdigest()


def get(key):
"""
Get value from cache for given key.
Returns:
(CachedResponse): CachedResponse object.
"""
return TieredCache.get_cached_response(key)


def set(key, value, timeout=DEFAULT_TIMEOUT): # pylint: disable=redefined-builtin
"""
Set value in cache for given key.
Arguments:
key (str): Cache key.
value (object): Value to be stored in cache.
timeout (int): Cache timeout in seconds.
"""
TieredCache.set_all_tiers(key, value, django_cache_timeout=timeout)
42 changes: 42 additions & 0 deletions enterprise_data/cache/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Decorators for caching the result of a function.
"""
from functools import wraps
from logging import getLogger

from enterprise_data import cache

LOGGER = getLogger(__name__)


def cache_it(timeout=cache.DEFAULT_TIMEOUT):
"""
Function to return the decorator to cache the result of a method.
Note: This decorator will only work for class methods.
Arguments:
timeout (int): Cache timeout in seconds.
Returns:
(function): Decorator function.
"""

def inner_decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
"""
Wrapper function to cache the result of the function.
"""
cache_key = cache.get_key(func.__name__, *args, **kwargs)
cached_response = cache.get(cache_key)
if cached_response.is_found:
LOGGER.info("[ANALYTICS]: Cache hit for key: (%s)", (func.__name__, args, kwargs))
return cached_response.value

LOGGER.info("[ANALYTICS]: Cache miss for key: (%s)", (func.__name__, args, kwargs))
result = func(self, *args, **kwargs)
cache.set(cache_key, result, timeout=timeout)
return result
return wrapper
return inner_decorator

0 comments on commit 8d04c12

Please sign in to comment.