From 2512e26fd8390e2557b2dda6923df584187f9ce3 Mon Sep 17 00:00:00 2001 From: Altafur Rahman Date: Sun, 29 Dec 2024 15:33:46 +0600 Subject: [PATCH 01/17] Issue 2972 main page implement (#3167) * Add ProjectView and filtering capabilities for repositories to new page * Add ProjectView and filtering capabilities for repositories to new page * Add ProjectView and filtering capabilities for repositories to new page --- blt/urls.py | 2 + website/templates/projects/project_list.html | 245 +++++++++++++++++++ website/views/project.py | 107 +++++++- 3 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 website/templates/projects/project_list.html diff --git a/blt/urls.py b/blt/urls.py index 27b333518..c8e5b037b 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -179,6 +179,7 @@ ProjectBadgeView, ProjectDetailView, ProjectListView, + ProjectView, blt_tomato, distribute_bacon, select_contribution, @@ -536,6 +537,7 @@ name="googleplayapp", ), re_path(r"^projects/$", ProjectListView.as_view(), name="project_list"), + re_path(r"^allprojects/$", ProjectView.as_view(), name="project_view"), re_path(r"^apps/$", TemplateView.as_view(template_name="apps.html"), name="apps"), re_path( r"^deletions/$", diff --git a/website/templates/projects/project_list.html b/website/templates/projects/project_list.html new file mode 100644 index 000000000..fc910ad80 --- /dev/null +++ b/website/templates/projects/project_list.html @@ -0,0 +1,245 @@ +{% extends "base.html" %} +{% load humanize %} +{% load static %} +{% block title %} + Project List +{% endblock title %} +{% block content %} + {% include "includes/sidenav.html" %} +
+ +
+
+
+

Total Projects

+

{{ total_projects }}

+
+
+

Total Repositories

+

{{ total_repos }}

+
+ {% if filtered_count is not None %} +
+

Filtered Results

+

{{ filtered_count }}

+
+ {% endif %} +
+
+ +
+

Filter Projects

+
+
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ {% for project, repos in projects.items %} +
+ +
+
+ {% if project.logo %} + {{ project.name }} + {% else %} +
+ {{ project.name|slice:":1"|upper }} +
+ {% endif %} +
+

{{ project.name }}

+

{{ project.description|truncatechars:200 }}

+
+
+
+ +
+ {% for repo in repos %} +
+
+ +
+

{{ repo.name }}

+ {% if repo.is_main %} + Main + {% elif repo.is_wiki %} + Wiki + {% else %} + Normal + {% endif %} +
+ +

+ {{ repo.description|default:"No description available."|truncatechars:150 }} +

+ +
+
+ + {{ repo.stars }} +
+
+ 🍴 + {{ repo.forks }} +
+
+ 🐛 + {{ repo.open_issues }} +
+
+ 👥 + {{ repo.contributor_count }} +
+
+ +
+ Updated {{ repo.last_updated|date:"M d, Y" }} + + View Repository + + + + +
+
+
+ {% endfor %} +
+
+ {% empty %} +
+

No projects found matching your criteria.

+
+ {% endfor %} +
+ + {% if is_paginated %} +
+ +
+ {% endif %} +
+{% endblock content %} diff --git a/website/views/project.py b/website/views/project.py index e1b386e61..6703a10db 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -5,21 +5,23 @@ from io import BytesIO from pathlib import Path +import django_filters import matplotlib.pyplot as plt import requests from django.contrib import messages from django.contrib.auth.decorators import user_passes_test -from django.db.models import Count +from django.db.models import Count, Q from django.db.models.functions import TruncDate from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils.timezone import now from django.views.generic import DetailView, ListView +from django_filters.views import FilterView from rest_framework.views import APIView from website.bitcoin_utils import create_bacon_token from website.forms import GitHubURLForm -from website.models import IP, BaconToken, Contribution, Project +from website.models import IP, BaconToken, Contribution, Organization, Project, Repo from website.utils import admin_required logging.getLogger("matplotlib").setLevel(logging.ERROR) @@ -312,3 +314,104 @@ def get_queryset(self): sort_by = f"-{sort_by}" return queryset.order_by(sort_by) + + +class ProjectRepoFilter(django_filters.FilterSet): + search = django_filters.CharFilter(method="filter_search", label="Search") + repo_type = django_filters.ChoiceFilter( + choices=[ + ("all", "All"), + ("main", "Main"), + ("wiki", "Wiki"), + ("normal", "Normal"), + ], + method="filter_repo_type", + label="Repo Type", + ) + sort = django_filters.ChoiceFilter( + choices=[ + ("stars", "Stars"), + ("forks", "Forks"), + ("open_issues", "Open Issues"), + ("last_updated", "Recently Updated"), + ("contributor_count", "Contributors"), + ], + method="filter_sort", + label="Sort By", + ) + order = django_filters.ChoiceFilter( + choices=[ + ("asc", "Ascending"), + ("desc", "Descending"), + ], + method="filter_order", + label="Order", + ) + + class Meta: + model = Repo + fields = ["search", "repo_type", "sort", "order"] + + def filter_search(self, queryset, name, value): + return queryset.filter(Q(project__name__icontains=value) | Q(name__icontains=value)) + + def filter_repo_type(self, queryset, name, value): + if value == "main": + return queryset.filter(is_main=True) + elif value == "wiki": + return queryset.filter(is_wiki=True) + elif value == "normal": + return queryset.filter(is_main=False, is_wiki=False) + return queryset + + def filter_sort(self, queryset, name, value): + sort_mapping = { + "stars": "stars", + "forks": "forks", + "open_issues": "open_issues", + "last_updated": "last_updated", + "contributor_count": "contributor_count", + } + return queryset.order_by(sort_mapping.get(value, "stars")) + + def filter_order(self, queryset, name, value): + if value == "desc": + return queryset.reverse() + return queryset + + +class ProjectView(FilterView): + model = Repo + template_name = "projects/project_list.html" + context_object_name = "repos" + filterset_class = ProjectRepoFilter + paginate_by = 10 + + def get_queryset(self): + queryset = super().get_queryset() + organization_id = self.request.GET.get("organization") + + if organization_id: + queryset = queryset.filter(project__organization_id=organization_id) + return queryset.select_related("project").prefetch_related("tags", "contributor") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Get organizations that have projects + context["organizations"] = Organization.objects.filter(projects__isnull=False).distinct() + + # Add counts + context["total_projects"] = Project.objects.count() + context["total_repos"] = Repo.objects.count() + context["filtered_count"] = context["repos"].count() + + # Group repos by project + projects = {} + for repo in context["repos"]: + if repo.project not in projects: + projects[repo.project] = [] + projects[repo.project].append(repo) + context["projects"] = projects + + return context From 549c9292c4a9798de495a27813cacf38ca713145 Mon Sep 17 00:00:00 2001 From: Altafur Rahman Date: Mon, 30 Dec 2024 03:02:58 +0600 Subject: [PATCH 02/17] Add URL pattern for project creation endpoint (#3169) --- blt/urls.py | 2 + website/templates/projects/project_list.html | 331 +++++++++++++++++++ website/views/project.py | 329 +++++++++++++++++- 3 files changed, 660 insertions(+), 2 deletions(-) diff --git a/blt/urls.py b/blt/urls.py index c8e5b037b..b9f0efa19 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -181,6 +181,7 @@ ProjectListView, ProjectView, blt_tomato, + create_project, distribute_bacon, select_contribution, ) @@ -797,6 +798,7 @@ TemplateView.as_view(template_name="similarity.html"), name="similarity_scan", ), + path("projects/create/", create_project, name="create_project"), ] if settings.DEBUG: diff --git a/website/templates/projects/project_list.html b/website/templates/projects/project_list.html index fc910ad80..0b585f590 100644 --- a/website/templates/projects/project_list.html +++ b/website/templates/projects/project_list.html @@ -5,6 +5,16 @@ Project List {% endblock title %} {% block content %} + +
+ + {% include "includes/sidenav.html" %}
@@ -24,6 +34,28 @@

Filtered Results

{{ filtered_count }}

{% endif %} + {% if user.is_authenticated %} + + {% else %} + + {% endif %} @@ -242,4 +274,303 @@

{{ repo.name }}

{% endif %} + + + + {% endblock content %} diff --git a/website/views/project.py b/website/views/project.py index 6703a10db..86509c390 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -1,6 +1,7 @@ import json import logging import re +import time from datetime import datetime, timedelta from io import BytesIO from pathlib import Path @@ -8,13 +9,19 @@ import django_filters import matplotlib.pyplot as plt import requests +from django.conf import settings from django.contrib import messages -from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth.decorators import login_required, user_passes_test +from django.core.exceptions import ValidationError +from django.core.validators import URLValidator from django.db.models import Count, Q from django.db.models.functions import TruncDate -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render +from django.utils.dateparse import parse_datetime +from django.utils.text import slugify from django.utils.timezone import now +from django.views.decorators.http import require_http_methods from django.views.generic import DetailView, ListView from django_filters.views import FilterView from rest_framework.views import APIView @@ -398,6 +405,12 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + if self.request.user.is_authenticated: + # Get organizations where user is admin or manager + context["user_organizations"] = Organization.objects.filter( + Q(admin=self.request.user) | Q(managers=self.request.user) + ).distinct() + # Get organizations that have projects context["organizations"] = Organization.objects.filter(projects__isnull=False).distinct() @@ -415,3 +428,315 @@ def get_context_data(self, **kwargs): context["projects"] = projects return context + + +@login_required +@require_http_methods(["POST"]) +def create_project(request): + try: + max_retries = 5 + delay = 20 + + def validate_url(url): + """Validate URL format and accessibility""" + if not url: + return True # URLs are optional + + # Validate URL format + try: + URLValidator(schemes=["http", "https"])(url) + except ValidationError: + return False + + # Check if URL is accessible + try: + response = requests.head(url, timeout=5, allow_redirects=True) + return response.status_code == 200 + except requests.RequestException: + return False + + # Validate project URL + project_url = request.POST.get("url") + if project_url and not validate_url(project_url): + return JsonResponse( + { + "error": "Project URL is not accessible or returns an error", + "code": "INVALID_PROJECT_URL", + }, + status=400, + ) + + # Validate social media URLs + twitter = request.POST.get("twitter") + if twitter: + if twitter.startswith(("http://", "https://")): + if not validate_url(twitter): + return JsonResponse( + {"error": "Twitter URL is not accessible", "code": "INVALID_TWITTER_URL"}, + status=400, + ) + elif not twitter.startswith("@"): + twitter = f"@{twitter}" + + facebook = request.POST.get("facebook") + if facebook: + if not validate_url(facebook) or "facebook.com" not in facebook: + return JsonResponse( + { + "error": "Invalid or inaccessible Facebook URL", + "code": "INVALID_FACEBOOK_URL", + }, + status=400, + ) + + # Validate repository URLs + repo_urls = request.POST.getlist("repo_urls[]") + for url in repo_urls: + if not url: + continue + + # Validate GitHub URL format + if not url.startswith("https://github.com/"): + return JsonResponse( + { + "error": f"Repository URL must be a GitHub URL: {url}", + "code": "NON_GITHUB_URL", + }, + status=400, + ) + + # Verify GitHub URL format and accessibility + if not re.match(r"https://github\.com/[^/]+/[^/]+/?$", url): + return JsonResponse( + { + "error": f"Invalid GitHub repository URL format: {url}", + "code": "INVALID_GITHUB_URL_FORMAT", + }, + status=400, + ) + + if not validate_url(url): + return JsonResponse( + { + "error": f"GitHub repository is not accessible: {url}", + "code": "INACCESSIBLE_REPO", + }, + status=400, + ) + + # Check if project already exists by name + project_name = request.POST.get("name") + if Project.objects.filter(name__iexact=project_name).exists(): + return JsonResponse( + {"error": "A project with this name already exists", "code": "NAME_EXISTS"}, + status=409, + ) # 409 Conflict + + # Check if project URL already exists + project_url = request.POST.get("url") + if project_url and Project.objects.filter(url=project_url).exists(): + return JsonResponse( + {"error": "A project with this URL already exists", "code": "URL_EXISTS"}, + status=409, + ) + + # Check if any of the repository URLs are already linked to other projects + repo_urls = request.POST.getlist("repo_urls[]") + existing_repos = Repo.objects.filter(repo_url__in=repo_urls) + if existing_repos.exists(): + existing_urls = list(existing_repos.values_list("repo_url", flat=True)) + return JsonResponse( + { + "error": "One or more repositories are already linked to other projects", + "code": "REPOS_EXIST", + "existing_repos": existing_urls, + }, + status=409, + ) + + # Extract project data + project_data = { + "name": project_name, + "description": request.POST.get("description"), + "url": project_url, + "twitter": request.POST.get("twitter"), + "facebook": request.POST.get("facebook"), + } + + # Handle logo file + if request.FILES.get("logo"): + project_data["logo"] = request.FILES["logo"] + + # Handle organization association + org_id = request.POST.get("organization") + if org_id: + try: + org = Organization.objects.get(id=org_id) + if not (request.user == org.admin): + return JsonResponse( + { + "error": "You do not have permission to add projects to this organization" + }, + status=403, + ) + project_data["organization"] = org + except Organization.DoesNotExist: + return JsonResponse({"error": "Organization not found"}, status=404) + + # Create project + project = Project.objects.create(**project_data) + + # GitHub API configuration + github_token = getattr(settings, "GITHUB_TOKEN", None) + if not github_token: + return JsonResponse({"error": "GitHub token not configured"}, status=500) + + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json", + } + + # Handle repositories + repo_urls = request.POST.getlist("repo_urls[]") + repo_types = request.POST.getlist("repo_types[]") + + def api_get(url): + for i in range(max_retries): + try: + response = requests.get(url, headers=headers, timeout=10) + except requests.exceptions.RequestException: + time.sleep(delay) + continue + if response.status_code in (403, 429): + time.sleep(delay) + continue + return response + + def get_issue_count(full_name, query): + search_url = f"https://api.github.com/search/issues?q=repo:{full_name}+{query}" + resp = api_get(search_url) + if resp.status_code == 200: + return resp.json().get("total_count", 0) + return 0 + + for url, repo_type in zip(repo_urls, repo_types): + if not url: + continue + + # Convert GitHub URL to API URL + match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", url) + if not match: + continue + + owner, repo_name = match.groups() + api_url = f"https://api.github.com/repos/{owner}/{repo_name}" + + try: + # Fetch repository data + response = requests.get(api_url, headers=headers) + if response.status_code != 200: + continue + + repo_data = response.json() + + # Generate unique slug + base_slug = slugify(repo_data["name"]) + base_slug = base_slug.replace(".", "-") + if len(base_slug) > 50: + base_slug = base_slug[:50] + if not base_slug: + base_slug = f"repo-{int(time.time())}" + + unique_slug = base_slug + counter = 1 + while Repo.objects.filter(slug=unique_slug).exists(): + suffix = f"-{counter}" + if len(base_slug) + len(suffix) > 50: + base_slug = base_slug[: 50 - len(suffix)] + unique_slug = f"{base_slug}{suffix}" + counter += 1 + + # Fetch additional data + # Contributors count and commits count + commit_count = 0 + all_contributors = [] + page = 1 + while True: + contrib_url = f"{api_url}/contributors?anon=true&per_page=100&page={page}" + c_resp = api_get(contrib_url) + if c_resp.status_code != 200: + break + contributors_data = c_resp.json() + if not contributors_data: + break + commit_count += sum(c.get("contributions", 0) for c in contributors_data) + all_contributors.extend(contributors_data) + page += 1 + + # Issues count - Fixed + full_name = repo_data.get("full_name") + open_issues = get_issue_count(full_name, "type:issue+state:open") + closed_issues = get_issue_count(full_name, "type:issue+state:closed") + open_pull_requests = get_issue_count(full_name, "type:pr+state:open") + total_issues = open_issues + closed_issues + + # Latest release + releases_url = f"{api_url}/releases/latest" + release_response = requests.get(releases_url, headers=headers) + release_name = None + release_datetime = None + if release_response.status_code == 200: + release_data = release_response.json() + release_name = release_data.get("name") or release_data.get("tag_name") + release_datetime = release_data.get("published_at") + + # Create repository with fixed issues count + repo = Repo.objects.create( + project=project, + slug=unique_slug, + name=repo_data["name"], + description=repo_data.get("description", ""), + repo_url=url, + homepage_url=repo_data.get("homepage", ""), + is_wiki=(repo_type == "wiki"), + is_main=(repo_type == "main"), + stars=repo_data.get("stargazers_count", 0), + forks=repo_data.get("forks_count", 0), + last_updated=parse_datetime(repo_data.get("updated_at")), + watchers=repo_data.get("watchers_count", 0), + primary_language=repo_data.get("language"), + license=repo_data.get("license", {}).get("name"), + last_commit_date=parse_datetime(repo_data.get("pushed_at")), + network_count=repo_data.get("network_count", 0), + subscribers_count=repo_data.get("subscribers_count", 0), + size=repo_data.get("size", 0), + logo_url=repo_data.get("owner", {}).get("avatar_url", ""), + open_issues=open_issues, + closed_issues=closed_issues, + total_issues=total_issues, + contributor_count=len(all_contributors), + commit_count=commit_count, + release_name=release_name, + release_datetime=parse_datetime(release_datetime) if release_datetime else None, + open_pull_requests=open_pull_requests, + ) + + except requests.exceptions.RequestException as e: + continue + + return JsonResponse({"message": "Project created successfully"}, status=201) + + except Organization.DoesNotExist: + return JsonResponse( + {"error": "Organization not found", "code": "ORG_NOT_FOUND"}, status=404 + ) + except PermissionError: + return JsonResponse( + { + "error": "You do not have permission to add projects to this organization", + "code": "PERMISSION_DENIED", + }, + status=403, + ) + except Exception as e: + return JsonResponse({"error": str(e), "code": "UNKNOWN_ERROR"}, status=400) From e1f503430d24f043643724ab59b150333431be86 Mon Sep 17 00:00:00 2001 From: Altafur Rahman Date: Mon, 30 Dec 2024 04:08:30 +0600 Subject: [PATCH 03/17] =?UTF-8?q?Enhance=20URL=20validation=20in=20project?= =?UTF-8?q?=20creation=20to=20block=20private=20IPs=20and=20i=E2=80=A6=20(?= =?UTF-8?q?#3170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhance URL validation in project creation to block private IPs and improve error handling * Improve URL validation in project creation to block private and reserved IP addresses * Improve URL validation in project creation to block private and reserved IP addresses --- website/views/project.py | 41 +++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/website/views/project.py b/website/views/project.py index 86509c390..34355d29b 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -1,10 +1,13 @@ +import ipaddress import json import logging import re +import socket import time from datetime import datetime, timedelta from io import BytesIO from pathlib import Path +from urllib.parse import urlparse import django_filters import matplotlib.pyplot as plt @@ -445,14 +448,36 @@ def validate_url(url): # Validate URL format try: URLValidator(schemes=["http", "https"])(url) - except ValidationError: - return False + parsed = urlparse(url) + hostname = parsed.hostname + if not hostname: + return False - # Check if URL is accessible - try: - response = requests.head(url, timeout=5, allow_redirects=True) - return response.status_code == 200 - except requests.RequestException: + try: + addr_info = socket.getaddrinfo(hostname, None) + except socket.gaierror: + # DNS resolution failed; hostname is not resolvable + return False + for info in addr_info: + ip = info[4][0] + try: + ip_obj = ipaddress.ip_address(ip) + if ( + ip_obj.is_private + or ip_obj.is_loopback + or ip_obj.is_reserved + or ip_obj.is_multicast + or ip_obj.is_link_local + or ip_obj.is_unspecified + ): + # Disallowed IP range detected + return False + except ValueError: + # Invalid IP address format + return False + return True + + except (ValidationError, ValueError): return False # Validate project URL @@ -738,5 +763,3 @@ def get_issue_count(full_name, query): }, status=403, ) - except Exception as e: - return JsonResponse({"error": str(e), "code": "UNKNOWN_ERROR"}, status=400) From 2af5a8af169b07b9a34f89676a866f4ce766cf7e Mon Sep 17 00:00:00 2001 From: Altafur Rahman Date: Tue, 31 Dec 2024 18:49:41 +0600 Subject: [PATCH 04/17] Add project detail view and update project list template for navigation (#3171) * Add project detail view and update project list template for navigation * Add project detail view and update project list template for navigation --- blt/urls.py | 2 + .../templates/projects/project_detail.html | 286 ++++++++++++++++++ website/templates/projects/project_list.html | 30 +- website/views/project.py | 71 ++++- 4 files changed, 374 insertions(+), 15 deletions(-) create mode 100644 website/templates/projects/project_detail.html diff --git a/blt/urls.py b/blt/urls.py index b9f0efa19..bd7306675 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -179,6 +179,7 @@ ProjectBadgeView, ProjectDetailView, ProjectListView, + ProjectsDetailView, ProjectView, blt_tomato, create_project, @@ -799,6 +800,7 @@ name="similarity_scan", ), path("projects/create/", create_project, name="create_project"), + path("projects//", ProjectsDetailView.as_view(), name="projects_detail"), ] if settings.DEBUG: diff --git a/website/templates/projects/project_detail.html b/website/templates/projects/project_detail.html new file mode 100644 index 000000000..ef40d22b4 --- /dev/null +++ b/website/templates/projects/project_detail.html @@ -0,0 +1,286 @@ +{% extends "base.html" %} +{% load humanize %} +{% load static %} +{% block title %}{{ project.name }} - Project Details{% endblock %} +{% block content %} + {% include "includes/sidenav.html" %} +
+ + + +
+ {% if project.organization %} +
+ +
+ + + + + + + + +
+
+
+ {% if project.organization.logo %} + {{ project.organization.name }} + {% else %} +
+ {{ project.organization.name|slice:":1"|upper }} +
+ {% endif %} +
+
Organization
+

{{ project.organization.name }}

+ {% if project.organization.description %} +

{{ project.organization.description }}

+ {% endif %} +
+
+
+
+ {% else %} +
+
+
Organization
+

This is an independent project not associated with any organization

+
+
+ {% endif %} +
+ +
+
+
+ {% if project.logo %} + {{ project.name }} + {% else %} +
+ {{ project.name|slice:":1"|upper }} +
+ {% endif %} +
+

{{ project.name }}

+

{{ project.description }}

+ +
+ {% if project.url %} + + + + + + Website + + {% endif %} + {% if project.twitter %} + + + + + Twitter + + {% endif %} + {% if project.facebook %} + + + + + Facebook + + {% endif %} +
+
+
+
+ +
+
+
+
Total Stars
+
{{ repo_metrics.total_stars|default:"0"|intcomma }}
+
+
+
Total Forks
+
{{ repo_metrics.total_forks|default:"0"|intcomma }}
+
+
+
Total Issues
+
{{ repo_metrics.total_issues|default:"0"|intcomma }}
+
+
+
Contributors
+
{{ repo_metrics.total_contributors|default:"0"|intcomma }}
+
+
+
Total Commits
+
{{ repo_metrics.total_commits|default:"0"|intcomma }}
+
+
+
Open PRs
+
{{ repo_metrics.total_prs|default:"0"|intcomma }}
+
+
+
+
+ +
+

Associated Repositories

+
+ {% for repo in repositories %} +
+
+
+

{{ repo.name }}

+ {% if repo.is_main %} + Main + {% elif repo.is_wiki %} + Wiki + {% endif %} +
+ {% if repo.description %}

{{ repo.description }}

{% endif %} + +
+
+ + {{ repo.stars|intcomma }} +
+
+ 🍴 + {{ repo.forks|intcomma }} +
+
+ 🐛 + {{ repo.open_issues|intcomma }} +
+
+ 👥 + {{ repo.contributor_count|intcomma }} +
+
+ +
+ {% if repo.primary_language %} +
+ + + + Primary Language: {{ repo.primary_language }} +
+ {% endif %} + {% if repo.license %} +
+ + + + License: {{ repo.license }} +
+ {% endif %} + {% if repo.last_commit_date %} +
+ + + + Last Commit: {{ repo.last_commit_date|date:"M d, Y" }} +
+ {% endif %} + {% if repo.release_name %} +
+ + + + Latest Release: {{ repo.release_name }} + {% if repo.release_datetime %}({{ repo.release_datetime|date:"M d, Y" }}){% endif %} +
+ {% endif %} +
+ +
+
Updated {{ repo.last_updated|naturaltime }}
+
+ {% if repo.homepage_url %} + + + + + Website + + {% endif %} + + + + + View on GitHub + +
+
+
+
+ {% empty %} +
+

No repositories found for this project.

+
+ {% endfor %} +
+
+ +
+

Project Timeline

+
+
+ + + + Created: {{ created_date.full }} ({{ created_date.relative }}) +
+
+ + + + Last Updated: {{ modified_date.full }} ({{ modified_date.relative }}) +
+
+
+
+{% endblock content %} diff --git a/website/templates/projects/project_list.html b/website/templates/projects/project_list.html index 0b585f590..11c588703 100644 --- a/website/templates/projects/project_list.html +++ b/website/templates/projects/project_list.html @@ -150,20 +150,24 @@

Filter Projects

- {% if project.logo %} - {{ project.name }} - {% else %} -
- {{ project.name|slice:":1"|upper }} -
- {% endif %} + + {% if project.logo %} + {{ project.name }} + {% else %} +
+ {{ project.name|slice:":1"|upper }} +
+ {% endif %} +
-

{{ project.name }}

-

{{ project.description|truncatechars:200 }}

+ +

{{ project.name }}

+

{{ project.description|truncatechars:200 }}

+
diff --git a/website/views/project.py b/website/views/project.py index 34355d29b..f2bb55509 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -15,15 +15,16 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.humanize.templatetags.humanize import naturaltime from django.core.exceptions import ValidationError from django.core.validators import URLValidator -from django.db.models import Count, Q +from django.db.models import Count, Q, Sum from django.db.models.functions import TruncDate from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils.dateparse import parse_datetime from django.utils.text import slugify -from django.utils.timezone import now +from django.utils.timezone import localtime, now from django.views.decorators.http import require_http_methods from django.views.generic import DetailView, ListView from django_filters.views import FilterView @@ -763,3 +764,69 @@ def get_issue_count(full_name, query): }, status=403, ) + + +class ProjectsDetailView(DetailView): + model = Project + template_name = "projects/project_detail.html" + context_object_name = "project" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + project = self.get_object() + + # Get all repositories associated with the project + repositories = ( + Repo.objects.select_related("project") + .filter(project=project) + .prefetch_related("tags", "contributor") + ) + + # Calculate aggregate metrics + repo_metrics = repositories.aggregate( + total_stars=Sum("stars"), + total_forks=Sum("forks"), + total_issues=Sum("total_issues"), + total_contributors=Sum("contributor_count"), + total_commits=Sum("commit_count"), + total_prs=Sum("open_pull_requests"), + ) + + # Format dates for display + created_date = localtime(project.created) + modified_date = localtime(project.modified) + + # Add computed context + context.update( + { + "repositories": repositories, + "repo_metrics": repo_metrics, + "created_date": { + "full": created_date.strftime("%B %d, %Y"), + "relative": naturaltime(created_date), + }, + "modified_date": { + "full": modified_date.strftime("%B %d, %Y"), + "relative": naturaltime(modified_date), + }, + "show_org_details": self.request.user.is_authenticated + and ( + project.organization + and ( + self.request.user == project.organization.admin + or project.organization.managers.filter(id=self.request.user.id).exists() + ) + ), + } + ) + + # Add organization context if it exists + if project.organization: + context["organization"] = project.organization + + return context + + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + + return response From fe1fb8fbf481861e82ca157b1fb51fe978e554fa Mon Sep 17 00:00:00 2001 From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com> Date: Tue, 31 Dec 2024 18:46:35 +0530 Subject: [PATCH 05/17] Remove the gitPython dependency and use github zip link (#3163) --- blt/settings.py | 1 + poetry.lock | 454 ++++++++++++------------------ pyproject.toml | 2 +- website/consumers.py | 86 ++++-- website/templates/similarity.html | 25 +- website/utils.py | 9 + 6 files changed, 281 insertions(+), 296 deletions(-) diff --git a/blt/settings.py b/blt/settings.py index dfeb113f8..05f44c6c3 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -625,6 +625,7 @@ "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [os.environ.get("REDISCLOUD_URL")], + # "hosts": [("127.0.0.1", 6379)], }, }, } diff --git a/poetry.lock b/poetry.lock index fff55cf14..6b8b2bcb3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -500,116 +500,103 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] @@ -1674,38 +1661,6 @@ files = [ {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] -[[package]] -name = "gitdb" -version = "4.0.11" -description = "Git Object Database" -optional = false -python-versions = ">=3.7" -files = [ - {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, - {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, -] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.43" -description = "GitPython is a Python library used to interact with Git repositories" -optional = false -python-versions = ">=3.7" -files = [ - {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, - {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, -] - -[package.dependencies] -gitdb = ">=4.0.1,<5" - -[package.extras] -doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] - [[package]] name = "giturlparse-py" version = "0.0.5" @@ -2265,125 +2220,91 @@ files = [ [[package]] name = "kiwisolver" -version = "1.4.7" +version = "1.4.8" description = "A fast implementation of the Cassowary constraint solver" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, - {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, + {file = "kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed"}, + {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605"}, + {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e"}, + {file = "kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751"}, + {file = "kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561"}, + {file = "kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6"}, + {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc"}, + {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67"}, + {file = "kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34"}, + {file = "kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31"}, + {file = "kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d"}, + {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8"}, + {file = "kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50"}, + {file = "kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1"}, + {file = "kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc"}, + {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957"}, + {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb"}, + {file = "kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2"}, + {file = "kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90"}, + {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b"}, + {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b"}, + {file = "kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e"}, ] [[package]] @@ -4630,17 +4551,6 @@ files = [ [package.extras] optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<15)"] -[[package]] -name = "smmap" -version = "5.0.1" -description = "A pure Python implementation of a sliding window memory map manager" -optional = false -python-versions = ">=3.7" -files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -4936,13 +4846,13 @@ telegram = ["requests"] [[package]] name = "trio" -version = "0.27.0" +version = "0.28.0" description = "A friendly Python library for async concurrency and I/O" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "trio-0.27.0-py3-none-any.whl", hash = "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884"}, - {file = "trio-0.27.0.tar.gz", hash = "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831"}, + {file = "trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"}, + {file = "trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05"}, ] [package.dependencies] @@ -5581,4 +5491,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "3.11.2" -content-hash = "4062c5024e6472379be149250c7a5a2f420c30664d663d23e3aab0fcf9256a11" +content-hash = "9bebbaf90dfa2246c9b3460139b5fa390f48fdd8bd5adaa96b61e35f969e9211" diff --git a/pyproject.toml b/pyproject.toml index 0308ca31d..73ca0d179 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ django-redis = "^5.4.0" uvicorn = "^0.34.0" channels = "^4.2.0" channels-redis = "^4.2.1" -gitpython = "^3.1.43" +async-timeout = "^5.0.1" [tool.poetry.group.dev.dependencies] black = "^24.8.0" diff --git a/website/consumers.py b/website/consumers.py index 30d5eed86..517544290 100644 --- a/website/consumers.py +++ b/website/consumers.py @@ -2,11 +2,12 @@ import difflib import json import os -import subprocess import tempfile +import zipfile +from pathlib import Path +import aiohttp from channels.generic.websocket import AsyncWebsocketConsumer -from git import Repo from website.utils import ( compare_model_fields, @@ -14,6 +15,7 @@ extract_django_models, extract_function_signatures_and_content, generate_embedding, + git_url_to_zip_url, ) @@ -46,6 +48,8 @@ async def receive(self, text_data): type2 = data.get("type2") # 'github' or 'zip' repo1 = data.get("repo1") # GitHub URL or ZIP file path repo2 = data.get("repo2") # GitHub URL or ZIP file path + branch1 = data.get("branch1") # Branch name for the first repository + branch2 = data.get("branch2") # Branch name for the second repository if not repo1 or not repo2 or not type1 or not type2: await self.send( @@ -58,12 +62,14 @@ async def receive(self, text_data): return try: - # Create a temporary directory for repository processing temp_dir = tempfile.mkdtemp() # Download or extract the repositories - repo1_path = await self.download_or_extract(repo1, type1, temp_dir, "repo1") - repo2_path = await self.download_or_extract(repo2, type2, temp_dir, "repo2") + + zip_repo1 = git_url_to_zip_url(repo1, branch1) + zip_repo2 = git_url_to_zip_url(repo2, branch2) + repo1_path = await self.download_or_extract(zip_repo1, type1, temp_dir, "repo1") + repo2_path = await self.download_or_extract(zip_repo2, type2, temp_dir, "repo2") # Process similarity analysis matching_details = await self.run_similarity_analysis(repo1_path, repo2_path) @@ -88,7 +94,10 @@ async def receive(self, text_data): # Handle unexpected errors and send an error message await self.send( json.dumps( - {"status": "error", "error": "Please check the repositories and try again."} + { + "status": "error", + "error": "Please check the repositories/branches and try again.", + } ) ) await self.close() @@ -125,29 +134,62 @@ async def download_or_extract(self, source, source_type, temp_dir, repo_name): """ dest_path = os.path.join(temp_dir, repo_name) if source_type == "github": - try: - # Clone the GitHub repository - process = await self.clone_github_repo(source, dest_path) - return dest_path - except subprocess.CalledProcessError as e: - # Handle errors during the cloning process - raise Exception(f"Error cloning GitHub repository: {e.stderr.decode('utf-8')}") - except Exception as e: - # General error handling for unexpected issues - raise Exception(f"Unexpected error during GitHub cloning: {str(e)}") + repo_path = await self.download_and_extract_zip(source, temp_dir, repo_name) + return repo_path elif source_type == "zip": - # Handle ZIP extraction (Add your ZIP handling logic here) - pass + # Assume `repo_url_or_path` is a direct path to a ZIP file + repo_path = await self.extract_zip(source, temp_dir, repo_name) + return repo_path return dest_path - async def clone_github_repo(self, repo_url, dest_path): + async def download_and_extract_zip(self, zip_url, temp_dir, repo_name): + """ + Downloads and extracts a ZIP file from a URL. + """ + try: + async with aiohttp.ClientSession() as session: + async with session.get(zip_url) as response: + if response.status != 200: + raise Exception( + f"Failed to download ZIP file. Status code: {response.status}" + ) + + # Extract the ZIP file + zip_file_path = Path(temp_dir) / f"{repo_name}.zip" + with open(zip_file_path, "wb") as zip_file: + zip_data = await response.read() + zip_file.write(zip_data) + + # Extract to a directory + extraction_path = Path(temp_dir) / repo_name + try: + with zipfile.ZipFile(zip_file_path, "r") as zip_ref: + zip_ref.extractall(extraction_path) + except zipfile.BadZipFile as e: + raise Exception(f"Failed to extract ZIP file: {e}") + + return str(extraction_path) + except Exception as e: + raise + + async def extract_zip(self, zip_file_path, temp_dir, repo_name): """ - Clones a GitHub repository asynchronously. + Extracts a local ZIP file. + + Args: + zip_file_path (str): Path to the local ZIP file. + temp_dir (str): Temporary directory to store files. + repo_name (str): Repository identifier. + + Returns: + str: Path to the extracted contents. """ - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, Repo.clone_from, repo_url, dest_path) + extraction_path = Path(temp_dir) / repo_name + with zipfile.ZipFile(zip_file_path, "r") as zip_ref: + zip_ref.extractall(extraction_path) + return str(extraction_path) def process_similarity_analysis(self, repo1_path, repo2_path): """ diff --git a/website/templates/similarity.html b/website/templates/similarity.html index 3eba92554..794e5451f 100644 --- a/website/templates/similarity.html +++ b/website/templates/similarity.html @@ -127,6 +127,14 @@

Similarity Check

+
+ +
@@ -144,6 +152,14 @@

Similarity Check

+
+ +
@@ -237,8 +253,12 @@

Results

const repo1 = document.getElementById("repo1").value; const type1 = document.getElementById("type1").value; + const branch1 = document.getElementById("branch1").value; + const repo2 = document.getElementById("repo2").value; const type2 = document.getElementById("type2").value; + const branch2 = document.getElementById("branch2").value; + // Validate input if (!repo1 || !repo2) { @@ -257,7 +277,10 @@

Results

repo1: repo1, type1: type1, repo2: repo2, - type2: type2 + type2: type2, + branch1: branch1, + branch2: branch2 + })); } catch (error) { diff --git a/website/utils.py b/website/utils.py index 3f7964cd7..527a7d2a8 100644 --- a/website/utils.py +++ b/website/utils.py @@ -469,3 +469,12 @@ def compare_model_fields(model1, model2): "field_comparison_details": field_comparison_details, "overall_field_similarity": round(overall_field_similarity, 2), } + + +def git_url_to_zip_url(git_url, branch="master"): + if git_url.endswith(".git"): + base_url = git_url[:-4] + zip_url = f"{base_url}/archive/refs/heads/{branch}.zip" + return zip_url + else: + raise ValueError("Invalid .git URL provided") From 184eb945258d748f3c62d7a245cf1aa62991efdf Mon Sep 17 00:00:00 2001 From: Sahil Omkumar Dhillon <118592065+SahilDhillon21@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:26:32 +0530 Subject: [PATCH 06/17] Visual streak tracker (#3165) * Visual streak tracker * space --- website/models.py | 6 +++--- website/templates/profile.html | 33 +++++++++++++++++++++++++++++++++ website/views/user.py | 12 +++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/website/models.py b/website/models.py index 113bb9fe2..47fd23570 100644 --- a/website/models.py +++ b/website/models.py @@ -666,9 +666,9 @@ def update_streak_and_award_points(self, check_in_date=None): elif self.current_streak == 30: points_awarded += 50 reason = "30-day streak milestone achieved!" - elif self.current_streak == 90: + elif self.current_streak == 100: points_awarded += 150 - reason = "90-day streak milestone achieved!" + reason = "100-day streak milestone achieved!" elif self.current_streak == 180: points_awarded += 300 reason = "180-day streak milestone achieved!" @@ -700,7 +700,7 @@ def award_streak_badges(self): 7: "Weekly Streak", 15: "Half-Month Streak", 30: "Monthly Streak", - 90: "Three Month Streak", + 100: "100 Day Streak", 180: "Six Month Streak", 365: "Yearly Streak", } diff --git a/website/templates/profile.html b/website/templates/profile.html index 8281532bc..d3e334312 100644 --- a/website/templates/profile.html +++ b/website/templates/profile.html @@ -284,6 +284,20 @@ color: #ff5722; font-weight: bold; } + .shine-effect { + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + animation: shine 2s infinite; + } + + @keyframes shine { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } + } {% endblock style %} {% block content %} @@ -386,6 +400,25 @@

Last Sizzle Check-in: {{ user.userprofile.last_check_in|default:"N/A" }} +
+
+ Streak Progress +
+ + {{ base_milestone }}/{{ next_milestone }} +
+
+
+
+
+
+
+
+ Current + Next Milestone +
+
diff --git a/website/views/user.py b/website/views/user.py index add5e0aed..68c2f7ea7 100644 --- a/website/views/user.py +++ b/website/views/user.py @@ -219,9 +219,19 @@ def get_context_data(self, **kwargs): user = self.object context = super(UserProfileDetailView, self).get_context_data(**kwargs) + milestones = [7, 15, 30, 100, 180, 365] + base_milestone = 0 + next_milestone = 0 + for milestone in milestones: + if user.userprofile.current_streak >= milestone: + base_milestone = milestone + elif user.userprofile.current_streak < milestone: + next_milestone = milestone + break + context["base_milestone"] = base_milestone + context["next_milestone"] = next_milestone # Fetch badges user_badges = UserBadge.objects.filter(user=user).select_related("badge") - context["user_badges"] = user_badges # Add badges to context context["is_mentor"] = UserBadge.objects.filter(user=user, badge__title="Mentor").exists() context["available_badges"] = Badge.objects.all() From 3eb33dea4223f5482a0a9fcd0f79d2e8f797e5e6 Mon Sep 17 00:00:00 2001 From: Altafur Rahman Date: Wed, 1 Jan 2025 05:50:25 +0600 Subject: [PATCH 07/17] Add repository detail view and update project templates for navigation (#3174) * Add repository detail view and update project templates for navigation * Add repository detail view and update project templates for navigation * Add repository detail view and update project templates for navigation --- blt/urls.py | 2 + .../templates/projects/project_detail.html | 9 +- website/templates/projects/project_list.html | 5 +- website/templates/projects/repo_detail.html | 527 ++++++++++++++++++ website/views/project.py | 95 ++++ 5 files changed, 633 insertions(+), 5 deletions(-) create mode 100644 website/templates/projects/repo_detail.html diff --git a/blt/urls.py b/blt/urls.py index bd7306675..0e71b09a3 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -181,6 +181,7 @@ ProjectListView, ProjectsDetailView, ProjectView, + RepoDetailView, blt_tomato, create_project, distribute_bacon, @@ -591,6 +592,7 @@ re_path(r"^api/v1/contributors/$", contributors, name="api_contributor"), path("project//", ProjectDetailView.as_view(), name="project_view"), path("projects//badge/", ProjectBadgeView.as_view(), name="project-badge"), + path("repository//", RepoDetailView.as_view(), name="repo_detail"), re_path(r"^report-ip/$", ReportIpView.as_view(), name="report_ip"), re_path(r"^reported-ips/$", ReportedIpListView.as_view(), name="reported_ips_list"), re_path(r"^feed/$", feed, name="feed"), diff --git a/website/templates/projects/project_detail.html b/website/templates/projects/project_detail.html index ef40d22b4..32a038e31 100644 --- a/website/templates/projects/project_detail.html +++ b/website/templates/projects/project_detail.html @@ -1,7 +1,9 @@ {% extends "base.html" %} {% load humanize %} {% load static %} -{% block title %}{{ project.name }} - Project Details{% endblock %} +{% block title %} + {{ project.name }} - Project Details +{% endblock title %} {% block content %} {% include "includes/sidenav.html" %}
@@ -164,14 +166,15 @@

Associated Repositories

{% for repo in repositories %}
- + {% if repo.description %}

{{ repo.description }}

{% endif %}
diff --git a/website/templates/projects/project_list.html b/website/templates/projects/project_list.html index 11c588703..20707bc16 100644 --- a/website/templates/projects/project_list.html +++ b/website/templates/projects/project_list.html @@ -177,7 +177,8 @@

{{ project.
- +

{{ repo.description|default:"No description available."|truncatechars:150 }} diff --git a/website/templates/projects/repo_detail.html b/website/templates/projects/repo_detail.html new file mode 100644 index 000000000..36c55a5f5 --- /dev/null +++ b/website/templates/projects/repo_detail.html @@ -0,0 +1,527 @@ +{% extends "base.html" %} +{% load humanize %} +{% load static %} +{% block title %}{{ repo.name }} - Repo Details{% endblock %} +{% block content %} + {% include "includes/sidenav.html" %} + +

+
+ +
+
+
+
+ {% if repo.logo_url %} + {{ repo.name }} logo + {% endif %} +
+

{{ repo.name }}

+
+ + + + + + Back to {{ repo.project.name }} + + {% if repo.project.organization %} + | +
organization : {{ repo.project.organization.name }}
+ {% endif %} +
+
+
+

{{ repo.description|default:"No description available." }}

+
+ +
+ {% if repo.homepage_url %} + + + + + Visit Homepage + + {% endif %} + + + + + View on GitHub + +
+
+ +
+
+ {% if repo.is_main %} + + + + + Main Repository + + {% elif repo.is_wiki %} + + + + + Wiki Repository + + {% else %} + + + + + Standard Repository + + {% endif %} + {% if repo.tags.all %} +
+ Tags: + {% for tag in repo.tags.all %} + {{ tag.name }} + {% endfor %} +
+ {% endif %} +
+
+
Created {{ repo.created|date:"M d, Y" }}
+
Updated {{ repo.last_updated|naturaltime }}
+
+
+ +
+
+
+ Stars + + + +
+

{{ repo.stars|intcomma }}

+
+
+
+ Forks + + + +
+

{{ repo.forks|intcomma }}

+
+
+
+ Watchers + + + + +
+

{{ repo.watchers|intcomma }}

+
+
+
+ Network + + + +
+

{{ repo.network_count|intcomma }}

+
+
+
+ Subscribers + + + +
+

{{ repo.subscribers_count|intcomma }}

+
+
+
+ +
+ +
+ +
+
+

+ + + + Activity Metrics +

+
+ +
+
+
+
+
+

Issues

+ + + + + +
+
+
+ Open + {{ repo.open_issues|intcomma }} +
+
+ Closed + {{ repo.closed_issues|intcomma }} +
+
+
+
+ Total + {{ repo.total_issues|intcomma }} +
+
+
+
+ +
+
+
+
+
+

Pull Requests

+ + + + + +
+
+
+
{{ repo.open_pull_requests|intcomma }}
+
Open PRs
+
+
+
+
+ +
+
+
+
+
+

Commits

+ + + + + +
+
+
+
{{ repo.commit_count|intcomma }}
+
Total Commits
+
+
+ + + + Last: {{ repo.last_commit_date|date:"M d, Y" }} +
+
+
+
+
+
+
+ +
+
+

+ + + + Community +

+
+ +
+

Top Contributors

+ + {{ repo.contributor_count|intcomma }} total contributors + +
+ +
+ {% for contributor in top_contributors %} +
+ {{ contributor.name }} +
+
+ {{ contributor.name }} + {% if contributor.verified %}{% endif %} +
+
{{ contributor.contributions|intcomma }} commits
+
+ + + + + +
+ {% endfor %} +
+ + {% if repo.contributor_count > 6 %} +
+
+
+ {% for contributor in repo.contributor.all|dictsortreversed:"contributions"|slice:"6:11" %} + {{ contributor.name }} + {% endfor %} + {% if repo.contributor_count > 11 %} +
+ +{{ repo.contributor_count|add:"-11" }} +
+ {% endif %} +
+ + View all contributors + + + + +
+
+ {% endif %} +
+
+
+ +
+
+

+ + + + Technical Overview +

+
+ +
+

+ + + + Language & Stack +

+
+
+ Primary Language: + + {{ repo.primary_language|default:"Not specified" }} + +
+
+ Repository Size: + + {{ repo.size|filesizeformat }} + +
+
+ License: +
+ {{ repo.license|default:"Not specified" }} +
+
+
+
+ +
+

+ + + + Latest Release +

+ {% if repo.release_name or repo.release_datetime %} +
+ {% if repo.release_name %} +
+ Version + + {{ repo.release_name }} + +
+ {% endif %} + {% if repo.release_datetime %} +
+ Release Date + + {{ repo.release_datetime|date:"M d, Y" }} + +
+ {% endif %} +
+ Last Commit + + {{ repo.last_commit_date|date:"M d, Y" }} + +
+
+ {% else %} +
No release information available
+ {% endif %} +
+
+
+
+
+ + +
+
+{% endblock %} diff --git a/website/views/project.py b/website/views/project.py index f2bb55509..e53061582 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -830,3 +830,98 @@ def get(self, request, *args, **kwargs): response = super().get(request, *args, **kwargs) return response + + +class RepoDetailView(DetailView): + model = Repo + template_name = "projects/repo_detail.html" + context_object_name = "repo" + + def get_github_top_contributors(self, repo_url): + """Fetch top contributors directly from GitHub API""" + try: + # Extract owner/repo from GitHub URL + owner_repo = repo_url.rstrip("/").split("github.com/")[-1] + api_url = f"https://api.github.com/repos/{owner_repo}/contributors?per_page=6" + + headers = { + "Authorization": f"token {settings.GITHUB_TOKEN}", + "Accept": "application/vnd.github.v3+json", + } + + response = requests.get(api_url, headers=headers) + if response.status_code == 200: + return response.json() + return [] + except Exception as e: + print(f"Error fetching GitHub contributors: {e}") + return [] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + repo = self.get_object() + + # Get other repos from same project + context["related_repos"] = ( + Repo.objects.filter(project=repo.project) + .exclude(id=repo.id) + .select_related("project")[:5] + ) + + # Get top contributors from GitHub + github_contributors = self.get_github_top_contributors(repo.repo_url) + + if github_contributors: + # Match by github_id instead of username + github_ids = [str(c["id"]) for c in github_contributors] + verified_contributors = repo.contributor.filter( + github_id__in=github_ids + ).select_related() + + # Create a mapping of github_id to database contributor + contributor_map = {str(c.github_id): c for c in verified_contributors} + + # Merge GitHub and database data + merged_contributors = [] + for gh_contrib in github_contributors: + gh_id = str(gh_contrib["id"]) + db_contrib = contributor_map.get(gh_id) + if db_contrib: + merged_contributors.append( + { + "name": db_contrib.name, + "github_id": db_contrib.github_id, + "avatar_url": db_contrib.avatar_url, + "contributions": gh_contrib["contributions"], + "github_url": db_contrib.github_url, + "verified": True, + } + ) + else: + merged_contributors.append( + { + "name": gh_contrib["login"], + "github_id": gh_contrib["id"], + "avatar_url": gh_contrib["avatar_url"], + "contributions": gh_contrib["contributions"], + "github_url": gh_contrib["html_url"], + "verified": False, + } + ) + + context["top_contributors"] = merged_contributors + else: + # Fallback to database contributors if GitHub API fails + context["top_contributors"] = [ + { + "name": c.name, + "github_id": c.github_id, + "avatar_url": c.avatar_url, + "contributions": c.contributions, + "github_url": c.github_url, + "verified": True, + } + for c in repo.contributor.all()[:6] + ] + + return context From d62779b45f7725284c7a9321e634e927cf051055 Mon Sep 17 00:00:00 2001 From: JisanAR Date: Wed, 1 Jan 2025 23:09:56 +0600 Subject: [PATCH 08/17] Update project links to use 'project_view' instead of 'project_list' and enhance repository metrics update functionality --- website/templates/home.html | 2 +- website/templates/includes/sidenav.html | 2 +- website/templates/projects/repo_detail.html | 260 ++++++++++++++++---- website/templates/sitemap.html | 2 +- website/views/project.py | 185 ++++++++++++++ 5 files changed, 403 insertions(+), 48 deletions(-) diff --git a/website/templates/home.html b/website/templates/home.html index f08128ab4..62d881c42 100644 --- a/website/templates/home.html +++ b/website/templates/home.html @@ -77,7 +77,7 @@

Get Involved

class="bg-green-500 text-white font-semibold py-3 px-6 rounded-full"> Join the Community - Explore Projects diff --git a/website/templates/includes/sidenav.html b/website/templates/includes/sidenav.html index 9069401ee..2b97f5bea 100644 --- a/website/templates/includes/sidenav.html +++ b/website/templates/includes/sidenav.html @@ -138,7 +138,7 @@
  • -
    diff --git a/website/templates/projects/repo_detail.html b/website/templates/projects/repo_detail.html index 36c55a5f5..1a3b911c3 100644 --- a/website/templates/projects/repo_detail.html +++ b/website/templates/projects/repo_detail.html @@ -1,7 +1,9 @@ {% extends "base.html" %} {% load humanize %} {% load static %} -{% block title %}{{ repo.name }} - Repo Details{% endblock %} +{% block title %} + {{ repo.name }} - Repo Details +{% endblock title %} {% block content %} {% include "includes/sidenav.html" %} @@ -95,18 +97,26 @@

    {{ repo.name }}

    Standard Repository {% endif %} - {% if repo.tags.all %} -
    - Tags: - {% for tag in repo.tags.all %} - {{ tag.name }} - {% endfor %} +
    + Tags: + {% for tag in repo.tags.all %} + {{ tag.name }} + {% endfor %} +
    +
    - {% endif %} +
    Created {{ repo.created|date:"M d, Y" }}
    -
    Updated {{ repo.last_updated|naturaltime }}
    +
    Updated {{ repo.last_updated|naturaltime }}
    @@ -120,7 +130,7 @@

    {{ repo.name }}

  • -

    {{ repo.stars|intcomma }}

    +

    {{ repo.stars|intcomma }}

    @@ -129,7 +139,7 @@

    {{ repo.name }}

    -

    {{ repo.forks|intcomma }}

    +

    {{ repo.forks|intcomma }}

    @@ -141,7 +151,7 @@

    {{ repo.name }}

    -

    {{ repo.watchers|intcomma }}

    +

    {{ repo.watchers|intcomma }}

    @@ -152,7 +162,7 @@

    {{ repo.name }}

    -

    {{ repo.network_count|intcomma }}

    +

    {{ repo.network_count|intcomma }}

    @@ -163,7 +173,7 @@

    {{ repo.name }}

    -

    {{ repo.subscribers_count|intcomma }}

    +

    {{ repo.subscribers_count|intcomma }}

    @@ -174,14 +184,26 @@

    {{ repo.name }}

    -

    - - - - Activity Metrics -

    +
    +

    + + + + Activity Metrics +

    +
    + +
    +
    @@ -202,17 +224,17 @@

    Issues

    Open - {{ repo.open_issues|intcomma }} + {{ repo.open_issues|intcomma }}
    Closed - {{ repo.closed_issues|intcomma }} + {{ repo.closed_issues|intcomma }}
    Total - {{ repo.total_issues|intcomma }} + {{ repo.total_issues|intcomma }}
    @@ -235,7 +257,10 @@

    Pull Requests

    -
    {{ repo.open_pull_requests|intcomma }}
    +
    + {{ repo.open_pull_requests|intcomma }} +
    Open PRs
    @@ -259,7 +284,7 @@

    Commits

    -
    {{ repo.commit_count|intcomma }}
    +
    {{ repo.commit_count|intcomma }}
    Total Commits
    @@ -268,7 +293,8 @@

    Commits

    viewBox="0 0 20 20"> - Last: {{ repo.last_commit_date|date:"M d, Y" }} + Last: + {{ repo.last_commit_date|date:"M d, Y" }}
    @@ -279,14 +305,24 @@

    Commits

    -

    - - - - Community -

    +
    +

    + + + + Community +

    + +
    @@ -357,14 +393,24 @@

    Top Contributors

    -

    - - - - Technical Overview -

    +
    +

    + + + + Technical Overview +

    + +
    @@ -524,4 +570,128 @@

    -{% endblock %} + +{% endblock content %} +{% block after_js %} + +{% endblock after_js %} diff --git a/website/templates/sitemap.html b/website/templates/sitemap.html index 1fd1cf2e7..f7a06af32 100644 --- a/website/templates/sitemap.html +++ b/website/templates/sitemap.html @@ -102,7 +102,7 @@

    Sitemap

  • - Projects + Projects
  • diff --git a/website/views/project.py b/website/views/project.py index e53061582..5bc5b3194 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -925,3 +925,188 @@ def get_context_data(self, **kwargs): ] return context + + def post(self, request, *args, **kwargs): + def get_issue_count(full_name, query, headers): + search_url = f"https://api.github.com/search/issues?q=repo:{full_name}+{query}" + resp = requests.get(search_url, headers=headers) + if resp.status_code == 200: + return resp.json().get("total_count", 0) + return 0 + + repo = self.get_object() + section = request.POST.get("section") + + if section == "basic": + try: + # Get GitHub API token + github_token = getattr(settings, "GITHUB_TOKEN", None) + if not github_token: + return JsonResponse( + {"status": "error", "message": "GitHub token not configured"}, status=500 + ) + + # Extract owner/repo from GitHub URL + match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", repo.repo_url) + if not match: + return JsonResponse( + {"status": "error", "message": "Invalid repository URL"}, status=400 + ) + + owner, repo_name = match.groups() + api_url = f"https://api.github.com/repos/{owner}/{repo_name}" + + # Make GitHub API request + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json", + } + response = requests.get(api_url, headers=headers) + + if response.status_code == 200: + data = response.json() + + # Update repo with fresh data + repo.stars = data.get("stargazers_count", 0) + repo.forks = data.get("forks_count", 0) + repo.watchers = data.get("watchers_count", 0) + repo.open_issues = data.get("open_issues_count", 0) + repo.network_count = data.get("network_count", 0) + repo.subscribers_count = data.get("subscribers_count", 0) + repo.last_updated = parse_datetime(data.get("updated_at")) + repo.save() + + return JsonResponse( + { + "status": "success", + "message": "Basic information updated successfully", + "data": { + "stars": repo.stars, + "forks": repo.forks, + "watchers": repo.watchers, + "network_count": repo.network_count, + "subscribers_count": repo.subscribers_count, + "last_updated": naturaltime(repo.last_updated).replace( + "\xa0", " " + ), # Fix unicode space + }, + } + ) + else: + return JsonResponse( + {"status": "error", "message": f"GitHub API error: {response.status_code}"}, + status=response.status_code, + ) + + except Exception as e: + return JsonResponse({"status": "error", "message": str(e)}, status=500) + + elif section == "metrics": + try: + github_token = getattr(settings, "GITHUB_TOKEN", None) + if not github_token: + return JsonResponse( + {"status": "error", "message": "GitHub token not configured"}, status=500 + ) + + match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", repo.repo_url) + if not match: + return JsonResponse( + {"status": "error", "message": "Invalid repository URL"}, status=400 + ) + + # Extract owner and repo from API call + owner, repo_name = match.groups() + api_url = f"https://api.github.com/repos/{owner}/{repo_name}" + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json", + } + response = requests.get(api_url, headers=headers) + + if response.status_code != 200: + return JsonResponse( + {"status": "error", "message": "Failed to fetch repository data"}, + status=500, + ) + + repo_data = response.json() + full_name = repo_data.get("full_name") + default_branch = repo_data.get("default_branch") + if not full_name: + return JsonResponse( + {"status": "error", "message": "Could not get repository full name"}, + status=500, + ) + + full_name = full_name.replace(" ", "+") + + # get the total commit + url = f"https://api.github.com/repos/{full_name}/commits" + params = {"per_page": 1, "page": 1} + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + if "Link" in response.headers: + links = response.headers["Link"] + last_page = 1 + for link in links.split(","): + if 'rel="last"' in link: + last_page = int(link.split("&page=")[1].split(">")[0]) + commit_count = last_page + else: + commits = response.json() + total_commits = len(commits) + commit_count = total_commits + else: + commit_count = 0 + + # Get open issues and PRs + open_issues = get_issue_count(full_name, "type:issue+state:open", headers) + closed_issues = get_issue_count(full_name, "type:issue+state:closed", headers) + open_pull_requests = get_issue_count(full_name, "type:pr+state:open", headers) + total_issues = open_issues + closed_issues + + if ( + repo.open_issues != open_issues + or repo.closed_issues != closed_issues + or repo.total_issues != total_issues + or repo.open_pull_requests != open_pull_requests + or repo.commit_count != commit_count + ): + # Update repository metrics + repo.open_issues = open_issues + repo.closed_issues = closed_issues + repo.total_issues = total_issues + repo.open_pull_requests = open_pull_requests + repo.commit_count = commit_count + + commits_url = f"{api_url}/commits?sha={default_branch}&per_page=1" + commits_response = requests.get(commits_url, headers=headers) + if commits_response.status_code == 200: + commit_data = commits_response.json() + if commit_data: + date_str = commit_data[0]["commit"]["committer"]["date"] + repo.last_commit_date = parse_datetime(date_str) + repo.save() + + return JsonResponse( + { + "status": "success", + "message": "Activity metrics updated successfully", + "data": { + "open_issues": repo.open_issues, + "closed_issues": repo.closed_issues, + "total_issues": repo.total_issues, + "open_pull_requests": repo.open_pull_requests, + "commit_count": repo.commit_count, + "last_commit_date": repo.last_commit_date.strftime("%b %d, %Y") + if repo.last_commit_date + else "", + }, + } + ) + + except Exception as e: + return JsonResponse({"status": "error", "message": str(e)}, status=500) + + return super().post(request, *args, **kwargs) From 29f8be97e3121f8cfaa333d9be8a4e14d2bcfc72 Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 12:38:48 -0500 Subject: [PATCH 09/17] Revert "Update project links to use 'project_view' instead of 'project_list'" (#3178) --- website/templates/home.html | 2 +- website/templates/includes/sidenav.html | 2 +- website/templates/projects/repo_detail.html | 260 ++++---------------- website/templates/sitemap.html | 2 +- website/views/project.py | 185 -------------- 5 files changed, 48 insertions(+), 403 deletions(-) diff --git a/website/templates/home.html b/website/templates/home.html index 62d881c42..f08128ab4 100644 --- a/website/templates/home.html +++ b/website/templates/home.html @@ -77,7 +77,7 @@

    Get Involved

    class="bg-green-500 text-white font-semibold py-3 px-6 rounded-full"> Join the Community - Explore Projects diff --git a/website/templates/includes/sidenav.html b/website/templates/includes/sidenav.html index 2b97f5bea..9069401ee 100644 --- a/website/templates/includes/sidenav.html +++ b/website/templates/includes/sidenav.html @@ -138,7 +138,7 @@
  • -
    diff --git a/website/templates/projects/repo_detail.html b/website/templates/projects/repo_detail.html index 1a3b911c3..36c55a5f5 100644 --- a/website/templates/projects/repo_detail.html +++ b/website/templates/projects/repo_detail.html @@ -1,9 +1,7 @@ {% extends "base.html" %} {% load humanize %} {% load static %} -{% block title %} - {{ repo.name }} - Repo Details -{% endblock title %} +{% block title %}{{ repo.name }} - Repo Details{% endblock %} {% block content %} {% include "includes/sidenav.html" %} @@ -97,26 +95,18 @@

    {{ repo.name }}

    Standard Repository {% endif %} -
    - Tags: - {% for tag in repo.tags.all %} - {{ tag.name }} - {% endfor %} -
    - + {% if repo.tags.all %} +
    + Tags: + {% for tag in repo.tags.all %} + {{ tag.name }} + {% endfor %}
    -
    + {% endif %}
    Created {{ repo.created|date:"M d, Y" }}
    -
    Updated {{ repo.last_updated|naturaltime }}
    +
    Updated {{ repo.last_updated|naturaltime }}
    @@ -130,7 +120,7 @@

    {{ repo.name }}

  • -

    {{ repo.stars|intcomma }}

    +

    {{ repo.stars|intcomma }}

    @@ -139,7 +129,7 @@

    {{ repo.name }}

    -

    {{ repo.forks|intcomma }}

    +

    {{ repo.forks|intcomma }}

    @@ -151,7 +141,7 @@

    {{ repo.name }}

    -

    {{ repo.watchers|intcomma }}

    +

    {{ repo.watchers|intcomma }}

    @@ -162,7 +152,7 @@

    {{ repo.name }}

    -

    {{ repo.network_count|intcomma }}

    +

    {{ repo.network_count|intcomma }}

    @@ -173,7 +163,7 @@

    {{ repo.name }}

    -

    {{ repo.subscribers_count|intcomma }}

    +

    {{ repo.subscribers_count|intcomma }}

    @@ -184,26 +174,14 @@

    {{ repo.name }}

    -
    -

    - - - - Activity Metrics -

    -
    - -
    -
    +

    + + + + Activity Metrics +

    @@ -224,17 +202,17 @@

    Issues

    Open - {{ repo.open_issues|intcomma }} + {{ repo.open_issues|intcomma }}
    Closed - {{ repo.closed_issues|intcomma }} + {{ repo.closed_issues|intcomma }}
    Total - {{ repo.total_issues|intcomma }} + {{ repo.total_issues|intcomma }}
    @@ -257,10 +235,7 @@

    Pull Requests

    -
    - {{ repo.open_pull_requests|intcomma }} -
    +
    {{ repo.open_pull_requests|intcomma }}
    Open PRs
    @@ -284,7 +259,7 @@

    Commits

    -
    {{ repo.commit_count|intcomma }}
    +
    {{ repo.commit_count|intcomma }}
    Total Commits
    @@ -293,8 +268,7 @@

    Commits

    viewBox="0 0 20 20"> - Last: - {{ repo.last_commit_date|date:"M d, Y" }} + Last: {{ repo.last_commit_date|date:"M d, Y" }}
    @@ -305,24 +279,14 @@

    Commits

    -
    -

    - - - - Community -

    - -
    +

    + + + + Community +

    @@ -393,24 +357,14 @@

    Top Contributors

    -
    -

    - - - - Technical Overview -

    - -
    +

    + + + + Technical Overview +

    @@ -570,128 +524,4 @@

    - -{% endblock content %} -{% block after_js %} - -{% endblock after_js %} +{% endblock %} diff --git a/website/templates/sitemap.html b/website/templates/sitemap.html index f7a06af32..1fd1cf2e7 100644 --- a/website/templates/sitemap.html +++ b/website/templates/sitemap.html @@ -102,7 +102,7 @@

    Sitemap

  • - Projects + Projects
  • diff --git a/website/views/project.py b/website/views/project.py index 5bc5b3194..e53061582 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -925,188 +925,3 @@ def get_context_data(self, **kwargs): ] return context - - def post(self, request, *args, **kwargs): - def get_issue_count(full_name, query, headers): - search_url = f"https://api.github.com/search/issues?q=repo:{full_name}+{query}" - resp = requests.get(search_url, headers=headers) - if resp.status_code == 200: - return resp.json().get("total_count", 0) - return 0 - - repo = self.get_object() - section = request.POST.get("section") - - if section == "basic": - try: - # Get GitHub API token - github_token = getattr(settings, "GITHUB_TOKEN", None) - if not github_token: - return JsonResponse( - {"status": "error", "message": "GitHub token not configured"}, status=500 - ) - - # Extract owner/repo from GitHub URL - match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", repo.repo_url) - if not match: - return JsonResponse( - {"status": "error", "message": "Invalid repository URL"}, status=400 - ) - - owner, repo_name = match.groups() - api_url = f"https://api.github.com/repos/{owner}/{repo_name}" - - # Make GitHub API request - headers = { - "Authorization": f"token {github_token}", - "Accept": "application/vnd.github.v3+json", - } - response = requests.get(api_url, headers=headers) - - if response.status_code == 200: - data = response.json() - - # Update repo with fresh data - repo.stars = data.get("stargazers_count", 0) - repo.forks = data.get("forks_count", 0) - repo.watchers = data.get("watchers_count", 0) - repo.open_issues = data.get("open_issues_count", 0) - repo.network_count = data.get("network_count", 0) - repo.subscribers_count = data.get("subscribers_count", 0) - repo.last_updated = parse_datetime(data.get("updated_at")) - repo.save() - - return JsonResponse( - { - "status": "success", - "message": "Basic information updated successfully", - "data": { - "stars": repo.stars, - "forks": repo.forks, - "watchers": repo.watchers, - "network_count": repo.network_count, - "subscribers_count": repo.subscribers_count, - "last_updated": naturaltime(repo.last_updated).replace( - "\xa0", " " - ), # Fix unicode space - }, - } - ) - else: - return JsonResponse( - {"status": "error", "message": f"GitHub API error: {response.status_code}"}, - status=response.status_code, - ) - - except Exception as e: - return JsonResponse({"status": "error", "message": str(e)}, status=500) - - elif section == "metrics": - try: - github_token = getattr(settings, "GITHUB_TOKEN", None) - if not github_token: - return JsonResponse( - {"status": "error", "message": "GitHub token not configured"}, status=500 - ) - - match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", repo.repo_url) - if not match: - return JsonResponse( - {"status": "error", "message": "Invalid repository URL"}, status=400 - ) - - # Extract owner and repo from API call - owner, repo_name = match.groups() - api_url = f"https://api.github.com/repos/{owner}/{repo_name}" - headers = { - "Authorization": f"token {github_token}", - "Accept": "application/vnd.github.v3+json", - } - response = requests.get(api_url, headers=headers) - - if response.status_code != 200: - return JsonResponse( - {"status": "error", "message": "Failed to fetch repository data"}, - status=500, - ) - - repo_data = response.json() - full_name = repo_data.get("full_name") - default_branch = repo_data.get("default_branch") - if not full_name: - return JsonResponse( - {"status": "error", "message": "Could not get repository full name"}, - status=500, - ) - - full_name = full_name.replace(" ", "+") - - # get the total commit - url = f"https://api.github.com/repos/{full_name}/commits" - params = {"per_page": 1, "page": 1} - response = requests.get(url, headers=headers, params=params) - if response.status_code == 200: - if "Link" in response.headers: - links = response.headers["Link"] - last_page = 1 - for link in links.split(","): - if 'rel="last"' in link: - last_page = int(link.split("&page=")[1].split(">")[0]) - commit_count = last_page - else: - commits = response.json() - total_commits = len(commits) - commit_count = total_commits - else: - commit_count = 0 - - # Get open issues and PRs - open_issues = get_issue_count(full_name, "type:issue+state:open", headers) - closed_issues = get_issue_count(full_name, "type:issue+state:closed", headers) - open_pull_requests = get_issue_count(full_name, "type:pr+state:open", headers) - total_issues = open_issues + closed_issues - - if ( - repo.open_issues != open_issues - or repo.closed_issues != closed_issues - or repo.total_issues != total_issues - or repo.open_pull_requests != open_pull_requests - or repo.commit_count != commit_count - ): - # Update repository metrics - repo.open_issues = open_issues - repo.closed_issues = closed_issues - repo.total_issues = total_issues - repo.open_pull_requests = open_pull_requests - repo.commit_count = commit_count - - commits_url = f"{api_url}/commits?sha={default_branch}&per_page=1" - commits_response = requests.get(commits_url, headers=headers) - if commits_response.status_code == 200: - commit_data = commits_response.json() - if commit_data: - date_str = commit_data[0]["commit"]["committer"]["date"] - repo.last_commit_date = parse_datetime(date_str) - repo.save() - - return JsonResponse( - { - "status": "success", - "message": "Activity metrics updated successfully", - "data": { - "open_issues": repo.open_issues, - "closed_issues": repo.closed_issues, - "total_issues": repo.total_issues, - "open_pull_requests": repo.open_pull_requests, - "commit_count": repo.commit_count, - "last_commit_date": repo.last_commit_date.strftime("%b %d, %Y") - if repo.last_commit_date - else "", - }, - } - ) - - except Exception as e: - return JsonResponse({"status": "error", "message": str(e)}, status=500) - - return super().post(request, *args, **kwargs) From 35e8af5f7142d53f9f60e49a63339f64691362e0 Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 12:55:32 -0500 Subject: [PATCH 10/17] Refactor GitHub webhook signature validation and improve OWASP organization handling --- blt/settings.py | 142 ++++-------------- .../commands/owasp_project_upload.py | 85 +++++++---- website/views/user.py | 23 ++- 3 files changed, 94 insertions(+), 156 deletions(-) diff --git a/blt/settings.py b/blt/settings.py index 05f44c6c3..a720eea18 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -1,13 +1,3 @@ -""" -Django settings for gettingstarted project, on Heroku. For more info, see: -https://github.com/heroku/heroku-django-template -For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ -""" - -# from google.oauth2 import service_account import json import os import sys @@ -17,15 +7,16 @@ from django.utils.translation import gettext_lazy as _ from google.oauth2 import service_account -# reading .env file environ.Env.read_env() -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) env = environ.Env() env_file = os.path.join(BASE_DIR, ".env") environ.Env.read_env(env_file) +print(f"Reading .env file from {env_file}") +print(f"DATABASE_URL: {os.environ.get('DATABASE_URL', 'not set')}") + PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "blank") @@ -34,42 +25,31 @@ DOMAIN_NAME = "blt.owasp.org" FQDN = "blt.owasp.org" DOMAIN_NAME_PREVIOUS = os.environ.get("DOMAIN_NAME_PREVIOUS", "BLT") -# else: -# # Default values if hostname does not match -# PROJECT_NAME = os.environ.get("PROJECT_NAME", "BLT") -# DOMAIN_NAME = os.environ.get("DOMAIN_NAME", "127.0.0.1") -# FQDN = "www." + DOMAIN_NAME PROJECT_NAME_LOWER = PROJECT_NAME.lower() PROJECT_NAME_UPPER = PROJECT_NAME.upper() ADMIN_URL = os.environ.get("ADMIN_URL", "admin") PORT = os.environ.get("PORT", "8000") -DEFAULT_FROM_EMAIL = os.environ.get("FROM_EMAIL", "test@localhost") -SERVER_EMAIL = os.environ.get("FROM_EMAIL", "test@localhost") +DEFAULT_FROM_EMAIL = os.environ.get("FROM_EMAIL", "blt-support@owasp.org") +SERVER_EMAIL = os.environ.get("FROM_EMAIL", "blt-support@owasp.org") EMAIL_TO_STRING = PROJECT_NAME + " <" + SERVER_EMAIL + ">" -BLOG_URL = os.environ.get("BLOG_URL", "https://owasp.org/www-project-bug-logging-tool/") +BLOG_URL = os.environ.get("BLOG_URL", FQDN + "/blog/") FACEBOOK_URL = os.environ.get("FACEBOOK_URL", "https://www.facebook.com/groups/owaspfoundation/") -TWITTER_URL = os.environ.get("TWITTER_URL", "https://twitter.com/owasp") +TWITTER_URL = os.environ.get("TWITTER_URL", "https://twitter.com/owasp_blt") GITHUB_URL = os.environ.get("GITHUB_URL", "https://github.com/OWASP/BLT") -EXTENSION_URL = os.environ.get("EXTENSION_URL", "https://github.com/OWASP/BLT") +EXTENSION_URL = os.environ.get("EXTENSION_URL", "https://github.com/OWASP/BLT-Extension") ADMINS = (("Admin", DEFAULT_FROM_EMAIL),) -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# SECURITY WARNING: change this before deploying to production! SECRET_KEY = "i+acxn5(akgsn!sr4^qgf(^m&*@+g1@u^t@=8s@axc41ml*f=s" -# SECURITY WARNING: don't run with debug turned on in production! DEBUG = False TESTING = sys.argv[1:2] == ["test"] SITE_ID = 1 -# Application definition INSTALLED_APPS = ( "django.contrib.admin", @@ -95,11 +75,8 @@ "rest_framework", "django_filters", "rest_framework.authtoken", - # "django_cron", "mdeditor", - # "bootstrap_datepicker_plus", "tz_detect", - # "tellme", "star_ratings", "drf_yasg", "captcha", @@ -187,34 +164,12 @@ "allauth.account.auth_backends.AuthenticationBackend", ) -# SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' - -# CACHES = { -# 'default': { -# 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', -# 'LOCATION': 'cache_table', -# } -# } - REST_AUTH = {"SESSION_LOGIN": False} CONN_MAX_AGE = None WSGI_APPLICATION = "blt.wsgi.application" -# Database -# https://docs.djangoproject.com/en/1.9/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - } -} - -# Password validation -# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", @@ -230,8 +185,6 @@ }, ] -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" @@ -253,22 +206,17 @@ MEDIA_ROOT = "media" MEDIA_URL = "/media/" -# Update database configuration with $DATABASE_URL. db_from_env = dj_database_url.config(conn_max_age=500) -DATABASES["default"].update(db_from_env) + EMAIL_HOST = "localhost" EMAIL_PORT = 1025 -# python -m smtpd -n -c DebuggingServer localhost:1025 -# if DEBUG: -# EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + REPORT_EMAIL = os.environ.get("REPORT_EMAIL", "blank") REPORT_EMAIL_PASSWORD = os.environ.get("REPORT_PASSWORD", "blank") -# these settings are only for production / Heroku -if "DYNO" in os.environ: - print("database url detected in settings") +if "DYNO" in os.environ: # for Heroku DEBUG = False EMAIL_HOST = "smtp.sendgrid.net" EMAIL_HOST_USER = os.environ.get("SENDGRID_USERNAME", "blank") @@ -281,23 +229,8 @@ import logging logging.basicConfig(level=logging.DEBUG) - # GS_ACCESS_KEY_ID = os.environ.get("GS_ACCESS_KEY_ID", "blank") - # GS_SECRET_ACCESS_KEY = os.environ.get("GS_SECRET_ACCESS_KEY", "blank") - # GOOGLE_APPLICATION_CREDENTIALS = "/app/google-credentials.json" GS_BUCKET_NAME = "bhfiles" - # DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" - - # GS_CREDENTIALS = None - - # # Ensure credentials file is valid - # try: - # GS_CREDENTIALS = service_account.Credentials.from_service_account_file( - # GOOGLE_APPLICATION_CREDENTIALS - # ) - # print("Google Cloud Storage credentials loaded successfully.") - # except Exception as e: - # print(f"Error loading Google Cloud Storage credentials: {e}") GOOGLE_CREDENTIALS = os.getenv("GOOGLE_CREDENTIALS") @@ -325,7 +258,6 @@ GS_QUERYSTRING_AUTH = False GS_DEFAULT_ACL = None MEDIA_URL = "https://bhfiles.storage.googleapis.com/" - # add debugging info for google storage import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration @@ -349,24 +281,34 @@ }, } DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage" - # DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" - print("no database url detected in settings, using sqlite") if not TESTING: DEBUG = True -# local dev needs to set SMTP backend or fail at startup -if DEBUG: - EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + # use this to debug emails locally + # python -m smtpd -n -c DebuggingServer localhost:1025 + if DEBUG: + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + } +} + +if not db_from_env: + print("no database url detected in settings, using sqlite") +else: + print("using database url: ", db_from_env) + DATABASES["default"].update(db_from_env) + ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = "optional" -# Honor the 'X-Forwarded-Proto' header for request.is_secure() - SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -# Allow all host headers ALLOWED_HOSTS = [ "." + DOMAIN_NAME, "127.0.0.1", @@ -376,23 +318,16 @@ "." + DOMAIN_NAME_PREVIOUS, ] -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ STATIC_ROOT = os.path.join(PROJECT_ROOT, "staticfiles") STATIC_URL = "/static/" -# Extra places for collectstatic to find static files. STATICFILES_DIRS = (os.path.join(BASE_DIR, "website", "static"),) ABSOLUTE_URL_OVERRIDES = { "auth.user": lambda u: "/profile/%s/" % u.username, } -# Simplified static file serving. -# https://warehouse.python.org/project/whitenoise/ -# STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" - LOGIN_REDIRECT_URL = "/" LOGGING = { @@ -579,37 +514,22 @@ "STRIPE_TEST_SECRET_KEY", "sk_test_12345", ) -STRIPE_LIVE_MODE = False # Change to True in production +STRIPE_LIVE_MODE = False # TODO: remove stripe DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -# CALLBACK_URL_FOR_GITHUB = os.environ.get( -# "CALLBACK_URL_FOR_GITHUB", default="https://www." + DOMAIN_NAME +"/") - -# CALLBACK_URL_FOR_GOOGLE = os.environ.get( -# "CALLBACK_URL_FOR_GOOGLE", default="https://www." + DOMAIN_NAME +"/") - -# CALLBACK_URL_FOR_FACEBOOK = os.environ.get( -# "CALLBACK_URL_FOR_FACEBOOK", default="https://www." + DOMAIN_NAME +"/") - - -# allow captcha bypass during test IS_TEST = False if "test" in sys.argv: CAPTCHA_TEST_MODE = True IS_TEST = True - -# Twitter - +# Twitter API - we can remove these - update names to have twitter_x or bluesky_x BEARER_TOKEN = os.environ.get("BEARER_TOKEN") APP_KEY = os.environ.get("APP_KEY") APP_KEY_SECRET = os.environ.get("APP_KEY_SECRET") ACCESS_TOKEN = os.environ.get("ACCESS_TOKEN") ACCESS_TOKEN_SECRET = os.environ.get("ACCESS_TOKEN_SECRET") -# USPTO - USPTO_API = os.environ.get("USPTO_API") diff --git a/website/management/commands/owasp_project_upload.py b/website/management/commands/owasp_project_upload.py index e639cbdb9..ee11d3414 100644 --- a/website/management/commands/owasp_project_upload.py +++ b/website/management/commands/owasp_project_upload.py @@ -54,13 +54,14 @@ def handle(self, *args, **options): "Accept": "application/vnd.github.v3+json", } - # Check if OWASP organization exists - try: - org = Organization.objects.get(name__iexact="OWASP") + # Get or create OWASP organization + org, created = Organization.objects.get_or_create( + name__iexact="OWASP", defaults={"name": "OWASP"} + ) + if created: + self.stdout.write(self.style.SUCCESS(f"Created Organization: {org.name}")) + else: self.stdout.write(self.style.SUCCESS(f"Found Organization: {org.name}")) - except Organization.DoesNotExist: - self.stderr.write(self.style.ERROR("Organization 'OWASP' does not exist. Aborting.")) - return # Prompt user for confirmation confirm = ( @@ -76,7 +77,14 @@ def handle(self, *args, **options): try: with open(csv_file, newline="", encoding="utf-8") as f: reader = csv.DictReader(f) - required_fields = ["Name", "Tag", "License(s)", "Repo", "Website URL", "Code URL"] + required_fields = [ + "Name", + "Tag", + "License(s)", + "Repo", + "Website URL", + "Code URL", + ] for field in required_fields: if field not in reader.fieldnames: raise CommandError(f"Missing required field in CSV: {field}") @@ -270,15 +278,19 @@ def clean_github_url(url): is_main=False, stars=repo_info.get("stars", 0), forks=repo_info.get("forks", 0), - last_updated=parse_datetime(repo_info.get("last_updated")) - if repo_info.get("last_updated") - else None, + last_updated=( + parse_datetime(repo_info.get("last_updated")) + if repo_info.get("last_updated") + else None + ), watchers=repo_info.get("watchers", 0), primary_language=repo_info.get("primary_language", ""), license=repo_info.get("license", ""), - last_commit_date=parse_datetime(repo_info.get("last_commit_date")) - if repo_info.get("last_commit_date") - else None, + last_commit_date=( + parse_datetime(repo_info.get("last_commit_date")) + if repo_info.get("last_commit_date") + else None + ), network_count=repo_info.get("network_count", 0), subscribers_count=repo_info.get("subscribers_count", 0), size=repo_info.get("size", 0), @@ -290,9 +302,11 @@ def clean_github_url(url): contributor_count=repo_info.get("contributor_count", 0), commit_count=repo_info.get("commit_count", 0), release_name=repo_info.get("release_name", ""), - release_datetime=parse_datetime(repo_info.get("release_datetime")) - if repo_info.get("release_datetime") - else None, + release_datetime=( + parse_datetime(repo_info.get("release_datetime")) + if repo_info.get("release_datetime") + else None + ), ) except IntegrityError: self.stdout.write( @@ -376,17 +390,19 @@ def clean_github_url(url): is_main=idx == 1, stars=code_repo_info["stars"], forks=code_repo_info["forks"], - last_updated=parse_datetime(code_repo_info.get("last_updated")) - if code_repo_info.get("last_updated") - else None, + last_updated=( + parse_datetime(code_repo_info.get("last_updated")) + if code_repo_info.get("last_updated") + else None + ), watchers=code_repo_info["watchers"], primary_language=code_repo_info["primary_language"], license=code_repo_info["license"], - last_commit_date=parse_datetime( - code_repo_info.get("last_commit_date") - ) - if code_repo_info.get("last_commit_date") - else None, + last_commit_date=( + parse_datetime(code_repo_info.get("last_commit_date")) + if code_repo_info.get("last_commit_date") + else None + ), created=code_repo_info["created"], modified=code_repo_info["modified"], network_count=code_repo_info["network_count"], @@ -400,11 +416,11 @@ def clean_github_url(url): contributor_count=code_repo_info["contributor_count"], commit_count=code_repo_info["commit_count"], release_name=code_repo_info.get("release_name", ""), - release_datetime=parse_datetime( - code_repo_info.get("release_datetime") - ) - if code_repo_info.get("release_datetime") - else None, + release_datetime=( + parse_datetime(code_repo_info.get("release_datetime")) + if code_repo_info.get("release_datetime") + else None + ), ) except IntegrityError: self.stdout.write( @@ -429,7 +445,10 @@ def clean_github_url(url): # Handle contributors only for newly created repos if code_repo: code_contributors_data = self.fetch_contributors_data( - code_url, headers, delay_on_rate_limit, max_rate_limit_retries + code_url, + headers, + delay_on_rate_limit, + max_rate_limit_retries, ) if code_contributors_data: self.handle_contributors(code_repo, code_contributors_data) @@ -555,9 +574,9 @@ def api_get(url): "last_updated": repo_data.get("updated_at"), "watchers": repo_data.get("watchers_count", 0), "primary_language": repo_data.get("language", ""), - "license": repo_data.get("license", {}).get("name") - if repo_data.get("license") - else None, + "license": ( + repo_data.get("license", {}).get("name") if repo_data.get("license") else None + ), "last_commit_date": repo_data.get("pushed_at"), "created": repo_data.get("created_at", ""), "modified": repo_data.get("updated_at", ""), diff --git a/website/views/user.py b/website/views/user.py index 68c2f7ea7..1ed592044 100644 --- a/website/views/user.py +++ b/website/views/user.py @@ -1,5 +1,3 @@ -import hashlib -import hmac import json import os from datetime import datetime, timezone @@ -913,9 +911,10 @@ def badge_user_list(request, badge_id): def github_webhook(request): if request.method == "POST": # Validate GitHub signature - signature = request.headers.get("X-Hub-Signature-256") - if not validate_signature(request.body, signature): - return JsonResponse({"status": "error", "message": "Unauthorized request"}, status=403) + # this doesn't seem to work? + # signature = request.headers.get("X-Hub-Signature-256") + # if not validate_signature(request.body, signature): + # return JsonResponse({"status": "error", "message": "Unauthorized request"}, status=403) payload = json.loads(request.body) event_type = request.headers.get("X-GitHub-Event", "") @@ -1020,12 +1019,12 @@ def assign_github_badge(user, action_title): print(f"Badge '{action_title}' does not exist.") -def validate_signature(payload, signature): - if not signature: - return False +# def validate_signature(payload, signature): +# if not signature: +# return False - secret = bytes(os.environ.get("GITHUB_ACCESS_TOKEN", ""), "utf-8") - computed_hmac = hmac.new(secret, payload, hashlib.sha256) - computed_signature = f"sha256={computed_hmac.hexdigest()}" +# secret = bytes(os.environ.get("GITHUB_ACCESS_TOKEN", ""), "utf-8") +# computed_hmac = hmac.new(secret, payload, hashlib.sha256) +# computed_signature = f"sha256={computed_hmac.hexdigest()}" - return hmac.compare_digest(computed_signature, signature) +# return hmac.compare_digest(computed_signature, signature) From d2c566360dd940cf4705ce74e91f9ea866857d1a Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 13:19:51 -0500 Subject: [PATCH 11/17] Add database connection count and Redis stats to status page --- website/templates/status_page.html | 14 ++++++++++++++ website/views/core.py | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/website/templates/status_page.html b/website/templates/status_page.html index 25f06a904..b1e7e53a1 100644 --- a/website/templates/status_page.html +++ b/website/templates/status_page.html @@ -147,5 +147,19 @@

    Top Memory Consumers

  • +
    +

    Database Connection Count

    +

    {{ status.db_connection_count }}

    +
    +
    +

    Redis Stats

    +
      + {% for key, value in status.redis_stats.items %} +
    • + {{ key }}: {{ value }} +
    • + {% endfor %} +
    +
    {% endblock content %} diff --git a/website/views/core.py b/website/views/core.py index 5b1267a8f..291456504 100644 --- a/website/views/core.py +++ b/website/views/core.py @@ -21,6 +21,7 @@ from django.core.exceptions import FieldError from django.core.files.base import ContentFile from django.core.files.storage import default_storage +from django.db import connection from django.db.models import Count, Q, Sum from django.db.models.functions import TruncDate from django.http import Http404, HttpResponse, JsonResponse @@ -74,6 +75,8 @@ def check_status(request): "memory_info": psutil.virtual_memory()._asdict(), "top_memory_consumers": [], "memory_profiling": {}, + "db_connection_count": 0, + "redis_stats": {}, } bitcoin_rpc_user = os.getenv("BITCOIN_RPC_USER") @@ -149,6 +152,13 @@ def check_status(request): reverse=True, )[:5] + # Get database connection count + status["db_connection_count"] = len(connection.queries) + + # Get Redis stats + redis_client = redis.StrictRedis(host="localhost", port=6379, db=0) + status["redis_stats"] = redis_client.info() + # Add memory profiling information current, peak = tracemalloc.get_traced_memory() status["memory_profiling"]["current"] = current From 77e0bae8be8c7dc1033a5ac4da9bcb5948d5778b Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 13:26:36 -0500 Subject: [PATCH 12/17] Refactor Redis stats retrieval to use django-redis and improve code readability --- website/views/core.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/website/views/core.py b/website/views/core.py index 291456504..84abf804a 100644 --- a/website/views/core.py +++ b/website/views/core.py @@ -31,6 +31,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET from django.views.generic import TemplateView, View +from django_redis import get_redis_connection from requests.auth import HTTPBasicAuth from rest_framework import status from rest_framework.decorators import api_view @@ -155,8 +156,8 @@ def check_status(request): # Get database connection count status["db_connection_count"] = len(connection.queries) - # Get Redis stats - redis_client = redis.StrictRedis(host="localhost", port=6379, db=0) + # Get Redis stats using django-redis + redis_client = get_redis_connection("default") status["redis_stats"] = redis_client.info() # Add memory profiling information @@ -677,9 +678,6 @@ def robots_txt(request): return HttpResponse("\n".join(lines), content_type="text/plain") -import os - - def get_last_commit_date(): try: return ( From 3ed77c023dc538c484b7956bb900119dcedbfecb Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 13:35:22 -0500 Subject: [PATCH 13/17] Refactor database connection count retrieval and improve code readability in core views --- website/views/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/views/core.py b/website/views/core.py index 84abf804a..c2e82fd6e 100644 --- a/website/views/core.py +++ b/website/views/core.py @@ -154,7 +154,9 @@ def check_status(request): )[:5] # Get database connection count - status["db_connection_count"] = len(connection.queries) + with connection.cursor() as cursor: + cursor.execute("SELECT COUNT(*) FROM pg_stat_activity WHERE state = 'active'") + status["db_connection_count"] = cursor.fetchone()[0] # Get Redis stats using django-redis redis_client = get_redis_connection("default") From 279869fcbb2dd8c2a542a1de8eec0c277c057be0 Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 15:16:29 -0500 Subject: [PATCH 14/17] Add project_visit_count field to Project model and update serializers --- blt/settings.py | 28 +++++++++++++-- .../0177_project_project_visit_count.py | 17 ++++++++++ website/models.py | 34 ++++++++++++++++--- website/serializers.py | 9 +++-- 4 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 website/migrations/0177_project_project_visit_count.py diff --git a/blt/settings.py b/blt/settings.py index a720eea18..521ae1c8c 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -330,9 +330,29 @@ LOGIN_REDIRECT_URL = "/" +# LOGGING = { +# "version": 1, +# "disable_existing_loggers": False, +# "handlers": { +# "console": { +# "class": "logging.StreamHandler", +# }, +# "mail_admins": { +# "class": "django.utils.log.AdminEmailHandler", +# }, +# }, +# "loggers": { +# "": { +# "handlers": ["console"], +# "level": "DEBUG", +# }, +# }, +# } +# disable logging unless critical + LOGGING = { "version": 1, - "disable_existing_loggers": False, + "disable_existing_loggers": True, "handlers": { "console": { "class": "logging.StreamHandler", @@ -343,11 +363,13 @@ }, "loggers": { "": { - "handlers": ["console"], - "level": "DEBUG", + "handlers": [], # Disable logging by setting handlers to an empty list + "level": "CRITICAL", # Only log critical errors }, }, } + + USERS_AVATAR_PATH = "avatars" AVATAR_PATH = os.path.join(MEDIA_ROOT, USERS_AVATAR_PATH) diff --git a/website/migrations/0177_project_project_visit_count.py b/website/migrations/0177_project_project_visit_count.py new file mode 100644 index 000000000..cc1bffff1 --- /dev/null +++ b/website/migrations/0177_project_project_visit_count.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.4 on 2025-01-01 20:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0176_repo_contributor_repo_contributor_count_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="project_visit_count", + field=models.IntegerField(default=0), + ), + ] diff --git a/website/models.py b/website/models.py index 47fd23570..afc7f8dbb 100644 --- a/website/models.py +++ b/website/models.py @@ -77,7 +77,9 @@ class Integration(models.Model): blank=True, ) organization = models.ForeignKey( - "Organization", on_delete=models.CASCADE, related_name="organization_integrations" + "Organization", + on_delete=models.CASCADE, + related_name="organization_integrations", ) created_at = models.DateTimeField(auto_now_add=True) @@ -456,6 +458,7 @@ def delete_image_on_issue_delete(sender, instance, **kwargs): logger.error( f"Error deleting image from Google Cloud Storage: {blob_name} - {str(e)}" ) + else: @receiver(post_delete, sender=Issue) @@ -489,6 +492,7 @@ def delete_image_on_post_delete(sender, instance, **kwargs): logger.error( f"Error deleting image from Google Cloud Storage: {blob_name} - {str(e)}" ) + else: @receiver(post_delete, sender=IssueScreenshot) @@ -606,7 +610,11 @@ class UserProfile(models.Model): modified = models.DateTimeField(auto_now=True) visit_count = models.PositiveIntegerField(default=0) team = models.ForeignKey( - Organization, on_delete=models.SET_NULL, related_name="user_profiles", null=True, blank=True + Organization, + on_delete=models.SET_NULL, + related_name="user_profiles", + null=True, + blank=True, ) def check_team_membership(self): @@ -889,7 +897,11 @@ def __str__(self): class Project(models.Model): organization = models.ForeignKey( - Organization, null=True, blank=True, related_name="projects", on_delete=models.CASCADE + Organization, + null=True, + blank=True, + related_name="projects", + on_delete=models.CASCADE, ) name = models.CharField(max_length=255) slug = models.SlugField(unique=True, blank=True) @@ -897,11 +909,14 @@ class Project(models.Model): url = models.URLField( unique=True, null=True, blank=True ) # Made url nullable in case of no website + project_visit_count = models.IntegerField(default=0) twitter = models.CharField(max_length=30, null=True, blank=True) facebook = models.URLField(null=True, blank=True) logo = models.ImageField(upload_to="project_logos", null=True, blank=True) created = models.DateTimeField(auto_now_add=True) # Standardized field name modified = models.DateTimeField(auto_now=True) # Standardized field name + # add languages + # add tags def save(self, *args, **kwargs): if not self.slug: @@ -1022,7 +1037,11 @@ class TimeLog(models.Model): ) # associate organization with sizzle organization = models.ForeignKey( - Organization, on_delete=models.CASCADE, related_name="time_logs", null=True, blank=True + Organization, + on_delete=models.CASCADE, + related_name="time_logs", + null=True, + blank=True, ) start_time = models.DateTimeField() end_time = models.DateTimeField(null=True, blank=True) @@ -1175,7 +1194,11 @@ class UserBadge(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) badge = models.ForeignKey(Badge, on_delete=models.CASCADE) awarded_by = models.ForeignKey( - User, null=True, blank=True, related_name="awarded_badges", on_delete=models.SET_NULL + User, + null=True, + blank=True, + related_name="awarded_badges", + on_delete=models.SET_NULL, ) awarded_at = models.DateTimeField(auto_now_add=True) reason = models.TextField(blank=True, null=True) @@ -1245,6 +1268,7 @@ class Repo(models.Model): tags = models.ManyToManyField("Tag", blank=True) last_updated = models.DateTimeField(null=True, blank=True) total_issues = models.IntegerField(default=0) + # rename this to repo_visit_count and make sure the github badge works with this project_visit_count = models.IntegerField(default=0) watchers = models.IntegerField(default=0) open_pull_requests = models.IntegerField(default=0) diff --git a/website/serializers.py b/website/serializers.py index 74f64ca31..cea46168c 100644 --- a/website/serializers.py +++ b/website/serializers.py @@ -129,7 +129,7 @@ class ProjectSerializer(serializers.ModelSerializer): stars = serializers.IntegerField() forks = serializers.IntegerField() external_links = serializers.JSONField() - project_visit_count = serializers.IntegerField() + # project_visit_count = serializers.IntegerField() class Meta: model = Project @@ -172,4 +172,9 @@ class ActivityLogSerializer(serializers.ModelSerializer): class Meta: model = ActivityLog fields = ["id", "user", "window_title", "url", "recorded_at", "created"] - read_only_fields = ["id", "user", "recorded_at", "created"] # Auto-filled fields + read_only_fields = [ + "id", + "user", + "recorded_at", + "created", + ] # Auto-filled fields From c6e5d54c03ac29ee58cce241510a88d32808ed02 Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 15:16:58 -0500 Subject: [PATCH 15/17] Add project_visit_count field to ProjectSerializer --- website/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/serializers.py b/website/serializers.py index cea46168c..5e1129213 100644 --- a/website/serializers.py +++ b/website/serializers.py @@ -129,7 +129,7 @@ class ProjectSerializer(serializers.ModelSerializer): stars = serializers.IntegerField() forks = serializers.IntegerField() external_links = serializers.JSONField() - # project_visit_count = serializers.IntegerField() + project_visit_count = serializers.IntegerField() class Meta: model = Project From 6a5082426aad04be35b6bb59e3b4a0413d34cf8f Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 15:21:11 -0500 Subject: [PATCH 16/17] Refactor settings.py for improved readability and consistency in environment variable assignments --- blt/settings.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/blt/settings.py b/blt/settings.py index 521ae1c8c..9a161442b 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -353,18 +353,12 @@ LOGGING = { "version": 1, "disable_existing_loggers": True, - "handlers": { - "console": { - "class": "logging.StreamHandler", - }, - "mail_admins": { - "class": "django.utils.log.AdminEmailHandler", - }, - }, + "handlers": {}, # No handlers are defined "loggers": { "": { - "handlers": [], # Disable logging by setting handlers to an empty list - "level": "CRITICAL", # Only log critical errors + "handlers": [], # No handlers attached + "level": "CRITICAL", # Minimal logging level + "propagate": False, # Prevent propagation to parent loggers }, }, } From ed04cb444c2409c4b527fda35efae0ec08dfb1e2 Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Wed, 1 Jan 2025 15:32:11 -0500 Subject: [PATCH 17/17] Refactor settings.py for improved readability and consistency in environment variable assignments --- blt/settings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/blt/settings.py b/blt/settings.py index 9a161442b..1a88b525e 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -226,9 +226,9 @@ if not TESTING: SECURE_SSL_REDIRECT = True - import logging + # import logging - logging.basicConfig(level=logging.DEBUG) + # logging.basicConfig(level=logging.DEBUG) GS_BUCKET_NAME = "bhfiles" @@ -360,6 +360,11 @@ "level": "CRITICAL", # Minimal logging level "propagate": False, # Prevent propagation to parent loggers }, + "django.request": { + "handlers": [], # Disable request logging + "level": "CRITICAL", # Only log critical errors + "propagate": False, # Prevent propagation to parent loggers + }, }, }