mirror of
https://github.com/ajurna/cbwebreader.git
synced 2026-11-02 10:36:55 +00:00
**Replace Flake8 with Ruff, refactor type hints, and enhance archive processing logic**
- Removed `flake8` and `flake8-annotations` from dependencies, replaced by `ruff` for linting. - Introduced cleaner type hinting and modern Python annotations (PEP 604 style) across multiple modules. - Moved archive processing to a dedicated helper (`comic.archives`) for improved maintainability. - Updated and simplified logic for archive file retrieval, directory cleaning, and user access validation. - Improved error handling and added support for user classification restrictions in API responses.
This commit is contained in:
1 parent
147a3a54ef
commit
1148f4072b
13 files changed
+226
-217
No files matched your search
@@ -8,3 +8,8 @@ repos:
|
|||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.17.0
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [ --fix ]
|
||||||
+4
-3
@@ -23,10 +23,11 @@ from drf_yasg import openapi
|
|||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
# from rest_framework_extensions.routers import ExtendedDefaultRouter
|
|
||||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenBlacklistView
|
|
||||||
|
|
||||||
from comic import rest, feeds
|
# from rest_framework_extensions.routers import ExtendedDefaultRouter
|
||||||
|
from rest_framework_simplejwt.views import TokenBlacklistView, TokenObtainPairView, TokenRefreshView
|
||||||
|
|
||||||
|
from comic import feeds, rest
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
openapi.Info(
|
openapi.Info(
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Directory, ComicBook, ComicStatus, UserMisc
|
from .models import ComicBook, ComicStatus, Directory, UserMisc
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Directory)
|
@admin.register(Directory)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Shared helpers for listing the pages inside a comic archive."""
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
import zipfile
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import rarfile
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveFile(NamedTuple):
|
||||||
|
file_name: str
|
||||||
|
mime_type: str
|
||||||
|
|
||||||
|
|
||||||
|
def get_archive_files(archive: zipfile.ZipFile | rarfile.RarFile) -> list[ArchiveFile]:
|
||||||
|
"""Return the image entries of an archive in page order.
|
||||||
|
|
||||||
|
Metadata entries such as ComicInfo.xml are not pages. Counting them would make
|
||||||
|
page_count disagree with the page list the reader uses, so a book could never
|
||||||
|
be marked as finished.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for name in sorted(archive.namelist()):
|
||||||
|
if name.endswith('/'):
|
||||||
|
continue
|
||||||
|
mime_type = mimetypes.guess_type(name)[0]
|
||||||
|
if not mime_type or name.lower().endswith('.xml'):
|
||||||
|
continue
|
||||||
|
result.append(ArchiveFile(name, mime_type))
|
||||||
|
return result
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.syndication.views import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from django.db.models import Case, When, PositiveSmallIntegerField, F, QuerySet
|
from django.db.models import Case, F, PositiveSmallIntegerField, QuerySet, When
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from comic.models import ComicBook, UserMisc, Directory
|
from comic.models import ComicBook, Directory, UserMisc
|
||||||
|
|
||||||
|
|
||||||
class RecentComicsAPI(Feed):
|
class RecentComicsAPI(Feed):
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.base_user import AbstractBaseUser
|
from django.contrib.auth.base_user import AbstractBaseUser
|
||||||
@@ -27,7 +26,7 @@ class Command(BaseCommand):
|
|||||||
self.OUTPUT = options.get('out', False)
|
self.OUTPUT = options.get('out', False)
|
||||||
self.scan_directory()
|
self.scan_directory()
|
||||||
|
|
||||||
def scan_directory(self, user: Optional[AbstractBaseUser] = None, directory: Optional[Directory] = None) -> None:
|
def scan_directory(self, user: AbstractBaseUser | None = None, directory: Directory | None = None) -> None:
|
||||||
if not user:
|
if not user:
|
||||||
user_model = get_user_model()
|
user_model = get_user_model()
|
||||||
user = user_model.objects.first()
|
user = user_model.objects.first()
|
||||||
|
|||||||
+17
-23
@@ -1,16 +1,13 @@
|
|||||||
import io
|
import io
|
||||||
import mimetypes
|
|
||||||
import uuid
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Union, Tuple, Final, IO
|
from typing import IO, Final
|
||||||
|
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
import pymupdf
|
import pymupdf
|
||||||
import rarfile
|
import rarfile
|
||||||
from PIL import Image, UnidentifiedImageError
|
|
||||||
from PIL.Image import Image as Image_type
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||||
@@ -19,7 +16,10 @@ from django.db.models import UniqueConstraint
|
|||||||
from django_boost.models.fields import AutoOneToOneField
|
from django_boost.models.fields import AutoOneToOneField
|
||||||
from imagekit.models import ProcessedImageField
|
from imagekit.models import ProcessedImageField
|
||||||
from imagekit.processors import ResizeToFill
|
from imagekit.processors import ResizeToFill
|
||||||
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
from PIL.Image import Image as Image_type
|
||||||
|
|
||||||
|
from comic.archives import get_archive_files
|
||||||
from comic.errors import NotCompatibleArchive
|
from comic.errors import NotCompatibleArchive
|
||||||
|
|
||||||
if settings.UNRAR_TOOL:
|
if settings.UNRAR_TOOL:
|
||||||
@@ -84,7 +84,7 @@ class Directory(models.Model):
|
|||||||
return reduce(lambda x, y: Path(x, y), path_items)
|
return reduce(lambda x, y: Path(x, y), path_items)
|
||||||
return Path(path_items[0])
|
return Path(path_items[0])
|
||||||
|
|
||||||
def get_path_items(self, path_items: Optional[List] = None) -> List[Path]:
|
def get_path_items(self, path_items: list | None = None) -> list[Path]:
|
||||||
if path_items is None:
|
if path_items is None:
|
||||||
path_items = []
|
path_items = []
|
||||||
path_items.append(self.name)
|
path_items.append(self.name)
|
||||||
@@ -92,7 +92,7 @@ class Directory(models.Model):
|
|||||||
self.parent.get_path_items(path_items)
|
self.parent.get_path_items(path_items)
|
||||||
return path_items
|
return path_items
|
||||||
|
|
||||||
def get_path_objects(self, path_items: Optional[List] = None) -> List["Directory"]:
|
def get_path_objects(self, path_items: list | None = None) -> list[Directory]:
|
||||||
if path_items is None:
|
if path_items is None:
|
||||||
path_items = []
|
path_items = []
|
||||||
path_items.append(self)
|
path_items.append(self)
|
||||||
@@ -141,7 +141,7 @@ class ComicBook(models.Model):
|
|||||||
return Path(base_dir, self.directory.get_path(), self.file_name)
|
return Path(base_dir, self.directory.get_path(), self.file_name)
|
||||||
return Path(base_dir, self.file_name)
|
return Path(base_dir, self.file_name)
|
||||||
|
|
||||||
def get_image(self, page: int) -> Union[Tuple[IO[bytes], str], Tuple[bool, bool]]:
|
def get_image(self, page: int) -> tuple[IO[bytes], str] | tuple[bool, bool]:
|
||||||
if self.file_name.lower().endswith('.pdf'):
|
if self.file_name.lower().endswith('.pdf'):
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
doc = pymupdf.open(self.get_pdf())
|
doc = pymupdf.open(self.get_pdf())
|
||||||
@@ -168,15 +168,16 @@ class ComicBook(models.Model):
|
|||||||
except zipfile.BadZipfile:
|
except zipfile.BadZipfile:
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
file_name, file_mime = self.get_archive_files(archive)[page]
|
file_name, file_mime = get_archive_files(archive)[page]
|
||||||
return archive.open(file_name), file_mime
|
return archive.open(file_name), file_mime
|
||||||
|
|
||||||
def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]:
|
def generate_thumbnail_pdf(self, page_index: int = 0) -> tuple[io.BytesIO, Image_type, str]:
|
||||||
img, pil_data = self._get_pdf_image(page_index if page_index else 0)
|
img, pil_data = self._get_pdf_image(page_index if page_index else 0)
|
||||||
return img, pil_data, 'Image/JPEG'
|
return img, pil_data, 'Image/JPEG'
|
||||||
|
|
||||||
def generate_thumbnail_archive(self, page_index: int = 0) -> Union[Tuple[io.BytesIO, Image_type, str],
|
def generate_thumbnail_archive(
|
||||||
Tuple[bool, bool, bool]]:
|
self, page_index: int = 0
|
||||||
|
) -> tuple[io.BytesIO, Image_type, str] | tuple[bool, bool, bool]:
|
||||||
img, content_type, pil_data = False, False, False
|
img, content_type, pil_data = False, False, False
|
||||||
if page_index:
|
if page_index:
|
||||||
img, content_type = self.get_image(page_index)
|
img, content_type = self.get_image(page_index)
|
||||||
@@ -209,7 +210,7 @@ class ComicBook(models.Model):
|
|||||||
)
|
)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def _get_pdf_image(self, page_index: int) -> Tuple[io.BytesIO, Image_type]:
|
def _get_pdf_image(self, page_index: int) -> tuple[io.BytesIO, Image_type]:
|
||||||
doc = pymupdf.open(self.get_pdf())
|
doc = pymupdf.open(self.get_pdf())
|
||||||
page = doc[page_index]
|
page = doc[page_index]
|
||||||
pix = page.get_pixmap()
|
pix = page.get_pixmap()
|
||||||
@@ -222,7 +223,7 @@ class ComicBook(models.Model):
|
|||||||
return img, pil_data
|
return img, pil_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
|
def process_comic_book(comic_file_path: Path, directory: Directory = False) -> ComicBook | Path:
|
||||||
try:
|
try:
|
||||||
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
|
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
|
||||||
book.directory = directory
|
book.directory = directory
|
||||||
@@ -240,7 +241,7 @@ class ComicBook(models.Model):
|
|||||||
return comic_file_path
|
return comic_file_path
|
||||||
|
|
||||||
if archive_type == 'archive':
|
if archive_type == 'archive':
|
||||||
book.page_count = len(book.get_archive_files(archive))
|
book.page_count = len(get_archive_files(archive))
|
||||||
elif archive_type == 'pdf':
|
elif archive_type == 'pdf':
|
||||||
book.page_count = archive.page_count
|
book.page_count = archive.page_count
|
||||||
|
|
||||||
@@ -252,7 +253,7 @@ class ComicBook(models.Model):
|
|||||||
return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name)
|
return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name)
|
||||||
return Path(settings.COMIC_BOOK_VOLUME, self.file_name)
|
return Path(settings.COMIC_BOOK_VOLUME, self.file_name)
|
||||||
|
|
||||||
def get_archive(self) -> Tuple[Union[rarfile.RarFile, zipfile.ZipFile, pymupdf.Document], str]:
|
def get_archive(self) -> tuple[rarfile.RarFile | zipfile.ZipFile | pymupdf.Document, str]:
|
||||||
archive_path = self.get_archive_path
|
archive_path = self.get_archive_path
|
||||||
try:
|
try:
|
||||||
return rarfile.RarFile(archive_path), 'archive'
|
return rarfile.RarFile(archive_path), 'archive'
|
||||||
@@ -273,17 +274,10 @@ class ComicBook(models.Model):
|
|||||||
def get_page_count(self) -> int:
|
def get_page_count(self) -> int:
|
||||||
archive, archive_type = self.get_archive()
|
archive, archive_type = self.get_archive()
|
||||||
if archive_type == 'archive':
|
if archive_type == 'archive':
|
||||||
return len(self.get_archive_files(archive))
|
return len(get_archive_files(archive))
|
||||||
elif archive_type == 'pdf':
|
elif archive_type == 'pdf':
|
||||||
return archive.page_count
|
return archive.page_count
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_archive_files(archive: Union[zipfile.ZipFile, rarfile.RarFile]) -> List[Tuple[str, str]]:
|
|
||||||
return [
|
|
||||||
(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
|
|
||||||
if not x.endswith('/') and mimetypes.guess_type(x)[0] and not x.endswith('xml')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ComicStatus(models.Model):
|
class ComicStatus(models.Model):
|
||||||
user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE)
|
||||||
|
|||||||
+20
-26
@@ -1,21 +1,27 @@
|
|||||||
import mimetypes
|
|
||||||
import zipfile
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NamedTuple, List, Optional, Union
|
|
||||||
|
|
||||||
import rarfile
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.base_user import AbstractBaseUser
|
from django.contrib.auth.base_user import AbstractBaseUser
|
||||||
from django.db.models import Count, Q, F, Case, When, PositiveSmallIntegerField, QuerySet, ExpressionWrapper, \
|
from django.db.models import (
|
||||||
IntegerField
|
Case,
|
||||||
|
Count,
|
||||||
|
ExpressionWrapper,
|
||||||
|
F,
|
||||||
|
IntegerField,
|
||||||
|
PositiveSmallIntegerField,
|
||||||
|
Q,
|
||||||
|
QuerySet,
|
||||||
|
When,
|
||||||
|
)
|
||||||
|
|
||||||
from comic import models
|
from comic import models
|
||||||
|
from comic.archives import get_archive_files
|
||||||
from comic.errors import NotCompatibleArchive
|
from comic.errors import NotCompatibleArchive
|
||||||
|
|
||||||
|
|
||||||
def generate_directory(user: AbstractBaseUser, directory: Optional[models.Directory] = None) \
|
def generate_directory(user: AbstractBaseUser, directory: models.Directory | None = None) \
|
||||||
-> List[Union[models.Directory, models.ComicBook]]:
|
-> list[models.Directory | models.ComicBook]:
|
||||||
dir_path = Path(settings.COMIC_BOOK_VOLUME, directory.path) if directory else settings.COMIC_BOOK_VOLUME
|
dir_path = Path(settings.COMIC_BOOK_VOLUME, directory.path) if directory else settings.COMIC_BOOK_VOLUME
|
||||||
files = []
|
files = []
|
||||||
|
|
||||||
@@ -63,9 +69,9 @@ def generate_directory(user: AbstractBaseUser, directory: Optional[models.Direct
|
|||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
def clean_directories(directories: QuerySet, dir_path: Path, directory: Optional[models.Directory] = None) -> None:
|
def clean_directories(directories: QuerySet, dir_path: Path, directory: models.Directory | None = None) -> None:
|
||||||
dir_db_set = set(Path(settings.COMIC_BOOK_VOLUME, x.path) for x in directories)
|
dir_db_set = {Path(settings.COMIC_BOOK_VOLUME, x.path) for x in directories}
|
||||||
dir_list = set(x for x in sorted(dir_path.glob('*')) if x.is_dir())
|
dir_list = {x for x in sorted(dir_path.glob('*')) if x.is_dir()}
|
||||||
# Create new directories db instances
|
# Create new directories db instances
|
||||||
for new_directory in dir_list - dir_db_set:
|
for new_directory in dir_list - dir_db_set:
|
||||||
models.Directory(name=new_directory.name, parent=directory).save()
|
models.Directory(name=new_directory.name, parent=directory).save()
|
||||||
@@ -75,10 +81,10 @@ def clean_directories(directories: QuerySet, dir_path: Path, directory: Optional
|
|||||||
models.Directory.objects.get(name=stale_directory.name, parent=directory).delete()
|
models.Directory.objects.get(name=stale_directory.name, parent=directory).delete()
|
||||||
|
|
||||||
|
|
||||||
def clean_files(files: QuerySet, user: AbstractBaseUser, dir_path: Path, directory: Optional[models.Directory] = None) \
|
def clean_files(files: QuerySet, user: AbstractBaseUser, dir_path: Path, directory: models.Directory | None = None) \
|
||||||
-> None:
|
-> None:
|
||||||
file_list = set(x for x in sorted(dir_path.glob('*')) if x.is_file())
|
file_list = {x for x in sorted(dir_path.glob('*')) if x.is_file()}
|
||||||
files_db_set = set(Path(dir_path, x.file_name) for x in files)
|
files_db_set = {Path(dir_path, x.file_name) for x in files}
|
||||||
|
|
||||||
# Parse new comics
|
# Parse new comics
|
||||||
books_to_add = []
|
books_to_add = []
|
||||||
@@ -106,15 +112,3 @@ def clean_files(files: QuerySet, user: AbstractBaseUser, dir_path: Path, directo
|
|||||||
# Remove stale comic instances
|
# Remove stale comic instances
|
||||||
for stale_comic in files_db_set - file_list:
|
for stale_comic in files_db_set - file_list:
|
||||||
models.ComicBook.objects.get(file_name=stale_comic.name, directory=directory).delete()
|
models.ComicBook.objects.get(file_name=stale_comic.name, directory=directory).delete()
|
||||||
|
|
||||||
|
|
||||||
class ArchiveFile(NamedTuple):
|
|
||||||
file_name: str
|
|
||||||
mime_type: str
|
|
||||||
|
|
||||||
|
|
||||||
def get_archive_files(archive: Union[zipfile.ZipFile, rarfile.RarFile]) -> List[ArchiveFile]:
|
|
||||||
return [
|
|
||||||
ArchiveFile(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
|
|
||||||
if not x.endswith('/') and mimetypes.guess_type(x)[0]
|
|
||||||
]
|
|
||||||
+95
-77
@@ -1,17 +1,18 @@
|
|||||||
|
import json
|
||||||
|
from collections.abc import Iterable
|
||||||
from http.client import HTTPResponse
|
from http.client import HTTPResponse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union, Optional, Dict, Iterable, List
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.password_validation import validate_password
|
from django.contrib.auth.password_validation import validate_password
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Case, When, F, PositiveSmallIntegerField, FileField, QuerySet
|
from django.db.models import Case, F, FileField, PositiveSmallIntegerField, QuerySet, When
|
||||||
from django.utils.crypto import get_random_string
|
|
||||||
from django.http import FileResponse
|
from django.http import FileResponse
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import viewsets, serializers, mixins, permissions, status, renderers
|
from rest_framework import mixins, permissions, renderers, serializers, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
@@ -23,6 +24,17 @@ from comic.processing import generate_directory
|
|||||||
from comic.util import generate_breadcrumbs_from_path
|
from comic.util import generate_breadcrumbs_from_path
|
||||||
|
|
||||||
|
|
||||||
|
def user_can_read(user: models.User, directory: models.Directory | None) -> bool:
|
||||||
|
"""Whether the user's classification allowance covers this book.
|
||||||
|
|
||||||
|
Books that are not inside a directory are treated as the highest rating, which
|
||||||
|
matches how the browse API reports them.
|
||||||
|
"""
|
||||||
|
misc, _ = models.UserMisc.objects.get_or_create(user=user)
|
||||||
|
classification = directory.classification if directory else models.Directory.Classification.C_18
|
||||||
|
return classification <= misc.allowed_to_read
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
classification = serializers.SlugRelatedField(many=False, read_only=True, slug_field='allowed_to_read',
|
classification = serializers.SlugRelatedField(many=False, read_only=True, slug_field='allowed_to_read',
|
||||||
source='usermisc')
|
source='usermisc')
|
||||||
@@ -61,8 +73,7 @@ class UserViewSet(viewsets.ModelViewSet): # pylint: disable=too-many-ancestors
|
|||||||
"""
|
"""
|
||||||
target_user = get_object_or_404(User, id=pk)
|
target_user = get_object_or_404(User, id=pk)
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid() and target_user.username == serializer.data['username']:
|
||||||
if target_user.username == serializer.data['username']:
|
|
||||||
password = get_random_string(12)
|
password = get_random_string(12)
|
||||||
target_user.set_password(password)
|
target_user.set_password(password)
|
||||||
target_user.save()
|
target_user.save()
|
||||||
@@ -89,7 +100,7 @@ class UserViewSet(viewsets.ModelViewSet): # pylint: disable=too-many-ancestors
|
|||||||
|
|
||||||
|
|
||||||
class BrowseFileField(serializers.FileField):
|
class BrowseFileField(serializers.FileField):
|
||||||
def to_representation(self, value: Optional[FileField]) -> Optional[str]:
|
def to_representation(self, value: FileField | None) -> str | None:
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
return Path(settings.MEDIA_URL, value.name).as_posix()
|
return Path(settings.MEDIA_URL, value.name).as_posix()
|
||||||
@@ -127,7 +138,7 @@ class BrowseViewSet(viewsets.GenericViewSet):
|
|||||||
|
|
||||||
@swagger_auto_schema(responses={status.HTTP_200_OK: BrowseSerializer(many=True)})
|
@swagger_auto_schema(responses={status.HTTP_200_OK: BrowseSerializer(many=True)})
|
||||||
def retrieve(self, request: Request, selector: UUID) -> Response:
|
def retrieve(self, request: Request, selector: UUID) -> Response:
|
||||||
directory = models.Directory.objects.get(selector=selector)
|
directory = get_object_or_404(models.Directory, selector=selector)
|
||||||
serializer = self.get_serializer(generate_directory(request.user, directory), many=True)
|
serializer = self.get_serializer(generate_directory(request.user, directory), many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@@ -135,12 +146,8 @@ class BrowseViewSet(viewsets.GenericViewSet):
|
|||||||
@action(methods=['get'], detail=True, serializer_class=BreadcrumbSerializer)
|
@action(methods=['get'], detail=True, serializer_class=BreadcrumbSerializer)
|
||||||
def breadcrumbs(self, _request: Request, selector: UUID) -> Response:
|
def breadcrumbs(self, _request: Request, selector: UUID) -> Response:
|
||||||
queryset = []
|
queryset = []
|
||||||
comic = False
|
comic = models.ComicBook.objects.filter(selector=selector).first()
|
||||||
try:
|
directory = comic.directory if comic else get_object_or_404(models.Directory, selector=selector)
|
||||||
directory = models.Directory.objects.get(selector=selector)
|
|
||||||
except models.Directory.DoesNotExist:
|
|
||||||
comic = models.ComicBook.objects.get(selector=selector)
|
|
||||||
directory = comic.directory
|
|
||||||
|
|
||||||
for index, item in enumerate(generate_breadcrumbs_from_path(directory, comic)):
|
for index, item in enumerate(generate_breadcrumbs_from_path(directory, comic)):
|
||||||
queryset.append({
|
queryset.append({
|
||||||
@@ -165,23 +172,15 @@ class GenerateThumbnailViewSet(viewsets.ViewSet):
|
|||||||
@swagger_auto_schema(responses={status.HTTP_200_OK: GenerateThumbnailSerializer()})
|
@swagger_auto_schema(responses={status.HTTP_200_OK: GenerateThumbnailSerializer()})
|
||||||
def retrieve(self, _request: Request, selector: UUID) -> Response:
|
def retrieve(self, _request: Request, selector: UUID) -> Response:
|
||||||
try:
|
try:
|
||||||
directory = models.Directory.objects.get(selector=selector)
|
target = models.Directory.objects.get(selector=selector)
|
||||||
if not directory.thumbnail:
|
|
||||||
directory.generate_thumbnail()
|
|
||||||
return Response(
|
|
||||||
self.serializer_class({
|
|
||||||
"selector": directory.selector,
|
|
||||||
"thumbnail": directory.thumbnail
|
|
||||||
}).data
|
|
||||||
)
|
|
||||||
except models.Directory.DoesNotExist:
|
except models.Directory.DoesNotExist:
|
||||||
comic = models.ComicBook.objects.get(selector=selector)
|
target = get_object_or_404(models.ComicBook, selector=selector)
|
||||||
if not comic.thumbnail:
|
if not target.thumbnail:
|
||||||
comic.generate_thumbnail()
|
target.generate_thumbnail()
|
||||||
return Response(
|
return Response(
|
||||||
self.serializer_class({
|
self.serializer_class({
|
||||||
"selector": comic.selector,
|
"selector": target.selector,
|
||||||
"thumbnail": comic.thumbnail
|
"thumbnail": target.thumbnail
|
||||||
}).data
|
}).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,9 +212,12 @@ class ReadViewSet(viewsets.GenericViewSet):
|
|||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
lookup_field = 'selector'
|
lookup_field = 'selector'
|
||||||
|
|
||||||
@swagger_auto_schema(responses={status.HTTP_200_OK: ReadSerializer()})
|
@swagger_auto_schema(responses={status.HTTP_200_OK: ReadSerializer(),
|
||||||
|
status.HTTP_403_FORBIDDEN: 'User below classification allowed'})
|
||||||
def retrieve(self, request: Request, selector: UUID) -> Response:
|
def retrieve(self, request: Request, selector: UUID) -> Response:
|
||||||
comic = get_object_or_404(models.ComicBook, selector=selector)
|
comic = get_object_or_404(models.ComicBook, selector=selector)
|
||||||
|
if not user_can_read(request.user, comic.directory):
|
||||||
|
return Response(status=status.HTTP_403_FORBIDDEN, data={'errors': 'Not allowed to read.'})
|
||||||
_, _ = models.UserMisc.objects.get_or_create(user=request.user)
|
_, _ = models.UserMisc.objects.get_or_create(user=request.user)
|
||||||
comic_status, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
|
comic_status, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
|
||||||
comic_list = list(models.ComicBook.objects.filter(directory=comic.directory).order_by('file_name'))
|
comic_list = list(models.ComicBook.objects.filter(directory=comic.directory).order_by('file_name'))
|
||||||
@@ -247,17 +249,17 @@ class ReadViewSet(viewsets.GenericViewSet):
|
|||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@swagger_auto_schema(responses={status.HTTP_200_OK: 'PDF Binary Data',
|
@swagger_auto_schema(responses={status.HTTP_200_OK: 'PDF Binary Data',
|
||||||
status.HTTP_400_BAD_REQUEST: 'User below classification allowed'})
|
status.HTTP_403_FORBIDDEN: 'User below classification allowed',
|
||||||
|
status.HTTP_404_NOT_FOUND: 'Comic file not found'})
|
||||||
@action(methods=['get'], detail=True)
|
@action(methods=['get'], detail=True)
|
||||||
def pdf(self, request: Request, selector: UUID) -> Union[FileResponse, Response, HTTPResponse]:
|
def pdf(self, request: Request, selector: UUID) -> FileResponse | Response | HTTPResponse:
|
||||||
book = models.ComicBook.objects.get(selector=selector)
|
book = get_object_or_404(models.ComicBook, selector=selector)
|
||||||
misc, _ = models.UserMisc.objects.get_or_create(user=request.user)
|
if not user_can_read(request.user, book.directory):
|
||||||
try:
|
return Response(status=status.HTTP_403_FORBIDDEN, data={'errors': 'Not allowed to read.'})
|
||||||
if book.directory.classification > misc.allowed_to_read:
|
pdf_path = book.get_pdf()
|
||||||
return Response(status=400, data={'errors': 'Not allowed to read.'})
|
if not pdf_path.is_file():
|
||||||
except AttributeError:
|
return Response(status=status.HTTP_404_NOT_FOUND, data={'errors': 'Comic file not found.'})
|
||||||
pass
|
return FileResponse(open(pdf_path, 'rb'), content_type='application/pdf')
|
||||||
return FileResponse(open(book.get_pdf(), 'rb'), content_type='application/pdf')
|
|
||||||
|
|
||||||
@swagger_auto_schema(responses={status.HTTP_200_OK: TypeSerializer()})
|
@swagger_auto_schema(responses={status.HTTP_200_OK: TypeSerializer()})
|
||||||
@action(methods=['get'], detail=True)
|
@action(methods=['get'], detail=True)
|
||||||
@@ -272,15 +274,11 @@ class ReadViewSet(viewsets.GenericViewSet):
|
|||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
comic_status, _ = models.ComicStatus.objects.annotate(page_count=F('comic__page_count')) \
|
comic = get_object_or_404(models.ComicBook, selector=selector)
|
||||||
.get_or_create(comic_id=selector, user=request.user)
|
comic_status, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
|
||||||
comic_status.last_read_page = serializer.data['page']
|
comic_status.last_read_page = serializer.data['page']
|
||||||
comic_status.unread = False
|
comic_status.unread = False
|
||||||
if comic_status.page_count - 1 == comic_status.last_read_page:
|
comic_status.finished = comic.page_count > 0 and comic_status.last_read_page >= comic.page_count - 1
|
||||||
comic_status.finished = True
|
|
||||||
else:
|
|
||||||
comic_status.finished = False
|
|
||||||
|
|
||||||
comic_status.save()
|
comic_status.save()
|
||||||
return Response(self.get_serializer(comic_status).data)
|
return Response(self.get_serializer(comic_status).data)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -293,20 +291,30 @@ class PassthroughRenderer(renderers.BaseRenderer): # pylint: disable=too-few-pu
|
|||||||
media_type = '*/*'
|
media_type = '*/*'
|
||||||
format = ''
|
format = ''
|
||||||
|
|
||||||
def render(self, data: bytes, accepted_media_type: Optional[str] = None, renderer_context: Optional[str] = None) \
|
def render(self, data: bytes, accepted_media_type: str | None = None, renderer_context: str | None = None) \
|
||||||
-> bytes:
|
-> bytes:
|
||||||
|
if isinstance(data, (bytes, bytearray)):
|
||||||
return data
|
return data
|
||||||
|
# Error responses still need to be serialised.
|
||||||
|
return json.dumps(data).encode()
|
||||||
|
|
||||||
|
|
||||||
class ImageViewSet(viewsets.ViewSet):
|
class ImageViewSet(viewsets.ViewSet):
|
||||||
queryset = models.ComicBook.objects.all()
|
queryset = models.ComicBook.objects.all()
|
||||||
lookup_field = 'page'
|
lookup_field = 'page'
|
||||||
renderer_classes = [PassthroughRenderer]
|
renderer_classes = [PassthroughRenderer, renderers.JSONRenderer]
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
@swagger_auto_schema(responses={status.HTTP_200_OK: "A Binary Image response"})
|
@swagger_auto_schema(responses={status.HTTP_200_OK: "A Binary Image response",
|
||||||
def retrieve(self, _request: Request, selector: UUID, page: int) -> FileResponse:
|
status.HTTP_403_FORBIDDEN: 'User below classification allowed',
|
||||||
book = models.ComicBook.objects.get(selector=selector)
|
status.HTTP_404_NOT_FOUND: 'Comic or page not found'})
|
||||||
|
def retrieve(self, _request: Request, selector: UUID, page: int) -> Response:
|
||||||
|
book = get_object_or_404(models.ComicBook, selector=selector)
|
||||||
|
if not user_can_read(_request.user, book.directory):
|
||||||
|
return Response(status=status.HTTP_403_FORBIDDEN, data={'errors': 'Not allowed to read.'})
|
||||||
img, content = book.get_image(int(page) - 1)
|
img, content = book.get_image(int(page) - 1)
|
||||||
|
if not img:
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND, data={'errors': 'Page could not be read.'})
|
||||||
self.renderer_classes[0].media_type = content
|
self.renderer_classes[0].media_type = content
|
||||||
return FileResponse(img, content_type=content)
|
return FileResponse(img, content_type=content)
|
||||||
|
|
||||||
@@ -434,7 +442,7 @@ class ActionViewSet(viewsets.GenericViewSet):
|
|||||||
obj, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
|
obj, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
|
||||||
obj.unread = False
|
obj.unread = False
|
||||||
obj.finished = True
|
obj.finished = True
|
||||||
obj.last_read_page = comic.total_pages
|
obj.last_read_page = max(comic.page_count - 1, 0)
|
||||||
status_to_update.append(obj)
|
status_to_update.append(obj)
|
||||||
models.ComicStatus.objects.bulk_update(status_to_update, ['unread', 'finished', 'last_read_page'])
|
models.ComicStatus.objects.bulk_update(status_to_update, ['unread', 'finished', 'last_read_page'])
|
||||||
return Response({'status': 'marked_read'})
|
return Response({'status': 'marked_read'})
|
||||||
@@ -466,7 +474,7 @@ class ActionViewSet(viewsets.GenericViewSet):
|
|||||||
return Response(serializer.errors,
|
return Response(serializer.errors,
|
||||||
status=status.HTTP_400_BAD_REQUEST)
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def get_comics(self, selectors: Iterable[Union[str, UUID]]) -> List[str]:
|
def get_comics(self, selectors: Iterable[str | UUID]) -> list[str]:
|
||||||
data = set()
|
data = set()
|
||||||
data = data.union(
|
data = data.union(
|
||||||
set(models.ComicBook.objects.filter(selector__in=selectors).values_list('selector', flat=True)))
|
set(models.ComicBook.objects.filter(selector__in=selectors).values_list('selector', flat=True)))
|
||||||
@@ -508,10 +516,10 @@ class PasswordResetSerializer(serializers.Serializer):
|
|||||||
try:
|
try:
|
||||||
validate_password(data)
|
validate_password(data)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
raise serializers.ValidationError(err)
|
raise serializers.ValidationError(err) from err
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, attrs: Dict[str, str]) -> Dict[str, str]:
|
def validate(self, attrs: dict[str, str]) -> dict[str, str]:
|
||||||
super().validate(attrs)
|
super().validate(attrs)
|
||||||
if attrs['new_password'] != attrs['new_password_confirm']:
|
if attrs['new_password'] != attrs['new_password_confirm']:
|
||||||
raise serializers.ValidationError('New passwords do not match')
|
raise serializers.ValidationError('New passwords do not match')
|
||||||
@@ -582,37 +590,47 @@ class DirectoryViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
|
|||||||
permission_classes = [permissions.IsAdminUser]
|
permission_classes = [permissions.IsAdminUser]
|
||||||
lookup_field = 'selector'
|
lookup_field = 'selector'
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: DirectorySerializer(many=True)})
|
@staticmethod
|
||||||
def update(self, request: Request, selector: UUID) -> Response: # pylint: disable=arguments-differ
|
def _descendants(directory: models.Directory) -> list[models.Directory]:
|
||||||
"""
|
"""The directory itself plus every directory below it."""
|
||||||
This will set the classification of a directory and all it's children.
|
to_update = [directory]
|
||||||
"""
|
to_visit = [directory]
|
||||||
main_parent = get_object_or_404(models.Directory, selector=selector)
|
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
|
|
||||||
if serializer.is_valid():
|
|
||||||
main_parent.classification = serializer.data['classification']
|
|
||||||
to_update = {main_parent}
|
|
||||||
to_visit = {main_parent}
|
|
||||||
|
|
||||||
while to_visit:
|
while to_visit:
|
||||||
parent = to_visit.pop()
|
parent = to_visit.pop()
|
||||||
for child in models.Directory.objects.filter(parent=parent):
|
for child in models.Directory.objects.filter(parent=parent):
|
||||||
child.classification = serializer.data['classification']
|
to_visit.append(child)
|
||||||
to_visit.add(child)
|
to_update.append(child)
|
||||||
to_update.add(child)
|
return to_update
|
||||||
models.Directory.objects.bulk_update(to_update, fields=['classification'])
|
|
||||||
data = models.Directory.objects.filter(directory__in=to_update)
|
@swagger_auto_schema(responses={200: DirectorySerializer(many=True)})
|
||||||
response = self.get_serializer(data, many=True)
|
def update(self, request: Request, selector: UUID) -> Response: # pylint: disable=arguments-differ
|
||||||
return Response(response.data)
|
"""
|
||||||
|
This will set the classification of a directory and all of its children.
|
||||||
|
"""
|
||||||
|
main_parent = get_object_or_404(models.Directory, selector=selector)
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
if not serializer.is_valid():
|
||||||
return Response(serializer.errors, status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
to_update = self._descendants(main_parent)
|
||||||
|
for directory in to_update:
|
||||||
|
directory.classification = serializer.data['classification']
|
||||||
|
models.Directory.objects.bulk_update(to_update, fields=['classification'])
|
||||||
|
return Response(self.get_serializer(to_update, many=True).data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(responses={200: DirectorySerializer()})
|
||||||
def partial_update(self, request: Request, *args, **kwargs) -> Response:
|
def partial_update(self, request: Request, *args, **kwargs) -> Response:
|
||||||
"""
|
"""
|
||||||
This will set the classification of a directory and none of its children.
|
This will set the classification of a directory and none of its children.
|
||||||
"""
|
"""
|
||||||
return super().update(request, *args, **kwargs)
|
directory = self.get_object()
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
directory.classification = serializer.data['classification']
|
||||||
|
directory.save()
|
||||||
|
return Response(self.get_serializer(directory).data)
|
||||||
|
|
||||||
|
|
||||||
class InitialSetupSerializer(serializers.Serializer):
|
class InitialSetupSerializer(serializers.Serializer):
|
||||||
|
|||||||
+5
-6
@@ -1,5 +1,4 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from comic.models import ComicBook, Directory
|
from comic.models import ComicBook, Directory
|
||||||
|
|
||||||
@@ -10,12 +9,12 @@ class Breadcrumb:
|
|||||||
selector: str = ''
|
selector: str = ''
|
||||||
|
|
||||||
|
|
||||||
def generate_breadcrumbs_from_path(directory: Optional[Directory] = None, book: Optional[ComicBook] = None):
|
def generate_breadcrumbs_from_path(
|
||||||
|
directory: Directory | None = None,
|
||||||
|
book: ComicBook | None = None,
|
||||||
|
) -> list[Breadcrumb]:
|
||||||
output = [Breadcrumb()]
|
output = [Breadcrumb()]
|
||||||
if directory:
|
folders = directory.get_path_objects() if directory else []
|
||||||
folders = directory.get_path_objects()
|
|
||||||
else:
|
|
||||||
folders = []
|
|
||||||
for item in folders[::-1]:
|
for item in folders[::-1]:
|
||||||
output.append(
|
output.append(
|
||||||
Breadcrumb(
|
Breadcrumb(
|
||||||
|
|||||||
+19
-2
@@ -39,13 +39,30 @@ dependencies = [
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"coverage>=7.13.5",
|
"coverage>=7.13.5",
|
||||||
"flake8>=7.3.0",
|
|
||||||
"flake8-annotations>=3.2.0",
|
|
||||||
"ipython>=9.13.0",
|
"ipython>=9.13.0",
|
||||||
"mypy>=2",
|
"mypy>=2",
|
||||||
"pre-commit>=4.6",
|
"pre-commit>=4.6",
|
||||||
"pylint>=4.0.5",
|
"pylint>=4.0.5",
|
||||||
"pylint-django>=2.7.0",
|
"pylint-django>=2.7.0",
|
||||||
"pyopenssl>=26.2.0",
|
"pyopenssl>=26.2.0",
|
||||||
|
"ruff>=0.17.0",
|
||||||
"werkzeug>=3.1.8",
|
"werkzeug>=3.1.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py314"
|
||||||
|
line-length = 120
|
||||||
|
extend-exclude = [
|
||||||
|
"*/migrations/*",
|
||||||
|
"cbreader/settings",
|
||||||
|
"frontend",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "W", "I", "ANN", "B", "C4", "SIM", "UP"]
|
||||||
|
# These were ignored by the previous flake8-annotations setup (ANN001 is the
|
||||||
|
# modern name for the old ANN101).
|
||||||
|
ignore = ["ANN001", "ANN002", "ANN003"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
combine-as-imports = true
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
[tool:pytest]
|
|
||||||
addopts = --flake8
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
max-line-length = 120
|
|
||||||
ignore =
|
|
||||||
* ANN101
|
|
||||||
* ANN002
|
|
||||||
* ANN003
|
|
||||||
# Ignore rules which contradicts black's formatting choices:
|
|
||||||
; * E501
|
|
||||||
; * W503
|
|
||||||
; * W504
|
|
||||||
; * E266
|
|
||||||
exclude =
|
|
||||||
# Exclude these files
|
|
||||||
*/migrations/*
|
|
||||||
cbreader/settings
|
|
||||||
frontend
|
|
||||||
|
|
||||||
|
|
||||||
[tool:isort]
|
|
||||||
line_length = 119
|
|
||||||
indent = ' '
|
|
||||||
multi_line_output = 3
|
|
||||||
include_trailing_comma = 1
|
|
||||||
@@ -129,14 +129,13 @@ dependencies = [
|
|||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "coverage" },
|
{ name = "coverage" },
|
||||||
{ name = "flake8" },
|
|
||||||
{ name = "flake8-annotations" },
|
|
||||||
{ name = "ipython" },
|
{ name = "ipython" },
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
{ name = "pylint" },
|
{ name = "pylint" },
|
||||||
{ name = "pylint-django" },
|
{ name = "pylint-django" },
|
||||||
{ name = "pyopenssl" },
|
{ name = "pyopenssl" },
|
||||||
|
{ name = "ruff" },
|
||||||
{ name = "werkzeug" },
|
{ name = "werkzeug" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -173,14 +172,13 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "coverage", specifier = ">=7.13.5" },
|
{ name = "coverage", specifier = ">=7.13.5" },
|
||||||
{ name = "flake8", specifier = ">=7.3.0" },
|
|
||||||
{ name = "flake8-annotations", specifier = ">=3.2.0" },
|
|
||||||
{ name = "ipython", specifier = ">=9.13.0" },
|
{ name = "ipython", specifier = ">=9.13.0" },
|
||||||
{ name = "mypy", specifier = ">=2" },
|
{ name = "mypy", specifier = ">=2" },
|
||||||
{ name = "pre-commit", specifier = ">=4.6" },
|
{ name = "pre-commit", specifier = ">=4.6" },
|
||||||
{ name = "pylint", specifier = ">=4.0.5" },
|
{ name = "pylint", specifier = ">=4.0.5" },
|
||||||
{ name = "pylint-django", specifier = ">=2.7.0" },
|
{ name = "pylint-django", specifier = ">=2.7.0" },
|
||||||
{ name = "pyopenssl", specifier = ">=26.2.0" },
|
{ name = "pyopenssl", specifier = ">=26.2.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.17.0" },
|
||||||
{ name = "werkzeug", specifier = ">=3.1.8" },
|
{ name = "werkzeug", specifier = ">=3.1.8" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -607,32 +605,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flake8"
|
|
||||||
version = "7.3.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "mccabe" },
|
|
||||||
{ name = "pycodestyle" },
|
|
||||||
{ name = "pyflakes" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flake8-annotations"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "flake8" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/99/ea/f36a8e013154f214715de01d46d98f12a673c7b978533079ac5caf4cbcce/flake8_annotations-3.2.0.tar.gz", hash = "sha256:71b52ec85a4ec82a10edfad497fe2b390609c95b2ad7d61cc94704bc88a498a0", size = 14169, upload-time = "2025-10-09T21:17:57.816Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/49/ae687154c2cfe623e273143000b81dbd3901d29e13ef30155f1652c18e7c/flake8_annotations-3.2.0-py3-none-any.whl", hash = "sha256:2b0203cac0eb33c43463f1a986c3cc70a448d9d593da75567173f46f64e9213c", size = 16873, upload-time = "2025-10-09T21:17:56.265Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gprof2dot"
|
name = "gprof2dot"
|
||||||
version = "2025.4.14"
|
version = "2025.4.14"
|
||||||
@@ -1130,15 +1102,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycodestyle"
|
|
||||||
version = "2.14.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.23"
|
version = "2.23"
|
||||||
@@ -1148,15 +1111,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyflakes"
|
|
||||||
version = "3.4.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.2"
|
version = "2.19.2"
|
||||||
@@ -1379,6 +1333,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "84.0.0"
|
version = "84.0.0"
|
||||||
|
|||||||
Reference in new issue
Block a user