diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4ddc0e08fe..51bdfac774 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,6 +37,20 @@ repos:
- --sequence=4
exclude: (.github|pnpm-lock.yaml)
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.15.0
+ hooks:
+ - id: mypy
+ additional_dependencies:
+ - types-jsonschema
+ - types-lxml
+ - types-python-dateutil
+ - types-PyYAML
+ - types-requests
+ args:
+ - --config-file
+ - backend/pyproject.toml
+
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
diff --git a/backend/apps/common/geocoding.py b/backend/apps/common/geocoding.py
index 3ade2579a3..616c26653e 100644
--- a/backend/apps/common/geocoding.py
+++ b/backend/apps/common/geocoding.py
@@ -3,11 +3,12 @@
import time
from geopy.geocoders import Nominatim
+from geopy.location import Location
from apps.common.utils import get_nest_user_agent
-def get_location_coordinates(query, delay=2):
+def get_location_coordinates(query: str, delay: int = 2) -> Location:
"""Get location geo coordinates.
Args:
diff --git a/backend/apps/common/index.py b/backend/apps/common/index.py
index 7fa881bcc1..fb90dbcb7e 100644
--- a/backend/apps/common/index.py
+++ b/backend/apps/common/index.py
@@ -1,5 +1,7 @@
"""Algolia index common classes and helpers."""
+from __future__ import annotations
+
import logging
from functools import lru_cache
from pathlib import Path
@@ -13,7 +15,7 @@
from apps.common.constants import NL
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
EXCLUDED_LOCAL_INDEX_NAMES = (
"projects_contributors_count_asc",
@@ -36,19 +38,19 @@ class IndexRegistry:
_instance = None
- def __init__(self):
+ def __init__(self) -> None:
"""Initialize index registry."""
- self.excluded_local_index_names = set()
+ self.excluded_local_index_names: set = set()
self.load_excluded_local_index_names()
@classmethod
- def get_instance(cls):
+ def get_instance(cls) -> IndexRegistry:
"""Get or create a singleton instance of IndexRegistry."""
if cls._instance is None:
cls._instance = IndexRegistry()
return cls._instance
- def is_indexable(self, name: str):
+ def is_indexable(self, name: str) -> bool:
"""Check if an index is enabled for indexing.
Args:
@@ -60,7 +62,7 @@ def is_indexable(self, name: str):
"""
return name.lower() not in self.excluded_local_index_names if IS_LOCAL_BUILD else True
- def load_excluded_local_index_names(self):
+ def load_excluded_local_index_names(self) -> IndexRegistry:
"""Load excluded local index names from settings.
Returns
@@ -81,7 +83,7 @@ def load_excluded_local_index_names(self):
return self
-def is_indexable(index_name: str):
+def is_indexable(index_name: str) -> bool:
"""Determine if an index should be created based on configuration.
Args:
@@ -120,7 +122,7 @@ class IndexBase(AlgoliaIndex):
"""Base index class."""
@staticmethod
- def get_client(ip_address=None):
+ def get_client(ip_address=None) -> SearchClientSync:
"""Return an instance of the search client.
Args:
@@ -140,7 +142,7 @@ def get_client(ip_address=None):
return SearchClientSync(config=config)
@staticmethod
- def configure_replicas(index_name: str, replicas: dict):
+ def configure_replicas(index_name: str, replicas: dict) -> None:
"""Configure replicas for an index.
Args:
@@ -168,7 +170,7 @@ def configure_replicas(index_name: str, replicas: dict):
client.set_settings(replica_name, {"ranking": replica_ranking})
@staticmethod
- def _parse_synonyms_file(file_path):
+ def _parse_synonyms_file(file_path) -> list | None:
"""Parse a synonyms file and return its content.
Args:
@@ -214,7 +216,7 @@ def _parse_synonyms_file(file_path):
return synonyms
@staticmethod
- def reindex_synonyms(app_name, index_name):
+ def reindex_synonyms(app_name: str, index_name: str) -> int | None:
"""Reindex synonyms for a specific index.
Args:
@@ -246,7 +248,7 @@ def reindex_synonyms(app_name, index_name):
@staticmethod
@lru_cache(maxsize=1024)
- def get_total_count(index_name, search_filters=None):
+ def get_total_count(index_name: str, search_filters=None) -> int | None:
"""Get the total count of records in an index.
Args:
diff --git a/backend/apps/common/management/commands/algolia_update_replicas.py b/backend/apps/common/management/commands/algolia_update_replicas.py
index 18dca79bdf..2c8b2fc1e2 100644
--- a/backend/apps/common/management/commands/algolia_update_replicas.py
+++ b/backend/apps/common/management/commands/algolia_update_replicas.py
@@ -8,14 +8,8 @@
class Command(BaseCommand):
help = "Update OWASP Nest index replicas."
- def handle(self, *_args, **_options):
- """Update replicas for Algolia indices.
-
- Args:
- *_args: Positional arguments (not used).
- **_options: Keyword arguments (not used).
-
- """
+ def handle(self, *_args, **_options) -> None:
+ """Update replicas for Algolia indices."""
print("\n Starting replica configuration...")
ProjectIndex.configure_replicas()
print("\n Replica have been Successfully created.")
diff --git a/backend/apps/common/management/commands/algolia_update_synonyms.py b/backend/apps/common/management/commands/algolia_update_synonyms.py
index b96620e224..d9285b9982 100644
--- a/backend/apps/common/management/commands/algolia_update_synonyms.py
+++ b/backend/apps/common/management/commands/algolia_update_synonyms.py
@@ -9,14 +9,8 @@
class Command(BaseCommand):
help = "Update OWASP Nest index synonyms."
- def handle(self, *_args, **_options):
- """Update synonyms for Algolia indices.
-
- Args:
- *_args: Positional arguments (not used).
- **_options: Keyword arguments (not used).
-
- """
+ def handle(self, *_args, **_options) -> None:
+ """Update synonyms for Algolia indices."""
print("\nThe following models synonyms were reindexed:")
for index in (IssueIndex, ProjectIndex):
count = index.update_synonyms()
diff --git a/backend/apps/common/management/commands/generate_sitemap.py b/backend/apps/common/management/commands/generate_sitemap.py
index a70ff86880..97558f64c0 100644
--- a/backend/apps/common/management/commands/generate_sitemap.py
+++ b/backend/apps/common/management/commands/generate_sitemap.py
@@ -1,6 +1,6 @@
"""Management command to generate OWASP Nest sitemap."""
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from pathlib import Path
from django.conf import settings
@@ -28,7 +28,7 @@ class Command(BaseCommand):
],
}
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -41,7 +41,7 @@ def add_arguments(self, parser):
help="Directory where sitemap files will be saved",
)
- def handle(self, *args, **options):
+ def handle(self, *args, **options) -> None:
"""Generate sitemaps for the OWASP Nest application.
Args:
@@ -70,7 +70,7 @@ def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS(f"Successfully generated sitemaps in {output_dir}"))
- def generate_project_sitemap(self, output_dir):
+ def generate_project_sitemap(self, output_dir: Path) -> None:
"""Generate a sitemap for projects.
Args:
@@ -91,7 +91,7 @@ def generate_project_sitemap(self, output_dir):
content = self.generate_sitemap_content(routes)
self.save_sitemap(content, output_dir / "sitemap-project.xml")
- def generate_chapter_sitemap(self, output_dir):
+ def generate_chapter_sitemap(self, output_dir: Path) -> None:
"""Generate a sitemap for chapters.
Args:
@@ -112,7 +112,7 @@ def generate_chapter_sitemap(self, output_dir):
content = self.generate_sitemap_content(routes)
self.save_sitemap(content, output_dir / "sitemap-chapters.xml")
- def generate_committee_sitemap(self, output_dir):
+ def generate_committee_sitemap(self, output_dir: Path) -> None:
"""Generate a sitemap for committees.
Args:
@@ -136,7 +136,7 @@ def generate_committee_sitemap(self, output_dir):
content = self.generate_sitemap_content(routes)
self.save_sitemap(content, output_dir / "sitemap-committees.xml")
- def generate_user_sitemap(self, output_dir):
+ def generate_user_sitemap(self, output_dir: Path) -> None:
"""Generate a sitemap for users.
Args:
@@ -172,7 +172,7 @@ def generate_sitemap_content(self, routes):
"""
urls = []
- lastmod = datetime.now(timezone.utc).strftime("%Y-%m-%d")
+ lastmod = datetime.now(UTC).strftime("%Y-%m-%d")
for route in routes:
url_entry = {
@@ -185,7 +185,7 @@ def generate_sitemap_content(self, routes):
return self.create_sitemap(urls)
- def generate_index_sitemap(self, sitemap_files):
+ def generate_index_sitemap(self, sitemap_files: list) -> str:
"""Generate the sitemap index file.
Args:
@@ -196,7 +196,7 @@ def generate_index_sitemap(self, sitemap_files):
"""
sitemaps = []
- lastmod = datetime.now(timezone.utc).strftime("%Y-%m-%d")
+ lastmod = datetime.now(UTC).strftime("%Y-%m-%d")
for sitemap_file in sitemap_files:
sitemap_entry = {"loc": f"{settings.SITE_URL}/{sitemap_file}", "lastmod": lastmod}
@@ -204,7 +204,7 @@ def generate_index_sitemap(self, sitemap_files):
return self.create_sitemap_index(sitemaps)
- def create_url_entry(self, url_data):
+ def create_url_entry(self, url_data: dict) -> str:
"""Create a URL entry for the sitemap.
Args:
@@ -223,7 +223,7 @@ def create_url_entry(self, url_data):
" "
).format(**url_data)
- def create_sitemap_index_entry(self, sitemap_data):
+ def create_sitemap_index_entry(self, sitemap_data: dict) -> str:
"""Create a sitemap index entry.
Args:
@@ -237,7 +237,7 @@ def create_sitemap_index_entry(self, sitemap_data):
" \n {loc}\n {lastmod}\n "
).format(**sitemap_data)
- def create_sitemap(self, urls):
+ def create_sitemap(self, urls: list) -> str:
"""Create the complete sitemap XML.
Args:
@@ -254,7 +254,7 @@ def create_sitemap(self, urls):
""
)
- def create_sitemap_index(self, sitemaps):
+ def create_sitemap_index(self, sitemaps: list) -> str:
"""Create the complete sitemap index XML.
Args:
@@ -272,7 +272,7 @@ def create_sitemap_index(self, sitemaps):
)
@staticmethod
- def save_sitemap(content, filepath):
+ def save_sitemap(content: str, filepath: Path) -> None:
"""Save the sitemap content to a file.
Args:
diff --git a/backend/apps/common/management/commands/load_data.py b/backend/apps/common/management/commands/load_data.py
index 8156a9570f..326a7d890a 100644
--- a/backend/apps/common/management/commands/load_data.py
+++ b/backend/apps/common/management/commands/load_data.py
@@ -10,14 +10,8 @@
class Command(BaseCommand):
help = "Load OWASP Nest data."
- def handle(self, *_args, **_options):
- """Load data into the OWASP Nest application.
-
- Args:
- *_args: Positional arguments (not used).
- **_options: Keyword arguments (not used).
-
- """
+ def handle(self, *_args, **_options) -> None:
+ """Load data into the OWASP Nest application."""
# Disable indexing
unregister_indexes()
diff --git a/backend/apps/common/management/commands/purge_data.py b/backend/apps/common/management/commands/purge_data.py
index 2c3c2a0ae5..0b08139aed 100644
--- a/backend/apps/common/management/commands/purge_data.py
+++ b/backend/apps/common/management/commands/purge_data.py
@@ -8,14 +8,8 @@
class Command(BaseCommand):
help = "Purge OWASP Nest data."
- def handle(self, *_args, **options):
- """Purge data from specified OWASP Nest applications.
-
- Args:
- *_args: Positional arguments (not used).
- **options: Keyword arguments (not used).
-
- """
+ def handle(self, *_args, **options) -> None:
+ """Purge data from specified OWASP Nest applications."""
nest_apps = ("github", "owasp")
with connection.cursor() as cursor:
diff --git a/backend/apps/common/models.py b/backend/apps/common/models.py
index 87eed8961c..2ef2e41dec 100644
--- a/backend/apps/common/models.py
+++ b/backend/apps/common/models.py
@@ -12,7 +12,7 @@ class Meta:
abstract = True
@staticmethod
- def bulk_save(model, objects, fields=None):
+ def bulk_save(model, objects, fields=None) -> None:
"""Bulk save objects.
Args:
diff --git a/backend/apps/common/open_ai.py b/backend/apps/common/open_ai.py
index 91faa1ad1e..2488da2f72 100644
--- a/backend/apps/common/open_ai.py
+++ b/backend/apps/common/open_ai.py
@@ -1,17 +1,21 @@
"""Open AI API module."""
+from __future__ import annotations
+
import logging
import openai
from django.conf import settings
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class OpenAi:
"""Open AI communication class."""
- def __init__(self, model="gpt-4o-mini", max_tokens=1000, temperature=0.7):
+ def __init__(
+ self, model: str = "gpt-4o-mini", max_tokens: int = 1000, temperature: float = 0.7
+ ) -> None:
"""OpenAi constructor.
Args:
@@ -29,7 +33,7 @@ def __init__(self, model="gpt-4o-mini", max_tokens=1000, temperature=0.7):
self.model = model
self.temperature = temperature
- def set_input(self, content):
+ def set_input(self, content: str) -> OpenAi:
"""Set system role content.
Args:
@@ -43,7 +47,7 @@ def set_input(self, content):
return self
- def set_max_tokens(self, max_tokens):
+ def set_max_tokens(self, max_tokens: int) -> OpenAi:
"""Set max tokens.
Args:
@@ -57,7 +61,7 @@ def set_max_tokens(self, max_tokens):
return self
- def set_prompt(self, content):
+ def set_prompt(self, content: str) -> OpenAi:
"""Set system role content.
Args:
@@ -71,7 +75,7 @@ def set_prompt(self, content):
return self
- def complete(self):
+ def complete(self) -> str | None:
"""Get API response.
Returns
@@ -98,3 +102,4 @@ def complete(self):
logger.exception("A connection error occurred during OpenAI API request.")
except Exception:
logger.exception("An error occurred during OpenAI API request.")
+ return None
diff --git a/backend/apps/common/utils.py b/backend/apps/common/utils.py
index 6daaedbade..ca49b63a9e 100644
--- a/backend/apps/common/utils.py
+++ b/backend/apps/common/utils.py
@@ -1,7 +1,9 @@
"""Common app utils."""
+from __future__ import annotations
+
import re
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from django.conf import settings
from django.template.defaultfilters import pluralize
@@ -10,7 +12,7 @@
from humanize import intword, naturaltime
-def get_absolute_url(path):
+def get_absolute_url(path: str) -> str:
"""Return the absolute URL for a given path.
Args:
@@ -23,7 +25,7 @@ def get_absolute_url(path):
return f"{settings.SITE_URL}/{path}"
-def get_nest_user_agent():
+def get_nest_user_agent() -> str:
"""Return the user agent string for the Nest application.
Returns
@@ -33,7 +35,7 @@ def get_nest_user_agent():
return settings.APP_NAME.replace(" ", "-").lower()
-def get_user_ip_address(request):
+def get_user_ip_address(request) -> str:
"""Retrieve the user's IP address from the request.
Args:
@@ -50,7 +52,7 @@ def get_user_ip_address(request):
return x_forwarded_for.split(",")[0] if x_forwarded_for else request.META.get("REMOTE_ADDR")
-def join_values(fields, delimiter=" "):
+def join_values(fields: list, delimiter: str = " ") -> str:
"""Join non-empty field values using a specified delimiter.
Args:
@@ -64,7 +66,7 @@ def join_values(fields, delimiter=" "):
return delimiter.join(field for field in fields if field)
-def natural_date(value):
+def natural_date(value: int | str) -> str:
"""Convert a date or timestamp into a human-readable format.
Args:
@@ -75,14 +77,16 @@ def natural_date(value):
"""
if isinstance(value, str):
- value = datetime.strptime(value, "%Y-%m-%d").replace(tzinfo=timezone.utc)
+ dt = datetime.strptime(value, "%Y-%m-%d").replace(tzinfo=UTC)
elif isinstance(value, int):
- value = datetime.fromtimestamp(value, tz=timezone.utc)
+ dt = datetime.fromtimestamp(value, tz=UTC)
+ else:
+ dt = value
- return naturaltime(value)
+ return naturaltime(dt)
-def natural_number(value, unit=None):
+def natural_number(value: int, unit=None) -> str:
"""Convert a number into a human-readable format.
Args:
@@ -97,7 +101,7 @@ def natural_number(value, unit=None):
return f"{number} {unit}{pluralize(value)}" if unit else number
-def slugify(text):
+def slugify(text: str) -> str:
"""Generate a slug from the given text.
Args:
@@ -110,7 +114,7 @@ def slugify(text):
return re.sub(r"-{2,}", "-", django_slugify(text))
-def truncate(text, limit, truncate="..."):
+def truncate(text: str, limit: int, truncate: str = "...") -> str:
"""Truncate text to a specified character limit.
Args:
diff --git a/backend/apps/core/api/algolia.py b/backend/apps/core/api/algolia.py
index c4c3b41b56..cab33ad56b 100644
--- a/backend/apps/core/api/algolia.py
+++ b/backend/apps/core/api/algolia.py
@@ -1,13 +1,16 @@
"""OWASP app Algolia search proxy API."""
+from __future__ import annotations
+
import json
+from typing import Any
import requests
from algoliasearch.http.exceptions import AlgoliaException
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
-from django.http import JsonResponse
+from django.http import HttpRequest, HttpResponseNotAllowed, JsonResponse
from apps.common.index import IndexBase
from apps.common.utils import get_user_ip_address
@@ -18,7 +21,14 @@
CACHE_TTL_IN_SECONDS = 3600 # 1 hour
-def get_search_results(index_name, query, page, hits_per_page, facet_filters, ip_address=None):
+def get_search_results(
+ index_name: str,
+ query: str,
+ page: int,
+ hits_per_page: int,
+ facet_filters: list,
+ ip_address=None,
+) -> dict[str, Any]:
"""Return search results for the given parameters.
Args:
@@ -54,7 +64,7 @@ def get_search_results(index_name, query, page, hits_per_page, facet_filters, ip
}
-def algolia_search(request):
+def algolia_search(request: HttpRequest) -> JsonResponse | HttpResponseNotAllowed:
"""Search Algolia API endpoint.
Args:
diff --git a/backend/apps/core/models/prompt.py b/backend/apps/core/models/prompt.py
index 3997a371aa..added88d77 100644
--- a/backend/apps/core/models/prompt.py
+++ b/backend/apps/core/models/prompt.py
@@ -8,7 +8,7 @@
from apps.common.models import TimestampedModel
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Prompt(TimestampedModel):
@@ -26,20 +26,14 @@ def __str__(self):
"""Prompt human readable representation."""
return self.name
- def save(self, *args, **kwargs):
- """Save prompt.
-
- Args:
- *args: Variable length argument list.
- **kwargs: Arbitrary keyword arguments.
-
- """
+ def save(self, *args, **kwargs) -> None:
+ """Save prompt."""
self.key = slugify(self.name)
super().save(*args, **kwargs)
@staticmethod
- def get_text(key):
+ def get_text(key: str) -> str:
"""Return prompt by key.
Args:
@@ -54,9 +48,10 @@ def get_text(key):
except Prompt.DoesNotExist:
if settings.OPEN_AI_SECRET_KEY != "None": # noqa: S105
logger.warning("Prompt with key '%s' does not exist.", key)
+ return ""
@staticmethod
- def get_github_issue_hint():
+ def get_github_issue_hint() -> str:
"""Return GitHub issue hint prompt.
Returns
@@ -66,7 +61,7 @@ def get_github_issue_hint():
return Prompt.get_text("github-issue-hint")
@staticmethod
- def get_github_issue_documentation_project_summary():
+ def get_github_issue_documentation_project_summary() -> str:
"""Return GitHub issue documentation project summary prompt.
Returns
@@ -76,7 +71,7 @@ def get_github_issue_documentation_project_summary():
return Prompt.get_text("github-issue-documentation-project-summary")
@staticmethod
- def get_github_issue_project_summary():
+ def get_github_issue_project_summary() -> str:
"""Return GitHub issue project summary prompt.
Returns
@@ -86,7 +81,7 @@ def get_github_issue_project_summary():
return Prompt.get_text("github-issue-project-summary")
@staticmethod
- def get_owasp_chapter_suggested_location():
+ def get_owasp_chapter_suggested_location() -> str:
"""Return OWASP chapter suggested location prompt.
Returns
@@ -96,7 +91,7 @@ def get_owasp_chapter_suggested_location():
return Prompt.get_text("owasp-chapter-suggested-location")
@staticmethod
- def get_owasp_chapter_summary():
+ def get_owasp_chapter_summary() -> str:
"""Return OWASP chapter summary prompt.
Returns
@@ -106,7 +101,7 @@ def get_owasp_chapter_summary():
return Prompt.get_text("owasp-chapter-summary")
@staticmethod
- def get_owasp_committee_summary():
+ def get_owasp_committee_summary() -> str:
"""Return OWASP committee summary prompt.
Returns
@@ -116,7 +111,7 @@ def get_owasp_committee_summary():
return Prompt.get_text("owasp-committee-summary")
@staticmethod
- def get_owasp_event_suggested_location():
+ def get_owasp_event_suggested_location() -> str:
"""Return OWASP event suggested location prompt.
Returns
@@ -126,7 +121,7 @@ def get_owasp_event_suggested_location():
return Prompt.get_text("owasp-event-suggested-location")
@staticmethod
- def get_owasp_event_summary():
+ def get_owasp_event_summary() -> str:
"""Return OWASP event summary prompt.
Returns
@@ -136,7 +131,7 @@ def get_owasp_event_summary():
return Prompt.get_text("owasp-event-summary")
@staticmethod
- def get_owasp_project_summary():
+ def get_owasp_project_summary() -> str:
"""Return OWASP project summary prompt.
Returns
diff --git a/backend/apps/core/utils/index.py b/backend/apps/core/utils/index.py
index 8a93391b78..7a37b94dc1 100644
--- a/backend/apps/core/utils/index.py
+++ b/backend/apps/core/utils/index.py
@@ -7,7 +7,7 @@
from django.apps import apps
-def get_params_for_index(index_name):
+def get_params_for_index(index_name: str) -> dict:
"""Return search parameters based on the index name.
Args:
@@ -135,7 +135,7 @@ def get_params_for_index(index_name):
return params
-def register_indexes(app_names=("github", "owasp")):
+def register_indexes(app_names: tuple[str, ...] = ("github", "owasp")) -> None:
"""Register indexes.
Args:
@@ -148,7 +148,7 @@ def register_indexes(app_names=("github", "owasp")):
register(model)
-def unregister_indexes(app_names=("github", "owasp")):
+def unregister_indexes(app_names: tuple[str, ...] = ("github", "owasp")) -> None:
"""Unregister indexes.
Args:
diff --git a/backend/apps/core/validators.py b/backend/apps/core/validators.py
index c6a2db556d..15e4bc8cad 100644
--- a/backend/apps/core/validators.py
+++ b/backend/apps/core/validators.py
@@ -6,7 +6,7 @@
from django.core.validators import validate_slug
-def validate_index_name(index_name):
+def validate_index_name(index_name: str) -> None:
"""Validate index name.
Args:
@@ -30,7 +30,7 @@ def validate_index_name(index_name):
raise ValidationError(message) from None
-def validate_limit(limit):
+def validate_limit(limit: int) -> None:
"""Validate limit.
Args:
@@ -51,7 +51,7 @@ def validate_limit(limit):
raise ValidationError(message)
-def validate_page(page):
+def validate_page(page: int) -> None:
"""Validate page.
Args:
@@ -70,7 +70,7 @@ def validate_page(page):
raise ValidationError(message)
-def validate_query(query):
+def validate_query(query: str) -> None:
"""Validate query.
Args:
@@ -95,7 +95,7 @@ def validate_query(query):
raise ValidationError(message)
-def validate_facet_filters(facet_filters):
+def validate_facet_filters(facet_filters: list) -> None:
"""Validate facet filters.
Args:
@@ -110,7 +110,7 @@ def validate_facet_filters(facet_filters):
raise ValidationError(message)
-def validate_search_params(data):
+def validate_search_params(data: dict) -> None:
"""Validate search parameters.
Args:
@@ -121,7 +121,7 @@ def validate_search_params(data):
"""
validate_facet_filters(data.get("facetFilters", []))
- validate_index_name(data.get("indexName"))
+ validate_index_name(data.get("indexName", ""))
validate_limit(data.get("hitsPerPage", 25))
validate_page(data.get("page", 1))
validate_query(data.get("query", ""))
diff --git a/backend/apps/github/admin.py b/backend/apps/github/admin.py
index 1eeb94c6c1..9f967a341d 100644
--- a/backend/apps/github/admin.py
+++ b/backend/apps/github/admin.py
@@ -42,7 +42,7 @@ class PullRequestAdmin(admin.ModelAdmin):
"title",
)
- def custom_field_github_url(self, obj):
+ def custom_field_github_url(self, obj: PullRequest) -> str:
"""Pull Request GitHub URL.
Args:
@@ -75,7 +75,7 @@ class IssueAdmin(admin.ModelAdmin):
)
search_fields = ("title",)
- def custom_field_github_url(self, obj):
+ def custom_field_github_url(self, obj) -> str:
"""Issue GitHub URL.
Args:
@@ -118,7 +118,7 @@ class RepositoryAdmin(admin.ModelAdmin):
ordering = ("-created_at",)
search_fields = ("name", "node_id")
- def custom_field_github_url(self, obj):
+ def custom_field_github_url(self, obj) -> str:
"""Repository GitHub URL.
Args:
@@ -132,7 +132,7 @@ def custom_field_github_url(self, obj):
f"↗️"
)
- def custom_field_title(self, obj):
+ def custom_field_title(self, obj: Repository) -> str:
"""Repository title.
Args:
diff --git a/backend/apps/github/api/search/user.py b/backend/apps/github/api/search/user.py
index a7d708dbe5..c85e4b6543 100644
--- a/backend/apps/github/api/search/user.py
+++ b/backend/apps/github/api/search/user.py
@@ -1,11 +1,19 @@
"""OWASP app user search API."""
+from __future__ import annotations
+
from algoliasearch_django import raw_search
from apps.github.models.user import User
-def get_users(query, attributes=None, limit=25, page=1, searchable_attributes=None):
+def get_users(
+ query: str,
+ attributes: list | None = None,
+ limit: int = 25,
+ page: int = 1,
+ searchable_attributes: list | None = None,
+) -> dict:
"""Return users relevant to a search query.
Args:
diff --git a/backend/apps/github/common.py b/backend/apps/github/common.py
index dba28e96dd..c86c768db8 100644
--- a/backend/apps/github/common.py
+++ b/backend/apps/github/common.py
@@ -1,5 +1,7 @@
"""GitHub app common module."""
+from __future__ import annotations
+
import logging
from datetime import timedelta as td
@@ -16,10 +18,12 @@
from apps.github.models.user import User
from apps.github.utils import check_owasp_site_repository
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def sync_repository(gh_repository, organization=None, user=None):
+def sync_repository(
+ gh_repository, organization=None, user=None
+) -> tuple[Organization, Repository]:
"""Sync GitHub repository data.
Args:
diff --git a/backend/apps/github/graphql/nodes/issue.py b/backend/apps/github/graphql/nodes/issue.py
index a036a410d1..429049b31d 100644
--- a/backend/apps/github/graphql/nodes/issue.py
+++ b/backend/apps/github/graphql/nodes/issue.py
@@ -22,7 +22,7 @@ class Meta:
"url",
)
- def resolve_organization_name(self, info):
+ def resolve_organization_name(self, info) -> str | None:
"""Return organization name."""
return self.repository.organization.login if self.repository.organization else None
diff --git a/backend/apps/github/graphql/nodes/release.py b/backend/apps/github/graphql/nodes/release.py
index 96629e6c5a..b6b7c302f7 100644
--- a/backend/apps/github/graphql/nodes/release.py
+++ b/backend/apps/github/graphql/nodes/release.py
@@ -1,5 +1,7 @@
"""GitHub release GraphQL node."""
+from __future__ import annotations
+
import graphene
from apps.common.graphql.nodes import BaseNode
@@ -27,18 +29,18 @@ class Meta:
"tag_name",
)
- def resolve_organization_name(self, info):
+ def resolve_organization_name(self, info) -> str | None:
"""Return organization name."""
return self.repository.organization.login if self.repository.organization else None
- def resolve_project_name(self, info):
+ def resolve_project_name(self, info) -> str:
"""Return project name."""
return self.repository.project.name.lstrip(OWASP_ORGANIZATION_NAME)
- def resolve_repository_name(self, info):
+ def resolve_repository_name(self, info) -> str:
"""Return repository name."""
return self.repository.name
- def resolve_url(self, info):
+ def resolve_url(self, info) -> str:
"""Return release URL."""
return self.url
diff --git a/backend/apps/github/graphql/nodes/user.py b/backend/apps/github/graphql/nodes/user.py
index 258c24c3df..9b1ba4fc4b 100644
--- a/backend/apps/github/graphql/nodes/user.py
+++ b/backend/apps/github/graphql/nodes/user.py
@@ -43,11 +43,11 @@ def resolve_created_at(self, info):
"""Resolve created at."""
return self.idx_created_at
- def resolve_issues_count(self, info):
+ def resolve_issues_count(self, info) -> int:
"""Resolve issues count."""
return self.idx_issues_count
- def resolve_releases_count(self, info):
+ def resolve_releases_count(self, info) -> int:
"""Resolve releases count."""
return self.idx_releases_count
@@ -55,6 +55,6 @@ def resolve_updated_at(self, info):
"""Resolve updated at."""
return self.idx_updated_at
- def resolve_url(self, info):
+ def resolve_url(self, info) -> str:
"""Resolve URL."""
return self.url
diff --git a/backend/apps/github/graphql/queries/issue.py b/backend/apps/github/graphql/queries/issue.py
index 98129302db..5777e8891d 100644
--- a/backend/apps/github/graphql/queries/issue.py
+++ b/backend/apps/github/graphql/queries/issue.py
@@ -1,7 +1,9 @@
"""GraphQL queries for handling GitHub issues."""
+from __future__ import annotations
+
import graphene
-from django.db.models import OuterRef, Subquery
+from django.db.models import OuterRef, QuerySet, Subquery
from apps.common.graphql.queries import BaseQuery
from apps.github.graphql.nodes.issue import IssueNode
@@ -13,20 +15,28 @@ class IssueQuery(BaseQuery):
recent_issues = graphene.List(
IssueNode,
- limit=graphene.Int(default_value=5),
distinct=graphene.Boolean(default_value=False),
+ limit=graphene.Int(default_value=5),
login=graphene.String(required=False),
organization=graphene.String(required=False),
)
- def resolve_recent_issues(root, info, limit, distinct=False, login=None, organization=None):
+ def resolve_recent_issues(
+ root,
+ info,
+ *,
+ distinct: bool = False,
+ limit: int = 5,
+ login: str | None = None,
+ organization: str | None = None,
+ ) -> QuerySet:
"""Resolve recent issues with optional filtering.
Args:
root (Any): The root query object.
info (ResolveInfo): The GraphQL execution context.
- limit (int): Maximum number of issues to return.
distinct (bool): Whether to return unique issues per author and repository.
+ limit (int): Maximum number of issues to return.
login (str, optional): Filter issues by a specific author's login.
organization (str, optional): Filter issues by a specific organization's login.
diff --git a/backend/apps/github/graphql/queries/pull_request.py b/backend/apps/github/graphql/queries/pull_request.py
index 9c87c96ec7..e139909cc1 100644
--- a/backend/apps/github/graphql/queries/pull_request.py
+++ b/backend/apps/github/graphql/queries/pull_request.py
@@ -1,7 +1,9 @@
"""Github pull requests GraphQL queries."""
+from __future__ import annotations
+
import graphene
-from django.db.models import OuterRef, Subquery
+from django.db.models import OuterRef, QuerySet, Subquery
from apps.common.graphql.queries import BaseQuery
from apps.github.graphql.nodes.pull_request import PullRequestNode
@@ -14,35 +16,36 @@ class PullRequestQuery(BaseQuery):
recent_pull_requests = graphene.List(
PullRequestNode,
- limit=graphene.Int(default_value=5),
distinct=graphene.Boolean(default_value=False),
+ limit=graphene.Int(default_value=5),
login=graphene.String(required=False),
organization=graphene.String(required=False),
- repository=graphene.String(required=False),
project=graphene.String(required=False),
+ repository=graphene.String(required=False),
)
def resolve_recent_pull_requests(
root,
info,
- limit,
- distinct=False,
- login=None,
- organization=None,
- repository=None,
- project=None,
- ):
+ *,
+ distinct: bool = False,
+ limit: int = 5,
+ login: str | None = None,
+ organization: str | None = None,
+ project: str | None = None,
+ repository: str | None = None,
+ ) -> QuerySet:
"""Resolve recent pull requests.
Args:
root (Any): The root query object.
info (ResolveInfo): The GraphQL execution context.
- limit (int): Maximum number of pull requests to return.
distinct (bool): Whether to return unique pull requests per author and repository.
+ limit (int): Maximum number of pull requests to return.
login (str, optional): Filter pull requests by a specific author's login.
organization (str, optional): Filter pull requests by a specific organization's login.
- repository (str, optional): Filter pull requests by a specific repository's login.
project (str, optional): Filter pull requests by a specific project.
+ repository (str, optional): Filter pull requests by a specific repository's login.
Returns:
QuerySet: Queryset containing the filtered list of pull requests.
diff --git a/backend/apps/github/graphql/queries/release.py b/backend/apps/github/graphql/queries/release.py
index 77943968a9..f3e3e670e2 100644
--- a/backend/apps/github/graphql/queries/release.py
+++ b/backend/apps/github/graphql/queries/release.py
@@ -1,7 +1,9 @@
"""GraphQL queries for handling OWASP releases."""
+from __future__ import annotations
+
import graphene
-from django.db.models import OuterRef, Subquery
+from django.db.models import OuterRef, QuerySet, Subquery
from apps.common.graphql.queries import BaseQuery
from apps.github.graphql.nodes.release import ReleaseNode
@@ -13,20 +15,28 @@ class ReleaseQuery(BaseQuery):
recent_releases = graphene.List(
ReleaseNode,
- limit=graphene.Int(default_value=6),
distinct=graphene.Boolean(default_value=False),
+ limit=graphene.Int(default_value=6),
login=graphene.String(required=False),
organization=graphene.String(required=False),
)
- def resolve_recent_releases(root, info, limit, distinct=False, login=None, organization=None):
+ def resolve_recent_releases(
+ root,
+ info,
+ *,
+ distinct: bool = False,
+ limit: int = 6,
+ login=None,
+ organization: str | None = None,
+ ) -> QuerySet:
"""Resolve recent releases with optional distinct filtering.
Args:
root (Any): The root query object.
info (ResolveInfo): The GraphQL execution context.
- limit (int): Maximum number of releases to return.
distinct (bool): Whether to return unique releases per author and repository.
+ limit (int): Maximum number of releases to return.
login (str): Optional GitHub username for filtering releases.
organization (str): Optional GitHub organization for filtering releases.
diff --git a/backend/apps/github/graphql/queries/repository.py b/backend/apps/github/graphql/queries/repository.py
index 80c0a347eb..34b15a2cd0 100644
--- a/backend/apps/github/graphql/queries/repository.py
+++ b/backend/apps/github/graphql/queries/repository.py
@@ -1,5 +1,7 @@
"""OWASP repository GraphQL queries."""
+from __future__ import annotations
+
import graphene
from apps.common.graphql.queries import BaseQuery
@@ -18,11 +20,16 @@ class RepositoryQuery(BaseQuery):
repositories = graphene.List(
RepositoryNode,
- organization=graphene.String(required=True),
limit=graphene.Int(default_value=12),
+ organization=graphene.String(required=True),
)
- def resolve_repository(root, info, organization_key, repository_key):
+ def resolve_repository(
+ root,
+ info,
+ organization_key: str,
+ repository_key: str,
+ ) -> Repository | None:
"""Resolve repository by key.
Args:
@@ -43,14 +50,20 @@ def resolve_repository(root, info, organization_key, repository_key):
except Repository.DoesNotExist:
return None
- def resolve_repositories(root, info, organization, limit):
+ def resolve_repositories(
+ root,
+ info,
+ organization: str,
+ *,
+ limit: int = 12,
+ ) -> list[Repository]:
"""Resolve repositories.
Args:
root (Any): The root query object.
info (ResolveInfo): The GraphQL execution context.
- organization (str): The login of the organization.
limit (int): Maximum number of repositories to return.
+ organization (str): The login of the organization.
Returns:
QuerySet: Queryset containing the repositories for the organization.
diff --git a/backend/apps/github/graphql/queries/repository_contributor.py b/backend/apps/github/graphql/queries/repository_contributor.py
index 3a74b7cc23..877d6ad607 100644
--- a/backend/apps/github/graphql/queries/repository_contributor.py
+++ b/backend/apps/github/graphql/queries/repository_contributor.py
@@ -1,5 +1,7 @@
"""OWASP repository contributor GraphQL queries."""
+from __future__ import annotations
+
import graphene
from django.db.models import F, Window
from django.db.models.functions import Rank
@@ -18,7 +20,13 @@ class RepositoryContributorQuery(BaseQuery):
organization=graphene.String(required=False),
)
- def resolve_top_contributors(root, info, limit, organization=None):
+ def resolve_top_contributors(
+ root,
+ info,
+ *,
+ limit: int = 15,
+ organization: str | None = None,
+ ) -> list[RepositoryContributorNode]:
"""Resolve top contributors only for repositories with projects.
Args:
diff --git a/backend/apps/github/index/organization.py b/backend/apps/github/index/organization.py
index 9ba7d2e817..18e3ff11d3 100644
--- a/backend/apps/github/index/organization.py
+++ b/backend/apps/github/index/organization.py
@@ -1,5 +1,7 @@
"""GitHub OWASP related organizations Algolia index configuration."""
+from django.db.models import QuerySet
+
from apps.common.index import IndexBase, register
from apps.github.models.organization import Organization
@@ -48,11 +50,11 @@ class OrganizationIndex(IndexBase):
should_index = "is_indexable"
@staticmethod
- def update_synonyms():
+ def update_synonyms() -> None:
"""Update synonyms for the organizations index."""
OrganizationIndex.reindex_synonyms("github", "organizations")
- def get_entities(self):
+ def get_entities(self) -> QuerySet:
"""Get the queryset of Organization objects to be indexed.
Returns:
diff --git a/backend/apps/github/index/release.py b/backend/apps/github/index/release.py
index 148c9788be..961219bc9b 100644
--- a/backend/apps/github/index/release.py
+++ b/backend/apps/github/index/release.py
@@ -1,5 +1,7 @@
"""GitHub release Algolia index configuration."""
+from django.db.models import QuerySet
+
from apps.common.index import IndexBase, register
from apps.github.models.release import Release
@@ -51,14 +53,14 @@ class ReleaseIndex(IndexBase):
should_index = "is_indexable"
@staticmethod
- def update_synonyms():
+ def update_synonyms() -> None:
"""Update synonyms for the release index."""
ReleaseIndex.reindex_synonyms("github", "releases")
- def get_entities(self):
+ def get_entities(self) -> QuerySet:
"""Get entities for indexing.
- Returns
+ Returns:
QuerySet: A queryset of Release objects to be indexed.
"""
diff --git a/backend/apps/github/index/repository.py b/backend/apps/github/index/repository.py
index 9b73014b78..8f28c88c21 100644
--- a/backend/apps/github/index/repository.py
+++ b/backend/apps/github/index/repository.py
@@ -1,5 +1,7 @@
"""GitHub repository Algolia index configuration."""
+from django.db.models import QuerySet
+
from apps.common.index import IndexBase, register
from apps.github.models.repository import Repository
@@ -57,14 +59,14 @@ class RepositoryIndex(IndexBase):
should_index = "is_indexable"
@staticmethod
- def update_synonyms():
+ def update_synonyms() -> None:
"""Update synonyms for the repository index."""
RepositoryIndex.reindex_synonyms("github", "repositories")
- def get_entities(self):
+ def get_entities(self) -> QuerySet:
"""Get entities for indexing.
- Returns
+ Returns:
QuerySet: A queryset of Repository objects to be indexed.
"""
diff --git a/backend/apps/github/index/user.py b/backend/apps/github/index/user.py
index 6b9c7098f4..7af3578cf3 100644
--- a/backend/apps/github/index/user.py
+++ b/backend/apps/github/index/user.py
@@ -1,5 +1,7 @@
"""GitHub user Algolia index configuration."""
+from django.db.models import QuerySet
+
from apps.common.index import IndexBase, register
from apps.github.models.user import User
@@ -63,14 +65,14 @@ class UserIndex(IndexBase):
should_index = "is_indexable"
@staticmethod
- def update_synonyms():
+ def update_synonyms() -> None:
"""Update synonyms for the user index."""
UserIndex.reindex_synonyms("github", "users")
- def get_entities(self):
+ def get_entities(self) -> QuerySet:
"""Get entities for indexing.
- Returns
+ Returns:
QuerySet: A queryset of User objects to be indexed.
"""
diff --git a/backend/apps/github/management/commands/github_enrich_issues.py b/backend/apps/github/management/commands/github_enrich_issues.py
index 052aacad12..23654e409f 100644
--- a/backend/apps/github/management/commands/github_enrich_issues.py
+++ b/backend/apps/github/management/commands/github_enrich_issues.py
@@ -7,13 +7,13 @@
from apps.common.open_ai import OpenAi
from apps.github.models.issue import Issue
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Enrich GitHub issue with AI generated data."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -30,7 +30,7 @@ def add_arguments(self, parser):
parser.add_argument("--update-hint", default=True, required=False, action="store_true")
parser.add_argument("--update-summary", default=True, required=False, action="store_true")
- def handle(self, *args, **options):
+ def handle(self, *args, **options) -> None:
"""Handle the command execution.
Args:
diff --git a/backend/apps/github/management/commands/github_update_owasp_organization.py b/backend/apps/github/management/commands/github_update_owasp_organization.py
index 4dff66794e..973339593f 100644
--- a/backend/apps/github/management/commands/github_update_owasp_organization.py
+++ b/backend/apps/github/management/commands/github_update_owasp_organization.py
@@ -16,7 +16,7 @@
from apps.owasp.models.committee import Committee
from apps.owasp.models.project import Project
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
@@ -24,7 +24,7 @@ class Command(BaseCommand):
help = "Fetch OWASP GitHub repository and update relevant entities."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -39,7 +39,7 @@ def add_arguments(self, parser):
help="The OWASP organization's repository name (e.g. Nest, www-project-nest')",
)
- def handle(self, *_args, **options):
+ def handle(self, *_args, **options) -> None:
"""Handle the command execution.
Args:
@@ -77,7 +77,7 @@ def handle(self, *_args, **options):
sort="created",
direction="desc",
)
- gh_repositories_count = gh_repositories.totalCount
+ gh_repositories_count = gh_repositories.totalCount # type: ignore[attr-defined]
for idx, gh_repository in enumerate(gh_repositories[offset:]):
prefix = f"{idx + offset + 1} of {gh_repositories_count}"
diff --git a/backend/apps/github/management/commands/github_update_project_related_repositories.py b/backend/apps/github/management/commands/github_update_project_related_repositories.py
index d3c5a472dc..423e19d065 100644
--- a/backend/apps/github/management/commands/github_update_project_related_repositories.py
+++ b/backend/apps/github/management/commands/github_update_project_related_repositories.py
@@ -12,7 +12,7 @@
from apps.github.utils import get_repository_path
from apps.owasp.models.project import Project
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
@@ -20,7 +20,7 @@ class Command(BaseCommand):
help = "Updates OWASP project related repositories."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -29,7 +29,7 @@ def add_arguments(self, parser):
"""
parser.add_argument("--offset", default=0, required=False, type=int)
- def handle(self, *args, **options):
+ def handle(self, *args, **options) -> None:
"""Handle the command execution.
Args:
diff --git a/backend/apps/github/migrations/0001_initial.py b/backend/apps/github/migrations/0001_initial.py
index 4e8e8e637f..313e37d4ba 100644
--- a/backend/apps/github/migrations/0001_initial.py
+++ b/backend/apps/github/migrations/0001_initial.py
@@ -140,7 +140,7 @@ class Migration(migrations.Migration):
"original_created_at",
models.DateTimeField(
default=datetime.datetime(
- 2024, 8, 20, 19, 14, 44, 920859, tzinfo=datetime.timezone.utc
+ 2024, 8, 20, 19, 14, 44, 920859, tzinfo=datetime.UTC
),
verbose_name="Original created_at",
),
@@ -149,7 +149,7 @@ class Migration(migrations.Migration):
"original_updated_at",
models.DateTimeField(
default=datetime.datetime(
- 2024, 8, 20, 19, 14, 50, 238981, tzinfo=datetime.timezone.utc
+ 2024, 8, 20, 19, 14, 50, 238981, tzinfo=datetime.UTC
),
verbose_name="Original updated_at",
),
@@ -355,9 +355,7 @@ class Migration(migrations.Migration):
model_name="organization",
name="created_at",
field=models.DateTimeField(
- default=datetime.datetime(
- 2024, 8, 21, 21, 8, 41, 7158, tzinfo=datetime.timezone.utc
- ),
+ default=datetime.datetime(2024, 8, 21, 21, 8, 41, 7158, tzinfo=datetime.UTC),
verbose_name="Created at",
),
preserve_default=False,
@@ -366,9 +364,7 @@ class Migration(migrations.Migration):
model_name="organization",
name="updated_at",
field=models.DateTimeField(
- default=datetime.datetime(
- 2024, 8, 21, 21, 8, 47, 175920, tzinfo=datetime.timezone.utc
- ),
+ default=datetime.datetime(2024, 8, 21, 21, 8, 47, 175920, tzinfo=datetime.UTC),
verbose_name="Updated at",
),
preserve_default=False,
@@ -387,9 +383,7 @@ class Migration(migrations.Migration):
model_name="user",
name="created_at",
field=models.DateTimeField(
- default=datetime.datetime(
- 2024, 8, 21, 21, 8, 55, 980536, tzinfo=datetime.timezone.utc
- ),
+ default=datetime.datetime(2024, 8, 21, 21, 8, 55, 980536, tzinfo=datetime.UTC),
verbose_name="Created at",
),
preserve_default=False,
@@ -398,9 +392,7 @@ class Migration(migrations.Migration):
model_name="user",
name="updated_at",
field=models.DateTimeField(
- default=datetime.datetime(
- 2024, 8, 21, 21, 8, 57, 168111, tzinfo=datetime.timezone.utc
- ),
+ default=datetime.datetime(2024, 8, 21, 21, 8, 57, 168111, tzinfo=datetime.UTC),
verbose_name="Updated at",
),
preserve_default=False,
diff --git a/backend/apps/github/models/common.py b/backend/apps/github/models/common.py
index e93fb55628..4ce0a6fb4b 100644
--- a/backend/apps/github/models/common.py
+++ b/backend/apps/github/models/common.py
@@ -31,16 +31,16 @@ class Meta:
updated_at = models.DateTimeField(verbose_name="Updated at")
@property
- def title(self):
+ def title(self) -> str:
"""Entity title."""
return f"{self.name or self.login}"
@property
- def url(self):
+ def url(self) -> str:
"""Entity URL."""
return f"https://github.com/{self.login.lower()}"
- def from_github(self, data):
+ def from_github(self, data) -> None:
"""Update instance based on GitHub data."""
field_mapping = {
"avatar_url": "avatar_url",
diff --git a/backend/apps/github/models/generic_issue_model.py b/backend/apps/github/models/generic_issue_model.py
index c004c8d734..f3dfae837d 100644
--- a/backend/apps/github/models/generic_issue_model.py
+++ b/backend/apps/github/models/generic_issue_model.py
@@ -37,7 +37,7 @@ class State(models.TextChoices):
created_at = models.DateTimeField(verbose_name="Created at")
updated_at = models.DateTimeField(verbose_name="Updated at", db_index=True)
- def __str__(self):
+ def __str__(self) -> str:
"""Return a human-readable representation of the issue.
Returns
diff --git a/backend/apps/github/models/issue.py b/backend/apps/github/models/issue.py
index 71a32fc922..1d4ca8b9d6 100644
--- a/backend/apps/github/models/issue.py
+++ b/backend/apps/github/models/issue.py
@@ -1,5 +1,7 @@
"""Github app issue model."""
+from __future__ import annotations
+
from functools import lru_cache
from django.db import models
@@ -74,7 +76,7 @@ class Meta:
blank=True,
)
- def from_github(self, gh_issue, author=None, repository=None):
+ def from_github(self, gh_issue, *, author=None, repository=None) -> None:
"""Update the instance based on GitHub issue data.
Args:
@@ -111,7 +113,7 @@ def from_github(self, gh_issue, author=None, repository=None):
# Repository.
self.repository = repository
- def generate_hint(self, open_ai=None, max_tokens=1000):
+ def generate_hint(self, open_ai: OpenAi | None = None, max_tokens: int = 1000) -> None:
"""Generate a hint for the issue using AI.
Args:
@@ -127,7 +129,7 @@ def generate_hint(self, open_ai=None, max_tokens=1000):
open_ai.set_max_tokens(max_tokens).set_prompt(prompt)
self.hint = open_ai.complete() or ""
- def generate_summary(self, open_ai=None, max_tokens=500):
+ def generate_summary(self, open_ai: OpenAi | None = None, max_tokens: int = 500) -> None:
"""Generate a summary for the issue using AI.
Args:
@@ -149,7 +151,7 @@ def generate_summary(self, open_ai=None, max_tokens=500):
open_ai.set_max_tokens(max_tokens).set_prompt(prompt)
self.summary = open_ai.complete() or ""
- def save(self, *args, **kwargs):
+ def save(self, *args, **kwargs) -> None:
"""Save issue."""
if self.is_open:
if not self.hint:
@@ -161,7 +163,7 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)
@staticmethod
- def bulk_save(issues, fields=None):
+ def bulk_save(issues, fields=None) -> None: # type: ignore[override]
"""Bulk save issues."""
BulkSaveModel.bulk_save(Issue, issues, fields=fields)
@@ -172,7 +174,7 @@ def open_issues_count():
return IndexBase.get_total_count("issues")
@staticmethod
- def update_data(gh_issue, author=None, repository=None, save=True):
+ def update_data(gh_issue, *, author=None, repository=None, save: bool = True):
"""Update issue data.
Args:
diff --git a/backend/apps/github/models/label.py b/backend/apps/github/models/label.py
index 05a9db8a61..247b72cb7c 100644
--- a/backend/apps/github/models/label.py
+++ b/backend/apps/github/models/label.py
@@ -28,7 +28,7 @@ def __str__(self):
"""
return f"{self.name} ({self.description})" if self.description else self.name
- def from_github(self, gh_label):
+ def from_github(self, gh_label) -> None:
"""Update the instance based on GitHub label data.
Args:
@@ -51,12 +51,12 @@ def from_github(self, gh_label):
setattr(self, model_field, value)
@staticmethod
- def bulk_save(labels):
+ def bulk_save(labels, *, fields=None) -> None: # type: ignore[override]
"""Bulk save labels."""
- BulkSaveModel.bulk_save(Label, labels)
+ BulkSaveModel.bulk_save(Label, labels, fields=fields)
@staticmethod
- def update_data(gh_label, save=True):
+ def update_data(gh_label, *, save: bool = True) -> "Label":
"""Update label data.
Args:
diff --git a/backend/apps/github/models/mixins/issue.py b/backend/apps/github/models/mixins/issue.py
index 7c13e5ce51..048586841d 100644
--- a/backend/apps/github/models/mixins/issue.py
+++ b/backend/apps/github/models/mixins/issue.py
@@ -1,5 +1,7 @@
"""GitHub issue mixins."""
+from __future__ import annotations
+
class IssueIndexMixin:
"""Issue index mixin."""
@@ -18,121 +20,121 @@ def is_indexable(self):
)
@property
- def idx_author_login(self):
+ def idx_author_login(self) -> str:
"""Return author login for indexing."""
return self.author.login if self.author else ""
@property
- def idx_author_name(self):
+ def idx_author_name(self) -> str:
"""Return author name for indexing."""
return self.author.name if self.author else ""
@property
- def idx_comments_count(self):
+ def idx_comments_count(self) -> int:
"""Return comments count at for indexing."""
return self.comments_count
@property
- def idx_created_at(self):
+ def idx_created_at(self) -> float:
"""Return created at for indexing."""
return self.created_at.timestamp()
@property
- def idx_hint(self):
+ def idx_hint(self) -> str:
"""Return hint for indexing."""
return self.hint
@property
- def idx_labels(self):
+ def idx_labels(self) -> list[str]:
"""Return labels for indexing."""
return [label.name for label in self.labels.all()]
@property
- def idx_project_description(self):
+ def idx_project_description(self) -> str:
"""Return project description for indexing."""
return self.project.idx_description if self.project else ""
@property
- def idx_project_level(self):
+ def idx_project_level(self) -> str:
"""Return project level or indexing."""
return self.project.idx_level if self.project else ""
@property
- def idx_project_level_raw(self):
+ def idx_project_level_raw(self) -> str:
"""Return project raw level for indexing."""
return self.project.idx_level_raw if self.project else ""
@property
- def idx_project_tags(self):
+ def idx_project_tags(self) -> list[str]:
"""Return project tags for indexing."""
return self.project.idx_tags if self.project else []
@property
- def idx_project_topics(self):
+ def idx_project_topics(self) -> list[str]:
"""Return project topics for indexing."""
return self.project.idx_topics if self.project else []
@property
- def idx_project_name(self):
+ def idx_project_name(self) -> str:
"""Return project name for indexing."""
return self.project.idx_name if self.project else ""
@property
- def idx_project_url(self):
+ def idx_project_url(self) -> str:
"""Return project URL for indexing."""
return self.project.idx_url if self.project else ""
@property
- def idx_repository_contributors_count(self):
+ def idx_repository_contributors_count(self) -> int:
"""Return repository contributors count for indexing."""
return self.repository.idx_contributors_count
@property
- def idx_repository_description(self):
+ def idx_repository_description(self) -> str:
"""Return repository description for indexing."""
return self.repository.idx_description
@property
- def idx_repository_forks_count(self):
+ def idx_repository_forks_count(self) -> int:
"""Return repository forks count for indexing."""
return self.repository.idx_forks_count
@property
- def idx_repository_languages(self):
+ def idx_repository_languages(self) -> list[str]:
"""Return repository languages for indexing."""
return self.repository.top_languages
@property
- def idx_repository_name(self):
+ def idx_repository_name(self) -> str:
"""Return repository name for indexing."""
return self.repository.idx_name
@property
- def idx_repository_stars_count(self):
+ def idx_repository_stars_count(self) -> int:
"""Return repository stars count for indexing."""
return self.repository.idx_stars_count
@property
- def idx_summary(self):
+ def idx_summary(self) -> str:
"""Return summary for indexing."""
return self.summary
@property
- def idx_title(self):
+ def idx_title(self) -> str:
"""Return title for indexing."""
return self.title
@property
- def idx_repository_topics(self):
+ def idx_repository_topics(self) -> list[str]:
"""Return repository stars count for indexing."""
return self.repository.idx_topics
@property
- def idx_updated_at(self):
+ def idx_updated_at(self) -> float:
"""Return updated at for indexing."""
return self.updated_at.timestamp()
@property
- def idx_url(self):
+ def idx_url(self) -> str:
"""Return URL for indexing."""
return self.url
diff --git a/backend/apps/github/models/mixins/organization.py b/backend/apps/github/models/mixins/organization.py
index 843e8a6889..be2f033c0c 100644
--- a/backend/apps/github/models/mixins/organization.py
+++ b/backend/apps/github/models/mixins/organization.py
@@ -1,16 +1,18 @@
"""GitHub organization mixins."""
+from __future__ import annotations
+
class OrganizationIndexMixin:
"""Organization index mixin."""
@property
- def is_indexable(self):
+ def is_indexable(self) -> bool:
"""Organizations to index."""
return bool(self.name and self.login)
@property
- def idx_avatar_url(self):
+ def idx_avatar_url(self) -> str:
"""Return avatar URL for indexing."""
return self.avatar_url
@@ -37,41 +39,41 @@ def idx_collaborators_count(self):
return self.collaborators_count
@property
- def idx_created_at(self):
+ def idx_created_at(self) -> float | None:
"""Return created at for indexing."""
return self.created_at.timestamp() if self.created_at else None
@property
- def idx_description(self):
+ def idx_description(self) -> str:
"""Return description for indexing."""
return self.description or ""
@property
- def idx_followers_count(self):
+ def idx_followers_count(self) -> int:
"""Return followers count for indexing."""
return self.followers_count
@property
- def idx_location(self):
+ def idx_location(self) -> str:
"""Return location for indexing."""
return self.location or ""
@property
- def idx_login(self):
+ def idx_login(self) -> str:
"""Return login for indexing."""
return self.login
@property
- def idx_name(self):
+ def idx_name(self) -> str:
"""Return name for indexing."""
return self.name or ""
@property
- def idx_public_repositories_count(self):
+ def idx_public_repositories_count(self) -> int:
"""Return public repositories count for indexing."""
return self.public_repositories_count
@property
- def idx_url(self):
+ def idx_url(self) -> str:
"""Return URL for indexing."""
return self.url
diff --git a/backend/apps/github/models/mixins/release.py b/backend/apps/github/models/mixins/release.py
index 7a7f9a6b8a..750e1dddc2 100644
--- a/backend/apps/github/models/mixins/release.py
+++ b/backend/apps/github/models/mixins/release.py
@@ -1,5 +1,7 @@
"""GitHub release model mixins for index-related functionality."""
+from __future__ import annotations
+
from django.utils.text import Truncator
@@ -7,12 +9,12 @@ class ReleaseIndexMixin:
"""Release index mixin."""
@property
- def is_indexable(self):
+ def is_indexable(self) -> bool:
"""Releases to index."""
return not self.is_draft
@property
- def idx_author(self):
+ def idx_author(self) -> list[dict[str, str]]:
"""Return author for indexing."""
"""Get top contributors."""
return (
@@ -28,41 +30,41 @@ def idx_author(self):
)
@property
- def idx_created_at(self):
+ def idx_created_at(self) -> float:
"""Return created at timestamp for indexing."""
return self.created_at.timestamp()
@property
- def idx_description(self):
+ def idx_description(self) -> str:
"""Return description for indexing."""
return Truncator(self.description).chars(1000, truncate="...")
@property
- def idx_is_pre_release(self):
+ def idx_is_pre_release(self) -> bool:
"""Return is pre release count for indexing."""
return self.is_pre_release
@property
- def idx_name(self):
+ def idx_name(self) -> str:
"""Return name for indexing."""
return self.name
@property
- def idx_project(self):
+ def idx_project(self) -> str:
"""Return project for indexing."""
return self.repository.project.nest_key if self.repository.project else ""
@property
- def idx_published_at(self):
+ def idx_published_at(self) -> float | None:
"""Return published at timestamp for indexing."""
return self.published_at.timestamp() if self.published_at else None
@property
- def idx_repository(self):
+ def idx_repository(self) -> str:
"""Return repository for indexing."""
return self.repository.path.lower()
@property
- def idx_tag_name(self):
+ def idx_tag_name(self) -> str:
"""Return tag name for indexing."""
return self.tag_name
diff --git a/backend/apps/github/models/mixins/repository.py b/backend/apps/github/models/mixins/repository.py
index 20bff3ece8..6ec3c179aa 100644
--- a/backend/apps/github/models/mixins/repository.py
+++ b/backend/apps/github/models/mixins/repository.py
@@ -1,5 +1,9 @@
"""GitHub repository mixins."""
+from __future__ import annotations
+
+from typing import Any
+
from apps.github.models.repository_contributor import (
TOP_CONTRIBUTORS_LIMIT,
RepositoryContributor,
@@ -11,7 +15,7 @@ class RepositoryIndexMixin:
"""Repository index mixin."""
@property
- def is_indexable(self):
+ def is_indexable(self) -> bool:
"""Repositories to index."""
return (
not self.is_archived
@@ -21,87 +25,87 @@ def is_indexable(self):
)
@property
- def idx_commits_count(self):
+ def idx_commits_count(self) -> int:
"""Return commits count for indexing."""
return self.commits_count
@property
- def idx_contributors_count(self):
+ def idx_contributors_count(self) -> int:
"""Return contributors count for indexing."""
return self.contributors_count
@property
- def idx_created_at(self):
+ def idx_created_at(self) -> float:
"""Return created at for indexing."""
return self.created_at.timestamp()
@property
- def idx_description(self):
+ def idx_description(self) -> str:
"""Return description for indexing."""
return self.description
@property
- def idx_forks_count(self):
+ def idx_forks_count(self) -> int:
"""Return forks count for indexing."""
return self.forks_count
@property
- def idx_has_funding_yml(self):
+ def idx_has_funding_yml(self) -> bool:
"""Return has funding.yml for indexing."""
return self.has_funding_yml
@property
- def idx_key(self):
+ def idx_key(self) -> str:
"""Return key for indexing."""
return self.nest_key
@property
- def idx_languages(self):
+ def idx_languages(self) -> list[str]:
"""Return languages for indexing."""
return self.languages
@property
- def idx_license(self):
+ def idx_license(self) -> str:
"""Return license for indexing."""
return self.license
@property
- def idx_name(self):
+ def idx_name(self) -> str:
"""Return name for indexing."""
return self.name
@property
- def idx_open_issues_count(self):
+ def idx_open_issues_count(self) -> int:
"""Return open issues count for indexing."""
return self.open_issues_count
@property
- def idx_project_key(self):
+ def idx_project_key(self) -> str:
"""Return project key for indexing."""
return self.project.nest_key if self.project else ""
@property
- def idx_pushed_at(self):
+ def idx_pushed_at(self) -> float:
"""Return pushed at for indexing."""
return self.pushed_at.timestamp()
@property
- def idx_size(self):
+ def idx_size(self) -> int:
"""Return size for indexing."""
return self.size
@property
- def idx_stars_count(self):
+ def idx_stars_count(self) -> int:
"""Return stars count for indexing."""
return self.stars_count
@property
- def idx_subscribers_count(self):
+ def idx_subscribers_count(self) -> int:
"""Return subscribers count for indexing."""
return self.stars_count
@property
- def idx_top_contributors(self):
+ def idx_top_contributors(self) -> list[dict[str, Any]]:
"""Return top contributors for indexing."""
return [
{
diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py
index be2a95e9a9..c74b5318b4 100644
--- a/backend/apps/github/models/mixins/user.py
+++ b/backend/apps/github/models/mixins/user.py
@@ -14,67 +14,67 @@ def is_indexable(self):
return not self.is_bot and self.login not in self.get_non_indexable_logins()
@property
- def idx_avatar_url(self):
+ def idx_avatar_url(self) -> str:
"""Return avatar URL for indexing."""
return self.avatar_url
@property
- def idx_bio(self):
+ def idx_bio(self) -> str:
"""Return bio for indexing."""
return self.bio
@property
- def idx_company(self):
+ def idx_company(self) -> str:
"""Return company for indexing."""
return self.company
@property
- def idx_created_at(self):
+ def idx_created_at(self) -> float:
"""Return created at timestamp for indexing."""
return self.created_at.timestamp()
@property
- def idx_email(self):
+ def idx_email(self) -> str:
"""Return email for indexing."""
return self.email
@property
- def idx_key(self):
+ def idx_key(self) -> str:
"""Return key for indexing."""
return self.login
@property
- def idx_followers_count(self):
+ def idx_followers_count(self) -> int:
"""Return followers count for indexing."""
return self.followers_count
@property
- def idx_following_count(self):
+ def idx_following_count(self) -> int:
"""Return following count for indexing."""
return self.following_count
@property
- def idx_location(self):
+ def idx_location(self) -> str:
"""Return location for indexing."""
return self.location
@property
- def idx_login(self):
+ def idx_login(self) -> str:
"""Return login for indexing."""
return self.login
@property
- def idx_name(self):
+ def idx_name(self) -> str:
"""Return name for indexing."""
return self.name
@property
- def idx_public_repositories_count(self):
+ def idx_public_repositories_count(self) -> int:
"""Return public repositories count for indexing."""
return self.public_repositories_count
@property
- def idx_title(self):
+ def idx_title(self) -> str:
"""Return title for indexing."""
return self.title
@@ -104,12 +104,12 @@ def idx_contributions(self):
]
@property
- def idx_contributions_count(self):
+ def idx_contributions_count(self) -> int:
"""Return contributions count for indexing."""
return self.contributions_count
@property
- def idx_issues(self):
+ def idx_issues(self) -> list[dict]:
"""Return issues for indexing."""
return [
{
@@ -130,12 +130,12 @@ def idx_issues(self):
]
@property
- def idx_issues_count(self):
+ def idx_issues_count(self) -> int:
"""Return issues count for indexing."""
return self.issues.count()
@property
- def idx_releases(self):
+ def idx_releases(self) -> list[dict]:
"""Return releases for indexing."""
return [
{
@@ -156,16 +156,16 @@ def idx_releases(self):
]
@property
- def idx_releases_count(self):
+ def idx_releases_count(self) -> int:
"""Return releases count for indexing."""
return self.releases.count()
@property
- def idx_updated_at(self):
+ def idx_updated_at(self) -> float:
"""Return updated at timestamp for indexing."""
return self.updated_at.timestamp()
@property
- def idx_url(self):
+ def idx_url(self) -> str:
"""Return GitHub profile URL for indexing."""
return self.url
diff --git a/backend/apps/github/models/organization.py b/backend/apps/github/models/organization.py
index 168420820f..cc64e1ed48 100644
--- a/backend/apps/github/models/organization.py
+++ b/backend/apps/github/models/organization.py
@@ -24,7 +24,7 @@ class Meta:
description = models.CharField(verbose_name="Description", max_length=1000, default="")
- def __str__(self):
+ def __str__(self) -> str:
"""Return a human-readable representation of the organization.
Returns
@@ -33,7 +33,7 @@ def __str__(self):
"""
return f"{self.name}"
- def from_github(self, gh_organization):
+ def from_github(self, gh_organization) -> None:
"""Update the instance based on GitHub organization data.
Args:
@@ -59,12 +59,12 @@ def get_logins():
return set(Organization.objects.values_list("login", flat=True))
@staticmethod
- def bulk_save(organizations):
+ def bulk_save(organizations) -> None: # type: ignore[override]
"""Bulk save organizations."""
BulkSaveModel.bulk_save(Organization, organizations)
@staticmethod
- def update_data(gh_organization, save=True):
+ def update_data(gh_organization, *, save: bool = True) -> "Organization":
"""Update organization data.
Args:
diff --git a/backend/apps/github/models/pull_request.py b/backend/apps/github/models/pull_request.py
index 778350eced..d2c7bd5c67 100644
--- a/backend/apps/github/models/pull_request.py
+++ b/backend/apps/github/models/pull_request.py
@@ -55,7 +55,7 @@ class Meta:
blank=True,
)
- def from_github(self, gh_pull_request, author=None, repository=None):
+ def from_github(self, gh_pull_request, *, author=None, repository=None) -> None:
"""Update the instance based on GitHub pull request data.
Args:
@@ -89,17 +89,23 @@ def from_github(self, gh_pull_request, author=None, repository=None):
# Repository.
self.repository = repository
- def save(self, *args, **kwargs):
+ def save(self, *args, **kwargs) -> None:
"""Save Pull Request."""
super().save(*args, **kwargs)
@staticmethod
- def bulk_save(pull_requests, fields=None):
+ def bulk_save(pull_requests, fields=None) -> None: # type: ignore[override]
"""Bulk save pull requests."""
BulkSaveModel.bulk_save(PullRequest, pull_requests, fields=fields)
@staticmethod
- def update_data(gh_pull_request, author=None, repository=None, save=True):
+ def update_data(
+ gh_pull_request,
+ *,
+ author=None,
+ repository=None,
+ save: bool = True,
+ ) -> "PullRequest":
"""Update pull request data.
Args:
diff --git a/backend/apps/github/models/release.py b/backend/apps/github/models/release.py
index 3b24a5b9ea..ab50ef3107 100644
--- a/backend/apps/github/models/release.py
+++ b/backend/apps/github/models/release.py
@@ -45,26 +45,26 @@ class Meta:
related_name="releases",
)
- def __str__(self):
+ def __str__(self) -> str:
"""Return a human-readable representation of the release.
- Returns
+ Returns:
str: The name of the release along with the author's name.
"""
return f"{self.name} by {self.author}"
@property
- def summary(self):
+ def summary(self) -> str:
"""Return release summary."""
return f"{self.tag_name} on {self.published_at.strftime('%b %d, %Y')}"
@property
- def url(self):
+ def url(self) -> str:
"""Return release URL."""
return f"{self.repository.url}/releases/tag/{self.tag_name}"
- def from_github(self, gh_release, author=None, repository=None):
+ def from_github(self, gh_release, author=None, repository=None) -> None:
"""Update the instance based on GitHub release data.
Args:
@@ -97,12 +97,18 @@ def from_github(self, gh_release, author=None, repository=None):
self.repository = repository
@staticmethod
- def bulk_save(releases):
+ def bulk_save(releases, fields=None) -> None: # type: ignore[override]
"""Bulk save releases."""
- BulkSaveModel.bulk_save(Release, releases)
+ BulkSaveModel.bulk_save(Release, releases, fields=fields)
@staticmethod
- def update_data(gh_release, author=None, repository=None, save=True):
+ def update_data(
+ gh_release,
+ *,
+ author=None,
+ repository=None,
+ save: bool = True,
+ ) -> "Release":
"""Update release data.
Args:
diff --git a/backend/apps/github/models/repository.py b/backend/apps/github/models/repository.py
index 7a28853beb..271ab2698c 100644
--- a/backend/apps/github/models/repository.py
+++ b/backend/apps/github/models/repository.py
@@ -1,5 +1,7 @@
"""Github app repository model."""
+from __future__ import annotations
+
from base64 import b64decode
import yaml
@@ -137,12 +139,12 @@ def latest_updated_pull_request(self):
return self.pull_requests.order_by("-updated_at").first()
@property
- def nest_key(self):
+ def nest_key(self) -> str:
"""Return repository Nest key."""
return f"{self.owner.login}-{self.name}"
@property
- def path(self):
+ def path(self) -> str:
"""Return repository path."""
return f"{self.owner.login}/{self.name}"
@@ -161,7 +163,7 @@ def published_releases(self):
)
@property
- def top_languages(self):
+ def top_languages(self) -> list[str]:
"""Return a list of top used languages."""
return sorted(
k
@@ -170,7 +172,7 @@ def top_languages(self):
)
@property
- def url(self):
+ def url(self) -> str:
"""Return repository URL."""
return f"https://github.com/{self.path}"
@@ -182,7 +184,7 @@ def from_github(
languages=None,
organization=None,
user=None,
- ):
+ ) -> None:
"""Update the repository instance based on GitHub repository data.
Args:
@@ -290,13 +292,14 @@ def from_github(
@staticmethod
def update_data(
gh_repository,
+ *,
commits=None,
contributors=None,
languages=None,
organization=None,
+ save: bool = True,
user=None,
- save=True,
- ):
+ ) -> Repository:
"""Update repository data.
Args:
diff --git a/backend/apps/github/models/repository_contributor.py b/backend/apps/github/models/repository_contributor.py
index 217fc12cc2..246c7ef99d 100644
--- a/backend/apps/github/models/repository_contributor.py
+++ b/backend/apps/github/models/repository_contributor.py
@@ -38,10 +38,10 @@ class Meta:
on_delete=models.CASCADE,
)
- def __str__(self):
+ def __str__(self) -> str:
"""Return a human-readable representation of the repository contributor.
- Returns
+ Returns:
str: A string describing the user's contributions to the repository.
"""
@@ -50,7 +50,7 @@ def __str__(self):
f"contribution{pluralize(self.contributions_count)} to {self.repository}"
)
- def from_github(self, gh_contributions):
+ def from_github(self, gh_contributions) -> None:
"""Update the instance based on GitHub contributor data.
Args:
@@ -68,12 +68,18 @@ def from_github(self, gh_contributions):
setattr(self, model_field, value)
@staticmethod
- def bulk_save(repository_contributors):
+ def bulk_save(repository_contributors) -> None: # type: ignore[override]
"""Bulk save repository contributors."""
BulkSaveModel.bulk_save(RepositoryContributor, repository_contributors)
@staticmethod
- def update_data(gh_contributor, repository, user, save=True):
+ def update_data(
+ gh_contributor,
+ repository,
+ user,
+ *,
+ save: bool = True,
+ ) -> "RepositoryContributor":
"""Update repository contributor data.
Args:
diff --git a/backend/apps/github/models/user.py b/backend/apps/github/models/user.py
index fe62e54ac2..c3c8afe75d 100644
--- a/backend/apps/github/models/user.py
+++ b/backend/apps/github/models/user.py
@@ -1,5 +1,7 @@
"""Github app user model."""
+from __future__ import annotations
+
from django.db import models
from apps.common.models import BulkSaveModel, TimestampedModel
@@ -28,7 +30,7 @@ class Meta:
verbose_name="Contributions count", default=0
)
- def __str__(self):
+ def __str__(self) -> str:
"""Return a human-readable representation of the user.
Returns
@@ -41,7 +43,7 @@ def __str__(self):
def issues(self):
"""Get issues created by the user.
- Returns
+ Returns:
QuerySet: A queryset of issues created by the user.
"""
@@ -57,7 +59,7 @@ def releases(self):
"""
return self.created_releases.all()
- def from_github(self, gh_user):
+ def from_github(self, gh_user) -> None:
"""Update the user instance based on GitHub user data.
Args:
@@ -81,12 +83,12 @@ def from_github(self, gh_user):
self.is_bot = gh_user.type == "Bot"
@staticmethod
- def bulk_save(users, fields=None):
+ def bulk_save(users, fields=None) -> None:
"""Bulk save users."""
BulkSaveModel.bulk_save(User, users, fields=fields)
@staticmethod
- def get_non_indexable_logins():
+ def get_non_indexable_logins() -> set:
"""Get logins that should not be indexed.
Returns
@@ -100,7 +102,7 @@ def get_non_indexable_logins():
}
@staticmethod
- def update_data(gh_user, save=True):
+ def update_data(gh_user, *, save: bool = True) -> User:
"""Update GitHub user data.
Args:
diff --git a/backend/apps/github/utils.py b/backend/apps/github/utils.py
index 79bd9d2a84..9ec5d1edc2 100644
--- a/backend/apps/github/utils.py
+++ b/backend/apps/github/utils.py
@@ -1,5 +1,7 @@
"""GitHub app utils."""
+from __future__ import annotations
+
import logging
from urllib.parse import urlparse
@@ -8,10 +10,10 @@
from apps.github.constants import GITHUB_REPOSITORY_RE
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def check_owasp_site_repository(key):
+def check_owasp_site_repository(key: str) -> bool:
"""Check if the repository is an OWASP site repository.
Args:
@@ -31,7 +33,7 @@ def check_owasp_site_repository(key):
)
-def check_funding_policy_compliance(platform, target):
+def check_funding_policy_compliance(platform: str, target: str) -> bool:
"""Check OWASP funding policy compliance.
Args:
@@ -55,7 +57,11 @@ def check_funding_policy_compliance(platform, target):
return False
-def get_repository_file_content(url, timeout=30):
+def get_repository_file_content(
+ url: str,
+ *,
+ timeout: float | None = 30,
+) -> str:
"""Get the content of a file from a repository.
Args:
@@ -70,9 +76,10 @@ def get_repository_file_content(url, timeout=30):
return requests.get(url, timeout=timeout).text
except RequestException as e:
logger.exception("Failed to fetch file", extra={"URL": url, "error": str(e)})
+ return ""
-def get_repository_path(url):
+def get_repository_path(url: str) -> str | None:
"""Parse a repository URL to extract the owner and repository name.
Args:
@@ -86,7 +93,7 @@ def get_repository_path(url):
return "/".join((match.group(1), match.group(2))) if match else None
-def normalize_url(url, check_path=False):
+def normalize_url(url: str, *, check_path: bool = False) -> str | None:
"""Normalize a URL.
Args:
diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py
index 6a57d99b33..adcadd6632 100644
--- a/backend/apps/owasp/admin.py
+++ b/backend/apps/owasp/admin.py
@@ -130,7 +130,7 @@ class ProjectAdmin(admin.ModelAdmin, GenericEntityAdminMixin):
"topics",
)
- def custom_field_name(self, obj):
+ def custom_field_name(self, obj) -> str:
"""Project custom name."""
return f"{obj.name or obj.key}"
diff --git a/backend/apps/owasp/api/search/chapter.py b/backend/apps/owasp/api/search/chapter.py
index a0935c3b5a..036ca0074c 100644
--- a/backend/apps/owasp/api/search/chapter.py
+++ b/backend/apps/owasp/api/search/chapter.py
@@ -1,11 +1,20 @@
"""OWASP app chapter search API."""
+from __future__ import annotations
+
from algoliasearch_django import raw_search
from apps.owasp.models.chapter import Chapter
-def get_chapters(query, attributes=None, limit=25, page=1, searchable_attributes=None):
+def get_chapters(
+ query: str,
+ *,
+ attributes: list | None = None,
+ limit: int = 25,
+ page: int = 1,
+ searchable_attributes: list | None = None,
+) -> dict:
"""Return chapters relevant to a search query.
Args:
diff --git a/backend/apps/owasp/api/search/committee.py b/backend/apps/owasp/api/search/committee.py
index 5ac38d76dd..3b951f0e76 100644
--- a/backend/apps/owasp/api/search/committee.py
+++ b/backend/apps/owasp/api/search/committee.py
@@ -1,11 +1,19 @@
"""OWASP app committee search API."""
+from __future__ import annotations
+
from algoliasearch_django import raw_search
from apps.owasp.models.committee import Committee
-def get_committees(query, attributes=None, limit=25, page=1):
+def get_committees(
+ query: str,
+ *,
+ attributes: list | None = None,
+ limit: int = 25,
+ page: int = 1,
+) -> dict:
"""Return committees relevant to a search query.
Args:
diff --git a/backend/apps/owasp/api/search/issue.py b/backend/apps/owasp/api/search/issue.py
index 66387365f0..a1e7d07916 100644
--- a/backend/apps/owasp/api/search/issue.py
+++ b/backend/apps/owasp/api/search/issue.py
@@ -1,5 +1,7 @@
"""OWASP app issue search API."""
+from __future__ import annotations
+
from algoliasearch_django import raw_search
from apps.github.models.issue import Issue
@@ -7,7 +9,14 @@
ISSUE_CACHE_PREFIX = "issue:"
-def get_issues(query, attributes=None, distinct=False, limit=25, page=1):
+def get_issues(
+ query: str,
+ *,
+ attributes: list | None = None,
+ distinct: bool = False,
+ limit: int = 25,
+ page: int = 1,
+) -> dict:
"""Return issues relevant to a search query.
Args:
diff --git a/backend/apps/owasp/api/search/project.py b/backend/apps/owasp/api/search/project.py
index 6306b4fc5c..e0bb2950e0 100644
--- a/backend/apps/owasp/api/search/project.py
+++ b/backend/apps/owasp/api/search/project.py
@@ -1,11 +1,20 @@
"""OWASP app project search API."""
+from __future__ import annotations
+
from algoliasearch_django import raw_search
from apps.owasp.models.project import Project
-def get_projects(query, attributes=None, limit=25, page=1, searchable_attributes=None):
+def get_projects(
+ query: str,
+ *,
+ attributes: list | None = None,
+ limit: int = 25,
+ page: int = 1,
+ searchable_attributes: list | None = None,
+) -> dict:
"""Return projects relevant to a search query.
Args:
diff --git a/backend/apps/owasp/graphql/nodes/chapter.py b/backend/apps/owasp/graphql/nodes/chapter.py
index 41ca7f6fc2..06df9b64d2 100644
--- a/backend/apps/owasp/graphql/nodes/chapter.py
+++ b/backend/apps/owasp/graphql/nodes/chapter.py
@@ -34,14 +34,14 @@ class Meta:
"tags",
)
- def resolve_geo_location(self, info):
+ def resolve_geo_location(self, info) -> GeoLocationType:
"""Resolve geographic location."""
return GeoLocationType(lat=self.latitude, lng=self.longitude)
- def resolve_key(self, info):
+ def resolve_key(self, info) -> str:
"""Resolve key."""
return self.idx_key
- def resolve_suggested_location(self, info):
+ def resolve_suggested_location(self, info) -> str:
"""Resolve suggested location."""
return self.idx_suggested_location
diff --git a/backend/apps/owasp/graphql/nodes/committee.py b/backend/apps/owasp/graphql/nodes/committee.py
index 62711c289a..50cc908b2f 100644
--- a/backend/apps/owasp/graphql/nodes/committee.py
+++ b/backend/apps/owasp/graphql/nodes/committee.py
@@ -23,26 +23,26 @@ class Meta:
"summary",
)
- def resolve_created_at(self, info):
+ def resolve_created_at(self, info) -> str:
"""Resolve created at."""
return self.idx_created_at
- def resolve_contributors_count(self, info):
+ def resolve_contributors_count(self, info) -> int:
"""Resolve contributors count."""
return self.owasp_repository.contributors_count
- def resolve_forks_count(self, info):
+ def resolve_forks_count(self, info) -> int:
"""Resolve forks count."""
return self.owasp_repository.forks_count
- def resolve_issues_count(self, info):
+ def resolve_issues_count(self, info) -> int:
"""Resolve issues count."""
return self.owasp_repository.open_issues_count
- def resolve_repositories_count(self, info):
+ def resolve_repositories_count(self, info) -> int:
"""Resolve repositories count."""
return 1
- def resolve_stars_count(self, info):
+ def resolve_stars_count(self, info) -> int:
"""Resolve stars count."""
return self.owasp_repository.stars_count
diff --git a/backend/apps/owasp/graphql/nodes/common.py b/backend/apps/owasp/graphql/nodes/common.py
index 5802f37979..244c4458e4 100644
--- a/backend/apps/owasp/graphql/nodes/common.py
+++ b/backend/apps/owasp/graphql/nodes/common.py
@@ -1,5 +1,7 @@
"""OWASP common GraphQL node."""
+from __future__ import annotations
+
import graphene
from apps.common.graphql.nodes import BaseNode
@@ -18,22 +20,22 @@ class GenericEntityNode(BaseNode):
class Meta:
abstract = True
- def resolve_url(self, info):
+ def resolve_url(self, info) -> str:
"""Resolve URL."""
return self.idx_url
- def resolve_updated_at(self, info):
+ def resolve_updated_at(self, info) -> str:
"""Resolve updated at."""
return self.idx_updated_at
- def resolve_related_urls(self, info):
+ def resolve_related_urls(self, info) -> list[str]:
"""Resolve related URLs."""
return self.idx_related_urls
- def resolve_leaders(self, info):
+ def resolve_leaders(self, info) -> list[str]:
"""Resolve leaders."""
return self.idx_leaders
- def resolve_top_contributors(self, info):
+ def resolve_top_contributors(self, info) -> list[RepositoryContributorNode]:
"""Resolve top contributors."""
return [RepositoryContributorNode(**tc) for tc in self.idx_top_contributors]
diff --git a/backend/apps/owasp/graphql/queries/post.py b/backend/apps/owasp/graphql/queries/post.py
index 50aed3420d..d7c5eb91b6 100644
--- a/backend/apps/owasp/graphql/queries/post.py
+++ b/backend/apps/owasp/graphql/queries/post.py
@@ -1,5 +1,7 @@
"""OWASP event GraphQL queries."""
+from __future__ import annotations
+
import graphene
from apps.common.graphql.queries import BaseQuery
@@ -12,6 +14,6 @@ class PostQuery(BaseQuery):
recent_posts = graphene.List(PostNode, limit=graphene.Int(default_value=5))
- def resolve_recent_posts(root, info, limit=6):
+ def resolve_recent_posts(root, info, limit: int = 6) -> list[PostNode]:
"""Return the 5 most recent posts."""
return Post.recent_posts()[:limit]
diff --git a/backend/apps/owasp/graphql/queries/stats.py b/backend/apps/owasp/graphql/queries/stats.py
index 3a053915ef..407be5d9aa 100644
--- a/backend/apps/owasp/graphql/queries/stats.py
+++ b/backend/apps/owasp/graphql/queries/stats.py
@@ -13,13 +13,12 @@ class StatsQuery:
stats_overview = graphene.Field(StatsNode)
- def resolve_stats_overview(self, info, **kwargs):
+ def resolve_stats_overview(self, info) -> StatsNode:
"""Resolve stats overview.
Args:
self: The StatsQuery instance.
info: GraphQL execution info.
- **kwargs: Additional arguments.
Returns:
StatsNode: A node containing aggregated statistics.
diff --git a/backend/apps/owasp/index/project.py b/backend/apps/owasp/index/project.py
index f72e7b5ca3..4452d3955b 100644
--- a/backend/apps/owasp/index/project.py
+++ b/backend/apps/owasp/index/project.py
@@ -77,7 +77,7 @@ class ProjectIndex(IndexBase):
should_index = "is_indexable"
@staticmethod
- def configure_replicas():
+ def configure_replicas() -> None: # type: ignore[override]
"""Configure the settings for project replicas."""
replicas = {
"contributors_count_asc": ["asc(idx_contributors_count)"],
diff --git a/backend/apps/owasp/management/commands/add_project_custom_tags.py b/backend/apps/owasp/management/commands/add_project_custom_tags.py
index 4952fbd490..718e3d41b9 100644
--- a/backend/apps/owasp/management/commands/add_project_custom_tags.py
+++ b/backend/apps/owasp/management/commands/add_project_custom_tags.py
@@ -1,6 +1,7 @@
"""A command to add project custom tags."""
import json
+from argparse import ArgumentParser
from pathlib import Path
from django.conf import settings
@@ -12,7 +13,7 @@
class Command(BaseCommand):
help = "Add custom tags to OWASP projects."
- def add_arguments(self, parser):
+ def add_arguments(self, parser: ArgumentParser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -27,14 +28,8 @@ def add_arguments(self, parser):
"Example: gsoc-2024.json",
)
- def handle(self, *_args, **options):
- """Handle the command execution.
-
- Args:
- *_args: Variable length argument list.
- **options: Arbitrary keyword arguments containing command options.
-
- """
+ def handle(self, *_args, **options) -> None:
+ """Handle the command execution."""
file_path = Path(settings.BASE_DIR / f"data/project-custom-tags/{options['file-name']}")
if not file_path.exists():
self.stderr.write(f"File not found: {file_path}")
diff --git a/backend/apps/owasp/management/commands/owasp_aggregate_projects.py b/backend/apps/owasp/management/commands/owasp_aggregate_projects.py
index b3b6a34d07..aecbae6b19 100644
--- a/backend/apps/owasp/management/commands/owasp_aggregate_projects.py
+++ b/backend/apps/owasp/management/commands/owasp_aggregate_projects.py
@@ -8,7 +8,7 @@
class Command(BaseCommand):
help = "Aggregate OWASP projects data."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -17,14 +17,8 @@ def add_arguments(self, parser):
"""
parser.add_argument("--offset", default=0, required=False, type=int)
- def handle(self, *_args, **options):
- """Handle the command execution.
-
- Args:
- *_args: Variable length argument list.
- **options: Arbitrary keyword arguments containing command options.
-
- """
+ def handle(self, *_args, **options) -> None:
+ """Handle the command execution."""
active_projects = Project.active_projects.order_by("-created_at")
active_projects_count = active_projects.count()
diff --git a/backend/apps/owasp/management/commands/owasp_enrich_chapters.py b/backend/apps/owasp/management/commands/owasp_enrich_chapters.py
index 7bb80f3f5b..d67723c11d 100644
--- a/backend/apps/owasp/management/commands/owasp_enrich_chapters.py
+++ b/backend/apps/owasp/management/commands/owasp_enrich_chapters.py
@@ -8,13 +8,13 @@
from apps.core.models.prompt import Prompt
from apps.owasp.models.chapter import Chapter
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Enrich OWASP chapters with extra data."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -23,14 +23,8 @@ def add_arguments(self, parser):
"""
parser.add_argument("--offset", default=0, required=False, type=int)
- def handle(self, *args, **options):
- """Handle the command execution.
-
- Args:
- *args: Variable length argument list.
- **options: Arbitrary keyword arguments containing command options.
-
- """
+ def handle(self, *args, **options) -> None:
+ """Handle the command execution."""
active_chapters = Chapter.active_chapters.without_geo_data.order_by("id")
active_chapters_count = active_chapters.count()
diff --git a/backend/apps/owasp/management/commands/owasp_enrich_committees.py b/backend/apps/owasp/management/commands/owasp_enrich_committees.py
index b865f22bcb..5829b1e0a3 100644
--- a/backend/apps/owasp/management/commands/owasp_enrich_committees.py
+++ b/backend/apps/owasp/management/commands/owasp_enrich_committees.py
@@ -8,13 +8,13 @@
from apps.core.models.prompt import Prompt
from apps.owasp.models.committee import Committee
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Enrich OWASP committees with AI generated data."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -27,17 +27,8 @@ def add_arguments(self, parser):
)
parser.add_argument("--update-summary", default=True, required=False, action="store_true")
- def handle(self, *args, **options):
- """Execute the enrichment process for OWASP committees.
-
- Args:
- *args: Variable length argument list.
- **options: Arbitrary keyword arguments containing:
- offset (int): The starting index for processing.
- force_update_summary (bool): Whether to force updating summaries.
- update_summary (bool): Whether to update summaries.
-
- """
+ def handle(self, *args, **options) -> None:
+ """Execute the enrichment process for OWASP committees."""
open_ai = OpenAi()
force_update_summary = options["force_update_summary"]
diff --git a/backend/apps/owasp/management/commands/owasp_enrich_events.py b/backend/apps/owasp/management/commands/owasp_enrich_events.py
index f267edb7ed..20e85ebe84 100644
--- a/backend/apps/owasp/management/commands/owasp_enrich_events.py
+++ b/backend/apps/owasp/management/commands/owasp_enrich_events.py
@@ -8,13 +8,13 @@
from apps.core.models.prompt import Prompt
from apps.owasp.models.event import Event
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Enrich events with extra data."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -23,15 +23,8 @@ def add_arguments(self, parser):
"""
parser.add_argument("--offset", default=0, required=False, type=int)
- def handle(self, *args, **options):
- """Handle the command execution.
-
- Args:
- *args: Variable length argument list.
- **options: Arbitrary keyword arguments containing command options.
- offset (int): The starting index for processing.
-
- """
+ def handle(self, *args, **options) -> None:
+ """Handle the command execution."""
events = Event.objects.order_by("id")
all_events = []
offset = options["offset"]
diff --git a/backend/apps/owasp/management/commands/owasp_enrich_projects.py b/backend/apps/owasp/management/commands/owasp_enrich_projects.py
index 7da327780d..e2b3d4e01e 100644
--- a/backend/apps/owasp/management/commands/owasp_enrich_projects.py
+++ b/backend/apps/owasp/management/commands/owasp_enrich_projects.py
@@ -8,13 +8,13 @@
from apps.core.models.prompt import Prompt
from apps.owasp.models.project import Project
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Enrich OWASP projects with AI generated data."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -27,17 +27,8 @@ def add_arguments(self, parser):
)
parser.add_argument("--update-summary", default=True, required=False, action="store_true")
- def handle(self, *args, **options):
- """Execute the enrichment process for OWASP projects.
-
- Args:
- *args: Variable length argument list.
- **options: Arbitrary keyword arguments containing:
- offset (int): The starting index for processing.
- force_update_summary (bool): Whether to force updating summaries.
- update_summary (bool): Whether to update summaries.
-
- """
+ def handle(self, *args, **options) -> None:
+ """Execute the enrichment process for OWASP projects."""
open_ai = OpenAi()
force_update_summary = options["force_update_summary"]
diff --git a/backend/apps/owasp/management/commands/owasp_process_snapshots.py b/backend/apps/owasp/management/commands/owasp_process_snapshots.py
index a47ad7a51a..74ab574206 100644
--- a/backend/apps/owasp/management/commands/owasp_process_snapshots.py
+++ b/backend/apps/owasp/management/commands/owasp_process_snapshots.py
@@ -13,27 +13,21 @@
from apps.owasp.models.project import Project
from apps.owasp.models.snapshot import Snapshot
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Process pending snapshots and populate them with new data"
- def handle(self, *args, **options):
- """Handle the command execution.
-
- Args:
- *args: Variable length argument list.
- **options: Arbitrary keyword arguments.
-
- """
+ def handle(self, *args, **options) -> None:
+ """Handle the command execution."""
try:
self.process_snapshots()
except Exception as e:
error_msg = f"Failed to process snapshot: {e}"
raise SnapshotProcessingError(error_msg) from e
- def process_snapshots(self):
+ def process_snapshots(self) -> None:
"""Process all pending snapshots."""
pending_snapshots = Snapshot.objects.filter(status=Snapshot.Status.PENDING)
@@ -52,7 +46,7 @@ def process_snapshots(self):
snapshot.error_message = error_msg
snapshot.save()
- def process_snapshot(self, snapshot):
+ def process_snapshot(self, snapshot: Snapshot) -> None:
"""Process a single snapshot.
Args:
diff --git a/backend/apps/owasp/management/commands/owasp_scrape_chapters.py b/backend/apps/owasp/management/commands/owasp_scrape_chapters.py
index d8716e37ed..a744340af8 100644
--- a/backend/apps/owasp/management/commands/owasp_scrape_chapters.py
+++ b/backend/apps/owasp/management/commands/owasp_scrape_chapters.py
@@ -9,13 +9,13 @@
from apps.owasp.models.chapter import Chapter
from apps.owasp.scraper import OwaspScraper
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Scrape owasp.org pages and update relevant chapters."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -24,15 +24,8 @@ def add_arguments(self, parser):
"""
parser.add_argument("--offset", default=0, required=False, type=int)
- def handle(self, *args, **options):
- """Handle the command execution.
-
- Args:
- *args: Variable length argument list.
- **options: Arbitrary keyword arguments containing command options.
- offset (int): The starting index for processing.
-
- """
+ def handle(self, *args, **options) -> None:
+ """Handle the command execution."""
active_chapters = Chapter.active_chapters.order_by("-created_at")
active_chapters_count = active_chapters.count()
offset = options["offset"]
diff --git a/backend/apps/owasp/management/commands/owasp_scrape_committees.py b/backend/apps/owasp/management/commands/owasp_scrape_committees.py
index 2e0133ee6a..cc472ea7fd 100644
--- a/backend/apps/owasp/management/commands/owasp_scrape_committees.py
+++ b/backend/apps/owasp/management/commands/owasp_scrape_committees.py
@@ -9,13 +9,13 @@
from apps.owasp.models.committee import Committee
from apps.owasp.scraper import OwaspScraper
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Scrape owasp.org pages and update relevant committees."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -24,15 +24,8 @@ def add_arguments(self, parser):
"""
parser.add_argument("--offset", default=0, required=False, type=int)
- def handle(self, *args, **options):
- """Handle the command execution.
-
- Args:
- *args: Variable length argument list.
- **options: Arbitrary keyword arguments containing command options.
- offset (int): The starting index for processing.
-
- """
+ def handle(self, *args, **options) -> None:
+ """Handle the command execution."""
active_committees = Committee.active_committees.order_by("-created_at")
active_committees_count = active_committees.count()
offset = options["offset"]
diff --git a/backend/apps/owasp/management/commands/owasp_scrape_projects.py b/backend/apps/owasp/management/commands/owasp_scrape_projects.py
index 3062022cbd..b8f291cda3 100644
--- a/backend/apps/owasp/management/commands/owasp_scrape_projects.py
+++ b/backend/apps/owasp/management/commands/owasp_scrape_projects.py
@@ -13,13 +13,13 @@
from apps.owasp.models.project import Project
from apps.owasp.scraper import OwaspScraper
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Scrape owasp.org pages and update relevant projects."
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser.
Args:
@@ -28,7 +28,7 @@ def add_arguments(self, parser):
"""
parser.add_argument("--offset", default=0, required=False, type=int)
- def handle(self, *args, **options):
+ def handle(self, *args, **options) -> None:
"""Handle the command execution.
Args:
@@ -62,8 +62,8 @@ def handle(self, *args, **options):
}
)
- invalid_urls = set()
- related_urls = set()
+ invalid_urls: set[str] = set()
+ related_urls: set[str] = set()
for scraped_url in scraped_urls:
verified_url = scraper.verify_url(scraped_url)
if not verified_url:
diff --git a/backend/apps/owasp/management/commands/owasp_sync_posts.py b/backend/apps/owasp/management/commands/owasp_sync_posts.py
index 7ee5c3fad8..e1c2c54cb3 100644
--- a/backend/apps/owasp/management/commands/owasp_sync_posts.py
+++ b/backend/apps/owasp/management/commands/owasp_sync_posts.py
@@ -13,7 +13,7 @@
class Command(BaseCommand):
- def get_author_image_url(self, author_image_url):
+ def get_author_image_url(self, author_image_url: str) -> str:
"""Return URL for author image.
Args:
@@ -25,7 +25,7 @@ def get_author_image_url(self, author_image_url):
"""
return f"{OWASP_WEBSITE_URL}{author_image_url}" if author_image_url else ""
- def get_blog_url(self, path):
+ def get_blog_url(self, path: str) -> str:
"""Return OWASP blog URL for a given path.
Args:
@@ -50,7 +50,7 @@ def get_blog_url(self, path):
else path
)
- def handle(self, *args, **options):
+ def handle(self, *args, **options) -> None:
"""Handle the command execution.
Args:
@@ -74,13 +74,12 @@ def handle(self, *args, **options):
if not post_content.startswith("---"):
continue
- metadata = {}
try:
if match := yaml_pattern.search(post_content):
metadata_yaml = match.group(1)
metadata = yaml.safe_load(metadata_yaml) or {}
except yaml.scanner.ScannerError:
- pass
+ metadata = {}
data = {
"author_image_url": self.get_author_image_url(metadata.get("author_image", "")),
diff --git a/backend/apps/owasp/management/commands/owasp_update_events.py b/backend/apps/owasp/management/commands/owasp_update_events.py
index 41ac9efb58..730b3df7fe 100644
--- a/backend/apps/owasp/management/commands/owasp_update_events.py
+++ b/backend/apps/owasp/management/commands/owasp_update_events.py
@@ -10,14 +10,8 @@
class Command(BaseCommand):
help = "Import events from the provided YAML file"
- def handle(self, *args, **kwargs):
- """Handle the command execution.
-
- Args:
- *args: Variable length argument list.
- **kwargs: Arbitrary keyword arguments.
-
- """
+ def handle(self, *args, **kwargs) -> None:
+ """Handle the command execution."""
data = yaml.safe_load(
get_repository_file_content(
"https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/events.yml"
diff --git a/backend/apps/owasp/management/commands/owasp_update_project_health_requirements.py b/backend/apps/owasp/management/commands/owasp_update_project_health_requirements.py
index 97eba06269..0a80ddd152 100644
--- a/backend/apps/owasp/management/commands/owasp_update_project_health_requirements.py
+++ b/backend/apps/owasp/management/commands/owasp_update_project_health_requirements.py
@@ -84,7 +84,7 @@ class Command(BaseCommand):
},
}
- def add_arguments(self, parser):
+ def add_arguments(self, parser) -> None:
parser.add_argument(
"--level",
type=str,
@@ -115,7 +115,8 @@ def get_level_requirements(self, level):
return self.LEVEL_REQUIREMENTS.get(level, defaults)
- def handle(self, *args, **options):
+ def handle(self, *args, **options) -> None:
+ """Handle the command execution."""
level = options.get("level")
if level:
diff --git a/backend/apps/owasp/management/commands/owasp_update_sponsors.py b/backend/apps/owasp/management/commands/owasp_update_sponsors.py
index 52963d8e87..12f56f1fbb 100644
--- a/backend/apps/owasp/management/commands/owasp_update_sponsors.py
+++ b/backend/apps/owasp/management/commands/owasp_update_sponsors.py
@@ -10,14 +10,8 @@
class Command(BaseCommand):
help = "Import sponsors from the provided YAML file"
- def handle(self, *args, **kwargs):
- """Handle the command execution.
-
- Args:
- *args: Variable length argument list.
- **kwargs: Arbitrary keyword arguments.
-
- """
+ def handle(self, *args, **kwargs) -> None:
+ """Handle the command execution."""
sponsors = yaml.safe_load(
get_repository_file_content(
"https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/corp_members.yml"
diff --git a/backend/apps/owasp/migrations/0016_remove_event_created_at_and_more.py b/backend/apps/owasp/migrations/0016_remove_event_created_at_and_more.py
index 9bf0bcbf1f..be2204dc85 100644
--- a/backend/apps/owasp/migrations/0016_remove_event_created_at_and_more.py
+++ b/backend/apps/owasp/migrations/0016_remove_event_created_at_and_more.py
@@ -71,9 +71,7 @@ class Migration(migrations.Migration):
model_name="event",
name="start_date",
field=models.DateField(
- default=datetime.datetime(
- 2025, 2, 28, 7, 53, 17, 155842, tzinfo=datetime.timezone.utc
- ),
+ default=datetime.datetime(2025, 2, 28, 7, 53, 17, 155842, tzinfo=datetime.UTC),
verbose_name="Start Date",
),
preserve_default=False,
diff --git a/backend/apps/owasp/models/chapter.py b/backend/apps/owasp/models/chapter.py
index d0e13bfe72..aca2cc71f9 100644
--- a/backend/apps/owasp/models/chapter.py
+++ b/backend/apps/owasp/models/chapter.py
@@ -1,5 +1,7 @@
"""OWASP app chapter model."""
+from __future__ import annotations
+
from functools import lru_cache
from django.db import models
@@ -60,7 +62,7 @@ class Meta:
latitude = models.FloatField(verbose_name="Latitude", blank=True, null=True)
longitude = models.FloatField(verbose_name="Longitude", blank=True, null=True)
- def __str__(self):
+ def __str__(self) -> str:
"""Chapter human readable representation."""
return f"{self.name or self.key}"
@@ -75,7 +77,7 @@ def active_chapters_count():
"""Return active chapters count."""
return IndexBase.get_total_count("chapters", search_filters="idx_is_active:true")
- def from_github(self, repository):
+ def from_github(self, repository) -> None:
"""Update instance based on GitHub repository data.
Args:
@@ -101,7 +103,7 @@ def from_github(self, repository):
self.created_at = repository.created_at
self.updated_at = repository.updated_at
- def generate_geo_location(self):
+ def generate_geo_location(self) -> None:
"""Add latitude and longitude data based on suggested location or geo string."""
location = None
if self.suggested_location and self.suggested_location != "None":
@@ -113,7 +115,11 @@ def generate_geo_location(self):
self.latitude = location.latitude
self.longitude = location.longitude
- def generate_suggested_location(self, open_ai=None, max_tokens=100):
+ def generate_suggested_location(
+ self,
+ open_ai: OpenAi | None = None,
+ max_tokens: int = 100,
+ ) -> None:
"""Generate a suggested location using OpenAI.
Args:
@@ -132,7 +138,7 @@ def generate_suggested_location(self, open_ai=None, max_tokens=100):
suggested_location if suggested_location and suggested_location != "None" else ""
)
- def get_geo_string(self, include_name=True):
+ def get_geo_string(self, *, include_name: bool = True) -> str:
"""Return a geo string for the chapter.
Args:
@@ -143,22 +149,16 @@ def get_geo_string(self, include_name=True):
"""
return join_values(
- (
+ [
self.name.replace("OWASP", "").strip() if include_name else "",
self.country,
self.postal_code,
- ),
+ ],
delimiter=", ",
)
- def save(self, *args, **kwargs):
- """Save the chapter instance.
-
- Args:
- *args: Variable length argument list.
- **kwargs: Arbitrary keyword arguments.
-
- """
+ def save(self, *args, **kwargs) -> None:
+ """Save the chapter instance."""
if not self.suggested_location:
self.generate_suggested_location()
@@ -168,7 +168,10 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)
@staticmethod
- def bulk_save(chapters, fields=None):
+ def bulk_save( # type: ignore[override]
+ chapters: list[Chapter],
+ fields: tuple[str, ...] | None = None,
+ ) -> None:
"""Bulk save chapters.
Args:
@@ -179,7 +182,7 @@ def bulk_save(chapters, fields=None):
BulkSaveModel.bulk_save(Chapter, chapters, fields=fields)
@staticmethod
- def update_data(gh_repository, repository, save=True):
+ def update_data(gh_repository, repository, *, save: bool = True) -> Chapter:
"""Update chapter data from GitHub repository.
Args:
diff --git a/backend/apps/owasp/models/committee.py b/backend/apps/owasp/models/committee.py
index c3ec2dede2..fc5a658143 100644
--- a/backend/apps/owasp/models/committee.py
+++ b/backend/apps/owasp/models/committee.py
@@ -27,11 +27,11 @@ class Meta:
db_table = "owasp_committees"
verbose_name_plural = "Committees"
- def __str__(self):
+ def __str__(self) -> str:
"""Committee human readable representation."""
return f"{self.name}"
- def from_github(self, repository):
+ def from_github(self, repository) -> None:
"""Update instance based on GitHub repository data."""
self.owasp_repository = repository
@@ -47,7 +47,7 @@ def from_github(self, repository):
self.created_at = repository.created_at
self.updated_at = repository.updated_at
- def save(self, *args, **kwargs):
+ def save(self, *args, **kwargs) -> None:
"""Save committee."""
if not self.summary and (prompt := Prompt.get_owasp_committee_summary()):
self.generate_summary(prompt=prompt)
@@ -66,12 +66,12 @@ def active_committees_count():
return IndexBase.get_total_count("committees")
@staticmethod
- def bulk_save(committees, fields=None):
+ def bulk_save(committees, fields=None) -> None: # type: ignore[override]
"""Bulk save committees."""
BulkSaveModel.bulk_save(Committee, committees, fields=fields)
@staticmethod
- def update_data(gh_repository, repository, save=True):
+ def update_data(gh_repository, repository, *, save: bool = True) -> "Committee":
"""Update committee data."""
key = gh_repository.name.lower()
try:
diff --git a/backend/apps/owasp/models/common.py b/backend/apps/owasp/models/common.py
index 2260bf247b..b45522c1c2 100644
--- a/backend/apps/owasp/models/common.py
+++ b/backend/apps/owasp/models/common.py
@@ -1,5 +1,7 @@
"""OWASP app common models."""
+from __future__ import annotations
+
import itertools
import logging
import re
@@ -72,12 +74,12 @@ class Meta:
)
@property
- def github_url(self):
+ def github_url(self) -> str:
"""Get GitHub URL."""
return f"https://github.com/owasp/{self.key}"
@property
- def index_md_url(self):
+ def index_md_url(self) -> str | None:
"""Return project's raw index.md GitHub URL."""
return (
"https://raw.githubusercontent.com/OWASP/"
@@ -87,7 +89,7 @@ def index_md_url(self):
)
@property
- def leaders_md_url(self):
+ def leaders_md_url(self) -> str | None:
"""Return entity's raw leaders.md GitHub URL."""
return (
"https://raw.githubusercontent.com/OWASP/"
@@ -97,12 +99,12 @@ def leaders_md_url(self):
)
@property
- def owasp_name(self):
+ def owasp_name(self) -> str:
"""Get OWASP name."""
return self.name if self.name.startswith("OWASP ") else f"OWASP {self.name}"
@property
- def owasp_url(self):
+ def owasp_url(self) -> str:
"""Get OWASP URL."""
return f"https://owasp.org/{self.key}"
@@ -173,7 +175,7 @@ def get_metadata(self):
extra={"repository": getattr(self.owasp_repository, "name", None)},
)
- def get_related_url(self, url, exclude_domains=(), include_domains=()):
+ def get_related_url(self, url, exclude_domains=(), include_domains=()) -> str | None:
"""Get OWASP entity related URL."""
if (
not url
@@ -195,7 +197,7 @@ def get_related_url(self, url, exclude_domains=(), include_domains=()):
return url
- def get_top_contributors(self, repositories=()):
+ def get_top_contributors(self, repositories=()) -> list[dict]:
"""Get top contributors."""
return [
{
@@ -215,7 +217,7 @@ def get_top_contributors(self, repositories=()):
.order_by("-total_contributions")[:TOP_CONTRIBUTORS_LIMIT]
]
- def parse_tags(self, tags):
+ def parse_tags(self, tags) -> list[str]:
"""Parse entity tags."""
if not tags:
return []
diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py
index 208a1805b6..a03494b080 100644
--- a/backend/apps/owasp/models/event.py
+++ b/backend/apps/owasp/models/event.py
@@ -1,5 +1,12 @@
"""OWASP app event model."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from datetime import date
+
from dateutil import parser
from django.db import models
from django.utils import timezone
@@ -49,7 +56,7 @@ class Category(models.TextChoices):
latitude = models.FloatField(verbose_name="Latitude", null=True, blank=True)
longitude = models.FloatField(verbose_name="Longitude", null=True, blank=True)
- def __str__(self):
+ def __str__(self) -> str:
"""Event human readable representation."""
return f"{self.name or self.key}"
@@ -68,7 +75,10 @@ def upcoming_events():
)
@staticmethod
- def bulk_save(events, fields=None):
+ def bulk_save( # type: ignore[override]
+ events: list,
+ fields: tuple[str, ...] | None = None,
+ ) -> None:
"""Bulk save events.
Args:
@@ -83,7 +93,7 @@ def bulk_save(events, fields=None):
# TODO(arkid15r): refactor this when there is a chance.
@staticmethod
- def parse_dates(dates, start_date):
+ def parse_dates(dates: str, start_date: date) -> date | None:
"""Parse event dates.
Args:
@@ -108,7 +118,7 @@ def parse_dates(dates, start_date):
if "-" in dates and "," in dates:
try:
# Split the date range into parts
- date_part, year = dates.rsplit(", ", 1)
+ date_part, year_part = dates.rsplit(", ", 1)
parts = date_part.split()
# Extract month and day range
@@ -119,7 +129,7 @@ def parse_dates(dates, start_date):
end_day = int(day_range.split("-")[-1])
# Parse the year
- year = int(year.strip())
+ year = int(year_part.strip())
# Use the start_date to determine the month if provided
if start_date:
@@ -127,15 +137,14 @@ def parse_dates(dates, start_date):
month = start_date_parsed.strftime("%B") # Full month name (e.g., "May")
# Parse the full end date string
- end_date_str = f"{month} {end_day}, {year}"
- return parser.parse(end_date_str).date()
+ return parser.parse(f"{month} {end_day}, {year}").date()
except (ValueError, IndexError, AttributeError):
return None
return None
@staticmethod
- def update_data(category, data, save=True):
+ def update_data(category, data, *, save: bool = True) -> Event:
"""Update event data.
Args:
@@ -159,7 +168,7 @@ def update_data(category, data, save=True):
return event
- def from_dict(self, category, data):
+ def from_dict(self, category: str, data: dict) -> None:
"""Update instance based on the dict data.
Args:
@@ -177,7 +186,7 @@ def from_dict(self, category, data):
"Partner": Event.Category.PARTNER,
}.get(category, Event.Category.OTHER),
"description": data.get("optional-text", ""),
- "end_date": Event.parse_dates(data.get("dates", ""), data.get("start-date")),
+ "end_date": Event.parse_dates(data.get("dates", ""), data["start-date"]),
"name": data["name"],
"start_date": parser.parse(data["start-date"]).date()
if isinstance(data["start-date"], str)
@@ -188,7 +197,7 @@ def from_dict(self, category, data):
for key, value in fields.items():
setattr(self, key, value)
- def generate_geo_location(self):
+ def generate_geo_location(self) -> None:
"""Add latitude and longitude data.
Returns:
@@ -204,7 +213,7 @@ def generate_geo_location(self):
self.latitude = location.latitude
self.longitude = location.longitude
- def generate_suggested_location(self, prompt):
+ def generate_suggested_location(self, prompt) -> None:
"""Generate a suggested location for the event.
Args:
@@ -225,7 +234,7 @@ def generate_suggested_location(self, prompt):
except (ValueError, TypeError):
self.suggested_location = ""
- def generate_summary(self, prompt):
+ def generate_summary(self, prompt) -> None:
"""Generate a summary for the event.
Args:
@@ -244,7 +253,7 @@ def generate_summary(self, prompt):
except (ValueError, TypeError):
self.summary = ""
- def get_context(self, include_dates=False):
+ def get_context(self, *, include_dates: bool = False) -> str:
"""Return geo string.
Args:
diff --git a/backend/apps/owasp/models/mixins/chapter.py b/backend/apps/owasp/models/mixins/chapter.py
index f5e5c078c3..79305ea102 100644
--- a/backend/apps/owasp/models/mixins/chapter.py
+++ b/backend/apps/owasp/models/mixins/chapter.py
@@ -7,7 +7,7 @@ class ChapterIndexMixin(RepositoryBasedEntityModelMixin):
"""Chapter index mixin."""
@property
- def is_indexable(self):
+ def is_indexable(self) -> bool:
"""Chapters to index."""
return (
self.latitude is not None
@@ -16,61 +16,61 @@ def is_indexable(self):
)
@property
- def idx_country(self):
+ def idx_country(self) -> str:
"""Return country for indexing."""
return self.country
@property
- def idx_created_at(self):
+ def idx_created_at(self) -> float:
"""Return created at for indexing."""
return (self.created_at or self.owasp_repository.created_at).timestamp()
@property
- def idx_geo_location(self):
+ def idx_geo_location(self) -> tuple[float, float]:
"""Return geographic location for indexing."""
return self.latitude, self.longitude
@property
- def idx_is_active(self):
+ def idx_is_active(self) -> bool:
"""Return active status for indexing."""
return self.is_active
@property
- def idx_key(self):
+ def idx_key(self) -> str:
"""Return key for indexing."""
return self.key.replace("www-chapter-", "")
@property
- def idx_meetup_group(self):
+ def idx_meetup_group(self) -> str:
"""Return meetup group for indexing."""
return self.meetup_group
@property
- def idx_postal_code(self):
+ def idx_postal_code(self) -> str:
"""Return postal code for indexing."""
return self.postal_code
@property
- def idx_region(self):
+ def idx_region(self) -> str:
"""Return region for indexing."""
return self.region
@property
- def idx_related_urls(self):
+ def idx_related_urls(self) -> list:
"""Return related URLs for indexing."""
return self.related_urls
@property
- def idx_suggested_location(self):
+ def idx_suggested_location(self) -> str:
"""Return suggested location for indexing."""
return self.suggested_location if self.suggested_location != "None" else ""
@property
- def idx_top_contributors(self):
+ def idx_top_contributors(self) -> list:
"""Return top contributors for indexing."""
- return super().get_top_contributors(repositories=[self.owasp_repository])
+ return self.get_top_contributors(repositories=[self.owasp_repository])
@property
- def idx_updated_at(self):
+ def idx_updated_at(self) -> float:
"""Return updated at for indexing."""
return (self.updated_at or self.owasp_repository.updated_at).timestamp()
diff --git a/backend/apps/owasp/models/mixins/committee.py b/backend/apps/owasp/models/mixins/committee.py
index fe7bd84efa..e5db99ee9e 100644
--- a/backend/apps/owasp/models/mixins/committee.py
+++ b/backend/apps/owasp/models/mixins/committee.py
@@ -1,5 +1,7 @@
"""OWASP app committee mixins."""
+from __future__ import annotations
+
from apps.owasp.models.mixins.common import RepositoryBasedEntityModelMixin
@@ -22,9 +24,9 @@ def idx_related_urls(self):
return self.related_urls
@property
- def idx_top_contributors(self):
+ def idx_top_contributors(self) -> list[str]:
"""Return top contributors for indexing."""
- return super().get_top_contributors(repositories=[self.owasp_repository])
+ return self.get_top_contributors(repositories=[self.owasp_repository])
@property
def idx_updated_at(self):
diff --git a/backend/apps/owasp/models/mixins/project.py b/backend/apps/owasp/models/mixins/project.py
index 1955461afc..5a7bcda38b 100644
--- a/backend/apps/owasp/models/mixins/project.py
+++ b/backend/apps/owasp/models/mixins/project.py
@@ -1,5 +1,7 @@
"""OWASP app project mixins."""
+from __future__ import annotations
+
from apps.common.utils import join_values
from apps.owasp.models.mixins.common import RepositoryBasedEntityModelMixin
@@ -12,67 +14,67 @@ class ProjectIndexMixin(RepositoryBasedEntityModelMixin):
"""Project index mixin."""
@property
- def idx_companies(self):
+ def idx_companies(self) -> str:
"""Return companies for indexing."""
- return join_values(fields=(o.company for o in self.organizations.all()))
+ return join_values(fields=[o.company for o in self.organizations.all()])
@property
- def idx_contributors_count(self):
+ def idx_contributors_count(self) -> int:
"""Return contributors count for indexing."""
return self.contributors_count
@property
- def idx_custom_tags(self):
+ def idx_custom_tags(self) -> str:
"""Return custom tags for indexing."""
return self.custom_tags
@property
- def idx_forks_count(self):
+ def idx_forks_count(self) -> int:
"""Return forks count for indexing."""
return self.forks_count
@property
- def idx_is_active(self):
+ def idx_is_active(self) -> bool:
"""Return active status for indexing."""
return self.is_active
@property
- def idx_issues_count(self):
+ def idx_issues_count(self) -> int:
"""Return issues count for indexing."""
return self.open_issues.count()
@property
- def idx_key(self):
+ def idx_key(self) -> str:
"""Return key for indexing."""
return self.key.replace("www-project-", "")
@property
- def idx_languages(self):
+ def idx_languages(self) -> list[str]:
"""Return languages for indexing."""
return self.languages
@property
- def idx_level(self):
+ def idx_level(self) -> str:
"""Return level text value for indexing."""
return self.level
@property
- def idx_level_raw(self):
+ def idx_level_raw(self) -> float | None:
"""Return level for indexing."""
return float(self.level_raw) if self.level_raw else None
@property
- def idx_name(self):
+ def idx_name(self) -> str:
"""Return name for indexing."""
return self.name or " ".join(self.key.replace("www-project-", "").capitalize().split("-"))
@property
- def idx_organizations(self):
+ def idx_organizations(self) -> str:
"""Return organizations for indexing."""
- return join_values(fields=(o.name for o in self.organizations.all()))
+ return join_values(fields=[o.name for o in self.organizations.all()])
@property
- def idx_repositories(self):
+ def idx_repositories(self) -> list[dict]:
"""Return repositories for indexing."""
return [
{
@@ -90,26 +92,26 @@ def idx_repositories(self):
]
@property
- def idx_repositories_count(self):
+ def idx_repositories_count(self) -> int:
"""Return repositories count for indexing."""
return self.repositories.count()
@property
- def idx_stars_count(self):
+ def idx_stars_count(self) -> int:
"""Return stars count for indexing."""
return self.stars_count
@property
- def idx_top_contributors(self):
+ def idx_top_contributors(self) -> list:
"""Return top contributors for indexing."""
- return super().get_top_contributors(repositories=self.repositories.all())
+ return self.get_top_contributors(repositories=self.repositories.all())
@property
- def idx_type(self):
+ def idx_type(self) -> str:
"""Return type for indexing."""
return self.type
@property
- def idx_updated_at(self):
+ def idx_updated_at(self) -> str | float:
"""Return updated at for indexing."""
return self.updated_at.timestamp() if self.updated_at else ""
diff --git a/backend/apps/owasp/models/post.py b/backend/apps/owasp/models/post.py
index 427ff440db..2ef681f7a5 100644
--- a/backend/apps/owasp/models/post.py
+++ b/backend/apps/owasp/models/post.py
@@ -1,6 +1,6 @@
"""OWASP app post model."""
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from django.db import models
from django.utils.dateparse import parse_datetime
@@ -29,7 +29,7 @@ def __str__(self):
return self.title
@staticmethod
- def bulk_save(posts, fields=None):
+ def bulk_save(posts, fields=None) -> None: # type: ignore[override]
"""Bulk save posts."""
BulkSaveModel.bulk_save(Post, posts, fields=fields)
@@ -39,7 +39,7 @@ def recent_posts():
return Post.objects.order_by("-published_at")
@staticmethod
- def update_data(data, save=True):
+ def update_data(data, *, save: bool = True) -> "Post":
"""Update post data."""
url = data.get("url")
@@ -54,7 +54,7 @@ def update_data(data, save=True):
return post
- def from_dict(self, data):
+ def from_dict(self, data) -> None:
"""Update instance based on dict data."""
published_at = data["published_at"]
published_at = (
@@ -64,7 +64,7 @@ def from_dict(self, data):
published_at.year,
published_at.month,
published_at.day,
- tzinfo=timezone.utc,
+ tzinfo=UTC,
)
)
diff --git a/backend/apps/owasp/models/project.py b/backend/apps/owasp/models/project.py
index 7da4cedd29..6ff6afdac8 100644
--- a/backend/apps/owasp/models/project.py
+++ b/backend/apps/owasp/models/project.py
@@ -1,5 +1,7 @@
"""OWASP app project models."""
+from __future__ import annotations
+
from functools import lru_cache
from django.db import models
@@ -122,22 +124,22 @@ class ProjectType(models.TextChoices):
blank=True,
)
- def __str__(self):
+ def __str__(self) -> str:
"""Project human readable representation."""
return f"{self.name or self.key}"
@property
- def is_code_type(self):
+ def is_code_type(self) -> bool:
"""Indicate whether project has CODE type."""
return self.type == self.ProjectType.CODE
@property
- def is_documentation_type(self):
+ def is_documentation_type(self) -> bool:
"""Indicate whether project has DOCUMENTATION type."""
return self.type == self.ProjectType.DOCUMENTATION
@property
- def is_tool_type(self):
+ def is_tool_type(self) -> bool:
"""Indicate whether project has TOOL type."""
return self.type == self.ProjectType.TOOL
@@ -151,12 +153,12 @@ def issues(self):
)
@property
- def nest_key(self):
+ def nest_key(self) -> str:
"""Get Nest key."""
return self.key.replace("www-project-", "")
@property
- def nest_url(self):
+ def nest_url(self) -> str:
"""Get Nest URL for project."""
return get_absolute_url(f"projects/{self.nest_key}")
@@ -180,12 +182,12 @@ def published_releases(self):
"repository",
)
- def deactivate(self):
+ def deactivate(self) -> None:
"""Deactivate project."""
self.is_active = False
self.save(update_fields=("is_active",))
- def from_github(self, repository):
+ def from_github(self, repository) -> None:
"""Update instance based on GitHub repository data.
Args:
@@ -226,14 +228,8 @@ def from_github(self, repository):
self.created_at = repository.created_at
self.updated_at = repository.updated_at
- def save(self, *args, **kwargs):
- """Save the project instance.
-
- Args:
- *args: Variable length argument list.
- **kwargs: Arbitrary keyword arguments.
-
- """
+ def save(self, *args, **kwargs) -> None:
+ """Save the project instance."""
if self.is_active and not self.summary and (prompt := Prompt.get_owasp_project_summary()):
self.generate_summary(prompt=prompt)
@@ -246,7 +242,7 @@ def active_projects_count():
return IndexBase.get_total_count("projects", search_filters="idx_is_active:true")
@staticmethod
- def bulk_save(projects, fields=None):
+ def bulk_save(projects: list, fields: list | None = None) -> None: # type: ignore[override]
"""Bulk save projects.
Args:
@@ -257,7 +253,7 @@ def bulk_save(projects, fields=None):
BulkSaveModel.bulk_save(Project, projects, fields=fields)
@staticmethod
- def update_data(gh_repository, repository, save=True):
+ def update_data(gh_repository, repository, *, save: bool = True) -> Project:
"""Update project data from GitHub repository.
Args:
diff --git a/backend/apps/owasp/models/project_health_metrics.py b/backend/apps/owasp/models/project_health_metrics.py
index 5b928601cf..9e7a35dbb9 100644
--- a/backend/apps/owasp/models/project_health_metrics.py
+++ b/backend/apps/owasp/models/project_health_metrics.py
@@ -62,6 +62,6 @@ class Meta:
verbose_name="Unassigned issues", default=0
)
- def __str__(self):
+ def __str__(self) -> str:
"""Project health metrics human readable representation."""
return f"Health Metrics for {self.project.name}"
diff --git a/backend/apps/owasp/models/project_health_requirements.py b/backend/apps/owasp/models/project_health_requirements.py
index c12477fbc7..a5f82795f4 100644
--- a/backend/apps/owasp/models/project_health_requirements.py
+++ b/backend/apps/owasp/models/project_health_requirements.py
@@ -52,6 +52,6 @@ class Meta:
verbose_name="Unassigned issues", default=0
)
- def __str__(self):
+ def __str__(self) -> str:
"""Project health requirements human readable representation."""
return f"Health Requirements for {self.get_level_display()} Projects"
diff --git a/backend/apps/owasp/models/snapshot.py b/backend/apps/owasp/models/snapshot.py
index 6b4edf7acc..a6285f9529 100644
--- a/backend/apps/owasp/models/snapshot.py
+++ b/backend/apps/owasp/models/snapshot.py
@@ -39,14 +39,8 @@ def __str__(self):
"""Return a string representation of the snapshot."""
return self.title
- def save(self, *args, **kwargs):
- """Save the snapshot instance.
-
- Args:
- *args: Variable length argument list.
- **kwargs: Arbitrary keyword arguments.
-
- """
+ def save(self, *args, **kwargs) -> None:
+ """Save the snapshot instance."""
if not self.key: # automatically set the key
self.key = now().strftime("%Y-%m")
diff --git a/backend/apps/owasp/models/sponsor.py b/backend/apps/owasp/models/sponsor.py
index 142bbb16b0..f69383473b 100644
--- a/backend/apps/owasp/models/sponsor.py
+++ b/backend/apps/owasp/models/sponsor.py
@@ -1,5 +1,7 @@
"""OWASP app sponsor models."""
+from __future__ import annotations
+
from django.db import models
from apps.common.models import BulkSaveModel, TimestampedModel
@@ -56,33 +58,36 @@ class MemberType(models.TextChoices):
default=SponsorType.NOT_SPONSOR,
)
- def __str__(self):
+ def __str__(self) -> str:
"""Sponsor human readable representation."""
return f"{self.name}"
@property
- def readable_member_type(self):
+ def readable_member_type(self) -> str:
"""Get human-readable member type."""
return self.MemberType(self.member_type).label
@property
- def readable_sponsor_type(self):
+ def readable_sponsor_type(self) -> str:
"""Get human-readable sponsor type."""
return self.SponsorType(self.sponsor_type).label
@staticmethod
- def bulk_save(sponsors, fields=None):
+ def bulk_save( # type: ignore[override]
+ sponsors: list[Sponsor],
+ fields: tuple[str, ...] | None = None,
+ ) -> None:
"""Bulk save sponsors.
Args:
sponsors (list[Sponsor]): List of Sponsor instances to save.
- fields (list[str], optional): List of fields to update.
+ fields (tuple[str], optional): Tuple of fields to update.
"""
BulkSaveModel.bulk_save(Sponsor, sponsors, fields=fields)
@staticmethod
- def update_data(data, save=True):
+ def update_data(data: dict, *, save: bool = True) -> Sponsor:
"""Update sponsor data.
Args:
@@ -106,7 +111,7 @@ def update_data(data, save=True):
return sponsor
- def from_dict(self, data):
+ def from_dict(self, data: dict) -> None:
"""Update instance based on the dictionary data.
Args:
diff --git a/backend/apps/owasp/scraper.py b/backend/apps/owasp/scraper.py
index eb802db97f..1bc41c1ce2 100644
--- a/backend/apps/owasp/scraper.py
+++ b/backend/apps/owasp/scraper.py
@@ -1,5 +1,7 @@
"""OWASP scraper."""
+from __future__ import annotations
+
import logging
from urllib.parse import urlparse
@@ -8,7 +10,7 @@
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
MAX_RETRIES = 3
TIMEOUT = 5, 10
@@ -17,7 +19,7 @@
class OwaspScraper:
"""OWASP scraper."""
- def __init__(self, url):
+ def __init__(self, url: bytes | str) -> None:
"""Create OWASP site scraper."""
self.page_tree = None
diff --git a/backend/apps/slack/__init__.py b/backend/apps/slack/__init__.py
index 6fd2991a68..00cb48132f 100644
--- a/backend/apps/slack/__init__.py
+++ b/backend/apps/slack/__init__.py
@@ -1,3 +1,5 @@
-from apps.slack.actions import * # noqa: F403
-from apps.slack.commands import * # noqa: F403
-from apps.slack.events import * # noqa: F403
+from apps.slack import (
+ actions,
+ commands,
+ events,
+)
diff --git a/backend/apps/slack/actions/home.py b/backend/apps/slack/actions/home.py
index 6c0e120e35..443b632192 100644
--- a/backend/apps/slack/actions/home.py
+++ b/backend/apps/slack/actions/home.py
@@ -2,6 +2,7 @@
import logging
+from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from apps.slack.apps import SlackConfig
@@ -23,10 +24,10 @@
VIEW_PROJECTS_ACTION_PREV,
)
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def handle_home_actions(ack, body, client):
+def handle_home_actions(ack, body, client: WebClient) -> None:
"""Handle actions triggered in the home view."""
ack()
diff --git a/backend/apps/slack/apps.py b/backend/apps/slack/apps.py
index e6f89d25ab..b7e02e1a71 100644
--- a/backend/apps/slack/apps.py
+++ b/backend/apps/slack/apps.py
@@ -3,8 +3,9 @@
from django.apps import AppConfig
from django.conf import settings
from slack_bolt import App
+from slack_sdk import WebClient
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
class SlackConfig(AppConfig):
@@ -24,7 +25,7 @@ class SlackConfig(AppConfig):
if SlackConfig.app:
@SlackConfig.app.error
- def error_handler(error, body, *args, **kwargs): # noqa: ARG001
+ def error_handler(error, body, *args, **kwargs) -> None: # noqa: ARG001
"""Handle Slack application errors.
Args:
@@ -37,7 +38,13 @@ def error_handler(error, body, *args, **kwargs): # noqa: ARG001
logger.exception(error, extra={"body": body})
@SlackConfig.app.use
- def log_events(client, context, logger, payload, next): # noqa: A002, ARG001
+ def log_events(
+ client: WebClient, # noqa: ARG001
+ context: dict,
+ logger: logging.Logger,
+ payload: dict,
+ next, # noqa: A002
+ ) -> None:
"""Log Slack events.
Args:
diff --git a/backend/apps/slack/blocks.py b/backend/apps/slack/blocks.py
index 5ef81dfbce..ce8eb25432 100644
--- a/backend/apps/slack/blocks.py
+++ b/backend/apps/slack/blocks.py
@@ -1,7 +1,11 @@
"""Slack blocks."""
+from __future__ import annotations
-def divider():
+from typing import Any
+
+
+def divider() -> dict[str, str]:
"""Return a divider block.
Returns
@@ -11,7 +15,7 @@ def divider():
return {"type": "divider"}
-def markdown(text):
+def markdown(text: str) -> dict:
"""Return a markdown block.
Args:
@@ -27,7 +31,7 @@ def markdown(text):
}
-def get_header():
+def get_header() -> list[dict[str, Any]]:
"""Return the header block.
Returns
@@ -83,7 +87,7 @@ def get_header():
]
-def get_pagination_buttons(entity_type, page, total_pages):
+def get_pagination_buttons(entity_type: str, page: int, total_pages: int) -> list[dict[str, Any]]:
"""Get pagination buttons for Slack blocks.
Args:
diff --git a/backend/apps/slack/commands/board.py b/backend/apps/slack/commands/board.py
index f8da6c859e..f9e543c3cc 100644
--- a/backend/apps/slack/commands/board.py
+++ b/backend/apps/slack/commands/board.py
@@ -10,8 +10,16 @@ def get_template_file_name(self):
"""Get the template file name."""
return "navigate.jinja"
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ string: The rendered text.
+
+ """
return {
**super().get_template_context(command),
"name": "Global board",
diff --git a/backend/apps/slack/commands/chapters.py b/backend/apps/slack/commands/chapters.py
index 3faa35a7ba..05b1f431ee 100644
--- a/backend/apps/slack/commands/chapters.py
+++ b/backend/apps/slack/commands/chapters.py
@@ -9,14 +9,22 @@
class Chapters(CommandBase):
"""Slack bot /chapters command."""
- def get_render_blocks(self, command):
- """Get the rendered blocks."""
+ def get_render_blocks(self, command: dict):
+ """Get the rendered blocks.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ list: A list of Slack blocks representing the projects.
+
+ """
command_text = command["text"].strip()
if command_text in COMMAND_HELP:
return super().get_render_blocks(command)
- search_query = "" if command_text in COMMAND_START else command_text
+
return get_blocks(
- search_query=search_query,
+ search_query="" if command_text in COMMAND_START else command_text,
limit=10,
presentation=EntityPresentation(
include_feedback=True,
diff --git a/backend/apps/slack/commands/committees.py b/backend/apps/slack/commands/committees.py
index 403cb3a1f6..dec803d2fd 100644
--- a/backend/apps/slack/commands/committees.py
+++ b/backend/apps/slack/commands/committees.py
@@ -8,8 +8,16 @@
class Committees(CommandBase):
"""Slack bot /committees command."""
- def get_render_blocks(self, command):
- """Get the rendered blocks."""
+ def get_render_blocks(self, command: dict):
+ """Get the rendered blocks.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ list: A list of Slack blocks representing the projects.
+
+ """
return get_blocks(
search_query=command["text"].strip(),
limit=10,
diff --git a/backend/apps/slack/commands/community.py b/backend/apps/slack/commands/community.py
index 2e36b21ece..cebe3a5cfc 100644
--- a/backend/apps/slack/commands/community.py
+++ b/backend/apps/slack/commands/community.py
@@ -11,8 +11,16 @@ def get_template_file_name(self):
"""Get the template file name."""
return "navigate.jinja"
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
return {
**super().get_template_context(command),
"name": "OWASP community",
diff --git a/backend/apps/slack/commands/contact.py b/backend/apps/slack/commands/contact.py
index e71e37cc41..2f40089eb3 100644
--- a/backend/apps/slack/commands/contact.py
+++ b/backend/apps/slack/commands/contact.py
@@ -10,8 +10,16 @@ def get_template_file_name(self):
"""Get the template file name."""
return "navigate.jinja"
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
return {
**super().get_template_context(command),
"name": "OWASP contact",
diff --git a/backend/apps/slack/commands/contribute.py b/backend/apps/slack/commands/contribute.py
index c0c3227134..713a8258dd 100644
--- a/backend/apps/slack/commands/contribute.py
+++ b/backend/apps/slack/commands/contribute.py
@@ -10,7 +10,15 @@ class Contribute(CommandBase):
"""Slack bot /contribute command."""
def get_render_blocks(self, command):
- """Get the rendered blocks."""
+ """Get the rendered blocks.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ list: A list of Slack blocks representing the projects.
+
+ """
command_text = command["text"].strip()
if command_text in COMMAND_HELP:
return super().get_render_blocks(command)
@@ -28,8 +36,16 @@ def get_render_blocks(self, command):
),
)
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
return {
**super().get_template_context(command),
}
diff --git a/backend/apps/slack/commands/donate.py b/backend/apps/slack/commands/donate.py
index ef30b525d8..689e90cf98 100644
--- a/backend/apps/slack/commands/donate.py
+++ b/backend/apps/slack/commands/donate.py
@@ -7,8 +7,16 @@
class Donate(CommandBase):
"""Slack bot /donate command."""
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
return {
**super().get_template_context(command),
"website_url": OWASP_WEBSITE_URL,
diff --git a/backend/apps/slack/commands/gsoc.py b/backend/apps/slack/commands/gsoc.py
index d6b3b89cd6..6b88674ed4 100644
--- a/backend/apps/slack/commands/gsoc.py
+++ b/backend/apps/slack/commands/gsoc.py
@@ -24,8 +24,16 @@
class Gsoc(CommandBase):
"""Slack bot /gsoc command."""
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
now = timezone.now()
gsoc_year = now.year if now.month > MARCH else now.year - 1
command_text = command["text"].strip()
diff --git a/backend/apps/slack/commands/jobs.py b/backend/apps/slack/commands/jobs.py
index fd2028ac00..8c2f0e3ebc 100644
--- a/backend/apps/slack/commands/jobs.py
+++ b/backend/apps/slack/commands/jobs.py
@@ -11,8 +11,16 @@
class Jobs(CommandBase):
"""Slack bot /jobs command."""
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
return {
**super().get_template_context(command),
"feedback_channel": OWASP_PROJECT_NEST_CHANNEL_ID,
diff --git a/backend/apps/slack/commands/leaders.py b/backend/apps/slack/commands/leaders.py
index 61ec147b89..a38ce6b997 100644
--- a/backend/apps/slack/commands/leaders.py
+++ b/backend/apps/slack/commands/leaders.py
@@ -7,8 +7,16 @@
class Leaders(CommandBase):
"""Slack bot /leaders command."""
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
from apps.owasp.api.search.chapter import get_chapters
from apps.owasp.api.search.project import get_projects
diff --git a/backend/apps/slack/commands/news.py b/backend/apps/slack/commands/news.py
index dcefbe4687..c95d5ffb1b 100644
--- a/backend/apps/slack/commands/news.py
+++ b/backend/apps/slack/commands/news.py
@@ -9,10 +9,17 @@ class News(CommandBase):
"""Slack bot /news command."""
def get_template_context(self, command):
- """Get the template context."""
- items = get_news_data()
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
return {
**super().get_template_context(command),
- "news_items": items,
+ "news_items": get_news_data(),
"news_url": OWASP_NEWS_URL,
}
diff --git a/backend/apps/slack/commands/owasp.py b/backend/apps/slack/commands/owasp.py
index 4127e27547..08ede9cf13 100644
--- a/backend/apps/slack/commands/owasp.py
+++ b/backend/apps/slack/commands/owasp.py
@@ -7,7 +7,7 @@
class Owasp(CommandBase):
"""Slack bot /owasp command."""
- def find_command(self, command_name):
+ def find_command(self, command_name: str):
"""Find the command class by name."""
if not command_name:
return None
@@ -27,8 +27,16 @@ def handler(self, ack, command, client):
return super().handler(ack, command, client)
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
command_tokens = command["text"].split()
if not command_tokens or command_tokens[0] in COMMAND_HELP:
return {
diff --git a/backend/apps/slack/commands/policies.py b/backend/apps/slack/commands/policies.py
index 38e33a7eb0..a749f9f51a 100644
--- a/backend/apps/slack/commands/policies.py
+++ b/backend/apps/slack/commands/policies.py
@@ -7,7 +7,15 @@ class Policies(CommandBase):
"""Slack bot /policies command."""
def get_template_context(self, command):
- """Get the template context."""
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
policies = (
("Chapters Policy", "https://owasp.org/www-policy/operational/chapters"),
("Code of Conduct", "https://owasp.org/www-policy/operational/code-of-conduct"),
diff --git a/backend/apps/slack/commands/projects.py b/backend/apps/slack/commands/projects.py
index 5dee967c02..147b138671 100644
--- a/backend/apps/slack/commands/projects.py
+++ b/backend/apps/slack/commands/projects.py
@@ -8,8 +8,16 @@
class Projects(CommandBase):
"""Slack bot /projects command."""
- def get_render_blocks(self, command):
- """Get the rendered blocks."""
+ def get_render_blocks(self, command: dict):
+ """Get the rendered blocks.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ list: A list of Slack blocks representing the projects.
+
+ """
return get_blocks(
search_query=command["text"].strip(),
limit=10,
diff --git a/backend/apps/slack/commands/sponsor.py b/backend/apps/slack/commands/sponsor.py
index 044b635c4e..3fafbbc264 100644
--- a/backend/apps/slack/commands/sponsor.py
+++ b/backend/apps/slack/commands/sponsor.py
@@ -7,5 +7,13 @@ class Sponsor(CommandBase):
"""Slack bot /sponsor command."""
def get_render_text(self, command):
- """Get the rendered text."""
+ """Get the rendered text.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ string: The rendered text.
+
+ """
return "Coming soon..."
diff --git a/backend/apps/slack/commands/sponsors.py b/backend/apps/slack/commands/sponsors.py
index d6b4c1177b..eaf476c4c4 100644
--- a/backend/apps/slack/commands/sponsors.py
+++ b/backend/apps/slack/commands/sponsors.py
@@ -10,7 +10,15 @@ class Sponsors(CommandBase):
"""Slack bot /sponsors command."""
def get_template_context(self, command):
- """Get the template context."""
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
sponsors = get_sponsors_data()
return {
**super().get_template_context(command),
diff --git a/backend/apps/slack/commands/staff.py b/backend/apps/slack/commands/staff.py
index 4c26b01576..248988b5d5 100644
--- a/backend/apps/slack/commands/staff.py
+++ b/backend/apps/slack/commands/staff.py
@@ -8,8 +8,16 @@
class Staff(CommandBase):
"""Slack bot /staff command."""
- def get_template_context(self, command):
- """Get the template context."""
+ def get_template_context(self, command: dict):
+ """Get the template context.
+
+ Args:
+ command (dict): The Slack command payload.
+
+ Returns:
+ dict: The template context.
+
+ """
items = get_staff_data()
return {
**super().get_template_context(command),
diff --git a/backend/apps/slack/commands/users.py b/backend/apps/slack/commands/users.py
index 4c4d3ea012..cf239c67a5 100644
--- a/backend/apps/slack/commands/users.py
+++ b/backend/apps/slack/commands/users.py
@@ -8,7 +8,7 @@
class Users(CommandBase):
"""Slack bot /users command."""
- def get_render_blocks(self, command):
+ def get_render_blocks(self, command: dict):
"""Get the rendered blocks."""
return get_blocks(
search_query=command["text"].strip(),
diff --git a/backend/apps/slack/common/handlers/chapters.py b/backend/apps/slack/common/handlers/chapters.py
index 3798745863..19c8265f62 100644
--- a/backend/apps/slack/common/handlers/chapters.py
+++ b/backend/apps/slack/common/handlers/chapters.py
@@ -15,15 +15,18 @@
def get_blocks(
- page=1, search_query: str = "", limit: int = 10, presentation: EntityPresentation | None = None
-):
+ limit: int = 10,
+ page: int = 1,
+ presentation: EntityPresentation | None = None,
+ search_query: str = "",
+) -> list:
"""Get chapters blocks.
Args:
- page (int): The current page number for pagination.
- search_query (str): The search query for filtering chapters.
limit (int): The maximum number of chapters to retrieve per page.
+ page (int): The current page number for pagination.
presentation (EntityPresentation | None): Configuration for entity presentation.
+ search_query (str): The search query for filtering chapters.
Returns:
list: A list of Slack blocks representing the chapters.
diff --git a/backend/apps/slack/common/handlers/committees.py b/backend/apps/slack/common/handlers/committees.py
index 38d7b2fba8..1789048ced 100644
--- a/backend/apps/slack/common/handlers/committees.py
+++ b/backend/apps/slack/common/handlers/committees.py
@@ -15,15 +15,18 @@
def get_blocks(
- page=1, search_query: str = "", limit: int = 10, presentation: EntityPresentation | None = None
-):
+ limit: int = 10,
+ page: int = 1,
+ presentation: EntityPresentation | None = None,
+ search_query: str = "",
+) -> list:
"""Get committees blocks.
Args:
- page (int): The current page number for pagination.
- search_query (str): The search query for filtering committees.
limit (int): The maximum number of committees to retrieve per page.
+ page (int): The current page number for pagination.
presentation (EntityPresentation | None): Configuration for entity presentation.
+ search_query (str): The search query for filtering committees.
Returns:
list: A list of Slack blocks representing the committees.
diff --git a/backend/apps/slack/common/handlers/contribute.py b/backend/apps/slack/common/handlers/contribute.py
index 34fc4c1971..ccce7a2728 100644
--- a/backend/apps/slack/common/handlers/contribute.py
+++ b/backend/apps/slack/common/handlers/contribute.py
@@ -13,15 +13,18 @@
def get_blocks(
- page=1, search_query: str = "", limit: int = 10, presentation: EntityPresentation | None = None
-):
+ limit: int = 10,
+ page: int = 1,
+ presentation: EntityPresentation | None = None,
+ search_query: str = "",
+) -> list:
"""Get contribute blocks.
Args:
- page (int): The current page number for pagination.
- search_query (str): The search query for filtering issues.
limit (int): The maximum number of issues to retrieve per page.
+ page (int): The current page number for pagination.
presentation (EntityPresentation | None): Configuration for entity presentation.
+ search_query (str): The search query for filtering issues.
Returns:
list: A list of Slack blocks representing the contribution issues.
diff --git a/backend/apps/slack/common/handlers/projects.py b/backend/apps/slack/common/handlers/projects.py
index 6c0e235831..6e4f0d64f6 100644
--- a/backend/apps/slack/common/handlers/projects.py
+++ b/backend/apps/slack/common/handlers/projects.py
@@ -15,15 +15,18 @@
def get_blocks(
- page=1, search_query: str = "", limit: int = 10, presentation: EntityPresentation | None = None
-):
+ limit: int = 10,
+ page: int = 1,
+ presentation: EntityPresentation | None = None,
+ search_query: str = "",
+) -> list[dict]:
"""Get projects blocks.
Args:
- page (int): The current page number for pagination.
- search_query (str): The search query for filtering projects.
limit (int): The maximum number of projects to retrieve per page.
+ page (int): The current page number for pagination.
presentation (EntityPresentation | None): Configuration for entity presentation.
+ search_query (str): The search query for filtering projects.
Returns:
list: A list of Slack blocks representing the projects.
diff --git a/backend/apps/slack/events/app_home_opened.py b/backend/apps/slack/events/app_home_opened.py
index bc7c4d03ba..285d939819 100644
--- a/backend/apps/slack/events/app_home_opened.py
+++ b/backend/apps/slack/events/app_home_opened.py
@@ -3,16 +3,17 @@
import logging
from django.conf import settings
+from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from apps.common.constants import NL, TAB
from apps.slack.apps import SlackConfig
from apps.slack.blocks import get_header, markdown
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def app_home_opened_handler(event, client, ack):
+def app_home_opened_handler(event: dict, client: WebClient, ack) -> None:
"""Handle the app_home_opened event.
Args:
diff --git a/backend/apps/slack/events/member_joined_channel/catch_all.py b/backend/apps/slack/events/member_joined_channel/catch_all.py
index 80e731e2b8..a58dcf49a7 100644
--- a/backend/apps/slack/events/member_joined_channel/catch_all.py
+++ b/backend/apps/slack/events/member_joined_channel/catch_all.py
@@ -1,10 +1,12 @@
"""Slack member joined any other channel handler."""
+from slack_sdk import WebClient
+
from apps.slack.apps import SlackConfig
from apps.slack.constants import OWASP_CONTRIBUTE_CHANNEL_ID, OWASP_GSOC_CHANNEL_ID
-def catch_all_handler(event, client, ack): # noqa: ARG001
+def catch_all_handler(event: dict, client: WebClient, ack) -> None: # noqa: ARG001
"""Slack new member cache all handler.
Args:
diff --git a/backend/apps/slack/events/member_joined_channel/contribute.py b/backend/apps/slack/events/member_joined_channel/contribute.py
index 24355f6559..dea4138ad8 100644
--- a/backend/apps/slack/events/member_joined_channel/contribute.py
+++ b/backend/apps/slack/events/member_joined_channel/contribute.py
@@ -3,6 +3,7 @@
import logging
from django.conf import settings
+from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from apps.common.constants import NL
@@ -17,10 +18,10 @@
)
from apps.slack.utils import get_text
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def contribute_handler(event, client, ack):
+def contribute_handler(event, client: WebClient, ack) -> None:
"""Slack #contribute new member handler.
Args:
@@ -53,7 +54,7 @@ def contribute_handler(event, client, ack):
user=user_id,
)
- blocks = [
+ blocks = (
markdown(
f"Hello <@{user_id}> and welcome to <{OWASP_CONTRIBUTE_CHANNEL_ID}> channel!{NL}"
"We're happy to have you here as part of the OWASP community! "
@@ -83,7 +84,7 @@ def contribute_handler(event, client, ack):
"contributions you'll make! "
),
markdown(f"{FEEDBACK_CHANNEL_MESSAGE}"),
- ]
+ )
client.chat_postMessage(
blocks=blocks,
channel=conversation["channel"]["id"],
diff --git a/backend/apps/slack/events/member_joined_channel/gsoc.py b/backend/apps/slack/events/member_joined_channel/gsoc.py
index 335279b19e..097891af4d 100644
--- a/backend/apps/slack/events/member_joined_channel/gsoc.py
+++ b/backend/apps/slack/events/member_joined_channel/gsoc.py
@@ -3,6 +3,7 @@
import logging
from django.conf import settings
+from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from apps.common.constants import NL
@@ -12,10 +13,10 @@
from apps.slack.constants import FEEDBACK_CHANNEL_MESSAGE, OWASP_GSOC_CHANNEL_ID
from apps.slack.utils import get_text
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def gsoc_handler(event, client, ack):
+def gsoc_handler(event: dict, client: WebClient, ack) -> None:
"""Slack #gsoc new member handler.
Args:
@@ -46,7 +47,7 @@ def gsoc_handler(event, client, ack):
user=user_id,
)
- blocks = [
+ blocks = (
markdown(
f"Hello <@{user_id}> and welcome to <{OWASP_GSOC_CHANNEL_ID}> channel!{NL}"
"Here's how you can start your journey toward contributing to OWASP projects and "
@@ -59,7 +60,7 @@ def gsoc_handler(event, client, ack):
"journey!"
),
markdown(f"{FEEDBACK_CHANNEL_MESSAGE}"),
- ]
+ )
client.chat_postMessage(
blocks=blocks,
channel=conversation["channel"]["id"],
diff --git a/backend/apps/slack/events/team_join.py b/backend/apps/slack/events/team_join.py
index fa5a2b905a..c65fc7a543 100644
--- a/backend/apps/slack/events/team_join.py
+++ b/backend/apps/slack/events/team_join.py
@@ -3,6 +3,7 @@
import logging
from django.conf import settings
+from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from apps.common.constants import NL
@@ -28,10 +29,10 @@
)
from apps.slack.utils import get_text
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def team_join_handler(event, client, ack):
+def team_join_handler(event: dict, client: WebClient, ack) -> None:
"""Handle the Slack team_join event.
Args:
@@ -54,7 +55,7 @@ def team_join_handler(event, client, ack):
logger.exception(client.users_info(user=user_id))
raise
- blocks = [
+ blocks = (
markdown(
f"*Welcome to the OWASP Slack Community, <@{user_id}>!*{NL}"
"We're excited to have you join us! Whether you're a newcomer to OWASP or "
@@ -104,7 +105,7 @@ def team_join_handler(event, client, ack):
"need help? Don't hesitate to ask -- this community thrives on collaboration!"
),
markdown(f"{FEEDBACK_CHANNEL_MESSAGE}"),
- ]
+ )
client.chat_postMessage(
blocks=blocks,
diff --git a/backend/apps/slack/models/event.py b/backend/apps/slack/models/event.py
index 69142cbdfa..e76e438c3a 100644
--- a/backend/apps/slack/models/event.py
+++ b/backend/apps/slack/models/event.py
@@ -22,7 +22,7 @@ class Meta:
user_id = models.CharField(verbose_name="User ID", max_length=15)
user_name = models.CharField(verbose_name="User name", max_length=100, default="")
- def __str__(self):
+ def __str__(self) -> str:
"""Event human readable representation.
Returns
@@ -31,7 +31,7 @@ def __str__(self):
"""
return f"Event from {self.user_name or self.user_id} triggered by {self.trigger}"
- def from_slack(self, context, payload):
+ def from_slack(self, context, payload) -> None:
"""Create instance based on Slack data.
Args:
@@ -59,7 +59,7 @@ def from_slack(self, context, payload):
self.user_name = payload.get("user_name", "")
@staticmethod
- def create(context, payload, save=True):
+ def create(context: dict, payload: dict, *, save: bool = True) -> "Event":
"""Create event.
Args:
diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py
index 8ac35e1cf2..50865a8fb0 100644
--- a/backend/apps/slack/utils.py
+++ b/backend/apps/slack/utils.py
@@ -1,11 +1,17 @@
"""Slack app utils."""
+from __future__ import annotations
+
import logging
import re
from functools import lru_cache
from html import escape as escape_html
+from typing import TYPE_CHECKING
from urllib.parse import urljoin
+if TYPE_CHECKING:
+ from django.db.models import QuerySet
+
import requests
import yaml
from django.utils import timezone
@@ -14,10 +20,10 @@
from apps.common.constants import NL, OWASP_NEWS_URL
-logger = logging.getLogger(__name__)
+logger: logging.Logger = logging.getLogger(__name__)
-def escape(content):
+def escape(content) -> str:
"""Escape HTML content.
Args:
@@ -31,7 +37,7 @@ def escape(content):
@lru_cache
-def get_gsoc_projects(year):
+def get_gsoc_projects(year: int) -> list:
"""Get GSoC projects.
Args:
@@ -56,7 +62,7 @@ def get_gsoc_projects(year):
@lru_cache
-def get_news_data(limit=10, timeout=30):
+def get_news_data(limit: int = 10, timeout: float | None = 30) -> list[dict[str, str]]:
"""Get news data.
Args:
@@ -92,7 +98,7 @@ def get_news_data(limit=10, timeout=30):
@lru_cache
-def get_staff_data(timeout=30):
+def get_staff_data(timeout: float | None = 30) -> list | None:
"""Get staff data.
Args:
@@ -115,9 +121,10 @@ def get_staff_data(timeout=30):
)
except (RequestException, yaml.scanner.ScannerError):
logger.exception("Unable to parse OWASP staff data file", extra={"file_path": file_path})
+ return None
-def get_events_data(limit=9):
+def get_events_data(limit=9) -> QuerySet:
"""Get events data.
Returns
@@ -133,7 +140,7 @@ def get_events_data(limit=9):
)[:limit]
-def get_sponsors_data(limit=10):
+def get_sponsors_data(limit: int = 10) -> QuerySet | None:
"""Get sponsors data.
Args:
@@ -153,7 +160,7 @@ def get_sponsors_data(limit=10):
@lru_cache
-def get_posts_data(limit=5):
+def get_posts_data(limit: int = 5) -> QuerySet | None:
"""Get posts data.
Args:
@@ -172,11 +179,11 @@ def get_posts_data(limit=5):
return None
-def get_text(blocks):
+def get_text(blocks: tuple) -> str:
"""Convert blocks to plain text.
Args:
- blocks (list): A list of Slack block elements.
+ blocks (tuple): A tuple of Slack block elements.
Returns:
str: The plain text representation of the blocks.
@@ -225,7 +232,7 @@ def get_text(blocks):
return NL.join(text).strip()
-def strip_markdown(text):
+def strip_markdown(text: str) -> str:
"""Strip markdown formatting.
Args:
diff --git a/backend/poetry.lock b/backend/poetry.lock
index ab3333a846..e2da10cbd5 100644
--- a/backend/poetry.lock
+++ b/backend/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
diff --git a/backend/py.typed b/backend/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 4dba2c7538..cd0e91f1e6 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -68,7 +68,6 @@ format_css = true
format_js = true
indent = 4
-
[tool.pytest.ini_options]
DJANGO_CONFIGURATION = "Test"
DJANGO_SETTINGS_MODULE = "settings.test"
@@ -90,13 +89,27 @@ filterwarnings = [
]
log_level = "INFO"
+[tool.mypy]
+explicit_package_bases = true
+ignore_missing_imports = true
+mypy_path = "backend"
+
+[[tool.mypy.overrides]]
+disable_error_code = ["attr-defined"]
+module = ["apps.*.models.mixins.*", "apps.*.admin", "schema.tests.*"]
+
+[[tool.mypy.overrides]]
+disable_error_code = ["var-annotated"]
+module = ["apps.*.migrations.*"]
+
[tool.ruff]
line-length = 99
+target-version = "py313"
[tool.ruff.lint]
extend-select = ["I"]
ignore = [
- "ANN",
+ "ANN", # TODO(arkid15r): remove when all annotations are added.
"ARG002",
"C901",
"COM812",
@@ -117,7 +130,7 @@ ignore = [
]
select = ["ALL"]
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint.per-file-ignores]
"**/__init__.py" = ["D104", "F401"]
"**/admin.py" = ["D100", "D101", "D104"]
"**/api/*.py" = ["D106"]
@@ -131,7 +144,6 @@ select = ["ALL"]
"**/models/*.py" = ["D106"]
"**/tests/**/*.py" = ["D100", "D101", "D102", "D103", "D107", "S101"]
-
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"]
diff --git a/backend/tests/apps/common/utils_test.py b/backend/tests/apps/common/utils_test.py
index 8c546bb4a5..69dbbebd2b 100644
--- a/backend/tests/apps/common/utils_test.py
+++ b/backend/tests/apps/common/utils_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import pytest
@@ -39,8 +39,8 @@ def test_join_values(self, values, delimiter, expected):
("value", "expected_calls"),
[
("2025-01-01", 1),
- (datetime(2025, 1, 2, tzinfo=timezone.utc), 1),
- (int(datetime(2025, 1, 2, tzinfo=timezone.utc).timestamp()), 1),
+ (datetime(2025, 1, 2, tzinfo=UTC), 1),
+ (int(datetime(2025, 1, 2, tzinfo=UTC).timestamp()), 1),
],
)
@patch("apps.common.utils.naturaltime")
diff --git a/backend/tests/apps/github/models/label_test.py b/backend/tests/apps/github/models/label_test.py
index f5c19723be..9963861cee 100644
--- a/backend/tests/apps/github/models/label_test.py
+++ b/backend/tests/apps/github/models/label_test.py
@@ -8,7 +8,7 @@ def test_bulk_save(self):
mock_labels = [Mock(id=None), Mock(id=1)]
with patch("apps.common.models.BulkSaveModel.bulk_save") as mock_bulk_save:
Label.bulk_save(mock_labels)
- mock_bulk_save.assert_called_once_with(Label, mock_labels)
+ mock_bulk_save.assert_called_once_with(Label, mock_labels, fields=None)
def test_update_data(self, mocker):
gh_label_mock = mocker.Mock()
diff --git a/backend/tests/apps/github/models/mixins/issue_test.py b/backend/tests/apps/github/models/mixins/issue_test.py
index 9cd78fa6c8..45e2c2f203 100644
--- a/backend/tests/apps/github/models/mixins/issue_test.py
+++ b/backend/tests/apps/github/models/mixins/issue_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from unittest.mock import MagicMock
import pytest
@@ -35,8 +35,8 @@ def issue_index_mixin_instance():
instance.repository.idx_topics = ["repo_topic1", "repo_topic2"]
instance.comments_count = COMMENTS_COUNT
- instance.created_at = datetime(2021, 9, 1, tzinfo=timezone.utc)
- instance.updated_at = datetime(2021, 9, 2, tzinfo=timezone.utc)
+ instance.created_at = datetime(2021, 9, 1, tzinfo=UTC)
+ instance.updated_at = datetime(2021, 9, 2, tzinfo=UTC)
instance.url = "https://example.com/issue"
instance.title = "Issue Title"
instance.summary = "Issue Summary"
@@ -65,8 +65,8 @@ class TestIssueIndexMixin:
("idx_repository_stars_count", STARS_COUNT),
("idx_repository_topics", ["repo_topic1", "repo_topic2"]),
("idx_comments_count", COMMENTS_COUNT),
- ("idx_created_at", datetime(2021, 9, 1, tzinfo=timezone.utc).timestamp()),
- ("idx_updated_at", datetime(2021, 9, 2, tzinfo=timezone.utc).timestamp()),
+ ("idx_created_at", datetime(2021, 9, 1, tzinfo=UTC).timestamp()),
+ ("idx_updated_at", datetime(2021, 9, 2, tzinfo=UTC).timestamp()),
("idx_url", "https://example.com/issue"),
("idx_title", "Issue Title"),
("idx_summary", "Issue Summary"),
diff --git a/backend/tests/apps/github/models/mixins/release_test.py b/backend/tests/apps/github/models/mixins/release_test.py
index fa83e73909..b460c110be 100644
--- a/backend/tests/apps/github/models/mixins/release_test.py
+++ b/backend/tests/apps/github/models/mixins/release_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from unittest.mock import MagicMock
import pytest
@@ -19,8 +19,8 @@ def release_index_mixin_instance():
path="mock/repository",
project=MagicMock(nest_key="mock/project"),
)
- instance.created_at = datetime(2023, 1, 1, tzinfo=timezone.utc)
- instance.published_at = datetime(2023, 6, 1, tzinfo=timezone.utc)
+ instance.created_at = datetime(2023, 1, 1, tzinfo=UTC)
+ instance.published_at = datetime(2023, 6, 1, tzinfo=UTC)
instance.description = "This is a long description"
instance.is_pre_release = True
instance.name = "Release v1.0.0"
@@ -41,7 +41,7 @@ def release_index_mixin_instance():
}
],
),
- ("idx_created_at", datetime(2023, 1, 1, tzinfo=timezone.utc).timestamp()),
+ ("idx_created_at", datetime(2023, 1, 1, tzinfo=UTC).timestamp()),
(
"idx_description",
"This is a long description",
@@ -49,7 +49,7 @@ def release_index_mixin_instance():
("idx_is_pre_release", True),
("idx_name", "Release v1.0.0"),
("idx_project", "mock/project"),
- ("idx_published_at", datetime(2023, 6, 1, tzinfo=timezone.utc).timestamp()),
+ ("idx_published_at", datetime(2023, 6, 1, tzinfo=UTC).timestamp()),
("idx_repository", "mock/repository"),
("idx_tag_name", "v1.0.0"),
("idx_author", []),
diff --git a/backend/tests/apps/github/models/mixins/repository_test.py b/backend/tests/apps/github/models/mixins/repository_test.py
index 55c7768e17..21d91e1d0d 100644
--- a/backend/tests/apps/github/models/mixins/repository_test.py
+++ b/backend/tests/apps/github/models/mixins/repository_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
import pytest
@@ -20,10 +20,10 @@ def repository_index_mixin_instance():
instance.languages = ["Python", "JavaScript"]
instance.name = "Name"
instance.open_issues_count = OPEN_ISSUES_COUNT
- instance.pushed_at = datetime(2021, 1, 1, tzinfo=timezone.utc)
+ instance.pushed_at = datetime(2021, 1, 1, tzinfo=UTC)
instance.stars_count = STARS_COUNT
instance.topics = ["Topic1", "Topic2"]
- instance.created_at = datetime(2020, 1, 1, tzinfo=timezone.utc)
+ instance.created_at = datetime(2020, 1, 1, tzinfo=UTC)
instance.size = 1024
instance.has_funding_yml = True
instance.license = "MIT"
@@ -54,7 +54,7 @@ def test_is_indexable(self, is_draft, expected_indexable):
("idx_languages", ["Python", "JavaScript"]),
("idx_name", "Name"),
("idx_open_issues_count", OPEN_ISSUES_COUNT),
- ("idx_pushed_at", datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp()),
+ ("idx_pushed_at", datetime(2021, 1, 1, tzinfo=UTC).timestamp()),
("idx_stars_count", STARS_COUNT),
("idx_topics", ["Topic1", "Topic2"]),
],
diff --git a/backend/tests/apps/github/models/mixins/user_test.py b/backend/tests/apps/github/models/mixins/user_test.py
index 3a09a47fde..9f10aff5db 100644
--- a/backend/tests/apps/github/models/mixins/user_test.py
+++ b/backend/tests/apps/github/models/mixins/user_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import pytest
@@ -13,7 +13,7 @@ def user_index_mixin_instance():
instance.avatar_url = "https://example.com/avatar.png"
instance.bio = "Developer bio"
instance.company = "Test Company"
- instance.created_at = datetime(2021, 1, 1, tzinfo=timezone.utc)
+ instance.created_at = datetime(2021, 1, 1, tzinfo=UTC)
instance.email = "test@example.com"
instance.login = "test_user"
instance.followers_count = 100
@@ -23,12 +23,12 @@ def user_index_mixin_instance():
instance.public_repositories_count = 10
instance.title = "Developer"
instance.url = "https://github.com/test_user"
- instance.updated_at = datetime(2021, 1, 2, tzinfo=timezone.utc)
+ instance.updated_at = datetime(2021, 1, 2, tzinfo=UTC)
instance.issues = MagicMock()
instance.issues.select_related.return_value.order_by.return_value = [
MagicMock(
- created_at=datetime(2021, 1, 1, tzinfo=timezone.utc),
+ created_at=datetime(2021, 1, 1, tzinfo=UTC),
comments_count=5,
number=1,
repository=MagicMock(
@@ -46,7 +46,7 @@ def user_index_mixin_instance():
MagicMock(
is_pre_release=False,
name="Release Name",
- published_at=datetime(2021, 1, 1, tzinfo=timezone.utc),
+ published_at=datetime(2021, 1, 1, tzinfo=UTC),
repository=MagicMock(
key="repo_key",
owner=MagicMock(login="owner_login"),
@@ -80,7 +80,7 @@ def test_is_indexable(self, mock_get_logins, login, expected_indexable):
("idx_avatar_url", "https://example.com/avatar.png"),
("idx_bio", "Developer bio"),
("idx_company", "Test Company"),
- ("idx_created_at", datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp()),
+ ("idx_created_at", datetime(2021, 1, 1, tzinfo=UTC).timestamp()),
("idx_email", "test@example.com"),
("idx_key", "test_user"),
("idx_followers_count", 100),
@@ -91,12 +91,12 @@ def test_is_indexable(self, mock_get_logins, login, expected_indexable):
("idx_public_repositories_count", 10),
("idx_title", "Developer"),
("idx_url", "https://github.com/test_user"),
- ("idx_updated_at", datetime(2021, 1, 2, tzinfo=timezone.utc).timestamp()),
+ ("idx_updated_at", datetime(2021, 1, 2, tzinfo=UTC).timestamp()),
(
"idx_issues",
[
{
- "created_at": datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp(),
+ "created_at": datetime(2021, 1, 1, tzinfo=UTC).timestamp(),
"comments_count": 5,
"number": 1,
"repository": {"key": "repo_key", "owner_key": "owner_login"},
diff --git a/backend/tests/apps/github/models/release_test.py b/backend/tests/apps/github/models/release_test.py
index 180be250e3..9c4b2ee136 100644
--- a/backend/tests/apps/github/models/release_test.py
+++ b/backend/tests/apps/github/models/release_test.py
@@ -8,7 +8,7 @@ def test_bulk_save(self, mocker):
mock_releases = [mocker.Mock(id=None), mocker.Mock(id=1)]
mock_bulk_save = mocker.patch("apps.common.models.BulkSaveModel.bulk_save")
Release.bulk_save(mock_releases)
- mock_bulk_save.assert_called_once_with(Release, mock_releases)
+ mock_bulk_save.assert_called_once_with(Release, mock_releases, fields=None)
def test_update_data(self, mocker):
gh_release_mock = mocker.Mock()
diff --git a/backend/tests/apps/owasp/models/post_test.py b/backend/tests/apps/owasp/models/post_test.py
index 4223398efa..dfe0a91a5c 100644
--- a/backend/tests/apps/owasp/models/post_test.py
+++ b/backend/tests/apps/owasp/models/post_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from unittest.mock import Mock, patch
import pytest
@@ -19,7 +19,7 @@ def test_post_str(self, title, expected_str):
post = Post(
title=title,
url="https://example.com",
- published_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
+ published_at=datetime(2025, 1, 1, tzinfo=UTC),
)
assert str(post) == expected_str
@@ -38,14 +38,14 @@ def test_bulk_save(self):
"title": "New Post",
"url": "https://example.com",
"author_name": "John Doe",
- "published_at": datetime(2025, 1, 1, tzinfo=timezone.utc),
+ "published_at": datetime(2025, 1, 1, tzinfo=UTC),
"author_image_url": "https://image.com",
},
{
"title": "New Post",
"url": "https://example.com",
"author_name": "John Doe",
- "published_at": datetime(2025, 1, 1, tzinfo=timezone.utc),
+ "published_at": datetime(2025, 1, 1, tzinfo=UTC),
"author_image_url": "https://image.com",
},
),
@@ -54,14 +54,14 @@ def test_bulk_save(self):
"title": "Another Post",
"url": "https://example.com",
"author_name": "Jane Doe",
- "published_at": datetime(2023, 1, 1, tzinfo=timezone.utc),
+ "published_at": datetime(2023, 1, 1, tzinfo=UTC),
"author_image_url": "",
},
{
"title": "Another Post",
"url": "https://example.com",
"author_name": "Jane Doe",
- "published_at": datetime(2023, 1, 1, tzinfo=timezone.utc),
+ "published_at": datetime(2023, 1, 1, tzinfo=UTC),
"author_image_url": "",
},
),
@@ -84,7 +84,7 @@ def test_update_data_existing_post(self, mock_get):
"title": "Updated Title",
"author_name": "Updated Author",
"author_image_url": "https://updatedimage.com",
- "published_at": datetime(2023, 1, 1, tzinfo=timezone.utc),
+ "published_at": datetime(2023, 1, 1, tzinfo=UTC),
}
result = Post.update_data(data)
diff --git a/backend/tests/apps/slack/commands/events_test.py b/backend/tests/apps/slack/commands/events_test.py
index 28817593a1..50483948f1 100644
--- a/backend/tests/apps/slack/commands/events_test.py
+++ b/backend/tests/apps/slack/commands/events_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import pytest
@@ -11,12 +11,12 @@ class MockEvent:
def __init__(self, name, start_date, end_date, suggested_location, url, description):
self.name = name
self.start_date = (
- datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc).date()
+ datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=UTC).date()
if start_date
else None
)
self.end_date = (
- datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=timezone.utc).date()
+ datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=UTC).date()
if end_date
else None
)
diff --git a/backend/tests/apps/slack/commands/news_test.py b/backend/tests/apps/slack/commands/news_test.py
index 65683c7a83..04a578c017 100644
--- a/backend/tests/apps/slack/commands/news_test.py
+++ b/backend/tests/apps/slack/commands/news_test.py
@@ -60,7 +60,9 @@ def test_news_handler_disabled_enabled(
if mock_get_news_data.return_value:
assert "*:newspaper: Latest OWASP news:*" in blocks[0]["text"]["text"]
news_blocks = blocks[1:-2]
- for item, block in zip(mock_get_news_data.return_value, news_blocks):
+ for item, block in zip(
+ mock_get_news_data.return_value, news_blocks, strict=False
+ ):
expected = f" • *<{item['url']}|{item['title']}>* by {item['author']}"
assert block["text"]["text"] == expected
assert blocks[-2]["type"] == "divider"
diff --git a/backend/tests/apps/slack/commands/projects_test.py b/backend/tests/apps/slack/commands/projects_test.py
index 4cc9d17df6..d0651e4540 100644
--- a/backend/tests/apps/slack/commands/projects_test.py
+++ b/backend/tests/apps/slack/commands/projects_test.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
import pytest
@@ -65,7 +65,7 @@ def test_projects_handler(
def test_projects_handler_with_results(self, mock_get_projects, mock_client, mock_command):
settings.SLACK_COMMANDS_ENABLED = True
- test_date = datetime(2024, 1, 1, tzinfo=timezone.utc)
+ test_date = datetime(2024, 1, 1, tzinfo=UTC)
mock_get_projects.return_value = {
"hits": [
{
diff --git a/schema/pyproject.toml b/schema/pyproject.toml
index 4826dbb92f..103da1b33e 100644
--- a/schema/pyproject.toml
+++ b/schema/pyproject.toml
@@ -28,7 +28,7 @@ ignore = [
]
select = ["ALL"]
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint.per-file-ignores]
"**/__init__.py" = ["D104"]
"**/*.py" = ["S101"]