Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions backend/apps/common/extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Strawberry extensions."""

import json
from typing import Any

from django.conf import settings
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
from strawberry.extensions.field_extension import FieldExtension
from strawberry.types.info import Info


class CacheFieldExtension(FieldExtension):
"""Cache FieldExtension class."""

def __init__(self, cache_timeout: int | None = None, prefix: str | None = None):
"""Initialize the cache extension.

Args:
cache_timeout (int | None): The TTL for cache entries in seconds.
prefix (str | None): A prefix for the cache key.

"""
self.cache_timeout = cache_timeout or settings.GRAPHQL_RESOLVER_CACHE_TIME_SECONDS
self.prefix = prefix or settings.GRAPHQL_RESOLVER_CACHE_PREFIX

def _convert_path_to_str(self, path: Any) -> str:
"""Convert the Strawberry path linked list to a string."""
parts = []
current = path
while current:
parts.append(str(current.key))
current = getattr(current, "prev", None)
return ".".join(reversed(parts))

def generate_key(self, source: Any | None, info: Info, kwargs: dict) -> str:
"""Generate a unique cache key for a field.

Args:
source (Any | None): The source/parent object.
info (Info): The Strawberry execution info.
kwargs (dict): The resolver's arguments.

Returns:
str: The unique cache key.

"""
key_kwargs = kwargs.copy()
if source and (source_id := getattr(source, "id", None)) is not None:
key_kwargs["__source_id__"] = str(source_id)

args_str = json.dumps(key_kwargs, sort_keys=True, cls=DjangoJSONEncoder)

return f"{self.prefix}:{self._convert_path_to_str(info.path)}:{args_str}"

def resolve(self, next_: Any, source: Any, info: Info, **kwargs: Any) -> Any:
"""Wrap the resolver to provide caching."""
return cache.get_or_set(
self.generate_key(source, info, kwargs),
lambda: next_(source, info, **kwargs),
timeout=self.cache_timeout,
)
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/nodes/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
import strawberry_django

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.user import UserNode
from apps.github.models.issue import Issue

Expand All @@ -19,7 +20,7 @@
class IssueNode(strawberry.relay.Node):
"""GitHub issue node."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def author(self) -> UserNode | None:
"""Resolve author."""
return self.author
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/nodes/milestone.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
import strawberry_django

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.user import UserNode
from apps.github.models.milestone import Milestone

Expand All @@ -22,7 +23,7 @@
class MilestoneNode(strawberry.relay.Node):
"""Github Milestone Node."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def author(self) -> UserNode | None:
"""Resolve author."""
return self.author
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/nodes/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import strawberry_django
from django.db import models

from apps.common.extensions import CacheFieldExtension
from apps.github.models.organization import Organization
from apps.github.models.repository import Repository
from apps.github.models.repository_contributor import RepositoryContributor
Expand Down Expand Up @@ -39,7 +40,7 @@ class OrganizationStatsNode:
class OrganizationNode(strawberry.relay.Node):
"""GitHub organization node."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def stats(self) -> OrganizationStatsNode:
"""Resolve organization stats."""
repositories = Repository.objects.filter(organization=self)
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/nodes/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
import strawberry_django

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.user import UserNode
from apps.github.models.pull_request import PullRequest

Expand All @@ -17,7 +18,7 @@
class PullRequestNode(strawberry.relay.Node):
"""GitHub pull request node."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def author(self) -> UserNode | None:
"""Resolve author."""
return self.author
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/nodes/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
import strawberry_django

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.user import UserNode
from apps.github.models.release import Release
from apps.owasp.constants import OWASP_ORGANIZATION_NAME
Expand All @@ -20,7 +21,7 @@
class ReleaseNode(strawberry.relay.Node):
"""GitHub release node."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def author(self) -> UserNode | None:
"""Resolve author."""
return self.author
Expand Down
13 changes: 7 additions & 6 deletions backend/apps/github/api/internal/nodes/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import strawberry
import strawberry_django

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.api.internal.nodes.milestone import MilestoneNode
from apps.github.api.internal.nodes.organization import OrganizationNode
Expand Down Expand Up @@ -41,7 +42,7 @@
class RepositoryNode(strawberry.relay.Node):
"""Repository node."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def issues(self) -> list[IssueNode]:
"""Resolve recent issues."""
# TODO(arkid15r): rename this to recent_issues.
Expand All @@ -57,7 +58,7 @@ def latest_release(self) -> str:
"""Resolve latest release."""
return self.latest_release

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def organization(self) -> OrganizationNode | None:
"""Resolve organization."""
return self.organization
Expand All @@ -67,25 +68,25 @@ def owner_key(self) -> str:
"""Resolve owner key."""
return self.owner_key

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def project(
self,
) -> Annotated["ProjectNode", strawberry.lazy("apps.owasp.api.internal.nodes.project")] | None:
"""Resolve project."""
return self.project

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def recent_milestones(self, limit: int = 5) -> list[MilestoneNode]:
"""Resolve recent milestones."""
return self.recent_milestones.select_related("repository").order_by("-created_at")[:limit]

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def releases(self) -> list[ReleaseNode]:
"""Resolve recent releases."""
# TODO(arkid15r): rename this to recent_releases.
return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT]

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def top_contributors(self) -> list[RepositoryContributorNode]:
"""Resolve top contributors."""
return self.idx_top_contributors
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/nodes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
import strawberry_django

from apps.common.extensions import CacheFieldExtension
from apps.github.models.user import User
from apps.nest.api.internal.nodes.badge import BadgeNode

Expand Down Expand Up @@ -33,7 +34,7 @@ def badge_count(self) -> int:
"""Resolve badge count."""
return self.user_badges.filter(is_active=True).count()

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def badges(self) -> list[BadgeNode]:
"""Return user badges."""
user_badges = (
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/queries/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
from django.db.models import OuterRef, Subquery

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.models.issue import Issue

Expand All @@ -11,7 +12,7 @@
class IssueQuery:
"""GraphQL query class for retrieving GitHub issues."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def recent_issues(
self,
*,
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/queries/milestone.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.core.exceptions import ValidationError
from django.db.models import OuterRef, Subquery

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.milestone import MilestoneNode
from apps.github.models.milestone import Milestone

Expand All @@ -12,7 +13,7 @@
class MilestoneQuery:
"""Github Milestone Queries."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def recent_milestones(
self,
*,
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/queries/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import strawberry

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.organization import OrganizationNode
from apps.github.models.organization import Organization

Expand All @@ -10,7 +11,7 @@
class OrganizationQuery:
"""Organization queries."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def organization(
self,
*,
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/queries/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
from django.db.models import OuterRef, Subquery

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.pull_request import PullRequestNode
from apps.github.models.pull_request import PullRequest
from apps.owasp.models.project import Project
Expand All @@ -12,7 +13,7 @@
class PullRequestQuery:
"""Pull request queries."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def recent_pull_requests(
self,
*,
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/github/api/internal/queries/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import strawberry
from django.db.models import OuterRef, Subquery

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.release import ReleaseNode
from apps.github.models.release import Release

Expand All @@ -11,7 +12,7 @@
class ReleaseQuery:
"""GraphQL query class for retrieving recent GitHub releases."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def recent_releases(
self,
*,
Expand Down
5 changes: 3 additions & 2 deletions backend/apps/github/api/internal/queries/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import strawberry

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.repository import RepositoryNode
from apps.github.models.repository import Repository

Expand All @@ -10,7 +11,7 @@
class RepositoryQuery:
"""Repository queries."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def repository(
self,
organization_key: str,
Expand All @@ -34,7 +35,7 @@ def repository(
except Repository.DoesNotExist:
return None

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def repositories(
self,
organization: str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import strawberry

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.repository_contributor import RepositoryContributorNode
from apps.github.models.repository_contributor import RepositoryContributor

Expand All @@ -10,7 +11,7 @@
class RepositoryContributorQuery:
"""Repository contributor queries."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def top_contributors(
self,
*,
Expand Down
5 changes: 3 additions & 2 deletions backend/apps/github/api/internal/queries/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import strawberry

from apps.common.extensions import CacheFieldExtension
from apps.github.api.internal.nodes.repository import RepositoryNode
from apps.github.api.internal.nodes.user import UserNode
from apps.github.models.repository_contributor import RepositoryContributor
Expand All @@ -12,7 +13,7 @@
class UserQuery:
"""User queries."""

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def top_contributed_repositories(
self,
login: str,
Expand All @@ -36,7 +37,7 @@ def top_contributed_repositories(
.order_by("-contributions_count")
]

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def user(
self,
login: str,
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/mentorship/api/internal/nodes/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import strawberry

from apps.common.extensions import CacheFieldExtension
from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum
from apps.mentorship.api.internal.nodes.mentor import MentorNode
from apps.mentorship.api.internal.nodes.program import ProgramNode
Expand All @@ -25,7 +26,7 @@ class ModuleNode:
started_at: datetime
tags: list[str] | None = None

@strawberry.field
@strawberry.field(extensions=[CacheFieldExtension()])
def mentors(self) -> list[MentorNode]:
"""Get the list of mentors for this module."""
return self.mentors.all()
Expand Down
Loading