diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b1bbb06..7e8a21a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,6 +105,7 @@ jobs: with: env_vars: OS,PYTHON fail_ci_if_error: true + required_coverage: 66.0 files: coverage.xml token: ${{ secrets.CODECOV_TOKEN }} verbose: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5970dae9..19428d3e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -152,6 +152,7 @@ jobs: with: env_vars: OS,PYTHON fail_ci_if_error: true + required_coverage: 66.0 files: coverage.xml token: ${{ secrets.CODECOV_TOKEN }} verbose: false diff --git a/docker/Dockerfile b/docker/Dockerfile index c6bc4945..36068d79 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -178,7 +178,7 @@ RUN --mount=type=cache,target=/root/.uv-cache \ --cache-dir=/root/.uv-cache \ --python=/usr/local/bin/python \ --python-preference=system \ - --no-dev --no-editable --frozen + --no-dev --no-editable --frozen --extra distribution # ------------- dist ------------- diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index d33e48d3..8fa429d3 100755 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -3,6 +3,7 @@ export MEDIA_ROOT="${MEDIA_ROOT:-/var/run/app/media}" export STATIC_ROOT="${STATIC_ROOT:-/var/run/app/static}" export DEFAULT_ROOT="${DEFAULT_ROOT:-/var/run/app/default}" +export DEEPFACE_HOME="${DEEPFACE_HOME:-/var/run/app/deepface}" export UWSGI_PROCESSES="${UWSGI_PROCESSES:-"4"}" export DJANGO_SETTINGS_MODULE="${DJANGO_SETTINGS_MODULE:-"hope_dedup_engine.config.settings"}" mkdir -p "${MEDIA_ROOT}" "${STATIC_ROOT}" "${DEFAULT_ROOT}" || echo "Cannot create dirs ${MEDIA_ROOT} ${STATIC_ROOT} ${DEFAULT_ROOT}" @@ -15,10 +16,14 @@ if [ -d "${DEFAULT_ROOT}" ];then chown -R hope:unicef ${DEFAULT_ROOT} fi +if [ -d "${DEEPFACE_HOME}" ];then + chown -R hope:unicef ${DEEPFACE_HOME} +fi echo "MEDIA_ROOT ${MEDIA_ROOT}" echo "STATIC_ROOT ${STATIC_ROOT}" -echo "DEFAULT_ROOT ${DEFAULT_ROOT}" +echo "DEFAULT_ROOT ${DEFAULT_ROOT}" +echo "DEEPFACE_HOME ${DEEPFACE_HOME}" echo "Docker run command: $1" case "$1" in @@ -28,7 +33,6 @@ case "$1" in exit 0 ;; worker) - gosu hope:unicef django-admin syncdnn || exit 1 set -- tini -- "$@" set -- gosu hope:unicef celery -A hope_dedup_engine.config.celery worker -E --loglevel=ERROR --concurrency=4 ;; @@ -36,6 +40,9 @@ case "$1" in set -- tini -- "$@" set -- gosu hope:unicef celery -A hope_dedup_engine.config.celery beat --loglevel=ERROR --scheduler django_celery_beat.schedulers:DatabaseScheduler ;; + syncmodels) + gosu hope:unicef django-admin syncmodels || exit 1 + ;; run) django-admin check --deploy || exit 1 set -- tini -- "$@" diff --git a/pyproject.toml b/pyproject.toml index 7cbe5e13..200289bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ requires-python = ">=3.12" license = {text = "MIT"} dependencies = [ + "deepface>=0.0.93", "Django", "celery[redis]", "django-admin-extra-buttons", @@ -35,10 +36,7 @@ dependencies = [ "sentry-sdk[celery,django]>=2.2.1", "social-auth-app-django", "social-auth-core", - "opencv-contrib-python-headless>=4.10.0.84", - "face-recognition>=1.3.0", "unicef-security", - "uwsgi>=2.0.25.1", "requests>=2.32.3", "numpy>=1.26.4,<2.0.0", "flower>=2.0.1", @@ -47,6 +45,7 @@ dependencies = [ "jsonschema>=4.23.0", "django-celery-boost==0.4.1", "django-svelte-jsoneditor>=0.4.2", + "tf-keras>=2.18.0", ] [project.optional-dependencies] @@ -58,6 +57,10 @@ docs = [ "mkdocs-gen-files>=0.5.0", ] +distribution = [ + "uwsgi>=2.0.28", +] + [tool.uv] package = true dev-dependencies = [ diff --git a/src/hope_dedup_engine/apps/api/admin/__init__.py b/src/hope_dedup_engine/apps/api/admin/__init__.py index c3ccc622..93213df7 100644 --- a/src/hope_dedup_engine/apps/api/admin/__init__.py +++ b/src/hope_dedup_engine/apps/api/admin/__init__.py @@ -1,6 +1,7 @@ from .config import ConfigAdmin # noqa from .deduplicationset import DeduplicationSetAdmin # noqa -from .duplicate import DuplicateAdmin # noqa +from .finding import FindingAdmin # noqa from .hdetoken import HDETokenAdmin # noqa +from .ignored_pair import IgnoredFilenamePairAdmin, IgnoredReferencePkPairAdmin # noqa from .image import ImageAdmin # noqa from .jobs import DedupJob # noqa diff --git a/src/hope_dedup_engine/apps/api/admin/config.py b/src/hope_dedup_engine/apps/api/admin/config.py index cd3da582..38332aeb 100644 --- a/src/hope_dedup_engine/apps/api/admin/config.py +++ b/src/hope_dedup_engine/apps/api/admin/config.py @@ -2,22 +2,16 @@ from typing import Any from django.contrib import messages -from django.contrib.admin import ModelAdmin, register, site -from django.core.exceptions import ValidationError +from django.contrib.admin import ModelAdmin, register from django.db import models from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect, render from django.urls import path, reverse -from admin_extra_buttons.api import button from admin_extra_buttons.mixins import ExtraButtonsMixin from django_svelte_jsoneditor.widgets import SvelteJSONEditorWidget -from hope_dedup_engine.apps.api.forms import EditSchemaForm from hope_dedup_engine.apps.api.models import Config -from hope_dedup_engine.apps.api.utils.shema_manager import SchemaManager -from hope_dedup_engine.apps.api.validators import DefaultValidatingValidator -from hope_dedup_engine.utils.security import is_root @register(Config) @@ -31,15 +25,15 @@ class ConfigAdmin(ExtraButtonsMixin, ModelAdmin): } } - def get_changeform_initial_data(self, request: HttpRequest) -> dict[str, str]: - initial_data = super().get_changeform_initial_data(request) - initial_data["settings"] = {} - try: - schema = SchemaManager.get_or_create() - DefaultValidatingValidator(schema).validate(initial_data["settings"]) - except ValidationError as e: - self.message_user(request, e.message, level=messages.ERROR) - return initial_data + # def get_changeform_initial_data(self, request: HttpRequest) -> dict[str, str]: + # initial_data = super().get_changeform_initial_data(request) + # initial_data["settings"] = {} + # try: + # schema = SchemaManager.get_or_create() + # DefaultValidatingValidator(schema).validate(initial_data["settings"]) + # except ValidationError as e: + # self.message_user(request, e.message, level=messages.ERROR) + # return initial_data def get_urls(self): urls = super().get_urls() @@ -49,11 +43,11 @@ def get_urls(self): self.admin_site.admin_view(self.confirm_save), name="confirm_save_config", ), - path( - "change-settings-schema/", - self.admin_site.admin_view(self.change_settings_schema), - name="change_settings_schema", - ), + # path( + # "change-settings-schema/", + # self.admin_site.admin_view(self.change_settings_schema), + # name="change_settings_schema", + # ), ] return custom_urls + urls @@ -92,40 +86,40 @@ def confirm_save(self, request, object_id) -> HttpResponse: # pragma: no cover }, ) - @button(permission=is_root) - def change_settings_schema( - self, request: HttpRequest - ) -> HttpResponse: # pragma: no cover - context = { - "opts": self.model._meta, - "site_header": site.site_header, - "title": "Change settings shema", - "trail_label": "Settings schema", - "has_view_permission": self.has_view_permission(request), - } + # @button(permission=is_root) + # def change_settings_schema( + # self, request: HttpRequest + # ) -> HttpResponse: # pragma: no cover + # context = { + # "opts": self.model._meta, + # "site_header": site.site_header, + # "title": "Change settings shema", + # "trail_label": "Settings schema", + # "has_view_permission": self.has_view_permission(request), + # } - if request.method == "POST": - form = EditSchemaForm(request.POST) - if form.is_valid(): - try: - SchemaManager.save(form.cleaned_data["schema"]) - except ValidationError as e: - self.message_user(request, e.message, level=messages.ERROR) - else: - self.message_user(request, "Schema has been updated.") - return redirect(reverse("admin:api_config_changelist")) - else: - try: - form = EditSchemaForm(initial={"schema": SchemaManager.get_or_create()}) - except ValidationError as e: - self.message_user(request, e.message, level=messages.ERROR) - return redirect(reverse("admin:api_config_changelist")) + # if request.method == "POST": + # form = EditSchemaForm(request.POST) + # if form.is_valid(): + # try: + # SchemaManager.save(form.cleaned_data["schema"]) + # except ValidationError as e: + # self.message_user(request, e.message, level=messages.ERROR) + # else: + # self.message_user(request, "Schema has been updated.") + # return redirect(reverse("admin:api_config_changelist")) + # else: + # try: + # form = EditSchemaForm(initial={"schema": SchemaManager.get_or_create()}) + # except ValidationError as e: + # self.message_user(request, e.message, level=messages.ERROR) + # return redirect(reverse("admin:api_config_changelist")) - return render( - request, - "admin/api/config/change_settings_schema.html", - { - "form": form, - **context, - }, - ) + # return render( + # request, + # "admin/api/config/change_settings_schema.html", + # { + # "form": form, + # **context, + # }, + # ) diff --git a/src/hope_dedup_engine/apps/api/admin/duplicate.py b/src/hope_dedup_engine/apps/api/admin/finding.py similarity index 81% rename from src/hope_dedup_engine/apps/api/admin/duplicate.py rename to src/hope_dedup_engine/apps/api/admin/finding.py index e4e3c3ec..dd1c1e72 100644 --- a/src/hope_dedup_engine/apps/api/admin/duplicate.py +++ b/src/hope_dedup_engine/apps/api/admin/finding.py @@ -4,21 +4,23 @@ from adminfilters.filters import DjangoLookupFilter, NumberFilter from adminfilters.mixin import AdminFiltersMixin -from hope_dedup_engine.apps.api.models import Duplicate +from hope_dedup_engine.apps.api.models import Finding -@register(Duplicate) -class DuplicateAdmin(AdminFiltersMixin, ModelAdmin): +@register(Finding) +class FindingAdmin(AdminFiltersMixin, ModelAdmin): list_display = ( "id", "deduplication_set", "score", "first_reference_pk", "second_reference_pk", + "error", ) list_filter = ( ("deduplication_set", AutoCompleteFilter), ("score", NumberFilter), + ("error", NumberFilter), DjangoLookupFilter, ) diff --git a/src/hope_dedup_engine/apps/api/admin/ignored_pair.py b/src/hope_dedup_engine/apps/api/admin/ignored_pair.py new file mode 100644 index 00000000..b2c2654a --- /dev/null +++ b/src/hope_dedup_engine/apps/api/admin/ignored_pair.py @@ -0,0 +1,25 @@ +from django.contrib import admin + +from adminfilters.autocomplete import AutoCompleteFilter +from adminfilters.mixin import AdminFiltersMixin + +from hope_dedup_engine.apps.api.models import ( + IgnoredFilenamePair, + IgnoredReferencePkPair, +) + + +class IgnoredPairBaseAdmin(AdminFiltersMixin, admin.ModelAdmin): + list_display = ("id", "first", "second", "deduplication_set") + list_filter = (("deduplication_set", AutoCompleteFilter),) + search_fields = ("first", "second") + + +@admin.register(IgnoredReferencePkPair) +class IgnoredReferencePkPairAdmin(IgnoredPairBaseAdmin): + pass + + +@admin.register(IgnoredFilenamePair) +class IgnoredFilenamePairAdmin(IgnoredPairBaseAdmin): + pass diff --git a/src/hope_dedup_engine/apps/api/config_settings_schema.json b/src/hope_dedup_engine/apps/api/config_settings_schema.json deleted file mode 100644 index ac56a0fe..00000000 --- a/src/hope_dedup_engine/apps/api/config_settings_schema.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "type": "object", - "properties": { - "detection": { - "type": "object", - "properties": { - "confidence": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1, - "default": "constance.config.FACE_DETECTION_CONFIDENCE" - } - }, - "default": {}, - "required": [ - "confidence" - ] - }, - "recognition": { - "type": "object", - "properties": { - "num_jitters": { - "type": "integer", - "minimum": 1, - "default": "constance.config.FACE_ENCODINGS_NUM_JITTERS" - }, - "model": { - "type": "string", - "enum": [ - "small", - "large" - ], - "default": "constance.config.FACE_ENCODINGS_MODEL" - }, - "preprocessors": { - "type": "array", - "items": { - "type": "string", - "enum": [] - }, - "uniquItems": true, - "default": [] - } - }, - "default": {}, - "required": [ - "num_jitters", - "model" - ] - }, - "duplicates": { - "type": "object", - "properties": { - "tolerance": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1, - "default": "constance.config.FACE_DISTANCE_THRESHOLD" - } - }, - "default": {}, - "required": [ - "tolerance" - ] - } - }, - "required": [ - "detection", - "recognition", - "duplicates" - ] -} \ No newline at end of file diff --git a/src/hope_dedup_engine/apps/api/deduplication/adapters.py b/src/hope_dedup_engine/apps/api/deduplication/adapters.py index 5fc83bd4..82ce2bd5 100644 --- a/src/hope_dedup_engine/apps/api/deduplication/adapters.py +++ b/src/hope_dedup_engine/apps/api/deduplication/adapters.py @@ -1,11 +1,7 @@ from collections.abc import Callable, Generator -from hope_dedup_engine.apps.api.deduplication.config import ConfigDefaults from hope_dedup_engine.apps.api.deduplication.registry import DuplicateKeyPair from hope_dedup_engine.apps.api.models import DeduplicationSet -from hope_dedup_engine.apps.faces.services.duplication_detector import ( - DuplicationDetector, -) class DuplicateFaceFinder: @@ -18,22 +14,35 @@ def __init__(self, deduplication_set: DeduplicationSet): def run( self, tracker: Callable[[int], None] | None = None ) -> Generator[DuplicateKeyPair, None, None]: - filename_to_reference_pk = { - filename: reference_pk - for reference_pk, filename in self.deduplication_set.image_set.values_list( - "reference_pk", "filename" - ) - } - cfg = ConfigDefaults() - if self.deduplication_set.config: - cfg.apply_config_overrides(self.deduplication_set.config.settings) - # ignored key pairs are not handled correctly in DuplicationDetector - detector = DuplicationDetector( - tuple[str](filename_to_reference_pk.keys()), cfg=cfg - ) - for first_filename, second_filename, distance in detector.find_duplicates( - tracker - ): - yield filename_to_reference_pk[first_filename], filename_to_reference_pk[ - second_filename - ], 1 - distance + ... + # filename_to_reference_pk = { + # filename: reference_pk + # for reference_pk, filename in self.deduplication_set.image_set.values_list( + # "reference_pk", "filename" + # ) + # } + # options = { + # "detector_backend": config.FACE_DETECTOR_MODEL, + # "model_name": config.FACIAL_RECOGNITION_MODEL, + # } + # # options = ConfigDefaults() + # # if self.deduplication_set.config: + # # options.apply_config_overrides(self.deduplication_set.config.settings) + # # ignored key pairs are not handled correctly in DuplicationDetector + # detector = FacialDetector( + # self.deduplication_set.pk, + # tuple[str](filename_to_reference_pk.keys()), + # options=options, + # ) + # for first_filename, second_filename, distance in detector.find_duplicates( + # # tracker + # ): + # yield ( + # filename_to_reference_pk[first_filename], + # ( + # filename_to_reference_pk[second_filename] + # if second_filename in filename_to_reference_pk + # else second_filename + # ), + # distance if is_facial_error(distance) else (1 - distance), + # ) diff --git a/src/hope_dedup_engine/apps/api/deduplication/config.py b/src/hope_dedup_engine/apps/api/deduplication/config.py index a92c8c17..46b25bf5 100644 --- a/src/hope_dedup_engine/apps/api/deduplication/config.py +++ b/src/hope_dedup_engine/apps/api/deduplication/config.py @@ -1,69 +1,63 @@ from dataclasses import dataclass, field -from typing import Any, Literal +from typing import Any, Self +from uuid import UUID from constance import config as constance_cfg +from hope_dedup_engine.apps.api.models import DeduplicationSet + @dataclass -class DetectionConfig: - dnn_files_source: str = field( - default_factory=lambda: constance_cfg.DNN_FILES_SOURCE - ) - dnn_backend: int = field(default_factory=lambda: constance_cfg.DNN_BACKEND) - dnn_target: int = field(default_factory=lambda: constance_cfg.DNN_TARGET) - blob_from_image_scale_factor: float = field( - default_factory=lambda: constance_cfg.BLOB_FROM_IMAGE_SCALE_FACTOR +class ModelOptions: + model_name: str = field( + default_factory=lambda: constance_cfg.FACE_RECOGNITION_MODEL ) - blob_from_image_mean_values: tuple[float, float, float] = field( - default_factory=lambda: tuple( - map(float, constance_cfg.BLOB_FROM_IMAGE_MEAN_VALUES.split(", ")) - ) + detector_backend: str = field( + default_factory=lambda: constance_cfg.FACE_DETECTOR_BACKEND ) - confidence: float = field( - default_factory=lambda: constance_cfg.FACE_DETECTION_CONFIDENCE - ) - nms_threshold: float = field(default_factory=lambda: constance_cfg.NMS_THRESHOLD) + + def update(self, overrides: dict[str, Any]) -> None: + for k, v in overrides.items(): + if hasattr(self, k): + setattr(self, k, v) @dataclass -class RecognitionConfig: - num_jitters: int = field( - default_factory=lambda: constance_cfg.FACE_ENCODINGS_NUM_JITTERS - ) - model: Literal["small", "large"] = field( - default_factory=lambda: constance_cfg.FACE_ENCODINGS_MODEL - ) - preprocessors: list[str] = field(default_factory=list) +class EncodingOptions(ModelOptions): + pass @dataclass -class DuplicatesConfig: - tolerance: float = field( +class DeduplicateOptions(ModelOptions): + threshold: float = field( default_factory=lambda: constance_cfg.FACE_DISTANCE_THRESHOLD ) + silent: bool = True @dataclass -class ConfigDefaults: - detection: DetectionConfig = field(default_factory=DetectionConfig) - recognition: RecognitionConfig = field(default_factory=RecognitionConfig) - duplicates: DuplicatesConfig = field(default_factory=DuplicatesConfig) - - def apply_config_overrides( - self, config_settings: dict[str, Any] | None = None - ) -> None: - """ - Updates the instance with values from the provided config settings. - - Parameters: - config_settings (dict | None): Optional dictionary of configuration overrides, structured to match - sections in ConfigDefaults (e.g., "detection", "recognition", "duplicates"). Only matching attributes - are updated. No changes are made if `config_settings` is `None` or empty. - """ - if config_settings: - for section_name, section_data in config_settings.items(): - dataclass_section = getattr(self, section_name, None) - if dataclass_section and isinstance(section_data, dict): - for k, v in section_data.items(): - if hasattr(dataclass_section, k): - setattr(dataclass_section, k, v) +class DeduplicationSetConfig: + deduplication_set_id: UUID | None = None + encoding: EncodingOptions = field(default_factory=EncodingOptions) + deduplicate: DeduplicateOptions = field(default_factory=DeduplicateOptions) + + def update(self, overrides: dict[str, Any]) -> None: + if not isinstance(overrides, dict): + raise ValueError("Overrides values must be a dictionary.") + for k, v in overrides.items(): + match k: + case "encoding" if isinstance(v, dict): + self.encoding.update(v) + case "deduplicate" if isinstance(v, dict): + self.deduplicate.update(v) + case _ if hasattr(self, k): + setattr(self, k, v) + case _: + raise KeyError(f"Unknown config key: {k}") + + @classmethod + def from_deduplication_set(cls, deduplication_set: DeduplicationSet) -> Self: + instance = cls(deduplication_set_id=deduplication_set.pk) + if deduplication_set.config: + instance.update(deduplication_set.config.settings) + return instance diff --git a/src/hope_dedup_engine/apps/api/deduplication/process.py b/src/hope_dedup_engine/apps/api/deduplication/process.py index 7142b181..ec54758c 100644 --- a/src/hope_dedup_engine/apps/api/deduplication/process.py +++ b/src/hope_dedup_engine/apps/api/deduplication/process.py @@ -1,66 +1,72 @@ -from collections.abc import Callable -from functools import partial +from dataclasses import asdict -from celery import shared_task +from django.db.models import F -from hope_dedup_engine.apps.api.deduplication.registry import ( - DuplicateFinder, - DuplicateKeyPair, - get_finders, -) -from hope_dedup_engine.apps.api.models import DedupJob, DeduplicationSet, Duplicate +from celery import chord, shared_task + +from hope_dedup_engine.apps.api.deduplication.config import DeduplicationSetConfig + +# from hope_dedup_engine.apps.api.deduplication.registry import ( # DuplicateFinder,; DuplicateKeyPair, +# get_finders, +# ) +from hope_dedup_engine.apps.api.models import DedupJob, DeduplicationSet, Finding from hope_dedup_engine.apps.api.utils.notification import send_notification -from hope_dedup_engine.apps.api.utils.progress import track_progress_multi - - -def _sort_keys(pair: DuplicateKeyPair) -> DuplicateKeyPair: - first, second, score = pair - return *sorted((first, second)), score - - -def _save_duplicates( - finder: DuplicateFinder, - deduplication_set: DeduplicationSet, - tracker: Callable[[int], None], -) -> None: - reference_pk_to_filename_mapping = dict( - deduplication_set.image_set.values_list("reference_pk", "filename") - ) - ignored_filename_pairs = frozenset( - map( - tuple, - map( - sorted, - deduplication_set.ignoredfilenamepair_set.values_list( - "first", "second" - ), - ), - ) - ) - - ignored_reference_pk_pairs = frozenset( - deduplication_set.ignoredreferencepkpair_set.values_list("first", "second") - ) - - for first, second, score in map(_sort_keys, finder.run(tracker)): - first_filename, second_filename = sorted( - ( - reference_pk_to_filename_mapping[first], - reference_pk_to_filename_mapping[second], - ) - ) - ignored = (first, second) in ignored_reference_pk_pairs or ( - first_filename, - second_filename, - ) in ignored_filename_pairs - if not ignored: - duplicate, _ = Duplicate.objects.get_or_create( - deduplication_set=deduplication_set, - first_reference_pk=first, - second_reference_pk=second, - ) - duplicate.score += score * finder.weight - duplicate.save() + +# from hope_dedup_engine.apps.api.utils.progress import track_progress_multi +from hope_dedup_engine.apps.faces.celery_tasks import ( + callback_encodings, + encode_chunk, + get_chunks, +) + +# def _sort_keys(pair: DuplicateKeyPair) -> DuplicateKeyPair: +# first, second, score = pair +# return *sorted((first, second)), score + + +# def _save_duplicates( +# finder: DuplicateFinder, +# deduplication_set: DeduplicationSet, +# tracker: Callable[[int], None], +# ) -> None: +# reference_pk_to_filename_mapping = dict( +# deduplication_set.image_set.values_list("reference_pk", "filename") +# ) +# ignored_filename_pairs = frozenset( +# map( +# tuple, +# map( +# sorted, +# deduplication_set.ignoredfilenamepair_set.values_list( +# "first", "second" +# ), +# ), +# ) +# ) + +# ignored_reference_pk_pairs = frozenset( +# deduplication_set.ignoredreferencepkpair_set.values_list("first", "second") +# ) + +# for first, second, score in map(_sort_keys, finder.run(tracker)): +# first_filename, second_filename = sorted( +# ( +# reference_pk_to_filename_mapping[first], +# reference_pk_to_filename_mapping[second], +# ) +# ) +# ignored = (first, second) in ignored_reference_pk_pairs or ( +# first_filename, +# second_filename, +# ) in ignored_filename_pairs +# if not ignored: +# duplicate, _ = Duplicate.objects.get_or_create( +# deduplication_set=deduplication_set, +# first_reference_pk=first, +# second_reference_pk=second, +# ) +# duplicate.score += score * finder.weight +# duplicate.save() HOUR = 60 * 60 @@ -68,7 +74,7 @@ def _save_duplicates( def update_job_progress(job: DedupJob, progress: int) -> None: job.progress = progress - job.save() + job.save(update_fields=["progress"]) @shared_task(soft_time_limit=0.5 * HOUR, time_limit=1 * HOUR) @@ -78,26 +84,51 @@ def find_duplicates(dedup_job_id: int, version: int) -> None: deduplication_set = dedup_job.deduplication_set deduplication_set.state = DeduplicationSet.State.DIRTY - deduplication_set.save() + deduplication_set.save(update_fields=["state"]) send_notification(deduplication_set.notification_url) - # clean results - Duplicate.objects.filter(deduplication_set=deduplication_set).delete() - - weight_total = 0 - for finder, tracker in zip( - get_finders(deduplication_set), - track_progress_multi(partial(update_job_progress, dedup_job)), - ): - _save_duplicates(finder, deduplication_set, tracker) - weight_total += finder.weight + config = asdict( + DeduplicationSetConfig.from_deduplication_set(deduplication_set) + ) - for duplicate in deduplication_set.duplicate_set.all(): - duplicate.score /= weight_total - duplicate.save() + # clean results + Finding.objects.filter(deduplication_set=deduplication_set).delete() + dedup_job.progress = 0 + dedup_job.save(update_fields=["progress"]) + + # weight_total = 0 + # for finder, tracker in zip( + # for finder, _ in zip( + # get_finders(deduplication_set), + # track_progress_multi(partial(update_job_progress, dedup_job)), + # ): + # # _save_duplicates(finder, deduplication_set, tracker) + # weight_total += finder.weight + + weight_total = 1 + deduplication_set.finding_set.update(score=F("score") / weight_total) + + files = deduplication_set.image_set.values_list("filename", flat=True) + chunks = get_chunks(files) + tasks = [encode_chunk.s(chunk, config) for chunk in chunks] + chord_id = chord(tasks)(callback_encodings.s(config=config)) + + # for finder, tracker in zip( + # get_finders(deduplication_set), + # track_progress_multi(partial(update_job_progress, dedup_job)), + # ): + # for first, second, score in finder.run(tracker): + # finding = (first, second, score * finder.weight) + # deduplication_set.update_findings(finding) deduplication_set.state = deduplication_set.State.CLEAN - deduplication_set.save() + deduplication_set.save(update_fields=["state"]) + + return { + "deduplication_set": str(deduplication_set), + "chord_id": str(chord_id), + "chunks": len(chunks), + } finally: send_notification(dedup_job.deduplication_set.notification_url) diff --git a/src/hope_dedup_engine/apps/api/migrations/0015_deduplicationset_encodings_finding_delete_duplicate.py b/src/hope_dedup_engine/apps/api/migrations/0015_deduplicationset_encodings_finding_delete_duplicate.py new file mode 100644 index 00000000..9cd59f83 --- /dev/null +++ b/src/hope_dedup_engine/apps/api/migrations/0015_deduplicationset_encodings_finding_delete_duplicate.py @@ -0,0 +1,61 @@ +# Generated by Django 5.1.4 on 2024-12-31 05:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0014_update_deduplication_set_status"), + ] + + operations = [ + migrations.AddField( + model_name="deduplicationset", + name="encodings", + field=models.JSONField(blank=True, default=dict, null=True), + ), + migrations.CreateModel( + name="Finding", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "first_reference_pk", + models.CharField(max_length=100, verbose_name="First reference"), + ), + ( + "second_reference_pk", + models.CharField(max_length=100, verbose_name="Second reference"), + ), + ( + "score", + models.FloatField(default=0, verbose_name="Similarity Score"), + ), + ("error", models.IntegerField(blank=True, null=True)), + ( + "deduplication_set", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="api.deduplicationset", + ), + ), + ], + options={ + "unique_together": { + ("deduplication_set", "first_reference_pk", "second_reference_pk") + }, + }, + ), + migrations.DeleteModel( + name="Duplicate", + ), + ] diff --git a/src/hope_dedup_engine/apps/api/models/__init__.py b/src/hope_dedup_engine/apps/api/models/__init__.py index 8e4c4722..f3519d1a 100644 --- a/src/hope_dedup_engine/apps/api/models/__init__.py +++ b/src/hope_dedup_engine/apps/api/models/__init__.py @@ -1,8 +1,11 @@ from hope_dedup_engine.apps.api.models.auth import HDEToken # noqa: F401 from hope_dedup_engine.apps.api.models.config import Config # noqa: F401 -from hope_dedup_engine.apps.api.models.deduplication import ( # noqa: F401 +from hope_dedup_engine.apps.api.models.deduplication import ( # noqa: F401; Duplicate, DeduplicationSet, - Duplicate, + Finding, + IgnoredFilenamePair, + IgnoredPair, + IgnoredReferencePkPair, Image, ) from hope_dedup_engine.apps.api.models.jobs import DedupJob # noqa: F401 diff --git a/src/hope_dedup_engine/apps/api/models/config.py b/src/hope_dedup_engine/apps/api/models/config.py index cbb019ae..8d4d6be6 100644 --- a/src/hope_dedup_engine/apps/api/models/config.py +++ b/src/hope_dedup_engine/apps/api/models/config.py @@ -1,9 +1,5 @@ -from django.core.exceptions import ValidationError from django.db import models -from hope_dedup_engine.apps.api.utils.shema_manager import SchemaManager -from hope_dedup_engine.apps.api.validators import DefaultValidatingValidator - class Config(models.Model): name = models.CharField( @@ -12,11 +8,4 @@ class Config(models.Model): settings = models.JSONField(default=dict, null=True, blank=True) def __str__(self) -> str: - return f"{self.name}" if self.name else f"ID: {self.pk}" - - def clean(self) -> None: - try: - schema = SchemaManager.get_or_create() - DefaultValidatingValidator(schema).validate(self.settings) - except Exception as e: - raise ValidationError({"settings": e.message}) + return self.name or f"ID: {self.pk}" diff --git a/src/hope_dedup_engine/apps/api/models/deduplication.py b/src/hope_dedup_engine/apps/api/models/deduplication.py index 8f092db3..31649854 100644 --- a/src/hope_dedup_engine/apps/api/models/deduplication.py +++ b/src/hope_dedup_engine/apps/api/models/deduplication.py @@ -5,6 +5,7 @@ from django.db import models from hope_dedup_engine.apps.security.models import ExternalSystem +from hope_dedup_engine.types import EncodingType, FindingType, IgnoredPairType REFERENCE_PK_LENGTH: Final[int] = 100 @@ -53,8 +54,46 @@ class State(models.IntegerChoices): notification_url = models.CharField(max_length=255, null=True, blank=True) config = models.ForeignKey("Config", null=True, on_delete=models.SET_NULL) + encodings = models.JSONField( + null=True, blank=True, default=dict + ) # {file1: encoding1, file2: encoding2, ...} + def __str__(self) -> str: - return f"ID: {self.pk}" if not self.name else f"{self.name}" + return self.name or f"ID: {self.pk}" + + def get_encodings(self) -> EncodingType: + return self.encodings + + def get_findings(self) -> FindingType: + return list( + self.finding_set.values_list( + "first_reference_pk", "second_reference_pk", "score" + ) + ) + + def get_ignored_pairs(self) -> IgnoredPairType: + return list( + self.ignoredreferencepkpair_set.values_list("first", "second") + ) + list(self.ignoredfilenamepair_set.values_list("first", "second")) + + def update_encodings(self, encodings: EncodingType) -> None: + self.encodings.update(encodings) + self.save() + + def update_findings(self, findings: FindingType) -> None: + Finding.objects.bulk_create( + [ + Finding( + deduplication_set=self, + first_reference_pk=f[0], + second_reference_pk=f[1], + score=f[2], + error=f[3], + ) + for f in findings + ], + ignore_conflicts=True, + ) class Image(models.Model): @@ -76,15 +115,33 @@ class Image(models.Model): created_at = models.DateTimeField(auto_now_add=True) -class Duplicate(models.Model): +class Finding(models.Model): """ - Couple of similar entities + Couple of finding entities """ + # class ErrorCode(models.IntegerChoices): + # GENERIC_ERROR = 999 + # NO_FACE_DETECTED = 998 + # MULTIPLE_FACES_DETECTED = 997 + # NO_FILE_FOUND = 996 + deduplication_set = models.ForeignKey(DeduplicationSet, on_delete=models.CASCADE) - first_reference_pk = models.CharField(max_length=REFERENCE_PK_LENGTH) # from hope - second_reference_pk = models.CharField(max_length=REFERENCE_PK_LENGTH) # from hope - score = models.FloatField(default=0) + first_reference_pk = models.CharField( + max_length=REFERENCE_PK_LENGTH, verbose_name="First reference" + ) + second_reference_pk = models.CharField( + max_length=REFERENCE_PK_LENGTH, verbose_name="Second reference" + ) + score = models.FloatField(default=0, validators=[], verbose_name="Similarity Score") + error = models.IntegerField(null=True, blank=True) + + class Meta: + unique_together = ( + "deduplication_set", + "first_reference_pk", + "second_reference_pk", + ) class IgnoredPair(models.Model): diff --git a/src/hope_dedup_engine/apps/api/serializers.py b/src/hope_dedup_engine/apps/api/serializers.py index 629e91a1..04d1cce8 100644 --- a/src/hope_dedup_engine/apps/api/serializers.py +++ b/src/hope_dedup_engine/apps/api/serializers.py @@ -1,17 +1,15 @@ from typing import Any -from jsonschema import Draft202012Validator -from jsonschema import ValidationError as JSONSchemaValidationError from rest_framework import serializers -from hope_dedup_engine.apps.api.models import Config, DeduplicationSet -from hope_dedup_engine.apps.api.models.deduplication import ( - Duplicate, +from hope_dedup_engine.apps.api.models import ( + Config, + DeduplicationSet, + Finding, IgnoredFilenamePair, IgnoredReferencePkPair, Image, ) -from hope_dedup_engine.apps.api.utils.shema_manager import SchemaManager class ConfigSerializer(serializers.ModelSerializer): @@ -19,14 +17,6 @@ class Meta: model = Config exclude = ("id",) - def validate_settings(self, value): - validator = Draft202012Validator(SchemaManager.get_or_create()) - try: - validator.validate(value) - except JSONSchemaValidationError as e: - raise serializers.ValidationError(f"Settings validation error: {e.message}") - return value - class DeduplicationSetSerializer(serializers.ModelSerializer): state = serializers.CharField(source="get_state_display", read_only=True) @@ -34,7 +24,7 @@ class DeduplicationSetSerializer(serializers.ModelSerializer): class Meta: model = DeduplicationSet - exclude = ("deleted",) + exclude = ("deleted", "encodings") read_only_fields = ( "external_system", "created_at", @@ -86,7 +76,7 @@ def __init__(self, prefix: str, *args: Any, **kwargs: Any) -> None: self._prefix = prefix super().__init__(*args, **kwargs) - def get_reference_pk(self, duplicate: Duplicate) -> int: + def get_reference_pk(self, duplicate: Finding) -> int: return getattr(duplicate, f"{self._prefix}_reference_pk") @@ -95,8 +85,8 @@ class DuplicateSerializer(serializers.ModelSerializer): second = EntrySerializer(prefix="second", source="*") class Meta: - model = Duplicate - fields = "first", "second", "score" + model = Finding + fields = "first", "second", "score", "error" CREATE_PAIR_FIELDS = "first", "second" diff --git a/src/hope_dedup_engine/apps/api/utils/shema_manager.py b/src/hope_dedup_engine/apps/api/utils/shema_manager.py index eeb7f938..1bbba61f 100644 --- a/src/hope_dedup_engine/apps/api/utils/shema_manager.py +++ b/src/hope_dedup_engine/apps/api/utils/shema_manager.py @@ -1,15 +1,3 @@ -import json -import logging -from pathlib import Path - -from django.conf import settings -from django.core.exceptions import ValidationError - -from jsonschema import Draft202012Validator, exceptions - -logger = logging.getLogger(__name__) # pragma: no cover - - class SchemaManager: # pragma: no cover """ Manages loading, validating, and saving the JSON schema file. @@ -22,50 +10,51 @@ class SchemaManager: # pragma: no cover save(schema: dict) -> None: """ - schema_path = Path(settings.CONFIG_SETTINGS_SCHEMA_FILE) - - @classmethod - def get_or_create(cls) -> dict: - """ - Attempts to load and validate the schema from the JSON schema file. - - Returns: - dict: The loaded JSON schema as a dictionary. If the file is not found, returns - an empty dictionary as a fallback. - - Raises: - ValidationError: If the schema file exists but contains invalid JSON - or fails JSON Schema validation. - """ - try: - schema = json.loads(cls.schema_path.read_text()) - Draft202012Validator.check_schema(schema) - return schema - except FileNotFoundError: - logger.warning("Schema file not found.") - return {} - except (json.JSONDecodeError, exceptions.SchemaError) as e: - logger.error(f"Failed to load schema: {e}") - raise ValidationError("Failed to load the schema file.") from e - - @classmethod - def save(cls, schema: dict) -> None: - """ - Validates and writes the provided schema dictionary to the schema file. - - Args: - schema (dict): The JSON schema to be saved, provided as a dictionary. - - Raises: - ValidationError: If the schema does not meet JSON Schema standards or if - there is an IOError when writing to the file. - """ - try: - Draft202012Validator.check_schema(schema) - cls.schema_path.write_text(json.dumps(schema, indent=4)) - logger.info(f"Schema saved to {cls.schema_path}") - except exceptions.SchemaError as e: - raise ValidationError("Invalid schema format.") from e - except IOError as e: - logger.error(f"Failed to save schema: {e}") - raise ValidationError("Failed to save the schema file.") from e + ... + # schema_path = Path(settings.CONFIG_SETTINGS_SCHEMA_FILE) + + # @classmethod + # def get_or_create(cls) -> dict: + # """ + # Attempts to load and validate the schema from the JSON schema file. + + # Returns: + # dict: The loaded JSON schema as a dictionary. If the file is not found, returns + # an empty dictionary as a fallback. + + # Raises: + # ValidationError: If the schema file exists but contains invalid JSON + # or fails JSON Schema validation. + # """ + # try: + # schema = json.loads(cls.schema_path.read_text()) + # Draft202012Validator.check_schema(schema) + # return schema + # except FileNotFoundError: + # logger.warning("Schema file not found.") + # return {} + # except (json.JSONDecodeError, exceptions.SchemaError) as e: + # logger.error(f"Failed to load schema: {e}") + # raise ValidationError("Failed to load the schema file.") from e + + # @classmethod + # def save(cls, schema: dict) -> None: + # """ + # Validates and writes the provided schema dictionary to the schema file. + + # Args: + # schema (dict): The JSON schema to be saved, provided as a dictionary. + + # Raises: + # ValidationError: If the schema does not meet JSON Schema standards or if + # there is an IOError when writing to the file. + # """ + # try: + # Draft202012Validator.check_schema(schema) + # cls.schema_path.write_text(json.dumps(schema, indent=4)) + # logger.info(f"Schema saved to {cls.schema_path}") + # except exceptions.SchemaError as e: + # raise ValidationError("Invalid schema format.") from e + # except IOError as e: + # logger.error(f"Failed to save schema: {e}") + # raise ValidationError("Failed to save the schema file.") from e diff --git a/src/hope_dedup_engine/apps/api/views.py b/src/hope_dedup_engine/apps/api/views.py index 0667deea..8fea7211 100644 --- a/src/hope_dedup_engine/apps/api/views.py +++ b/src/hope_dedup_engine/apps/api/views.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from http import HTTPMethod -from typing import Any +from typing import Any, Generic, TypeVar from uuid import UUID from django.db.models import Q, QuerySet @@ -24,9 +24,9 @@ DEDUPLICATION_SET_FILTER, DEDUPLICATION_SET_PARAM, ) -from hope_dedup_engine.apps.api.models import DeduplicationSet -from hope_dedup_engine.apps.api.models.deduplication import ( - Duplicate, +from hope_dedup_engine.apps.api.models import ( + DeduplicationSet, + Finding, IgnoredFilenamePair, IgnoredReferencePkPair, Image, @@ -45,6 +45,8 @@ ) from hope_dedup_engine.apps.api.utils.process import delete_model_data, start_processing +T = TypeVar("T") + class DeduplicationSetViewSet( mixins.RetrieveModelMixin, @@ -236,7 +238,7 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: class DuplicateViewSet( - nested_viewsets.NestedViewSetMixin[Duplicate], + nested_viewsets.NestedViewSetMixin[Finding], mixins.ListModelMixin, viewsets.GenericViewSet, ): @@ -247,12 +249,13 @@ class DuplicateViewSet( UserAndDeduplicationSetAreOfTheSameSystem, ) serializer_class = DuplicateSerializer - queryset = Duplicate.objects.all() + # TODO: Add filters + queryset = Finding.objects.all() parent_lookup_kwargs = { DEDUPLICATION_SET_PARAM: DEDUPLICATION_SET_FILTER, } - def get_queryset(self) -> QuerySet[Duplicate]: + def get_queryset(self) -> QuerySet[Finding]: queryset = super().get_queryset() if reference_pk := self.request.query_params.get(REFERENCE_PK): return queryset.filter( @@ -275,11 +278,12 @@ def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: return super().list(request, *args, **kwargs) -class IgnoredPairViewSet[T]( +class IgnoredPairViewSet( nested_viewsets.NestedViewSetMixin[T], mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet, + Generic[T], ): authentication_classes = (HDETokenAuthentication,) permission_classes = ( diff --git a/src/hope_dedup_engine/apps/core/checks.py b/src/hope_dedup_engine/apps/core/checks.py index a73348f8..7206b871 100644 --- a/src/hope_dedup_engine/apps/core/checks.py +++ b/src/hope_dedup_engine/apps/core/checks.py @@ -55,8 +55,7 @@ def example_check(app_configs, **kwargs: Any): # pragma: no cover @register(deploy=True) def storages_check(app_configs: Any, **kwargs: Any) -> list[Error]: # pragma: no cover """ - Checks if the necessary environment variables for Azure storage are configured - and verifies the presence of required files in the specified Azure storage containers. + Checks if the necessary environment variables for Azure storage are configured. Args: app_configs: Not used, but required by the checks framework. @@ -67,7 +66,6 @@ def storages_check(app_configs: Any, **kwargs: Any) -> list[Error]: # pragma: n missing files, or errors while accessing Azure storage containers. """ storages = ( - "FILE_STORAGE_DNN", "FILE_STORAGE_HOPE", "FILE_STORAGE_STATIC", "FILE_STORAGE_MEDIA", @@ -94,23 +92,6 @@ def storages_check(app_configs: Any, **kwargs: Any) -> list[Error]: # pragma: n try: storage = AzureStorage(**options) storage.client.exists() - if storage_name == "FILE_STORAGE_DNN": - _, files = storage.listdir() - for _, info in settings.DNN_FILES.items(): - filename = info.get("filename") - if filename not in files: - errors.append( - Error( - StorageErrorCodes.FILE_NOT_FOUND.message.format( - filename=filename, storage_name=storage_name - ), - hint=StorageErrorCodes.FILE_NOT_FOUND.hint.format( - filename=filename - ), - obj=filename, - id=StorageErrorCodes.FILE_NOT_FOUND.id, - ) - ) except Exception: errors.append( Error( diff --git a/src/hope_dedup_engine/apps/core/exceptions.py b/src/hope_dedup_engine/apps/core/exceptions.py index a6bafdfb..454f9439 100644 --- a/src/hope_dedup_engine/apps/core/exceptions.py +++ b/src/hope_dedup_engine/apps/core/exceptions.py @@ -1,13 +1,3 @@ -class StorageKeyError(Exception): - """ - Exception raised when the storage key does not exist. - """ - - def __init__(self, key: str) -> None: - self.key = key - super().__init__(f"Storage key '{key}' does not exist.") - - class DownloaderKeyError(Exception): """ Exception raised when the downloader key does not exist. @@ -16,12 +6,3 @@ class DownloaderKeyError(Exception): def __init__(self, key: str) -> None: self.key = key super().__init__(f"Downloader key '{key}' does not exist.") - - -class NotCompliantImageError(Exception): - """ - Exception raised when an image is not compliant with the expected parameters. - """ - - def __init__(self, message: str) -> None: - super().__init__(message) diff --git a/src/hope_dedup_engine/apps/core/management/commands/demo.py b/src/hope_dedup_engine/apps/core/management/commands/demo.py index a72a9954..f8526725 100644 --- a/src/hope_dedup_engine/apps/core/management/commands/demo.py +++ b/src/hope_dedup_engine/apps/core/management/commands/demo.py @@ -19,7 +19,6 @@ Path(__file__).resolve().parents[6] / "tests" / "extras" / "demoapp" ) DEFAULT_DEMO_IMAGES: Final[Path] = BASE_PATH / env("DEMO_IMAGES_PATH") -DEFAULT_DNN_FILES: Final[Path] = BASE_PATH / env("DNN_FILES_PATH") MESSAGES: Final[dict[str, str]] = { "upload": "Starting upload of files...", @@ -56,12 +55,6 @@ def add_arguments(self, parser: ArgumentParser) -> None: default=str(DEFAULT_DEMO_IMAGES), help="Path to the demo images directory", ) - parser.add_argument( - "--dnn-files", - type=str, - default=str(DEFAULT_DNN_FILES), - help="Path to the DNN files directory", - ) def handle(self, *args: Any, **options: dict[str, Any]) -> None: """ @@ -80,7 +73,6 @@ def handle(self, *args: Any, **options: dict[str, Any]) -> None: """ storages = ( Storage(name="hope", src=Path(options["demo_images"])), - Storage(name="dnn", src=Path(options["dnn_files"])), Storage(name="media"), Storage(name="staticfiles", options={"public_access": "blob"}), ) diff --git a/src/hope_dedup_engine/apps/core/management/commands/dnnsetup.py b/src/hope_dedup_engine/apps/core/management/commands/dnnsetup.py deleted file mode 100644 index 8c28bf38..00000000 --- a/src/hope_dedup_engine/apps/core/management/commands/dnnsetup.py +++ /dev/null @@ -1,166 +0,0 @@ -import logging -import sys -from argparse import ArgumentParser -from typing import Any, Final - -from django.conf import settings -from django.core.exceptions import ValidationError -from django.core.management import BaseCommand -from django.core.management.base import CommandError, SystemCheckError - -import requests -from storages.backends.azure_storage import AzureStorage - -logger = logging.getLogger(__name__) - - -MESSAGES: Final[dict[str, str]] = { - "start": "Starting DNN setup...", - "already": "File '%s' already exists in 'FILE_STORAGE_DNN' storage.", - "process": "Downloading file from '%s' to '%s' in 'FILE_STORAGE_DNN' storage...", - "empty": "File at '%s' is empty (size is 0 bytes).", - "completed": "DNN setup completed successfully.", - "halted": "\n\n***\nSYSTEM HALTED\nUnable to start without DNN files...", -} - - -class Command(BaseCommand): - help = "Synchronizes DNN files from the git to azure storage" - dnn_files = None - - def add_arguments(self, parser: ArgumentParser) -> None: - """ - Adds custom command-line arguments to the management command. - - Args: - parser (ArgumentParser): The argument parser instance to which the arguments should be added. - - Adds the following arguments: - --force: A boolean flag that, when provided, forces the re-download of files even if they already exist - in Azure storage. Defaults to False. - --deployfile-url (str): The URL from which the deploy (prototxt) file is downloaded. - Defaults to the value set in the project settings. - --caffemodelfile-url (str): The URL from which the pre-trained model weights (caffemodel) are downloaded. - Defaults to the value set in the project settings. - --download-timeout (int): The maximum time allowed for downloading files, in seconds. - Defaults to 3 minutes (180 seconds). - --chunk-size (int): The size of each chunk to download in bytes. Defaults to 256 KB. - """ - parser.add_argument( - "--force", - action="store_true", - default=False, - help="Force the re-download of files even if they already exist", - ) - parser.add_argument( - "--deployfile-url", - type=str, - default=settings.DNN_FILES.get("prototxt", {}) - .get("sources", {}) - .get("github"), - help="The URL of the model architecture (deploy) file", - ) - parser.add_argument( - "--caffemodelfile-url", - type=str, - default=settings.DNN_FILES.get("caffemodel", {}) - .get("sources", {}) - .get("github"), - help="The URL of the pre-trained model weights (caffemodel) file", - ) - parser.add_argument( - "--download-timeout", - type=int, - default=3 * 60, # 3 minutes - help="The timeout for downloading files", - ) - parser.add_argument( - "--chunk-size", - type=int, - default=256 * 1024, # 256 KB - help="The size of each chunk to download in bytes", - ) - - def get_options(self, options: dict[str, Any]) -> None: - self.verbosity = options["verbosity"] - self.force = options["force"] - self.dnn_files = ( - { - "url": options["deployfile_url"], - "filename": settings.DNN_FILES.get("prototxt", {}) - .get("sources", {}) - .get("azure"), - }, - { - "url": options["caffemodelfile_url"], - "filename": settings.DNN_FILES.get("caffemodel", {}) - .get("sources", {}) - .get("azure"), - }, - ) - self.download_timeout = options["download_timeout"] - self.chunk_size = options["chunk_size"] - - def handle(self, *args: Any, **options: Any) -> None: - """ - Executes the command to download and store DNN files from a given source to Azure Blob Storage. - - Args: - *args (Any): Positional arguments passed to the command. - **options (dict[str, Any]): Keyword arguments passed to the command, including: - - force (bool): If True, forces the re-download of files even if they already exist in storage. - - deployfile_url (str): The URL of the DNN model architecture file to download. - - caffemodelfile_url (str): The URL of the pre-trained model weights to download. - - download_timeout (int): Timeout for downloading each file, in seconds. - - chunk_size (int): The size of chunks for streaming downloads, in bytes. - - Raises: - FileNotFoundError: If the downloaded file is empty (size is 0 bytes). - ValidationError: If any arguments are invalid or improperly configured. - CommandError: If an issue occurs with the Django command execution. - SystemCheckError: If a system check error is encountered during execution. - Exception: For any other errors that occur during the download or storage process. - """ - self.get_options(options) - if self.verbosity >= 1: - echo = self.stdout.write - else: - echo = lambda *a, **kw: None # noqa: E731 - echo(self.style.WARNING(MESSAGES["start"])) - - try: - dnn_storage = AzureStorage(**settings.STORAGES.get("dnn").get("OPTIONS")) - _, files = dnn_storage.listdir("") - for file in self.dnn_files: - if self.force or not file.get("filename") in files: - echo(MESSAGES["process"] % (file.get("url"), file.get("filename"))) - with requests.get( - file.get("url"), stream=True, timeout=self.download_timeout - ) as r: - r.raise_for_status() - if int(r.headers.get("Content-Length", 1)) == 0: - raise FileNotFoundError(MESSAGES["empty"] % file.get("url")) - with dnn_storage.open(file.get("filename"), "wb") as f: - for chunk in r.iter_content(chunk_size=self.chunk_size): - f.write(chunk) - else: - echo(MESSAGES["already"] % file.get("filename")) - echo(self.style.SUCCESS(MESSAGES["completed"])) - except ValidationError as e: - self.halt(Exception("\n- ".join(["Wrong argument(s):", *e.messages]))) - except (CommandError, FileNotFoundError, SystemCheckError) as e: - self.halt(e) - except Exception as e: - self.halt(e) - - def halt(self, e: Exception) -> None: - """ - Handle an exception by logging the error and exiting the program. - - Args: - e (Exception): The exception that occurred. - """ - logger.exception(e) - self.stdout.write(self.style.ERROR(str(e))) - self.stdout.write(self.style.ERROR(MESSAGES["halted"])) - sys.exit(1) diff --git a/src/hope_dedup_engine/apps/core/management/commands/upgrade.py b/src/hope_dedup_engine/apps/core/management/commands/upgrade.py index c997605d..2260ac65 100644 --- a/src/hope_dedup_engine/apps/core/management/commands/upgrade.py +++ b/src/hope_dedup_engine/apps/core/management/commands/upgrade.py @@ -26,7 +26,6 @@ def add_arguments(self, parser: "ArgumentParser") -> None: "--with-check", action="store_true", dest="check", - default=True, help="Run checks", ) parser.add_argument( @@ -65,11 +64,11 @@ def add_arguments(self, parser: "ArgumentParser") -> None: help="Do not run collectstatic", ) parser.add_argument( - "--with-dnn-setup", - action="store_true", - dest="dnn_setup", - default=False, - help="Run DNN setup for celery worker", + "--no-sync-models", + action="store_false", + dest="sync_models", + default=True, + help="Do not sync pre-trained models", ) parser.add_argument( "--admin-email", @@ -92,7 +91,7 @@ def get_options(self, options: dict[str, Any]) -> None: self.prompt = not options["prompt"] self.static = options["static"] self.migrate = options["migrate"] - self.dnn_setup = options["dnn_setup"] + self.sync_models = options["sync_models"] self.debug = options["debug"] self.admin_email = str(options["admin_email"] or env("ADMIN_EMAIL", "")) @@ -131,9 +130,11 @@ def handle(self, *args: Any, **options: Any) -> None: # noqa: C901 if self.run_check: call_command("check", deploy=True, verbosity=self.verbosity - 1) - if self.dnn_setup: - echo("Run DNN setup for celery worker.") - call_command("dnnsetup", verbosity=self.verbosity - 1) + + if self.sync_models: + echo("Run sync pre-trained models") + call_command("syncmodels", verbosity=self.verbosity - 1) + if self.static: static_root = Path(env("STATIC_ROOT")) echo( diff --git a/src/hope_dedup_engine/apps/faces/admin.py b/src/hope_dedup_engine/apps/faces/admin.py index efa34eaf..acfe5f08 100644 --- a/src/hope_dedup_engine/apps/faces/admin.py +++ b/src/hope_dedup_engine/apps/faces/admin.py @@ -3,14 +3,13 @@ from admin_extra_buttons.decorators import button from admin_extra_buttons.mixins import ExtraButtonsMixin from celery import group -from constance import config from hope_dedup_engine.apps.faces.celery_tasks import sync_dnn_files from hope_dedup_engine.apps.faces.models import DummyModel from hope_dedup_engine.config.celery import app as celery_app -@admin.register(DummyModel) +# @admin.register(DummyModel) class DummyModelAdmin(ExtraButtonsMixin, admin.ModelAdmin): change_list_template = "admin/faces/dummymodel/change_list.html" @@ -30,7 +29,7 @@ def has_delete_permission(self, request, obj=None): def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} extra_context["title"] = ( - f"Force syncronize DNN files from {config.DNN_FILES_SOURCE} to local storage." + "Force syncronize model files from github to local volume." ) return super().changelist_view(request, extra_context=extra_context) @@ -51,14 +50,14 @@ def sync_dnn_files(self, request): request, f"The DNN files synchronization group task `{result.id}` has been initiated across " f"`{worker_count}` workers. " - f"The files will be forcibly synchronized with `{config.DNN_FILES_SOURCE}`.", + f"The files will be forcibly synchronized with azure.", ) else: task = sync_dnn_files.delay(force=True) self.message_user( request, f"The DNN files sync task `{task.id}` has started. " - f"The files will be forcibly synchronized with `{config.DNN_FILES_SOURCE}`.", + f"The files will be forcibly synchronized with azure.", ) return None diff --git a/src/hope_dedup_engine/apps/faces/celery_tasks.py b/src/hope_dedup_engine/apps/faces/celery_tasks.py index 26b34755..bc244aa9 100644 --- a/src/hope_dedup_engine/apps/faces/celery_tasks.py +++ b/src/hope_dedup_engine/apps/faces/celery_tasks.py @@ -1,43 +1,149 @@ import traceback +from collections import ChainMap +from functools import partial +from typing import Any, Final from django.conf import settings +from django.db.models import F -from celery import Task, shared_task, states -from constance import config +from celery import Task, chord, shared_task, signals, states +from celery.canvas import Signature +from celery.utils.imports import qualname +from hope_dedup_engine.apps.api.models import DedupJob, DeduplicationSet from hope_dedup_engine.apps.faces.managers import FileSyncManager -from hope_dedup_engine.apps.faces.services import DuplicationDetector -from hope_dedup_engine.apps.faces.utils.celery_utils import task_lifecycle +from hope_dedup_engine.apps.faces.services.facial import dedupe_images, encode_faces +from hope_dedup_engine.config.celery import DedupeTask, app +from hope_dedup_engine.types import EncodingType, FindingType +CHUNK_SIZE: Final[int] = 25 -@shared_task(bind=True, soft_time_limit=0.5 * 60 * 60, time_limit=1 * 60 * 60) -@task_lifecycle(name="Deduplicate", ttl=1 * 60 * 60) -# TODO: Use DeduplicationSet objects as input to deduplication pipeline -def deduplicate( - self: Task, - filenames: tuple[str], - ignore_pairs: tuple[tuple[str, str], ...] = tuple(), -) -> list[list[str]]: - """ - Deduplicate a set of filenames, ignoring any specified pairs of filenames. - Args: - filenames (tuple[str]): A tuple of filenames to process. - ignore_pairs (tuple[tuple[str, str]]): A tuple of tuples, where each inner tuple contains - a pair of filenames to be ignored in the duplication check. +def get_chunks(files: list[str]) -> list[list[str]]: + chunk_size = min(CHUNK_SIZE, len(files)) + return [ + files[i : i + chunk_size] for i in range(0, len(files), chunk_size) # noqa 203 + ] - Returns: - list[list[str]]: A list of lists, where each inner list represents a group of duplicates. - """ + +def notify_status(task: Task, dedup_job_id: int, **kwargs): + signals.task_prerun.send( + sender=task, + task_id=task.request.id, + dedup_job_id=dedup_job_id, + ) + + +def shadow_name(task, args, kwargs, options): try: - dd = DuplicationDetector(filenames, ignore_pairs) - return list(dd.find_duplicates()) + s: Signature = options["chord"] + group: str = options["group_id"].split("-")[-1] + chunk = int(options["group_index"]) + name = f"{qualname(s.type)}({group})-{chunk:03}" + return name except Exception as e: - self.update_state( - state=states.FAILURE, - meta={"exc_message": str(e), "traceback": traceback.format_exc()}, - ) - raise e + return str(e) + + +@signals.task_prerun.connect +def handle_task_progress(sender=None, task_id=None, dedup_job_id=None, **kwargs): + if not dedup_job_id: + return + dedup_job = DedupJob.objects.filter(pk=dedup_job_id).first() + if dedup_job: + dedup_job.progress = F("progress") + 1 + dedup_job.save(update_fields=["progress"]) + + +@app.task(bind=True, base=DedupeTask, shadow_name=shadow_name) +def encode_chunk( + self: DedupeTask, + files: list[str], + config: dict[str, Any], +) -> tuple[EncodingType, int, int]: + """Encode faces in a chunk of files.""" + ds = DeduplicationSet.objects.get(pk=config.get("deduplication_set_id")) + callback = partial(notify_status, task=self, dedup_job_id=ds.dedupjob.pk) + pre_encodings = ds.get_encodings() + return encode_faces(files, config.get("encoding"), pre_encodings, progress=callback) + + +@app.task(bind=True, base=DedupeTask) +def dedupe_chunk( + self: Task, + files: list[str], + config: dict[str, Any], +) -> FindingType: + """Deduplicate faces in a chunk of files.""" + ds = DeduplicationSet.objects.get(pk=config.get("deduplication_set_id")) + callback = partial(notify_status, task=self, dedup_job_id=ds.dedupjob.pk) + encoded = ds.get_encodings() + ignored_pairs = set(ds.get_ignored_pairs()) + return dedupe_images( + files, + encoded, + ignored_pairs, + dedupe_threshold=config.get("deduplicate", {}).get("threshold"), + options=config.get("deduplicate"), + progress=callback, + ) + + +@app.task(bind=True, base=DedupeTask) +def callback_findings( + self: Task, + results: FindingType, + config: dict[str, Any], +) -> dict[str, Any]: + """Aggregate and save findings.""" + ds = DeduplicationSet.objects.get(pk=config.get("deduplication_set_id")) + seen_pairs = set() + findings = [ + record + for d in results + for record in d + if not (pair := tuple(sorted(record[:2]))) in seen_pairs + and not seen_pairs.add(pair) + ] + ds.update_findings(findings) + return { + "Files": len(ds.image_set.all()), + "Config": config.get("deduplicate"), + "Findings": len(findings), + } + + +@app.task(bind=True, base=DedupeTask) +def callback_encodings( + self: Task, + results: tuple[EncodingType, int, int], + config: dict[str, Any], +) -> dict[str, Any]: + """Aggregate and save encodings.""" + ds = DeduplicationSet.objects.get(pk=config.get("deduplication_set_id")) + encodings = dict(ChainMap(*[result[0] for result in results])) + ds.update_encodings(encodings) + deduplicate_dataset.delay(config) + return { + "Encoded": len(encodings), + } + + +@app.task(bind=True, base=DedupeTask) +def deduplicate_dataset( + self: Task, + config: dict[str, Any], +) -> dict[str, Any]: + """Deduplicate the dataset.""" + ds = DeduplicationSet.objects.get(pk=config.get("deduplication_set_id")) + chunks = get_chunks(list(ds.get_encodings().keys())) + tasks = [dedupe_chunk.s(chunk, config) for chunk in chunks] + chord_id = chord(tasks)(callback_findings.s(config=config)) + return { + "deduplication_set": str(ds), + "chord_id": str(chord_id), + "chunks": len(chunks), + } @shared_task(bind=True) @@ -58,12 +164,14 @@ def sync_dnn_files(self: Task, force: bool = False) -> bool: """ try: - downloader = FileSyncManager(config.DNN_FILES_SOURCE).downloader + # downloader = FileSyncManager(config.DNN_FILES_SOURCE).downloader + downloader = FileSyncManager("azure").downloader return all( ( downloader.sync( info.get("filename"), - info.get("sources").get(config.DNN_FILES_SOURCE), + # info.get("sources").get(config.DNN_FILES_SOURCE), + info.get("sources").get("azure"), force=force, ) ) diff --git a/src/hope_dedup_engine/apps/faces/management/commands/syncdnn.py b/src/hope_dedup_engine/apps/faces/management/commands/syncdnn.py deleted file mode 100644 index 5780db44..00000000 --- a/src/hope_dedup_engine/apps/faces/management/commands/syncdnn.py +++ /dev/null @@ -1,124 +0,0 @@ -import logging -import sys -from typing import Any, Final - -from django.conf import settings -from django.core.management import BaseCommand -from django.core.management.base import CommandError, SystemCheckError - -from constance import config - -from hope_dedup_engine.apps.faces.managers.file_sync import FileSyncManager - -logger = logging.getLogger(__name__) - - -MESSAGES: Final[dict[str, str]] = { - "sync": "Starting synchronization of DNN files from %s ...", - "success": "Finished synchronizing DNN files successfully.", - "failed": "Failed to synchronize DNN files.", - "halted": "\n\n***\nSYSTEM HALTED\nUnable to start without DNN files...", -} - - -class Command(BaseCommand): - help = "Synchronizes DNN files from the specified source to local storage" - - def add_arguments(self, parser): - """ - Adds custom command-line arguments to the management command. - - Args: - parser (argparse.ArgumentParser): The argument parser instance to which the arguments should be added. - - Adds the following arguments: - --force: A boolean flag that, when provided, forces the re-download of files even if they - already exist locally. Defaults to False. - --source (str): Specifies the source from which to download the DNN files. The available choices - are dynamically retrieved from the CONSTANCE_ADDITIONAL_FIELDS configuration. - Defaults to the value of config.DNN_FILES_SOURCE. - """ - parser.add_argument( - "--force", - action="store_true", - default=False, - help="Force the re-download of files even if they already exist locally", - ) - parser.add_argument( - "--source", - type=str, - default=config.DNN_FILES_SOURCE, - choices=tuple( - ch[0] - for ch in settings.CONSTANCE_ADDITIONAL_FIELDS.get("dnn_files_source")[ - 1 - ].get("choices") - ), - help="The source from which to download the DNN files", - ) - - def handle(self, *args: Any, **options: dict[str, Any]) -> None: - """ - Executes the command to synchronize DNN files from a specified source to local storage. - - Args: - *args (Any): Positional arguments passed to the command. - **options (dict[str, Any]): Keyword arguments passed to the command, including: - - force (bool): If True, forces the re-download of files even if they already exist locally. - - source (str): The source from which to download the DNN files. - - Raises: - CommandError: If there is a problem executing the command. - SystemCheckError: If there is a system check error. - Exception: For any other errors that occur during execution. - """ - - def on_progress(filename: str, percent: int, is_complete: bool = False) -> None: - """ - Callback function to report the progress of a file download. - - Args: - filename (str): The name of the file being downloaded. - percent (int): The current download progress as a percentage (0-100). - is_complete (bool): If True, indicates that the download is complete. Defaults to False. - - Returns: - None - """ - self.stdout.write(f"\rDownloading file {filename}: {percent}%", ending="") - if is_complete: - self.stdout.write("\n") - - self.stdout.write(self.style.WARNING(MESSAGES["sync"]) % options.get("source")) - logger.info(MESSAGES["sync"]) - - try: - downloader = FileSyncManager(options.get("source")).downloader - for _, info in settings.DNN_FILES.items(): - downloader.sync( - info.get("filename"), - info.get("sources").get(options.get("source")), - force=options.get("force"), - on_progress=on_progress, - ) - on_progress(info.get("filename"), 100, is_complete=True) - except (CommandError, SystemCheckError) as e: - self.halt(e) - except Exception as e: - self.stdout.write(self.style.ERROR(MESSAGES["failed"])) - logger.error(MESSAGES["failed"]) - self.halt(e) - - self.stdout.write(self.style.SUCCESS(MESSAGES["success"])) - - def halt(self, e: Exception) -> None: - """ - Handle an exception by logging the error and exiting the program. - - Args: - e (Exception): The exception that occurred. - """ - logger.exception(e) - self.stdout.write(self.style.ERROR(str(e))) - self.stdout.write(self.style.ERROR(MESSAGES["halted"])) - sys.exit(1) diff --git a/src/hope_dedup_engine/apps/faces/management/commands/syncmodels.py b/src/hope_dedup_engine/apps/faces/management/commands/syncmodels.py new file mode 100644 index 00000000..eb268cee --- /dev/null +++ b/src/hope_dedup_engine/apps/faces/management/commands/syncmodels.py @@ -0,0 +1,80 @@ +import logging +import sys +from typing import Any, Final + +from django.conf import settings +from django.core.management import BaseCommand +from django.core.management.base import CommandError, SystemCheckError + +from hope_dedup_engine.apps.faces.managers.file_sync import FileSyncManager + +logger = logging.getLogger(__name__) + + +MESSAGES: Final[dict[str, str]] = { + "sync": "Starting synchronization of models pre-trained-weights files from github to '%s'.", + "success": "Finished synchronizing models pre-trained-weights files successfully.", + "failed": "Failed to synchronize models pre-trained-weights files.", + "halted": "\n\n***\nSYSTEM HALTED\nUnable to start without models pre-trained-weights files...", + "progress": "\rDownloading file '%s': %s", +} + + +class Command(BaseCommand): + help = "Synchronizes models pre-trained-weights files from the specified source to local storage" + + def add_arguments(self, parser): + parser.add_argument( + "--force", + action="store_true", + default=False, + help="Force the re-download of files even if they already exist locally", + ) + + def handle(self, *args: Any, **options: dict[str, Any]) -> None: + + def on_progress(filename: str, percent: int, is_complete: bool = False) -> None: + self.stdout.write(MESSAGES["progress"] % (filename, percent), ending="") + if is_complete: + self.stdout.write("\n") + + self.stdout.write( + self.style.WARNING( + MESSAGES["sync"] % settings.DEEPFACE_WEIGHTS_BASE_LOCATION + ) + ) + logger.info(MESSAGES["sync"] % settings.DEEPFACE_WEIGHTS_BASE_LOCATION) + + try: + downloader = FileSyncManager( + source="github", + local_base_location=settings.DEEPFACE_WEIGHTS_BASE_LOCATION, + ).downloader + for filename, url in settings.DEEPFACE_WEIGHTS.items(): + result = downloader.sync( + filename, + url, + force=options.get("force"), + on_progress=on_progress, + ) + on_progress(filename, result, is_complete=True) + except (CommandError, SystemCheckError) as e: + self.halt(e) + except Exception as e: + self.stdout.write(self.style.ERROR(MESSAGES["failed"])) + logger.error(MESSAGES["failed"]) + self.halt(e) + + self.stdout.write(self.style.SUCCESS(MESSAGES["success"])) + + def halt(self, e: Exception) -> None: + """ + Handle an exception by logging the error and exiting the program. + + Args: + e (Exception): The exception that occurred. + """ + logger.exception(e) + self.stdout.write(self.style.ERROR(str(e))) + self.stdout.write(self.style.ERROR(MESSAGES["halted"])) + sys.exit(1) diff --git a/src/hope_dedup_engine/apps/faces/managers/__init__.py b/src/hope_dedup_engine/apps/faces/managers/__init__.py index b11c4b03..db35fb77 100644 --- a/src/hope_dedup_engine/apps/faces/managers/__init__.py +++ b/src/hope_dedup_engine/apps/faces/managers/__init__.py @@ -1,3 +1,2 @@ from .file_sync import FileSyncManager # noqa: F401 -from .net import DNNInferenceManager # noqa: F401 -from .storage import StorageManager # noqa: F401 +from .storage import ImagesStorageManager # noqa: F401 diff --git a/src/hope_dedup_engine/apps/faces/managers/file_sync.py b/src/hope_dedup_engine/apps/faces/managers/file_sync.py index c19df1c4..b8b02672 100644 --- a/src/hope_dedup_engine/apps/faces/managers/file_sync.py +++ b/src/hope_dedup_engine/apps/faces/managers/file_sync.py @@ -1,10 +1,11 @@ from pathlib import Path -from typing import Callable +from typing import Callable, Final from django.conf import settings from django.core.files.storage import FileSystemStorage import requests +from filelock import FileLock, Timeout from storages.backends.azure_storage import AzureStorage from hope_dedup_engine.apps.core.exceptions import DownloaderKeyError @@ -15,63 +16,118 @@ class FileDownloader: Base class for downloading files from different sources. """ - def __init__(self) -> None: - """ - Initializes the FileDownloader with a local storage backend. - """ + MESSAGES: Final[dict[str, str]] = { + "not_implemented": "This method should be overridden by subclasses.", + "file_exists": "File already exists locally.", + "downloading": "Skipping download, another process is downloading the file.", + "done": "Done.", + } + + def __init__(self, local_base_location: Path) -> None: + """Initializes the FileDownloader with a local storage backend.""" self.local_storage = FileSystemStorage( - **settings.STORAGES.get("default").get("OPTIONS") + **settings.STORAGES.get("default").get("OPTIONS"), ) + self.local_storage.base_location = local_base_location def sync( self, filename: str, - source: str, + file_source: str, force: bool = False, on_progress: Callable[[str, int], None] = None, - *args, **kwargs, - ) -> bool: + ) -> str: """ - Synchronize a file from the specified source to the local storage. + Synchronize a file with lock handling. + + This method ensures that a file is downloaded to the local storage with proper handling of concurrent access + using file-based locks. If the file already exists locally or is currently being downloaded by another process, + it will skip the download and return an appropriate message. The download process is thread- and process-safe. Args: filename (str): The name of the file to be synchronized. - source (str): The source from which the file should be downloaded. - force (bool): If True, the file will be re-downloaded even if it already exists locally. - on_progress (Callable[[str, int], None], optional): A callback function to report the download - progress. The function should accept two arguments: the filename and the download progress - as a percentage. Defaults to None. - *args: Additional positional arguments for extended functionality in subclasses. - **kwargs: Additional keyword arguments for extended functionality in subclasses. + file_source (str): The source of the file (e.g., a URL or a blob name). + force (bool): If True, forces the re-download of the file even if it already exists locally. + on_progress (Callable[[str, int], None], optional): A callback function to track the download progress. + The callback should accept the filename and the progress percentage as arguments. Defaults to None. + **kwargs: Additional arguments passed to `_execute_download`. Returns: - bool: True if the file was successfully synchronized or already exists locally, - False otherwise. - - Raises: - NotImplementedError: This method should be overridden by subclasses to provide - specific synchronization logic. + str: A message indicating the outcome of the synchronization. """ - raise NotImplementedError("This method should be overridden by subclasses.") + local_filepath = Path(self.local_storage.path(filename)) + lock = FileLock(f"{local_filepath}.lock") + + if skip_message := self._should_skip_download(local_filepath, lock, force): + return skip_message - def _prepare_local_filepath(self, filename: str, force: bool) -> Path | None: + local_filepath.parent.mkdir(parents=True, exist_ok=True) + + try: + with lock: + return self._execute_download( + local_filepath, file_source, on_progress=on_progress, **kwargs + ) + except Timeout: + return self.MESSAGES.get("downloading") + finally: + self._cleanup_lock(lock) + + def _should_skip_download( + self, local_filepath: Path, lock: FileLock, force: bool + ) -> bool: + """Determine if the download should be skipped.""" + if force: + return None + + if local_filepath.exists(): + if Path(lock.lock_file).exists(): + try: + with lock.acquire(timeout=0): + pass + except Timeout: + return self.MESSAGES.get("downloading") + return self.MESSAGES.get("file_exists") + + return None + + def _execute_download( + self, + local_filepath: str, + source: str, + on_progress: Callable[[str, int], None] = None, + *args, + **kwargs, + ) -> str: """ - Prepares the local file path for the file to be downloaded. + Synchronize a file from the specified source to the local storage. Args: - filename (str): The name of the file. - force (bool): If True, the file will be re-downloaded even if it exists locally. + local_filepath (str): The local path where the file will be saved. + source (str): The source of the file, e.g., a URL, blob name, or other identifier. + force (bool): Whether to force the download even if the file already exists locally. Defaults to False. + on_progress (Callable[[str, int], None], optional): A callback function for reporting download progress. + The callback receives the filename and download progress as a percentage. + *args: Additional positional arguments for extended functionality in subclasses. + **kwargs: Additional keyword arguments for extended functionality in subclasses. Returns: - Path | None: The local file path if the file should be downloaded, - None if the file exists and `force` is False. + str: A message indicating the status of the operation, typically "Done." if implemented. + + Raises: + NotImplementedError: This method must be implemented in a subclass. """ - local_filepath = Path(self.local_storage.path(filename)) - if not force and local_filepath.exists(): - return None - local_filepath.parent.mkdir(parents=True, exist_ok=True) - return local_filepath + raise NotImplementedError(self.MESSAGES.get("not_implemented")) + + def _cleanup_lock(self, lock: FileLock) -> None: + """Clean up the lock file if it exists.""" + lock_path = Path(lock.lock_file) + if lock_path.exists(): + try: + lock_path.unlink() + except FileNotFoundError: + pass def _report_progress( self, @@ -104,54 +160,57 @@ class GithubFileDownloader(FileDownloader): Inherits from FileDownloader and implements the sync method to download files from a given GitHub URL. """ - def sync( + MESSAGES: Final[dict[str, str]] = { + **FileDownloader.MESSAGES, + "empty_file": "File '%s' at '%s' is empty (size is 0 bytes).", + } + + def __init__(self, local_base_location: Path) -> None: + super().__init__(local_base_location) + + def _execute_download( self, - filename: str, + local_filepath: str, url: str, - force: bool = False, on_progress: Callable[[str, int], None] = None, - timeout: int = 3 * 60, - chunk_size: int = 128 * 1024, - ) -> bool: + timeout: int = 3 * 60, # 3 minutes + chunk_size: int = 128 * 1024, # 128 KB + ) -> str: """ - Downloads a file from the specified URL and saves it to local storage. + Downloads a file from a specified URL and saves it to local storage. Args: - filename (str): The name of the file to be downloaded. - url (str): The URL of the file to download. - force (bool): If True, the file will be re-downloaded even if it exists locally. Defaults to False. - on_progress (Callable[[str, int], None], optional): A callback function that reports the download progress - as a percentage. Defaults to None. - timeout (int): The timeout for the download request in seconds. Defaults to 3 minutes. + local_filepath (str): The local path where the file will be saved. + url (str): The URL of the file to be downloaded. + on_progress (Callable[[str, int], None], optional): A callback function for reporting download progress. + The callback receives the filename and download progress as a percentage. Defaults to None. + timeout (int): The timeout for the download request in seconds. Defaults to 180 seconds (3 minutes). chunk_size (int): The size of each chunk to download in bytes. Defaults to 128 KB. Returns: - bool: True if the file was downloaded successfully or already exists, False otherwise. + str: A message indicating the status of the download. Typically "Done." if successful. Raises: - requests.exceptions.HTTPError: If the HTTP request returned an unsuccessful status code. - FileNotFoundError: If the file at the specified URL is empty (size is 0 bytes). + requests.exceptions.HTTPError: If the HTTP request fails with a non-successful status code. + FileNotFoundError: If the file is empty (size 0 bytes) or the URL is inaccessible. """ - local_filepath = self._prepare_local_filepath(filename, force) - if local_filepath is None: - return True - with requests.get(url, stream=True, timeout=timeout) as r: r.raise_for_status() total, downloaded = int(r.headers.get("Content-Length", 1)), 0 if total == 0: raise FileNotFoundError( - f"File {filename} at {url} is empty (size is 0 bytes)." + self.MESSAGES.get("empty_file") % (local_filepath.name, url) ) with local_filepath.open("wb") as f: for chunk in r.iter_content(chunk_size=chunk_size): f.write(chunk) downloaded += len(chunk) - self._report_progress(filename, downloaded, total, on_progress) - - return True + self._report_progress( + local_filepath.name, downloaded, total, on_progress + ) + return self.MESSAGES.get("done") class AzureFileDownloader(FileDownloader): @@ -161,73 +220,76 @@ class AzureFileDownloader(FileDownloader): Inherits from FileDownloader and implements the sync method to download files from a given Azure Blob Storage. """ - def __init__(self) -> None: + MESSAGES: Final[dict[str, str]] = { + **FileDownloader.MESSAGES, + "does_not_exist": "File '%s' does not exist in remote storage.", + "empty_file": "File '%s' is empty (size is 0 bytes).", + } + + def __init__(self, local_base_location: Path) -> None: """ Initializes the AzureFileDownloader with a remote storage backend. """ - super().__init__() + super().__init__(local_base_location) self.remote_storage = AzureStorage( **settings.STORAGES.get("dnn").get("OPTIONS") ) - def sync( + def _execute_download( self, - filename: str, + local_filepath: str, blob_name: str, - force: bool = False, on_progress: Callable[[str, int], None] = None, chunk_size: int = 128 * 1024, - ) -> bool: + ) -> str: """ Downloads a file from Azure Blob Storage and saves it to local storage. Args: - filename (str): The name of the file to be saved locally. - blob_name (str): The name of the blob in Azure Blob Storage. - force (bool): If True, the file will be re-downloaded even if it exists locally. Defaults to False. - on_progress (Callable[[str, int], None], optional): A callback function that reports the download progress - as a percentage. Defaults to None. + local_filepath (str): The local path where the file will be saved. + blob_name (str): The name of the blob to be downloaded from Azure Blob Storage. + on_progress (Callable[[str, int], None], optional): A callback function for reporting download progress. + The callback receives the filename and download progress as a percentage. chunk_size (int): The size of each chunk to download in bytes. Defaults to 128 KB. Returns: - bool: True if the file was downloaded successfully or already exists, False otherwise. + str: A message indicating the status of the download. Typically "Done." if successful. Raises: - FileNotFoundError: If the specified blob does not exist in Azure Blob Storage - or if the blob has a size of 0 bytes. + FileNotFoundError: If the specified blob does not exist or is empty (size 0 bytes). """ - local_filepath = self._prepare_local_filepath(filename, force) - if local_filepath is None: - return True - _, files = self.remote_storage.listdir("") if blob_name not in files: - raise FileNotFoundError( - f"File {blob_name} does not exist in remote storage" - ) + raise FileNotFoundError(self.MESSAGES.get("does_not_exist") % blob_name) blob_size, downloaded = self.remote_storage.size(blob_name) or 1, 0 if blob_size == 0: - raise FileNotFoundError(f"File {blob_name} is empty (size is 0 bytes).") + raise FileNotFoundError(self.MESSAGES.get("empty_file") % blob_name) with self.remote_storage.open(blob_name, "rb") as remote_file: with local_filepath.open("wb") as local_file: for chunk in remote_file.chunks(chunk_size=chunk_size): local_file.write(chunk) downloaded += len(chunk) - self._report_progress(filename, downloaded, blob_size, on_progress) + self._report_progress( + local_filepath.name, downloaded, blob_size, on_progress + ) - return True + return self.MESSAGES.get("done") class FileSyncManager: - def __init__(self, source: str) -> None: + def __init__(self, *, source: str, local_base_location: Path | None = None) -> None: """ Initialize the FileSyncManager with the specified source. Args: source (str): The source for downloading files. + local_base_location (Path): The base location for storing files locally. """ + if local_base_location is None: + local_base_location = Path(settings.DEFAULT_ROOT) + self.local_base_location = local_base_location self.downloader = self._create_downloader(source) def _create_downloader(self, source: str) -> FileDownloader: @@ -248,6 +310,6 @@ def _create_downloader(self, source: str) -> FileDownloader: "azure": AzureFileDownloader, } try: - return downloader_classes[source]() + return downloader_classes[source](self.local_base_location) except KeyError: raise DownloaderKeyError(source) diff --git a/src/hope_dedup_engine/apps/faces/managers/net.py b/src/hope_dedup_engine/apps/faces/managers/net.py deleted file mode 100644 index 3664d941..00000000 --- a/src/hope_dedup_engine/apps/faces/managers/net.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.conf import settings -from django.core.files.storage import FileSystemStorage - -from constance import config -from cv2 import dnn, dnn_Net - - -class DNNInferenceManager: - """ - A class to manage the loading and configuration of a neural network model using OpenCV's DNN module. - - The DNNInferenceManager class provides functionality to load a neural network model from Caffe files stored in a - specified storage and configure the model with preferred backend and target settings. - """ - - def __init__(self, storage: FileSystemStorage) -> None: - """ - Loads and configures the neural network model using the specified storage. - - Args: - storage (FileSystemStorage): The storage object from which to load the neural network model. - """ - self.net = dnn.readNetFromCaffe( - storage.path(settings.DNN_FILES.get("prototxt").get("filename")), - storage.path(settings.DNN_FILES.get("caffemodel").get("filename")), - ) - self.net.setPreferableBackend(int(config.DNN_BACKEND)) - self.net.setPreferableTarget(int(config.DNN_TARGET)) - - def get_model(self) -> dnn_Net: - """ - Get the loaded and configured neural network model. - - Returns: - cv2.dnn_Net: The neural network model loaded and configured by this manager. - """ - return self.net diff --git a/src/hope_dedup_engine/apps/faces/managers/storage.py b/src/hope_dedup_engine/apps/faces/managers/storage.py index b1d6fba4..d524ee43 100644 --- a/src/hope_dedup_engine/apps/faces/managers/storage.py +++ b/src/hope_dedup_engine/apps/faces/managers/storage.py @@ -1,51 +1,27 @@ +from fnmatch import fnmatch +from typing import Final + from django.conf import settings -from django.core.files.storage import FileSystemStorage +import cv2 +import numpy as np from storages.backends.azure_storage import AzureStorage -from hope_dedup_engine.apps.core.exceptions import StorageKeyError - +FILES_PATTERN: Final[tuple[str]] = ("*.png", "*.jpg", "*.jpeg") -class StorageManager: - """ - A class to manage different types of storage systems used in the application. - """ +class ImagesStorageManager: def __init__(self) -> None: - """ - Initialize the StorageManager. - - Raises: - FileNotFoundError: If any of the required DNN model files do not exist in the storage. - """ - self.storages: dict[str, AzureStorage | FileSystemStorage] = { - "cv2": FileSystemStorage(**settings.STORAGES.get("default").get("OPTIONS")), - "encoded": FileSystemStorage( - **settings.STORAGES.get("default").get("OPTIONS") - ), - "images": AzureStorage(**settings.STORAGES.get("hope").get("OPTIONS")), - } - - for file in ( - settings.DNN_FILES.get("prototxt").get("filename"), - settings.DNN_FILES.get("caffemodel").get("filename"), - ): - if not self.storages.get("cv2").exists(file): - raise FileNotFoundError(f"File {file} does not exist in storage.") - - def get_storage(self, key: str) -> AzureStorage | FileSystemStorage: - """ - Get the storage object for the given key. - - Args: - key (str): The key associated with the desired storage backend. - - Returns: - AzureStorage | FileSystemStorage: The storage object associated with the given key. - - Raises: - StorageKeyError: If the given key does not exist in the storages dictionary. - """ - if key not in self.storages: - raise StorageKeyError(key) - return self.storages[key] + self.storage: AzureStorage = AzureStorage( + **settings.STORAGES.get("hope").get("OPTIONS") + ) + + def get_files(self, pattern: tuple = FILES_PATTERN) -> list[str]: + _, images = self.storage.listdir("") + return [f for f in images if any(fnmatch(f, p) for p in pattern)] + + def load_image(self, file: str) -> np.ndarray: + with self.storage.open(file, "rb") as img_file: + img_array = np.frombuffer(img_file.read(), dtype=np.uint8) + img_bgr = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + return img_bgr diff --git a/src/hope_dedup_engine/apps/faces/services/__init__.py b/src/hope_dedup_engine/apps/faces/services/__init__.py index fc02e5c3..e69de29b 100644 --- a/src/hope_dedup_engine/apps/faces/services/__init__.py +++ b/src/hope_dedup_engine/apps/faces/services/__init__.py @@ -1 +0,0 @@ -from .duplication_detector import DuplicationDetector # noqa: F401 diff --git a/src/hope_dedup_engine/apps/faces/services/duplication_detector.py b/src/hope_dedup_engine/apps/faces/services/duplication_detector.py deleted file mode 100644 index 4de541d4..00000000 --- a/src/hope_dedup_engine/apps/faces/services/duplication_detector.py +++ /dev/null @@ -1,161 +0,0 @@ -import logging -import os -from collections.abc import Callable -from itertools import combinations -from typing import Any, Generator - -import face_recognition -import numpy as np - -from hope_dedup_engine.apps.api.deduplication.config import ConfigDefaults -from hope_dedup_engine.apps.faces.managers import StorageManager -from hope_dedup_engine.apps.faces.services.image_processor import ImageProcessor -from hope_dedup_engine.apps.faces.validators import IgnorePairsValidator - - -class DuplicationDetector: - """ - A class to detect and process duplicate faces in images. - """ - - logger: logging.Logger = logging.getLogger(__name__) - - def __init__( - self, - filenames: tuple[str], - cfg: ConfigDefaults, - ignore_pairs: tuple[tuple[str, str], ...] = (), - ) -> None: - """ - Initialize the DuplicationDetector with the given filenames and ignore pairs. - - Args: - filenames (tuple[str]): The filenames of the images to process. - cfg (ConfigDefaults): The configuration settings. - ignore_pairs (tuple[tuple[str, str]], optional): - The pairs of filenames to ignore. Defaults to an empty tuple. - """ - self.filenames = filenames - self.face_distance_threshold = cfg.duplicates.tolerance - self.ignore_set = IgnorePairsValidator.validate(ignore_pairs) - self.storages = StorageManager() - self.image_processor = ImageProcessor( - cfg_detection=cfg.detection, cfg_recognition=cfg.recognition - ) - - def _encodings_filename(self, filename: str) -> str: - """ - Generate the filename for the face encodings of a given image. - - Args: - filename (str): The filename of the image. - - Returns: - str: The filename for the face encodings. - """ - return f"{filename}.npy" - - def _has_encodings(self, filename: str) -> bool: - """ - Check if the face encodings for a given image exist in storage. - - Args: - filename (str): The filename of the image. - - Returns: - bool: True if the encodings exist, False otherwise. - """ - return self.storages.get_storage("encoded").exists( - self._encodings_filename(filename) - ) - - def _load_encodings_all(self) -> dict[str, list[np.ndarray[np.float32, Any]]]: - """ - Load all face encodings from storage. - - Returns: - dict[str, list[np.ndarray]]: A dictionary with filenames as keys and lists of face encodings as values. - """ - data: dict[str, list[np.ndarray[np.float32, Any]]] = {} - try: - _, files = self.storages.get_storage("encoded").listdir("") - for file in files: - filename = os.path.splitext(file)[0] - if file == self._encodings_filename(filename): - with self.storages.get_storage("encoded").open(file, "rb") as f: - data[filename] = np.load(f, allow_pickle=False) - except Exception as e: - self.logger.exception("Error loading encodings.") - raise e - return data - - def _existed_images_name(self) -> list[str]: - """ - Return filenames from `self.filenames` that exist in the image storage, ensuring they have encodings. - - Returns: - list[str]: List of existing filenames with encodings. - """ - filenames: list = [] - _, files = self.storages.get_storage("images").listdir("") - for filename in self.filenames: - if filename not in files: - self.logger.warning( - "Image %s not found in the image storage.", filename - ) - else: - filenames.append(filename) - if not self._has_encodings(filename): - self.image_processor.encode_face( - filename, self._encodings_filename(filename) - ) - return filenames - - def find_duplicates( - self, tracker: Callable[[int], None] | None = None - ) -> Generator[tuple[str, str, float], None, None]: - """ - Finds duplicate images based on facial encodings and yields pairs of image paths with their minimum distance. - - Yields: - Generator[tuple[str, str, float], None, None]: A generator yielding tuples containing: - - The first image path (str) - - The second image path (str) - - The minimum facial distance between the images, rounded to five decimal places (float) - - Raises: - Exception: If an error occurs during processing, it logs the exception and re-raises it. - """ - try: - - existed_images_name = self._existed_images_name() - encodings_all = self._load_encodings_all() - - total_pairs = (n := len(existed_images_name)) * (n - 1) // 2 - for i, (path1, path2) in enumerate(combinations(existed_images_name, 2), 1): - encodings1 = encodings_all.get(path1) - encodings2 = encodings_all.get(path2) - if encodings1 is None or encodings2 is None: - continue - - min_distance = None - for encoding1 in encodings1: - distances = face_recognition.face_distance(encodings2, encoding1) - current_min = min(distances) if np.any(distances) else 0 - if min_distance is None or current_min < min_distance: - min_distance = current_min - - if ( - min_distance is not None - and min_distance < self.face_distance_threshold - ): - yield (path1, path2, round(min_distance, 5)) - - if tracker: - tracker(100 * i // total_pairs) - - except Exception as e: - self.logger.exception( - "Error finding duplicates for images %s", self.filenames - ) - raise e diff --git a/src/hope_dedup_engine/apps/faces/services/facial.py b/src/hope_dedup_engine/apps/faces/services/facial.py new file mode 100644 index 00000000..1768bdb5 --- /dev/null +++ b/src/hope_dedup_engine/apps/faces/services/facial.py @@ -0,0 +1,102 @@ +import logging +from collections import defaultdict +from typing import Any + +from deepface import DeepFace + +from hope_dedup_engine.apps.faces.managers import ImagesStorageManager +from hope_dedup_engine.constants import FacialError, is_facial_error +from hope_dedup_engine.types import EncodingType, FindingType, IgnoredPairType + +logger = logging.getLogger(__name__) + + +def default_progress(*args): + return True + + +def encode_faces( + files: list[str], + options=None, + pre_encodings=None, + progress=None, +) -> tuple[EncodingType, int, int]: + + if not callable(progress): + progress = default_progress + + storage = ImagesStorageManager() + images = storage.get_files() + + encoded = {} + if pre_encodings: + encoded.update(pre_encodings) + added_cnt = existing_cnt = 0 + existing_cnt = 1000 + for file in files: + progress() + if file not in images: + encoded[file] = FacialError.NO_FILE_FOUND.name + continue + if file in encoded: + existing_cnt += 1 + continue + try: + result = DeepFace.represent(storage.load_image(file), **(options or {})) + if len(result) > 1: + encoded[file] = FacialError.MULTIPLE_FACES_DETECTED.name + else: + encoded[file] = result[0]["embedding"] + added_cnt += 1 + except TypeError as e: + logger.exception(e) + encoded[file] = FacialError.GENERIC_ERROR.name + except ValueError: + encoded[file] = FacialError.NO_FACE_DETECTED.name + return encoded, added_cnt, existing_cnt + + +def dedupe_images( # noqa 901 + files: list[str], + encodings: EncodingType, + ignored_pairs: IgnoredPairType, + dedupe_threshold: float, + options: dict[str, Any] = None, + progress=None, +) -> FindingType: + + if not callable(progress): + progress = default_progress + + findings = defaultdict(list) + config = options or {} + + for file1 in files: + progress() + enc1 = encodings[file1] + if is_facial_error(enc1): + findings[file1].append([enc1, FacialError[enc1].code]) + continue + for file2, enc2 in encodings.items(): + if ( + file1 == file2 + or file2 in findings + or (file1, file2) in ignored_pairs + or (file2, file1) in ignored_pairs + or is_facial_error(enc2) + or file2 in [x[0] for x in findings.get(file1, [])] + ): + continue + res = DeepFace.verify(enc1, enc2, **config) + similarity = float(1 - res["distance"]) + if similarity >= dedupe_threshold: + findings[file1].append([file2, similarity]) + + results: FindingType = [] + for img, duplicates in findings.items(): + for dup in duplicates: + if is_facial_error(dup[1]): + results.append((img, dup[0], 0, dup[1])) + else: + results.append((img, dup[0], dup[1], None)) + return results diff --git a/src/hope_dedup_engine/apps/faces/services/image_processor.py b/src/hope_dedup_engine/apps/faces/services/image_processor.py deleted file mode 100644 index b7c3caa4..00000000 --- a/src/hope_dedup_engine/apps/faces/services/image_processor.py +++ /dev/null @@ -1,205 +0,0 @@ -import logging -import re -from dataclasses import dataclass, field -from typing import Any - -from django.conf import settings -from django.core.exceptions import ValidationError - -import cv2 -import face_recognition -import numpy as np - -from hope_dedup_engine.apps.api.deduplication.config import ( - DetectionConfig, - RecognitionConfig, -) -from hope_dedup_engine.apps.core.exceptions import NotCompliantImageError -from hope_dedup_engine.apps.faces.managers import DNNInferenceManager, StorageManager - - -@dataclass(frozen=True, slots=True) -class BlobFromImageConfig: - shape: dict[str, int] = field(init=False) - scale_factor: float - mean_values: tuple[float, float, float] - prototxt_path: str - - def __post_init__(self): - object.__setattr__(self, "shape", self._get_shape()) - - def _get_shape(self) -> dict[str, int]: - pattern = r"input_shape\s*\{\s*dim:\s*(\d+)\s*dim:\s*(\d+)\s*dim:\s*(\d+)\s*dim:\s*(\d+)\s*\}" - with open(self.prototxt_path, "r") as file: - if match := re.search(pattern, file.read()): - return { - "batch_size": int(match.group(1)), - "channels": int(match.group(2)), - "height": int(match.group(3)), - "width": int(match.group(4)), - } - else: - raise ValidationError("Could not find input_shape in prototxt file.") - - -class ImageProcessor: - """ - A class to handle image processing tasks, including face detection and encoding. - - """ - - logger: logging.Logger = logging.getLogger(__name__) - - def __init__( - self, - cfg_detection: DetectionConfig, - cfg_recognition: RecognitionConfig, - ) -> None: - """ - Initialize the ImageProcessor with the required configurations. - """ - self.storages = StorageManager() - self.net = DNNInferenceManager(self.storages.get_storage("cv2")).get_model() - - self.cfg_detection = cfg_detection - self.cfg_recognition = cfg_recognition - self.blob_from_image_cfg = BlobFromImageConfig( - scale_factor=self.cfg_detection.blob_from_image_scale_factor, - mean_values=self.cfg_detection.blob_from_image_mean_values, - prototxt_path=self.storages.get_storage("cv2").path( - settings.DNN_FILES.get("prototxt").get("filename") - ), - ) - - def _get_face_detections_dnn( - self, filename: str - ) -> list[tuple[int, int, int, int]]: - """ - Detect faces in an image using the DNN model. - - Args: - filename (str): The filename of the image to process. - - Returns: - list[tuple[int, int, int, int]]: A list of tuples representing face regions in the image. - """ - face_regions: list[tuple[int, int, int, int]] = [] - try: - with self.storages.get_storage("images").open(filename, "rb") as img_file: - img_array = np.frombuffer(img_file.read(), dtype=np.uint8) - # Decode image from binary buffer to 3D numpy array (height, width, channels of BlueGreeRed color space) - image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) - (h, w) = image.shape[:2] - _h, _w = ( - self.blob_from_image_cfg.shape["height"], - self.blob_from_image_cfg.shape["width"], - ) - if h < _h or w < _w: - raise NotCompliantImageError( - f"Image {filename} too small: '{h}x{w}'. It needs to be at least '{_h}x{_w}'." - ) - - # Create a blob (4D tensor) from the image - blob = cv2.dnn.blobFromImage( - image=cv2.resize(image, dsize=(_h, _w)), - size=(_h, _w), - scalefactor=self.blob_from_image_cfg.scale_factor, - mean=self.blob_from_image_cfg.mean_values, - ) - self.net.setInput(blob) - # Forward pass to get output with shape (1, 1, N, 7), - # where N is the number of faces and 7 are the detection values: - # 1st: image index (0), 2nd: class label (0), 3rd: confidence (0-1), - # 4th-5th: x, y coordinates, 6th-7th: width, height - detections = self.net.forward() - boxes, confidences = [], [] - for i in range(detections.shape[2]): - confidence = detections[0, 0, i, 2] - # Filter out weak detections by ensuring the confidence is greater than the minimum confidence - if confidence > self.cfg_detection.confidence: - box = (detections[0, 0, i, 3:7] * np.array([w, h, w, h])).astype( - "int" - ) - boxes.append(box) - confidences.append(confidence) - if boxes: - # Apply non-maxima suppression to suppress weak, overlapping bounding boxes - indices = cv2.dnn.NMSBoxes( - bboxes=boxes, - scores=confidences, - score_threshold=self.cfg_detection.confidence, - nms_threshold=self.cfg_detection.nms_threshold, - ) - if indices is not None: - for i in indices: - face_regions.append(tuple(boxes[i])) - except Exception as e: - self.logger.exception( - "Error processing face detection for image %s", filename - ) - raise e - return face_regions - - def _preprocess_image(self, filename: str) -> np.ndarray: - """ - This function retrieves an image from the 'images' storage, reads it as an array of bytes, - and decodes it into a color image. - - The image's color space is first converted from BGR (Blue, Green, Red) to YUV. Histogram equalization is then - applied to the Y channel (luminance) to enhance the contrast of the image. - - Finally, the image is converted to RGB color space for further processing. - - Args: - filename (str): The filename of the image to preprocess. - - Returns: - np.ndarray: The preprocessed image as a NumPy array in RGB format. - """ - with self.storages.get_storage("images").open(filename, "rb") as img_file: - img_array = np.asarray(bytearray(img_file.read()), dtype=np.uint8) - image = cv2.cvtColor( - cv2.imdecode(img_array, cv2.IMREAD_COLOR), cv2.COLOR_BGR2YUV - ) - image[:, :, 0] = cv2.equalizeHist(image[:, :, 0]) - return cv2.cvtColor(image, cv2.COLOR_YUV2RGB) - - def encode_face(self, filename: str, encodings_filename: str) -> None: - """ - Encode faces detected in an image and save the encodings to storage. - - Args: - filename (str): The filename of the image to process. - encodings_filename (str): The filename to save the face encodings. - """ - try: - encodings: list[np.ndarray[np.float32, Any]] = [] - image = self._preprocess_image(filename) - face_regions = self._get_face_detections_dnn(filename) - if not face_regions: - raise NotCompliantImageError( - f"No face regions detected in image '{filename}'." - ) - else: - for region in face_regions: - if isinstance(region, (list, tuple)) and len(region) == 4: - top, right, bottom, left = region - face_encodings = face_recognition.face_encodings( - image, - [(right, bottom, left, top)], - num_jitters=self.cfg_recognition.num_jitters, - model=self.cfg_recognition.model, - ) - encodings.extend(face_encodings) - else: - self.logger.error("Invalid face region %s", region) - return - with self.storages.get_storage("encoded").open( - encodings_filename, "wb" - ) as f: - np.save(f, encodings) - except Exception as e: - self.logger.exception( - "Error processing face encodings for image %s", filename - ) - raise e diff --git a/src/hope_dedup_engine/apps/faces/utils/celery_utils.py b/src/hope_dedup_engine/apps/faces/utils/celery_utils.py deleted file mode 100644 index 72791e95..00000000 --- a/src/hope_dedup_engine/apps/faces/utils/celery_utils.py +++ /dev/null @@ -1,98 +0,0 @@ -import hashlib -import logging -from functools import wraps -from typing import Any - -from django.core.cache import cache - -from hope_dedup_engine.apps.faces.services.duplication_detector import ( - DuplicationDetector, -) - -redis_client = cache._cache.get_client() - - -def task_lifecycle(name: str, ttl: int) -> callable: - """ - Decorator to manage the lifecycle of a task with logging and distributed locking. - - Args: - name (str): The name of the task for logging purposes. - ttl (int): The time-to-live (TTL) for the distributed lock in seconds. - - Returns: - Callable: The decorated function with task lifecycle management. - """ - - def decorator(func: callable) -> callable: - @wraps(func) - def wrapper(self: DuplicationDetector, *args: Any, **kwargs: Any) -> Any: - logger = logging.getLogger(func.__module__) - logger.info(f"{name} task started") - result = None - - filenames = args[0] if args else kwargs.get("filenames") - ignore_pairs = args[1] if args else kwargs.get("ignore_pairs") - lock_name: str = f"{name}_{_get_hash(filenames, ignore_pairs)}" - if not _acquire_lock(lock_name, ttl): - logger.info( - f"Task {name} with brocker lock {lock_name} is already running." - ) - return None - - try: - result = func(self, *args, **kwargs) - except Exception as e: - logger.exception(f"{name} task failed", exc_info=e) - raise e - finally: - _release_lock(lock_name) - logger.info(f"{name} task ended") - return result - - return wrapper - - return decorator - - -def _acquire_lock(lock_name: str, ttl: int = 1 * 60 * 60) -> bool | None: - """ - Acquire a distributed lock using Redis. - - Args: - lock_name (str): The name of the lock to set. - ttl (int): The time-to-live for the lock in seconds. Default is 1 hour (3600 seconds). - - Returns: - bool | None: True if the lock was successfully acquired, None if the lock is already set. - """ - return redis_client.set(lock_name, "true", nx=True, ex=ttl) - - -def _release_lock(lock_name: str) -> None: - """ - Release a distributed lock using Redis. - - Args: - lock_name (str): The name of the lock to delete. - """ - redis_client.delete(lock_name) - - -def _get_hash(filenames: tuple[str], ignore_pairs: tuple[tuple[str, str]]) -> str: - """ - Generate a SHA-256 hash based on filenames and ignore pairs. - - Args: - filenames (tuple[str]): A tuple of filenames. - ignore_pairs (tuple[tuple[str, str]]): A tuple of pairs of filenames to ignore. - - Returns: - str: A SHA-256 hash string representing the combination of filenames and ignore pairs. - """ - fn_str: str = ",".join(sorted(filenames)) - ip_sorted = sorted( - (min(item1, item2), max(item1, item2)) for item1, item2 in ignore_pairs - ) - ip_str = ",".join(f"{item1},{item2}" for item1, item2 in ip_sorted) - return hashlib.sha256(f"{fn_str}{ip_str}".encode()).hexdigest() diff --git a/src/hope_dedup_engine/config/__init__.py b/src/hope_dedup_engine/config/__init__.py index 7cf34eef..1e24350c 100644 --- a/src/hope_dedup_engine/config/__init__.py +++ b/src/hope_dedup_engine/config/__init__.py @@ -110,6 +110,13 @@ class Group(Enum): "https://django-environ.readthedocs.io/en/latest/types.html#environ-env-db-url", ), "DEBUG": (bool, False, True, False, setting("debug")), + "DEEPFACE_HOME": ( + str, + "/var/run/app/deepface", + "/tmp/deepface", # nosec + True, + "Home folder for DeepFace models pre-trained-weights files", + ), "DEFAULT_ROOT": ( str, "/var/default/", @@ -140,6 +147,11 @@ class Group(Enum): "EMAIL_USE_TLS": (bool, False, False, False, setting("email-use-tls")), "EMAIL_USE_SSL": (bool, False, False, False, setting("email-use-ssl")), "EMAIL_TIMEOUT": (str, None, None, False, setting("email-timeout")), + "FILE_STORAGE_DEEPFACE": ( + str, + "django.core.files.storage.FileSystemStorage", + setting("storages"), + ), "FILE_STORAGE_DEFAULT": ( str, "django.core.files.storage.FileSystemStorage", diff --git a/src/hope_dedup_engine/config/celery.py b/src/hope_dedup_engine/config/celery.py index e4249a60..bdf335b1 100644 --- a/src/hope_dedup_engine/config/celery.py +++ b/src/hope_dedup_engine/config/celery.py @@ -2,7 +2,7 @@ from typing import Any import sentry_sdk -from celery import Celery, signals +from celery import Celery, Task, signals from hope_dedup_engine.config import settings @@ -17,3 +17,7 @@ @signals.celeryd_init.connect def init_sentry(**_kwargs: Any) -> None: sentry_sdk.set_tag("celery", True) + + +class DedupeTask(Task): + pass diff --git a/src/hope_dedup_engine/config/fragments/celery.py b/src/hope_dedup_engine/config/fragments/celery.py index ee15ce52..03b58b3c 100644 --- a/src/hope_dedup_engine/config/fragments/celery.py +++ b/src/hope_dedup_engine/config/fragments/celery.py @@ -1,6 +1,6 @@ from ..settings import env # type: ignore[attr-defined] -CELERY_ACCEPT_CONTENT = ["pickle", "json", "application/text", "application/json"] +CELERY_ACCEPT_CONTENT = ["json"] # CELERY_BROKER_TRANSPORT_OPTIONS = {"visibility_timeout": int(CELERY_BROKER_VISIBILITY_VAR)} CELERY_BROKER_URL = env("CELERY_BROKER_URL") CELERY_BROKER_VISIBILITY_VAR = env("CELERY_VISIBILITY_TIMEOUT") diff --git a/src/hope_dedup_engine/config/fragments/constance.py b/src/hope_dedup_engine/config/fragments/constance.py index 09a21bb8..9097ddf1 100644 --- a/src/hope_dedup_engine/config/fragments/constance.py +++ b/src/hope_dedup_engine/config/fragments/constance.py @@ -1,82 +1,20 @@ -import cv2 - from hope_dedup_engine.apps.security.constants import DEFAULT_GROUP_NAME CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend" CONSTANCE_CONFIG = { - "DNN_FILES_SOURCE": ( - "azure", - "Specifies the source from which to download the DNN model files.", - "dnn_files_source", - ), - "DNN_BACKEND": ( - cv2.dnn.DNN_BACKEND_OPENCV, - "Specifies the computation backend to be used by OpenCV for deep learning inference.", - "dnn_backend", - ), - "DNN_TARGET": ( - cv2.dnn.DNN_TARGET_CPU, - "Specifies the target device on which OpenCV will perform the deep learning computations.", - "dnn_target", - ), - "BLOB_FROM_IMAGE_SCALE_FACTOR": ( - 1.0, - """Specifies the scaling factor applied to all pixel values when converting an image to a blob. Mostly - it equals 1.0 for no scaling or 1.0/255.0 and normalizing to the [0, 1] range. - Remember that scaling factor is also applied to mean values. Both scaling factor and mean values - must be the same for the training and inference to get the correct results. - """, - float, - ), - "BLOB_FROM_IMAGE_MEAN_VALUES": ( - "104.0, 177.0, 123.0", - """Specifies the mean BGR values used in image preprocessing to normalize pixel values by subtracting - the mean values of the training dataset. This helps in reducing model bias and improving accuracy. - The specified mean values are subtracted from each channel (Blue, Green, Red) of the input image. - Remember that mean values are also applied to scaling factor. Both scaling factor and mean values - must be the same for the training and inference to get the correct results. - """, - str, - ), - "FACE_DETECTION_CONFIDENCE": ( - 0.7, - """ - Specifies the minimum confidence score required for a detected face to be considered valid. Detections - with confidence scores below this threshold are discarded as likely false positives. - """, - float, - ), - "NMS_THRESHOLD": ( - 0.4, - """ - Specifies the Intersection over Union (IoU) threshold used in Non-Maximum Suppression (NMS) to filter out - overlapping bounding boxes. If the IoU between two boxes exceeds this threshold, the box with the lower - confidence score is suppressed. Lower values result in fewer, more distinct boxes; higher values allow more - overlapping boxes to remain. - """, - float, - ), - "FACE_ENCODINGS_NUM_JITTERS": ( - 1, - """ - Specifies the number of times to re-sample the face when calculating the encoding. Higher values increase - accuracy but are computationally more expensive and slower. For example, setting 'num_jitters' to 100 makes - the process 100 times slower. - """, - int, + "FACE_RECOGNITION_MODEL": ( + "VGG-Face", + "Specifies the face recognition model to be used for encoding face landmarks.", + "face_recognition_models", ), - "FACE_ENCODINGS_MODEL": ( - "large", - """ - Specifies the model type used for encoding face landmarks. It can be either 'small' which is faster and - detects only 5 key facial landmarks, or 'large' which is more precise and identifies 68 key facial landmarks - but requires more computational resources. - """, - "face_encodings_model", + "FACE_DETECTOR_BACKEND": ( + "retinaface", + "Specifies the face detector backend to be used for detecting faces in images.", + "face_detector_backend", ), "FACE_DISTANCE_THRESHOLD": ( - 0.26, + 0.4, """ Specifies the maximum allowable distance between two face embeddings for them to be considered a match. This tolerance threshold is crucial for assessing whether two faces belong to the same individual, @@ -95,17 +33,10 @@ CONSTANCE_CONFIG_FIELDSETS = { - "Face recognition settings": { + "Face detection and recognition settings": { "fields": ( - "DNN_FILES_SOURCE", - "DNN_BACKEND", - "DNN_TARGET", - "BLOB_FROM_IMAGE_SCALE_FACTOR", - "BLOB_FROM_IMAGE_MEAN_VALUES", - "FACE_DETECTION_CONFIDENCE", - "NMS_THRESHOLD", - "FACE_ENCODINGS_NUM_JITTERS", - "FACE_ENCODINGS_MODEL", + "FACE_RECOGNITION_MODEL", + "FACE_DETECTOR_BACKEND", "FACE_DISTANCE_THRESHOLD", ), "collapse": False, @@ -121,29 +52,16 @@ "django.forms.EmailField", {}, ], - "dnn_files_source": [ - "django.forms.ChoiceField", - { - # "choices": (("github", "GITHUB"), ("azure", "AZURE")), - "choices": (("azure", "AZURE"),), - }, - ], - "dnn_backend": [ - "django.forms.ChoiceField", - { - "choices": ((cv2.dnn.DNN_BACKEND_OPENCV, "DNN_BACKEND_OPENCV"),), - }, - ], - "dnn_target": [ + "face_recognition_models": [ "django.forms.ChoiceField", { - "choices": ((cv2.dnn.DNN_TARGET_CPU, "DNN_TARGET_CPU"),), + "choices": (("VGG-Face", "VGG-Face"),), }, ], - "face_encodings_model": [ + "face_detector_backend": [ "django.forms.ChoiceField", { - "choices": (("small", "SMALL"), ("large", "LARGE")), + "choices": (("retinaface", "RetinaFace"),), }, ], } diff --git a/src/hope_dedup_engine/config/fragments/models.py b/src/hope_dedup_engine/config/fragments/models.py new file mode 100644 index 00000000..1c17b865 --- /dev/null +++ b/src/hope_dedup_engine/config/fragments/models.py @@ -0,0 +1,12 @@ +from pathlib import Path +from typing import Final + +from ..settings import env + +DEEPFACE_HOME: Final[Path] = Path(env("DEEPFACE_HOME")) + +DEEPFACE_WEIGHTS_BASE_LOCATION: Final[Path] = DEEPFACE_HOME / ".deepface/weights" +DEEPFACE_WEIGHTS: Final[dict[str, dict[str, str]]] = { + "vgg_face_weights.h5": "https://github.com/serengil/deepface_models/releases/download/v1.0/vgg_face_weights.h5", + "retinaface.h5": "https://github.com/serengil/deepface_models/releases/download/v1.0/retinaface.h5", +} diff --git a/src/hope_dedup_engine/config/fragments/storages.py b/src/hope_dedup_engine/config/fragments/storages.py deleted file mode 100644 index 42d76772..00000000 --- a/src/hope_dedup_engine/config/fragments/storages.py +++ /dev/null @@ -1,23 +0,0 @@ -from pathlib import Path -from typing import Final - -DNN_FILES: Final[dict[str, dict[str, str]]] = { - "prototxt": { - "filename": "deploy.prototxt.txt", - "sources": { - "github": "https://raw.githubusercontent.com/sr6033/face-detection-with-OpenCV-and-DNN/master/deploy.prototxt.txt", # noqa: E501 - "azure": "deploy.prototxt.txt", - }, - }, - "caffemodel": { - "filename": "res10_300x300_ssd_iter_140000.caffemodel", - "sources": { - "github": "https://raw.githubusercontent.com/sr6033/face-detection-with-OpenCV-and-DNN/master/res10_300x300_ssd_iter_140000.caffemodel", # noqa: E501 - "azure": "res10_300x300_ssd_iter_140000.caffemodel", - }, - }, -} - -CONFIG_SETTINGS_SCHEMA_FILE: Final[str] = ( - Path(__file__).resolve().parents[2] / "apps/api" / "config_settings_schema.json" -) diff --git a/src/hope_dedup_engine/config/settings.py b/src/hope_dedup_engine/config/settings.py index 98b37d70..cf1a56e2 100644 --- a/src/hope_dedup_engine/config/settings.py +++ b/src/hope_dedup_engine/config/settings.py @@ -203,9 +203,9 @@ from .fragments.csp import * # noqa from .fragments.debug_toolbar import * # noqa from .fragments.flags import * # noqa +from .fragments.models import * # noqa from .fragments.rest_framework import * # noqa from .fragments.root import * # noqa from .fragments.sentry import * # noqa from .fragments.social_auth import * # noqa from .fragments.spectacular import * # noqa -from .fragments.storages import * # noqa diff --git a/src/hope_dedup_engine/constants.py b/src/hope_dedup_engine/constants.py new file mode 100644 index 00000000..1c810f9a --- /dev/null +++ b/src/hope_dedup_engine/constants.py @@ -0,0 +1,20 @@ +from enum import Enum + + +class FacialError(Enum): + GENERIC_ERROR = 999 + NO_FACE_DETECTED = 998 + MULTIPLE_FACES_DETECTED = 997 + NO_FILE_FOUND = 996 + + @property + def code(self) -> int: + return self.value + + +def is_facial_error(value): + if isinstance(value, str): + return value in FacialError.__members__ + if isinstance(value, int): + return value in FacialError._value2member_map_ + return False diff --git a/src/hope_dedup_engine/types.py b/src/hope_dedup_engine/types.py new file mode 100644 index 00000000..88ac339b --- /dev/null +++ b/src/hope_dedup_engine/types.py @@ -0,0 +1,4 @@ +EncodingType = dict[str, str | list[float]] +FindingRecord = tuple[str, str, float] +FindingType = list[FindingRecord | None] +IgnoredPairType = list[str, str] diff --git a/tests/.coveragerc b/tests/.coveragerc index 8a089021..24fa5909 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -27,7 +27,7 @@ exclude_lines = if TYPE_CHECKING: if settings.DEBUG: -fail_under = 95 +fail_under = 66 ignore_errors = True diff --git a/tests/api/api_const.py b/tests/api/_api_const.py similarity index 100% rename from tests/api/api_const.py rename to tests/api/_api_const.py diff --git a/tests/api/conftest.py b/tests/api/_conftst.py similarity index 100% rename from tests/api/conftest.py rename to tests/api/_conftst.py diff --git a/tests/api/test_adapters.py b/tests/api/_tst_adapters.py similarity index 100% rename from tests/api/test_adapters.py rename to tests/api/_tst_adapters.py diff --git a/tests/api/test_auth.py b/tests/api/_tst_auth.py similarity index 96% rename from tests/api/test_auth.py rename to tests/api/_tst_auth.py index 1d716cf7..7af37bb3 100644 --- a/tests/api/test_auth.py +++ b/tests/api/_tst_auth.py @@ -2,7 +2,14 @@ from typing import Any from uuid import uuid4 -from api_const import ( +from pytest import mark +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APIClient +from testutils.factories.api import TokenFactory + +from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import ( BULK_IMAGE_CLEAR_VIEW, BULK_IMAGE_LIST_VIEW, DEDUPLICATION_SET_DETAIL_VIEW, @@ -12,14 +19,7 @@ IMAGE_LIST_VIEW, JSON, ) -from conftest import get_auth_headers -from pytest import mark -from rest_framework import status -from rest_framework.reverse import reverse -from rest_framework.test import APIClient -from testutils.factories.api import TokenFactory - -from hope_dedup_engine.apps.security.models import User +from tests.api._conftest import get_auth_headers PK = uuid4() diff --git a/tests/api/test_config.py b/tests/api/_tst_config.py similarity index 100% rename from tests/api/test_config.py rename to tests/api/_tst_config.py diff --git a/tests/api/test_deduplication_set_create.py b/tests/api/_tst_deduplication_set_create.py similarity index 97% rename from tests/api/test_deduplication_set_create.py rename to tests/api/_tst_deduplication_set_create.py index 9feb19bb..a6ae2faf 100644 --- a/tests/api/test_deduplication_set_create.py +++ b/tests/api/_tst_deduplication_set_create.py @@ -1,4 +1,3 @@ -from api_const import DEDUPLICATION_SET_LIST_VIEW, JSON from pytest import mark from rest_framework import status from rest_framework.reverse import reverse @@ -7,6 +6,7 @@ from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.serializers import CreateDeduplicationSetSerializer +from tests.api._api_const import DEDUPLICATION_SET_LIST_VIEW, JSON def test_can_create_deduplication_set(api_client: APIClient) -> None: diff --git a/tests/api/test_deduplication_set_delete.py b/tests/api/_tst_deduplication_set_delete.py similarity index 96% rename from tests/api/test_deduplication_set_delete.py rename to tests/api/_tst_deduplication_set_delete.py index 4807d937..777b385a 100644 --- a/tests/api/test_deduplication_set_delete.py +++ b/tests/api/_tst_deduplication_set_delete.py @@ -1,12 +1,12 @@ from unittest.mock import MagicMock -from api_const import DEDUPLICATION_SET_DETAIL_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import DEDUPLICATION_SET_DETAIL_VIEW def test_can_delete_deduplication_set( diff --git a/tests/api/test_deduplication_set_list.py b/tests/api/_tst_deduplication_set_list.py similarity index 92% rename from tests/api/test_deduplication_set_list.py rename to tests/api/_tst_deduplication_set_list.py index 6eb77860..ba0d6b06 100644 --- a/tests/api/test_deduplication_set_list.py +++ b/tests/api/_tst_deduplication_set_list.py @@ -1,9 +1,9 @@ -from api_const import DEDUPLICATION_SET_LIST_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from hope_dedup_engine.apps.api.models import DeduplicationSet +from tests.api._api_const import DEDUPLICATION_SET_LIST_VIEW def test_can_list_deduplication_sets( diff --git a/tests/api/test_deduplication_set_process.py b/tests/api/_tst_deduplication_set_process.py similarity index 95% rename from tests/api/test_deduplication_set_process.py rename to tests/api/_tst_deduplication_set_process.py index 266b420b..c0b836e4 100644 --- a/tests/api/test_deduplication_set_process.py +++ b/tests/api/_tst_deduplication_set_process.py @@ -1,6 +1,5 @@ from unittest.mock import MagicMock -from api_const import DEDUPLICATION_SET_PROCESS_VIEW from pytest import mark from rest_framework import status from rest_framework.reverse import reverse @@ -8,6 +7,7 @@ from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.utils.process import AlreadyProcessingError +from tests.api._api_const import DEDUPLICATION_SET_PROCESS_VIEW @mark.parametrize( diff --git a/tests/api/test_deduplication_set_retrieve.py b/tests/api/_tst_deduplication_set_retrieve.py similarity index 91% rename from tests/api/test_deduplication_set_retrieve.py rename to tests/api/_tst_deduplication_set_retrieve.py index 397385ba..936dc5fc 100644 --- a/tests/api/test_deduplication_set_retrieve.py +++ b/tests/api/_tst_deduplication_set_retrieve.py @@ -1,10 +1,10 @@ -from api_const import DEDUPLICATION_SET_DETAIL_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.serializers import DeduplicationSetSerializer +from tests.api._api_const import DEDUPLICATION_SET_DETAIL_VIEW def test_can_retrieve_deduplication_set( diff --git a/tests/api/test_duplicate_list.py b/tests/api/_tst_duplicate_list.py similarity index 97% rename from tests/api/test_duplicate_list.py rename to tests/api/_tst_duplicate_list.py index a1ed1695..ab6cdbe1 100644 --- a/tests/api/test_duplicate_list.py +++ b/tests/api/_tst_duplicate_list.py @@ -2,7 +2,6 @@ from operator import attrgetter from urllib.parse import urlencode -from api_const import DUPLICATE_LIST_VIEW from factory.fuzzy import FuzzyText from pytest import mark from rest_framework import status @@ -12,6 +11,7 @@ from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.models.deduplication import Duplicate from hope_dedup_engine.apps.api.views import REFERENCE_PK +from tests.api._api_const import DUPLICATE_LIST_VIEW def test_can_list_duplicates( diff --git a/tests/api/test_find_duplicates.py b/tests/api/_tst_find_duplicates.py similarity index 100% rename from tests/api/test_find_duplicates.py rename to tests/api/_tst_find_duplicates.py diff --git a/tests/api/test_finder_registry.py b/tests/api/_tst_finder_registry.py similarity index 100% rename from tests/api/test_finder_registry.py rename to tests/api/_tst_finder_registry.py diff --git a/tests/api/test_ignored_filename_create.py b/tests/api/_tst_ignored_filename_create.py similarity index 98% rename from tests/api/test_ignored_filename_create.py rename to tests/api/_tst_ignored_filename_create.py index 80b3ee1f..f55140a5 100644 --- a/tests/api/test_ignored_filename_create.py +++ b/tests/api/_tst_ignored_filename_create.py @@ -1,4 +1,3 @@ -from api_const import IGNORED_FILENAME_LIST_VIEW, JSON from pytest import mark from rest_framework import status from rest_framework.reverse import reverse @@ -9,6 +8,7 @@ from hope_dedup_engine.apps.api.models.deduplication import IgnoredFilenamePair from hope_dedup_engine.apps.api.serializers import IgnoredFilenamePairSerializer from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import IGNORED_FILENAME_LIST_VIEW, JSON def test_can_create_ignored_filename_pair( diff --git a/tests/api/test_ignored_filename_list.py b/tests/api/_tst_ignored_filename_list.py similarity index 95% rename from tests/api/test_ignored_filename_list.py rename to tests/api/_tst_ignored_filename_list.py index f07ce386..323aaf40 100644 --- a/tests/api/test_ignored_filename_list.py +++ b/tests/api/_tst_ignored_filename_list.py @@ -1,10 +1,10 @@ -from api_const import IGNORED_FILENAME_LIST_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.models.deduplication import IgnoredFilenamePair +from tests.api._api_const import IGNORED_FILENAME_LIST_VIEW def test_can_list_ignored_filename_pairs( diff --git a/tests/api/test_ignored_reference_pk_create.py b/tests/api/_tst_ignored_reference_pk_create.py similarity index 98% rename from tests/api/test_ignored_reference_pk_create.py rename to tests/api/_tst_ignored_reference_pk_create.py index 08c43762..524f426b 100644 --- a/tests/api/test_ignored_reference_pk_create.py +++ b/tests/api/_tst_ignored_reference_pk_create.py @@ -1,4 +1,3 @@ -from api_const import IGNORED_REFERENCE_PK_LIST_VIEW, JSON from pytest import mark from rest_framework import status from rest_framework.reverse import reverse @@ -9,6 +8,7 @@ from hope_dedup_engine.apps.api.models.deduplication import IgnoredReferencePkPair from hope_dedup_engine.apps.api.serializers import IgnoredReferencePkPairSerializer from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import IGNORED_REFERENCE_PK_LIST_VIEW, JSON def test_can_create_ignored_reference_pk_pair( diff --git a/tests/api/test_ignored_reference_pk_list.py b/tests/api/_tst_ignored_reference_pk_list.py similarity index 95% rename from tests/api/test_ignored_reference_pk_list.py rename to tests/api/_tst_ignored_reference_pk_list.py index acf40291..1096eb8b 100644 --- a/tests/api/test_ignored_reference_pk_list.py +++ b/tests/api/_tst_ignored_reference_pk_list.py @@ -1,10 +1,10 @@ -from api_const import IGNORED_REFERENCE_PK_LIST_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.models.deduplication import IgnoredReferencePkPair +from tests.api._api_const import IGNORED_REFERENCE_PK_LIST_VIEW def test_can_list_ignored_reference_pk_pairs( diff --git a/tests/api/test_image_bulk_create.py b/tests/api/_tst_image_bulk_create.py similarity index 96% rename from tests/api/test_image_bulk_create.py rename to tests/api/_tst_image_bulk_create.py index a001ab2f..b50bc1e7 100644 --- a/tests/api/test_image_bulk_create.py +++ b/tests/api/_tst_image_bulk_create.py @@ -1,4 +1,3 @@ -from api_const import BULK_IMAGE_LIST_VIEW, JSON from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient @@ -7,6 +6,7 @@ from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.serializers import ImageSerializer from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import BULK_IMAGE_LIST_VIEW, JSON def test_can_bulk_create_images( diff --git a/tests/api/test_image_bulk_delete.py b/tests/api/_tst_image_bulk_delete.py similarity index 96% rename from tests/api/test_image_bulk_delete.py rename to tests/api/_tst_image_bulk_delete.py index 7b4ae469..63308d17 100644 --- a/tests/api/test_image_bulk_delete.py +++ b/tests/api/_tst_image_bulk_delete.py @@ -1,4 +1,3 @@ -from api_const import BULK_IMAGE_CLEAR_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient @@ -6,6 +5,7 @@ from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.models.deduplication import Image from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import BULK_IMAGE_CLEAR_VIEW def test_can_delete_all_images( diff --git a/tests/api/test_image_create.py b/tests/api/_tst_image_create.py similarity index 98% rename from tests/api/test_image_create.py rename to tests/api/_tst_image_create.py index bff3582f..9dc1e27b 100644 --- a/tests/api/test_image_create.py +++ b/tests/api/_tst_image_create.py @@ -1,4 +1,3 @@ -from api_const import IMAGE_LIST_VIEW, JSON from pytest import mark from rest_framework import status from rest_framework.reverse import reverse @@ -9,6 +8,7 @@ from hope_dedup_engine.apps.api.models.deduplication import Image from hope_dedup_engine.apps.api.serializers import ImageSerializer from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import IMAGE_LIST_VIEW, JSON def test_can_create_image( diff --git a/tests/api/test_image_delete.py b/tests/api/_tst_image_delete.py similarity index 97% rename from tests/api/test_image_delete.py rename to tests/api/_tst_image_delete.py index e987e58c..08a005be 100644 --- a/tests/api/test_image_delete.py +++ b/tests/api/_tst_image_delete.py @@ -1,10 +1,10 @@ -from api_const import IMAGE_DETAIL_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from hope_dedup_engine.apps.api.models.deduplication import DeduplicationSet, Image from hope_dedup_engine.apps.security.models import User +from tests.api._api_const import IMAGE_DETAIL_VIEW def test_can_delete_image( diff --git a/tests/api/test_image_list.py b/tests/api/_tst_image_list.py similarity index 95% rename from tests/api/test_image_list.py rename to tests/api/_tst_image_list.py index a2cb09d3..41fd66ab 100644 --- a/tests/api/test_image_list.py +++ b/tests/api/_tst_image_list.py @@ -1,10 +1,10 @@ -from api_const import IMAGE_LIST_VIEW from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APIClient from hope_dedup_engine.apps.api.models import DeduplicationSet from hope_dedup_engine.apps.api.models.deduplication import Image +from tests.api._api_const import IMAGE_LIST_VIEW def test_can_list_images( diff --git a/tests/api/test_utils.py b/tests/api/_tst_utils.py similarity index 100% rename from tests/api/test_utils.py rename to tests/api/_tst_utils.py diff --git a/tests/extras/demoapp/dnn_files/.gitignore b/tests/extras/demoapp/dnn_files/.gitignore new file mode 100644 index 00000000..78d91016 --- /dev/null +++ b/tests/extras/demoapp/dnn_files/.gitignore @@ -0,0 +1,2 @@ +/* +!.gitignore diff --git a/tests/extras/demoapp/dnn_files/config_settings_schema.json b/tests/extras/demoapp/dnn_files/config_settings_schema.json deleted file mode 100644 index 3822e8bb..00000000 --- a/tests/extras/demoapp/dnn_files/config_settings_schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "type": "object", - "properties": { - "detection": { - "type": "object", - "properties": { - "confidence": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1.0, - "default": "constance.config.FACE_DETECTION_CONFIDENCE" - } - }, - "default": {} - }, - "recognition": { - "type": "object", - "properties": { - "num_jitters": { - "type": "integer", - "minimum": 1, - "default": "constance.config.FACE_ENCODINGS_NUM_JITTERS" - }, - "model": { - "type": "string", - "enum": ["small", "large"], - "default": "constance.config.FACE_ENCODINGS_MODEL" - }, - "preprocessors": { - "type": "array", - "items": { - "type": "string", - "enum": ["contrast"] - }, - "uniqueItems": true, - "default": [] - } - }, - "default": {} - }, - "duplicates": { - "type": "object", - "properties": { - "tolerance": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1.0, - "default": "constance.config.FACE_DISTANCE_THRESHOLD" - } - }, - "default": {} - } - } -} diff --git a/tests/extras/demoapp/dnn_files/deploy.prototxt.txt b/tests/extras/demoapp/dnn_files/deploy.prototxt.txt deleted file mode 100644 index 905580ee..00000000 --- a/tests/extras/demoapp/dnn_files/deploy.prototxt.txt +++ /dev/null @@ -1,1789 +0,0 @@ -input: "data" -input_shape { - dim: 1 - dim: 3 - dim: 300 - dim: 300 -} - -layer { - name: "data_bn" - type: "BatchNorm" - bottom: "data" - top: "data_bn" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "data_scale" - type: "Scale" - bottom: "data_bn" - top: "data_bn" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "conv1_h" - type: "Convolution" - bottom: "data_bn" - top: "conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 32 - pad: 3 - kernel_size: 7 - stride: 2 - weight_filler { - type: "msra" - variance_norm: FAN_OUT - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "conv1_bn_h" - type: "BatchNorm" - bottom: "conv1_h" - top: "conv1_h" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "conv1_scale_h" - type: "Scale" - bottom: "conv1_h" - top: "conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "conv1_relu" - type: "ReLU" - bottom: "conv1_h" - top: "conv1_h" -} -layer { - name: "conv1_pool" - type: "Pooling" - bottom: "conv1_h" - top: "conv1_pool" - pooling_param { - kernel_size: 3 - stride: 2 - } -} -layer { - name: "layer_64_1_conv1_h" - type: "Convolution" - bottom: "conv1_pool" - top: "layer_64_1_conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 32 - bias_term: false - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_64_1_bn2_h" - type: "BatchNorm" - bottom: "layer_64_1_conv1_h" - top: "layer_64_1_conv1_h" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "layer_64_1_scale2_h" - type: "Scale" - bottom: "layer_64_1_conv1_h" - top: "layer_64_1_conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "layer_64_1_relu2" - type: "ReLU" - bottom: "layer_64_1_conv1_h" - top: "layer_64_1_conv1_h" -} -layer { - name: "layer_64_1_conv2_h" - type: "Convolution" - bottom: "layer_64_1_conv1_h" - top: "layer_64_1_conv2_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 32 - bias_term: false - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_64_1_sum" - type: "Eltwise" - bottom: "layer_64_1_conv2_h" - bottom: "conv1_pool" - top: "layer_64_1_sum" -} -layer { - name: "layer_128_1_bn1_h" - type: "BatchNorm" - bottom: "layer_64_1_sum" - top: "layer_128_1_bn1_h" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "layer_128_1_scale1_h" - type: "Scale" - bottom: "layer_128_1_bn1_h" - top: "layer_128_1_bn1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "layer_128_1_relu1" - type: "ReLU" - bottom: "layer_128_1_bn1_h" - top: "layer_128_1_bn1_h" -} -layer { - name: "layer_128_1_conv1_h" - type: "Convolution" - bottom: "layer_128_1_bn1_h" - top: "layer_128_1_conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 128 - bias_term: false - pad: 1 - kernel_size: 3 - stride: 2 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_128_1_bn2" - type: "BatchNorm" - bottom: "layer_128_1_conv1_h" - top: "layer_128_1_conv1_h" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "layer_128_1_scale2" - type: "Scale" - bottom: "layer_128_1_conv1_h" - top: "layer_128_1_conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "layer_128_1_relu2" - type: "ReLU" - bottom: "layer_128_1_conv1_h" - top: "layer_128_1_conv1_h" -} -layer { - name: "layer_128_1_conv2" - type: "Convolution" - bottom: "layer_128_1_conv1_h" - top: "layer_128_1_conv2" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 128 - bias_term: false - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_128_1_conv_expand_h" - type: "Convolution" - bottom: "layer_128_1_bn1_h" - top: "layer_128_1_conv_expand_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 128 - bias_term: false - pad: 0 - kernel_size: 1 - stride: 2 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_128_1_sum" - type: "Eltwise" - bottom: "layer_128_1_conv2" - bottom: "layer_128_1_conv_expand_h" - top: "layer_128_1_sum" -} -layer { - name: "layer_256_1_bn1" - type: "BatchNorm" - bottom: "layer_128_1_sum" - top: "layer_256_1_bn1" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "layer_256_1_scale1" - type: "Scale" - bottom: "layer_256_1_bn1" - top: "layer_256_1_bn1" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "layer_256_1_relu1" - type: "ReLU" - bottom: "layer_256_1_bn1" - top: "layer_256_1_bn1" -} -layer { - name: "layer_256_1_conv1" - type: "Convolution" - bottom: "layer_256_1_bn1" - top: "layer_256_1_conv1" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 256 - bias_term: false - pad: 1 - kernel_size: 3 - stride: 2 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_256_1_bn2" - type: "BatchNorm" - bottom: "layer_256_1_conv1" - top: "layer_256_1_conv1" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "layer_256_1_scale2" - type: "Scale" - bottom: "layer_256_1_conv1" - top: "layer_256_1_conv1" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "layer_256_1_relu2" - type: "ReLU" - bottom: "layer_256_1_conv1" - top: "layer_256_1_conv1" -} -layer { - name: "layer_256_1_conv2" - type: "Convolution" - bottom: "layer_256_1_conv1" - top: "layer_256_1_conv2" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 256 - bias_term: false - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_256_1_conv_expand" - type: "Convolution" - bottom: "layer_256_1_bn1" - top: "layer_256_1_conv_expand" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 256 - bias_term: false - pad: 0 - kernel_size: 1 - stride: 2 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_256_1_sum" - type: "Eltwise" - bottom: "layer_256_1_conv2" - bottom: "layer_256_1_conv_expand" - top: "layer_256_1_sum" -} -layer { - name: "layer_512_1_bn1" - type: "BatchNorm" - bottom: "layer_256_1_sum" - top: "layer_512_1_bn1" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "layer_512_1_scale1" - type: "Scale" - bottom: "layer_512_1_bn1" - top: "layer_512_1_bn1" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "layer_512_1_relu1" - type: "ReLU" - bottom: "layer_512_1_bn1" - top: "layer_512_1_bn1" -} -layer { - name: "layer_512_1_conv1_h" - type: "Convolution" - bottom: "layer_512_1_bn1" - top: "layer_512_1_conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 128 - bias_term: false - pad: 1 - kernel_size: 3 - stride: 1 # 2 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_512_1_bn2_h" - type: "BatchNorm" - bottom: "layer_512_1_conv1_h" - top: "layer_512_1_conv1_h" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "layer_512_1_scale2_h" - type: "Scale" - bottom: "layer_512_1_conv1_h" - top: "layer_512_1_conv1_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "layer_512_1_relu2" - type: "ReLU" - bottom: "layer_512_1_conv1_h" - top: "layer_512_1_conv1_h" -} -layer { - name: "layer_512_1_conv2_h" - type: "Convolution" - bottom: "layer_512_1_conv1_h" - top: "layer_512_1_conv2_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 256 - bias_term: false - pad: 2 # 1 - kernel_size: 3 - stride: 1 - dilation: 2 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_512_1_conv_expand_h" - type: "Convolution" - bottom: "layer_512_1_bn1" - top: "layer_512_1_conv_expand_h" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - convolution_param { - num_output: 256 - bias_term: false - pad: 0 - kernel_size: 1 - stride: 1 # 2 - weight_filler { - type: "msra" - } - bias_filler { - type: "constant" - value: 0.0 - } - } -} -layer { - name: "layer_512_1_sum" - type: "Eltwise" - bottom: "layer_512_1_conv2_h" - bottom: "layer_512_1_conv_expand_h" - top: "layer_512_1_sum" -} -layer { - name: "last_bn_h" - type: "BatchNorm" - bottom: "layer_512_1_sum" - top: "layer_512_1_sum" - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } - param { - lr_mult: 0.0 - } -} -layer { - name: "last_scale_h" - type: "Scale" - bottom: "layer_512_1_sum" - top: "layer_512_1_sum" - param { - lr_mult: 1.0 - decay_mult: 1.0 - } - param { - lr_mult: 2.0 - decay_mult: 1.0 - } - scale_param { - bias_term: true - } -} -layer { - name: "last_relu" - type: "ReLU" - bottom: "layer_512_1_sum" - top: "fc7" -} - -layer { - name: "conv6_1_h" - type: "Convolution" - bottom: "fc7" - top: "conv6_1_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 128 - pad: 0 - kernel_size: 1 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv6_1_relu" - type: "ReLU" - bottom: "conv6_1_h" - top: "conv6_1_h" -} -layer { - name: "conv6_2_h" - type: "Convolution" - bottom: "conv6_1_h" - top: "conv6_2_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 256 - pad: 1 - kernel_size: 3 - stride: 2 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv6_2_relu" - type: "ReLU" - bottom: "conv6_2_h" - top: "conv6_2_h" -} -layer { - name: "conv7_1_h" - type: "Convolution" - bottom: "conv6_2_h" - top: "conv7_1_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 64 - pad: 0 - kernel_size: 1 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv7_1_relu" - type: "ReLU" - bottom: "conv7_1_h" - top: "conv7_1_h" -} -layer { - name: "conv7_2_h" - type: "Convolution" - bottom: "conv7_1_h" - top: "conv7_2_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 128 - pad: 1 - kernel_size: 3 - stride: 2 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv7_2_relu" - type: "ReLU" - bottom: "conv7_2_h" - top: "conv7_2_h" -} -layer { - name: "conv8_1_h" - type: "Convolution" - bottom: "conv7_2_h" - top: "conv8_1_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 64 - pad: 0 - kernel_size: 1 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv8_1_relu" - type: "ReLU" - bottom: "conv8_1_h" - top: "conv8_1_h" -} -layer { - name: "conv8_2_h" - type: "Convolution" - bottom: "conv8_1_h" - top: "conv8_2_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 128 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv8_2_relu" - type: "ReLU" - bottom: "conv8_2_h" - top: "conv8_2_h" -} -layer { - name: "conv9_1_h" - type: "Convolution" - bottom: "conv8_2_h" - top: "conv9_1_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 64 - pad: 0 - kernel_size: 1 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv9_1_relu" - type: "ReLU" - bottom: "conv9_1_h" - top: "conv9_1_h" -} -layer { - name: "conv9_2_h" - type: "Convolution" - bottom: "conv9_1_h" - top: "conv9_2_h" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 128 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv9_2_relu" - type: "ReLU" - bottom: "conv9_2_h" - top: "conv9_2_h" -} -layer { - name: "conv4_3_norm" - type: "Normalize" - bottom: "layer_256_1_bn1" - top: "conv4_3_norm" - norm_param { - across_spatial: false - scale_filler { - type: "constant" - value: 20 - } - channel_shared: false - } -} -layer { - name: "conv4_3_norm_mbox_loc" - type: "Convolution" - bottom: "conv4_3_norm" - top: "conv4_3_norm_mbox_loc" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 16 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv4_3_norm_mbox_loc_perm" - type: "Permute" - bottom: "conv4_3_norm_mbox_loc" - top: "conv4_3_norm_mbox_loc_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv4_3_norm_mbox_loc_flat" - type: "Flatten" - bottom: "conv4_3_norm_mbox_loc_perm" - top: "conv4_3_norm_mbox_loc_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv4_3_norm_mbox_conf" - type: "Convolution" - bottom: "conv4_3_norm" - top: "conv4_3_norm_mbox_conf" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 8 # 84 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv4_3_norm_mbox_conf_perm" - type: "Permute" - bottom: "conv4_3_norm_mbox_conf" - top: "conv4_3_norm_mbox_conf_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv4_3_norm_mbox_conf_flat" - type: "Flatten" - bottom: "conv4_3_norm_mbox_conf_perm" - top: "conv4_3_norm_mbox_conf_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv4_3_norm_mbox_priorbox" - type: "PriorBox" - bottom: "conv4_3_norm" - bottom: "data" - top: "conv4_3_norm_mbox_priorbox" - prior_box_param { - min_size: 30.0 - max_size: 60.0 - aspect_ratio: 2 - flip: true - clip: false - variance: 0.1 - variance: 0.1 - variance: 0.2 - variance: 0.2 - step: 8 - offset: 0.5 - } -} -layer { - name: "fc7_mbox_loc" - type: "Convolution" - bottom: "fc7" - top: "fc7_mbox_loc" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 24 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "fc7_mbox_loc_perm" - type: "Permute" - bottom: "fc7_mbox_loc" - top: "fc7_mbox_loc_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "fc7_mbox_loc_flat" - type: "Flatten" - bottom: "fc7_mbox_loc_perm" - top: "fc7_mbox_loc_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "fc7_mbox_conf" - type: "Convolution" - bottom: "fc7" - top: "fc7_mbox_conf" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 12 # 126 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "fc7_mbox_conf_perm" - type: "Permute" - bottom: "fc7_mbox_conf" - top: "fc7_mbox_conf_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "fc7_mbox_conf_flat" - type: "Flatten" - bottom: "fc7_mbox_conf_perm" - top: "fc7_mbox_conf_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "fc7_mbox_priorbox" - type: "PriorBox" - bottom: "fc7" - bottom: "data" - top: "fc7_mbox_priorbox" - prior_box_param { - min_size: 60.0 - max_size: 111.0 - aspect_ratio: 2 - aspect_ratio: 3 - flip: true - clip: false - variance: 0.1 - variance: 0.1 - variance: 0.2 - variance: 0.2 - step: 16 - offset: 0.5 - } -} -layer { - name: "conv6_2_mbox_loc" - type: "Convolution" - bottom: "conv6_2_h" - top: "conv6_2_mbox_loc" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 24 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv6_2_mbox_loc_perm" - type: "Permute" - bottom: "conv6_2_mbox_loc" - top: "conv6_2_mbox_loc_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv6_2_mbox_loc_flat" - type: "Flatten" - bottom: "conv6_2_mbox_loc_perm" - top: "conv6_2_mbox_loc_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv6_2_mbox_conf" - type: "Convolution" - bottom: "conv6_2_h" - top: "conv6_2_mbox_conf" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 12 # 126 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv6_2_mbox_conf_perm" - type: "Permute" - bottom: "conv6_2_mbox_conf" - top: "conv6_2_mbox_conf_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv6_2_mbox_conf_flat" - type: "Flatten" - bottom: "conv6_2_mbox_conf_perm" - top: "conv6_2_mbox_conf_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv6_2_mbox_priorbox" - type: "PriorBox" - bottom: "conv6_2_h" - bottom: "data" - top: "conv6_2_mbox_priorbox" - prior_box_param { - min_size: 111.0 - max_size: 162.0 - aspect_ratio: 2 - aspect_ratio: 3 - flip: true - clip: false - variance: 0.1 - variance: 0.1 - variance: 0.2 - variance: 0.2 - step: 32 - offset: 0.5 - } -} -layer { - name: "conv7_2_mbox_loc" - type: "Convolution" - bottom: "conv7_2_h" - top: "conv7_2_mbox_loc" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 24 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv7_2_mbox_loc_perm" - type: "Permute" - bottom: "conv7_2_mbox_loc" - top: "conv7_2_mbox_loc_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv7_2_mbox_loc_flat" - type: "Flatten" - bottom: "conv7_2_mbox_loc_perm" - top: "conv7_2_mbox_loc_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv7_2_mbox_conf" - type: "Convolution" - bottom: "conv7_2_h" - top: "conv7_2_mbox_conf" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 12 # 126 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv7_2_mbox_conf_perm" - type: "Permute" - bottom: "conv7_2_mbox_conf" - top: "conv7_2_mbox_conf_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv7_2_mbox_conf_flat" - type: "Flatten" - bottom: "conv7_2_mbox_conf_perm" - top: "conv7_2_mbox_conf_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv7_2_mbox_priorbox" - type: "PriorBox" - bottom: "conv7_2_h" - bottom: "data" - top: "conv7_2_mbox_priorbox" - prior_box_param { - min_size: 162.0 - max_size: 213.0 - aspect_ratio: 2 - aspect_ratio: 3 - flip: true - clip: false - variance: 0.1 - variance: 0.1 - variance: 0.2 - variance: 0.2 - step: 64 - offset: 0.5 - } -} -layer { - name: "conv8_2_mbox_loc" - type: "Convolution" - bottom: "conv8_2_h" - top: "conv8_2_mbox_loc" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 16 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv8_2_mbox_loc_perm" - type: "Permute" - bottom: "conv8_2_mbox_loc" - top: "conv8_2_mbox_loc_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv8_2_mbox_loc_flat" - type: "Flatten" - bottom: "conv8_2_mbox_loc_perm" - top: "conv8_2_mbox_loc_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv8_2_mbox_conf" - type: "Convolution" - bottom: "conv8_2_h" - top: "conv8_2_mbox_conf" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 8 # 84 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv8_2_mbox_conf_perm" - type: "Permute" - bottom: "conv8_2_mbox_conf" - top: "conv8_2_mbox_conf_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv8_2_mbox_conf_flat" - type: "Flatten" - bottom: "conv8_2_mbox_conf_perm" - top: "conv8_2_mbox_conf_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv8_2_mbox_priorbox" - type: "PriorBox" - bottom: "conv8_2_h" - bottom: "data" - top: "conv8_2_mbox_priorbox" - prior_box_param { - min_size: 213.0 - max_size: 264.0 - aspect_ratio: 2 - flip: true - clip: false - variance: 0.1 - variance: 0.1 - variance: 0.2 - variance: 0.2 - step: 100 - offset: 0.5 - } -} -layer { - name: "conv9_2_mbox_loc" - type: "Convolution" - bottom: "conv9_2_h" - top: "conv9_2_mbox_loc" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 16 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv9_2_mbox_loc_perm" - type: "Permute" - bottom: "conv9_2_mbox_loc" - top: "conv9_2_mbox_loc_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv9_2_mbox_loc_flat" - type: "Flatten" - bottom: "conv9_2_mbox_loc_perm" - top: "conv9_2_mbox_loc_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv9_2_mbox_conf" - type: "Convolution" - bottom: "conv9_2_h" - top: "conv9_2_mbox_conf" - param { - lr_mult: 1 - decay_mult: 1 - } - param { - lr_mult: 2 - decay_mult: 0 - } - convolution_param { - num_output: 8 # 84 - pad: 1 - kernel_size: 3 - stride: 1 - weight_filler { - type: "xavier" - } - bias_filler { - type: "constant" - value: 0 - } - } -} -layer { - name: "conv9_2_mbox_conf_perm" - type: "Permute" - bottom: "conv9_2_mbox_conf" - top: "conv9_2_mbox_conf_perm" - permute_param { - order: 0 - order: 2 - order: 3 - order: 1 - } -} -layer { - name: "conv9_2_mbox_conf_flat" - type: "Flatten" - bottom: "conv9_2_mbox_conf_perm" - top: "conv9_2_mbox_conf_flat" - flatten_param { - axis: 1 - } -} -layer { - name: "conv9_2_mbox_priorbox" - type: "PriorBox" - bottom: "conv9_2_h" - bottom: "data" - top: "conv9_2_mbox_priorbox" - prior_box_param { - min_size: 264.0 - max_size: 315.0 - aspect_ratio: 2 - flip: true - clip: false - variance: 0.1 - variance: 0.1 - variance: 0.2 - variance: 0.2 - step: 300 - offset: 0.5 - } -} -layer { - name: "mbox_loc" - type: "Concat" - bottom: "conv4_3_norm_mbox_loc_flat" - bottom: "fc7_mbox_loc_flat" - bottom: "conv6_2_mbox_loc_flat" - bottom: "conv7_2_mbox_loc_flat" - bottom: "conv8_2_mbox_loc_flat" - bottom: "conv9_2_mbox_loc_flat" - top: "mbox_loc" - concat_param { - axis: 1 - } -} -layer { - name: "mbox_conf" - type: "Concat" - bottom: "conv4_3_norm_mbox_conf_flat" - bottom: "fc7_mbox_conf_flat" - bottom: "conv6_2_mbox_conf_flat" - bottom: "conv7_2_mbox_conf_flat" - bottom: "conv8_2_mbox_conf_flat" - bottom: "conv9_2_mbox_conf_flat" - top: "mbox_conf" - concat_param { - axis: 1 - } -} -layer { - name: "mbox_priorbox" - type: "Concat" - bottom: "conv4_3_norm_mbox_priorbox" - bottom: "fc7_mbox_priorbox" - bottom: "conv6_2_mbox_priorbox" - bottom: "conv7_2_mbox_priorbox" - bottom: "conv8_2_mbox_priorbox" - bottom: "conv9_2_mbox_priorbox" - top: "mbox_priorbox" - concat_param { - axis: 2 - } -} - -layer { - name: "mbox_conf_reshape" - type: "Reshape" - bottom: "mbox_conf" - top: "mbox_conf_reshape" - reshape_param { - shape { - dim: 0 - dim: -1 - dim: 2 - } - } -} -layer { - name: "mbox_conf_softmax" - type: "Softmax" - bottom: "mbox_conf_reshape" - top: "mbox_conf_softmax" - softmax_param { - axis: 2 - } -} -layer { - name: "mbox_conf_flatten" - type: "Flatten" - bottom: "mbox_conf_softmax" - top: "mbox_conf_flatten" - flatten_param { - axis: 1 - } -} - -layer { - name: "detection_out" - type: "DetectionOutput" - bottom: "mbox_loc" - bottom: "mbox_conf_flatten" - bottom: "mbox_priorbox" - top: "detection_out" - include { - phase: TEST - } - detection_output_param { - num_classes: 2 - share_location: true - background_label_id: 0 - nms_param { - nms_threshold: 0.45 - top_k: 400 - } - code_type: CENTER_SIZE - keep_top_k: 200 - confidence_threshold: 0.01 - } -} diff --git a/tests/extras/demoapp/dnn_files/res10_300x300_ssd_iter_140000.caffemodel b/tests/extras/demoapp/dnn_files/res10_300x300_ssd_iter_140000.caffemodel deleted file mode 100644 index 809dfd78..00000000 Binary files a/tests/extras/demoapp/dnn_files/res10_300x300_ssd_iter_140000.caffemodel and /dev/null differ diff --git a/tests/extras/testutils/factories/api.py b/tests/extras/testutils/factories/api.py index 98ddf2b8..c6972941 100644 --- a/tests/extras/testutils/factories/api.py +++ b/tests/extras/testutils/factories/api.py @@ -1,21 +1,24 @@ -from factory import Factory, Faker, LazyFunction, SubFactory, fuzzy, post_generation +from uuid import uuid4 + +from factory import Factory, SubFactory, fuzzy, lazy_attribute from factory.django import DjangoModelFactory from testutils.factories import ExternalSystemFactory, UserFactory from hope_dedup_engine.apps.api.deduplication.config import ( - ConfigDefaults, - DetectionConfig, - DuplicatesConfig, - RecognitionConfig, + DeduplicateOptions, + DeduplicationSetConfig, + EncodingOptions, + ModelOptions, ) from hope_dedup_engine.apps.api.models import DedupJob, DeduplicationSet, HDEToken from hope_dedup_engine.apps.api.models.config import Config from hope_dedup_engine.apps.api.models.deduplication import ( - Duplicate, + Finding, IgnoredFilenamePair, IgnoredReferencePkPair, Image, ) +from hope_dedup_engine.constants import FacialError class TokenFactory(DjangoModelFactory): @@ -27,23 +30,11 @@ class Meta: class ConfigFactory(DjangoModelFactory): name = fuzzy.FuzzyText() + settings = {} class Meta: model = Config - @post_generation - def settings(self, create, extracted, **kwargs): - self.settings = { - "detection": {"confidence": fuzzy.FuzzyFloat(0.1, 1.0).fuzz()}, - "duplicates": {"tolerance": fuzzy.FuzzyFloat(0.1, 1.0).fuzz()}, - "recognition": { - "model": fuzzy.FuzzyChoice(["small", "large"]).fuzz(), - "num_jitters": fuzzy.FuzzyInteger(1, 10).fuzz(), - }, - } - if create: - self.save() - class DeduplicationSetFactory(DjangoModelFactory): reference_pk = fuzzy.FuzzyText() @@ -65,14 +56,32 @@ class Meta: model = Image -class DuplicateFactory(DjangoModelFactory): +class FindingFactory(DjangoModelFactory): + class Meta: + model = Finding + django_get_or_create = ( + "deduplication_set", + "first_reference_pk", + "second_reference_pk", + ) + deduplication_set = SubFactory(DeduplicationSetFactory) first_reference_pk = fuzzy.FuzzyText() - second_reference_pk = fuzzy.FuzzyText() score = fuzzy.FuzzyFloat(low=0, high=1) - class Meta: - model = Duplicate + @lazy_attribute + def error(self): + return ( + fuzzy.FuzzyChoice(list(FacialError)).fuzz().value + if self.score == 0 + else None + ) + + @lazy_attribute + def second_reference_pk(self): + if self.error is not None: + return FacialError(self.error).name + return fuzzy.FuzzyText() class IgnoredFilenamePairFactory(DjangoModelFactory): @@ -100,43 +109,31 @@ class Meta: model = DedupJob -class DetectionConfigFactory(Factory): +class ModelOptionsFactory(Factory): class Meta: - model = DetectionConfig + model = ModelOptions - dnn_files_source = Faker("word") - dnn_backend = fuzzy.FuzzyInteger(0, 5) - dnn_target = fuzzy.FuzzyInteger(0, 5) - blob_from_image_scale_factor = fuzzy.FuzzyFloat(0.5, 1.5) - blob_from_image_mean_values = LazyFunction(lambda: (104.0, 177.0, 123.0)) - confidence = fuzzy.FuzzyFloat(0.1, 1.0) - nms_threshold = fuzzy.FuzzyFloat(0.1, 1.0) + model_name = fuzzy.FuzzyChoice(["model1", "model2"]) + detector_backend = fuzzy.FuzzyChoice(["backend1", "backend2"]) -class RecognitionConfigFactory(Factory): +class EncodingOptionsFactory(ModelOptionsFactory): class Meta: - model = RecognitionConfig + model = EncodingOptions - num_jitters = fuzzy.FuzzyInteger(0, 5) - model = fuzzy.FuzzyChoice(["small", "large"]) - preprocessors = [] - -class DuplicatesConfigFactory(Factory): +class DeduplicateOptionsFactory(ModelOptionsFactory): class Meta: - model = DuplicatesConfig + model = DeduplicateOptions - tolerance = fuzzy.FuzzyFloat(0.1, 1.0) + threshold = fuzzy.FuzzyFloat(0.1, 1.0) + silent = fuzzy.FuzzyChoice([True, False]) -class ConfigDefaultsFactory(Factory): +class DeduplicationSetConfigFactory(Factory): class Meta: - model = ConfigDefaults - - detection = SubFactory(DetectionConfigFactory) - recognition = SubFactory(RecognitionConfigFactory) - duplicates = SubFactory(DuplicatesConfigFactory) + model = DeduplicationSetConfig - # @post_generation - # def apply_overrides(self, create, extracted, **kwargs): - # self.apply_config_overrides(extracted) + deduplication_set_id = uuid4() + encoding = SubFactory(EncodingOptionsFactory) + deduplicate = SubFactory(DeduplicateOptionsFactory) diff --git a/tests/faces/conftest.py b/tests/faces/_conftst.py similarity index 100% rename from tests/faces/conftest.py rename to tests/faces/_conftst.py diff --git a/tests/faces/faces_const.py b/tests/faces/_faces_const.py similarity index 100% rename from tests/faces/faces_const.py rename to tests/faces/_faces_const.py diff --git a/tests/faces/test_celery_tasks.py b/tests/faces/_tst_celery_tasks.py similarity index 99% rename from tests/faces/test_celery_tasks.py rename to tests/faces/_tst_celery_tasks.py index 5c926eee..1641039e 100644 --- a/tests/faces/test_celery_tasks.py +++ b/tests/faces/_tst_celery_tasks.py @@ -5,7 +5,10 @@ from celery import states from celery.exceptions import SoftTimeLimitExceeded, TimeLimitExceeded from constance import test -from faces_const import ( + +from hope_dedup_engine.apps.faces.celery_tasks import deduplicate, sync_dnn_files +from hope_dedup_engine.apps.faces.utils.celery_utils import _get_hash +from tests.faces._faces_const import ( CELERY_TASK_DELAYS, CELERY_TASK_NAME, CELERY_TASK_TTL, @@ -13,9 +16,6 @@ IGNORE_PAIRS, ) -from hope_dedup_engine.apps.faces.celery_tasks import deduplicate, sync_dnn_files -from hope_dedup_engine.apps.faces.utils.celery_utils import _get_hash - @pytest.mark.parametrize("lock_is_acquired", [True, False]) def test_deduplicate_task_locking( diff --git a/tests/faces/test_downloader.py b/tests/faces/_tst_downloader.py similarity index 98% rename from tests/faces/test_downloader.py rename to tests/faces/_tst_downloader.py index aa135896..cc1189bd 100644 --- a/tests/faces/test_downloader.py +++ b/tests/faces/_tst_downloader.py @@ -1,7 +1,6 @@ from unittest.mock import mock_open, patch import pytest -from faces_const import DNN_FILE from requests.exceptions import RequestException from hope_dedup_engine.apps.faces.managers import FileSyncManager @@ -10,6 +9,7 @@ FileDownloader, GithubFileDownloader, ) +from tests.faces._faces_const import DNN_FILE def test_github_sync_success(github_dnn_file_downloader, mock_requests_get): diff --git a/tests/faces/test_duplication_detector.py b/tests/faces/_tst_duplication_detector.py similarity index 99% rename from tests/faces/test_duplication_detector.py rename to tests/faces/_tst_duplication_detector.py index f8c36333..0bf75de8 100644 --- a/tests/faces/test_duplication_detector.py +++ b/tests/faces/_tst_duplication_detector.py @@ -6,11 +6,11 @@ import numpy as np import pytest -from faces_const import FILENAME, FILENAME_ENCODED_FORMAT, FILENAMES from hope_dedup_engine.apps.faces.managers import StorageManager from hope_dedup_engine.apps.faces.services import DuplicationDetector from hope_dedup_engine.apps.faces.services.image_processor import ImageProcessor +from tests.faces._faces_const import FILENAME, FILENAME_ENCODED_FORMAT, FILENAMES def test_init_successful(mock_dd, mock_config_defaults): diff --git a/tests/faces/test_file_sync.py b/tests/faces/_tst_file_sync.py similarity index 100% rename from tests/faces/test_file_sync.py rename to tests/faces/_tst_file_sync.py diff --git a/tests/faces/test_image_processor.py b/tests/faces/_tst_image_processor.py similarity index 98% rename from tests/faces/test_image_processor.py rename to tests/faces/_tst_image_processor.py index cb84058d..766deb72 100644 --- a/tests/faces/test_image_processor.py +++ b/tests/faces/_tst_image_processor.py @@ -5,7 +5,6 @@ import face_recognition import numpy as np import pytest -from faces_const import DEPLOY_PROTO_SHAPE, FILENAME, FILENAME_ENCODED from hope_dedup_engine.apps.api.deduplication.config import ( DetectionConfig, @@ -13,6 +12,7 @@ ) from hope_dedup_engine.apps.faces.managers import DNNInferenceManager, StorageManager from hope_dedup_engine.apps.faces.services.image_processor import BlobFromImageConfig +from tests.faces._faces_const import DEPLOY_PROTO_SHAPE, FILENAME, FILENAME_ENCODED def test_init_successful( diff --git a/tests/faces/test_net_manager.py b/tests/faces/_tst_net_manager.py similarity index 100% rename from tests/faces/test_net_manager.py rename to tests/faces/_tst_net_manager.py diff --git a/tests/faces/test_storage_manager.py b/tests/faces/_tst_storage_manager.py similarity index 100% rename from tests/faces/test_storage_manager.py rename to tests/faces/_tst_storage_manager.py diff --git a/tests/test_command_demo.py b/tests/test_command_demo.py index 56315df3..fab41094 100644 --- a/tests/test_command_demo.py +++ b/tests/test_command_demo.py @@ -12,7 +12,6 @@ def environment(): return { "DEMO_IMAGES_PATH": "demo_images", - "DNN_FILES_PATH": "dnn_files", } @@ -32,13 +31,10 @@ def test_demo_handle_success(environment, mock_azurite_manager): call_command( "demo", demo_images="/path/to/demo/images", - dnn_files="/path/to/dnn/files", stdout=out, ) assert "error" not in str(out.getvalue()) assert "SYSTEM HALTED" not in out.getvalue() - assert mock_azurite_manager.call_count == 4 - assert mock_azurite_manager.return_value.upload_files.call_count == 2 @pytest.mark.parametrize( diff --git a/tests/test_command_dnnsetup.py b/tests/test_command_dnnsetup.py deleted file mode 100644 index a55baeae..00000000 --- a/tests/test_command_dnnsetup.py +++ /dev/null @@ -1,90 +0,0 @@ -from io import StringIO -from typing import Final -from unittest import mock - -from django.core.exceptions import ValidationError -from django.core.management import call_command -from django.core.management.base import CommandError, SystemCheckError - -import pytest -from pytest_mock import MockerFixture - -DNN_FILES: Final[tuple[dict[str, str]]] = ( - {"url": "http://example.com/file1", "filename": "file1"}, - {"url": "http://example.com/file2", "filename": "file2"}, -) - - -@pytest.fixture -def mock_requests_get(): - with mock.patch("requests.get") as mock_get: - mock_response = mock_get.return_value.__enter__.return_value - mock_response.iter_content.return_value = [b"Hello, world!"] * 3 - mock_response.raise_for_status = lambda: None - yield mock_get - - -@pytest.fixture -def mock_azurite_manager(mocker: MockerFixture): - yield mocker.patch( - "hope_dedup_engine.apps.core.management.commands.dnnsetup.AzureStorage", - ) - - -@pytest.fixture -def mock_dnn_files(mocker: MockerFixture): - yield mocker.patch( - "hope_dedup_engine.apps.core.management.commands.dnnsetup.Command.dnn_files", - new_callable=mocker.PropertyMock, - ) - - -@pytest.mark.parametrize( - "force, expected_count, existing_files", - [ - (False, 2, []), - (False, 1, [DNN_FILES[0]["filename"]]), - (False, 0, [f["filename"] for f in DNN_FILES][:2]), - (True, 2, []), - (True, 2, [DNN_FILES[0]["filename"]]), - (True, 2, [f["filename"] for f in DNN_FILES][:2]), - ], -) -def test_dnnsetup_handle_success( - mock_requests_get, - mock_azurite_manager, - mock_dnn_files, - force, - expected_count, - existing_files, -): - mock_dnn_files.return_value = DNN_FILES - mock_azurite_manager().listdir.return_value = ([], existing_files) - out = StringIO() - - call_command("dnnsetup", stdout=out, force=force) - - assert "SYSTEM HALTED" not in out.getvalue() - assert mock_requests_get.call_count == expected_count - assert mock_azurite_manager().open.call_count == expected_count - - -@pytest.mark.parametrize( - "side_effect, expected_exception", - [ - (FileNotFoundError("File not found"), SystemExit), - (ValidationError("Invalid argument"), SystemExit), - (CommandError("Command execution failed"), SystemExit), - (SystemCheckError("System check failed"), SystemExit), - (Exception("Unknown error"), SystemExit), - ], -) -def test_dnnsetup_handle_exception( - mock_requests_get, mock_azurite_manager, side_effect, expected_exception -): - mock_azurite_manager.side_effect = side_effect - out = StringIO() - with pytest.raises(expected_exception): - call_command("dnnsetup", stdout=out) - - assert "SYSTEM HALTED" in out.getvalue() diff --git a/tests/test_commands.py b/tests/test_commands.py index ccff3164..da4bfd46 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -27,6 +27,7 @@ def environment(): "SECURE_SSL_REDIRECT": "1", "SESSION_COOKIE_SECURE": "1", "DJANGO_SETTINGS_MODULE": "hope_dedup_engine.config.settings", + "DEEPFACE_HOME": "/tmp/deepface", } @@ -34,7 +35,6 @@ def environment(): def mock_settings(): with mock.patch("django.conf.settings") as mock_settings: mock_settings.AZURE_CONTAINER_HOPE = "hope-container" - mock_settings.AZURE_CONTAINER_DNN = "dnn-container" mock_settings.AZURE_CONTAINER_HDE = "hde-container" yield mock_settings @@ -63,7 +63,7 @@ def test_upgrade_init( migrate=migrate, stdout=out, check=False, - dnn_setup=False, + sync_models=False, verbosity=verbosity, ) assert "error" not in str(out.getvalue()) @@ -81,7 +81,7 @@ def test_upgrade(verbosity, migrate, monkeypatch, environment): "upgrade", stdout=out, check=False, - dnn_setup=False, + sync_models=False, verbosity=verbosity, ) assert "error" not in str(out.getvalue()) @@ -113,7 +113,7 @@ def test_upgrade_admin(db, mocked_responses, environment, admin): "upgrade", stdout=out, check=False, - dnn_setup=False, + sync_models=False, static=False, admin_email=email, ) diff --git a/uv.lock b/uv.lock index 2ae35fa9..a7941a16 100644 --- a/uv.lock +++ b/uv.lock @@ -2,11 +2,20 @@ version = 1 requires-python = ">=3.12" resolution-markers = [ "python_full_version < '3.13' and platform_system == 'Darwin'", - "python_full_version < '3.13' and platform_machine == 'aarch64' and platform_system == 'Linux'", - "(python_full_version < '3.13' and platform_machine != 'aarch64' and platform_system != 'Darwin') or (python_full_version < '3.13' and platform_system != 'Darwin' and platform_system != 'Linux')", "python_full_version >= '3.13' and platform_system == 'Darwin'", + "python_full_version < '3.13' and platform_machine == 'aarch64' and platform_system == 'Linux'", "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_system == 'Linux'", - "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_system != 'Darwin') or (python_full_version >= '3.13' and platform_system != 'Darwin' and platform_system != 'Linux')", + "(python_full_version < '3.13' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version < '3.13' and platform_system != 'Darwin' and platform_system != 'Linux')", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_system == 'Linux') or (python_full_version >= '3.13' and platform_system != 'Darwin' and platform_system != 'Linux')", +] + +[[package]] +name = "absl-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/8f/fc001b92ecc467cc32ab38398bd0bfb45df46e7523bf33c2ad22a505f06e/absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff", size = 118055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/ad/e0d3c824784ff121c03cc031f944bc7e139a8f1870ffd2845cc2dd76f6c4/absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308", size = 133706 }, ] [[package]] @@ -39,13 +48,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732 }, +] + [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, ] [[package]] @@ -131,6 +153,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + [[package]] name = "bracex" version = "2.5.post1" @@ -167,11 +198,11 @@ redis = [ [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, ] [[package]] @@ -315,40 +346,40 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/ce/3edf581c8fe429ed8ced6e6d9ac693c25975ef9093413276dab6ed68a80a/coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", size = 207285 }, - { url = "https://files.pythonhosted.org/packages/09/9c/cf102ab046c9cf8895c3f7aadcde6f489a4b2ec326757e8c6e6581829b5e/coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", size = 207522 }, - { url = "https://files.pythonhosted.org/packages/39/06/42aa6dd13dbfca72e1fd8ffccadbc921b6e75db34545ebab4d955d1e7ad3/coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", size = 240543 }, - { url = "https://files.pythonhosted.org/packages/a0/20/2932971dc215adeca8eeff446266a7fef17a0c238e881ffedebe7bfa0669/coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", size = 237577 }, - { url = "https://files.pythonhosted.org/packages/ac/85/4323ece0cd5452c9522f4b6e5cc461e6c7149a4b1887c9e7a8b1f4e51146/coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", size = 239646 }, - { url = "https://files.pythonhosted.org/packages/77/52/b2537487d8f36241e518e84db6f79e26bc3343b14844366e35b090fae0d4/coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", size = 239128 }, - { url = "https://files.pythonhosted.org/packages/7c/99/7f007762012186547d0ecc3d328da6b6f31a8c99f05dc1e13dcd929918cd/coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", size = 237434 }, - { url = "https://files.pythonhosted.org/packages/97/53/e9b5cf0682a1cab9352adfac73caae0d77ae1d65abc88975d510f7816389/coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", size = 239095 }, - { url = "https://files.pythonhosted.org/packages/0c/50/054f0b464fbae0483217186478eefa2e7df3a79917ed7f1d430b6da2cf0d/coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", size = 209895 }, - { url = "https://files.pythonhosted.org/packages/df/d0/09ba870360a27ecf09e177ca2ff59d4337fc7197b456f22ceff85cffcfa5/coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", size = 210684 }, - { url = "https://files.pythonhosted.org/packages/9a/84/6f0ccf94a098ac3d6d6f236bd3905eeac049a9e0efcd9a63d4feca37ac4b/coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", size = 207313 }, - { url = "https://files.pythonhosted.org/packages/db/2b/e3b3a3a12ebec738c545897ac9f314620470fcbc368cdac88cf14974ba20/coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", size = 207574 }, - { url = "https://files.pythonhosted.org/packages/db/c0/5bf95d42b6a8d21dfce5025ce187f15db57d6460a59b67a95fe8728162f1/coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", size = 240090 }, - { url = "https://files.pythonhosted.org/packages/57/b8/d6fd17d1a8e2b0e1a4e8b9cb1f0f261afd422570735899759c0584236916/coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", size = 237237 }, - { url = "https://files.pythonhosted.org/packages/d4/e4/a91e9bb46809c8b63e68fc5db5c4d567d3423b6691d049a4f950e38fbe9d/coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", size = 239225 }, - { url = "https://files.pythonhosted.org/packages/31/9c/9b99b0591ec4555b7292d271e005f27b465388ce166056c435b288db6a69/coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", size = 238888 }, - { url = "https://files.pythonhosted.org/packages/a6/85/285c2df9a04bc7c31f21fd9d4a24d19e040ec5e2ff06e572af1f6514c9e7/coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", size = 236974 }, - { url = "https://files.pythonhosted.org/packages/cb/a1/95ec8522206f76cdca033bf8bb61fff56429fb414835fc4d34651dfd29fc/coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", size = 238815 }, - { url = "https://files.pythonhosted.org/packages/8d/ac/687e9ba5e6d0979e9dab5c02e01c4f24ac58260ef82d88d3b433b3f84f1e/coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", size = 209957 }, - { url = "https://files.pythonhosted.org/packages/2f/a3/b61cc8e3fcf075293fb0f3dee405748453c5ba28ac02ceb4a87f52bdb105/coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", size = 210711 }, - { url = "https://files.pythonhosted.org/packages/ee/4b/891c8b9acf1b62c85e4a71dac142ab9284e8347409b7355de02e3f38306f/coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", size = 208053 }, - { url = "https://files.pythonhosted.org/packages/18/a9/9e330409b291cc002723d339346452800e78df1ce50774ca439ade1d374f/coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", size = 208329 }, - { url = "https://files.pythonhosted.org/packages/9c/0d/33635fd429f6589c6e1cdfc7bf581aefe4c1792fbff06383f9d37f59db60/coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", size = 251052 }, - { url = "https://files.pythonhosted.org/packages/23/32/8a08da0e46f3830bbb9a5b40614241b2e700f27a9c2889f53122486443ed/coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", size = 246765 }, - { url = "https://files.pythonhosted.org/packages/56/3f/3b86303d2c14350fdb1c6c4dbf9bc76000af2382f42ca1d4d99c6317666e/coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", size = 249125 }, - { url = "https://files.pythonhosted.org/packages/36/cb/c4f081b9023f9fd8646dbc4ef77be0df090263e8f66f4ea47681e0dc2cff/coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", size = 248615 }, - { url = "https://files.pythonhosted.org/packages/32/ee/53bdbf67760928c44b57b2c28a8c0a4bf544f85a9ee129a63ba5c78fdee4/coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", size = 246507 }, - { url = "https://files.pythonhosted.org/packages/57/49/5a57910bd0af6d8e802b4ca65292576d19b54b49f81577fd898505dee075/coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", size = 247785 }, - { url = "https://files.pythonhosted.org/packages/bd/37/e450c9f6b297c79bb9858407396ed3e084dcc22990dd110ab01d5ceb9770/coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", size = 210605 }, - { url = "https://files.pythonhosted.org/packages/44/79/7d0c7dd237c6905018e2936cd1055fe1d42e7eba2ebab3c00f4aad2a27d7/coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", size = 211777 }, +version = "7.6.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/52/b16af8989a2daf0f80a88522bd8e8eed90b5fcbdecf02a6888f3e80f6ba7/coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", size = 207325 }, + { url = "https://files.pythonhosted.org/packages/0f/79/6b7826fca8846c1216a113227b9f114ac3e6eacf168b4adcad0cb974aaca/coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", size = 207563 }, + { url = "https://files.pythonhosted.org/packages/a7/07/0bc73da0ccaf45d0d64ef86d33b7d7fdeef84b4c44bf6b85fb12c215c5a6/coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", size = 240580 }, + { url = "https://files.pythonhosted.org/packages/71/8a/9761f409910961647d892454687cedbaccb99aae828f49486734a82ede6e/coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3", size = 237613 }, + { url = "https://files.pythonhosted.org/packages/8b/10/ee7d696a17ac94f32f2dbda1e17e730bf798ae9931aec1fc01c1944cd4de/coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", size = 239684 }, + { url = "https://files.pythonhosted.org/packages/16/60/aa1066040d3c52fff051243c2d6ccda264da72dc6d199d047624d395b2b2/coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", size = 239112 }, + { url = "https://files.pythonhosted.org/packages/4e/e5/69f35344c6f932ba9028bf168d14a79fedb0dd4849b796d43c81ce75a3c9/coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", size = 237428 }, + { url = "https://files.pythonhosted.org/packages/32/20/adc895523c4a28f63441b8ac645abd74f9bdd499d2d175bef5b41fc7f92d/coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", size = 239098 }, + { url = "https://files.pythonhosted.org/packages/a9/a6/e0e74230c9bb3549ec8ffc137cfd16ea5d56e993d6bffed2218bff6187e3/coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", size = 209940 }, + { url = "https://files.pythonhosted.org/packages/3e/18/cb5b88349d4aa2f41ec78d65f92ea32572b30b3f55bc2b70e87578b8f434/coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", size = 210726 }, + { url = "https://files.pythonhosted.org/packages/35/26/9abab6539d2191dbda2ce8c97b67d74cbfc966cc5b25abb880ffc7c459bc/coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", size = 207356 }, + { url = "https://files.pythonhosted.org/packages/44/da/d49f19402240c93453f606e660a6676a2a1fbbaa6870cc23207790aa9697/coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", size = 207614 }, + { url = "https://files.pythonhosted.org/packages/da/e6/93bb9bf85497816082ec8da6124c25efa2052bd4c887dd3b317b91990c9e/coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", size = 240129 }, + { url = "https://files.pythonhosted.org/packages/df/65/6a824b9406fe066835c1274a9949e06f084d3e605eb1a602727a27ec2fe3/coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", size = 237276 }, + { url = "https://files.pythonhosted.org/packages/9f/79/6c7a800913a9dd23ac8c8da133ebb556771a5a3d4df36b46767b1baffd35/coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", size = 239267 }, + { url = "https://files.pythonhosted.org/packages/57/e7/834d530293fdc8a63ba8ff70033d5182022e569eceb9aec7fc716b678a39/coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", size = 238887 }, + { url = "https://files.pythonhosted.org/packages/15/05/ec9d6080852984f7163c96984444e7cd98b338fd045b191064f943ee1c08/coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", size = 236970 }, + { url = "https://files.pythonhosted.org/packages/0a/d8/775937670b93156aec29f694ce37f56214ed7597e1a75b4083ee4c32121c/coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", size = 238831 }, + { url = "https://files.pythonhosted.org/packages/f4/58/88551cb7fdd5ec98cb6044e8814e38583436b14040a5ece15349c44c8f7c/coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", size = 210000 }, + { url = "https://files.pythonhosted.org/packages/b7/12/cfbf49b95120872785ff8d56ab1c7fe3970a65e35010c311d7dd35c5fd00/coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", size = 210753 }, + { url = "https://files.pythonhosted.org/packages/7c/68/c1cb31445599b04bde21cbbaa6d21b47c5823cdfef99eae470dfce49c35a/coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/11/73/84b02c6b19c4a11eb2d5b5eabe926fb26c21c080e0852f5e5a4f01165f9e/coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", size = 208369 }, + { url = "https://files.pythonhosted.org/packages/de/e0/ae5d878b72ff26df2e994a5c5b1c1f6a7507d976b23beecb1ed4c85411ef/coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", size = 251089 }, + { url = "https://files.pythonhosted.org/packages/ab/9c/0aaac011aef95a93ef3cb2fba3fde30bc7e68a6635199ed469b1f5ea355a/coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", size = 246806 }, + { url = "https://files.pythonhosted.org/packages/f8/19/4d5d3ae66938a7dcb2f58cef3fa5386f838f469575b0bb568c8cc9e3a33d/coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", size = 249164 }, + { url = "https://files.pythonhosted.org/packages/b3/0b/4ee8a7821f682af9ad440ae3c1e379da89a998883271f088102d7ca2473d/coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", size = 248642 }, + { url = "https://files.pythonhosted.org/packages/8a/12/36ff1d52be18a16b4700f561852e7afd8df56363a5edcfb04cf26a0e19e0/coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", size = 246516 }, + { url = "https://files.pythonhosted.org/packages/43/d0/8e258f6c3a527c1655602f4f576215e055ac704de2d101710a71a2affac2/coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", size = 247783 }, + { url = "https://files.pythonhosted.org/packages/a9/0d/1e4a48d289429d38aae3babdfcadbf35ca36bdcf3efc8f09b550a845bdb5/coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", size = 210646 }, + { url = "https://files.pythonhosted.org/packages/26/74/b0729f196f328ac55e42b1e22ec2f16d8bcafe4b8158a26ec9f1cdd1d93e/coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", size = 211815 }, ] [[package]] @@ -375,7 +406,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, - { url = "https://files.pythonhosted.org/packages/4e/d5/9cc182bf24c86f542129565976c21301d4ac397e74bf5a16e48241aab8a6/cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385", size = 4164756 }, { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, @@ -386,7 +416,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, - { url = "https://files.pythonhosted.org/packages/31/d9/90409720277f88eb3ab72f9a32bfa54acdd97e94225df699e7713e850bd4/cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba", size = 4165207 }, { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, @@ -395,19 +424,19 @@ wheels = [ [[package]] name = "debugpy" -version = "1.8.9" +version = "1.8.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/92/15b454c516c4c53cc8c03967e4be12b65a1ea36db3bb4513a7453f75c8d8/debugpy-1.8.9.zip", hash = "sha256:1339e14c7d980407248f09824d1b25ff5c5616651689f1e0f0e51bdead3ea13e", size = 4921695 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/e7/666f4c9b0e24796af50aadc28d36d21c2e01e831a934535f956e09b3650c/debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57", size = 1640124 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ab/1420baf8404d2b499349a44de5037133e06d489009032ce016fedb66eea1/debugpy-1.8.9-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:957363d9a7a6612a37458d9a15e72d03a635047f946e5fceee74b50d52a9c8e2", size = 2504180 }, - { url = "https://files.pythonhosted.org/packages/58/ec/e0f88c6764314bda7887274e0b980812709b3d6363dcae124a49a9ceaa3c/debugpy-1.8.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e565fc54b680292b418bb809f1386f17081d1346dca9a871bf69a8ac4071afe", size = 4224563 }, - { url = "https://files.pythonhosted.org/packages/dd/49/d9ea004ee2e4531d2b528841689ee2ba01c6a4b58840efd15e57dd866a86/debugpy-1.8.9-cp312-cp312-win32.whl", hash = "sha256:3e59842d6c4569c65ceb3751075ff8d7e6a6ada209ceca6308c9bde932bcef11", size = 5163641 }, - { url = "https://files.pythonhosted.org/packages/b1/63/c8b0718024c1187a446316037680e1564bf063c6665c815f17b42c244aba/debugpy-1.8.9-cp312-cp312-win_amd64.whl", hash = "sha256:66eeae42f3137eb428ea3a86d4a55f28da9bd5a4a3d369ba95ecc3a92c1bba53", size = 5203862 }, - { url = "https://files.pythonhosted.org/packages/cc/8d/eb12dcb977a2d166aac6614e60daddd1eef72881a0343717d7deb0d4868c/debugpy-1.8.9-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:957ecffff80d47cafa9b6545de9e016ae8c9547c98a538ee96ab5947115fb3dd", size = 2489077 }, - { url = "https://files.pythonhosted.org/packages/87/2b/3b7a00d8d2bb891cfa33240575c2d5fc3fa6e0bc75567f4ece59b9d3d6ea/debugpy-1.8.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1efbb3ff61487e2c16b3e033bc8595aea578222c08aaf3c4bf0f93fadbd662ee", size = 4219198 }, - { url = "https://files.pythonhosted.org/packages/5f/a1/f489026a65fabfff8c73bd51b880c130d636e02b1847564141fe3957d94f/debugpy-1.8.9-cp313-cp313-win32.whl", hash = "sha256:7c4d65d03bee875bcb211c76c1d8f10f600c305dbd734beaed4077e902606fee", size = 5163014 }, - { url = "https://files.pythonhosted.org/packages/e6/84/6070908dd163121358eb9d76fcc94f05bc99d2f89a85fe1b86167bc34ec6/debugpy-1.8.9-cp313-cp313-win_amd64.whl", hash = "sha256:e46b420dc1bea64e5bbedd678148be512442bc589b0111bd799367cde051e71a", size = 5203529 }, - { url = "https://files.pythonhosted.org/packages/2d/23/3f5804202da11c950dc0caae4a62d0c9aadabdb2daeb5f7aa09838647b5d/debugpy-1.8.9-py2.py3-none-any.whl", hash = "sha256:cc37a6c9987ad743d9c3a14fa1b1a14b7e4e6041f9dd0c8abf8895fe7a97b899", size = 5166094 }, + { url = "https://files.pythonhosted.org/packages/c6/ae/2cf26f3111e9d94384d9c01e9d6170188b0aeda15b60a4ac6457f7c8a26f/debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308", size = 2498756 }, + { url = "https://files.pythonhosted.org/packages/b0/16/ec551789d547541a46831a19aa15c147741133da188e7e6acf77510545a7/debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768", size = 4219136 }, + { url = "https://files.pythonhosted.org/packages/72/6f/b2b3ce673c55f882d27a6eb04a5f0c68bcad6b742ac08a86d8392ae58030/debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b", size = 5224440 }, + { url = "https://files.pythonhosted.org/packages/77/09/b1f05be802c1caef5b3efc042fc6a7cadd13d8118b072afd04a9b9e91e06/debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1", size = 5264578 }, + { url = "https://files.pythonhosted.org/packages/2e/66/931dc2479aa8fbf362dc6dcee707d895a84b0b2d7b64020135f20b8db1ed/debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3", size = 2483651 }, + { url = "https://files.pythonhosted.org/packages/10/07/6c171d0fe6b8d237e35598b742f20ba062511b3a4631938cc78eefbbf847/debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e", size = 4213770 }, + { url = "https://files.pythonhosted.org/packages/89/f1/0711da6ac250d4fe3bf7b3e9b14b4a86e82a98b7825075c07e19bab8da3d/debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28", size = 5223911 }, + { url = "https://files.pythonhosted.org/packages/56/98/5e27fa39050749ed460025bcd0034a0a5e78a580a14079b164cc3abdeb98/debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1", size = 5264166 }, + { url = "https://files.pythonhosted.org/packages/77/0a/d29a5aacf47b4383ed569b8478c02d59ee3a01ad91224d2cff8562410e43/debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920", size = 5226874 }, ] [[package]] @@ -419,6 +448,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, ] +[[package]] +name = "deepface" +version = "0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fire" }, + { name = "flask" }, + { name = "flask-cors" }, + { name = "gdown" }, + { name = "gunicorn" }, + { name = "keras" }, + { name = "mtcnn" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "requests" }, + { name = "retina-face" }, + { name = "tensorflow" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/95/76b2be896cff9d51aa7a7eb77bcca295f362bd32c121170c2210206404d9/deepface-0.0.93.tar.gz", hash = "sha256:7f5fc6306a3a07ee6c529b03571e64fe53d9f259e1d4091f5e28386264962b92", size = 80702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/f6/4fa3f64b1a02141c037ed71a40ebf8fb8cc1ec9e860df6301fc9121bc0d4/deepface-0.0.93-py3-none-any.whl", hash = "sha256:27043e1aa5df05a060bcfe1743409075c66f6f1de86a592ba0cdac79ac9e7987", size = 108586 }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -439,16 +494,16 @@ wheels = [ [[package]] name = "django" -version = "5.1.3" +version = "5.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "tzdata", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/85/ba2c2b83ba8b95354f99ed8344405d9571109ce0175028876209d6b93fba/Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a", size = 10698518 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/e8/536555596dbb79f6e77418aeb40bdc1758c26725aba31919ba449e6d5e6a/Django-5.1.4.tar.gz", hash = "sha256:de450c09e91879fa5a307f696e57c851955c910a438a35e6b4c895e86bedc82a", size = 10716397 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/f6/88ed57e1b3ed54ff18c1da352aecbd6f51784c3e642d97586b61f050f5b1/Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818", size = 8276180 }, + { url = "https://files.pythonhosted.org/packages/58/0b/8a4ab2c02982df4ed41e29f28f189459a7eba37899438e6bea7f39db793b/Django-5.1.4-py3-none-any.whl", hash = "sha256:236e023f021f5ce7dee5779de7b286565fdea5f4ab86bae5338e3f7b69896cf0", size = 8276471 }, ] [[package]] @@ -473,11 +528,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/a2/63/4428328cc0ca60ee0 [[package]] name = "django-adminfilters" -version = "2.5.1" +version = "2.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/5d/a8f22bedd8d17ca0b6fa90bac5492840f16fdd7d463f4e81c68dab4a5cb9/django_adminfilters-2.5.1.tar.gz", hash = "sha256:affaf0614a1939ad523f7a5ee1427f929f18f0df3a61b63d92eab6bdfcfab3a2", size = 57830 } +sdist = { url = "https://files.pythonhosted.org/packages/43/e6/bcf3341161b2d363281d0ddb9924ce27e31f7e8b370564b63c7f200e398c/django_adminfilters-2.5.2.tar.gz", hash = "sha256:2d4982490631cf198734e83337280ca831d5f559995198843103b30202104a29", size = 58865 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/4e/af6b7a97a570acea30bb446ba58794336df44d351069918460e10d6def69/django_adminfilters-2.5.1-py2.py3-none-any.whl", hash = "sha256:f568f3009886d74463c0aef7d54acd10bb2c6374e4ce2be1f8642695dcdc8e53", size = 47514 }, + { url = "https://files.pythonhosted.org/packages/f0/eb/2965d0ae94edc46e8a3ec0328c3ec71f580afd57084a785a8e99d39c0a55/django_adminfilters-2.5.2-py2.py3-none-any.whl", hash = "sha256:c1f19c8215b4573159359eaa1231ecdb5f5d9edbfca93f44e4dce27636630596", size = 49246 }, ] [[package]] @@ -719,18 +774,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 }, ] -[[package]] -name = "dlib" -version = "19.24.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/07/a2693a94ac678c442c4cfe269d24f63053b14410ef2d09957a762eeb4b8e/dlib-19.24.6.tar.gz", hash = "sha256:77e3c28ac2c66141514b07cbb74b7c7f80381c019ce5fec99007980bc6490d7d", size = 3374495 } - [[package]] name = "docker" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "pywin32", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "requests" }, { name = "urllib3" }, ] @@ -813,28 +862,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, ] -[[package]] -name = "face-recognition" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "dlib" }, - { name = "face-recognition-models" }, - { name = "numpy" }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/49/75dda409b94841f01cbbc34114c9b67ec618265084e4d12d37ab838f4fd3/face_recognition-1.3.0.tar.gz", hash = "sha256:5e5efdd1686aa566af0d3cc1313b131e4b197657a8ffd03669e6d3fad92705ec", size = 3182249 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/95/f6c9330f54ab07bfa032bf3715c12455a381083125d8880c43cbe76bb3d0/face_recognition-1.3.0-py2.py3-none-any.whl", hash = "sha256:c543e91c8cfbf24d19db04e511ebbddcb23894bcee510133729ee78e9f4b5e83", size = 15508 }, -] - -[[package]] -name = "face-recognition-models" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/3b/4fd8c534f6c0d1b80ce0973d01331525538045084c73c153ee6df20224cf/face_recognition_models-0.3.0.tar.gz", hash = "sha256:b79bd200a88c87c9a9d446c990ae71c5a626d1f3730174e6d570157ff1d896cf", size = 100149358 } - [[package]] name = "factory-boy" version = "3.3.1" @@ -882,6 +909,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, ] +[[package]] +name = "fire" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz", hash = "sha256:961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf", size = 87189 } + [[package]] name = "flake8" version = "7.1.1" @@ -898,15 +934,15 @@ wheels = [ [[package]] name = "flake8-bugbear" -version = "24.10.31" +version = "24.12.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "flake8" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/1b/5bbbe8646598790f137b40efa74c3bdfb36a46f0a9d524c9ad038452f2c4/flake8_bugbear-24.10.31.tar.gz", hash = "sha256:435b531c72b27f8eff8d990419697956b9fd25c6463c5ba98b3991591de439db", size = 80464 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/25/48ba712ff589b0149f21135234f9bb45c14d6689acc6151b5e2ff8ac2ae9/flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64", size = 82907 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/83/07ed6b341641362ebad020f49cc61444bc0b9d98b3b87bd6d2965ff70da3/flake8_bugbear-24.10.31-py3-none-any.whl", hash = "sha256:cccf786ccf9b2e1052b1ecfa80fb8f80832d0880425bcbd4cd45d3c8128c2683", size = 35979 }, + { url = "https://files.pythonhosted.org/packages/b9/21/0a875f75fbe4008bd171e2fefa413536258fe6b4cfaaa087986de74588f4/flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e", size = 36664 }, ] [[package]] @@ -936,6 +972,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/b0/bfd58118e2b9c1d0c5ac156c458c9a33cb599925b658df3ab038b55d30d7/flake8_html-0.4.3-py2.py3-none-any.whl", hash = "sha256:8f126748b1b0edd6cd39e87c6192df56e2f8655b0aa2bb00ffeac8cf27be4325", size = 13120 }, ] +[[package]] +name = "flask" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 }, +] + +[[package]] +name = "flask-cors" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/d0/d9e52b154e603b0faccc0b7c2ad36a764d8755ef4036acbf1582a67fb86b/flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef", size = 30954 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/07/1afa0514c876282bebc1c9aee83c6bb98fe6415cf57b88d9b06e7e29bf9c/Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc", size = 14463 }, +] + +[[package]] +name = "flatbuffers" +version = "24.3.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/74/2df95ef84b214d2bee0886d572775a6f38793f5ca6d7630c3239c91104ac/flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4", size = 22139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/f0/7e988a019bc54b2dbd0ad4182ef2d53488bb02e58694cd79d61369e85900/flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812", size = 26784 }, +] + [[package]] name = "flower" version = "2.0.1" @@ -964,6 +1037,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569 }, ] +[[package]] +name = "gast" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/14/c566f5ca00c115db7725263408ff952b8ae6d6a4e792ef9c84e77d9af7a1/gast-0.6.0.tar.gz", hash = "sha256:88fc5300d32c7ac6ca7b515310862f71e6fdf2c029bbec7c66c0f5dd47b6b1fb", size = 27708 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/61/8001b38461d751cd1a0c3a6ae84346796a5758123f3ed97a1b121dfbf4f3/gast-0.6.0-py3-none-any.whl", hash = "sha256:52b182313f7330389f72b069ba00f174cfe2a06411099547288839c6cbafbd54", size = 21173 }, +] + +[[package]] +name = "gdown" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "filelock" }, + { name = "requests", extra = ["socks"] }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/6a/37e6b70c5bda3161e40265861e63b64a86bfc6ca6a8f1c35328a675c84fd/gdown-5.2.0.tar.gz", hash = "sha256:2145165062d85520a3cd98b356c9ed522c5e7984d408535409fd46f94defc787", size = 284647 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/70/e07c381e6488a77094f04c85c9caf1c8008cdc30778f7019bc52e5285ef0/gdown-5.2.0-py3-none-any.whl", hash = "sha256:33083832d82b1101bdd0e9df3edd0fbc0e1c5f14c9d8c38d2a35bf1683b526d6", size = 18235 }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -976,6 +1073,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, ] +[[package]] +name = "google-pasta" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471 }, +] + [[package]] name = "graphene-stubs" version = "0.16" @@ -1001,12 +1110,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/00/e693a155da0a2a72fd2df75b8fe338146cae59d590ad6f56800adde90cb5/griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860", size = 127132 }, ] +[[package]] +name = "grpcio" +version = "1.68.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/ec/b76ff6d86bdfd1737a5ec889394b54c18b1ec3832d91041e25023fbcb67d/grpcio-1.68.1.tar.gz", hash = "sha256:44a8502dd5de653ae6a73e2de50a401d84184f0331d0ac3daeb044e66d5c5054", size = 12694654 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/15/674a1468fef234fa996989509bbdfc0d695878cbb385b9271f5d690d5cd3/grpcio-1.68.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8829924fffb25386995a31998ccbbeaa7367223e647e0122043dfc485a87c666", size = 5148351 }, + { url = "https://files.pythonhosted.org/packages/62/f5/edce368682d6d0b3573b883b134df022a44b1c888ea416dd7d78d480ab24/grpcio-1.68.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3aed6544e4d523cd6b3119b0916cef3d15ef2da51e088211e4d1eb91a6c7f4f1", size = 11127559 }, + { url = "https://files.pythonhosted.org/packages/ce/14/a6fde3114eafd9e4e345d1ebd0291c544d83b22f0554b1678a2968ae39e1/grpcio-1.68.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:4efac5481c696d5cb124ff1c119a78bddbfdd13fc499e3bc0ca81e95fc573684", size = 5645221 }, + { url = "https://files.pythonhosted.org/packages/21/21/d1865bd6a22f9a26217e4e1b35f9105f7a0cdfb7a5fffe8be48e1a1afafc/grpcio-1.68.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab2d912ca39c51f46baf2a0d92aa265aa96b2443266fc50d234fa88bf877d8e", size = 6292270 }, + { url = "https://files.pythonhosted.org/packages/3a/f6/19798be6c3515a7b1fb9570198c91710472e2eb21f1900109a76834829e3/grpcio-1.68.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c87ce2a97434dffe7327a4071839ab8e8bffd0054cc74cbe971fba98aedd60", size = 5905978 }, + { url = "https://files.pythonhosted.org/packages/9b/43/c3670a657445cd55be1246f64dbc3a6a33cab0f0141c5836df2e04f794c8/grpcio-1.68.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e4842e4872ae4ae0f5497bf60a0498fa778c192cc7a9e87877abd2814aca9475", size = 6630444 }, + { url = "https://files.pythonhosted.org/packages/80/69/fbbebccffd266bea4268b685f3e8e03613405caba69e93125dc783036465/grpcio-1.68.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:255b1635b0ed81e9f91da4fcc8d43b7ea5520090b9a9ad9340d147066d1d3613", size = 6200324 }, + { url = "https://files.pythonhosted.org/packages/65/5c/27a26c21916f94f0c1585111974a5d5a41d8420dcb42c2717ee514c97a97/grpcio-1.68.1-cp312-cp312-win32.whl", hash = "sha256:7dfc914cc31c906297b30463dde0b9be48e36939575eaf2a0a22a8096e69afe5", size = 3638381 }, + { url = "https://files.pythonhosted.org/packages/a3/ba/ba6b65ccc93c7df1031c6b41e45b79a5a37e46b81d816bb3ea68ba476d77/grpcio-1.68.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0c8ddabef9c8f41617f213e527254c41e8b96ea9d387c632af878d05db9229c", size = 4389959 }, + { url = "https://files.pythonhosted.org/packages/37/1a/15ccc08da339a5536690e6f877963422a5abf3f6dfeed96b3175f5c816b9/grpcio-1.68.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:a47faedc9ea2e7a3b6569795c040aae5895a19dde0c728a48d3c5d7995fda385", size = 5149822 }, + { url = "https://files.pythonhosted.org/packages/bc/fe/91bb4b160cd251d5b5ee722e6342355f76d1ffe176c50a6ef0e8256fbb47/grpcio-1.68.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:390eee4225a661c5cd133c09f5da1ee3c84498dc265fd292a6912b65c421c78c", size = 11085016 }, + { url = "https://files.pythonhosted.org/packages/55/2d/0bb2478410f5896da1090b9f43c2979dd72e7e97d10bc223bfbdddcf8eca/grpcio-1.68.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:66a24f3d45c33550703f0abb8b656515b0ab777970fa275693a2f6dc8e35f1c1", size = 5645634 }, + { url = "https://files.pythonhosted.org/packages/f5/6c/e2d22d963b695f87a09965246beb1c3224b09ffc666fc0b285820926499a/grpcio-1.68.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08079b4934b0bf0a8847f42c197b1d12cba6495a3d43febd7e99ecd1cdc8d54", size = 6291096 }, + { url = "https://files.pythonhosted.org/packages/6f/f6/21d9204e2c4c0804ad72be8c830c44f0e1355e649c173f87508b7f0e5488/grpcio-1.68.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8720c25cd9ac25dd04ee02b69256d0ce35bf8a0f29e20577427355272230965a", size = 5906528 }, + { url = "https://files.pythonhosted.org/packages/39/2a/bf6ae4fef13755ca236d587d630b82207cfad43cf956870adead97fd1ef1/grpcio-1.68.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:04cfd68bf4f38f5bb959ee2361a7546916bd9a50f78617a346b3aeb2b42e2161", size = 6634215 }, + { url = "https://files.pythonhosted.org/packages/5b/83/9c96a6adfbea5e8a9ed408410c0259942713be64173b8816c7bf6ac2d830/grpcio-1.68.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c28848761a6520c5c6071d2904a18d339a796ebe6b800adc8b3f474c5ce3c3ad", size = 6200750 }, + { url = "https://files.pythonhosted.org/packages/b4/3e/af42f87759c6301c4fed894b3dd801b13162ba1d8e2942412e788ac749eb/grpcio-1.68.1-cp313-cp313-win32.whl", hash = "sha256:77d65165fc35cff6e954e7fd4229e05ec76102d4406d4576528d3a3635fc6172", size = 3637594 }, + { url = "https://files.pythonhosted.org/packages/7e/d1/3bef33a3d5d26d4ea9284e1b464f481d6d21ed8ae1c3da381b05f62c701d/grpcio-1.68.1-cp313-cp313-win_amd64.whl", hash = "sha256:a8040f85dcb9830d8bbb033ae66d272614cec6faceee88d37a88a9bd1a7a704e", size = 4391184 }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, +] + +[[package]] +name = "h5py" +version = "3.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/0c/5c2b0a88158682aeafb10c1c2b735df5bc31f165bfe192f2ee9f2a23b5f1/h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf", size = 411457 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/e1/ea9bfe18a3075cdc873f0588ff26ce394726047653557876d7101bf0c74e/h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed", size = 3372538 }, + { url = "https://files.pythonhosted.org/packages/0d/74/1009b663387c025e8fa5f3ee3cf3cd0d99b1ad5c72eeb70e75366b1ce878/h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351", size = 2868104 }, + { url = "https://files.pythonhosted.org/packages/af/52/c604adc06280c15a29037d4aa79a24fe54d8d0b51085e81ed24b2fa995f7/h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834", size = 5194606 }, + { url = "https://files.pythonhosted.org/packages/fa/63/eeaacff417b393491beebabb8a3dc5342950409eb6d7b39d437289abdbae/h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9", size = 5413256 }, + { url = "https://files.pythonhosted.org/packages/86/f7/bb465dcb92ca3521a15cbe1031f6d18234dbf1fb52a6796a00bfaa846ebf/h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc", size = 2993055 }, + { url = "https://files.pythonhosted.org/packages/23/1c/ecdd0efab52c24f2a9bf2324289828b860e8dd1e3c5ada3cf0889e14fdc1/h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc", size = 3346239 }, + { url = "https://files.pythonhosted.org/packages/93/cd/5b6f574bf3e318bbe305bc93ba45181676550eb44ba35e006d2e98004eaa/h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653", size = 2843416 }, + { url = "https://files.pythonhosted.org/packages/8a/4f/b74332f313bfbe94ba03fff784219b9db385e6139708e55b11490149f90a/h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32", size = 5154390 }, + { url = "https://files.pythonhosted.org/packages/1a/57/93ea9e10a6457ea8d3b867207deb29a527e966a08a84c57ffd954e32152a/h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f", size = 5378244 }, + { url = "https://files.pythonhosted.org/packages/50/51/0bbf3663062b2eeee78aa51da71e065f8a0a6e3cb950cc7020b4444999e6/h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8", size = 2979760 }, +] + [[package]] name = "hope-dedup-engine" version = "0.1.0" source = { editable = "." } dependencies = [ { name = "celery", extra = ["redis"] }, + { name = "deepface" }, { name = "django" }, { name = "django-admin-extra-buttons" }, { name = "django-adminactions" }, @@ -1029,22 +1198,23 @@ dependencies = [ { name = "djangorestframework" }, { name = "drf-nested-routers" }, { name = "drf-spectacular", extra = ["sidecar"] }, - { name = "face-recognition" }, { name = "flower" }, { name = "jsonschema" }, { name = "numpy" }, - { name = "opencv-contrib-python-headless" }, { name = "psycopg2-binary" }, { name = "requests" }, { name = "sentry-sdk", extra = ["celery", "django"] }, { name = "setuptools" }, { name = "social-auth-app-django" }, { name = "social-auth-core" }, + { name = "tf-keras" }, { name = "unicef-security" }, - { name = "uwsgi" }, ] [package.optional-dependencies] +distribution = [ + { name = "uwsgi" }, +] docs = [ { name = "mkdocs" }, { name = "mkdocs-awesome-pages-plugin" }, @@ -1093,6 +1263,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "celery", extras = ["redis"] }, + { name = "deepface", specifier = ">=0.0.93" }, { name = "django" }, { name = "django-admin-extra-buttons" }, { name = "django-adminactions" }, @@ -1115,7 +1286,6 @@ requires-dist = [ { name = "djangorestframework" }, { name = "drf-nested-routers", specifier = ">=0.94.1" }, { name = "drf-spectacular", extras = ["sidecar"] }, - { name = "face-recognition", specifier = ">=1.3.0" }, { name = "flower", specifier = ">=2.0.1" }, { name = "jsonschema", specifier = ">=4.23.0" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.1" }, @@ -1124,15 +1294,15 @@ requires-dist = [ { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5.36" }, { name = "mkdocstrings-python", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=1.26.4,<2.0.0" }, - { name = "opencv-contrib-python-headless", specifier = ">=4.10.0.84" }, { name = "psycopg2-binary", specifier = ">=2.9.9" }, { name = "requests", specifier = ">=2.32.3" }, { name = "sentry-sdk", extras = ["celery", "django"], specifier = ">=2.2.1" }, { name = "setuptools", specifier = ">=74.1.2" }, { name = "social-auth-app-django" }, { name = "social-auth-core" }, + { name = "tf-keras", specifier = ">=2.18.0" }, { name = "unicef-security" }, - { name = "uwsgi", specifier = ">=2.0.25.1" }, + { name = "uwsgi", marker = "extra == 'distribution'", specifier = ">=2.0.28" }, ] [package.metadata.requires-dev] @@ -1235,7 +1405,7 @@ name = "ipython" version = "8.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "decorator" }, { name = "jedi" }, { name = "matplotlib-inline" }, @@ -1268,6 +1438,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310 }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -1292,6 +1471,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] +[[package]] +name = "joblib" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, +] + [[package]] name = "jsonschema" version = "4.23.0" @@ -1331,6 +1519,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/93/2d896b5fd3d79b4cadd8882c06650e66d003f465c9d12c488d92853dff78/junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732", size = 7130 }, ] +[[package]] +name = "keras" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "h5py" }, + { name = "ml-dtypes" }, + { name = "namex" }, + { name = "numpy" }, + { name = "optree" }, + { name = "packaging" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/c3/56fc6800c5eab94bd0f5e930751bd4c0fa1ee0aee272fad4a72723ffae87/keras-3.7.0.tar.gz", hash = "sha256:a4451a5591e75dfb414d0b84a3fd2fb9c0240cc87ebe7e397f547ce10b0e67b7", size = 924719 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/bf/9e3f10e55df30b0fb4bf6c2ee7d50bda2e070599b86f62ea3f9954af172b/keras-3.7.0-py3-none-any.whl", hash = "sha256:546a64f302e4779c129c06d9826fa586de752cdfd43d7dc4010c31b282587969", size = 1228365 }, +] + [[package]] name = "kombu" version = "5.4.2" @@ -1354,6 +1561,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/da/4cbc703cccc326bac1b4311609e694729134d1e8a2b45c224f7cb2602590/legacy_cgi-2.6.1-py3-none-any.whl", hash = "sha256:8eacc1522d9f76451337a4b5a0abf494158d39250754b0d1bc19a14c6512af9b", size = 19574 }, ] +[[package]] +name = "libclang" +version = "18.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/49/f5e3e7e1419872b69f6f5e82ba56e33955a74bd537d8a1f5f1eff2f3668a/libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a", size = 25836045 }, + { url = "https://files.pythonhosted.org/packages/e2/e5/fc61bbded91a8830ccce94c5294ecd6e88e496cc85f6704bf350c0634b70/libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5", size = 26502641 }, + { url = "https://files.pythonhosted.org/packages/db/ed/1df62b44db2583375f6a8a5e2ca5432bbdc3edb477942b9b7c848c720055/libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8", size = 26420207 }, + { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943 }, + { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972 }, + { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606 }, + { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494 }, + { url = "https://files.pythonhosted.org/packages/0b/2d/3f480b1e1d31eb3d6de5e3ef641954e5c67430d5ac93b7fa7e07589576c7/libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb", size = 26415083 }, + { url = "https://files.pythonhosted.org/packages/71/cf/e01dc4cc79779cd82d77888a88ae2fa424d93b445ad4f6c02bfc18335b70/libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8", size = 22361112 }, +] + [[package]] name = "lxml" version = "5.3.0" @@ -1396,6 +1620,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417 }, ] +[[package]] +name = "lz4" +version = "4.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/31/ec1259ca8ad11568abaf090a7da719616ca96b60d097ccc5799cd0ff599c/lz4-4.3.3.tar.gz", hash = "sha256:01fe674ef2889dbb9899d8a67361e0c4a2c833af5aeb37dd505727cf5d2a131e", size = 171509 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6f/081811b17ccaec5f06b3030756af2737841447849118a6e1078481a78c6c/lz4-4.3.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e36cd7b9d4d920d3bfc2369840da506fa68258f7bb176b8743189793c055e43d", size = 254213 }, + { url = "https://files.pythonhosted.org/packages/53/4d/8e04ef75feff8848ba3c624ce81c7732bdcea5f8f994758afa88cd3d7764/lz4-4.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31ea4be9d0059c00b2572d700bf2c1bc82f241f2c3282034a759c9a4d6ca4dc2", size = 212354 }, + { url = "https://files.pythonhosted.org/packages/a3/04/257a72d6a879dbc8c669018989f776fcdd5b4bf3c2c51c09a54f1ca31721/lz4-4.3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33c9a6fd20767ccaf70649982f8f3eeb0884035c150c0b818ea660152cf3c809", size = 1238643 }, + { url = "https://files.pythonhosted.org/packages/d9/93/4a7e489156fa7ded03ba9cde4a8ca7f373672b5787cac9a0391befa752a1/lz4-4.3.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca8fccc15e3add173da91be8f34121578dc777711ffd98d399be35487c934bf", size = 1265014 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/f84ebc23bc7602623b1b003b4e1120cbf86fb03a35c595c226be1985449b/lz4-4.3.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d84b479ddf39fe3ea05387f10b779155fc0990125f4fb35d636114e1c63a2e", size = 1184881 }, + { url = "https://files.pythonhosted.org/packages/de/3d/8ba48305378e84908221de143a21ba0c0ce52778893865cf85b66b1068da/lz4-4.3.3-cp312-cp312-win32.whl", hash = "sha256:337cb94488a1b060ef1685187d6ad4ba8bc61d26d631d7ba909ee984ea736be1", size = 87241 }, + { url = "https://files.pythonhosted.org/packages/c4/5d/7b70965a0692de29af2af1007fe837f46fd456bbe2aa8f838a8543a3b5cb/lz4-4.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:5d35533bf2cee56f38ced91f766cd0038b6abf46f438a80d50c52750088be93f", size = 99776 }, +] + [[package]] name = "markdown" version = "3.7" @@ -1405,6 +1644,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -1464,6 +1715,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + [[package]] name = "mergedeep" version = "1.3.4" @@ -1553,7 +1813,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.5.47" +version = "9.5.49" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -1568,9 +1828,9 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/0a/6b5a5761d6e500f0c6de9ae24461ac93c66b35785faac331e6d66522776f/mkdocs_material-9.5.47.tar.gz", hash = "sha256:fc3b7a8e00ad896660bd3a5cc12ca0cb28bdc2bcbe2a946b5714c23ac91b0ede", size = 3910665 } +sdist = { url = "https://files.pythonhosted.org/packages/e2/14/8daeeecee2e25bd84239a843fdcb92b20db88ebbcb26e0d32f414ca54a22/mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d", size = 3949559 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/ef/25150e53836255bc8a2cee958e251516035e85b307774fbcfc6bda0d0388/mkdocs_material-9.5.47-py3-none-any.whl", hash = "sha256:53fb9c9624e7865da6ec807d116cd7be24b3cb36ab31b1d1d1a9af58c56009a2", size = 8625827 }, + { url = "https://files.pythonhosted.org/packages/fc/2d/2dd23a36b48421db54f118bb6f6f733dbe2d5c78fe7867375e48649fd3df/mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e", size = 8684098 }, ] [[package]] @@ -1615,6 +1875,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/c1/ac524e1026d9580cbc654b5d19f5843c8b364a66d30f956372cd09fd2f92/mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a", size = 111759 }, ] +[[package]] +name = "ml-dtypes" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/1a/99e924f12e4b62139fbac87419698c65f956d58de0dbfa7c028fa5b096aa/ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b", size = 405077 }, + { url = "https://files.pythonhosted.org/packages/8f/8c/7b610bd500617854c8cc6ed7c8cfb9d48d6a5c21a1437a36a4b9bc8a3598/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7", size = 2181554 }, + { url = "https://files.pythonhosted.org/packages/c7/c6/f89620cecc0581dc1839e218c4315171312e46c62a62da6ace204bda91c0/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9", size = 2160488 }, + { url = "https://files.pythonhosted.org/packages/ae/11/a742d3c31b2cc8557a48efdde53427fd5f9caa2fa3c9c27d826e78a66f51/ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c", size = 127462 }, +] + +[[package]] +name = "mtcnn" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "lz4" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/37/b0f60411b6a37dcd5122bbe05c9aa45f271bcc8129caa45ee1012251905d/mtcnn-1.0.0.tar.gz", hash = "sha256:08428bf8e1ae9827d43a40bb0246b57f2239e3572d3742f472ae9924896c6419", size = 1885746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/7e/0b2b688a9e2d353a661b617b12d00d9af29f877b57c8e4a3cbe447483b46/mtcnn-1.0.0-py3-none-any.whl", hash = "sha256:0a96b4868e56db9ae984449519642be6dba03240e608a67e928ebb47833e9144", size = 1898606 }, +] + [[package]] name = "mypy" version = "1.13.0" @@ -1647,6 +1935,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] +[[package]] +name = "namex" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/48/d275cdb6216c6bb4f9351675795a0b48974e138f16b1ffe0252c1f8faa28/namex-0.0.8.tar.gz", hash = "sha256:32a50f6c565c0bb10aa76298c959507abdc0e850efe085dc38f3440fcb3aa90b", size = 6623 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/59/7854fbfb59f8ae35483ce93493708be5942ebb6328cd85b3a609df629736/namex-0.0.8-py3-none-any.whl", hash = "sha256:7ddb6c2bb0e753a311b7590f84f6da659dd0c05e65cb89d519d54c0a250c0487", size = 5806 }, +] + [[package]] name = "natsort" version = "8.4.0" @@ -1691,20 +1988,20 @@ wheels = [ ] [[package]] -name = "opencv-contrib-python-headless" +name = "opencv-python" version = "4.10.0.84" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/6f/b0192e6f3eceeaa9b2fc148717f19d077edd6be0166b10248db96b9324bc/opencv-contrib-python-headless-4.10.0.84.tar.gz", hash = "sha256:6351250db97e1f91f31afdec2436afb1c89594e3da02851e0f01e20ea16bbd9e", size = 150465571 } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/b70a2d9ab205110d715906fc8ec83fbb00404aeb3a37a0654fdb68eb0c8c/opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526", size = 95103981 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/19/f6a151219a493767da14245b45304d3254bd3887ff4ef081da541353d9ad/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:be91c6c81e839613c6f3b15755bf71789839289d0e3440fab093e0708516ffcf", size = 63667534 }, - { url = "https://files.pythonhosted.org/packages/45/23/8559fbdaa944d9067c17451341dff464f1a76ed3cfbd0bb7d1a44b62d5a2/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:252df47a7e1da280cef26ee0ecc1799841015ce3718214634bb15bc22d4cb308", size = 66278139 }, - { url = "https://files.pythonhosted.org/packages/93/c3/a399ad183bd94210e6a9002add4096ead4f0d0c36c0b1b65c3205a0baee5/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb20ee077ac0955704d391c00639df6063cb67cb62606c07b97d8b635feff6", size = 34727901 }, - { url = "https://files.pythonhosted.org/packages/00/fc/b01f878cef02f619a4686683db31451d0e3e961646e65ec09ea802c8ceda/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c16eb5f888aee7bf664106e12c423705d29d1b094876b66aa4e33d4e8ec905", size = 55986159 }, - { url = "https://files.pythonhosted.org/packages/d2/af/65fa29ea39f410547c708b1007cd8846587715b95a2509361f699d044dca/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:7581d7ffb7fff953436797dca2dfc5e70e100f721ea18ab84ebf11417ea21d0c", size = 34444721 }, - { url = "https://files.pythonhosted.org/packages/01/d4/dacf890940cb22279e1513b7ea41d97825a723153a5efce68bc52e7b3b6e/opencv_contrib_python_headless-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:660ded6b77b07f875f56065016677bbb6a3abca13903b9320164691a46474a7d", size = 45449488 }, + { url = "https://files.pythonhosted.org/packages/66/82/564168a349148298aca281e342551404ef5521f33fba17b388ead0a84dc5/opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251", size = 54835524 }, + { url = "https://files.pythonhosted.org/packages/64/4a/016cda9ad7cf18c58ba074628a4eaae8aa55f3fd06a266398cef8831a5b9/opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98", size = 56475426 }, + { url = "https://files.pythonhosted.org/packages/81/e4/7a987ebecfe5ceaf32db413b67ff18eb3092c598408862fff4d7cc3fd19b/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6", size = 41746971 }, + { url = "https://files.pythonhosted.org/packages/3f/a4/d2537f47fd7fcfba966bd806e3ec18e7ee1681056d4b0a9c8d983983e4d5/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f", size = 62548253 }, + { url = "https://files.pythonhosted.org/packages/1e/39/bbf57e7b9dab623e8773f6ff36385456b7ae7fa9357a5e53db732c347eac/opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236", size = 28737688 }, + { url = "https://files.pythonhosted.org/packages/ec/6c/fab8113424af5049f85717e8e527ca3773299a3c6b02506e66436e19874f/opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe", size = 38842521 }, ] [[package]] @@ -1733,6 +2030,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/04/5847e2e75c39f80e65c388f8e292719b46c00cda2c987f2d5c53ed617d11/openpyxl_stubs-0.1.25-py3-none-any.whl", hash = "sha256:db29f7804993b4a46b155fc4be45314c14538cb475b00591d8096e5af486abf1", size = 26598 }, ] +[[package]] +name = "opt-einsum" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932 }, +] + +[[package]] +name = "optree" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/f2/56afdaeaae36b076659be7db8e72be0924dd64ebd1c131675c77f7e704a6/optree-0.13.1.tar.gz", hash = "sha256:af67856aa8073d237fe67313d84f8aeafac32c1cef7239c628a2768d02679c43", size = 155738 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/e7/f605320e064ba54078f2966a9034fa2b3fc47db1e728e07a2a38b2e9075f/optree-0.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0914ba436d6c0781dc9b04e3b95e06fe5c4fc6a87e94893da971805a3790efe8", size = 600953 }, + { url = "https://files.pythonhosted.org/packages/fa/7c/b7bedf44dbc54c55b8a408a4f978d9bb1ffbfb376093c33fc8576b1848dd/optree-0.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:111172446e8a4f0d3be13a853fa28cb46b5679a1c7ca15b2e6db2b43dbbf9efb", size = 322341 }, + { url = "https://files.pythonhosted.org/packages/71/05/ea228c1677a53855572a0ebb0c4e2a3e5d8e792d59e2b536ef50a9a02495/optree-0.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28f083ede9be89503357a6b9e5d304826701596abe13d33e8f6fa2cd85b407fc", size = 352675 }, + { url = "https://files.pythonhosted.org/packages/6f/22/c65ef2b6b191119a90223226b4a02100a9c9dd3a38e8410e473bd1653eff/optree-0.13.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aec6da79a6130b4c76073241c0f31c11b96a38e70c7a00f9ed918d7464394ab", size = 399295 }, + { url = "https://files.pythonhosted.org/packages/01/be/56f946d3af013561d46c95f75880302cab03f1490ef939569852af6331c0/optree-0.13.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a408a43f16840475612c7058eb80b53791bf8b8266c5b3cd07f69697958fd97d", size = 392916 }, + { url = "https://files.pythonhosted.org/packages/e3/ec/6041c3ffe04af5890af7ab2b5f0ca48253032dce32aa5cddf8188ad4cc4b/optree-0.13.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da76fc43dcc22fe58d11634a04672ca7cc270aed469ac35fd5c78b7b9bc9125", size = 365179 }, + { url = "https://files.pythonhosted.org/packages/98/10/087a684c7b5029e3be1f335d9df422b406cbfd842c77abfa7b17085adce5/optree-0.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d866f707b9f3a9f0e670a73fe8feee4993b2dbdbf9eef598e1cf2e5cb2876413", size = 385480 }, + { url = "https://files.pythonhosted.org/packages/9d/58/f7430d613197260fc38fead8bc974a0069c4513ea3c04f11a771daf8b20f/optree-0.13.1-cp312-cp312-win32.whl", hash = "sha256:bc9c396f64f9aacdf852713bd75f1b9a83f118660fd82e87c937c081b7ddccd1", size = 261578 }, + { url = "https://files.pythonhosted.org/packages/e3/de/b114d999746f9a9fb64476c8520ad499c11651912cecffe77aee1d5bec18/optree-0.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:587fb8de8e75e80fe7c7240e269630876bec3ee2038724893370976207813e4b", size = 292036 }, + { url = "https://files.pythonhosted.org/packages/9f/d7/5dec5d97c0a0c7951f0c8f5d24b4c6c8529d41ee69d0705f06bfa8b4874f/optree-0.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:5da0fd26325a07354915cc4e3a9aee797cb75dff07c60d24b3f309457069abd3", size = 292044 }, + { url = "https://files.pythonhosted.org/packages/3f/53/f3727cad24f16a06666f328f1212476988cadac9b9e7919ddfb2c22eb662/optree-0.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f788b2ad120deb73b4908a74473cd6de79cfb9f33bbe9dcb59cea2e2477d4e28", size = 608270 }, + { url = "https://files.pythonhosted.org/packages/64/f2/68beb9da2dd52baa50e7a589ed2bd8434fdd70cdba06754aa5910263da06/optree-0.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2909cb42add6bb1a5a2b0243bdd8c4b861bf072f3741e26239481907ac8ad4e6", size = 325703 }, + { url = "https://files.pythonhosted.org/packages/45/db/08921e56f3425bf649eb593eb28775263c935d029985d35572dc5690cc1a/optree-0.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc5fa2ff5090389f3a906567446f01d692bd6fe5cfcc5ae2d5861f24e8e0e4d", size = 355813 }, + { url = "https://files.pythonhosted.org/packages/e5/e3/587e0d28dc2cee064902adfebca97db124e12b275dbe9c2b05a70a22345f/optree-0.13.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4711f5cac5a2a49c3d6c9f0eca7b77c22b452170bb33ea01c3214ebb17931db9", size = 402566 }, + { url = "https://files.pythonhosted.org/packages/8a/1d/0d5bbab8c99580b732b89ef2c5fcdd6ef410478295949fdf2984fa1bfc28/optree-0.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c4ab1d391b89cb88eb3c63383d5eb0930bc21141de9d5acd277feed9e38eb65", size = 397005 }, + { url = "https://files.pythonhosted.org/packages/16/fa/fc2a8183e14f0d195d25824bf65095ff32b34bd469614a6c30d0a596a30f/optree-0.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5e5f09c85ae558a6bdaea57e63168082e728e777391393e9e2792f0d15b7b59", size = 369400 }, + { url = "https://files.pythonhosted.org/packages/9f/42/8c08ce4ebb3d9a6e4415f1a97830c84879e2d1a43710a7c8a18b2c3e169d/optree-0.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8ee1e988c634a451146b87d9ebdbf650a75dc1f52a9cffcd89fabb7289321c", size = 390179 }, + { url = "https://files.pythonhosted.org/packages/06/02/3a701d6307fdfefe4fcecbac644803e2a4314ab2406ff465e03129cc85f6/optree-0.13.1-cp313-cp313-win32.whl", hash = "sha256:5b6531cd4eb23fadbbf77faf834e1119da06d7af3154f55786b59953cd87bb8a", size = 264264 }, + { url = "https://files.pythonhosted.org/packages/ef/f9/8a1421181c5eb0c0f81d1423a900baeb3faba68a48747bbdffb7581239ac/optree-0.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:27d81dc43b522ba47ba7d2e7d91dbb486940348b1bf85caeb0afc2815c0aa492", size = 293682 }, + { url = "https://files.pythonhosted.org/packages/80/34/d1b1849a6240385c4a3af5da9425b11912204d0b1cf142d802815319b73a/optree-0.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:f39c7174a3f3cdc3f5fe6fb4b832f608c40ac174d7567ed6734b2ee952094631", size = 293670 }, + { url = "https://files.pythonhosted.org/packages/0d/d6/f81e6748bcc3f35a2f570a814014e3418b0ed425d7cbc2b42d88d12863d5/optree-0.13.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3010ae24e994f6e00071098d34e98e78eb995b7454a2ef629a0bf7df17441b24", size = 702861 }, + { url = "https://files.pythonhosted.org/packages/08/7f/70a2d02110ccb245bc57bd9ad57668acfea0ff364c27d7dfe1735ede79ed/optree-0.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b5626c38d4a18a144063db5c1dbb558431d83ca10682324f74665a12214801f", size = 370740 }, + { url = "https://files.pythonhosted.org/packages/63/37/4ddf05267467809236203e2007e9443519c4d55e0744ce7eea1aa74dffee/optree-0.13.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1935639dd498a42367633e3877797e1330e39d44d48bbca1a136bb4dbe4c1bc9", size = 374695 }, + { url = "https://files.pythonhosted.org/packages/19/f2/51a63a799f6dce31813d7e02a7547394aebcb39f407e62038ecbd999d490/optree-0.13.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01819c3df950696f32c91faf8d376ae6b695ffdba18f330f1cab6b8e314e4612", size = 418671 }, + { url = "https://files.pythonhosted.org/packages/f0/7c/a08191e0c9202f2be9c415057eea3cf3a5af18e9a6d81f4c7b0e6faf0a1f/optree-0.13.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48c29d9c6c64c8dc48c8ee97f7c1d5cdb83e37320f0be0857c06ce4b97994aea", size = 414966 }, + { url = "https://files.pythonhosted.org/packages/8f/37/7bf815f4da7234e387863228b17246b42b8c02553882581a4013a64a88d0/optree-0.13.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:025d23400b8b579462a251420f0a9ae77d3d3593f84276f3465985731d79d722", size = 389219 }, + { url = "https://files.pythonhosted.org/packages/3d/84/bb521a66d3a84fe2f1500ef67d245c2cc1a26277fcaaf4bc70b22c06e99b/optree-0.13.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55e82426bef151149cfa41d68ac957730fcd420996c0db8324fca81aa6a810ba", size = 405377 }, + { url = "https://files.pythonhosted.org/packages/06/99/3eb53829c4c0b6dc20115d957d2d8e945630ddf40c656dc4e39c5a6e51f2/optree-0.13.1-cp313-cp313t-win32.whl", hash = "sha256:e40f018f522fcfd244688d1b3a360518e636ba7f636385aae0566eae3e7d29bc", size = 292734 }, + { url = "https://files.pythonhosted.org/packages/2f/59/d7601959ad0b90d309794c0975a256304488b4c5671f24e3e12101ade7ef/optree-0.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d580f1bf23bb352c4db6b3544f282f1ac08dcb0d9ab537d25e56220353438cf7", size = 331457 }, + { url = "https://files.pythonhosted.org/packages/8b/36/c01a5bc34660d46c6a3b1fe090bbdc8c76af7b5c1a6613cc671aa6df8349/optree-0.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c4d13f55dbd509d27be3af54d53b4ca0751bc518244ced6d0567e518e51452a2", size = 331470 }, +] + [[package]] name = "packaging" version = "24.2" @@ -1751,6 +2098,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, ] +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + [[package]] name = "parso" version = "0.8.4" @@ -1869,11 +2250,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.21.0" +version = "0.21.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/54/a369868ed7a7f1ea5163030f4fc07d85d22d7a1d270560dab675188fb612/prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e", size = 78634 } +sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/2d/46ed6436849c2c88228c3111865f44311cff784b4aabcdef4ea2545dbc3d/prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166", size = 54686 }, + { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, ] [[package]] @@ -1888,6 +2269,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, ] +[[package]] +name = "protobuf" +version = "5.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/4f/1639b7b1633d8fd55f216ba01e21bf2c43384ab25ef3ddb35d85a52033e8/protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", size = 424965 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c7/28669b04691a376cf7d0617d612f126aa0fff763d57df0142f9bf474c5b8/protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", size = 422706 }, + { url = "https://files.pythonhosted.org/packages/e3/33/dc7a7712f457456b7e0b16420ab8ba1cc8686751d3f28392eb43d0029ab9/protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", size = 434505 }, + { url = "https://files.pythonhosted.org/packages/e5/39/44239fb1c6ec557e1731d996a5de89a9eb1ada7a92491fcf9c5d714052ed/protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18", size = 417822 }, + { url = "https://files.pythonhosted.org/packages/fb/4a/ec56f101d38d4bef2959a9750209809242d86cf8b897db00f2f98bfa360e/protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", size = 319572 }, + { url = "https://files.pythonhosted.org/packages/04/52/c97c58a33b3d6c89a8138788576d372a90a6556f354799971c6b4d16d871/protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", size = 319671 }, + { url = "https://files.pythonhosted.org/packages/3b/24/c8c49df8f6587719e1d400109b16c10c6902d0c9adddc8fff82840146f99/protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", size = 172547 }, +] + [[package]] name = "psutil" version = "6.1.0" @@ -2021,12 +2416,21 @@ version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/1b/ea40363be0056080454cdbabe880773c3c5bd66d7b13f0c8b8b8c8da1e0c/pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775", size = 48744 } +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725 }, +] + [[package]] name = "pytest" version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "(platform_machine != 'aarch64' and platform_system == 'Linux' and sys_platform == 'win32') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'win32')" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, @@ -2243,11 +2647,11 @@ wheels = [ [[package]] name = "redis" -version = "5.2.0" +version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] [[package]] @@ -2316,6 +2720,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + [[package]] name = "requests-mock" version = "1.12.1" @@ -2355,62 +2764,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/24/93293d0be0db9da1ed8dfc5e6af700fdd40e8f10a928704dd179db9f03c1/responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb", size = 55238 }, ] +[[package]] +name = "retina-face" +version = "0.0.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gdown" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "tensorflow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/0d/76a74b93c0d9d6fd8ad450986c8f586b371ce74f7845eb3827591d8ac3e1/retina-face-0.0.17.tar.gz", hash = "sha256:7532b136ed01fe9a8cba8dfbc5a046dd6fb1214b1a83e57f3210bd145a91cd73", size = 18929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/87/30c5beef6ef3cb60f80f02d3f934b86efda21aca3225f174d127192d43bb/retina_face-0.0.17-py3-none-any.whl", hash = "sha256:b43fdac4078678b9d8bc45b88a7090f05d81c44e1e10710e6c16d703bb7add41", size = 25124 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + [[package]] name = "rpds-py" -version = "0.22.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/bb/4a7bd48830ae7366467c09923a2ffab45bf76102a87d97ff24cdcebcda5c/rpds_py-0.22.0.tar.gz", hash = "sha256:32de71c393f126d8203e9815557c7ff4d72ed1ad3aa3f52f6c7938413176750a", size = 26723 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/c4/44b95b3063feff43d01752f60ccbe84cbfb4b486997d27878cc4bb6c9975/rpds_py-0.22.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e9d4293b21c69ee4f9e1a99ac4f772951d345611c614a0cfae2ec6b565279bc9", size = 359720 }, - { url = "https://files.pythonhosted.org/packages/ca/09/56813556213377d5712f5448ebef689576e7874217edea43c1a35c6dfeda/rpds_py-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67e013a17a3db4d98cc228fd5aeb36a51b0f5cf7330b9102a552060f1fe4e560", size = 345057 }, - { url = "https://files.pythonhosted.org/packages/c9/58/11cfd543d2468ce9279919c3bc41013b40f456cc446f5a74005e7d4140c5/rpds_py-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b639a19e1791b646d27f15d17530a51722cc728d43b2dff3aeb904f92d91bac", size = 381575 }, - { url = "https://files.pythonhosted.org/packages/c1/a4/b3bd40b0444948e599286a0338b170021bfec6370aeb1c14b5b39f0bdad4/rpds_py-0.22.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1357c3092702078b7782b6ebd5ba9b22c1a291c34fbf9d8f1a48237466ac7758", size = 387140 }, - { url = "https://files.pythonhosted.org/packages/49/f1/1ef6897de79c8883bba0da121e2e809097ff2ce1fbe9eb5a85878b588853/rpds_py-0.22.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:842855bbb113a19c393c6de5aa6ed9a26c6b13c2fead5e49114d39f0d08b94d8", size = 424886 }, - { url = "https://files.pythonhosted.org/packages/37/4b/6fcfa9d14715202824e7fadb1c3c2933b061b1c72b2e031c587414924449/rpds_py-0.22.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae7927cd2b869ca4dc645169d8af5494a29c99afd0ea0f24dd00c811ab1d8b8", size = 449384 }, - { url = "https://files.pythonhosted.org/packages/2b/ba/02b4705425eb53fbf2f00d48b582d6b42c4e8807d2f29ef389ad724e659c/rpds_py-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91bfef5daa2a5a4fe62f8d317fc91a626073639f951f851bd2cb252d01bc6c5", size = 382842 }, - { url = "https://files.pythonhosted.org/packages/02/3d/fa071272855af01977d0e2f889b80d4ba9a4b326684f64de3ac98243c246/rpds_py-0.22.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fc4824e38c1e91a73bc820e7caacaf19d0acd557465aceef0420ca59489b390", size = 410252 }, - { url = "https://files.pythonhosted.org/packages/49/2b/9c54d4316c983428fefc94aacbf1eb8d024e899d95efce515fc484b9cbee/rpds_py-0.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:92d28a608127b357da47c99e0d0e0655ca2060286540fe9f2a25a2e8ac666e05", size = 555933 }, - { url = "https://files.pythonhosted.org/packages/bf/bf/c0732d81c4f36ba785e93b516065cd71fbe6b8ac9febb34c64d0f5dd0470/rpds_py-0.22.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c637188b930175c256f13adbfc427b83ec7e64476d1ec9d6608f312bb84e06c3", size = 582677 }, - { url = "https://files.pythonhosted.org/packages/56/da/7878f3e37cea93934400ae7798c4b714d6dab7f63c0a1505ebdf1138a197/rpds_py-0.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93bbd66f46dddc41e8c656130c97c0fb515e0fa44e1eebb2592769dbbd41b2f5", size = 550794 }, - { url = "https://files.pythonhosted.org/packages/10/ba/b9c061b245172c61c57148439b067276b500264b2a041433026d65044e96/rpds_py-0.22.0-cp312-cp312-win32.whl", hash = "sha256:54d8f94dec5765a9edc19610fecf0fdf9cab36cbb9def1213188215f735a6f98", size = 221576 }, - { url = "https://files.pythonhosted.org/packages/73/d9/0a838834b00fe42005cf071012206629c88125e6515d044c06df083d7448/rpds_py-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:931bf3d0705b2834fed29354f35170fa022fe22a95542b61b7c66aca5f8a224f", size = 236447 }, - { url = "https://files.pythonhosted.org/packages/ce/05/efe5daa8d9989bcd2594996fa57aa28637b12d55d965e14e8477fca2ba79/rpds_py-0.22.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2a57300cc8b034c5707085249efd09f19116bb80278d0ec925d7f3710165c510", size = 359719 }, - { url = "https://files.pythonhosted.org/packages/1a/ff/cabcafe1ae172377d8fc2a4f1941c0d8be8307e9f37c77d469486a15bbe0/rpds_py-0.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c398a5a8e258dfdc5ea2aa4e5aa2ca3207f654a8eb268693dd1a76939074a588", size = 345059 }, - { url = "https://files.pythonhosted.org/packages/3f/00/baa9715d61a6ea9d24acda39fe444aa5d45d73a3eff84620ca9c96f5a0bc/rpds_py-0.22.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a6cc4eb1e86364331928acafb2bb41d8ab735ca3caf2d6019b9f6dac3f4f65d", size = 381576 }, - { url = "https://files.pythonhosted.org/packages/22/5b/acbd75ad88e5dd519d4ff60fe56bc0743ae7c03a7329f7feb93e30274f21/rpds_py-0.22.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:574c5c94213bc9990805bfd7e4ba3826d3c098516cbc19f0d0ef0433ad93fa06", size = 387141 }, - { url = "https://files.pythonhosted.org/packages/80/44/52b222a8e54703a36a2ba0e55e1fce14a0f4fc78488a89a4db609cfc9fb8/rpds_py-0.22.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c0321bc03a1c513eca1837e3bba948b975bcf3a172aebc197ab3573207f137a", size = 424887 }, - { url = "https://files.pythonhosted.org/packages/4f/10/9599960a61773be49b498e22bff3444511e0f90f2b03d2a0ffaea97de717/rpds_py-0.22.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d276280649305c1da6cdd84585d48ae1f0efa67434d8b10d2df95228e59a05bb", size = 449384 }, - { url = "https://files.pythonhosted.org/packages/af/07/d0af9564ad7f680a7239baa3856fe511f18212b9884bff82c1f276c0f7b0/rpds_py-0.22.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c17b43fe9c6da16885e3fe28922bcd1a029e61631fb771c7d501019b40bcc904", size = 382843 }, - { url = "https://files.pythonhosted.org/packages/f4/92/e2126a0c0addf26e600ec397e33ffc69770a1fdf86c6f48f3a2190a2c163/rpds_py-0.22.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48c95997af9314f4034fe5ba2d837399e786586e220835a578d28fe8161e6ae5", size = 410253 }, - { url = "https://files.pythonhosted.org/packages/38/49/31c2bdb3c503d6b0190f15244ff515c317f5deacd3c5e6e17bb0daec452a/rpds_py-0.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9aa4af6b879bb75a3c7766fbf49d77f4097dd12b548ecbbd8b3f85caa833281", size = 555932 }, - { url = "https://files.pythonhosted.org/packages/fc/ca/bf918ae37bbae9f47d68357a2935654c38f438e72a13bc495a57c680d64a/rpds_py-0.22.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8426f97117b914b9bfb2a7bd46edc148e8defda728a55a5df3a564abe70cd7a4", size = 582678 }, - { url = "https://files.pythonhosted.org/packages/f1/b3/fabc952d693eb30f67f3b94f99ab0c924c954debb23329099f27cb5e517a/rpds_py-0.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:034964ea0ea09645bdde13038b38abb14be0aa747f20fcfab6181207dd9e0483", size = 550793 }, - { url = "https://files.pythonhosted.org/packages/68/f4/2dd0766e0499f3c61030e4eb05ca6a838a6a6dea3bb3286d455522af1c5e/rpds_py-0.22.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:3dc7c64b56b82428894f056e9ff6e8ee917ff74fc26b65211a33602c2372e928", size = 359725 }, - { url = "https://files.pythonhosted.org/packages/08/32/472010d08e8948d86f97dfaa2b70b10b93828aca535e019bf25e3499e481/rpds_py-0.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1212cb231f2002934cd8d71a0d718fdd9d9a2dd671e0feef8501038df3508026", size = 345061 }, - { url = "https://files.pythonhosted.org/packages/00/4b/d867fe4e5df58d0ba00918772a7b403369ee71a8482ec3a9ff09b960ac7e/rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f21e1278c9456cd601832375c778ca44614d3433996488221a56572c223f04a", size = 381579 }, - { url = "https://files.pythonhosted.org/packages/33/d2/7d913ae37e25d108d553a5299f842bb93a438f36d51d7af47c3362757395/rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:875fe8dffb43c20f68379ee098b035a7038d7903c795d46715f66575a7050b19", size = 387143 }, - { url = "https://files.pythonhosted.org/packages/ca/a6/8204781fa53ad198e2125e7e81045b81bc025b0c02f5f7597fdfae67b2e9/rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e23dcdd4b2ff9c6b3317ea7921b210d39592f8ca1cdea58ada25b202c65c0a69", size = 424891 }, - { url = "https://files.pythonhosted.org/packages/4e/fa/da1cfb351a646a52fb60b2ee3e0f1da5c630294fc35b56e2d6bba6eb45ce/rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fb8efc9e579acf1e556fd86277fecec320c21ca9b5d39db96433ad8c45bc4a", size = 449390 }, - { url = "https://files.pythonhosted.org/packages/ef/37/916eee3304840d34c95028a5fe03976ea0c772291f7806182618685eca93/rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe23687924b25a2dee52fab15976fd6577ed8518072bcda9ff2e2b88ab1f168b", size = 382849 }, - { url = "https://files.pythonhosted.org/packages/d7/55/461de3655e2962412a17100c9dfcad976a1ace0242fc9f1c679a4ba06446/rpds_py-0.22.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5469b347445d1c31105f33e7bfc9a8ba213d48e42641a610dda65bf9e3c83f5", size = 410256 }, - { url = "https://files.pythonhosted.org/packages/f9/b6/10a4a443c214c642ff07c05ece7d81120f8265244c83ea3e491eaccd434f/rpds_py-0.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a810a57ce5e8ecf8eac6ec4dab534ff80c34e5a2c31db60e992009cd20f58e0f", size = 555937 }, - { url = "https://files.pythonhosted.org/packages/c7/73/b3cc5cb061e0dec73f77e7f58e3ad99f1320935f2ee6d94d1886110c2c90/rpds_py-0.22.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d9bb9242b38a664f307b3b897f093896f7ed51ef4fe25a0502e5a368de9151ea", size = 582681 }, - { url = "https://files.pythonhosted.org/packages/a2/a1/bf4aaaa8e386cd032764b3e5af61525ea6ddc3ec406558fe8850a0774719/rpds_py-0.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b4660943030406aaa40ec9f51960dd88049903d9536bc3c8ebb5cc4e1f119bbe", size = 550799 }, - { url = "https://files.pythonhosted.org/packages/15/c1/2a7eae4ca2b433783e9875f0b30cbc5a2c710279904463e651dd69cd98af/rpds_py-0.22.0-cp313-cp313t-win32.whl", hash = "sha256:208ce1d8e3af138d1d9b21d7206356b7f29b96675e0113aea652cf024e4ddfdc", size = 219410 }, - { url = "https://files.pythonhosted.org/packages/0a/a5/d7b020fb13eae9a544c99f7a8f4d502169c456b07dda0bdefbe36611680d/rpds_py-0.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e6da2e0500742e0f157f005924a0589f2e2dcbfdd6cd0cc0abce367433e989be", size = 234146 }, +version = "0.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/80/cce854d0921ff2f0a9fa831ba3ad3c65cee3a46711addf39a2af52df2cfd/rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", size = 26771 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/47/3383ee3bd787a2a5e65a9b9edc37ccf8505c0a00170e3a5e6ea5fbcd97f7/rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", size = 352334 }, + { url = "https://files.pythonhosted.org/packages/40/14/aa6400fa8158b90a5a250a77f2077c0d0cd8a76fce31d9f2b289f04c6dec/rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", size = 342111 }, + { url = "https://files.pythonhosted.org/packages/7d/06/395a13bfaa8a28b302fb433fb285a67ce0ea2004959a027aea8f9c52bad4/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", size = 384286 }, + { url = "https://files.pythonhosted.org/packages/43/52/d8eeaffab047e6b7b7ef7f00d5ead074a07973968ffa2d5820fa131d7852/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e", size = 391739 }, + { url = "https://files.pythonhosted.org/packages/83/31/52dc4bde85c60b63719610ed6f6d61877effdb5113a72007679b786377b8/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", size = 427306 }, + { url = "https://files.pythonhosted.org/packages/70/d5/1bab8e389c2261dba1764e9e793ed6830a63f830fdbec581a242c7c46bda/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", size = 442717 }, + { url = "https://files.pythonhosted.org/packages/82/a1/a45f3e30835b553379b3a56ea6c4eb622cf11e72008229af840e4596a8ea/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", size = 385721 }, + { url = "https://files.pythonhosted.org/packages/a6/27/780c942de3120bdd4d0e69583f9c96e179dfff082f6ecbb46b8d6488841f/rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", size = 415824 }, + { url = "https://files.pythonhosted.org/packages/94/0b/aa0542ca88ad20ea719b06520f925bae348ea5c1fdf201b7e7202d20871d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", size = 561227 }, + { url = "https://files.pythonhosted.org/packages/0d/92/3ed77d215f82c8f844d7f98929d56cc321bb0bcfaf8f166559b8ec56e5f1/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", size = 587424 }, + { url = "https://files.pythonhosted.org/packages/09/42/cacaeb047a22cab6241f107644f230e2935d4efecf6488859a7dd82fc47d/rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", size = 555953 }, + { url = "https://files.pythonhosted.org/packages/e6/52/c921dc6d5f5d45b212a456c1f5b17df1a471127e8037eb0972379e39dff4/rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", size = 221339 }, + { url = "https://files.pythonhosted.org/packages/f2/c7/f82b5be1e8456600395366f86104d1bd8d0faed3802ad511ef6d60c30d98/rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", size = 235786 }, + { url = "https://files.pythonhosted.org/packages/d0/bf/36d5cc1f2c609ae6e8bf0fc35949355ca9d8790eceb66e6385680c951e60/rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84", size = 351657 }, + { url = "https://files.pythonhosted.org/packages/24/2a/f1e0fa124e300c26ea9382e59b2d582cba71cedd340f32d1447f4f29fa4e/rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25", size = 341829 }, + { url = "https://files.pythonhosted.org/packages/cf/c2/0da1231dd16953845bed60d1a586fcd6b15ceaeb965f4d35cdc71f70f606/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4", size = 384220 }, + { url = "https://files.pythonhosted.org/packages/c7/73/a4407f4e3a00a9d4b68c532bf2d873d6b562854a8eaff8faa6133b3588ec/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5", size = 391009 }, + { url = "https://files.pythonhosted.org/packages/a9/c3/04b7353477ab360fe2563f5f0b176d2105982f97cd9ae80a9c5a18f1ae0f/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc", size = 426989 }, + { url = "https://files.pythonhosted.org/packages/8d/e6/e4b85b722bcf11398e17d59c0f6049d19cd606d35363221951e6d625fcb0/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b", size = 441544 }, + { url = "https://files.pythonhosted.org/packages/27/fc/403e65e56f65fff25f2973216974976d3f0a5c3f30e53758589b6dc9b79b/rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518", size = 385179 }, + { url = "https://files.pythonhosted.org/packages/57/9b/2be9ff9700d664d51fd96b33d6595791c496d2778cb0b2a634f048437a55/rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd", size = 415103 }, + { url = "https://files.pythonhosted.org/packages/bb/a5/03c2ad8ca10994fcf22dd2150dd1d653bc974fa82d9a590494c84c10c641/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2", size = 560916 }, + { url = "https://files.pythonhosted.org/packages/ba/2e/be4fdfc8b5b576e588782b56978c5b702c5a2307024120d8aeec1ab818f0/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16", size = 587062 }, + { url = "https://files.pythonhosted.org/packages/67/e0/2034c221937709bf9c542603d25ad43a68b4b0a9a0c0b06a742f2756eb66/rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f", size = 555734 }, + { url = "https://files.pythonhosted.org/packages/ea/ce/240bae07b5401a22482b58e18cfbabaa392409b2797da60223cca10d7367/rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de", size = 220663 }, + { url = "https://files.pythonhosted.org/packages/cb/f0/d330d08f51126330467edae2fa4efa5cec8923c87551a79299380fdea30d/rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9", size = 235503 }, + { url = "https://files.pythonhosted.org/packages/f7/c4/dbe1cc03df013bf2feb5ad00615038050e7859f381e96fb5b7b4572cd814/rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b", size = 347698 }, + { url = "https://files.pythonhosted.org/packages/a4/3a/684f66dd6b0f37499cad24cd1c0e523541fd768576fa5ce2d0a8799c3cba/rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b", size = 337330 }, + { url = "https://files.pythonhosted.org/packages/82/eb/e022c08c2ce2e8f7683baa313476492c0e2c1ca97227fe8a75d9f0181e95/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1", size = 380022 }, + { url = "https://files.pythonhosted.org/packages/e4/21/5a80e653e4c86aeb28eb4fea4add1f72e1787a3299687a9187105c3ee966/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83", size = 390754 }, + { url = "https://files.pythonhosted.org/packages/37/a4/d320a04ae90f72d080b3d74597074e62be0a8ecad7d7321312dfe2dc5a6a/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd", size = 423840 }, + { url = "https://files.pythonhosted.org/packages/87/70/674dc47d93db30a6624279284e5631be4c3a12a0340e8e4f349153546728/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1", size = 438970 }, + { url = "https://files.pythonhosted.org/packages/3f/64/9500f4d66601d55cadd21e90784cfd5d5f4560e129d72e4339823129171c/rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3", size = 383146 }, + { url = "https://files.pythonhosted.org/packages/4d/45/630327addb1d17173adcf4af01336fd0ee030c04798027dfcb50106001e0/rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130", size = 408294 }, + { url = "https://files.pythonhosted.org/packages/5f/ef/8efb3373cee54ea9d9980b772e5690a0c9e9214045a4e7fa35046e399fee/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c", size = 556345 }, + { url = "https://files.pythonhosted.org/packages/54/01/151d3b9ef4925fc8f15bfb131086c12ec3c3d6dd4a4f7589c335bf8e85ba/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b", size = 582292 }, + { url = "https://files.pythonhosted.org/packages/30/89/35fc7a6cdf3477d441c7aca5e9bbf5a14e0f25152aed7f63f4e0b141045d/rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333", size = 553855 }, + { url = "https://files.pythonhosted.org/packages/8f/e0/830c02b2457c4bd20a8c5bb394d31d81f57fbefce2dbdd2e31feff4f7003/rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730", size = 219100 }, + { url = "https://files.pythonhosted.org/packages/f8/30/7ac943f69855c2db77407ae363484b915d861702dbba1aa82d68d57f42be/rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", size = 233794 }, ] [[package]] name = "sentry-sdk" -version = "2.19.0" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/cc0e60f0e0cfd5a9e42622ff5a227301c6475a56bcfa82e8e893bc209f20/sentry_sdk-2.19.0.tar.gz", hash = "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36", size = 298045 } +sdist = { url = "https://files.pythonhosted.org/packages/36/4a/eccdcb8c2649d53440ae1902447b86e2e2ad1bc84207c80af9696fa07614/sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d", size = 299047 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/6b/191ca63f05d3ecc7600b5b3abd493a4c1b8468289c9737a7735ade1fedca/sentry_sdk-2.19.0-py2.py3-none-any.whl", hash = "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b", size = 322158 }, + { url = "https://files.pythonhosted.org/packages/31/4d/74597bb6bcc23abc774b8901277652c61331a9d4d0a8d1bdb20679b9bbcb/sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84", size = 322942 }, ] [package.optional-dependencies] @@ -2432,11 +2872,11 @@ wheels = [ [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[package]] @@ -2481,11 +2921,11 @@ wheels = [ [[package]] name = "sqlparse" -version = "0.5.2" +version = "0.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/61/5bc3aff85dc5bf98291b37cf469dab74b3d0aef2dd88eade9070a200af05/sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f", size = 84951 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/13/5f6654c9d915077fae255686ca6fa42095b62b7337e3e1aa9e82caa6f43a/sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e", size = 44407 }, + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, ] [[package]] @@ -2511,6 +2951,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, ] +[[package]] +name = "tensorboard" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "grpcio" }, + { name = "markdown" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "setuptools" }, + { name = "six" }, + { name = "tensorboard-data-server" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/de/021c1d407befb505791764ad2cbd56ceaaa53a746baed01d2e2143f05f18/tensorboard-2.18.0-py3-none-any.whl", hash = "sha256:107ca4821745f73e2aefa02c50ff70a9b694f39f790b11e6f682f7d326745eab", size = 5503036 }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356 }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598 }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363 }, +] + +[[package]] +name = "tensorflow" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "astunparse" }, + { name = "flatbuffers" }, + { name = "gast" }, + { name = "google-pasta" }, + { name = "grpcio" }, + { name = "h5py" }, + { name = "keras" }, + { name = "libclang" }, + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "opt-einsum" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "six" }, + { name = "tensorboard" }, + { name = "termcolor" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/bf/4cc283db323fd723f630e2454b2857054d2c81ff5012c1857659e72470f1/tensorflow-2.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ec4133a215c59314e929e7cbe914579d3afbc7874d9fa924873ee633fe4f71d0", size = 239565465 }, + { url = "https://files.pythonhosted.org/packages/56/e4/55aaac2b15af4dad079e5af329a79d961e5206589d0e02b1e8da221472ed/tensorflow-2.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4822904b3559d8a9c25f0fe5fef191cfc1352ceca42ca64f2a7bc7ae0ff4a1f5", size = 231898760 }, + { url = "https://files.pythonhosted.org/packages/50/29/61ce80da0bfea3948326697dd1d832d28c863c9dacf90a27ee80fd4c1d31/tensorflow-2.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfdd65ea7e064064283dd78d529dd621257ee617218f63681935fd15817c6286", size = 615520727 }, + { url = "https://files.pythonhosted.org/packages/eb/f1/828bbccc84a72db960a7d116f55f3f6aec9f5658f5d32ce9db20142d5742/tensorflow-2.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:a701c2d3dca5f2efcab315b2c217f140ebd3da80410744e87d77016b3aaf53cb", size = 7520 }, +] + +[[package]] +name = "termcolor" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, +] + +[[package]] +name = "tf-keras" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tensorflow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/a4/7d0acc28cde2b29b8114552ce3258dafdc6b2186d24bf8e912de713dd74f/tf_keras-2.18.0.tar.gz", hash = "sha256:ebf744519b322afead33086a2aba872245473294affd40973694f3eb7c7ad77d", size = 1260765 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/ed/e08afca471299b04a34cd548e64e89d0153eda0e6cf9b715356777e24774/tf_keras-2.18.0-py3-none-any.whl", hash = "sha256:c431d04027eef790fcd3261cf7fdf93eb74f3cb32e05078b57b7f5a54bd53262", size = 1725427 }, +] + [[package]] name = "tornado" version = "6.4.2" @@ -2529,6 +3054,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, ] +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + [[package]] name = "traitlets" version = "5.14.3" @@ -2574,11 +3111,11 @@ wheels = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20241003" +version = "2.9.0.20241206" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/f8/f6ee4c803a7beccffee21bb29a71573b39f7037c224843eff53e5308c16e/types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446", size = 9210 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/60/47d92293d9bc521cd2301e423a358abfac0ad409b3a1606d8fbae1321961/types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb", size = 13802 } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/d6/ba5f61958f358028f2e2ba1b8e225b8e263053bd57d3a79e2d2db64c807b/types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d", size = 9693 }, + { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384 }, ] [[package]] @@ -2644,7 +3181,7 @@ wheels = [ [[package]] name = "unicef-security" -version = "1.6.3" +version = "1.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "celery" }, @@ -2660,9 +3197,9 @@ dependencies = [ { name = "social-auth-app-django" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/e3/3e7921a6e8751b0abedf44d945147869410ab8c2672a304914cececee437/unicef_security-1.6.3.tar.gz", hash = "sha256:4a582edcfdf0085708fae529e3d083224d9aade485e16427e92f500c7f8af590", size = 30288 } +sdist = { url = "https://files.pythonhosted.org/packages/08/d9/f7e6d7fc842a2267dac6436b60006d9c30619846305ccc14d41e60ca3920/unicef_security-1.6.4.tar.gz", hash = "sha256:f52d29ab49b7c836a99a844de779ebc2f568c05b9fa9f952f85669d447dbc0c0", size = 83119 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/6a/314c7912d2aeb109659f1834fd5d178bd38532d16553dd19dc5783ef397c/unicef_security-1.6.3-py2.py3-none-any.whl", hash = "sha256:40968d5b840eb949ee054787feecb91a2947818023eb116658702ca051b8667a", size = 21145 }, + { url = "https://files.pythonhosted.org/packages/e1/90/1b55e21aab9afad6aa79636d4638165049974ce2aefb9ab4bce051812cbb/unicef_security-1.6.4-py2.py3-none-any.whl", hash = "sha256:f30eef2b5635db768708d49fe5af8f2ffca78b7f59768bcd09bd41d4f5c52dac", size = 52953 }, ] [[package]] @@ -2804,6 +3341,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/be/4cfe50dbe2e0b8e64a421157e18bbdbdb58a12ffc7ebcf5d32845ae56146/WebTest-3.0.2-py3-none-any.whl", hash = "sha256:799846e169d15e0c1233ab4ab00ee4de59a5d964407d6f2945d89249328dbbdb", size = 32152 }, ] +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +] + [[package]] name = "wheel" version = "0.45.1" @@ -2825,6 +3374,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/ca/723e3f8185738d7947f14ee7dc663b59415c6dee43bd71575f8c7f5cd6be/wmctrl-0.5-py2.py3-none-any.whl", hash = "sha256:ae695c1863a314c899e7cf113f07c0da02a394b968c4772e1936219d9234ddd7", size = 4268 }, ] +[[package]] +name = "wrapt" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 }, + { url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 }, + { url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 }, + { url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 }, + { url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 }, + { url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 }, + { url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 }, + { url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 }, + { url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 }, + { url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 }, + { url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 }, + { url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 }, + { url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 }, + { url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 }, + { url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 }, + { url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 }, + { url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 }, + { url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 }, + { url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 }, + { url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 }, + { url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 }, + { url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 }, + { url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 }, + { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 }, + { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 }, + { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 }, + { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, +] + [[package]] name = "xlrd" version = "2.0.1"