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"]