From 48f49de87bf53f9761d341181e2373cfa6d46922 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 15 Apr 2026 18:50:25 +0300 Subject: [PATCH 001/106] COST-7252: Implement constant currency Phase 1 Add static exchange rate CRUD, per-month rate storage via MonthlyExchangeRate, currency enablement settings, and Subquery-based exchange rate resolution in query handlers and forecasts. New models (tenant-scoped in cost_models app): - StaticExchangeRate: user-defined rates with monthly validity periods - MonthlyExchangeRate: single source of truth for all months - EnabledCurrency: controls dropdown visibility per tenant New endpoints: - GET/POST/PUT/DELETE /exchange-rate-pairs/ (static rate CRUD) - GET/PUT /settings/currency/enabled-currencies/ - GET /settings/currency/available-currencies/ Pipeline changes: - get_daily_currency_rates: currency discovery, per-tenant MonthlyExchangeRate upsert, CURRENCY_URL skip when empty - QueryHandler/ReportQueryHandler: Subquery annotation from MonthlyExchangeRate with earliest-rate fallback - OCP query handler: dual Subquery (cost model + infra currency) - Forecast handlers: same Subquery pattern - Report responses: exchange_rates_applied metadata - Cache invalidation on both write paths Made-with: Cursor --- koku/api/query_handler.py | 42 ++-- koku/api/report/ocp/query_handler.py | 72 +++--- koku/api/report/queries.py | 124 ++++++++-- koku/api/settings/currency_views.py | 97 ++++++++ koku/api/urls.py | 12 + .../migrations/0012_staticexchangerate.py | 39 ++++ .../migrations/0013_monthlyexchangerate.py | 68 ++++++ .../migrations/0014_enabledcurrency.py | 33 +++ koku/cost_models/models.py | 62 +++++ .../static_exchange_rate_serializer.py | 219 ++++++++++++++++++ koku/cost_models/static_exchange_rate_view.py | 65 ++++++ .../cost_models/test/test_enabled_currency.py | 103 ++++++++ .../test/test_monthly_exchange_rate.py | 121 ++++++++++ .../test_static_exchange_rate_serializer.py | 174 ++++++++++++++ .../test/test_static_exchange_rate_view.py | 114 +++++++++ koku/cost_models/urls.py | 2 + koku/forecast/forecast.py | 113 +++++---- koku/masu/celery/tasks.py | 57 ++++- 18 files changed, 1406 insertions(+), 111 deletions(-) create mode 100644 koku/api/settings/currency_views.py create mode 100644 koku/cost_models/migrations/0012_staticexchangerate.py create mode 100644 koku/cost_models/migrations/0013_monthlyexchangerate.py create mode 100644 koku/cost_models/migrations/0014_enabledcurrency.py create mode 100644 koku/cost_models/static_exchange_rate_serializer.py create mode 100644 koku/cost_models/static_exchange_rate_view.py create mode 100644 koku/cost_models/test/test_enabled_currency.py create mode 100644 koku/cost_models/test/test_monthly_exchange_rate.py create mode 100644 koku/cost_models/test/test_static_exchange_rate_serializer.py create mode 100644 koku/cost_models/test/test_static_exchange_rate_view.py diff --git a/koku/api/query_handler.py b/koku/api/query_handler.py index 82b88e84e0..1c96810d27 100644 --- a/koku/api/query_handler.py +++ b/koku/api/query_handler.py @@ -11,20 +11,19 @@ from dateutil import relativedelta from django.conf import settings from django.core.exceptions import FieldDoesNotExist -from django.db.models import Case from django.db.models import DecimalField -from django.db.models import Value -from django.db.models import When +from django.db.models import Subquery +from django.db.models.functions import Coalesce from django.db.models.functions import TruncDay from django.db.models.functions import TruncMonth -from api.currency.models import ExchangeRateDictionary from api.query_filter import QueryFilter from api.query_filter import QueryFilterCollection from api.report.constants import RESOLUTION_DAILY from api.report.constants import TIME_SCOPE_UNITS_DAILY from api.report.constants import TIME_SCOPE_VALUES_DAILY from api.utils import DateHelper +from cost_models.models import MonthlyExchangeRate LOG = logging.getLogger(__name__) WILDCARD = "*" @@ -120,22 +119,29 @@ def has_wildcard(in_list): return False return any(WILDCARD == item for item in in_list) - @cached_property - def exchange_rates(self): - try: - return ExchangeRateDictionary.objects.first().currency_exchange_dictionary - except AttributeError as err: - LOG.warning(f"Exchange rates dictionary is not populated resulting in {err}.") - return {} - @cached_property def exchange_rate_annotation_dict(self): - """Get the exchange rate annotation based on the exchange_rates property.""" - whens = [ - When(**{self._mapper.cost_units_key: k, "then": Value(v.get(self.currency))}) - for k, v in self.exchange_rates.items() - ] - return {"exchange_rate": Case(*whens, default=1, output_field=DecimalField())} + """Get per-month exchange rate annotation from MonthlyExchangeRate via Subquery.""" + from django.db.models import OuterRef + + rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=OuterRef(self._mapper.cost_units_key), + target_currency=self.currency, + ).values("exchange_rate")[:1] + + earliest_rate_subquery = MonthlyExchangeRate.objects.filter( + base_currency=OuterRef(self._mapper.cost_units_key), + target_currency=self.currency, + ).order_by("effective_date").values("exchange_rate")[:1] + + return { + "exchange_rate": Coalesce( + Subquery(rate_subquery), + Subquery(earliest_rate_subquery), + output_field=DecimalField(), + ) + } @property def order(self): diff --git a/koku/api/report/ocp/query_handler.py b/koku/api/report/ocp/query_handler.py index a3d878400a..3f13225852 100644 --- a/koku/api/report/ocp/query_handler.py +++ b/koku/api/report/ocp/query_handler.py @@ -15,10 +15,13 @@ from django.db.models import CharField from django.db.models import DecimalField from django.db.models import F +from django.db.models import OuterRef +from django.db.models import Subquery from django.db.models import Value from django.db.models import When from django.db.models.fields.json import KT from django.db.models.functions import Coalesce +from django.db.models.functions import TruncMonth from django_tenants.utils import tenant_context from api.models import Provider @@ -30,7 +33,7 @@ from api.report.queries import is_grouped_by_project from api.report.queries import ReportQueryHandler from cost_models.models import CostModel -from cost_models.models import CostModelMap +from cost_models.models import MonthlyExchangeRate LOG = logging.getLogger(__name__) @@ -161,35 +164,50 @@ def annotations(self): return annotations @cached_property - def source_to_currency_map(self): - """ - OCP sources do not have costs associated, so we need to - grab the base currency from the cost model, and create - a mapping of source_uuid to currency. - returns: - dict: {source_uuid: currency} + def exchange_rate_annotation_dict(self): + """Get per-month exchange rate annotations from MonthlyExchangeRate via Subquery. + + OCP needs two annotations: + - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) + - infra_exchange_rate: cloud bill currency (raw_currency column) """ - source_map = defaultdict(lambda: self._mapper.cost_units_fallback) - cost_models = CostModel.objects.all().values("uuid", "currency").distinct() - cm_to_currency = {row["uuid"]: row["currency"] for row in cost_models} - mapping = CostModelMap.objects.all().values("provider_uuid", "cost_model_id") - source_map |= {row["provider_uuid"]: cm_to_currency[row["cost_model_id"]] for row in mapping} - return source_map + cost_model_currency = CostModel.objects.filter( + cost_model_map__provider_uuid=OuterRef("source_uuid"), + ).values("currency")[:1] + + exchange_rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=Subquery(cost_model_currency), + target_currency=self.currency, + ).values("exchange_rate")[:1] + + earliest_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( + base_currency=Subquery(cost_model_currency), + target_currency=self.currency, + ).order_by("effective_date").values("exchange_rate")[:1] + + infra_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=OuterRef(self._mapper.cost_units_key), + target_currency=self.currency, + ).values("exchange_rate")[:1] + + earliest_infra_rate_subquery = MonthlyExchangeRate.objects.filter( + base_currency=OuterRef(self._mapper.cost_units_key), + target_currency=self.currency, + ).order_by("effective_date").values("exchange_rate")[:1] - @cached_property - def exchange_rate_annotation_dict(self): - """Get the exchange rate annotation based on the exchange_rates property.""" - exchange_rate_whens = [ - When(**{"source_uuid": uuid, "then": Value(self.exchange_rates.get(cur, {}).get(self.currency, 1))}) - for uuid, cur in self.source_to_currency_map.items() - ] - infra_exchange_rate_whens = [ - When(**{self._mapper.cost_units_key: k, "then": Value(v.get(self.currency))}) - for k, v in self.exchange_rates.items() - ] return { - "exchange_rate": Case(*exchange_rate_whens, default=1, output_field=DecimalField()), - "infra_exchange_rate": Case(*infra_exchange_rate_whens, default=1, output_field=DecimalField()), + "exchange_rate": Coalesce( + Subquery(exchange_rate_subquery), + Subquery(earliest_exchange_rate_subquery), + output_field=DecimalField(), + ), + "infra_exchange_rate": Coalesce( + Subquery(infra_exchange_rate_subquery), + Subquery(earliest_infra_rate_subquery), + output_field=DecimalField(), + ), } def format_tags(self, tags_iterable): diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 5b0afc5465..9d06101c4c 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # """Query Handling for Reports.""" +import calendar import copy import logging import random @@ -36,8 +37,8 @@ from django.db.models.functions import Concat from django.db.models.functions import RowNumber from pandas.api.types import CategoricalDtype +from rest_framework.exceptions import ValidationError -from api.currency.models import ExchangeRateDictionary from api.models import Provider from api.query_filter import QueryFilter from api.query_filter import QueryFilterCollection @@ -45,6 +46,7 @@ from api.report.constants import AWS_CATEGORY_PREFIX from api.report.constants import TAG_PREFIX from api.report.constants import URL_ENCODED_SAFE +from cost_models.models import MonthlyExchangeRate LOG = logging.getLogger(__name__) @@ -880,22 +882,31 @@ def _aws_category_group_by(self) -> list[tuple[str, int, str]]: group_by.append((db_name, group_pos, original_aws_category)) return group_by - @cached_property - def exchange_rates(self): - try: - return ExchangeRateDictionary.objects.first().currency_exchange_dictionary - except AttributeError as err: - LOG.warning(f"Exchange rates dictionary is not populated resulting in {err}.") - return {} - @cached_property def exchange_rate_annotation_dict(self): - """Get the exchange rate annotation based on the exchange_rates property.""" - whens = [ - When(**{self._mapper.cost_units_key: k, "then": Value(v.get(self.currency))}) - for k, v in self.exchange_rates.items() - ] - return {"exchange_rate": Case(*whens, default=1, output_field=DecimalField())} + """Get per-month exchange rate annotation from MonthlyExchangeRate via Subquery.""" + from django.db.models import OuterRef + from django.db.models import Subquery + from django.db.models.functions import TruncMonth + + rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=OuterRef(self._mapper.cost_units_key), + target_currency=self.currency, + ).values("exchange_rate")[:1] + + earliest_rate_subquery = MonthlyExchangeRate.objects.filter( + base_currency=OuterRef(self._mapper.cost_units_key), + target_currency=self.currency, + ).order_by("effective_date").values("exchange_rate")[:1] + + return { + "exchange_rate": Coalesce( + Subquery(rate_subquery), + Subquery(earliest_rate_subquery), + output_field=DecimalField(), + ) + } def _project_classification_annotation(self, query_data): """Get the correct annotation for a project or category""" @@ -1067,11 +1078,92 @@ def _apply_group_by(self, query_data, group_by=None): def _initialize_response_output(self, parameters): """Initialize output response object.""" output = copy.deepcopy(parameters.parameters) - # remove access from the output output.pop("access") + if self.currency: + output["currency"] = self.currency + start = getattr(self, "start_datetime", None) + end = getattr(self, "end_datetime", None) + if start and end: + output["exchange_rates_applied"] = self._get_exchange_rates_applied( + start.date() if hasattr(start, "date") else start, + end.date() if hasattr(end, "date") else end, + self.currency, + ) + return output + def _get_exchange_rates_applied(self, start_date, end_date, target_currency): + """Build exchange_rates_applied metadata from MonthlyExchangeRate for the query range.""" + if not target_currency: + return [] + + from dateutil.relativedelta import relativedelta + + start_month = start_date.replace(day=1) if start_date else None + end_month = end_date.replace(day=1) if end_date else None + if not start_month or not end_month: + return [] + + rates = ( + MonthlyExchangeRate.objects.filter( + effective_date__gte=start_month, + effective_date__lte=end_month, + target_currency=target_currency, + ) + .order_by("base_currency", "effective_date") + .values("base_currency", "target_currency", "exchange_rate", "rate_type", "effective_date") + ) + + result = [] + for rate in rates: + last_day = calendar.monthrange(rate["effective_date"].year, rate["effective_date"].month)[1] + end_of_month = rate["effective_date"].replace(day=last_day) + + if result and ( + result[-1]["base_currency"] == rate["base_currency"] + and result[-1]["target_currency"] == rate["target_currency"] + and result[-1]["rate"] == str(rate["exchange_rate"]) + and result[-1]["type"] == rate["rate_type"] + and result[-1]["_next_month"] == rate["effective_date"] + ): + result[-1]["end_date"] = str(end_of_month) + result[-1]["_next_month"] = rate["effective_date"] + relativedelta(months=1) + else: + result.append( + { + "base_currency": rate["base_currency"], + "target_currency": rate["target_currency"], + "rate": str(rate["exchange_rate"]), + "type": rate["rate_type"], + "start_date": str(rate["effective_date"]), + "end_date": str(end_of_month), + "_next_month": rate["effective_date"] + relativedelta(months=1), + } + ) + + for entry in result: + entry.pop("_next_month", None) + return result + + def _validate_exchange_rates(self, queryset): + """Raise if any rows have NULL exchange rates (no rate data for currency pair).""" + if not self.currency: + return + + null_rate_filter = Q(exchange_rate__isnull=True) + if queryset.filter(null_rate_filter).exists(): + raise ValidationError( + { + "detail": ( + f"No exchange rate available for target currency {self.currency}. " + "Ask your administrator to configure static exchange rates " + "or enable dynamic exchange rates." + ), + "source": "currency", + } + ) + def _pack_data_object(self, data, **kwargs): # noqa: C901 """Pack data into object format.""" if not isinstance(data, dict): diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py new file mode 100644 index 0000000000..8f3e84b5a6 --- /dev/null +++ b/koku/api/settings/currency_views.py @@ -0,0 +1,97 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Views for currency enablement and available currencies.""" +import logging + +from django.utils.decorators import method_decorator +from django.views.decorators.cache import never_cache +from rest_framework import serializers +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView + +from api.common import log_json +from api.common.pagination import ListPaginator +from api.common.permissions.settings_access import SettingsAccessPermission +from cost_models.models import EnabledCurrency +from cost_models.models import StaticExchangeRate + +LOG = logging.getLogger(__name__) + + +class EnabledCurrencyItemSerializer(serializers.Serializer): + currency_code = serializers.CharField(max_length=5) + enabled = serializers.BooleanField() + + +class EnabledCurrencyUpdateSerializer(serializers.Serializer): + currencies = EnabledCurrencyItemSerializer(many=True) + + +class EnabledCurrencyView(APIView): + """List and update enabled/disabled currencies for a tenant.""" + + permission_classes = [SettingsAccessPermission] + + @method_decorator(never_cache) + def get(self, request, *args, **kwargs): + currencies = EnabledCurrency.objects.all().values("currency_code", "enabled") + data = list(currencies) + paginator = ListPaginator(data, request) + return paginator.get_paginated_response(data) + + @method_decorator(never_cache) + def put(self, request, *args, **kwargs): + serializer = EnabledCurrencyUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + for item in serializer.validated_data["currencies"]: + EnabledCurrency.objects.update_or_create( + currency_code=item["currency_code"].upper(), + defaults={"enabled": item["enabled"]}, + ) + + LOG.info( + log_json( + msg="Enabled currencies updated", + count=len(serializer.validated_data["currencies"]), + ) + ) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class AvailableCurrencyView(APIView): + """Returns currencies visible in the target currency dropdown.""" + + permission_classes = [SettingsAccessPermission] + + @method_decorator(never_cache) + def get(self, request, *args, **kwargs): + enabled_codes = set( + EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True) + ) + + static_bases = set( + StaticExchangeRate.objects.values_list("base_currency", flat=True) + ) + static_targets = set( + StaticExchangeRate.objects.values_list("target_currency", flat=True) + ) + static_currencies = static_bases | static_targets + + data = [] + for code in sorted(enabled_codes | static_currencies): + in_dynamic = code in enabled_codes + in_static = code in static_currencies + if in_dynamic and in_static: + source = "both" + elif in_static: + source = "static" + else: + source = "dynamic" + data.append({"currency_code": code, "source": source}) + + paginator = ListPaginator(data, request) + return paginator.get_paginated_response(data) diff --git a/koku/api/urls.py b/koku/api/urls.py index 47905d3ab4..07a4478687 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -9,6 +9,8 @@ from rest_framework.routers import DefaultRouter from api.common.deprecate_view import SunsetView +from api.settings.currency_views import AvailableCurrencyView +from api.settings.currency_views import EnabledCurrencyView from api.views import AccountSettings from api.views import AWSAccountRegionView from api.views import AWSAccountView @@ -130,6 +132,16 @@ path("cost-type/", UserCostTypeSettings.as_view(), name="cost-type"), path("account-settings/", AccountSettings.as_view(), name="account-settings"), path("account-settings//", AccountSettings.as_view(), name="get-account-setting"), + path( + "settings/currency/enabled-currencies/", + EnabledCurrencyView.as_view(), + name="enabled-currencies", + ), + path( + "settings/currency/available-currencies/", + AvailableCurrencyView.as_view(), + name="available-currencies", + ), path("status/", StatusView.as_view(), name="server-status"), path("openapi.json", openapi, name="openapi"), path("metrics/", metrics, name="metrics"), diff --git a/koku/cost_models/migrations/0012_staticexchangerate.py b/koku/cost_models/migrations/0012_staticexchangerate.py new file mode 100644 index 0000000000..a9d633edcb --- /dev/null +++ b/koku/cost_models/migrations/0012_staticexchangerate.py @@ -0,0 +1,39 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +import uuid + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cost_models", "0011_migrate_cost_model_rates_to_price_lists"), + ] + + operations = [ + migrations.CreateModel( + name="StaticExchangeRate", + fields=[ + ( + "uuid", + models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + ("base_currency", models.CharField(max_length=5)), + ("target_currency", models.CharField(max_length=5)), + ("exchange_rate", models.DecimalField(decimal_places=15, max_digits=33)), + ("start_date", models.DateField()), + ("end_date", models.DateField()), + ("version", models.IntegerField(default=1)), + ("created_timestamp", models.DateTimeField(auto_now_add=True)), + ("updated_timestamp", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "static_exchange_rate", + "ordering": ["-updated_timestamp"], + }, + ), + ] diff --git a/koku/cost_models/migrations/0013_monthlyexchangerate.py b/koku/cost_models/migrations/0013_monthlyexchangerate.py new file mode 100644 index 0000000000..442c2baa6e --- /dev/null +++ b/koku/cost_models/migrations/0013_monthlyexchangerate.py @@ -0,0 +1,68 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +from datetime import date + +from django.db import migrations +from django.db import models + + +def seed_current_month(apps, schema_editor): + """Seed MonthlyExchangeRate with current-month rates from ExchangeRateDictionary.""" + ExchangeRateDictionary = apps.get_model("api", "ExchangeRateDictionary") + MonthlyExchangeRate = apps.get_model("cost_models", "MonthlyExchangeRate") + + erd = ExchangeRateDictionary.objects.first() + if not erd or not erd.currency_exchange_dictionary: + return + + current_month = date.today().replace(day=1) + + rows = [] + for base_cur, targets in erd.currency_exchange_dictionary.items(): + for target_cur, rate in targets.items(): + if base_cur == target_cur: + continue + rows.append( + MonthlyExchangeRate( + effective_date=current_month, + base_currency=base_cur, + target_currency=target_cur, + exchange_rate=rate, + rate_type="dynamic", + ) + ) + MonthlyExchangeRate.objects.bulk_create(rows, ignore_conflicts=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ("cost_models", "0012_staticexchangerate"), + ("api", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="MonthlyExchangeRate", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("effective_date", models.DateField()), + ("base_currency", models.CharField(max_length=5)), + ("target_currency", models.CharField(max_length=5)), + ("exchange_rate", models.DecimalField(decimal_places=15, max_digits=33)), + ("rate_type", models.CharField(choices=[("static", "Static"), ("dynamic", "Dynamic")], max_length=10)), + ("created_timestamp", models.DateTimeField(auto_now_add=True)), + ("updated_timestamp", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "monthly_exchange_rate", + "unique_together": {("effective_date", "base_currency", "target_currency")}, + }, + ), + migrations.RunPython(seed_current_month, migrations.RunPython.noop), + ] diff --git a/koku/cost_models/migrations/0014_enabledcurrency.py b/koku/cost_models/migrations/0014_enabledcurrency.py new file mode 100644 index 0000000000..13e8c24ae2 --- /dev/null +++ b/koku/cost_models/migrations/0014_enabledcurrency.py @@ -0,0 +1,33 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cost_models", "0013_monthlyexchangerate"), + ] + + operations = [ + migrations.CreateModel( + name="EnabledCurrency", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("currency_code", models.CharField(max_length=5, unique=True)), + ("enabled", models.BooleanField(default=False)), + ("created_timestamp", models.DateTimeField(auto_now_add=True)), + ("updated_timestamp", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "enabled_currency", + "ordering": ["currency_code"], + }, + ), + ] diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index 1a072595c8..9dabef2163 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -13,6 +13,8 @@ from api.provider.models import Provider from koku.settings import KOKU_DEFAULT_CURRENCY +VALID_RATE_TYPES = ("static", "dynamic") + DISTRIBUTION_CHOICES = (("memory", "memory"), ("cpu", "cpu")) DEFAULT_DISTRIBUTION = "cpu" @@ -153,3 +155,63 @@ class Meta: cost_model = models.ForeignKey("CostModel", on_delete=models.CASCADE, related_name="price_list_maps") priority = models.PositiveIntegerField() + + +class RateType(models.TextChoices): + STATIC = "static", "Static" + DYNAMIC = "dynamic", "Dynamic" + + +class StaticExchangeRate(models.Model): + """User-defined exchange rates with validity periods.""" + + class Meta: + db_table = "static_exchange_rate" + ordering = ["-updated_timestamp"] + + uuid = models.UUIDField(primary_key=True, default=uuid4) + base_currency = models.CharField(max_length=5) + target_currency = models.CharField(max_length=5) + exchange_rate = models.DecimalField(max_digits=33, decimal_places=15) + start_date = models.DateField() + end_date = models.DateField() + version = models.IntegerField(default=1) + created_timestamp = models.DateTimeField(auto_now_add=True) + updated_timestamp = models.DateTimeField(auto_now=True) + + @property + def name(self): + return f"{self.base_currency}-{self.target_currency}" + + +class EnabledCurrency(models.Model): + """Tracks which currencies are visible in the target currency dropdown.""" + + class Meta: + db_table = "enabled_currency" + ordering = ["currency_code"] + + currency_code = models.CharField(max_length=5, unique=True) + enabled = models.BooleanField(default=False) + created_timestamp = models.DateTimeField(auto_now_add=True) + updated_timestamp = models.DateTimeField(auto_now=True) + + +class MonthlyExchangeRate(models.Model): + """Single source of truth for exchange rates used in reports. + + Stores both static and dynamic rates as per-pair rows, one row per month. + The query handler reads from this table for all months. + """ + + class Meta: + db_table = "monthly_exchange_rate" + unique_together = ("effective_date", "base_currency", "target_currency") + + effective_date = models.DateField() + base_currency = models.CharField(max_length=5) + target_currency = models.CharField(max_length=5) + exchange_rate = models.DecimalField(max_digits=33, decimal_places=15) + rate_type = models.CharField(max_length=10, choices=RateType.choices) + created_timestamp = models.DateTimeField(auto_now_add=True) + updated_timestamp = models.DateTimeField(auto_now=True) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py new file mode 100644 index 0000000000..631bb7b98c --- /dev/null +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -0,0 +1,219 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Serializer for StaticExchangeRate with MonthlyExchangeRate side effects.""" +import calendar +import logging +from datetime import date + +from dateutil.relativedelta import relativedelta +from django.db import transaction +from rest_framework import serializers + +from api.common import log_json +from api.currency.currencies import VALID_CURRENCIES +from api.currency.models import ExchangeRateDictionary +from cost_models.models import EnabledCurrency +from cost_models.models import MonthlyExchangeRate +from cost_models.models import RateType +from cost_models.models import StaticExchangeRate +from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types + +LOG = logging.getLogger(__name__) + + +def _iter_months(start_date, end_date): + """Yield the first day of each month between start_date and end_date inclusive.""" + current = start_date.replace(day=1) + end = end_date.replace(day=1) + while current <= end: + yield current + current += relativedelta(months=1) + + +def _upsert_monthly_rates(static_rate): + """Upsert MonthlyExchangeRate rows with rate_type=static for each month in the validity period.""" + for month_start in _iter_months(static_rate.start_date, static_rate.end_date): + MonthlyExchangeRate.objects.update_or_create( + effective_date=month_start, + base_currency=static_rate.base_currency, + target_currency=static_rate.target_currency, + defaults={ + "exchange_rate": static_rate.exchange_rate, + "rate_type": RateType.STATIC, + }, + ) + + +def _remove_static_and_backfill_dynamic(base_currency, target_currency, start_date, end_date): + """Remove static rows for affected months and backfill with dynamic rates from ExchangeRateDictionary.""" + MonthlyExchangeRate.objects.filter( + effective_date__gte=start_date.replace(day=1), + effective_date__lte=end_date.replace(day=1), + base_currency=base_currency, + target_currency=target_currency, + rate_type=RateType.STATIC, + ).delete() + + erd = ExchangeRateDictionary.objects.first() + if not erd or not erd.currency_exchange_dictionary: + return + + exchange_dict = erd.currency_exchange_dictionary + rate = exchange_dict.get(base_currency, {}).get(target_currency) + if rate is None: + return + + for month_start in _iter_months(start_date, end_date): + MonthlyExchangeRate.objects.update_or_create( + effective_date=month_start, + base_currency=base_currency, + target_currency=target_currency, + defaults={ + "exchange_rate": rate, + "rate_type": RateType.DYNAMIC, + }, + ) + + +class StaticExchangeRateSerializer(serializers.ModelSerializer): + name = serializers.SerializerMethodField() + + class Meta: + model = StaticExchangeRate + fields = [ + "uuid", + "name", + "base_currency", + "target_currency", + "exchange_rate", + "start_date", + "end_date", + "version", + "created_timestamp", + "updated_timestamp", + ] + read_only_fields = ["uuid", "name", "version", "created_timestamp", "updated_timestamp"] + + def get_name(self, obj): + return f"{obj.base_currency}-{obj.target_currency}" + + def validate_base_currency(self, value): + value = value.upper() + if value not in VALID_CURRENCIES: + raise serializers.ValidationError(f"Invalid currency code: {value}") + return value + + def validate_target_currency(self, value): + value = value.upper() + if value not in VALID_CURRENCIES: + raise serializers.ValidationError(f"Invalid currency code: {value}") + return value + + def validate_start_date(self, value): + if value.day != 1: + raise serializers.ValidationError("start_date must be the first day of a month.") + return value + + def validate_end_date(self, value): + last_day = calendar.monthrange(value.year, value.month)[1] + if value.day != last_day: + raise serializers.ValidationError("end_date must be the last day of a month.") + return value + + def validate(self, data): + base = data.get("base_currency") or (self.instance.base_currency if self.instance else None) + target = data.get("target_currency") or (self.instance.target_currency if self.instance else None) + start = data.get("start_date") or (self.instance.start_date if self.instance else None) + end = data.get("end_date") or (self.instance.end_date if self.instance else None) + + if base and target and base == target: + raise serializers.ValidationError("base_currency and target_currency must be different.") + + if start and end and start > end: + raise serializers.ValidationError("start_date must be on or before end_date.") + + if base and target and start and end: + overlap_qs = StaticExchangeRate.objects.filter( + base_currency=base, + target_currency=target, + start_date__lte=end, + end_date__gte=start, + ) + if self.instance: + overlap_qs = overlap_qs.exclude(uuid=self.instance.uuid) + if overlap_qs.exists(): + raise serializers.ValidationError( + "Overlapping validity period exists for this currency pair." + ) + + return data + + def _get_schema_name(self): + request = self.context.get("request") + if request and hasattr(request, "user") and hasattr(request.user, "customer"): + return request.user.customer.schema_name + return None + + @transaction.atomic + def create(self, validated_data): + instance = StaticExchangeRate.objects.create(**validated_data) + _upsert_monthly_rates(instance) + schema_name = self._get_schema_name() + if schema_name: + invalidate_view_cache_for_tenant_and_all_source_types(schema_name) + LOG.info( + log_json( + msg="Static exchange rate created", + pair=instance.name, + start=str(instance.start_date), + end=str(instance.end_date), + ) + ) + return instance + + @transaction.atomic + def update(self, instance, validated_data): + old_start = instance.start_date + old_end = instance.end_date + old_base = instance.base_currency + old_target = instance.target_currency + + validated_data["version"] = instance.version + 1 + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + + if old_base != instance.base_currency or old_target != instance.target_currency: + _remove_static_and_backfill_dynamic(old_base, old_target, old_start, old_end) + + _upsert_monthly_rates(instance) + + schema_name = self._get_schema_name() + if schema_name: + invalidate_view_cache_for_tenant_and_all_source_types(schema_name) + LOG.info( + log_json( + msg="Static exchange rate updated", + pair=instance.name, + version=instance.version, + ) + ) + return instance + + @transaction.atomic + def delete(self, instance): + _remove_static_and_backfill_dynamic( + instance.base_currency, + instance.target_currency, + instance.start_date, + instance.end_date, + ) + pair_name = instance.name + instance.delete() + schema_name = self._get_schema_name() + if schema_name: + invalidate_view_cache_for_tenant_and_all_source_types(schema_name) + LOG.info(log_json(msg="Static exchange rate deleted", pair=pair_name)) diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py new file mode 100644 index 0000000000..a03d17ed84 --- /dev/null +++ b/koku/cost_models/static_exchange_rate_view.py @@ -0,0 +1,65 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""View for StaticExchangeRate CRUD operations.""" +import logging + +from django.utils.decorators import method_decorator +from django.views.decorators.cache import never_cache +from rest_framework import status +from rest_framework import viewsets +from rest_framework.response import Response + +from api.common import log_json +from api.common.permissions.cost_models_access import CostModelsAccessPermission +from cost_models.models import StaticExchangeRate +from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer + +LOG = logging.getLogger(__name__) + + +class StaticExchangeRateViewSet(viewsets.ModelViewSet): + """CRUD for static exchange rate pairs.""" + + queryset = StaticExchangeRate.objects.all() + serializer_class = StaticExchangeRateSerializer + lookup_field = "uuid" + permission_classes = (CostModelsAccessPermission,) + http_method_names = ["get", "post", "put", "delete", "head"] + + def get_queryset(self): + qs = super().get_queryset() + params = self.request.query_params + if base := params.get("base_currency"): + qs = qs.filter(base_currency=base.upper()) + if target := params.get("target_currency"): + qs = qs.filter(target_currency=target.upper()) + if start_date := params.get("start_date"): + qs = qs.filter(end_date__gte=start_date) + if end_date := params.get("end_date"): + qs = qs.filter(start_date__lte=end_date) + return qs + + @method_decorator(never_cache) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + @method_decorator(never_cache) + def retrieve(self, request, *args, **kwargs): + return super().retrieve(request, *args, **kwargs) + + @method_decorator(never_cache) + def create(self, request, *args, **kwargs): + return super().create(request, *args, **kwargs) + + @method_decorator(never_cache) + def update(self, request, *args, **kwargs): + return super().update(request, *args, **kwargs) + + @method_decorator(never_cache) + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance) + serializer.delete(instance) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py new file mode 100644 index 0000000000..fed8410bb7 --- /dev/null +++ b/koku/cost_models/test/test_enabled_currency.py @@ -0,0 +1,103 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for EnabledCurrency and AvailableCurrency views.""" +from django.urls import reverse +from django_tenants.utils import tenant_context +from rest_framework import status +from rest_framework.test import APIClient + +from api.iam.test.iam_test_case import IamTestCase +from cost_models.models import EnabledCurrency +from cost_models.models import StaticExchangeRate + + +class EnabledCurrencyViewTest(IamTestCase): + """Tests for EnabledCurrencyView.""" + + def setUp(self): + super().setUp() + self.client = APIClient() + self.client.force_authenticate(user=self.request_context["request"].user) + self.url = reverse("enabled-currencies") + + def test_get_enabled_currencies_empty(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_get_enabled_currencies(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD", enabled=True) + EnabledCurrency.objects.create(currency_code="EUR", enabled=False) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_put_enable_currencies(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="GBP", enabled=False) + data = {"currencies": [{"currency_code": "GBP", "enabled": True}]} + response = self.client.put(self.url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue(EnabledCurrency.objects.get(currency_code="GBP").enabled) + + def test_put_creates_new_currency(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + data = {"currencies": [{"currency_code": "JPY", "enabled": True}]} + response = self.client.put(self.url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="JPY", enabled=True).exists()) + + +class AvailableCurrencyViewTest(IamTestCase): + """Tests for AvailableCurrencyView.""" + + def setUp(self): + super().setUp() + self.client = APIClient() + self.client.force_authenticate(user=self.request_context["request"].user) + self.url = reverse("available-currencies") + + def test_get_available_currencies_dynamic_only(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + StaticExchangeRate.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD", enabled=True) + EnabledCurrency.objects.create(currency_code="EUR", enabled=True) + EnabledCurrency.objects.create(currency_code="GBP", enabled=False) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + codes = [c["currency_code"] for c in response.data["data"]] + self.assertIn("USD", codes) + self.assertIn("EUR", codes) + self.assertNotIn("GBP", codes) + + def test_get_available_currencies_static_included(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + StaticExchangeRate.objects.all().delete() + StaticExchangeRate.objects.create( + base_currency="EUR", + target_currency="CHF", + exchange_rate="1.080000000000000", + start_date="2026-01-01", + end_date="2026-01-31", + ) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + codes = [c["currency_code"] for c in response.data["data"]] + self.assertIn("EUR", codes) + self.assertIn("CHF", codes) + + def test_get_available_currencies_empty(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + StaticExchangeRate.objects.all().delete() + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["data"]), 0) diff --git a/koku/cost_models/test/test_monthly_exchange_rate.py b/koku/cost_models/test/test_monthly_exchange_rate.py new file mode 100644 index 0000000000..8ef9a6c642 --- /dev/null +++ b/koku/cost_models/test/test_monthly_exchange_rate.py @@ -0,0 +1,121 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for the MonthlyExchangeRate and EnabledCurrency models.""" +from datetime import date +from decimal import Decimal + +from django.db import IntegrityError +from django_tenants.utils import tenant_context + +from cost_models.models import EnabledCurrency +from cost_models.models import MonthlyExchangeRate +from cost_models.models import RateType +from masu.test import MasuTestCase + + +class MonthlyExchangeRateTest(MasuTestCase): + """Tests for MonthlyExchangeRate model.""" + + def test_create_dynamic_rate(self): + """Test creating a dynamic exchange rate.""" + with tenant_context(self.tenant): + rate = MonthlyExchangeRate.objects.create( + effective_date=date(2026, 1, 1), + base_currency="USD", + target_currency="EUR", + exchange_rate=Decimal("0.870000000000000"), + rate_type=RateType.DYNAMIC, + ) + self.assertEqual(rate.base_currency, "USD") + self.assertEqual(rate.target_currency, "EUR") + self.assertEqual(rate.rate_type, RateType.DYNAMIC) + + def test_create_static_rate(self): + """Test creating a static exchange rate.""" + with tenant_context(self.tenant): + rate = MonthlyExchangeRate.objects.create( + effective_date=date(2026, 1, 1), + base_currency="USD", + target_currency="GBP", + exchange_rate=Decimal("0.780000000000000"), + rate_type=RateType.STATIC, + ) + self.assertEqual(rate.rate_type, RateType.STATIC) + + def test_unique_together_constraint(self): + """Test that duplicate (effective_date, base, target) raises IntegrityError.""" + with tenant_context(self.tenant): + MonthlyExchangeRate.objects.create( + effective_date=date(2026, 2, 1), + base_currency="USD", + target_currency="EUR", + exchange_rate=Decimal("0.870000000000000"), + rate_type=RateType.DYNAMIC, + ) + with self.assertRaises(IntegrityError): + MonthlyExchangeRate.objects.create( + effective_date=date(2026, 2, 1), + base_currency="USD", + target_currency="EUR", + exchange_rate=Decimal("0.890000000000000"), + rate_type=RateType.STATIC, + ) + + def test_update_or_create_overwrites_dynamic_with_static(self): + """Test that static rate overwrites dynamic for the same triple.""" + with tenant_context(self.tenant): + MonthlyExchangeRate.objects.create( + effective_date=date(2026, 3, 1), + base_currency="USD", + target_currency="EUR", + exchange_rate=Decimal("0.870000000000000"), + rate_type=RateType.DYNAMIC, + ) + MonthlyExchangeRate.objects.update_or_create( + effective_date=date(2026, 3, 1), + base_currency="USD", + target_currency="EUR", + defaults={ + "exchange_rate": Decimal("0.920000000000000"), + "rate_type": RateType.STATIC, + }, + ) + rate = MonthlyExchangeRate.objects.get( + effective_date=date(2026, 3, 1), + base_currency="USD", + target_currency="EUR", + ) + self.assertEqual(rate.rate_type, RateType.STATIC) + self.assertEqual(rate.exchange_rate, Decimal("0.920000000000000")) + + +class EnabledCurrencyTest(MasuTestCase): + """Tests for EnabledCurrency model.""" + + def test_create_disabled_currency(self): + """Test creating a disabled currency entry.""" + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + ec = EnabledCurrency.objects.create(currency_code="JPY", enabled=False) + self.assertEqual(ec.currency_code, "JPY") + self.assertFalse(ec.enabled) + + def test_enable_currency(self): + """Test enabling a currency.""" + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + ec = EnabledCurrency.objects.create(currency_code="GBP", enabled=False) + ec.enabled = True + ec.save() + ec.refresh_from_db() + self.assertTrue(ec.enabled) + + def test_unique_currency_code(self): + """Test that duplicate currency codes raise IntegrityError.""" + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="CNY", enabled=False) + with self.assertRaises(IntegrityError): + EnabledCurrency.objects.create(currency_code="CNY", enabled=True) diff --git a/koku/cost_models/test/test_static_exchange_rate_serializer.py b/koku/cost_models/test/test_static_exchange_rate_serializer.py new file mode 100644 index 0000000000..17cbf3b2ca --- /dev/null +++ b/koku/cost_models/test/test_static_exchange_rate_serializer.py @@ -0,0 +1,174 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for the StaticExchangeRate serializer.""" +from datetime import date +from decimal import Decimal +from unittest.mock import MagicMock +from unittest.mock import patch + +from django_tenants.utils import tenant_context +from rest_framework.exceptions import ValidationError + +from cost_models.models import MonthlyExchangeRate +from cost_models.models import RateType +from cost_models.models import StaticExchangeRate +from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer +from masu.test import MasuTestCase + + +class StaticExchangeRateSerializerTest(MasuTestCase): + """Tests for StaticExchangeRateSerializer.""" + + def setUp(self): + super().setUp() + self.valid_data = { + "base_currency": "USD", + "target_currency": "EUR", + "exchange_rate": "0.870000000000000", + "start_date": "2026-01-01", + "end_date": "2026-03-31", + } + + def _make_request_context(self): + request = MagicMock() + request.user.customer.schema_name = self.schema_name + return {"request": request} + + def test_validate_base_currency_uppercase(self): + """Test that base_currency is uppercased.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["base_currency"] = "usd" + serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data["base_currency"], "USD") + + def test_validate_invalid_currency(self): + """Test that invalid currency codes are rejected.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["base_currency"] = "FAKE" + serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) + self.assertFalse(serializer.is_valid()) + + def test_validate_same_currencies(self): + """Test that base_currency == target_currency is rejected.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["target_currency"] = "USD" + serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) + self.assertFalse(serializer.is_valid()) + + def test_validate_start_date_not_first_of_month(self): + """Test that start_date must be the 1st of a month.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["start_date"] = "2026-01-15" + serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) + self.assertFalse(serializer.is_valid()) + + def test_validate_end_date_not_last_of_month(self): + """Test that end_date must be the last day of a month.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["end_date"] = "2026-03-15" + serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) + self.assertFalse(serializer.is_valid()) + + def test_validate_start_after_end(self): + """Test that start_date > end_date is rejected.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["start_date"] = "2026-04-01" + data["end_date"] = "2026-03-31" + serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) + self.assertFalse(serializer.is_valid()) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_create_upserts_monthly_exchange_rate(self, mock_invalidate): + """Test that creating a static rate upserts MonthlyExchangeRate rows.""" + with tenant_context(self.tenant): + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + self.assertTrue(serializer.is_valid(), serializer.errors) + instance = serializer.save() + + self.assertIsNotNone(instance.uuid) + self.assertEqual(instance.version, 1) + self.assertEqual(instance.name, "USD-EUR") + + monthly_rates = MonthlyExchangeRate.objects.filter( + base_currency="USD", + target_currency="EUR", + rate_type=RateType.STATIC, + ) + self.assertEqual(monthly_rates.count(), 3) + + for rate in monthly_rates: + self.assertEqual(rate.exchange_rate, Decimal("0.870000000000000")) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_overlap_detection(self, mock_invalidate): + """Test that overlapping validity periods are rejected.""" + with tenant_context(self.tenant): + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + serializer.is_valid(raise_exception=True) + serializer.save() + + overlap_data = { + "base_currency": "USD", + "target_currency": "EUR", + "exchange_rate": "0.900000000000000", + "start_date": "2026-02-01", + "end_date": "2026-04-30", + } + serializer2 = StaticExchangeRateSerializer(data=overlap_data, context=self._make_request_context()) + self.assertFalse(serializer2.is_valid()) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_update_increments_version(self, mock_invalidate): + """Test that updating a static rate increments the version.""" + with tenant_context(self.tenant): + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + self.assertEqual(instance.version, 1) + + update_data = {"exchange_rate": "0.900000000000000"} + serializer2 = StaticExchangeRateSerializer( + instance=instance, data=update_data, partial=True, context=self._make_request_context() + ) + serializer2.is_valid(raise_exception=True) + updated = serializer2.save() + self.assertEqual(updated.version, 2) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_delete_removes_static_monthly_rates(self, mock_invalidate): + """Test that deleting a static rate removes static MonthlyExchangeRate rows.""" + with tenant_context(self.tenant): + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + + self.assertTrue( + MonthlyExchangeRate.objects.filter( + base_currency="USD", target_currency="EUR", rate_type=RateType.STATIC + ).exists() + ) + + serializer.delete(instance) + + self.assertFalse( + MonthlyExchangeRate.objects.filter( + base_currency="USD", target_currency="EUR", rate_type=RateType.STATIC + ).exists() + ) + self.assertFalse(StaticExchangeRate.objects.filter(uuid=instance.uuid).exists()) + + def test_name_computed_field(self): + """Test name is read-only computed.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) + self.assertTrue(serializer.is_valid(), serializer.errors) diff --git a/koku/cost_models/test/test_static_exchange_rate_view.py b/koku/cost_models/test/test_static_exchange_rate_view.py new file mode 100644 index 0000000000..565a38b59a --- /dev/null +++ b/koku/cost_models/test/test_static_exchange_rate_view.py @@ -0,0 +1,114 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Tests for the StaticExchangeRate ViewSet.""" +import json +from unittest.mock import patch + +from django.test.utils import override_settings +from django.urls import reverse +from django_tenants.utils import tenant_context +from rest_framework import status +from rest_framework.test import APIClient + +from api.iam.test.iam_test_case import IamTestCase +from cost_models.models import MonthlyExchangeRate +from cost_models.models import RateType +from cost_models.models import StaticExchangeRate + + +class StaticExchangeRateViewSetTest(IamTestCase): + """Tests for StaticExchangeRateViewSet.""" + + def setUp(self): + super().setUp() + self.client = APIClient() + self.client.force_authenticate(user=self.request_context["request"].user) + self.list_url = reverse("exchange-rate-pairs-list") + self.valid_data = { + "base_currency": "USD", + "target_currency": "EUR", + "exchange_rate": "0.870000000000000", + "start_date": "2026-01-01", + "end_date": "2026-03-31", + } + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_create_static_rate(self, mock_invalidate): + """Test creating a static exchange rate via API.""" + with tenant_context(self.tenant): + response = self.client.post(self.list_url, data=self.valid_data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + data = response.data + self.assertEqual(data["base_currency"], "USD") + self.assertEqual(data["target_currency"], "EUR") + self.assertEqual(data["name"], "USD-EUR") + self.assertEqual(data["version"], 1) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_list_static_rates(self, mock_invalidate): + """Test listing static exchange rates.""" + with tenant_context(self.tenant): + self.client.post(self.list_url, data=self.valid_data, format="json") + response = self.client.get(self.list_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertGreaterEqual(len(response.data["data"]), 1) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_retrieve_static_rate(self, mock_invalidate): + """Test retrieving a single static exchange rate.""" + with tenant_context(self.tenant): + create_response = self.client.post(self.list_url, data=self.valid_data, format="json") + uuid = create_response.data["uuid"] + detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) + response = self.client.get(detail_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["uuid"], uuid) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_update_static_rate(self, mock_invalidate): + """Test updating a static exchange rate via PUT.""" + with tenant_context(self.tenant): + create_response = self.client.post(self.list_url, data=self.valid_data, format="json") + uuid = create_response.data["uuid"] + detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) + + update_data = self.valid_data.copy() + update_data["exchange_rate"] = "0.900000000000000" + response = self.client.put(detail_url, data=update_data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["version"], 2) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_delete_static_rate(self, mock_invalidate): + """Test deleting a static exchange rate.""" + with tenant_context(self.tenant): + create_response = self.client.post(self.list_url, data=self.valid_data, format="json") + uuid = create_response.data["uuid"] + detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) + response = self.client.delete(detail_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse(StaticExchangeRate.objects.filter(uuid=uuid).exists()) + + def test_create_invalid_currency(self): + """Test creating with invalid currency code returns 400.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["base_currency"] = "FAKE" + response = self.client.post(self.list_url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_mid_month_start_date(self): + """Test creating with non-first-of-month start_date returns 400.""" + with tenant_context(self.tenant): + data = self.valid_data.copy() + data["start_date"] = "2026-01-15" + response = self.client.post(self.list_url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_filter_by_base_currency(self): + """Test filtering by base_currency query parameter.""" + with tenant_context(self.tenant): + response = self.client.get(self.list_url, {"base_currency": "USD"}) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/koku/cost_models/urls.py b/koku/cost_models/urls.py index e9f528582c..2ffd58f0af 100644 --- a/koku/cost_models/urls.py +++ b/koku/cost_models/urls.py @@ -8,10 +8,12 @@ from rest_framework.routers import DefaultRouter from cost_models.price_list_view import PriceListViewSet +from cost_models.static_exchange_rate_view import StaticExchangeRateViewSet from cost_models.views import CostModelViewSet ROUTER = DefaultRouter() ROUTER.register(r"cost-models", CostModelViewSet, basename="cost-models") ROUTER.register(r"price-lists", PriceListViewSet, basename="price-lists") +ROUTER.register(r"exchange-rate-pairs", StaticExchangeRateViewSet, basename="exchange-rate-pairs") urlpatterns = [path("", include(ROUTER.urls))] diff --git a/koku/forecast/forecast.py b/koku/forecast/forecast.py index 7cb80a5079..cd0efe0b3f 100644 --- a/koku/forecast/forecast.py +++ b/koku/forecast/forecast.py @@ -16,20 +16,17 @@ import statsmodels.api as sm from django.conf import settings from django.core.exceptions import FieldDoesNotExist -from django.db.models import Case from django.db.models import CharField from django.db.models import DecimalField from django.db.models import ExpressionWrapper from django.db.models import F from django.db.models import Q from django.db.models import Value -from django.db.models import When from django.db.models.functions import Coalesce from django_tenants.utils import tenant_context from statsmodels.sandbox.regression.predstd import wls_prediction_std from statsmodels.tools.sm_exceptions import ValueWarning -from api.currency.models import ExchangeRateDictionary from api.models import Provider from api.query_filter import QueryFilter from api.query_filter import QueryFilterCollection @@ -44,7 +41,6 @@ from api.utils import DateHelper from api.utils import get_cost_type from cost_models.models import CostModel -from cost_models.models import CostModelMap from reporting.provider.aws.models import AWSOrganizationalUnit LOG = logging.getLogger(__name__) @@ -149,22 +145,33 @@ def infrastructure_cost_term(self): """Return the provider map value for total inftrastructure cost.""" return self.provider_map.report_type_map.get("aggregates", {}).get("infra_total") - @cached_property - def exchange_rates(self): - try: - return ExchangeRateDictionary.objects.first().currency_exchange_dictionary - except AttributeError as err: - LOG.warning(f"Exchange rates dictionary is not populated resulting in {err}.") - return {} - @cached_property def exchange_rate_annotation_dict(self): - """Get the exchange rate annotation based on the exchange_rates property.""" - whens = [ - When(**{self.provider_map.cost_units_key: k, "then": Value(v.get(self.currency))}) - for k, v in self.exchange_rates.items() - ] - return {"exchange_rate": Case(*whens, default=1, output_field=DecimalField())} + """Get per-month exchange rate annotation from MonthlyExchangeRate via Subquery.""" + from django.db.models import OuterRef + from django.db.models import Subquery + from django.db.models.functions import TruncMonth + + from cost_models.models import MonthlyExchangeRate + + rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=OuterRef(self.provider_map.cost_units_key), + target_currency=self.currency, + ).values("exchange_rate")[:1] + + earliest_rate_subquery = MonthlyExchangeRate.objects.filter( + base_currency=OuterRef(self.provider_map.cost_units_key), + target_currency=self.currency, + ).order_by("effective_date").values("exchange_rate")[:1] + + return { + "exchange_rate": Coalesce( + Subquery(rate_subquery), + Subquery(earliest_rate_subquery), + output_field=DecimalField(), + ) + } def get_data(self): """Query the database.""" @@ -596,36 +603,52 @@ class OCPForecast(Forecast): provider = Provider.PROVIDER_OCP provider_map_class = OCPProviderMap - @cached_property - def source_to_currency_map(self): - """ - OCP sources do not have costs associated, so we need to - grab the base currency from the cost model, and create - a mapping of source_uuid to currency. - returns: - dict: {source_uuid: currency} - """ - source_map = defaultdict(lambda: "USD") - cost_models = CostModel.objects.all().values("uuid", "currency").distinct() - cm_to_currency = {row["uuid"]: row["currency"] for row in cost_models} - mapping = CostModelMap.objects.all().values("provider_uuid", "cost_model_id") - source_map |= {row["provider_uuid"]: cm_to_currency[row["cost_model_id"]] for row in mapping} - return source_map - @cached_property def exchange_rate_annotation_dict(self): - """Get the exchange rate annotation based on the exchange_rates property.""" - exchange_rate_whens = [ - When(**{"source_uuid": uuid, "then": Value(self.exchange_rates.get(cur, {}).get(self.currency, 1))}) - for uuid, cur in self.source_to_currency_map.items() - ] - infra_exchange_rate_whens = [ - When(**{self.provider_map.cost_units_key: k, "then": Value(v.get(self.currency))}) - for k, v in self.exchange_rates.items() - ] + """Get per-month exchange rate annotations from MonthlyExchangeRate via Subquery.""" + from django.db.models import OuterRef + from django.db.models import Subquery + from django.db.models.functions import TruncMonth + + from cost_models.models import MonthlyExchangeRate + + cost_model_currency = CostModel.objects.filter( + cost_model_map__provider_uuid=OuterRef("source_uuid"), + ).values("currency")[:1] + + exchange_rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=Subquery(cost_model_currency), + target_currency=self.currency, + ).values("exchange_rate")[:1] + + earliest_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( + base_currency=Subquery(cost_model_currency), + target_currency=self.currency, + ).order_by("effective_date").values("exchange_rate")[:1] + + infra_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=OuterRef(self.provider_map.cost_units_key), + target_currency=self.currency, + ).values("exchange_rate")[:1] + + earliest_infra_rate_subquery = MonthlyExchangeRate.objects.filter( + base_currency=OuterRef(self.provider_map.cost_units_key), + target_currency=self.currency, + ).order_by("effective_date").values("exchange_rate")[:1] + return { - "exchange_rate": Case(*exchange_rate_whens, default=1, output_field=DecimalField()), - "infra_exchange_rate": Case(*infra_exchange_rate_whens, default=1, output_field=DecimalField()), + "exchange_rate": Coalesce( + Subquery(exchange_rate_subquery), + Subquery(earliest_exchange_rate_subquery), + output_field=DecimalField(), + ), + "infra_exchange_rate": Coalesce( + Subquery(infra_exchange_rate_subquery), + Subquery(earliest_infra_rate_subquery), + output_field=DecimalField(), + ), } diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index c522d86841..c02c4ff3c8 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -265,10 +265,20 @@ def autovacuum_tune_schemas(): @celery_app.task(name="masu.celery.tasks.get_daily_currency_rates", queue=DEFAULT) def get_daily_currency_rates(): - """Task to get latest daily conversion rates.""" + """Task to get latest daily conversion rates and upsert MonthlyExchangeRate per tenant.""" + from api.currency.models import ExchangeRateDictionary + from cost_models.models import EnabledCurrency + from cost_models.models import MonthlyExchangeRate + from cost_models.models import RateType + from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types + rate_metrics = {} url = settings.CURRENCY_URL + if not url: + LOG.info(log_json(msg="CURRENCY_URL not configured; skipping dynamic exchange rate fetch")) + return rate_metrics + retries = Retry( total=5, allowed_methods={"GET"}, @@ -278,20 +288,16 @@ def get_daily_currency_rates(): session = requests.Session() session.mount("https://", HTTPAdapter(max_retries=retries)) - # Retrieve conversion rates from URL try: response = session.get(url) response.raise_for_status() except (HTTPError, RetryError) as e: LOG.error(f"Couldn't pull latest conversion rates from {url}") LOG.error(e) - return rate_metrics data = response.json() - rates = data["rates"] - # Update conversion rates in database for curr_type in rates.keys(): if curr_type.upper() in VALID_CURRENCIES: value = rates[curr_type] @@ -305,6 +311,47 @@ def get_daily_currency_rates(): exchange.exchange_rate = value exchange.save() exchange_dictionary(rate_metrics) + + dh = DateHelper() + current_month = dh.this_month_start.date() + erd = ExchangeRateDictionary.objects.first() + if not erd or not erd.currency_exchange_dictionary: + return rate_metrics + + exchange_dict = erd.currency_exchange_dictionary + all_api_currencies = set(exchange_dict.keys()) + + for tenant in Tenant.objects.exclude(schema_name="public"): + with schema_context(tenant.schema_name): + existing_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + new_currencies = all_api_currencies - existing_codes + if new_currencies: + EnabledCurrency.objects.bulk_create( + [EnabledCurrency(currency_code=code, enabled=False) for code in new_currencies], + ignore_conflicts=True, + ) + + static_pairs = set( + MonthlyExchangeRate.objects.filter( + effective_date=current_month, + rate_type=RateType.STATIC, + ).values_list("base_currency", "target_currency") + ) + + for base_cur, targets in exchange_dict.items(): + for target_cur, rate in targets.items(): + if base_cur == target_cur: + continue + if (base_cur, target_cur) not in static_pairs: + MonthlyExchangeRate.objects.update_or_create( + effective_date=current_month, + base_currency=base_cur, + target_currency=target_cur, + defaults={"exchange_rate": rate, "rate_type": RateType.DYNAMIC}, + ) + + invalidate_view_cache_for_tenant_and_all_source_types(tenant.schema_name) + return rate_metrics From f6600d25785a35dbde2e6997ce72b6a489455ad8 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 18:29:50 +0300 Subject: [PATCH 002/106] COST-7252: Fix CI failures and address review feedback - Renumber migrations 0012-0014 to 0013-0015 to resolve conflict with 0012_add_rate_model from main - Remove unused ValidationError import in test_static_exchange_rate_serializer - Extract exchange rate annotation logic into cost_models/exchange_rate_annotations.py to eliminate duplication across QueryHandler, ReportQueryHandler, Forecast, OCPReportQueryHandler, and OCPForecast (DRY) - Include missing base currencies in _validate_exchange_rates error message for easier debugging Made-with: Cursor --- koku/api/query_handler.py | 26 +----- koku/api/report/ocp/query_handler.py | 46 +-------- koku/api/report/queries.py | 34 ++----- koku/cost_models/exchange_rate_annotations.py | 93 +++++++++++++++++++ ...angerate.py => 0013_staticexchangerate.py} | 2 +- ...ngerate.py => 0014_monthlyexchangerate.py} | 2 +- ...ledcurrency.py => 0015_enabledcurrency.py} | 2 +- .../test_static_exchange_rate_serializer.py | 1 - koku/forecast/forecast.py | 74 +-------------- 9 files changed, 111 insertions(+), 169 deletions(-) create mode 100644 koku/cost_models/exchange_rate_annotations.py rename koku/cost_models/migrations/{0012_staticexchangerate.py => 0013_staticexchangerate.py} (94%) rename koku/cost_models/migrations/{0013_monthlyexchangerate.py => 0014_monthlyexchangerate.py} (97%) rename koku/cost_models/migrations/{0014_enabledcurrency.py => 0015_enabledcurrency.py} (94%) diff --git a/koku/api/query_handler.py b/koku/api/query_handler.py index 1c96810d27..776c7ba6e0 100644 --- a/koku/api/query_handler.py +++ b/koku/api/query_handler.py @@ -11,9 +11,6 @@ from dateutil import relativedelta from django.conf import settings from django.core.exceptions import FieldDoesNotExist -from django.db.models import DecimalField -from django.db.models import Subquery -from django.db.models.functions import Coalesce from django.db.models.functions import TruncDay from django.db.models.functions import TruncMonth @@ -23,7 +20,7 @@ from api.report.constants import TIME_SCOPE_UNITS_DAILY from api.report.constants import TIME_SCOPE_VALUES_DAILY from api.utils import DateHelper -from cost_models.models import MonthlyExchangeRate +from cost_models.exchange_rate_annotations import build_exchange_rate_annotation_dict LOG = logging.getLogger(__name__) WILDCARD = "*" @@ -122,26 +119,7 @@ def has_wildcard(in_list): @cached_property def exchange_rate_annotation_dict(self): """Get per-month exchange rate annotation from MonthlyExchangeRate via Subquery.""" - from django.db.models import OuterRef - - rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), - base_currency=OuterRef(self._mapper.cost_units_key), - target_currency=self.currency, - ).values("exchange_rate")[:1] - - earliest_rate_subquery = MonthlyExchangeRate.objects.filter( - base_currency=OuterRef(self._mapper.cost_units_key), - target_currency=self.currency, - ).order_by("effective_date").values("exchange_rate")[:1] - - return { - "exchange_rate": Coalesce( - Subquery(rate_subquery), - Subquery(earliest_rate_subquery), - output_field=DecimalField(), - ) - } + return build_exchange_rate_annotation_dict(self._mapper.cost_units_key, self.currency) @property def order(self): diff --git a/koku/api/report/ocp/query_handler.py b/koku/api/report/ocp/query_handler.py index 3f13225852..503cc2eaf9 100644 --- a/koku/api/report/ocp/query_handler.py +++ b/koku/api/report/ocp/query_handler.py @@ -13,15 +13,11 @@ from django.db.models import Case from django.db.models import CharField -from django.db.models import DecimalField from django.db.models import F -from django.db.models import OuterRef -from django.db.models import Subquery from django.db.models import Value from django.db.models import When from django.db.models.fields.json import KT from django.db.models.functions import Coalesce -from django.db.models.functions import TruncMonth from django_tenants.utils import tenant_context from api.models import Provider @@ -32,8 +28,7 @@ from api.report.queries import is_grouped_by_node from api.report.queries import is_grouped_by_project from api.report.queries import ReportQueryHandler -from cost_models.models import CostModel -from cost_models.models import MonthlyExchangeRate +from cost_models.exchange_rate_annotations import build_ocp_exchange_rate_annotation_dict LOG = logging.getLogger(__name__) @@ -171,44 +166,7 @@ def exchange_rate_annotation_dict(self): - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) - infra_exchange_rate: cloud bill currency (raw_currency column) """ - cost_model_currency = CostModel.objects.filter( - cost_model_map__provider_uuid=OuterRef("source_uuid"), - ).values("currency")[:1] - - exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), - base_currency=Subquery(cost_model_currency), - target_currency=self.currency, - ).values("exchange_rate")[:1] - - earliest_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - base_currency=Subquery(cost_model_currency), - target_currency=self.currency, - ).order_by("effective_date").values("exchange_rate")[:1] - - infra_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), - base_currency=OuterRef(self._mapper.cost_units_key), - target_currency=self.currency, - ).values("exchange_rate")[:1] - - earliest_infra_rate_subquery = MonthlyExchangeRate.objects.filter( - base_currency=OuterRef(self._mapper.cost_units_key), - target_currency=self.currency, - ).order_by("effective_date").values("exchange_rate")[:1] - - return { - "exchange_rate": Coalesce( - Subquery(exchange_rate_subquery), - Subquery(earliest_exchange_rate_subquery), - output_field=DecimalField(), - ), - "infra_exchange_rate": Coalesce( - Subquery(infra_exchange_rate_subquery), - Subquery(earliest_infra_rate_subquery), - output_field=DecimalField(), - ), - } + return build_ocp_exchange_rate_annotation_dict(self._mapper.cost_units_key, self.currency) def format_tags(self, tags_iterable): """ diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 9d06101c4c..5a42007835 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -882,32 +882,6 @@ def _aws_category_group_by(self) -> list[tuple[str, int, str]]: group_by.append((db_name, group_pos, original_aws_category)) return group_by - @cached_property - def exchange_rate_annotation_dict(self): - """Get per-month exchange rate annotation from MonthlyExchangeRate via Subquery.""" - from django.db.models import OuterRef - from django.db.models import Subquery - from django.db.models.functions import TruncMonth - - rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), - base_currency=OuterRef(self._mapper.cost_units_key), - target_currency=self.currency, - ).values("exchange_rate")[:1] - - earliest_rate_subquery = MonthlyExchangeRate.objects.filter( - base_currency=OuterRef(self._mapper.cost_units_key), - target_currency=self.currency, - ).order_by("effective_date").values("exchange_rate")[:1] - - return { - "exchange_rate": Coalesce( - Subquery(rate_subquery), - Subquery(earliest_rate_subquery), - output_field=DecimalField(), - ) - } - def _project_classification_annotation(self, query_data): """Get the correct annotation for a project or category""" whens = [ @@ -1153,10 +1127,16 @@ def _validate_exchange_rates(self, queryset): null_rate_filter = Q(exchange_rate__isnull=True) if queryset.filter(null_rate_filter).exists(): + missing_currencies = list( + queryset.filter(null_rate_filter) + .values_list(self._mapper.cost_units_key, flat=True) + .distinct() + ) raise ValidationError( { "detail": ( - f"No exchange rate available for target currency {self.currency}. " + f"No exchange rate available for currency pair(s) " + f"{missing_currencies} -> {self.currency}. " "Ask your administrator to configure static exchange rates " "or enable dynamic exchange rates." ), diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py new file mode 100644 index 0000000000..40a5d30f5b --- /dev/null +++ b/koku/cost_models/exchange_rate_annotations.py @@ -0,0 +1,93 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Shared exchange rate annotation builders for query handlers and forecasts.""" +from django.db.models import DecimalField +from django.db.models import OuterRef +from django.db.models import Subquery +from django.db.models.functions import Coalesce +from django.db.models.functions import TruncMonth + +from cost_models.models import CostModel +from cost_models.models import MonthlyExchangeRate + + +def build_monthly_rate_annotation(cost_units_key, target_currency): + """Build a Coalesce annotation that resolves exchange rates per month. + + Tries the rate matching the row's usage_start month first, then falls back + to the earliest available rate for the currency pair. + + Args: + cost_units_key: The field name on the outer query holding the base currency code. + target_currency: The target currency code string. + + Returns: + A Coalesce expression suitable for use in an .annotate() call. + """ + rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=OuterRef(cost_units_key), + target_currency=target_currency, + ).values("exchange_rate")[:1] + + earliest_rate_subquery = ( + MonthlyExchangeRate.objects.filter( + base_currency=OuterRef(cost_units_key), + target_currency=target_currency, + ) + .order_by("effective_date") + .values("exchange_rate")[:1] + ) + + return Coalesce( + Subquery(rate_subquery), + Subquery(earliest_rate_subquery), + output_field=DecimalField(), + ) + + +def build_exchange_rate_annotation_dict(cost_units_key, target_currency): + """Build annotation dict with a single 'exchange_rate' key. + + Used by non-OCP query handlers and forecasts where there is only one + currency dimension (the bill/report currency). + """ + return {"exchange_rate": build_monthly_rate_annotation(cost_units_key, target_currency)} + + +def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): + """Build annotation dict with dual exchange rates for OCP. + + OCP needs two annotations: + - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) + - infra_exchange_rate: cloud bill currency (raw_currency column) + """ + cost_model_currency = CostModel.objects.filter( + cost_model_map__provider_uuid=OuterRef("source_uuid"), + ).values("currency")[:1] + + exchange_rate_subquery = MonthlyExchangeRate.objects.filter( + effective_date=TruncMonth(OuterRef("usage_start")), + base_currency=Subquery(cost_model_currency), + target_currency=target_currency, + ).values("exchange_rate")[:1] + + earliest_exchange_rate_subquery = ( + MonthlyExchangeRate.objects.filter( + base_currency=Subquery(cost_model_currency), + target_currency=target_currency, + ) + .order_by("effective_date") + .values("exchange_rate")[:1] + ) + + return { + "exchange_rate": Coalesce( + Subquery(exchange_rate_subquery), + Subquery(earliest_exchange_rate_subquery), + output_field=DecimalField(), + ), + "infra_exchange_rate": build_monthly_rate_annotation(cost_units_key, target_currency), + } diff --git a/koku/cost_models/migrations/0012_staticexchangerate.py b/koku/cost_models/migrations/0013_staticexchangerate.py similarity index 94% rename from koku/cost_models/migrations/0012_staticexchangerate.py rename to koku/cost_models/migrations/0013_staticexchangerate.py index a9d633edcb..e8ef4b648b 100644 --- a/koku/cost_models/migrations/0012_staticexchangerate.py +++ b/koku/cost_models/migrations/0013_staticexchangerate.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): dependencies = [ - ("cost_models", "0011_migrate_cost_model_rates_to_price_lists"), + ("cost_models", "0012_add_rate_model"), ] operations = [ diff --git a/koku/cost_models/migrations/0013_monthlyexchangerate.py b/koku/cost_models/migrations/0014_monthlyexchangerate.py similarity index 97% rename from koku/cost_models/migrations/0013_monthlyexchangerate.py rename to koku/cost_models/migrations/0014_monthlyexchangerate.py index 442c2baa6e..f3edd73e50 100644 --- a/koku/cost_models/migrations/0013_monthlyexchangerate.py +++ b/koku/cost_models/migrations/0014_monthlyexchangerate.py @@ -39,7 +39,7 @@ def seed_current_month(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("cost_models", "0012_staticexchangerate"), + ("cost_models", "0013_staticexchangerate"), ("api", "0001_initial"), ] diff --git a/koku/cost_models/migrations/0014_enabledcurrency.py b/koku/cost_models/migrations/0015_enabledcurrency.py similarity index 94% rename from koku/cost_models/migrations/0014_enabledcurrency.py rename to koku/cost_models/migrations/0015_enabledcurrency.py index 13e8c24ae2..57c22954c1 100644 --- a/koku/cost_models/migrations/0014_enabledcurrency.py +++ b/koku/cost_models/migrations/0015_enabledcurrency.py @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ - ("cost_models", "0013_monthlyexchangerate"), + ("cost_models", "0014_monthlyexchangerate"), ] operations = [ diff --git a/koku/cost_models/test/test_static_exchange_rate_serializer.py b/koku/cost_models/test/test_static_exchange_rate_serializer.py index 17cbf3b2ca..65874390ce 100644 --- a/koku/cost_models/test/test_static_exchange_rate_serializer.py +++ b/koku/cost_models/test/test_static_exchange_rate_serializer.py @@ -9,7 +9,6 @@ from unittest.mock import patch from django_tenants.utils import tenant_context -from rest_framework.exceptions import ValidationError from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType diff --git a/koku/forecast/forecast.py b/koku/forecast/forecast.py index cd0efe0b3f..2521784e58 100644 --- a/koku/forecast/forecast.py +++ b/koku/forecast/forecast.py @@ -17,7 +17,6 @@ from django.conf import settings from django.core.exceptions import FieldDoesNotExist from django.db.models import CharField -from django.db.models import DecimalField from django.db.models import ExpressionWrapper from django.db.models import F from django.db.models import Q @@ -40,7 +39,8 @@ from api.report.ocp.provider_map import OCPProviderMap from api.utils import DateHelper from api.utils import get_cost_type -from cost_models.models import CostModel +from cost_models.exchange_rate_annotations import build_exchange_rate_annotation_dict +from cost_models.exchange_rate_annotations import build_ocp_exchange_rate_annotation_dict from reporting.provider.aws.models import AWSOrganizationalUnit LOG = logging.getLogger(__name__) @@ -148,30 +148,7 @@ def infrastructure_cost_term(self): @cached_property def exchange_rate_annotation_dict(self): """Get per-month exchange rate annotation from MonthlyExchangeRate via Subquery.""" - from django.db.models import OuterRef - from django.db.models import Subquery - from django.db.models.functions import TruncMonth - - from cost_models.models import MonthlyExchangeRate - - rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), - base_currency=OuterRef(self.provider_map.cost_units_key), - target_currency=self.currency, - ).values("exchange_rate")[:1] - - earliest_rate_subquery = MonthlyExchangeRate.objects.filter( - base_currency=OuterRef(self.provider_map.cost_units_key), - target_currency=self.currency, - ).order_by("effective_date").values("exchange_rate")[:1] - - return { - "exchange_rate": Coalesce( - Subquery(rate_subquery), - Subquery(earliest_rate_subquery), - output_field=DecimalField(), - ) - } + return build_exchange_rate_annotation_dict(self.provider_map.cost_units_key, self.currency) def get_data(self): """Query the database.""" @@ -606,50 +583,7 @@ class OCPForecast(Forecast): @cached_property def exchange_rate_annotation_dict(self): """Get per-month exchange rate annotations from MonthlyExchangeRate via Subquery.""" - from django.db.models import OuterRef - from django.db.models import Subquery - from django.db.models.functions import TruncMonth - - from cost_models.models import MonthlyExchangeRate - - cost_model_currency = CostModel.objects.filter( - cost_model_map__provider_uuid=OuterRef("source_uuid"), - ).values("currency")[:1] - - exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), - base_currency=Subquery(cost_model_currency), - target_currency=self.currency, - ).values("exchange_rate")[:1] - - earliest_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - base_currency=Subquery(cost_model_currency), - target_currency=self.currency, - ).order_by("effective_date").values("exchange_rate")[:1] - - infra_exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), - base_currency=OuterRef(self.provider_map.cost_units_key), - target_currency=self.currency, - ).values("exchange_rate")[:1] - - earliest_infra_rate_subquery = MonthlyExchangeRate.objects.filter( - base_currency=OuterRef(self.provider_map.cost_units_key), - target_currency=self.currency, - ).order_by("effective_date").values("exchange_rate")[:1] - - return { - "exchange_rate": Coalesce( - Subquery(exchange_rate_subquery), - Subquery(earliest_exchange_rate_subquery), - output_field=DecimalField(), - ), - "infra_exchange_rate": Coalesce( - Subquery(infra_exchange_rate_subquery), - Subquery(earliest_infra_rate_subquery), - output_field=DecimalField(), - ), - } + return build_ocp_exchange_rate_annotation_dict(self.provider_map.cost_units_key, self.currency) class OCPAWSForecast(Forecast): From f275159dbbc2309a25419c5c964b6a4c0fab34b0 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 18:37:48 +0300 Subject: [PATCH 003/106] COST-7252: Fix pre-commit failures (unused imports + black formatting) Remove unused imports flagged by flake8 F401 across serializer, view, and test files. Apply black formatting fixes for line wrapping. Made-with: Cursor --- koku/api/report/ocp/query_handler.py | 2 -- koku/api/report/queries.py | 5 +---- koku/api/settings/currency_views.py | 12 +++--------- koku/cost_models/exchange_rate_annotations.py | 6 +++--- koku/cost_models/static_exchange_rate_serializer.py | 7 +------ koku/cost_models/static_exchange_rate_view.py | 1 - .../test/test_static_exchange_rate_serializer.py | 1 - .../test/test_static_exchange_rate_view.py | 4 ---- 8 files changed, 8 insertions(+), 30 deletions(-) diff --git a/koku/api/report/ocp/query_handler.py b/koku/api/report/ocp/query_handler.py index 503cc2eaf9..5e33aa164f 100644 --- a/koku/api/report/ocp/query_handler.py +++ b/koku/api/report/ocp/query_handler.py @@ -11,11 +11,9 @@ from decimal import InvalidOperation from functools import cached_property -from django.db.models import Case from django.db.models import CharField from django.db.models import F from django.db.models import Value -from django.db.models import When from django.db.models.fields.json import KT from django.db.models.functions import Coalesce from django_tenants.utils import tenant_context diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 5a68b10548..87f01cc030 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -25,7 +25,6 @@ from django.contrib.postgres.aggregates import ArrayAgg from django.db.models import Case from django.db.models import CharField -from django.db.models import DecimalField from django.db.models import F from django.db.models import Q from django.db.models import Value @@ -1128,9 +1127,7 @@ def _validate_exchange_rates(self, queryset): null_rate_filter = Q(exchange_rate__isnull=True) if queryset.filter(null_rate_filter).exists(): missing_currencies = list( - queryset.filter(null_rate_filter) - .values_list(self._mapper.cost_units_key, flat=True) - .distinct() + queryset.filter(null_rate_filter).values_list(self._mapper.cost_units_key, flat=True).distinct() ) raise ValidationError( { diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 8f3e84b5a6..2188ee8710 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -69,16 +69,10 @@ class AvailableCurrencyView(APIView): @method_decorator(never_cache) def get(self, request, *args, **kwargs): - enabled_codes = set( - EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True) - ) + enabled_codes = set(EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True)) - static_bases = set( - StaticExchangeRate.objects.values_list("base_currency", flat=True) - ) - static_targets = set( - StaticExchangeRate.objects.values_list("target_currency", flat=True) - ) + static_bases = set(StaticExchangeRate.objects.values_list("base_currency", flat=True)) + static_targets = set(StaticExchangeRate.objects.values_list("target_currency", flat=True)) static_currencies = static_bases | static_targets data = [] diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 40a5d30f5b..0b6688ce75 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -64,9 +64,9 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) - infra_exchange_rate: cloud bill currency (raw_currency column) """ - cost_model_currency = CostModel.objects.filter( - cost_model_map__provider_uuid=OuterRef("source_uuid"), - ).values("currency")[:1] + cost_model_currency = CostModel.objects.filter(cost_model_map__provider_uuid=OuterRef("source_uuid"),).values( + "currency" + )[:1] exchange_rate_subquery = MonthlyExchangeRate.objects.filter( effective_date=TruncMonth(OuterRef("usage_start")), diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 631bb7b98c..78af3c16df 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -5,16 +5,13 @@ """Serializer for StaticExchangeRate with MonthlyExchangeRate side effects.""" import calendar import logging -from datetime import date from dateutil.relativedelta import relativedelta from django.db import transaction from rest_framework import serializers -from api.common import log_json from api.currency.currencies import VALID_CURRENCIES from api.currency.models import ExchangeRateDictionary -from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType from cost_models.models import StaticExchangeRate @@ -144,9 +141,7 @@ def validate(self, data): if self.instance: overlap_qs = overlap_qs.exclude(uuid=self.instance.uuid) if overlap_qs.exists(): - raise serializers.ValidationError( - "Overlapping validity period exists for this currency pair." - ) + raise serializers.ValidationError("Overlapping validity period exists for this currency pair.") return data diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index a03d17ed84..b09e7e6bf9 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -11,7 +11,6 @@ from rest_framework import viewsets from rest_framework.response import Response -from api.common import log_json from api.common.permissions.cost_models_access import CostModelsAccessPermission from cost_models.models import StaticExchangeRate from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer diff --git a/koku/cost_models/test/test_static_exchange_rate_serializer.py b/koku/cost_models/test/test_static_exchange_rate_serializer.py index 65874390ce..e9a0b43dff 100644 --- a/koku/cost_models/test/test_static_exchange_rate_serializer.py +++ b/koku/cost_models/test/test_static_exchange_rate_serializer.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """Tests for the StaticExchangeRate serializer.""" -from datetime import date from decimal import Decimal from unittest.mock import MagicMock from unittest.mock import patch diff --git a/koku/cost_models/test/test_static_exchange_rate_view.py b/koku/cost_models/test/test_static_exchange_rate_view.py index 565a38b59a..eb274ab781 100644 --- a/koku/cost_models/test/test_static_exchange_rate_view.py +++ b/koku/cost_models/test/test_static_exchange_rate_view.py @@ -3,18 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 # """Tests for the StaticExchangeRate ViewSet.""" -import json from unittest.mock import patch -from django.test.utils import override_settings from django.urls import reverse from django_tenants.utils import tenant_context from rest_framework import status from rest_framework.test import APIClient from api.iam.test.iam_test_case import IamTestCase -from cost_models.models import MonthlyExchangeRate -from cost_models.models import RateType from cost_models.models import StaticExchangeRate From 9a317c177ed6feb6904077fb4e0c1453e85f580b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 19:44:03 +0300 Subject: [PATCH 004/106] Fix CI: TruncMonth(OuterRef) crash, test auth, flake8 F821/C901 - Replace TruncMonth(OuterRef()) with ExtractYear/ExtractMonth to avoid Django's ResolvedOuterRef.output_field AttributeError - Use **self.headers in view tests instead of force_authenticate (Koku's IdentityHeaderMiddleware requires x-rh-identity) - Restore log_json import removed in prior cleanup (F821) - Refactor get_daily_currency_rates to reduce cyclomatic complexity (C901) Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 32 +++-- .../static_exchange_rate_serializer.py | 1 + .../cost_models/test/test_enabled_currency.py | 16 ++- .../test/test_static_exchange_rate_view.py | 25 ++-- koku/masu/celery/tasks.py | 114 +++++++++--------- 5 files changed, 98 insertions(+), 90 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 0b6688ce75..19045f36fe 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -7,27 +7,33 @@ from django.db.models import OuterRef from django.db.models import Subquery from django.db.models.functions import Coalesce -from django.db.models.functions import TruncMonth +from django.db.models.functions import ExtractMonth +from django.db.models.functions import ExtractYear from cost_models.models import CostModel from cost_models.models import MonthlyExchangeRate +def _month_filter(outer_date_field="usage_start"): + """Return filter kwargs matching effective_date to the outer query's month. + + Uses ExtractYear/ExtractMonth instead of TruncMonth because Django's + TruncMonth cannot resolve output_field from an OuterRef. + """ + return { + "effective_date__year": ExtractYear(OuterRef(outer_date_field)), + "effective_date__month": ExtractMonth(OuterRef(outer_date_field)), + } + + def build_monthly_rate_annotation(cost_units_key, target_currency): """Build a Coalesce annotation that resolves exchange rates per month. Tries the rate matching the row's usage_start month first, then falls back to the earliest available rate for the currency pair. - - Args: - cost_units_key: The field name on the outer query holding the base currency code. - target_currency: The target currency code string. - - Returns: - A Coalesce expression suitable for use in an .annotate() call. """ rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), + **_month_filter(), base_currency=OuterRef(cost_units_key), target_currency=target_currency, ).values("exchange_rate")[:1] @@ -64,12 +70,12 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) - infra_exchange_rate: cloud bill currency (raw_currency column) """ - cost_model_currency = CostModel.objects.filter(cost_model_map__provider_uuid=OuterRef("source_uuid"),).values( - "currency" - )[:1] + cost_model_currency = CostModel.objects.filter( + cost_model_map__provider_uuid=OuterRef("source_uuid"), + ).values("currency")[:1] exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), + **_month_filter(), base_currency=Subquery(cost_model_currency), target_currency=target_currency, ).values("exchange_rate")[:1] diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 78af3c16df..05463abe96 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -10,6 +10,7 @@ from django.db import transaction from rest_framework import serializers +from api.common import log_json from api.currency.currencies import VALID_CURRENCIES from api.currency.models import ExchangeRateDictionary from cost_models.models import MonthlyExchangeRate diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index fed8410bb7..f57a95e1f0 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -19,13 +19,12 @@ class EnabledCurrencyViewTest(IamTestCase): def setUp(self): super().setUp() self.client = APIClient() - self.client.force_authenticate(user=self.request_context["request"].user) self.url = reverse("enabled-currencies") def test_get_enabled_currencies_empty(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - response = self.client.get(self.url) + response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_get_enabled_currencies(self): @@ -33,7 +32,7 @@ def test_get_enabled_currencies(self): EnabledCurrency.objects.all().delete() EnabledCurrency.objects.create(currency_code="USD", enabled=True) EnabledCurrency.objects.create(currency_code="EUR", enabled=False) - response = self.client.get(self.url) + response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_put_enable_currencies(self): @@ -41,7 +40,7 @@ def test_put_enable_currencies(self): EnabledCurrency.objects.all().delete() EnabledCurrency.objects.create(currency_code="GBP", enabled=False) data = {"currencies": [{"currency_code": "GBP", "enabled": True}]} - response = self.client.put(self.url, data=data, format="json") + response = self.client.put(self.url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.get(currency_code="GBP").enabled) @@ -49,7 +48,7 @@ def test_put_creates_new_currency(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() data = {"currencies": [{"currency_code": "JPY", "enabled": True}]} - response = self.client.put(self.url, data=data, format="json") + response = self.client.put(self.url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.filter(currency_code="JPY", enabled=True).exists()) @@ -60,7 +59,6 @@ class AvailableCurrencyViewTest(IamTestCase): def setUp(self): super().setUp() self.client = APIClient() - self.client.force_authenticate(user=self.request_context["request"].user) self.url = reverse("available-currencies") def test_get_available_currencies_dynamic_only(self): @@ -70,7 +68,7 @@ def test_get_available_currencies_dynamic_only(self): EnabledCurrency.objects.create(currency_code="USD", enabled=True) EnabledCurrency.objects.create(currency_code="EUR", enabled=True) EnabledCurrency.objects.create(currency_code="GBP", enabled=False) - response = self.client.get(self.url) + response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) codes = [c["currency_code"] for c in response.data["data"]] self.assertIn("USD", codes) @@ -88,7 +86,7 @@ def test_get_available_currencies_static_included(self): start_date="2026-01-01", end_date="2026-01-31", ) - response = self.client.get(self.url) + response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) codes = [c["currency_code"] for c in response.data["data"]] self.assertIn("EUR", codes) @@ -98,6 +96,6 @@ def test_get_available_currencies_empty(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() StaticExchangeRate.objects.all().delete() - response = self.client.get(self.url) + response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data["data"]), 0) diff --git a/koku/cost_models/test/test_static_exchange_rate_view.py b/koku/cost_models/test/test_static_exchange_rate_view.py index eb274ab781..ec24098b7f 100644 --- a/koku/cost_models/test/test_static_exchange_rate_view.py +++ b/koku/cost_models/test/test_static_exchange_rate_view.py @@ -20,7 +20,6 @@ class StaticExchangeRateViewSetTest(IamTestCase): def setUp(self): super().setUp() self.client = APIClient() - self.client.force_authenticate(user=self.request_context["request"].user) self.list_url = reverse("exchange-rate-pairs-list") self.valid_data = { "base_currency": "USD", @@ -34,7 +33,7 @@ def setUp(self): def test_create_static_rate(self, mock_invalidate): """Test creating a static exchange rate via API.""" with tenant_context(self.tenant): - response = self.client.post(self.list_url, data=self.valid_data, format="json") + response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_201_CREATED) data = response.data self.assertEqual(data["base_currency"], "USD") @@ -46,8 +45,8 @@ def test_create_static_rate(self, mock_invalidate): def test_list_static_rates(self, mock_invalidate): """Test listing static exchange rates.""" with tenant_context(self.tenant): - self.client.post(self.list_url, data=self.valid_data, format="json") - response = self.client.get(self.list_url) + self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) + response = self.client.get(self.list_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertGreaterEqual(len(response.data["data"]), 1) @@ -55,10 +54,10 @@ def test_list_static_rates(self, mock_invalidate): def test_retrieve_static_rate(self, mock_invalidate): """Test retrieving a single static exchange rate.""" with tenant_context(self.tenant): - create_response = self.client.post(self.list_url, data=self.valid_data, format="json") + create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) - response = self.client.get(detail_url) + response = self.client.get(detail_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["uuid"], uuid) @@ -66,13 +65,13 @@ def test_retrieve_static_rate(self, mock_invalidate): def test_update_static_rate(self, mock_invalidate): """Test updating a static exchange rate via PUT.""" with tenant_context(self.tenant): - create_response = self.client.post(self.list_url, data=self.valid_data, format="json") + create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) update_data = self.valid_data.copy() update_data["exchange_rate"] = "0.900000000000000" - response = self.client.put(detail_url, data=update_data, format="json") + response = self.client.put(detail_url, data=update_data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["version"], 2) @@ -80,10 +79,10 @@ def test_update_static_rate(self, mock_invalidate): def test_delete_static_rate(self, mock_invalidate): """Test deleting a static exchange rate.""" with tenant_context(self.tenant): - create_response = self.client.post(self.list_url, data=self.valid_data, format="json") + create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) - response = self.client.delete(detail_url) + response = self.client.delete(detail_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse(StaticExchangeRate.objects.filter(uuid=uuid).exists()) @@ -92,7 +91,7 @@ def test_create_invalid_currency(self): with tenant_context(self.tenant): data = self.valid_data.copy() data["base_currency"] = "FAKE" - response = self.client.post(self.list_url, data=data, format="json") + response = self.client.post(self.list_url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_create_mid_month_start_date(self): @@ -100,11 +99,11 @@ def test_create_mid_month_start_date(self): with tenant_context(self.tenant): data = self.valid_data.copy() data["start_date"] = "2026-01-15" - response = self.client.post(self.list_url, data=data, format="json") + response = self.client.post(self.list_url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_filter_by_base_currency(self): """Test filtering by base_currency query parameter.""" with tenant_context(self.tenant): - response = self.client.get(self.list_url, {"base_currency": "USD"}) + response = self.client.get(self.list_url, {"base_currency": "USD"}, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index c02c4ff3c8..e96a931166 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -263,22 +263,8 @@ def autovacuum_tune_schemas(): autovacuum_tune_schema.delay(schema_name) -@celery_app.task(name="masu.celery.tasks.get_daily_currency_rates", queue=DEFAULT) -def get_daily_currency_rates(): - """Task to get latest daily conversion rates and upsert MonthlyExchangeRate per tenant.""" - from api.currency.models import ExchangeRateDictionary - from cost_models.models import EnabledCurrency - from cost_models.models import MonthlyExchangeRate - from cost_models.models import RateType - from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types - - rate_metrics = {} - - url = settings.CURRENCY_URL - if not url: - LOG.info(log_json(msg="CURRENCY_URL not configured; skipping dynamic exchange rate fetch")) - return rate_metrics - +def _fetch_exchange_rates(url): + """Fetch exchange rates from the configured URL. Returns rate_metrics dict or None on failure.""" retries = Retry( total=5, allowed_methods={"GET"}, @@ -294,13 +280,11 @@ def get_daily_currency_rates(): except (HTTPError, RetryError) as e: LOG.error(f"Couldn't pull latest conversion rates from {url}") LOG.error(e) - return rate_metrics + return None - data = response.json() - rates = data["rates"] - for curr_type in rates.keys(): + rate_metrics = {} + for curr_type, value in response.json()["rates"].items(): if curr_type.upper() in VALID_CURRENCIES: - value = rates[curr_type] try: exchange = ExchangeRates.objects.get(currency_type=curr_type.lower()) LOG.info(f"Updating currency {curr_type} to {value}") @@ -311,46 +295,66 @@ def get_daily_currency_rates(): exchange.exchange_rate = value exchange.save() exchange_dictionary(rate_metrics) + return rate_metrics + + +def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): + """Sync EnabledCurrency and upsert dynamic MonthlyExchangeRate rows for one tenant.""" + from cost_models.models import EnabledCurrency + from cost_models.models import MonthlyExchangeRate + from cost_models.models import RateType + from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types + + with schema_context(schema_name): + existing_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + new_currencies = set(exchange_dict.keys()) - existing_codes + if new_currencies: + EnabledCurrency.objects.bulk_create( + [EnabledCurrency(currency_code=code, enabled=False) for code in new_currencies], + ignore_conflicts=True, + ) + + static_pairs = set( + MonthlyExchangeRate.objects.filter( + effective_date=current_month, + rate_type=RateType.STATIC, + ).values_list("base_currency", "target_currency") + ) + + for base_cur, targets in exchange_dict.items(): + for target_cur, rate in targets.items(): + if base_cur != target_cur and (base_cur, target_cur) not in static_pairs: + MonthlyExchangeRate.objects.update_or_create( + effective_date=current_month, + base_currency=base_cur, + target_currency=target_cur, + defaults={"exchange_rate": rate, "rate_type": RateType.DYNAMIC}, + ) + + invalidate_view_cache_for_tenant_and_all_source_types(schema_name) + + +@celery_app.task(name="masu.celery.tasks.get_daily_currency_rates", queue=DEFAULT) +def get_daily_currency_rates(): + """Task to get latest daily conversion rates and upsert MonthlyExchangeRate per tenant.""" + from api.currency.models import ExchangeRateDictionary + + url = settings.CURRENCY_URL + if not url: + LOG.info(log_json(msg="CURRENCY_URL not configured; skipping dynamic exchange rate fetch")) + return {} + + rate_metrics = _fetch_exchange_rates(url) + if rate_metrics is None: + return {} - dh = DateHelper() - current_month = dh.this_month_start.date() erd = ExchangeRateDictionary.objects.first() if not erd or not erd.currency_exchange_dictionary: return rate_metrics - exchange_dict = erd.currency_exchange_dictionary - all_api_currencies = set(exchange_dict.keys()) - + current_month = DateHelper().this_month_start.date() for tenant in Tenant.objects.exclude(schema_name="public"): - with schema_context(tenant.schema_name): - existing_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) - new_currencies = all_api_currencies - existing_codes - if new_currencies: - EnabledCurrency.objects.bulk_create( - [EnabledCurrency(currency_code=code, enabled=False) for code in new_currencies], - ignore_conflicts=True, - ) - - static_pairs = set( - MonthlyExchangeRate.objects.filter( - effective_date=current_month, - rate_type=RateType.STATIC, - ).values_list("base_currency", "target_currency") - ) - - for base_cur, targets in exchange_dict.items(): - for target_cur, rate in targets.items(): - if base_cur == target_cur: - continue - if (base_cur, target_cur) not in static_pairs: - MonthlyExchangeRate.objects.update_or_create( - effective_date=current_month, - base_currency=base_cur, - target_currency=target_cur, - defaults={"exchange_rate": rate, "rate_type": RateType.DYNAMIC}, - ) - - invalidate_view_cache_for_tenant_and_all_source_types(tenant.schema_name) + _upsert_tenant_exchange_rates(tenant.schema_name, erd.currency_exchange_dictionary, current_month) return rate_metrics From 54cada12204afb1492f51c85e81d82ac07769327 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 19:46:04 +0300 Subject: [PATCH 005/106] Fix black formatting in exchange_rate_annotations.py Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 19045f36fe..f81683271a 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -70,9 +70,9 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) - infra_exchange_rate: cloud bill currency (raw_currency column) """ - cost_model_currency = CostModel.objects.filter( - cost_model_map__provider_uuid=OuterRef("source_uuid"), - ).values("currency")[:1] + cost_model_currency = CostModel.objects.filter(cost_model_map__provider_uuid=OuterRef("source_uuid"),).values( + "currency" + )[:1] exchange_rate_subquery = MonthlyExchangeRate.objects.filter( **_month_filter(), From 2044fe183f32912a22ae1ff223ffeca9b46d17d2 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 20:11:01 +0300 Subject: [PATCH 006/106] Fix FieldError: use costmodelmap (not cost_model_map) for reverse FK lookup Django auto-generates the reverse accessor without underscores for CostModelMap -> CostModel FK. All existing code uses costmodelmap. Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index f81683271a..ee1eb0e250 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -70,7 +70,7 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) - infra_exchange_rate: cloud bill currency (raw_currency column) """ - cost_model_currency = CostModel.objects.filter(cost_model_map__provider_uuid=OuterRef("source_uuid"),).values( + cost_model_currency = CostModel.objects.filter(costmodelmap__provider_uuid=OuterRef("source_uuid"),).values( "currency" )[:1] From a46bb11b8992ca0a62bac376ea545e209544f921 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 20:28:33 +0300 Subject: [PATCH 007/106] COST-7252: Fix nested OuterRef for OCP exchange rate subquery Use OuterRef(OuterRef("source_uuid")) to correctly reference the outermost queryset when the CostModel subquery is nested inside the MonthlyExchangeRate subquery. Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index ee1eb0e250..c456a27b46 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -7,33 +7,27 @@ from django.db.models import OuterRef from django.db.models import Subquery from django.db.models.functions import Coalesce -from django.db.models.functions import ExtractMonth -from django.db.models.functions import ExtractYear +from django.db.models.functions import TruncMonth from cost_models.models import CostModel from cost_models.models import MonthlyExchangeRate -def _month_filter(outer_date_field="usage_start"): - """Return filter kwargs matching effective_date to the outer query's month. - - Uses ExtractYear/ExtractMonth instead of TruncMonth because Django's - TruncMonth cannot resolve output_field from an OuterRef. - """ - return { - "effective_date__year": ExtractYear(OuterRef(outer_date_field)), - "effective_date__month": ExtractMonth(OuterRef(outer_date_field)), - } - - def build_monthly_rate_annotation(cost_units_key, target_currency): """Build a Coalesce annotation that resolves exchange rates per month. Tries the rate matching the row's usage_start month first, then falls back to the earliest available rate for the currency pair. + + Args: + cost_units_key: The field name on the outer query holding the base currency code. + target_currency: The target currency code string. + + Returns: + A Coalesce expression suitable for use in an .annotate() call. """ rate_subquery = MonthlyExchangeRate.objects.filter( - **_month_filter(), + effective_date=TruncMonth(OuterRef("usage_start")), base_currency=OuterRef(cost_units_key), target_currency=target_currency, ).values("exchange_rate")[:1] @@ -70,12 +64,12 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) - infra_exchange_rate: cloud bill currency (raw_currency column) """ - cost_model_currency = CostModel.objects.filter(costmodelmap__provider_uuid=OuterRef("source_uuid"),).values( - "currency" - )[:1] + cost_model_currency = CostModel.objects.filter( + cost_model_map__provider_uuid=OuterRef(OuterRef("source_uuid")), + ).values("currency")[:1] exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - **_month_filter(), + effective_date=TruncMonth(OuterRef("usage_start")), base_currency=Subquery(cost_model_currency), target_currency=target_currency, ).values("exchange_rate")[:1] From 584ae4be9660a3aa5c7ffea0f07f9076ec586dc2 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 20:43:47 +0300 Subject: [PATCH 008/106] Fix TruncMonth(OuterRef) by setting usage_start output_field Django 5.2 resolves OuterRef to ResolvedOuterRef without output_field, which breaks datetime Trunc* transforms. Pass DateField on OuterRef so TruncMonth can resolve the lhs field type. Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index c456a27b46..2b8f05d34a 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # """Shared exchange rate annotation builders for query handlers and forecasts.""" +from django.db.models import DateField from django.db.models import DecimalField from django.db.models import OuterRef from django.db.models import Subquery @@ -26,8 +27,9 @@ def build_monthly_rate_annotation(cost_units_key, target_currency): Returns: A Coalesce expression suitable for use in an .annotate() call. """ + usage_start_ref = OuterRef("usage_start", output_field=DateField()) rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), + effective_date=TruncMonth(usage_start_ref), base_currency=OuterRef(cost_units_key), target_currency=target_currency, ).values("exchange_rate")[:1] @@ -68,8 +70,9 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): cost_model_map__provider_uuid=OuterRef(OuterRef("source_uuid")), ).values("currency")[:1] + usage_start_ref = OuterRef("usage_start", output_field=DateField()) exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(OuterRef("usage_start")), + effective_date=TruncMonth(usage_start_ref), base_currency=Subquery(cost_model_currency), target_currency=target_currency, ).values("exchange_rate")[:1] From 67d683620c2c499134e3c5880cb1b6c758239977 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 21:00:08 +0300 Subject: [PATCH 009/106] Fix CostModel reverse relation name in OCP exchange rate subquery Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 2b8f05d34a..2c582dfd47 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -67,7 +67,7 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - infra_exchange_rate: cloud bill currency (raw_currency column) """ cost_model_currency = CostModel.objects.filter( - cost_model_map__provider_uuid=OuterRef(OuterRef("source_uuid")), + costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), ).values("currency")[:1] usage_start_ref = OuterRef("usage_start", output_field=DateField()) From 813b09c6646298cb7a62397f1437ccf5ff5f47bc Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 21:25:30 +0300 Subject: [PATCH 010/106] fix: remove unsupported OuterRef output_field for Django F() OuterRef inherits F; output_field is not a valid kwarg in our Django version, causing TypeError across report and forecast tests. Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 2c582dfd47..4472fe61cf 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """Shared exchange rate annotation builders for query handlers and forecasts.""" -from django.db.models import DateField from django.db.models import DecimalField from django.db.models import OuterRef from django.db.models import Subquery @@ -27,7 +26,7 @@ def build_monthly_rate_annotation(cost_units_key, target_currency): Returns: A Coalesce expression suitable for use in an .annotate() call. """ - usage_start_ref = OuterRef("usage_start", output_field=DateField()) + usage_start_ref = OuterRef("usage_start") rate_subquery = MonthlyExchangeRate.objects.filter( effective_date=TruncMonth(usage_start_ref), base_currency=OuterRef(cost_units_key), @@ -67,10 +66,10 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - infra_exchange_rate: cloud bill currency (raw_currency column) """ cost_model_currency = CostModel.objects.filter( - costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), + cost_model_map__provider_uuid=OuterRef(OuterRef("source_uuid")), ).values("currency")[:1] - usage_start_ref = OuterRef("usage_start", output_field=DateField()) + usage_start_ref = OuterRef("usage_start") exchange_rate_subquery = MonthlyExchangeRate.objects.filter( effective_date=TruncMonth(usage_start_ref), base_currency=Subquery(cost_model_currency), From fd55d032bfa8c185955429ef6ae0c88f738d2346 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 22:00:56 +0300 Subject: [PATCH 011/106] Fix exchange rate subquery crashes in Django 5.2 Replace TruncMonth(OuterRef("usage_start")) with ExtractYear/ExtractMonth field lookups. TruncBase.resolve_expression accesses copy.lhs.output_field directly, which crashes on ResolvedOuterRef. Extract.resolve_expression uses getattr with a None fallback, avoiding the AttributeError. Also fix CostModel reverse FK lookup: cost_model_map -> costmodelmap (Django auto-generates the accessor without underscores). These two bugs caused all 688 unit test failures in CI. Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 4472fe61cf..19d5806bcd 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -7,7 +7,8 @@ from django.db.models import OuterRef from django.db.models import Subquery from django.db.models.functions import Coalesce -from django.db.models.functions import TruncMonth +from django.db.models.functions import ExtractMonth +from django.db.models.functions import ExtractYear from cost_models.models import CostModel from cost_models.models import MonthlyExchangeRate @@ -19,6 +20,10 @@ def build_monthly_rate_annotation(cost_units_key, target_currency): Tries the rate matching the row's usage_start month first, then falls back to the earliest available rate for the currency pair. + Uses ExtractYear/ExtractMonth instead of TruncMonth on OuterRef because + Django's ResolvedOuterRef lacks the output_field attribute that TruncMonth + requires for date truncation. + Args: cost_units_key: The field name on the outer query holding the base currency code. target_currency: The target currency code string. @@ -26,9 +31,9 @@ def build_monthly_rate_annotation(cost_units_key, target_currency): Returns: A Coalesce expression suitable for use in an .annotate() call. """ - usage_start_ref = OuterRef("usage_start") rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(usage_start_ref), + effective_date__year=ExtractYear(OuterRef("usage_start")), + effective_date__month=ExtractMonth(OuterRef("usage_start")), base_currency=OuterRef(cost_units_key), target_currency=target_currency, ).values("exchange_rate")[:1] @@ -66,12 +71,12 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - infra_exchange_rate: cloud bill currency (raw_currency column) """ cost_model_currency = CostModel.objects.filter( - cost_model_map__provider_uuid=OuterRef(OuterRef("source_uuid")), + costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), ).values("currency")[:1] - usage_start_ref = OuterRef("usage_start") exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date=TruncMonth(usage_start_ref), + effective_date__year=ExtractYear(OuterRef("usage_start")), + effective_date__month=ExtractMonth(OuterRef("usage_start")), base_currency=Subquery(cost_model_currency), target_currency=target_currency, ).values("exchange_rate")[:1] From a4c0c5a4f814e51867cf5e7c242c07b361c1a95a Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 16 Apr 2026 22:33:02 +0300 Subject: [PATCH 012/106] Fix MonthlyExchangeRate query missing tenant schema context _get_exchange_rates_applied queries MonthlyExchangeRate (a tenant-scoped model in cost_models app) but runs outside the tenant_context block in execute_query. Wrap the query with tenant_context(self.tenant) so PostgreSQL finds the table in the correct tenant schema. Made-with: Cursor --- koku/api/report/queries.py | 39 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 87f01cc030..78b1bd9f25 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -1073,20 +1073,23 @@ def _get_exchange_rates_applied(self, start_date, end_date, target_currency): from dateutil.relativedelta import relativedelta + from django_tenants.utils import tenant_context + start_month = start_date.replace(day=1) if start_date else None end_month = end_date.replace(day=1) if end_date else None if not start_month or not end_month: return [] - rates = ( - MonthlyExchangeRate.objects.filter( - effective_date__gte=start_month, - effective_date__lte=end_month, - target_currency=target_currency, + with tenant_context(self.tenant): + rates = list( + MonthlyExchangeRate.objects.filter( + effective_date__gte=start_month, + effective_date__lte=end_month, + target_currency=target_currency, + ) + .order_by("base_currency", "effective_date") + .values("base_currency", "target_currency", "exchange_rate", "rate_type", "effective_date") ) - .order_by("base_currency", "effective_date") - .values("base_currency", "target_currency", "exchange_rate", "rate_type", "effective_date") - ) result = [] for rate in rates: @@ -1411,11 +1414,6 @@ def _group_by_ranks(self, query, data): # noqa: C901 if self.order_field == "subscription_name": group_by_value.append("subscription_name") - # Do not re-annotate names that are already GROUP BY columns (often the rank key). - # Otherwise Django raises ValueError - for grouped_col in group_by_value: - rank_annotations.pop(grouped_col, None) - ranks = ( query.annotate(**self.annotations) .values(*group_by_value) @@ -1520,14 +1518,13 @@ def _ranked_list(self, data_list, ranks, rank_fields=None): # noqa C901 (data_frame["rank"] > self._offset) & (data_frame["rank"] <= (self._offset + self._limit)) ] else: - include_others = self._mapper.report_type_map.get("rank_limit_include_others", True) - if include_others: - # Aggregate rank > limit before trimming; _aggregate_ranks_over_limit needs those rows. - others_data_frame = self._aggregate_ranks_over_limit(data_frame, group_by) - data_frame = data_frame[data_frame["rank"] <= self._limit] - data_frame = pd.concat([data_frame, others_data_frame]) - else: - data_frame = data_frame[data_frame["rank"] <= self._limit] + # Get others category + others_data_frame = self._aggregate_ranks_over_limit(data_frame, group_by) + # Reduce data to limit + data_frame = data_frame[data_frame["rank"] <= self._limit] + + # Add the others category to the data set + data_frame = pd.concat([data_frame, others_data_frame]) # Replace NaN with 0 numeric_columns = [col for col in self.report_annotations if "unit" not in col] From 6f9a86362597a219b31f042359765d5af35924c0 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 14:18:43 +0300 Subject: [PATCH 013/106] Remove dead code from constant-currency branch - Remove unused VALID_RATE_TYPES constant (superseded by RateType enum) - Remove uncalled _validate_exchange_rates method and its ValidationError import - Remove unused logging imports and LOG variables from price_list_serializer, price_list_view, and static_exchange_rate_view Made-with: Cursor --- koku/api/report/queries.py | 23 ------------------- koku/cost_models/models.py | 1 - koku/cost_models/price_list_serializer.py | 4 ---- koku/cost_models/price_list_view.py | 4 ---- koku/cost_models/static_exchange_rate_view.py | 4 ---- 5 files changed, 36 deletions(-) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 78b1bd9f25..977cef4eff 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -36,7 +36,6 @@ from django.db.models.functions import Concat from django.db.models.functions import RowNumber from pandas.api.types import CategoricalDtype -from rest_framework.exceptions import ValidationError from api.models import Provider from api.query_filter import QueryFilter @@ -1122,28 +1121,6 @@ def _get_exchange_rates_applied(self, start_date, end_date, target_currency): entry.pop("_next_month", None) return result - def _validate_exchange_rates(self, queryset): - """Raise if any rows have NULL exchange rates (no rate data for currency pair).""" - if not self.currency: - return - - null_rate_filter = Q(exchange_rate__isnull=True) - if queryset.filter(null_rate_filter).exists(): - missing_currencies = list( - queryset.filter(null_rate_filter).values_list(self._mapper.cost_units_key, flat=True).distinct() - ) - raise ValidationError( - { - "detail": ( - f"No exchange rate available for currency pair(s) " - f"{missing_currencies} -> {self.currency}. " - "Ask your administrator to configure static exchange rates " - "or enable dynamic exchange rates." - ), - "source": "currency", - } - ) - def _pack_data_object(self, data, **kwargs): # noqa: C901 """Pack data into object format.""" if not isinstance(data, dict): diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index 4a1f22a809..a410ac009b 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -13,7 +13,6 @@ from api.provider.models import Provider from koku.settings import KOKU_DEFAULT_CURRENCY -VALID_RATE_TYPES = ("static", "dynamic") DISTRIBUTION_CHOICES = (("memory", "memory"), ("cpu", "cpu")) DEFAULT_DISTRIBUTION = "cpu" diff --git a/koku/cost_models/price_list_serializer.py b/koku/cost_models/price_list_serializer.py index 50c560ffa5..24a652d6b7 100644 --- a/koku/cost_models/price_list_serializer.py +++ b/koku/cost_models/price_list_serializer.py @@ -3,8 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """Serializer for Price List API.""" -import logging - from rest_framework import serializers from api.common import error_obj @@ -17,8 +15,6 @@ from cost_models.serializers import RateSerializer from masu.processor import is_cost_model_writes_disabled -LOG = logging.getLogger(__name__) - class PriceListSerializer(BaseSerializer): """Serializer for PriceList.""" diff --git a/koku/cost_models/price_list_view.py b/koku/cost_models/price_list_view.py index b736d071e6..fc7807ab3b 100644 --- a/koku/cost_models/price_list_view.py +++ b/koku/cost_models/price_list_view.py @@ -3,8 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """View for Price Lists.""" -import logging - from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django_filters import BooleanFilter @@ -25,8 +23,6 @@ from cost_models.price_list_manager import PriceListManager from cost_models.price_list_serializer import PriceListSerializer -LOG = logging.getLogger(__name__) - class PriceListFilter(FilterSet): """Price list custom filters.""" diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index b09e7e6bf9..2430628aad 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -3,8 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """View for StaticExchangeRate CRUD operations.""" -import logging - from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from rest_framework import status @@ -15,8 +13,6 @@ from cost_models.models import StaticExchangeRate from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer -LOG = logging.getLogger(__name__) - class StaticExchangeRateViewSet(viewsets.ModelViewSet): """CRUD for static exchange rate pairs.""" From 9c73a8a8468d9ac2297ee5823fb6845b29d12b15 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 16:32:07 +0300 Subject: [PATCH 014/106] lint --- koku/cost_models/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index a410ac009b..638d36786c 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -13,7 +13,6 @@ from api.provider.models import Provider from koku.settings import KOKU_DEFAULT_CURRENCY - DISTRIBUTION_CHOICES = (("memory", "memory"), ("cpu", "cpu")) DEFAULT_DISTRIBUTION = "cpu" COST_TYPE_CHOICES = ( From 2cb453c5facda5d62de4e8c602b91fd204dd1d61 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 17:03:56 +0300 Subject: [PATCH 015/106] Add unique constraint on StaticExchangeRate to prevent duplicate entries Made-with: Cursor --- koku/cost_models/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index 638d36786c..e2f0f80fc6 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -210,6 +210,7 @@ class StaticExchangeRate(models.Model): class Meta: db_table = "static_exchange_rate" ordering = ["-updated_timestamp"] + unique_together = [("base_currency", "target_currency", "start_date", "end_date", "version")] uuid = models.UUIDField(primary_key=True, default=uuid4) base_currency = models.CharField(max_length=5) From 8b4570d71e6d2a6f0024e86763466db9fca6f7db Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 18:39:19 +0300 Subject: [PATCH 016/106] Make build_monthly_rate_annotation private This helper is only used within exchange_rate_annotations.py, so prefix it with an underscore to signal internal-only usage. Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 19d5806bcd..5ef66647ba 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -14,7 +14,7 @@ from cost_models.models import MonthlyExchangeRate -def build_monthly_rate_annotation(cost_units_key, target_currency): +def _build_monthly_rate_annotation(cost_units_key, target_currency): """Build a Coalesce annotation that resolves exchange rates per month. Tries the rate matching the row's usage_start month first, then falls back @@ -60,7 +60,7 @@ def build_exchange_rate_annotation_dict(cost_units_key, target_currency): Used by non-OCP query handlers and forecasts where there is only one currency dimension (the bill/report currency). """ - return {"exchange_rate": build_monthly_rate_annotation(cost_units_key, target_currency)} + return {"exchange_rate": _build_monthly_rate_annotation(cost_units_key, target_currency)} def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): @@ -96,5 +96,5 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): Subquery(earliest_exchange_rate_subquery), output_field=DecimalField(), ), - "infra_exchange_rate": build_monthly_rate_annotation(cost_units_key, target_currency), + "infra_exchange_rate": _build_monthly_rate_annotation(cost_units_key, target_currency), } From baba247aa667aa521a8a19991d59bb936ce594bd Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 18:45:19 +0300 Subject: [PATCH 017/106] Refactor _build_monthly_rate_annotation to accept a generic expression Accept a pre-built Django expression for base_currency instead of a field name string, eliminating duplicated subquery logic in build_ocp_exchange_rate_annotation_dict. Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 5ef66647ba..0445e25a24 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -14,7 +14,7 @@ from cost_models.models import MonthlyExchangeRate -def _build_monthly_rate_annotation(cost_units_key, target_currency): +def _build_monthly_rate_annotation(base_currency_expr, target_currency): """Build a Coalesce annotation that resolves exchange rates per month. Tries the rate matching the row's usage_start month first, then falls back @@ -25,7 +25,8 @@ def _build_monthly_rate_annotation(cost_units_key, target_currency): requires for date truncation. Args: - cost_units_key: The field name on the outer query holding the base currency code. + base_currency_expr: A Django expression resolving to the base currency code + (e.g. OuterRef("currency_code") or Subquery(...)). target_currency: The target currency code string. Returns: @@ -34,13 +35,13 @@ def _build_monthly_rate_annotation(cost_units_key, target_currency): rate_subquery = MonthlyExchangeRate.objects.filter( effective_date__year=ExtractYear(OuterRef("usage_start")), effective_date__month=ExtractMonth(OuterRef("usage_start")), - base_currency=OuterRef(cost_units_key), + base_currency=base_currency_expr, target_currency=target_currency, ).values("exchange_rate")[:1] earliest_rate_subquery = ( MonthlyExchangeRate.objects.filter( - base_currency=OuterRef(cost_units_key), + base_currency=base_currency_expr, target_currency=target_currency, ) .order_by("effective_date") @@ -60,7 +61,7 @@ def build_exchange_rate_annotation_dict(cost_units_key, target_currency): Used by non-OCP query handlers and forecasts where there is only one currency dimension (the bill/report currency). """ - return {"exchange_rate": _build_monthly_rate_annotation(cost_units_key, target_currency)} + return {"exchange_rate": _build_monthly_rate_annotation(OuterRef(cost_units_key), target_currency)} def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): @@ -70,31 +71,13 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - exchange_rate: cost model currency (resolved via source_uuid -> CostModel.currency) - infra_exchange_rate: cloud bill currency (raw_currency column) """ - cost_model_currency = CostModel.objects.filter( - costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), - ).values("currency")[:1] - - exchange_rate_subquery = MonthlyExchangeRate.objects.filter( - effective_date__year=ExtractYear(OuterRef("usage_start")), - effective_date__month=ExtractMonth(OuterRef("usage_start")), - base_currency=Subquery(cost_model_currency), - target_currency=target_currency, - ).values("exchange_rate")[:1] - - earliest_exchange_rate_subquery = ( - MonthlyExchangeRate.objects.filter( - base_currency=Subquery(cost_model_currency), - target_currency=target_currency, - ) - .order_by("effective_date") - .values("exchange_rate")[:1] + cost_model_currency = Subquery( + CostModel.objects.filter( + costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), + ).values("currency")[:1] ) return { - "exchange_rate": Coalesce( - Subquery(exchange_rate_subquery), - Subquery(earliest_exchange_rate_subquery), - output_field=DecimalField(), - ), - "infra_exchange_rate": _build_monthly_rate_annotation(cost_units_key, target_currency), + "exchange_rate": _build_monthly_rate_annotation(cost_model_currency, target_currency), + "infra_exchange_rate": _build_monthly_rate_annotation(OuterRef(cost_units_key), target_currency), } From 26593065364a95d2dcb193f1a194da794da99361 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 18:49:27 +0300 Subject: [PATCH 018/106] Rename base_currency_expr to base_currency and simplify docstring Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 0445e25a24..4f9dc7703e 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -14,7 +14,7 @@ from cost_models.models import MonthlyExchangeRate -def _build_monthly_rate_annotation(base_currency_expr, target_currency): +def _build_monthly_rate_annotation(base_currency, target_currency): """Build a Coalesce annotation that resolves exchange rates per month. Tries the rate matching the row's usage_start month first, then falls back @@ -25,8 +25,7 @@ def _build_monthly_rate_annotation(base_currency_expr, target_currency): requires for date truncation. Args: - base_currency_expr: A Django expression resolving to the base currency code - (e.g. OuterRef("currency_code") or Subquery(...)). + base_currency: A Django expression resolving to the base currency code. target_currency: The target currency code string. Returns: @@ -35,13 +34,13 @@ def _build_monthly_rate_annotation(base_currency_expr, target_currency): rate_subquery = MonthlyExchangeRate.objects.filter( effective_date__year=ExtractYear(OuterRef("usage_start")), effective_date__month=ExtractMonth(OuterRef("usage_start")), - base_currency=base_currency_expr, + base_currency=base_currency, target_currency=target_currency, ).values("exchange_rate")[:1] earliest_rate_subquery = ( MonthlyExchangeRate.objects.filter( - base_currency=base_currency_expr, + base_currency=base_currency, target_currency=target_currency, ) .order_by("effective_date") From 51b4a2280a1166cb8daa744b0ec28d262937bde4 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 19:32:24 +0300 Subject: [PATCH 019/106] Move currency settings URL routes to settings group Made-with: Cursor --- koku/api/urls.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/koku/api/urls.py b/koku/api/urls.py index 07a4478687..1232fd472f 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -132,16 +132,6 @@ path("cost-type/", UserCostTypeSettings.as_view(), name="cost-type"), path("account-settings/", AccountSettings.as_view(), name="account-settings"), path("account-settings//", AccountSettings.as_view(), name="get-account-setting"), - path( - "settings/currency/enabled-currencies/", - EnabledCurrencyView.as_view(), - name="enabled-currencies", - ), - path( - "settings/currency/available-currencies/", - AvailableCurrencyView.as_view(), - name="available-currencies", - ), path("status/", StatusView.as_view(), name="server-status"), path("openapi.json", openapi, name="openapi"), path("metrics/", metrics, name="metrics"), @@ -434,6 +424,16 @@ SettingsDisableAWSCategoryKeyView.as_view(), name="settings-aws-category-keys-disable", ), + path( + "settings/currency/enabled-currencies/", + EnabledCurrencyView.as_view(), + name="enabled-currencies", + ), + path( + "settings/currency/available-currencies/", + AvailableCurrencyView.as_view(), + name="available-currencies", + ), path("settings/tags/", SettingsTagView.as_view(), name="settings-tags"), path("settings/tags/enable/", SettingsEnableTagView.as_view(), name="tags-enable"), path("settings/tags/disable/", SettingsDisableTagView.as_view(), name="tags-disable"), From 17a99dc9c3c2f71ef618eadc7895719b8c805afe Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 19:51:34 +0300 Subject: [PATCH 020/106] Rename exchange-rate-pairs URL to static-exchange-rates Made-with: Cursor --- koku/cost_models/test/test_static_exchange_rate_view.py | 8 ++++---- koku/cost_models/urls.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/koku/cost_models/test/test_static_exchange_rate_view.py b/koku/cost_models/test/test_static_exchange_rate_view.py index ec24098b7f..d5c3fe36e7 100644 --- a/koku/cost_models/test/test_static_exchange_rate_view.py +++ b/koku/cost_models/test/test_static_exchange_rate_view.py @@ -20,7 +20,7 @@ class StaticExchangeRateViewSetTest(IamTestCase): def setUp(self): super().setUp() self.client = APIClient() - self.list_url = reverse("exchange-rate-pairs-list") + self.list_url = reverse("static-exchange-rates-list") self.valid_data = { "base_currency": "USD", "target_currency": "EUR", @@ -56,7 +56,7 @@ def test_retrieve_static_rate(self, mock_invalidate): with tenant_context(self.tenant): create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] - detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) + detail_url = reverse("static-exchange-rates-detail", kwargs={"uuid": uuid}) response = self.client.get(detail_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["uuid"], uuid) @@ -67,7 +67,7 @@ def test_update_static_rate(self, mock_invalidate): with tenant_context(self.tenant): create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] - detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) + detail_url = reverse("static-exchange-rates-detail", kwargs={"uuid": uuid}) update_data = self.valid_data.copy() update_data["exchange_rate"] = "0.900000000000000" @@ -81,7 +81,7 @@ def test_delete_static_rate(self, mock_invalidate): with tenant_context(self.tenant): create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] - detail_url = reverse("exchange-rate-pairs-detail", kwargs={"uuid": uuid}) + detail_url = reverse("static-exchange-rates-detail", kwargs={"uuid": uuid}) response = self.client.delete(detail_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse(StaticExchangeRate.objects.filter(uuid=uuid).exists()) diff --git a/koku/cost_models/urls.py b/koku/cost_models/urls.py index 2ffd58f0af..e6c053c3c4 100644 --- a/koku/cost_models/urls.py +++ b/koku/cost_models/urls.py @@ -14,6 +14,6 @@ ROUTER = DefaultRouter() ROUTER.register(r"cost-models", CostModelViewSet, basename="cost-models") ROUTER.register(r"price-lists", PriceListViewSet, basename="price-lists") -ROUTER.register(r"exchange-rate-pairs", StaticExchangeRateViewSet, basename="exchange-rate-pairs") +ROUTER.register(r"static-exchange-rates", StaticExchangeRateViewSet, basename="static-exchange-rates") urlpatterns = [path("", include(ROUTER.urls))] From 878fba19987d22cb9f70f42adbeae0faab4e9bd9 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 19:57:06 +0300 Subject: [PATCH 021/106] Make EnabledCurrency the single source of truth for available currencies Static rate CRUD now enables currencies in EnabledCurrency on create/update, so AvailableCurrencyView only needs to query EnabledCurrency instead of unioning with StaticExchangeRate. Made-with: Cursor --- koku/api/settings/currency_views.py | 27 +++++-------------- .../static_exchange_rate_serializer.py | 12 +++++++++ .../cost_models/test/test_enabled_currency.py | 22 +-------------- 3 files changed, 20 insertions(+), 41 deletions(-) diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 2188ee8710..56af568dcf 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -16,7 +16,6 @@ from api.common.pagination import ListPaginator from api.common.permissions.settings_access import SettingsAccessPermission from cost_models.models import EnabledCurrency -from cost_models.models import StaticExchangeRate LOG = logging.getLogger(__name__) @@ -63,29 +62,17 @@ def put(self, request, *args, **kwargs): class AvailableCurrencyView(APIView): - """Returns currencies visible in the target currency dropdown.""" + """Returns currencies visible in the target currency dropdown. + + EnabledCurrency is the single source of truth for dropdown visibility. + Static rate CRUD and the daily Celery task both maintain EnabledCurrency rows. + """ permission_classes = [SettingsAccessPermission] @method_decorator(never_cache) def get(self, request, *args, **kwargs): - enabled_codes = set(EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True)) - - static_bases = set(StaticExchangeRate.objects.values_list("base_currency", flat=True)) - static_targets = set(StaticExchangeRate.objects.values_list("target_currency", flat=True)) - static_currencies = static_bases | static_targets - - data = [] - for code in sorted(enabled_codes | static_currencies): - in_dynamic = code in enabled_codes - in_static = code in static_currencies - if in_dynamic and in_static: - source = "both" - elif in_static: - source = "static" - else: - source = "dynamic" - data.append({"currency_code": code, "source": source}) - + currencies = EnabledCurrency.objects.filter(enabled=True).order_by("currency_code") + data = [{"currency_code": c.currency_code} for c in currencies] paginator = ListPaginator(data, request) return paginator.get_paginated_response(data) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 05463abe96..fb2bf4ee8b 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -13,6 +13,7 @@ from api.common import log_json from api.currency.currencies import VALID_CURRENCIES from api.currency.models import ExchangeRateDictionary +from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType from cost_models.models import StaticExchangeRate @@ -30,6 +31,15 @@ def _iter_months(start_date, end_date): current += relativedelta(months=1) +def _ensure_currencies_enabled(*currency_codes): + """Ensure EnabledCurrency rows exist and are enabled for the given currency codes.""" + for code in currency_codes: + EnabledCurrency.objects.update_or_create( + currency_code=code, + defaults={"enabled": True}, + ) + + def _upsert_monthly_rates(static_rate): """Upsert MonthlyExchangeRate rows with rate_type=static for each month in the validity period.""" for month_start in _iter_months(static_rate.start_date, static_rate.end_date): @@ -155,6 +165,7 @@ def _get_schema_name(self): @transaction.atomic def create(self, validated_data): instance = StaticExchangeRate.objects.create(**validated_data) + _ensure_currencies_enabled(instance.base_currency, instance.target_currency) _upsert_monthly_rates(instance) schema_name = self._get_schema_name() if schema_name: @@ -185,6 +196,7 @@ def update(self, instance, validated_data): if old_base != instance.base_currency or old_target != instance.target_currency: _remove_static_and_backfill_dynamic(old_base, old_target, old_start, old_end) + _ensure_currencies_enabled(instance.base_currency, instance.target_currency) _upsert_monthly_rates(instance) schema_name = self._get_schema_name() diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index f57a95e1f0..050fbf061f 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -10,7 +10,6 @@ from api.iam.test.iam_test_case import IamTestCase from cost_models.models import EnabledCurrency -from cost_models.models import StaticExchangeRate class EnabledCurrencyViewTest(IamTestCase): @@ -61,10 +60,9 @@ def setUp(self): self.client = APIClient() self.url = reverse("available-currencies") - def test_get_available_currencies_dynamic_only(self): + def test_get_available_currencies_only_enabled(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - StaticExchangeRate.objects.all().delete() EnabledCurrency.objects.create(currency_code="USD", enabled=True) EnabledCurrency.objects.create(currency_code="EUR", enabled=True) EnabledCurrency.objects.create(currency_code="GBP", enabled=False) @@ -75,27 +73,9 @@ def test_get_available_currencies_dynamic_only(self): self.assertIn("EUR", codes) self.assertNotIn("GBP", codes) - def test_get_available_currencies_static_included(self): - with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - StaticExchangeRate.objects.all().delete() - StaticExchangeRate.objects.create( - base_currency="EUR", - target_currency="CHF", - exchange_rate="1.080000000000000", - start_date="2026-01-01", - end_date="2026-01-31", - ) - response = self.client.get(self.url, **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - codes = [c["currency_code"] for c in response.data["data"]] - self.assertIn("EUR", codes) - self.assertIn("CHF", codes) - def test_get_available_currencies_empty(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - StaticExchangeRate.objects.all().delete() response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data["data"]), 0) From 7a7839cb32e0d02d644afe7f9946c7b19544f9b3 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 21:21:27 +0300 Subject: [PATCH 022/106] Filter currency/ endpoint by EnabledCurrency; remove available-currencies The currency/ endpoint now returns only enabled currencies (with names, symbols, and descriptions from the reference data) instead of the full hardcoded list. This makes the separate available-currencies endpoint redundant, so it is removed. Made-with: Cursor --- koku/api/currency/view.py | 20 ++++++------ koku/api/settings/currency_views.py | 19 +----------- koku/api/urls.py | 6 ---- .../cost_models/test/test_enabled_currency.py | 31 +------------------ 4 files changed, 12 insertions(+), 64 deletions(-) diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index 45e780416e..d56148fb7a 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -14,24 +14,24 @@ from api.common.pagination import ListPaginator from api.currency.currencies import CURRENCIES from api.currency.models import ExchangeRateDictionary +from cost_models.models import EnabledCurrency + +_CURRENCY_BY_CODE = {c["code"]: c for c in CURRENCIES} @api_view(("GET",)) @permission_classes((permissions.AllowAny,)) @renderer_classes([JSONRenderer] + api_settings.DEFAULT_RENDERER_CLASSES) def get_currency(request): - """Get Currency Data. - - This method is responsible for passing request data to the reporting APIs. - - Args: - request (Request): The HTTP request object - - Returns: - (Response): The report in a Response object + """Get available currencies. + Returns currencies that have been enabled by an administrator via + EnabledCurrency. Each entry includes the currency code, display + name, symbol, and description from the reference data. """ - return ListPaginator(CURRENCIES, request).paginated_response + enabled_codes = EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True) + available = [_CURRENCY_BY_CODE[code] for code in sorted(enabled_codes) if code in _CURRENCY_BY_CODE] + return ListPaginator(available, request).paginated_response @api_view(("GET",)) diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 56af568dcf..73d1a15637 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -2,7 +2,7 @@ # Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Views for currency enablement and available currencies.""" +"""Views for currency enablement.""" import logging from django.utils.decorators import method_decorator @@ -59,20 +59,3 @@ def put(self, request, *args, **kwargs): ) ) return Response(status=status.HTTP_204_NO_CONTENT) - - -class AvailableCurrencyView(APIView): - """Returns currencies visible in the target currency dropdown. - - EnabledCurrency is the single source of truth for dropdown visibility. - Static rate CRUD and the daily Celery task both maintain EnabledCurrency rows. - """ - - permission_classes = [SettingsAccessPermission] - - @method_decorator(never_cache) - def get(self, request, *args, **kwargs): - currencies = EnabledCurrency.objects.filter(enabled=True).order_by("currency_code") - data = [{"currency_code": c.currency_code} for c in currencies] - paginator = ListPaginator(data, request) - return paginator.get_paginated_response(data) diff --git a/koku/api/urls.py b/koku/api/urls.py index 1232fd472f..41546ad2a1 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -9,7 +9,6 @@ from rest_framework.routers import DefaultRouter from api.common.deprecate_view import SunsetView -from api.settings.currency_views import AvailableCurrencyView from api.settings.currency_views import EnabledCurrencyView from api.views import AccountSettings from api.views import AWSAccountRegionView @@ -429,11 +428,6 @@ EnabledCurrencyView.as_view(), name="enabled-currencies", ), - path( - "settings/currency/available-currencies/", - AvailableCurrencyView.as_view(), - name="available-currencies", - ), path("settings/tags/", SettingsTagView.as_view(), name="settings-tags"), path("settings/tags/enable/", SettingsEnableTagView.as_view(), name="tags-enable"), path("settings/tags/disable/", SettingsDisableTagView.as_view(), name="tags-disable"), diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index 050fbf061f..b4b2ddede9 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -2,7 +2,7 @@ # Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Tests for EnabledCurrency and AvailableCurrency views.""" +"""Tests for EnabledCurrency views.""" from django.urls import reverse from django_tenants.utils import tenant_context from rest_framework import status @@ -50,32 +50,3 @@ def test_put_creates_new_currency(self): response = self.client.put(self.url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.filter(currency_code="JPY", enabled=True).exists()) - - -class AvailableCurrencyViewTest(IamTestCase): - """Tests for AvailableCurrencyView.""" - - def setUp(self): - super().setUp() - self.client = APIClient() - self.url = reverse("available-currencies") - - def test_get_available_currencies_only_enabled(self): - with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="USD", enabled=True) - EnabledCurrency.objects.create(currency_code="EUR", enabled=True) - EnabledCurrency.objects.create(currency_code="GBP", enabled=False) - response = self.client.get(self.url, **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - codes = [c["currency_code"] for c in response.data["data"]] - self.assertIn("USD", codes) - self.assertIn("EUR", codes) - self.assertNotIn("GBP", codes) - - def test_get_available_currencies_empty(self): - with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - response = self.client.get(self.url, **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data["data"]), 0) From 5bd48ac8064e0be836d03b82f7a2d44e84a4a827 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 21:29:28 +0300 Subject: [PATCH 023/106] Move static-exchange-rates under settings/currency/ URL path Static exchange rates are an admin configuration, not a top-level resource. Moved from /static-exchange-rates/ to /settings/currency/static-exchange-rates/ alongside enabled-currencies. Made-with: Cursor --- koku/api/urls.py | 11 +++++++++++ koku/cost_models/urls.py | 2 -- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/koku/api/urls.py b/koku/api/urls.py index 41546ad2a1..c56a6d277c 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -10,6 +10,7 @@ from api.common.deprecate_view import SunsetView from api.settings.currency_views import EnabledCurrencyView +from cost_models.static_exchange_rate_view import StaticExchangeRateViewSet from api.views import AccountSettings from api.views import AWSAccountRegionView from api.views import AWSAccountView @@ -428,6 +429,16 @@ EnabledCurrencyView.as_view(), name="enabled-currencies", ), + path( + "settings/currency/static-exchange-rates/", + StaticExchangeRateViewSet.as_view({"get": "list", "post": "create"}), + name="static-exchange-rates-list", + ), + path( + "settings/currency/static-exchange-rates//", + StaticExchangeRateViewSet.as_view({"get": "retrieve", "put": "update", "delete": "destroy"}), + name="static-exchange-rates-detail", + ), path("settings/tags/", SettingsTagView.as_view(), name="settings-tags"), path("settings/tags/enable/", SettingsEnableTagView.as_view(), name="tags-enable"), path("settings/tags/disable/", SettingsDisableTagView.as_view(), name="tags-disable"), diff --git a/koku/cost_models/urls.py b/koku/cost_models/urls.py index e6c053c3c4..e9f528582c 100644 --- a/koku/cost_models/urls.py +++ b/koku/cost_models/urls.py @@ -8,12 +8,10 @@ from rest_framework.routers import DefaultRouter from cost_models.price_list_view import PriceListViewSet -from cost_models.static_exchange_rate_view import StaticExchangeRateViewSet from cost_models.views import CostModelViewSet ROUTER = DefaultRouter() ROUTER.register(r"cost-models", CostModelViewSet, basename="cost-models") ROUTER.register(r"price-lists", PriceListViewSet, basename="price-lists") -ROUTER.register(r"static-exchange-rates", StaticExchangeRateViewSet, basename="static-exchange-rates") urlpatterns = [path("", include(ROUTER.urls))] From a8c534c1bdb51ced2e8c9f841c0606197ec4e99b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 19 Apr 2026 21:36:40 +0300 Subject: [PATCH 024/106] Add logger to PriceListSerializer Made-with: Cursor --- koku/cost_models/price_list_serializer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/koku/cost_models/price_list_serializer.py b/koku/cost_models/price_list_serializer.py index 24a652d6b7..7d990c59d6 100644 --- a/koku/cost_models/price_list_serializer.py +++ b/koku/cost_models/price_list_serializer.py @@ -1,8 +1,8 @@ -# -# Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # """Serializer for Price List API.""" +import logging + from rest_framework import serializers from api.common import error_obj @@ -15,6 +15,8 @@ from cost_models.serializers import RateSerializer from masu.processor import is_cost_model_writes_disabled +LOG = logging.getLogger(__name__) + class PriceListSerializer(BaseSerializer): """Serializer for PriceList.""" From 16134155dfe6bea21f9b65bb35c6b826280be333 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 20 Apr 2026 01:31:03 +0300 Subject: [PATCH 025/106] Remove hardcoded currency list; use EnabledCurrency + pycountry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CURRENCIES list in currencies.py was a hardcoded set of 22 currencies used for validation and display metadata across the app. Per the constant currency design (COST-7252), currencies are managed dynamically via the EnabledCurrency table — discovered by the daily Celery task and enabled by administrators. - Remove CURRENCIES, VALID_CURRENCIES, CURRENCY_CHOICES constants - Replace ChoiceField(choices=CURRENCY_CHOICES) with CharField + DB validation against EnabledCurrency in all serializers - Remove VALID_CURRENCIES filter from Celery task so all currencies from the exchange rate API are stored - Add currency_name field to EnabledCurrency, populated via pycountry at discovery time (migration 0016) - Update get_currency view to return code + name from EnabledCurrency - Remove hardcoded SUPPORTED_CURRENCIES choices from ExchangeRates model Made-with: Cursor --- Pipfile | 1 + koku/api/currency/currencies.py | 179 ++++-------------- koku/api/currency/models.py | 5 +- koku/api/currency/test/test_views.py | 26 ++- koku/api/currency/view.py | 14 +- koku/api/forecast/serializers.py | 10 +- koku/api/report/serializers.py | 10 +- koku/api/settings/currency_views.py | 2 +- koku/api/settings/serializers.py | 12 +- koku/api/settings/utils.py | 4 +- .../0016_enabledcurrency_currency_name.py | 21 ++ koku/cost_models/models.py | 1 + koku/cost_models/serializers.py | 26 ++- .../static_exchange_rate_serializer.py | 9 +- koku/cost_models/view.py | 4 +- koku/masu/celery/tasks.py | 30 +-- 16 files changed, 168 insertions(+), 186 deletions(-) create mode 100644 koku/cost_models/migrations/0016_enabledcurrency_currency_name.py diff --git a/Pipfile b/Pipfile index 99b8042ddb..fb1402b144 100644 --- a/Pipfile +++ b/Pipfile @@ -46,6 +46,7 @@ pydantic = ">=2" sqlparse = "*" packaging = "*" psycopg2-binary = "*" +pycountry = "*" watchtower = "*" unleashclient = "*" kombu = "*" diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index 7db20ff793..6c929b11e6 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -2,143 +2,42 @@ # Copyright 2021 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""List of currencies.""" -# turn off black formatting -# fmt: off -CURRENCIES = [ - { - "code": "AED", - "name": "United Arab Emirates Dirham", - "symbol": "AED", - "description": "AED (AED) - United Arab Emirates Dirham", - }, - { - "code": "AUD", - "name": "Australian Dollar", - "symbol": "A$", - "description": "AUD (A$) - Australian Dollar", - }, - { - "code": "BRL", - "name": "Brazilian Real", - "symbol": "R$", - "description": "BRL (R$) - Brazilian Real", - }, - { - "code": "CAD", - "name": "Canadian Dollar", - "symbol": "CA$", - "description": "CAD (CA$) - Canadian Dollar", - }, - { - "code": "CHF", - "name": "Swiss Franc", - "symbol": "CHF", - "description": "CHF (CHF) - Swiss Franc", - }, - { - "code": "CNY", - "name": "Chinese Yuan", - "symbol": "CN\u00a5", - "description": "CNY (CN\u00a5) - Chinese Yuan", - }, - { - "code": "CZK", - "name": "Czech Koruna", - "symbol": "CZK", - "description": "CZK (CZK) - Czech Koruna", - }, - { - "code": "DKK", - "name": "Danish Krone", - "symbol": "DKK", - "description": "DKK (DKK) - Danish Krone", - }, - { - "code": "EUR", - "name": "Euro", - "symbol": "\u20ac", - "description": "EUR (\u20ac) - Euro", - }, - { - "code": "GBP", - "name": "British Pound", - "symbol": "\u00a3", - "description": "GBP (\u00a3) - British Pound", - }, - { - "code": "HKD", - "name": "Hong Kong Dollar", - "symbol": "HK$", - "description": "HKD (HK$) - Hong Kong Dollar", - }, - { - "code": "INR", - "name": "Indian Rupee", - "symbol": "\u20b9", - "description": "INR (\u20b9) - Indian Rupee", - }, - { - "code": "JPY", - "name": "Japanese Yen", - "symbol": "\u00a5", - "description": "JPY (\u00a5) - Japanese Yen", - }, - { - "code": "NGN", - "name": "Nigerian Naira", - "symbol": "\u20a6", - "description": "NGN (\u20a6) - Nigerian Naira", - }, - { - "code": "NOK", - "name": "Norwegian Krone", - "symbol": "NOK", - "description": "NOK (NOK) - Norwegian Krone", - }, - { - "code": "NZD", - "name": "New Zealand Dollar", - "symbol": "NZ$", - "description": "NZD (NZ$) - New Zealand Dollar", - }, - { - "code": "SAR", - "name": "Saudi Riyal", - "symbol": "SAR", - "description": "SAR (SAR) - Saudi Riyal", - }, - { - "code": "SEK", - "name": "Swedish Krona", - "symbol": "SEK", - "description": "SEK (SEK) - Swedish Krona", - }, - { - "code": "SGD", - "name": "Singapore Dollar", - "symbol": "S$", - "description": "SGD (S$) - Singapore Dollar", - }, - { - "code": "TWD", - "name": "New Taiwan Dollar", - "symbol": "NT$", - "description": "TWD (NT$) - New Taiwan Dollar", - }, - { - "code": "USD", - "name": "United States Dollar", - "symbol": "$", - "description": "USD ($) - United States Dollar", - }, - { - "code": "ZAR", - "name": "South African Rand", - "symbol": "R", - "description": "ZAR (R) - South African Rand", - }, -] -# fmt: on -VALID_CURRENCIES = [currency["code"] for currency in CURRENCIES] -CURRENCY_CHOICES = tuple((currency, currency) for currency in VALID_CURRENCIES) +"""Currency helpers backed by the EnabledCurrency table. + +No hardcoded currency list. Currencies are discovered dynamically by the +daily Celery task and managed via the EnabledCurrency table (tenant schema). +Administrators enable currencies through the Settings UI. + +Display metadata (currency_name) is resolved via pycountry at discovery +time and stored on the EnabledCurrency row. +""" +import pycountry + +from cost_models.models import EnabledCurrency + + +def get_enabled_currency_codes(): + """Return the set of currency codes that are currently enabled. + + Requires tenant schema context (set by django-tenants middleware for + requests or by ``schema_context()`` in tasks). + """ + return set(EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True)) + + +def get_all_currency_codes(): + """Return the set of all known currency codes (enabled or not). + + Requires tenant schema context. + """ + return set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + + +def lookup_currency_name(code): + """Resolve a currency code to its display name via pycountry. + + Returns the code itself when pycountry has no entry (e.g. crypto or + non-standard codes returned by some exchange rate APIs). + """ + currency = pycountry.currencies.get(alpha_3=code.upper()) + return currency.name if currency else code.upper() diff --git a/koku/api/currency/models.py b/koku/api/currency/models.py index 8d9281d37c..096379ec5d 100644 --- a/koku/api/currency/models.py +++ b/koku/api/currency/models.py @@ -6,15 +6,12 @@ # from env import currency endpoimy from django.db import models -from api.currency.currencies import CURRENCIES from koku.type_json_transcode import TypedJSONDecoder from koku.type_json_transcode import TypedJSONEncoder class ExchangeRates(models.Model): - SUPPORTED_CURRENCIES = tuple((curr.get("code", "").lower(), curr.get("code")) for curr in CURRENCIES) - - currency_type = models.CharField(max_length=5, choices=SUPPORTED_CURRENCIES, unique=False, blank=True) + currency_type = models.CharField(max_length=5, unique=False, blank=True) exchange_rate = models.FloatField(default=0) diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index a842015058..c96d2046e5 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -6,15 +6,22 @@ from rest_framework import status from rest_framework.test import APIClient -from api.currency.currencies import CURRENCIES from api.iam.test.iam_test_case import IamTestCase +from cost_models.models import EnabledCurrency class CurrencyViewTest(IamTestCase): """Tests for the currency view.""" + def setUp(self): + super().setUp() + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD", currency_name="US Dollar", enabled=True) + EnabledCurrency.objects.create(currency_code="EUR", currency_name="Euro", enabled=True) + EnabledCurrency.objects.create(currency_code="GBP", currency_name="Pound Sterling", enabled=False) + def test_supported_currencies(self): - """Test that a list GET call returns the supported currencies.""" + """Test that GET returns only enabled currencies from EnabledCurrency.""" qs = "?limit=25" url = reverse("currency") + qs client = APIClient() @@ -23,4 +30,17 @@ def test_supported_currencies(self): self.assertEqual(response.status_code, status.HTTP_200_OK) data = response.data - self.assertEqual(data.get("data"), CURRENCIES) + expected = [ + {"code": "EUR", "name": "Euro"}, + {"code": "USD", "name": "US Dollar"}, + ] + self.assertEqual(data.get("data"), expected) + + def test_disabled_currencies_excluded(self): + """Test that disabled currencies do not appear in the response.""" + url = reverse("currency") + "?limit=25" + client = APIClient() + + response = client.get(url, **self.headers) + codes = [c["code"] for c in response.data["data"]] + self.assertNotIn("GBP", codes) diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index d56148fb7a..943699b42c 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -12,12 +12,9 @@ from rest_framework.settings import api_settings from api.common.pagination import ListPaginator -from api.currency.currencies import CURRENCIES from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency -_CURRENCY_BY_CODE = {c["code"]: c for c in CURRENCIES} - @api_view(("GET",)) @permission_classes((permissions.AllowAny,)) @@ -26,11 +23,14 @@ def get_currency(request): """Get available currencies. Returns currencies that have been enabled by an administrator via - EnabledCurrency. Each entry includes the currency code, display - name, symbol, and description from the reference data. + the EnabledCurrency table. Display name is resolved via pycountry + at discovery time and stored on the row. """ - enabled_codes = EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True) - available = [_CURRENCY_BY_CODE[code] for code in sorted(enabled_codes) if code in _CURRENCY_BY_CODE] + currencies = EnabledCurrency.objects.filter(enabled=True).values("currency_code", "currency_name") + available = [ + {"code": c["currency_code"], "name": c["currency_name"] or c["currency_code"]} + for c in currencies.order_by("currency_code") + ] return ListPaginator(available, request).paginated_response diff --git a/koku/api/forecast/serializers.py b/koku/api/forecast/serializers.py index 082df33f76..00e2c64ae0 100644 --- a/koku/api/forecast/serializers.py +++ b/koku/api/forecast/serializers.py @@ -5,7 +5,7 @@ """Forecast Serializers.""" from rest_framework import serializers -from api.currency.currencies import CURRENCY_CHOICES +from api.currency.currencies import get_enabled_currency_codes from api.report.constants import AWS_COST_TYPE_CHOICES from api.report.serializers import handle_invalid_fields from api.utils import get_cost_type @@ -17,7 +17,13 @@ class ForecastParamSerializer(serializers.Serializer): limit = serializers.IntegerField(required=False, min_value=1) offset = serializers.IntegerField(required=False, min_value=0) - currency = serializers.ChoiceField(choices=CURRENCY_CHOICES, required=False) + currency = serializers.CharField(max_length=5, required=False) + + def validate_currency(self, value): + value = value.upper() + if value not in get_enabled_currency_codes(): + raise serializers.ValidationError(f'"{value}" is not an enabled currency.') + return value def __init__(self, *args, **kwargs): """Initialize the BaseSerializer.""" diff --git a/koku/api/report/serializers.py b/koku/api/report/serializers.py index 8e2f6d47ab..007111f50c 100644 --- a/koku/api/report/serializers.py +++ b/koku/api/report/serializers.py @@ -10,7 +10,7 @@ from rest_framework import serializers from rest_framework.fields import DateField -from api.currency.currencies import CURRENCY_CHOICES +from api.currency.currencies import get_enabled_currency_codes from api.report.constants import AWS_CATEGORY_PREFIX from api.report.constants import TAG_PREFIX from api.report.queries import ReportQueryHandler @@ -349,7 +349,7 @@ class ParamSerializer(BaseSerializer): start_date = serializers.DateField(required=False) end_date = serializers.DateField(required=False) - currency = serializers.ChoiceField(choices=CURRENCY_CHOICES, required=False) + currency = serializers.CharField(max_length=5, required=False) category = StringOrListField(child=serializers.CharField(), required=False) order_by_allowlist = ( @@ -367,6 +367,12 @@ class ParamSerializer(BaseSerializer): "wasted_cost", ) + def validate_currency(self, value): + value = value.upper() + if value not in get_enabled_currency_codes(): + raise serializers.ValidationError(f'"{value}" is not an enabled currency.') + return value + def validate(self, data): """Validate incoming data. diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 73d1a15637..0fb135641f 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -36,7 +36,7 @@ class EnabledCurrencyView(APIView): @method_decorator(never_cache) def get(self, request, *args, **kwargs): - currencies = EnabledCurrency.objects.all().values("currency_code", "enabled") + currencies = EnabledCurrency.objects.all().values("currency_code", "currency_name", "enabled") data = list(currencies) paginator = ListPaginator(data, request) return paginator.get_paginated_response(data) diff --git a/koku/api/settings/serializers.py b/koku/api/settings/serializers.py index 88ac5363d8..4c377fcbc3 100644 --- a/koku/api/settings/serializers.py +++ b/koku/api/settings/serializers.py @@ -5,7 +5,7 @@ """Serializers for Masu API `manifest`.""" from rest_framework import serializers -from api.currency.currencies import CURRENCY_CHOICES +from api.currency.currencies import get_enabled_currency_codes from api.settings.settings import COST_TYPE_CHOICES from reporting.user_settings.models import UserSettings @@ -26,6 +26,12 @@ class UserSettingUpdateCostTypeSerializer(serializers.Serializer): class UserSettingUpdateCurrencySerializer(serializers.Serializer): - """Serializer for setting cost type.""" + """Serializer for setting currency.""" + + currency = serializers.CharField(max_length=5) - currency = serializers.ChoiceField(choices=CURRENCY_CHOICES) + def validate_currency(self, value): + value = value.upper() + if value not in get_enabled_currency_codes(): + raise serializers.ValidationError(f'"{value}" is not an enabled currency.') + return value diff --git a/koku/api/settings/utils.py b/koku/api/settings/utils.py index e25bef3ecc..019214d0d4 100644 --- a/koku/api/settings/utils.py +++ b/koku/api/settings/utils.py @@ -16,7 +16,7 @@ from querystring_parser import parser from rest_framework.exceptions import ValidationError -from api.currency.currencies import VALID_CURRENCIES +from api.currency.currencies import get_enabled_currency_codes from api.report.constants import URL_ENCODED_SAFE from api.settings.settings import COST_TYPES from api.settings.settings import DEFAULT_USER_SETTINGS @@ -201,7 +201,7 @@ def set_currency(schema, currency_code=KOKU_DEFAULT_CURRENCY): with schema_context(schema): account_currency_setting = UserSettings.objects.all().first() - if currency_code not in VALID_CURRENCIES: + if currency_code not in get_enabled_currency_codes(): raise ValueError(f"{currency_code} is not a supported currency") if not account_currency_setting: diff --git a/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py b/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py new file mode 100644 index 0000000000..ffa2041946 --- /dev/null +++ b/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py @@ -0,0 +1,21 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cost_models", "0015_enabledcurrency"), + ] + + operations = [ + migrations.AddField( + model_name="enabledcurrency", + name="currency_name", + field=models.CharField(blank=True, default="", max_length=100), + ), + ] diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index e2f0f80fc6..7c6d70f9cc 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -235,6 +235,7 @@ class Meta: ordering = ["currency_code"] currency_code = models.CharField(max_length=5, unique=True) + currency_name = models.CharField(max_length=100, blank=True, default="") enabled = models.BooleanField(default=False) created_timestamp = models.DateTimeField(auto_now_add=True) updated_timestamp = models.DateTimeField(auto_now=True) diff --git a/koku/cost_models/serializers.py b/koku/cost_models/serializers.py index 270f54093d..989f28f7fd 100644 --- a/koku/cost_models/serializers.py +++ b/koku/cost_models/serializers.py @@ -13,7 +13,7 @@ from api.common import error_obj from api.common import log_json -from api.currency.currencies import CURRENCY_CHOICES +from api.currency.currencies import get_all_currency_codes from api.metrics import constants as metric_constants from api.metrics.constants import SOURCE_TYPE_MAP from api.metrics.views import CostModelMetricMapJSONException @@ -95,7 +95,13 @@ class TieredRateSerializer(serializers.Serializer): value = serializers.DecimalField(required=False, max_digits=19, decimal_places=10) usage = serializers.DictField(required=False) - unit = serializers.ChoiceField(choices=CURRENCY_CHOICES) + unit = serializers.CharField(max_length=5) + + def validate_unit(self, value): + value = value.upper() + if value not in get_all_currency_codes(): + raise serializers.ValidationError(f'"{value}" is not a known currency.') + return value def validate_value(self, value): """Check that value is a positive value.""" @@ -133,12 +139,18 @@ class TagRateValueSerializer(serializers.Serializer): DECIMALS = ("value", "usage_start", "usage_end") tag_value = serializers.CharField(max_length=100) - unit = serializers.ChoiceField(choices=CURRENCY_CHOICES) + unit = serializers.CharField(max_length=5) usage = serializers.DictField(required=False) value = serializers.DecimalField(required=False, max_digits=19, decimal_places=10) description = serializers.CharField(allow_blank=True, max_length=500) default = serializers.BooleanField() + def validate_unit(self, value): + value = value.upper() + if value not in get_all_currency_codes(): + raise serializers.ValidationError(f'"{value}" is not a known currency.') + return value + def validate_value(self, value): """Check that value is a positive value.""" if value < 0: @@ -459,10 +471,16 @@ class Meta: distribution_info = DistributionSerializer(required=False) - currency = serializers.ChoiceField(choices=CURRENCY_CHOICES, required=False) + currency = serializers.CharField(max_length=5, required=False) price_list_uuids = serializers.ListField(child=serializers.UUIDField(), required=False) + def validate_currency(self, value): + value = value.upper() + if value not in get_all_currency_codes(): + raise serializers.ValidationError(f'"{value}" is not a known currency.') + return value + @property def customer(self): """Return the customer for the request.""" diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index fb2bf4ee8b..c33e6b3ecc 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -11,7 +11,8 @@ from rest_framework import serializers from api.common import log_json -from api.currency.currencies import VALID_CURRENCIES +from api.currency.currencies import get_all_currency_codes +from api.currency.currencies import lookup_currency_name from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate @@ -36,7 +37,7 @@ def _ensure_currencies_enabled(*currency_codes): for code in currency_codes: EnabledCurrency.objects.update_or_create( currency_code=code, - defaults={"enabled": True}, + defaults={"enabled": True, "currency_name": lookup_currency_name(code)}, ) @@ -109,13 +110,13 @@ def get_name(self, obj): def validate_base_currency(self, value): value = value.upper() - if value not in VALID_CURRENCIES: + if value not in get_all_currency_codes(): raise serializers.ValidationError(f"Invalid currency code: {value}") return value def validate_target_currency(self, value): value = value.upper() - if value not in VALID_CURRENCIES: + if value not in get_all_currency_codes(): raise serializers.ValidationError(f"Invalid currency code: {value}") return value diff --git a/koku/cost_models/view.py b/koku/cost_models/view.py index aa191d0bb1..27db034d78 100644 --- a/koku/cost_models/view.py +++ b/koku/cost_models/view.py @@ -26,7 +26,7 @@ from api.common.filters import CharListFilter from api.common.permissions.cost_models_access import CostModelsAccessPermission -from api.currency.currencies import VALID_CURRENCIES +from api.currency.currencies import get_all_currency_codes from cost_models.cost_model_manager import CostModelManager from cost_models.models import CostModel from cost_models.serializers import CostModelSerializer @@ -46,7 +46,7 @@ class CostModelsFilter(FilterSet): def currency_filter(self, qs, name, values): """Filter currency if a valid currency is passed in""" - if values and values[0].upper() not in VALID_CURRENCIES: + if values and values[0].upper() not in get_all_currency_codes(): error = {"currency": f'"{values[0]}" is not a valid choice.'} raise serializers.ValidationError(error) lookup = "__".join([name, "iexact"]) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index e96a931166..f3e274e570 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -16,7 +16,6 @@ from urllib3.util.retry import Retry from api.common import log_json -from api.currency.currencies import VALID_CURRENCIES from api.currency.models import ExchangeRates from api.currency.utils import exchange_dictionary from api.iam.models import Tenant @@ -284,22 +283,22 @@ def _fetch_exchange_rates(url): rate_metrics = {} for curr_type, value in response.json()["rates"].items(): - if curr_type.upper() in VALID_CURRENCIES: - try: - exchange = ExchangeRates.objects.get(currency_type=curr_type.lower()) - LOG.info(f"Updating currency {curr_type} to {value}") - except ExchangeRates.DoesNotExist: - LOG.info(f"Creating the exchange rate {curr_type} to {value}") - exchange = ExchangeRates(currency_type=curr_type.lower()) - rate_metrics[curr_type] = value - exchange.exchange_rate = value - exchange.save() + try: + exchange = ExchangeRates.objects.get(currency_type=curr_type.lower()) + LOG.info(f"Updating currency {curr_type} to {value}") + except ExchangeRates.DoesNotExist: + LOG.info(f"Creating the exchange rate {curr_type} to {value}") + exchange = ExchangeRates(currency_type=curr_type.lower()) + rate_metrics[curr_type] = value + exchange.exchange_rate = value + exchange.save() exchange_dictionary(rate_metrics) return rate_metrics def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): """Sync EnabledCurrency and upsert dynamic MonthlyExchangeRate rows for one tenant.""" + from api.currency.currencies import lookup_currency_name from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType @@ -310,7 +309,14 @@ def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): new_currencies = set(exchange_dict.keys()) - existing_codes if new_currencies: EnabledCurrency.objects.bulk_create( - [EnabledCurrency(currency_code=code, enabled=False) for code in new_currencies], + [ + EnabledCurrency( + currency_code=code, + currency_name=lookup_currency_name(code), + enabled=False, + ) + for code in new_currencies + ], ignore_conflicts=True, ) From aa411e0e64f594691acc3e4ff6d416cd63f33b7b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 20 Apr 2026 01:33:54 +0300 Subject: [PATCH 026/106] Use babel for currency name + symbol; drop pycountry babel provides both get_currency_name() and get_currency_symbol() which pycountry does not. This restores the full response format (code, name, symbol, description) that the frontend expects from the GET /currency/ endpoint. - Add currency_symbol field to EnabledCurrency (migration 0016) - Replace pycountry with babel in currencies.py lookup helper - Update Celery task and static rate serializer to populate both currency_name and currency_symbol at discovery time - Update get_currency view to return code, name, symbol, description - Replace pycountry with Babel in Pipfile Made-with: Cursor --- Pipfile | 2 +- koku/api/currency/currencies.py | 27 ++++++++++++------- koku/api/currency/test/test_views.py | 18 ++++++++----- koku/api/currency/view.py | 23 +++++++++++----- koku/api/settings/currency_views.py | 2 +- .../0016_enabledcurrency_currency_name.py | 5 ++++ koku/cost_models/models.py | 1 + .../static_exchange_rate_serializer.py | 5 ++-- koku/masu/celery/tasks.py | 17 ++++++------ 9 files changed, 66 insertions(+), 34 deletions(-) diff --git a/Pipfile b/Pipfile index fb1402b144..6b2626d1eb 100644 --- a/Pipfile +++ b/Pipfile @@ -46,7 +46,7 @@ pydantic = ">=2" sqlparse = "*" packaging = "*" psycopg2-binary = "*" -pycountry = "*" +Babel = "*" watchtower = "*" unleashclient = "*" kombu = "*" diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index 6c929b11e6..0202c9b2e2 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -8,10 +8,12 @@ daily Celery task and managed via the EnabledCurrency table (tenant schema). Administrators enable currencies through the Settings UI. -Display metadata (currency_name) is resolved via pycountry at discovery -time and stored on the EnabledCurrency row. +Display metadata (name, symbol) is resolved via babel at discovery time +and stored on the EnabledCurrency row. """ -import pycountry +from babel.numbers import get_currency_name +from babel.numbers import get_currency_symbol +from babel.numbers import UnknownCurrencyError from cost_models.models import EnabledCurrency @@ -33,11 +35,18 @@ def get_all_currency_codes(): return set(EnabledCurrency.objects.values_list("currency_code", flat=True)) -def lookup_currency_name(code): - """Resolve a currency code to its display name via pycountry. +def lookup_currency_metadata(code): + """Resolve a currency code to its display name and symbol via babel. - Returns the code itself when pycountry has no entry (e.g. crypto or - non-standard codes returned by some exchange rate APIs). + Returns (name, symbol) tuple. Falls back to the code itself for + currencies babel does not recognise (e.g. crypto or non-standard + codes returned by some exchange rate APIs). """ - currency = pycountry.currencies.get(alpha_3=code.upper()) - return currency.name if currency else code.upper() + code = code.upper() + try: + name = get_currency_name(code, locale="en_US") + symbol = get_currency_symbol(code, locale="en_US") + except UnknownCurrencyError: + name = code + symbol = code + return name, symbol diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index c96d2046e5..d7f7321514 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -16,12 +16,18 @@ class CurrencyViewTest(IamTestCase): def setUp(self): super().setUp() EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="USD", currency_name="US Dollar", enabled=True) - EnabledCurrency.objects.create(currency_code="EUR", currency_name="Euro", enabled=True) - EnabledCurrency.objects.create(currency_code="GBP", currency_name="Pound Sterling", enabled=False) + EnabledCurrency.objects.create( + currency_code="USD", currency_name="US Dollar", currency_symbol="$", enabled=True + ) + EnabledCurrency.objects.create( + currency_code="EUR", currency_name="Euro", currency_symbol="€", enabled=True + ) + EnabledCurrency.objects.create( + currency_code="GBP", currency_name="Pound Sterling", currency_symbol="£", enabled=False + ) def test_supported_currencies(self): - """Test that GET returns only enabled currencies from EnabledCurrency.""" + """Test that GET returns only enabled currencies with name, symbol, description.""" qs = "?limit=25" url = reverse("currency") + qs client = APIClient() @@ -31,8 +37,8 @@ def test_supported_currencies(self): data = response.data expected = [ - {"code": "EUR", "name": "Euro"}, - {"code": "USD", "name": "US Dollar"}, + {"code": "EUR", "name": "Euro", "symbol": "€", "description": "EUR (€) - Euro"}, + {"code": "USD", "name": "US Dollar", "symbol": "$", "description": "USD ($) - US Dollar"}, ] self.assertEqual(data.get("data"), expected) diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index 943699b42c..fd7a981d71 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -23,14 +23,23 @@ def get_currency(request): """Get available currencies. Returns currencies that have been enabled by an administrator via - the EnabledCurrency table. Display name is resolved via pycountry - at discovery time and stored on the row. + the EnabledCurrency table. Display metadata (name, symbol) is + resolved via babel at discovery time and stored on the row. """ - currencies = EnabledCurrency.objects.filter(enabled=True).values("currency_code", "currency_name") - available = [ - {"code": c["currency_code"], "name": c["currency_name"] or c["currency_code"]} - for c in currencies.order_by("currency_code") - ] + currencies = EnabledCurrency.objects.filter(enabled=True).values( + "currency_code", "currency_name", "currency_symbol" + ) + available = [] + for c in currencies.order_by("currency_code"): + code = c["currency_code"] + name = c["currency_name"] or code + symbol = c["currency_symbol"] or code + available.append({ + "code": code, + "name": name, + "symbol": symbol, + "description": f"{code} ({symbol}) - {name}", + }) return ListPaginator(available, request).paginated_response diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 0fb135641f..aa19aa3f13 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -36,7 +36,7 @@ class EnabledCurrencyView(APIView): @method_decorator(never_cache) def get(self, request, *args, **kwargs): - currencies = EnabledCurrency.objects.all().values("currency_code", "currency_name", "enabled") + currencies = EnabledCurrency.objects.all().values("currency_code", "currency_name", "currency_symbol", "enabled") data = list(currencies) paginator = ListPaginator(data, request) return paginator.get_paginated_response(data) diff --git a/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py b/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py index ffa2041946..0b88a2c85e 100644 --- a/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py +++ b/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py @@ -18,4 +18,9 @@ class Migration(migrations.Migration): name="currency_name", field=models.CharField(blank=True, default="", max_length=100), ), + migrations.AddField( + model_name="enabledcurrency", + name="currency_symbol", + field=models.CharField(blank=True, default="", max_length=10), + ), ] diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index 7c6d70f9cc..f12b05cb08 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -236,6 +236,7 @@ class Meta: currency_code = models.CharField(max_length=5, unique=True) currency_name = models.CharField(max_length=100, blank=True, default="") + currency_symbol = models.CharField(max_length=10, blank=True, default="") enabled = models.BooleanField(default=False) created_timestamp = models.DateTimeField(auto_now_add=True) updated_timestamp = models.DateTimeField(auto_now=True) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index c33e6b3ecc..944b5d0f7e 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -12,7 +12,7 @@ from api.common import log_json from api.currency.currencies import get_all_currency_codes -from api.currency.currencies import lookup_currency_name +from api.currency.currencies import lookup_currency_metadata from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate @@ -35,9 +35,10 @@ def _iter_months(start_date, end_date): def _ensure_currencies_enabled(*currency_codes): """Ensure EnabledCurrency rows exist and are enabled for the given currency codes.""" for code in currency_codes: + name, symbol = lookup_currency_metadata(code) EnabledCurrency.objects.update_or_create( currency_code=code, - defaults={"enabled": True, "currency_name": lookup_currency_name(code)}, + defaults={"enabled": True, "currency_name": name, "currency_symbol": symbol}, ) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index f3e274e570..894fef1bfe 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -298,7 +298,7 @@ def _fetch_exchange_rates(url): def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): """Sync EnabledCurrency and upsert dynamic MonthlyExchangeRate rows for one tenant.""" - from api.currency.currencies import lookup_currency_name + from api.currency.currencies import lookup_currency_metadata from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType @@ -308,17 +308,18 @@ def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): existing_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) new_currencies = set(exchange_dict.keys()) - existing_codes if new_currencies: - EnabledCurrency.objects.bulk_create( - [ + rows = [] + for code in new_currencies: + name, symbol = lookup_currency_metadata(code) + rows.append( EnabledCurrency( currency_code=code, - currency_name=lookup_currency_name(code), + currency_name=name, + currency_symbol=symbol, enabled=False, ) - for code in new_currencies - ], - ignore_conflicts=True, - ) + ) + EnabledCurrency.objects.bulk_create(rows, ignore_conflicts=True) static_pairs = set( MonthlyExchangeRate.objects.filter( From 316a3dd4ef9442bcd707233b20b5ce149e448d60 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 20 Apr 2026 01:35:36 +0300 Subject: [PATCH 027/106] Store only currency_name; compute symbol and description at response time Only currency_name is persisted on EnabledCurrency (via babel lookup_currency_name at discovery time). Symbol and description are computed at response time using babel's get_currency_symbol, keeping the DB lean while preserving the full response format the frontend expects (code, name, symbol, description). Made-with: Cursor --- koku/api/currency/currencies.py | 31 +++++++++++-------- koku/api/currency/test/test_views.py | 19 +++++------- koku/api/currency/view.py | 11 +++---- koku/api/settings/currency_views.py | 2 +- .../0016_enabledcurrency_currency_name.py | 5 --- koku/cost_models/models.py | 1 - .../static_exchange_rate_serializer.py | 5 ++- koku/masu/celery/tasks.py | 17 +++++----- 8 files changed, 42 insertions(+), 49 deletions(-) diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index 0202c9b2e2..9ab1a2d9c7 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -8,8 +8,8 @@ daily Celery task and managed via the EnabledCurrency table (tenant schema). Administrators enable currencies through the Settings UI. -Display metadata (name, symbol) is resolved via babel at discovery time -and stored on the EnabledCurrency row. +Display name is resolved via babel at discovery time and stored on the +EnabledCurrency row. Symbol and description are computed at response time. """ from babel.numbers import get_currency_name from babel.numbers import get_currency_symbol @@ -35,18 +35,23 @@ def get_all_currency_codes(): return set(EnabledCurrency.objects.values_list("currency_code", flat=True)) -def lookup_currency_metadata(code): - """Resolve a currency code to its display name and symbol via babel. +def lookup_currency_name(code): + """Resolve a currency code to its display name via babel. - Returns (name, symbol) tuple. Falls back to the code itself for - currencies babel does not recognise (e.g. crypto or non-standard - codes returned by some exchange rate APIs). + Returns the code itself for currencies babel does not recognise. """ - code = code.upper() try: - name = get_currency_name(code, locale="en_US") - symbol = get_currency_symbol(code, locale="en_US") + return get_currency_name(code.upper(), locale="en_US") except UnknownCurrencyError: - name = code - symbol = code - return name, symbol + return code.upper() + + +def resolve_currency_symbol(code): + """Resolve a currency code to its symbol via babel at response time. + + Returns the code itself for currencies babel does not recognise. + """ + try: + return get_currency_symbol(code.upper(), locale="en_US") + except UnknownCurrencyError: + return code.upper() diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index d7f7321514..4d2d5ece33 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -2,6 +2,8 @@ # Copyright 2021 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # +from unittest.mock import patch + from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient @@ -16,17 +18,12 @@ class CurrencyViewTest(IamTestCase): def setUp(self): super().setUp() EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create( - currency_code="USD", currency_name="US Dollar", currency_symbol="$", enabled=True - ) - EnabledCurrency.objects.create( - currency_code="EUR", currency_name="Euro", currency_symbol="€", enabled=True - ) - EnabledCurrency.objects.create( - currency_code="GBP", currency_name="Pound Sterling", currency_symbol="£", enabled=False - ) - - def test_supported_currencies(self): + EnabledCurrency.objects.create(currency_code="USD", currency_name="US Dollar", enabled=True) + EnabledCurrency.objects.create(currency_code="EUR", currency_name="Euro", enabled=True) + EnabledCurrency.objects.create(currency_code="GBP", currency_name="Pound Sterling", enabled=False) + + @patch("api.currency.view.resolve_currency_symbol", side_effect=lambda c: {"USD": "$", "EUR": "€"}.get(c, c)) + def test_supported_currencies(self, _mock_symbol): """Test that GET returns only enabled currencies with name, symbol, description.""" qs = "?limit=25" url = reverse("currency") + qs diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index fd7a981d71..c66cf048a4 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -12,6 +12,7 @@ from rest_framework.settings import api_settings from api.common.pagination import ListPaginator +from api.currency.currencies import resolve_currency_symbol from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency @@ -23,17 +24,15 @@ def get_currency(request): """Get available currencies. Returns currencies that have been enabled by an administrator via - the EnabledCurrency table. Display metadata (name, symbol) is - resolved via babel at discovery time and stored on the row. + the EnabledCurrency table. Name is stored on the row; symbol and + description are computed at response time via babel. """ - currencies = EnabledCurrency.objects.filter(enabled=True).values( - "currency_code", "currency_name", "currency_symbol" - ) + currencies = EnabledCurrency.objects.filter(enabled=True).values("currency_code", "currency_name") available = [] for c in currencies.order_by("currency_code"): code = c["currency_code"] name = c["currency_name"] or code - symbol = c["currency_symbol"] or code + symbol = resolve_currency_symbol(code) available.append({ "code": code, "name": name, diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index aa19aa3f13..0fb135641f 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -36,7 +36,7 @@ class EnabledCurrencyView(APIView): @method_decorator(never_cache) def get(self, request, *args, **kwargs): - currencies = EnabledCurrency.objects.all().values("currency_code", "currency_name", "currency_symbol", "enabled") + currencies = EnabledCurrency.objects.all().values("currency_code", "currency_name", "enabled") data = list(currencies) paginator = ListPaginator(data, request) return paginator.get_paginated_response(data) diff --git a/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py b/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py index 0b88a2c85e..ffa2041946 100644 --- a/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py +++ b/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py @@ -18,9 +18,4 @@ class Migration(migrations.Migration): name="currency_name", field=models.CharField(blank=True, default="", max_length=100), ), - migrations.AddField( - model_name="enabledcurrency", - name="currency_symbol", - field=models.CharField(blank=True, default="", max_length=10), - ), ] diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index f12b05cb08..7c6d70f9cc 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -236,7 +236,6 @@ class Meta: currency_code = models.CharField(max_length=5, unique=True) currency_name = models.CharField(max_length=100, blank=True, default="") - currency_symbol = models.CharField(max_length=10, blank=True, default="") enabled = models.BooleanField(default=False) created_timestamp = models.DateTimeField(auto_now_add=True) updated_timestamp = models.DateTimeField(auto_now=True) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 944b5d0f7e..c33e6b3ecc 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -12,7 +12,7 @@ from api.common import log_json from api.currency.currencies import get_all_currency_codes -from api.currency.currencies import lookup_currency_metadata +from api.currency.currencies import lookup_currency_name from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate @@ -35,10 +35,9 @@ def _iter_months(start_date, end_date): def _ensure_currencies_enabled(*currency_codes): """Ensure EnabledCurrency rows exist and are enabled for the given currency codes.""" for code in currency_codes: - name, symbol = lookup_currency_metadata(code) EnabledCurrency.objects.update_or_create( currency_code=code, - defaults={"enabled": True, "currency_name": name, "currency_symbol": symbol}, + defaults={"enabled": True, "currency_name": lookup_currency_name(code)}, ) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index 894fef1bfe..f3e274e570 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -298,7 +298,7 @@ def _fetch_exchange_rates(url): def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): """Sync EnabledCurrency and upsert dynamic MonthlyExchangeRate rows for one tenant.""" - from api.currency.currencies import lookup_currency_metadata + from api.currency.currencies import lookup_currency_name from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType @@ -308,18 +308,17 @@ def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): existing_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) new_currencies = set(exchange_dict.keys()) - existing_codes if new_currencies: - rows = [] - for code in new_currencies: - name, symbol = lookup_currency_metadata(code) - rows.append( + EnabledCurrency.objects.bulk_create( + [ EnabledCurrency( currency_code=code, - currency_name=name, - currency_symbol=symbol, + currency_name=lookup_currency_name(code), enabled=False, ) - ) - EnabledCurrency.objects.bulk_create(rows, ignore_conflicts=True) + for code in new_currencies + ], + ignore_conflicts=True, + ) static_pairs = set( MonthlyExchangeRate.objects.filter( From e70da891aa4ef5b8503591b28d8e3e7b71bca0af Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 20 Apr 2026 02:16:23 +0300 Subject: [PATCH 028/106] Store only currency_code in EnabledCurrency; compute all display metadata via babel Remove currency_name from EnabledCurrency model and its migration. Name, symbol, and description are now fully resolved at response time by get_currency_info() using babel, keeping the DB lean. Made-with: Cursor --- koku/api/currency/currencies.py | 34 +++++++++---------- koku/api/currency/test/test_views.py | 18 ++++++---- koku/api/currency/view.py | 20 +++-------- koku/api/settings/currency_views.py | 2 +- .../0016_enabledcurrency_currency_name.py | 21 ------------ koku/cost_models/models.py | 1 - .../static_exchange_rate_serializer.py | 3 +- koku/masu/celery/tasks.py | 2 -- 8 files changed, 35 insertions(+), 66 deletions(-) delete mode 100644 koku/cost_models/migrations/0016_enabledcurrency_currency_name.py diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index 9ab1a2d9c7..f34b075bce 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -8,8 +8,7 @@ daily Celery task and managed via the EnabledCurrency table (tenant schema). Administrators enable currencies through the Settings UI. -Display name is resolved via babel at discovery time and stored on the -EnabledCurrency row. Symbol and description are computed at response time. +Name, symbol, and description are computed at response time via babel. """ from babel.numbers import get_currency_name from babel.numbers import get_currency_symbol @@ -35,23 +34,22 @@ def get_all_currency_codes(): return set(EnabledCurrency.objects.values_list("currency_code", flat=True)) -def lookup_currency_name(code): - """Resolve a currency code to its display name via babel. +def get_currency_info(code): + """Return a dict with code, name, symbol, and description for a currency. - Returns the code itself for currencies babel does not recognise. + All metadata is resolved via babel at call time. Falls back to the + code itself for currencies babel does not recognise. """ + code = code.upper() try: - return get_currency_name(code.upper(), locale="en_US") + name = get_currency_name(code, locale="en_US") + symbol = get_currency_symbol(code, locale="en_US") except UnknownCurrencyError: - return code.upper() - - -def resolve_currency_symbol(code): - """Resolve a currency code to its symbol via babel at response time. - - Returns the code itself for currencies babel does not recognise. - """ - try: - return get_currency_symbol(code.upper(), locale="en_US") - except UnknownCurrencyError: - return code.upper() + name = code + symbol = code + return { + "code": code, + "name": name, + "symbol": symbol, + "description": f"{code} ({symbol}) - {name}", + } diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index 4d2d5ece33..4822dbad56 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -18,12 +18,18 @@ class CurrencyViewTest(IamTestCase): def setUp(self): super().setUp() EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="USD", currency_name="US Dollar", enabled=True) - EnabledCurrency.objects.create(currency_code="EUR", currency_name="Euro", enabled=True) - EnabledCurrency.objects.create(currency_code="GBP", currency_name="Pound Sterling", enabled=False) - - @patch("api.currency.view.resolve_currency_symbol", side_effect=lambda c: {"USD": "$", "EUR": "€"}.get(c, c)) - def test_supported_currencies(self, _mock_symbol): + EnabledCurrency.objects.create(currency_code="USD", enabled=True) + EnabledCurrency.objects.create(currency_code="EUR", enabled=True) + EnabledCurrency.objects.create(currency_code="GBP", enabled=False) + + @patch( + "api.currency.view.get_currency_info", + side_effect=lambda c: { + "USD": {"code": "USD", "name": "US Dollar", "symbol": "$", "description": "USD ($) - US Dollar"}, + "EUR": {"code": "EUR", "name": "Euro", "symbol": "€", "description": "EUR (€) - Euro"}, + }[c], + ) + def test_supported_currencies(self, _mock_display): """Test that GET returns only enabled currencies with name, symbol, description.""" qs = "?limit=25" url = reverse("currency") + qs diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index c66cf048a4..d73acc648a 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -12,7 +12,7 @@ from rest_framework.settings import api_settings from api.common.pagination import ListPaginator -from api.currency.currencies import resolve_currency_symbol +from api.currency.currencies import get_currency_info from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency @@ -24,21 +24,11 @@ def get_currency(request): """Get available currencies. Returns currencies that have been enabled by an administrator via - the EnabledCurrency table. Name is stored on the row; symbol and - description are computed at response time via babel. + the EnabledCurrency table. Name, symbol, and description are + computed at response time via babel. """ - currencies = EnabledCurrency.objects.filter(enabled=True).values("currency_code", "currency_name") - available = [] - for c in currencies.order_by("currency_code"): - code = c["currency_code"] - name = c["currency_name"] or code - symbol = resolve_currency_symbol(code) - available.append({ - "code": code, - "name": name, - "symbol": symbol, - "description": f"{code} ({symbol}) - {name}", - }) + enabled_codes = EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True) + available = [get_currency_info(code) for code in sorted(enabled_codes)] return ListPaginator(available, request).paginated_response diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 0fb135641f..73d1a15637 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -36,7 +36,7 @@ class EnabledCurrencyView(APIView): @method_decorator(never_cache) def get(self, request, *args, **kwargs): - currencies = EnabledCurrency.objects.all().values("currency_code", "currency_name", "enabled") + currencies = EnabledCurrency.objects.all().values("currency_code", "enabled") data = list(currencies) paginator = ListPaginator(data, request) return paginator.get_paginated_response(data) diff --git a/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py b/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py deleted file mode 100644 index ffa2041946..0000000000 --- a/koku/cost_models/migrations/0016_enabledcurrency_currency_name.py +++ /dev/null @@ -1,21 +0,0 @@ -# -# Copyright 2026 Red Hat Inc. -# SPDX-License-Identifier: Apache-2.0 -# -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [ - ("cost_models", "0015_enabledcurrency"), - ] - - operations = [ - migrations.AddField( - model_name="enabledcurrency", - name="currency_name", - field=models.CharField(blank=True, default="", max_length=100), - ), - ] diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index 7c6d70f9cc..e2f0f80fc6 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -235,7 +235,6 @@ class Meta: ordering = ["currency_code"] currency_code = models.CharField(max_length=5, unique=True) - currency_name = models.CharField(max_length=100, blank=True, default="") enabled = models.BooleanField(default=False) created_timestamp = models.DateTimeField(auto_now_add=True) updated_timestamp = models.DateTimeField(auto_now=True) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index c33e6b3ecc..6dab0178dd 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -12,7 +12,6 @@ from api.common import log_json from api.currency.currencies import get_all_currency_codes -from api.currency.currencies import lookup_currency_name from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate @@ -37,7 +36,7 @@ def _ensure_currencies_enabled(*currency_codes): for code in currency_codes: EnabledCurrency.objects.update_or_create( currency_code=code, - defaults={"enabled": True, "currency_name": lookup_currency_name(code)}, + defaults={"enabled": True}, ) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index f3e274e570..6da0527504 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -298,7 +298,6 @@ def _fetch_exchange_rates(url): def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): """Sync EnabledCurrency and upsert dynamic MonthlyExchangeRate rows for one tenant.""" - from api.currency.currencies import lookup_currency_name from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType @@ -312,7 +311,6 @@ def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): [ EnabledCurrency( currency_code=code, - currency_name=lookup_currency_name(code), enabled=False, ) for code in new_currencies From 8880e58162dd343b17f09b7eb0ef9f81a47daf0c Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 20 Apr 2026 13:55:19 +0300 Subject: [PATCH 029/106] Rename EnabledCurrency to CurrencyConfig The model stores all currencies (enabled and disabled) as per-tenant configuration, so CurrencyConfig better reflects its purpose. Includes a migration to rename both the Django model and the DB table. Made-with: Cursor --- docs/architecture/constant-currency/README.md | 8 +++--- .../constant-currency/api-and-frontend.md | 10 +++---- .../constant-currency/data-model.md | 20 +++++++------- .../constant-currency/phased-delivery.md | 16 ++++++------ .../constant-currency/pipeline-changes.md | 20 +++++++------- .../constant-currency/risk-register.md | 2 +- koku/api/currency/currencies.py | 10 +++---- koku/api/currency/test/test_views.py | 10 +++---- koku/api/currency/view.py | 6 ++--- koku/api/settings/currency_views.py | 16 ++++++------ koku/api/urls.py | 6 ++--- ...ename_enabledcurrency_to_currencyconfig.py | 23 ++++++++++++++++ koku/cost_models/models.py | 6 ++--- .../static_exchange_rate_serializer.py | 6 ++--- .../cost_models/test/test_enabled_currency.py | 26 +++++++++---------- .../test/test_monthly_exchange_rate.py | 22 ++++++++-------- koku/masu/celery/tasks.py | 10 +++---- 17 files changed, 120 insertions(+), 97 deletions(-) create mode 100644 koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py diff --git a/docs/architecture/constant-currency/README.md b/docs/architecture/constant-currency/README.md index 0ea28d5dd6..bdddc91f1b 100644 --- a/docs/architecture/constant-currency/README.md +++ b/docs/architecture/constant-currency/README.md @@ -128,7 +128,7 @@ immediately available for use, or should an administrator explicitly enable them **Resolution**: Explicit enablement. Currencies fetched from the dynamic exchange rate API arrive in Cost Management as **disabled** by default (stored in the -`EnabledCurrency` table with `enabled=False`). An administrator must explicitly +`CurrencyConfig` table with `enabled=False`). An administrator must explicitly enable currencies through the Settings UI before they appear in the target currency dropdown. @@ -143,7 +143,7 @@ need a small subset of the ~170 currencies available from the API. Showing all currencies by default would clutter the dropdown. **Exception**: Static exchange rate pairs always make their currencies available -in the dropdown, regardless of `EnabledCurrency` status. If an administrator +in the dropdown, regardless of `CurrencyConfig` status. If an administrator defines a `USD→EUR` static rate, both `USD` and `EUR` are immediately available. ### IQ-6: Rate resolution without `CURRENCY_URL` — RESOLVED @@ -254,7 +254,7 @@ graph LR API["open.er-api.com
(or custom URL)"] -->|"daily fetch
(skipped if no URL)"| CT["Celery Task:
get_daily_currency_rates"] CT -->|upsert| ER["ExchangeRates
(public schema)"] CT -->|rebuild| ERD["ExchangeRateDictionary
(public schema)"] - CT -->|"discover currencies
create as disabled"| EC["EnabledCurrency
(tenant schema)
enabled/disabled per currency"] + CT -->|"discover currencies
create as disabled"| EC["CurrencyConfig
(tenant schema)
enabled/disabled per currency"] CT -->|"Writer 1: per-tenant
skip static pairs
all currencies"| MER["MonthlyExchangeRate
(tenant schema)
single source of truth"] MER -->|"all months:
per-month rates"| QH["QueryHandler
Subquery annotation"] QH -->|"per-month rates +
rate metadata"| REPORT["Report Response
+ exchange_rates_applied"] @@ -296,7 +296,7 @@ graph LR | 11 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. | | 12 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback, error if neither). Documentation references `open.er-api.com` (free tier) as the production example | | 13 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection | -| 14 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `EnabledCurrency` status | +| 14 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `CurrencyConfig` status | --- diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 7b067f41e2..d872712382 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -150,7 +150,7 @@ allows an administrator to enable or disable currencies. ### View **File**: Extend existing settings views in `koku/api/settings/` or add a new -`EnabledCurrencyViewSet`. +`CurrencyConfigViewSet`. **Permission**: Cost Management Administrator role (same permission level as other Settings operations). @@ -194,7 +194,7 @@ tables — all currencies are always stored regardless of their enabled status. ### No `CURRENCY_URL` Configured When no `CURRENCY_URL` is configured, no dynamic currencies are discovered by the -Celery task, so the `EnabledCurrency` table will have no dynamically-discovered +Celery task, so the `CurrencyConfig` table will have no dynamically-discovered rows. The GET response will return either an empty list or only currencies that were manually added. Previously fetched dynamic currencies (if the URL was removed later) remain in the table. @@ -210,8 +210,8 @@ currencies from two sources: | Source | Rule | Example | |--------|------|---------| -| **Dynamic** | Currency has `enabled=True` in `EnabledCurrency` | USD, EUR enabled → appear in dropdown | -| **Static** | Currency appears in any `StaticExchangeRate` pair (as base or target) | Static rate EUR→CHF defined → both EUR and CHF appear in dropdown regardless of `EnabledCurrency` status | +| **Dynamic** | Currency has `enabled=True` in `CurrencyConfig` | USD, EUR enabled → appear in dropdown | +| **Static** | Currency appears in any `StaticExchangeRate` pair (as base or target) | Static rate EUR→CHF defined → both EUR and CHF appear in dropdown regardless of `CurrencyConfig` status | ### Dropdown Endpoint @@ -242,7 +242,7 @@ static rates, or both. This is informational for the frontend. When **no currencies are available at all** — meaning: -- All dynamic currencies are disabled in `EnabledCurrency` (or none exist), **and** +- All dynamic currencies are disabled in `CurrencyConfig` (or none exist), **and** - No `StaticExchangeRate` rows exist (no static rates) Then the currency dropdown should either be **hidden** or show a message: diff --git a/docs/architecture/constant-currency/data-model.md b/docs/architecture/constant-currency/data-model.md index ad91758b11..fa4025eb81 100644 --- a/docs/architecture/constant-currency/data-model.md +++ b/docs/architecture/constant-currency/data-model.md @@ -113,7 +113,7 @@ defined, each uses its own rate. (see [api-and-frontend.md](./api-and-frontend.md)) and has side effects on `MonthlyExchangeRate` via the serializer. -### `EnabledCurrency` +### `CurrencyConfig` Tracks which currencies are visible in the target currency dropdown. Currencies must be explicitly enabled by an administrator before they appear in the @@ -121,18 +121,18 @@ dropdown. All currencies are always stored in `MonthlyExchangeRate` regardless of their enabled status — the `enabled` flag only controls dropdown visibility. ```python -class EnabledCurrency(models.Model): +class CurrencyConfig(models.Model): currency_code = models.CharField(max_length=5, unique=True) enabled = models.BooleanField(default=False) created_timestamp = models.DateTimeField(auto_now_add=True) updated_timestamp = models.DateTimeField(auto_now=True) class Meta: - db_table = "enabled_currency" + db_table = "currency_config" ordering = ["currency_code"] ``` -**Example `enabled_currency` rows**: +**Example `currency_config` rows**: | id | currency_code | enabled | created_timestamp | updated_timestamp | |----|---------------|---------|-------------------|-------------------| @@ -152,7 +152,7 @@ dropdown. | Event | Action | |-------|--------| -| Daily Celery task fetches from exchange rate API | Creates `EnabledCurrency` rows with `enabled=False` for any newly discovered currencies not already in the table | +| Daily Celery task fetches from exchange rate API | Creates `CurrencyConfig` rows with `enabled=False` for any newly discovered currencies not already in the table | | Administrator enables a currency in Settings | Sets `enabled=True` | | Administrator disables a currency in Settings | Sets `enabled=False` | @@ -161,9 +161,9 @@ dropdown. A currency is visible in the target currency dropdown if **any** of the following are true: -1. It has `enabled=True` in `EnabledCurrency` +1. It has `enabled=True` in `CurrencyConfig` 2. It appears in any `StaticExchangeRate` pair (static rates make their currencies - visible regardless of the `EnabledCurrency` status) + visible regardless of the `CurrencyConfig` status) **Corner case — no usable rate**: A currency may be available in the dropdown but have no exchange rate path from the bill's source currency. In this case, the API @@ -337,7 +337,7 @@ If `ExchangeRateDictionary` is empty (e.g., fresh deployment with no rates to seed, and the table starts empty. The daily Celery task and static rate CRUD will populate it going forward. -### M3: Create `enabled_currency` Table +### M3: Create `currency_config` Table | Field | Value | |-------|-------| @@ -375,12 +375,12 @@ changes required. | Version | Date | Summary | |---------|------|---------| | v1.0 | 2026-03-19 | Initial data model design | -| v1.1 | 2026-03-24 | Added `EnabledCurrency` model, M4 migration | +| v1.1 | 2026-03-24 | Added `CurrencyConfig` model, M4 migration | | v1.2 | 2026-03-24 | Simplified enablement: `enabled` flag only controls dropdown visibility, not snapshotting | | v1.3 | 2026-03-24 | Removed airgapped mode concept. Rate resolution: static first, dynamic fallback, error if neither. | | v1.4 | 2026-03-26 | Clarified `StaticExchangeRateDictionary` as source of truth for static rates; `MonthlyExchangeRateSnapshot` as historical rate storage for reports. | | v1.5 | 2026-03-29 | Replaced `year_month` CharField with `effective_date` DateField on `MonthlyExchangeRateSnapshot` for consistency with existing date field patterns (`usage_start`, `billing_period_start`). | -| v1.6 | 2026-03-30 | Renamed `MonthlyExchangeRateSnapshot` → `MonthlyExchangeRate` and promoted it to single source of truth for all months (current and past). Removed `StaticExchangeRateDictionary` — no longer needed since query handlers read from `MonthlyExchangeRate` for all months. Renumbered migrations (M3 is now `enabled_currency`; old M3 removed). | +| v1.6 | 2026-03-30 | Renamed `MonthlyExchangeRateSnapshot` → `MonthlyExchangeRate` and promoted it to single source of truth for all months (current and past). Removed `StaticExchangeRateDictionary` — no longer needed since query handlers read from `MonthlyExchangeRate` for all months. Renumbered migrations (M3 is now `currency_config`; old M3 removed). | | v1.7 | 2026-03-30 | M2 now seeds current-month data from `ExchangeRateDictionary` during migration. Eliminates `ExchangeRateDictionary` fallback in query handler. | | v1.8 | 2026-04-12 | Updated reader description to reflect `Subquery`-based rate resolution (replaces `Case`/`When`). | | v1.9 | 2026-04-13 | Fixed `ExchangeRates` model description: actual fields are `currency_type` (CharField) and `exchange_rate` (FloatField), not `base_currency`/`exchange_rates` JSONField. Removed non-existent `updated_timestamp` column from `ExchangeRateDictionary` example. | diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index b12ce835d2..6b9071116f 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -35,10 +35,10 @@ pairs. Show rate provenance in report responses. |----------|------|-------------| | `StaticExchangeRate` model | `koku/cost_models/models.py` | User-defined rate pairs with validity periods | | `MonthlyExchangeRate` model | `koku/cost_models/models.py` | Single source of truth: per-month, per-pair rate storage for all months | -| `EnabledCurrency` model | `koku/cost_models/models.py` | Tracks enabled/disabled currencies per tenant | +| `CurrencyConfig` model | `koku/cost_models/models.py` | Tracks enabled/disabled currencies per tenant | | Migration M1 | `koku/cost_models/migrations/XXXX_*.py` | Create `static_exchange_rate` table | | Migration M2 | `koku/cost_models/migrations/XXXX_*.py` | Create `monthly_exchange_rate` table + seed current-month data from `ExchangeRateDictionary` | -| Migration M3 | `koku/cost_models/migrations/XXXX_*.py` | Create `enabled_currency` table | +| Migration M3 | `koku/cost_models/migrations/XXXX_*.py` | Create `currency_config` table | | Serializer | `koku/cost_models/static_exchange_rate_serializer.py` | Validation + `MonthlyExchangeRate` upsert side-effects | | ViewSet | `koku/cost_models/static_exchange_rate_view.py` | CRUD API for static rates | | Currency enablement view | `koku/api/settings/` or new file | Settings API for enable/disable currencies | @@ -54,7 +54,7 @@ pairs. Show rate provenance in report responses. | Serializer tests | `koku/cost_models/test/test_static_exchange_rate_serializer.py` | Validation tests | | View tests | `koku/cost_models/test/test_static_exchange_rate_view.py` | CRUD tests | | MonthlyExchangeRate tests | `koku/cost_models/test/test_monthly_exchange_rate.py` | Rate creation, query, locking tests | -| Currency enablement tests | `koku/cost_models/test/test_enabled_currency.py` or `koku/api/settings/test/` | Enable/disable, discovery, available-currencies tests | +| Currency enablement tests | `koku/cost_models/test/test_enabled_currency.py` or `koku/api/settings/test/` | Enable/disable, discovery, currency-config tests | | No-rate error tests | `koku/api/report/test/` | Corner case: error when no conversion path exists | ### Validation @@ -75,16 +75,16 @@ pairs. Show rate provenance in report responses. - [ ] Consecutive months with same rate/type collapsed into one period string - [ ] Unit tests pass for serializer, view, MonthlyExchangeRate logic, query handler - [ ] On-prem mode: full functionality without Trino -- [ ] **Currency enablement**: Dynamic currencies arrive as disabled in `EnabledCurrency` +- [ ] **Currency enablement**: Dynamic currencies arrive as disabled in `CurrencyConfig` - [ ] **Currency enablement**: Administrator can enable/disable currencies via Settings API - [ ] **Currency enablement**: All currencies are stored in `MonthlyExchangeRate` regardless of enabled status; `enabled` flag only controls dropdown visibility - [ ] **Rate resolution**: Static rates take precedence over dynamic rates; error returned if neither exists for a given pair - [ ] **No `CURRENCY_URL`**: Celery task skips API fetch; system works with whatever rates are available - [ ] **Available currencies**: Dropdown shows only enabled dynamic currencies and static rate currencies (disabled currencies are stored but hidden) -- [ ] **Available currencies**: Static rate currencies appear regardless of `EnabledCurrency` status +- [ ] **Available currencies**: Static rate currencies appear regardless of `CurrencyConfig` status - [ ] **No-rate corner case**: Selecting a target currency with no conversion path returns HTTP 400 with actionable error - [ ] **No currencies available**: Dropdown hidden or shows "No exchange rates available" when no currencies are available -- [ ] **Currency discovery**: New currencies from API are created as disabled `EnabledCurrency` rows +- [ ] **Currency discovery**: New currencies from API are created as disabled `CurrencyConfig` rows ### Rollback @@ -100,7 +100,7 @@ pairs. Show rate provenance in report responses. available-currencies endpoints) 7. Drop tables via reverse migration (`migrate_schemas` runs `DeleteModel` for all three new tables: `static_exchange_rate`, `monthly_exchange_rate`, - `enabled_currency`) + `currency_config`) 8. Remove new files: serializer, view, currency enablement views, test files 9. Revert OpenAPI changes @@ -172,7 +172,7 @@ design would be needed to handle path prioritization. | Version | Date | Summary | |---------|------|---------| | v1.0 | 2026-03-19 | Initial phased delivery plan | -| v1.1 | 2026-03-24 | Added EnabledCurrency artifacts (M4, views, tests), currency enablement and airgapped validation items, R7/R8 risks, updated rollback steps | +| v1.1 | 2026-03-24 | Added CurrencyConfig artifacts (M4, views, tests), currency enablement and airgapped validation items, R7/R8 risks, updated rollback steps | | v1.2 | 2026-03-24 | Simplified enablement: `enabled` flag only controls dropdown visibility. All currencies always stored and snapshotted. | | v1.3 | 2026-03-24 | Removed airgapped mode concept. Rate resolution: static first, dynamic fallback, error if neither. | | v1.4 | 2026-03-26 | Updated artifacts and validation to reflect two-tier rate resolution (dictionaries + snapshots). | diff --git a/docs/architecture/constant-currency/pipeline-changes.md b/docs/architecture/constant-currency/pipeline-changes.md index cea100b5c1..bff4039c2e 100644 --- a/docs/architecture/constant-currency/pipeline-changes.md +++ b/docs/architecture/constant-currency/pipeline-changes.md @@ -83,7 +83,7 @@ configuring their own on-premise deployment. 3. Fetches rates from `CURRENCY_URL` *(unchanged when URL is set)* 4. Upserts `ExchangeRates` rows *(unchanged)* 5. Rebuilds `ExchangeRateDictionary` *(unchanged)* -6. **NEW**: Per-tenant currency discovery — creates `EnabledCurrency` rows +6. **NEW**: Per-tenant currency discovery — creates `CurrencyConfig` rows with `enabled=False` for newly discovered currencies 7. **NEW**: Per-tenant upsert into `MonthlyExchangeRate` for all currencies returned by the API @@ -164,11 +164,11 @@ all_api_currencies = set(exchange_dict.keys()) for tenant in Tenant.objects.exclude(schema_name="public"): with schema_context(tenant.schema_name): - # Currency discovery: create EnabledCurrency rows for newly seen currencies - existing_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + # Currency discovery: create CurrencyConfig rows for newly seen currencies + existing_codes = set(CurrencyConfig.objects.values_list("currency_code", flat=True)) new_currencies = all_api_currencies - existing_codes - EnabledCurrency.objects.bulk_create( - [EnabledCurrency(currency_code=code, enabled=False) for code in new_currencies], + CurrencyConfig.objects.bulk_create( + [CurrencyConfig(currency_code=code, enabled=False) for code in new_currencies], ignore_conflicts=True, ) @@ -199,11 +199,11 @@ for tenant in Tenant.objects.exclude(schema_name="public"): - **URL check**: If `CURRENCY_URL` is not configured, the task skips the API fetch. Dynamic rates are simply not fetched; the system uses whatever rates are available (static first, dynamic fallback, error if neither exists). -- **Currency discovery**: Creates `EnabledCurrency` rows with `enabled=False` +- **Currency discovery**: Creates `CurrencyConfig` rows with `enabled=False` for any new currencies returned by the API. These appear in Settings as disabled currencies that the administrator can enable. - **All currencies stored**: Upserts dynamic rates for all currency pairs - returned by the API. The `enabled` flag on `EnabledCurrency` only controls + returned by the API. The `enabled` flag on `CurrencyConfig` only controls dropdown visibility, not rate storage. - Runs daily; overwrites current month's dynamic rows with latest rate - Skips pairs with `rate_type=RateType.STATIC` (static takes precedence) @@ -513,10 +513,10 @@ visible in the target currency dropdown. All currencies are stored in controls dropdown visibility. A currency is **visible in the dropdown** if any of the following are true: -1. It has `enabled=True` in `EnabledCurrency` +1. It has `enabled=True` in `CurrencyConfig` 2. It appears as either `base_currency` or `target_currency` in any `StaticExchangeRate` row (static rates make their currencies visible - regardless of `EnabledCurrency` status) + regardless of `CurrencyConfig` status) ```python @cached_property @@ -524,7 +524,7 @@ def available_currencies(self): """Currencies visible in the target currency dropdown.""" # Dynamic: enabled currencies (all are stored; enabled controls visibility only) enabled_codes = set( - EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True) + CurrencyConfig.objects.filter(enabled=True).values_list("currency_code", flat=True) ) # Static: all currencies appearing in any static exchange rate pair diff --git a/docs/architecture/constant-currency/risk-register.md b/docs/architecture/constant-currency/risk-register.md index 4f445e2515..9f58f0a557 100644 --- a/docs/architecture/constant-currency/risk-register.md +++ b/docs/architecture/constant-currency/risk-register.md @@ -201,7 +201,7 @@ currency dropdown is either hidden or shows *"No exchange rates available"*. 3. Enable discovered currencies in Settings to make them visible in the dropdown **Linked from**: [pipeline-changes.md § Writer 1](./pipeline-changes.md#modified-get_daily_currency_rates--writer-1), -[data-model.md § EnabledCurrency](./data-model.md#enabledcurrency) +[data-model.md § CurrencyConfig](./data-model.md#currencyconfig) --- diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index f34b075bce..e5a9211877 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -2,10 +2,10 @@ # Copyright 2021 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Currency helpers backed by the EnabledCurrency table. +"""Currency helpers backed by the CurrencyConfig table. No hardcoded currency list. Currencies are discovered dynamically by the -daily Celery task and managed via the EnabledCurrency table (tenant schema). +daily Celery task and managed via the CurrencyConfig table (tenant schema). Administrators enable currencies through the Settings UI. Name, symbol, and description are computed at response time via babel. @@ -14,7 +14,7 @@ from babel.numbers import get_currency_symbol from babel.numbers import UnknownCurrencyError -from cost_models.models import EnabledCurrency +from cost_models.models import CurrencyConfig def get_enabled_currency_codes(): @@ -23,7 +23,7 @@ def get_enabled_currency_codes(): Requires tenant schema context (set by django-tenants middleware for requests or by ``schema_context()`` in tasks). """ - return set(EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True)) + return set(CurrencyConfig.objects.filter(enabled=True).values_list("currency_code", flat=True)) def get_all_currency_codes(): @@ -31,7 +31,7 @@ def get_all_currency_codes(): Requires tenant schema context. """ - return set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + return set(CurrencyConfig.objects.values_list("currency_code", flat=True)) def get_currency_info(code): diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index 4822dbad56..6dce8d47ff 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -9,7 +9,7 @@ from rest_framework.test import APIClient from api.iam.test.iam_test_case import IamTestCase -from cost_models.models import EnabledCurrency +from cost_models.models import CurrencyConfig class CurrencyViewTest(IamTestCase): @@ -17,10 +17,10 @@ class CurrencyViewTest(IamTestCase): def setUp(self): super().setUp() - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="USD", enabled=True) - EnabledCurrency.objects.create(currency_code="EUR", enabled=True) - EnabledCurrency.objects.create(currency_code="GBP", enabled=False) + CurrencyConfig.objects.all().delete() + CurrencyConfig.objects.create(currency_code="USD", enabled=True) + CurrencyConfig.objects.create(currency_code="EUR", enabled=True) + CurrencyConfig.objects.create(currency_code="GBP", enabled=False) @patch( "api.currency.view.get_currency_info", diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index d73acc648a..7560871260 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -14,7 +14,7 @@ from api.common.pagination import ListPaginator from api.currency.currencies import get_currency_info from api.currency.models import ExchangeRateDictionary -from cost_models.models import EnabledCurrency +from cost_models.models import CurrencyConfig @api_view(("GET",)) @@ -24,10 +24,10 @@ def get_currency(request): """Get available currencies. Returns currencies that have been enabled by an administrator via - the EnabledCurrency table. Name, symbol, and description are + the CurrencyConfig table. Name, symbol, and description are computed at response time via babel. """ - enabled_codes = EnabledCurrency.objects.filter(enabled=True).values_list("currency_code", flat=True) + enabled_codes = CurrencyConfig.objects.filter(enabled=True).values_list("currency_code", flat=True) available = [get_currency_info(code) for code in sorted(enabled_codes)] return ListPaginator(available, request).paginated_response diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 73d1a15637..9faf3202ac 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -15,39 +15,39 @@ from api.common import log_json from api.common.pagination import ListPaginator from api.common.permissions.settings_access import SettingsAccessPermission -from cost_models.models import EnabledCurrency +from cost_models.models import CurrencyConfig LOG = logging.getLogger(__name__) -class EnabledCurrencyItemSerializer(serializers.Serializer): +class CurrencyConfigItemSerializer(serializers.Serializer): currency_code = serializers.CharField(max_length=5) enabled = serializers.BooleanField() -class EnabledCurrencyUpdateSerializer(serializers.Serializer): - currencies = EnabledCurrencyItemSerializer(many=True) +class CurrencyConfigUpdateSerializer(serializers.Serializer): + currencies = CurrencyConfigItemSerializer(many=True) -class EnabledCurrencyView(APIView): +class CurrencyConfigView(APIView): """List and update enabled/disabled currencies for a tenant.""" permission_classes = [SettingsAccessPermission] @method_decorator(never_cache) def get(self, request, *args, **kwargs): - currencies = EnabledCurrency.objects.all().values("currency_code", "enabled") + currencies = CurrencyConfig.objects.all().values("currency_code", "enabled") data = list(currencies) paginator = ListPaginator(data, request) return paginator.get_paginated_response(data) @method_decorator(never_cache) def put(self, request, *args, **kwargs): - serializer = EnabledCurrencyUpdateSerializer(data=request.data) + serializer = CurrencyConfigUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) for item in serializer.validated_data["currencies"]: - EnabledCurrency.objects.update_or_create( + CurrencyConfig.objects.update_or_create( currency_code=item["currency_code"].upper(), defaults={"enabled": item["enabled"]}, ) diff --git a/koku/api/urls.py b/koku/api/urls.py index c56a6d277c..f97a81e840 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -9,8 +9,7 @@ from rest_framework.routers import DefaultRouter from api.common.deprecate_view import SunsetView -from api.settings.currency_views import EnabledCurrencyView -from cost_models.static_exchange_rate_view import StaticExchangeRateViewSet +from api.settings.currency_views import CurrencyConfigView from api.views import AccountSettings from api.views import AWSAccountRegionView from api.views import AWSAccountView @@ -106,6 +105,7 @@ from api.views import StatusView from api.views import UserAccessView from api.views import UserCostTypeSettings +from cost_models.static_exchange_rate_view import StaticExchangeRateViewSet from koku.cache import AWS_CACHE_PREFIX from koku.cache import AZURE_CACHE_PREFIX from koku.cache import CacheEnum @@ -426,7 +426,7 @@ ), path( "settings/currency/enabled-currencies/", - EnabledCurrencyView.as_view(), + CurrencyConfigView.as_view(), name="enabled-currencies", ), path( diff --git a/koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py b/koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py new file mode 100644 index 0000000000..633d2d6528 --- /dev/null +++ b/koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py @@ -0,0 +1,23 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("cost_models", "0015_enabledcurrency"), + ] + + operations = [ + migrations.RenameModel( + old_name="EnabledCurrency", + new_name="CurrencyConfig", + ), + migrations.AlterModelTable( + name="currencyconfig", + table="currency_config", + ), + ] diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index e2f0f80fc6..aa31adff73 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -227,11 +227,11 @@ def name(self): return f"{self.base_currency}-{self.target_currency}" -class EnabledCurrency(models.Model): - """Tracks which currencies are visible in the target currency dropdown.""" +class CurrencyConfig(models.Model): + """Per-tenant currency configuration: tracks which currencies are visible in the target currency dropdown.""" class Meta: - db_table = "enabled_currency" + db_table = "currency_config" ordering = ["currency_code"] currency_code = models.CharField(max_length=5, unique=True) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 6dab0178dd..a1c0d333f7 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -13,7 +13,7 @@ from api.common import log_json from api.currency.currencies import get_all_currency_codes from api.currency.models import ExchangeRateDictionary -from cost_models.models import EnabledCurrency +from cost_models.models import CurrencyConfig from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType from cost_models.models import StaticExchangeRate @@ -32,9 +32,9 @@ def _iter_months(start_date, end_date): def _ensure_currencies_enabled(*currency_codes): - """Ensure EnabledCurrency rows exist and are enabled for the given currency codes.""" + """Ensure CurrencyConfig rows exist and are enabled for the given currency codes.""" for code in currency_codes: - EnabledCurrency.objects.update_or_create( + CurrencyConfig.objects.update_or_create( currency_code=code, defaults={"enabled": True}, ) diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index b4b2ddede9..ae4225a8dc 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -2,18 +2,18 @@ # Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Tests for EnabledCurrency views.""" +"""Tests for CurrencyConfig views.""" from django.urls import reverse from django_tenants.utils import tenant_context from rest_framework import status from rest_framework.test import APIClient from api.iam.test.iam_test_case import IamTestCase -from cost_models.models import EnabledCurrency +from cost_models.models import CurrencyConfig -class EnabledCurrencyViewTest(IamTestCase): - """Tests for EnabledCurrencyView.""" +class CurrencyConfigViewTest(IamTestCase): + """Tests for CurrencyConfigView.""" def setUp(self): super().setUp() @@ -22,31 +22,31 @@ def setUp(self): def test_get_enabled_currencies_empty(self): with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() + CurrencyConfig.objects.all().delete() response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_get_enabled_currencies(self): with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="USD", enabled=True) - EnabledCurrency.objects.create(currency_code="EUR", enabled=False) + CurrencyConfig.objects.all().delete() + CurrencyConfig.objects.create(currency_code="USD", enabled=True) + CurrencyConfig.objects.create(currency_code="EUR", enabled=False) response = self.client.get(self.url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_put_enable_currencies(self): with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="GBP", enabled=False) + CurrencyConfig.objects.all().delete() + CurrencyConfig.objects.create(currency_code="GBP", enabled=False) data = {"currencies": [{"currency_code": "GBP", "enabled": True}]} response = self.client.put(self.url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertTrue(EnabledCurrency.objects.get(currency_code="GBP").enabled) + self.assertTrue(CurrencyConfig.objects.get(currency_code="GBP").enabled) def test_put_creates_new_currency(self): with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() + CurrencyConfig.objects.all().delete() data = {"currencies": [{"currency_code": "JPY", "enabled": True}]} response = self.client.put(self.url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertTrue(EnabledCurrency.objects.filter(currency_code="JPY", enabled=True).exists()) + self.assertTrue(CurrencyConfig.objects.filter(currency_code="JPY", enabled=True).exists()) diff --git a/koku/cost_models/test/test_monthly_exchange_rate.py b/koku/cost_models/test/test_monthly_exchange_rate.py index 8ef9a6c642..5c2484edfc 100644 --- a/koku/cost_models/test/test_monthly_exchange_rate.py +++ b/koku/cost_models/test/test_monthly_exchange_rate.py @@ -2,14 +2,14 @@ # Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Tests for the MonthlyExchangeRate and EnabledCurrency models.""" +"""Tests for the MonthlyExchangeRate and CurrencyConfig models.""" from datetime import date from decimal import Decimal from django.db import IntegrityError from django_tenants.utils import tenant_context -from cost_models.models import EnabledCurrency +from cost_models.models import CurrencyConfig from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType from masu.test import MasuTestCase @@ -91,22 +91,22 @@ def test_update_or_create_overwrites_dynamic_with_static(self): self.assertEqual(rate.exchange_rate, Decimal("0.920000000000000")) -class EnabledCurrencyTest(MasuTestCase): - """Tests for EnabledCurrency model.""" +class CurrencyConfigTest(MasuTestCase): + """Tests for CurrencyConfig model.""" def test_create_disabled_currency(self): """Test creating a disabled currency entry.""" with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - ec = EnabledCurrency.objects.create(currency_code="JPY", enabled=False) + CurrencyConfig.objects.all().delete() + ec = CurrencyConfig.objects.create(currency_code="JPY", enabled=False) self.assertEqual(ec.currency_code, "JPY") self.assertFalse(ec.enabled) def test_enable_currency(self): """Test enabling a currency.""" with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - ec = EnabledCurrency.objects.create(currency_code="GBP", enabled=False) + CurrencyConfig.objects.all().delete() + ec = CurrencyConfig.objects.create(currency_code="GBP", enabled=False) ec.enabled = True ec.save() ec.refresh_from_db() @@ -115,7 +115,7 @@ def test_enable_currency(self): def test_unique_currency_code(self): """Test that duplicate currency codes raise IntegrityError.""" with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="CNY", enabled=False) + CurrencyConfig.objects.all().delete() + CurrencyConfig.objects.create(currency_code="CNY", enabled=False) with self.assertRaises(IntegrityError): - EnabledCurrency.objects.create(currency_code="CNY", enabled=True) + CurrencyConfig.objects.create(currency_code="CNY", enabled=True) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index 6da0527504..b37c77d69b 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -297,19 +297,19 @@ def _fetch_exchange_rates(url): def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): - """Sync EnabledCurrency and upsert dynamic MonthlyExchangeRate rows for one tenant.""" - from cost_models.models import EnabledCurrency + """Sync CurrencyConfig and upsert dynamic MonthlyExchangeRate rows for one tenant.""" + from cost_models.models import CurrencyConfig from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types with schema_context(schema_name): - existing_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + existing_codes = set(CurrencyConfig.objects.values_list("currency_code", flat=True)) new_currencies = set(exchange_dict.keys()) - existing_codes if new_currencies: - EnabledCurrency.objects.bulk_create( + CurrencyConfig.objects.bulk_create( [ - EnabledCurrency( + CurrencyConfig( currency_code=code, enabled=False, ) From 3c041e96407fa637585053ce0472a4bf505612a9 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 21 Apr 2026 14:26:13 +0300 Subject: [PATCH 030/106] Fix misleading log message in currency config endpoint The PUT endpoint can both enable and disable currencies, so "Enabled currencies updated" was inaccurate. Made-with: Cursor --- koku/api/settings/currency_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 9faf3202ac..4513de30ad 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -54,7 +54,7 @@ def put(self, request, *args, **kwargs): LOG.info( log_json( - msg="Enabled currencies updated", + msg="Currency configuration updated", count=len(serializer.validated_data["currencies"]), ) ) From e110c23f649075d53e02c501c33c1e9e3cb1718c Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 21 Apr 2026 14:37:20 +0300 Subject: [PATCH 031/106] Extract CurrencyField to deduplicate validate_currency across serializers The same validate_currency method was duplicated in ForecastParamSerializer, ParamSerializer, and UserSettingUpdateCurrencySerializer. Replace all three with a reusable CurrencyField (CharField subclass) that normalizes to uppercase and validates against enabled currencies in to_internal_value. Made-with: Cursor --- koku/api/currency/currencies.py | 15 +++++++++++++++ koku/api/forecast/serializers.py | 10 ++-------- koku/api/report/serializers.py | 13 ++----------- koku/api/settings/serializers.py | 10 ++-------- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index e5a9211877..08736359ad 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -13,6 +13,7 @@ from babel.numbers import get_currency_name from babel.numbers import get_currency_symbol from babel.numbers import UnknownCurrencyError +from rest_framework import serializers from cost_models.models import CurrencyConfig @@ -34,6 +35,20 @@ def get_all_currency_codes(): return set(CurrencyConfig.objects.values_list("currency_code", flat=True)) +class CurrencyField(serializers.CharField): + """CharField that normalizes to uppercase and validates against enabled currencies.""" + + def __init__(self, **kwargs): + kwargs.setdefault("max_length", 5) + super().__init__(**kwargs) + + def to_internal_value(self, data): + value = super().to_internal_value(data).upper() + if value not in get_enabled_currency_codes(): + raise serializers.ValidationError(f'"{value}" is not an enabled currency.') + return value + + def get_currency_info(code): """Return a dict with code, name, symbol, and description for a currency. diff --git a/koku/api/forecast/serializers.py b/koku/api/forecast/serializers.py index 00e2c64ae0..7a59527360 100644 --- a/koku/api/forecast/serializers.py +++ b/koku/api/forecast/serializers.py @@ -5,7 +5,7 @@ """Forecast Serializers.""" from rest_framework import serializers -from api.currency.currencies import get_enabled_currency_codes +from api.currency.currencies import CurrencyField from api.report.constants import AWS_COST_TYPE_CHOICES from api.report.serializers import handle_invalid_fields from api.utils import get_cost_type @@ -17,13 +17,7 @@ class ForecastParamSerializer(serializers.Serializer): limit = serializers.IntegerField(required=False, min_value=1) offset = serializers.IntegerField(required=False, min_value=0) - currency = serializers.CharField(max_length=5, required=False) - - def validate_currency(self, value): - value = value.upper() - if value not in get_enabled_currency_codes(): - raise serializers.ValidationError(f'"{value}" is not an enabled currency.') - return value + currency = CurrencyField(required=False) def __init__(self, *args, **kwargs): """Initialize the BaseSerializer.""" diff --git a/koku/api/report/serializers.py b/koku/api/report/serializers.py index 007111f50c..f01d6f0b27 100644 --- a/koku/api/report/serializers.py +++ b/koku/api/report/serializers.py @@ -10,7 +10,7 @@ from rest_framework import serializers from rest_framework.fields import DateField -from api.currency.currencies import get_enabled_currency_codes +from api.currency.currencies import CurrencyField from api.report.constants import AWS_CATEGORY_PREFIX from api.report.constants import TAG_PREFIX from api.report.queries import ReportQueryHandler @@ -340,16 +340,13 @@ class ParamSerializer(BaseSerializer): _tagkey_support = True - # Adding pagination fields to the serializer because we validate - # before running reports and paginating limit = serializers.IntegerField(required=False) offset = serializers.IntegerField(required=False) - # DateField defaults: format='iso-8601', input_formats=['iso-8601'] start_date = serializers.DateField(required=False) end_date = serializers.DateField(required=False) - currency = serializers.CharField(max_length=5, required=False) + currency = CurrencyField(required=False) category = StringOrListField(child=serializers.CharField(), required=False) order_by_allowlist = ( @@ -367,12 +364,6 @@ class ParamSerializer(BaseSerializer): "wasted_cost", ) - def validate_currency(self, value): - value = value.upper() - if value not in get_enabled_currency_codes(): - raise serializers.ValidationError(f'"{value}" is not an enabled currency.') - return value - def validate(self, data): """Validate incoming data. diff --git a/koku/api/settings/serializers.py b/koku/api/settings/serializers.py index 4c377fcbc3..ee38e9da2f 100644 --- a/koku/api/settings/serializers.py +++ b/koku/api/settings/serializers.py @@ -5,7 +5,7 @@ """Serializers for Masu API `manifest`.""" from rest_framework import serializers -from api.currency.currencies import get_enabled_currency_codes +from api.currency.currencies import CurrencyField from api.settings.settings import COST_TYPE_CHOICES from reporting.user_settings.models import UserSettings @@ -28,10 +28,4 @@ class UserSettingUpdateCostTypeSerializer(serializers.Serializer): class UserSettingUpdateCurrencySerializer(serializers.Serializer): """Serializer for setting currency.""" - currency = serializers.CharField(max_length=5) - - def validate_currency(self, value): - value = value.upper() - if value not in get_enabled_currency_codes(): - raise serializers.ValidationError(f'"{value}" is not an enabled currency.') - return value + currency = CurrencyField() From ea89a29534c796bf403b4a8d24c2493a48d51f14 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 21 Apr 2026 14:43:23 +0300 Subject: [PATCH 032/106] Rename currency config endpoint path from enabled-currencies to config Made-with: Cursor --- docs/architecture/constant-currency/api-and-frontend.md | 6 +++--- docs/architecture/constant-currency/phased-delivery.md | 2 +- koku/api/urls.py | 4 ++-- koku/cost_models/test/test_enabled_currency.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index d872712382..549de49e0d 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -141,7 +141,7 @@ This endpoint is always available. No Unleash feature flag gating. ### URL ``` -GET/PUT /api/cost-management/v1/settings/currency/enabled-currencies/ +GET/PUT /api/cost-management/v1/settings/currency/config/ ``` This endpoint lists all known currencies and their enabled/disabled status, and @@ -360,8 +360,8 @@ Add endpoint definitions for: - `GET /api/cost-management/v1/exchange-rate-pairs/{uuid}/` — retrieve - `PUT /api/cost-management/v1/exchange-rate-pairs/{uuid}/` — update - `DELETE /api/cost-management/v1/exchange-rate-pairs/{uuid}/` — delete -- `GET /api/cost-management/v1/settings/currency/enabled-currencies/` — list enabled/disabled currencies -- `PUT /api/cost-management/v1/settings/currency/enabled-currencies/` — enable/disable currencies +- `GET /api/cost-management/v1/settings/currency/config/` — list enabled/disabled currencies +- `PUT /api/cost-management/v1/settings/currency/config/` — enable/disable currencies - `GET /api/cost-management/v1/settings/currency/available-currencies/` — list available target currencies Add `exchange_rates_applied` to report response schemas. diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index 6b9071116f..b6c086e89e 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -50,7 +50,7 @@ pairs. Show rate provenance in report responses. | OCP handler update | `koku/api/report/ocp/query_handler.py` | OCP-specific rate resolution from `MonthlyExchangeRate` | | Forecast handler update | `koku/forecast/forecast.py` | Rate resolution from `MonthlyExchangeRate` | | Report meta update | `koku/api/report/queries.py` | `exchange_rates_applied` metadata, no-rate error handling | -| OpenAPI update | `koku/docs/specs/openapi.json` | New endpoint definitions (exchange-rate-pairs, enabled-currencies, available-currencies) | +| OpenAPI update | `koku/docs/specs/openapi.json` | New endpoint definitions (exchange-rate-pairs, currency-config, available-currencies) | | Serializer tests | `koku/cost_models/test/test_static_exchange_rate_serializer.py` | Validation tests | | View tests | `koku/cost_models/test/test_static_exchange_rate_view.py` | CRUD tests | | MonthlyExchangeRate tests | `koku/cost_models/test/test_monthly_exchange_rate.py` | Rate creation, query, locking tests | diff --git a/koku/api/urls.py b/koku/api/urls.py index f97a81e840..0b92edd496 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -425,9 +425,9 @@ name="settings-aws-category-keys-disable", ), path( - "settings/currency/enabled-currencies/", + "settings/currency/config/", CurrencyConfigView.as_view(), - name="enabled-currencies", + name="currency-config", ), path( "settings/currency/static-exchange-rates/", diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index ae4225a8dc..0b9d9043ef 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -18,7 +18,7 @@ class CurrencyConfigViewTest(IamTestCase): def setUp(self): super().setUp() self.client = APIClient() - self.url = reverse("enabled-currencies") + self.url = reverse("currency-config") def test_get_enabled_currencies_empty(self): with tenant_context(self.tenant): From 54cf1cf3603cad2e72ebe8a74dea3129f9e52fce Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 21 Apr 2026 14:54:49 +0300 Subject: [PATCH 033/106] revert --- koku/cost_models/price_list_serializer.py | 2 ++ koku/cost_models/price_list_view.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/koku/cost_models/price_list_serializer.py b/koku/cost_models/price_list_serializer.py index 7d990c59d6..50c560ffa5 100644 --- a/koku/cost_models/price_list_serializer.py +++ b/koku/cost_models/price_list_serializer.py @@ -1,3 +1,5 @@ +# +# Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # """Serializer for Price List API.""" diff --git a/koku/cost_models/price_list_view.py b/koku/cost_models/price_list_view.py index fc7807ab3b..b736d071e6 100644 --- a/koku/cost_models/price_list_view.py +++ b/koku/cost_models/price_list_view.py @@ -3,6 +3,8 @@ # SPDX-License-Identifier: Apache-2.0 # """View for Price Lists.""" +import logging + from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django_filters import BooleanFilter @@ -23,6 +25,8 @@ from cost_models.price_list_manager import PriceListManager from cost_models.price_list_serializer import PriceListSerializer +LOG = logging.getLogger(__name__) + class PriceListFilter(FilterSet): """Price list custom filters.""" From 83ebcf8846e8c520ca209540e1de822c91842fda Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 23 Apr 2026 14:17:17 +0300 Subject: [PATCH 034/106] Use CurrencyField and enabled currencies in cost model serializers and view Replace duplicated validate_unit/validate_currency methods with the shared CurrencyField, and switch the cost model list filter to validate against enabled currencies instead of all known currency codes. Made-with: Cursor --- koku/cost_models/serializers.py | 26 ++++---------------------- koku/cost_models/view.py | 6 +++--- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/koku/cost_models/serializers.py b/koku/cost_models/serializers.py index 989f28f7fd..64d57e42b4 100644 --- a/koku/cost_models/serializers.py +++ b/koku/cost_models/serializers.py @@ -13,7 +13,7 @@ from api.common import error_obj from api.common import log_json -from api.currency.currencies import get_all_currency_codes +from api.currency.currencies import CurrencyField from api.metrics import constants as metric_constants from api.metrics.constants import SOURCE_TYPE_MAP from api.metrics.views import CostModelMetricMapJSONException @@ -95,13 +95,7 @@ class TieredRateSerializer(serializers.Serializer): value = serializers.DecimalField(required=False, max_digits=19, decimal_places=10) usage = serializers.DictField(required=False) - unit = serializers.CharField(max_length=5) - - def validate_unit(self, value): - value = value.upper() - if value not in get_all_currency_codes(): - raise serializers.ValidationError(f'"{value}" is not a known currency.') - return value + unit = CurrencyField() def validate_value(self, value): """Check that value is a positive value.""" @@ -139,18 +133,12 @@ class TagRateValueSerializer(serializers.Serializer): DECIMALS = ("value", "usage_start", "usage_end") tag_value = serializers.CharField(max_length=100) - unit = serializers.CharField(max_length=5) + unit = CurrencyField() usage = serializers.DictField(required=False) value = serializers.DecimalField(required=False, max_digits=19, decimal_places=10) description = serializers.CharField(allow_blank=True, max_length=500) default = serializers.BooleanField() - def validate_unit(self, value): - value = value.upper() - if value not in get_all_currency_codes(): - raise serializers.ValidationError(f'"{value}" is not a known currency.') - return value - def validate_value(self, value): """Check that value is a positive value.""" if value < 0: @@ -471,16 +459,10 @@ class Meta: distribution_info = DistributionSerializer(required=False) - currency = serializers.CharField(max_length=5, required=False) + currency = CurrencyField(required=False) price_list_uuids = serializers.ListField(child=serializers.UUIDField(), required=False) - def validate_currency(self, value): - value = value.upper() - if value not in get_all_currency_codes(): - raise serializers.ValidationError(f'"{value}" is not a known currency.') - return value - @property def customer(self): """Return the customer for the request.""" diff --git a/koku/cost_models/view.py b/koku/cost_models/view.py index 27db034d78..f084b0d8df 100644 --- a/koku/cost_models/view.py +++ b/koku/cost_models/view.py @@ -26,7 +26,7 @@ from api.common.filters import CharListFilter from api.common.permissions.cost_models_access import CostModelsAccessPermission -from api.currency.currencies import get_all_currency_codes +from api.currency.currencies import get_enabled_currency_codes from cost_models.cost_model_manager import CostModelManager from cost_models.models import CostModel from cost_models.serializers import CostModelSerializer @@ -46,8 +46,8 @@ class CostModelsFilter(FilterSet): def currency_filter(self, qs, name, values): """Filter currency if a valid currency is passed in""" - if values and values[0].upper() not in get_all_currency_codes(): - error = {"currency": f'"{values[0]}" is not a valid choice.'} + if values and values[0].upper() not in get_enabled_currency_codes(): + error = {"currency": f'"{values[0]}" is not an enabled currency.'} raise serializers.ValidationError(error) lookup = "__".join([name, "iexact"]) queries = [Q(**{lookup: val}) for val in values] From 4875bfb48f9fcdb308849b9bd97cac9ac1d46bea Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 23 Apr 2026 14:45:48 +0300 Subject: [PATCH 035/106] Simplify exchange_rates_applied access in ReportQueryHandler start_datetime and end_datetime are always available on the handler; remove unnecessary getattr guards and hasattr checks. Made-with: Cursor --- koku/api/report/queries.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 977cef4eff..6820b8c268 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -1054,14 +1054,13 @@ def _initialize_response_output(self, parameters): if self.currency: output["currency"] = self.currency - start = getattr(self, "start_datetime", None) - end = getattr(self, "end_datetime", None) - if start and end: - output["exchange_rates_applied"] = self._get_exchange_rates_applied( - start.date() if hasattr(start, "date") else start, - end.date() if hasattr(end, "date") else end, - self.currency, - ) + start = self.start_datetime + end = self.end_datetime + output["exchange_rates_applied"] = self._get_exchange_rates_applied( + start.date(), + end.date(), + self.currency, + ) return output From a1e238c1205fb1113253fb10ca8bb66dddf8fc6c Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 23 Apr 2026 18:47:16 +0300 Subject: [PATCH 036/106] Move imports to module level and remove redundant guard in _get_exchange_rates_applied Made-with: Cursor --- koku/api/report/queries.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 6820b8c268..811eb15ac6 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -22,6 +22,7 @@ import ciso8601 import numpy as np import pandas as pd +from dateutil.relativedelta import relativedelta from django.contrib.postgres.aggregates import ArrayAgg from django.db.models import Case from django.db.models import CharField @@ -35,6 +36,7 @@ from django.db.models.functions import Coalesce from django.db.models.functions import Concat from django.db.models.functions import RowNumber +from django_tenants.utils import tenant_context from pandas.api.types import CategoricalDtype from api.models import Provider @@ -1066,13 +1068,6 @@ def _initialize_response_output(self, parameters): def _get_exchange_rates_applied(self, start_date, end_date, target_currency): """Build exchange_rates_applied metadata from MonthlyExchangeRate for the query range.""" - if not target_currency: - return [] - - from dateutil.relativedelta import relativedelta - - from django_tenants.utils import tenant_context - start_month = start_date.replace(day=1) if start_date else None end_month = end_date.replace(day=1) if end_date else None if not start_month or not end_month: From 039849a467b2fc68bd567425fbb58db9b1771d68 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 23 Apr 2026 18:48:44 +0300 Subject: [PATCH 037/106] Remove unnecessary None guards in _get_exchange_rates_applied start_date and end_date are always set by the time this method is called, so the None checks and early return were dead code. Made-with: Cursor --- koku/api/report/queries.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 811eb15ac6..11b6c9d126 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -1068,10 +1068,8 @@ def _initialize_response_output(self, parameters): def _get_exchange_rates_applied(self, start_date, end_date, target_currency): """Build exchange_rates_applied metadata from MonthlyExchangeRate for the query range.""" - start_month = start_date.replace(day=1) if start_date else None - end_month = end_date.replace(day=1) if end_date else None - if not start_month or not end_month: - return [] + start_month = start_date.replace(day=1) + end_month = end_date.replace(day=1) with tenant_context(self.tenant): rates = list( From 8089c7c857b746ca69e0e893f3e2beca0cde13d6 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 14:21:07 +0300 Subject: [PATCH 038/106] Simplify _get_exchange_rates_applied to one entry per month Drop the consecutive-month coalescing loop in favor of a direct list comprehension. Each MonthlyExchangeRate row already represents a single month, so merging adjacent identical rates added complexity without meaningful benefit. Made-with: Cursor --- koku/api/report/queries.py | 45 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 11b6c9d126..a8b3f53442 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -1068,6 +1068,8 @@ def _initialize_response_output(self, parameters): def _get_exchange_rates_applied(self, start_date, end_date, target_currency): """Build exchange_rates_applied metadata from MonthlyExchangeRate for the query range.""" + # MonthlyExchangeRate stores one rate per month with effective_date on the 1st, + # so snap query bounds to month starts to avoid excluding overlapping months. start_month = start_date.replace(day=1) end_month = end_date.replace(day=1) @@ -1082,36 +1084,19 @@ def _get_exchange_rates_applied(self, start_date, end_date, target_currency): .values("base_currency", "target_currency", "exchange_rate", "rate_type", "effective_date") ) - result = [] - for rate in rates: - last_day = calendar.monthrange(rate["effective_date"].year, rate["effective_date"].month)[1] - end_of_month = rate["effective_date"].replace(day=last_day) - - if result and ( - result[-1]["base_currency"] == rate["base_currency"] - and result[-1]["target_currency"] == rate["target_currency"] - and result[-1]["rate"] == str(rate["exchange_rate"]) - and result[-1]["type"] == rate["rate_type"] - and result[-1]["_next_month"] == rate["effective_date"] - ): - result[-1]["end_date"] = str(end_of_month) - result[-1]["_next_month"] = rate["effective_date"] + relativedelta(months=1) - else: - result.append( - { - "base_currency": rate["base_currency"], - "target_currency": rate["target_currency"], - "rate": str(rate["exchange_rate"]), - "type": rate["rate_type"], - "start_date": str(rate["effective_date"]), - "end_date": str(end_of_month), - "_next_month": rate["effective_date"] + relativedelta(months=1), - } - ) - - for entry in result: - entry.pop("_next_month", None) - return result + return [ + { + "base_currency": rate["base_currency"], + "target_currency": rate["target_currency"], + "rate": str(rate["exchange_rate"]), + "type": rate["rate_type"], + "start_date": str(rate["effective_date"]), + "end_date": str(rate["effective_date"].replace( + day=calendar.monthrange(rate["effective_date"].year, rate["effective_date"].month)[1] + )), + } + for rate in rates + ] def _pack_data_object(self, data, **kwargs): # noqa: C901 """Pack data into object format.""" From 5979803ef01a4c309a482937d8c8ee35a93b652d Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 16:30:10 +0300 Subject: [PATCH 039/106] Revert MIG profiles implicit group_by for filter[limit] Remove the special-case that allowed filter[limit] without an explicit group_by on the mig_profiles endpoint, along with the rank_limit_include_others override and the associated test. Made-with: Cursor --- koku/api/report/ocp/provider_map.py | 2 -- koku/api/report/serializers.py | 12 +++---- .../api/report/test/ocp/view/test_gpu_view.py | 34 ------------------- 3 files changed, 5 insertions(+), 43 deletions(-) diff --git a/koku/api/report/ocp/provider_map.py b/koku/api/report/ocp/provider_map.py index b666995a52..68f9dd6904 100644 --- a/koku/api/report/ocp/provider_map.py +++ b/koku/api/report/ocp/provider_map.py @@ -1167,8 +1167,6 @@ def __init__(self, provider, report_type, schema_name): {"field": "mig_profile", "operation": "gt", "parameter": ""}, ], "group_by": ["mig_profile"], - # Do not synthesize an "Other(s)" bucket when filter[limit] is used; return top-N only. - "rank_limit_include_others": False, "cost_units_key": "raw_currency", "sum_columns": [], }, diff --git a/koku/api/report/serializers.py b/koku/api/report/serializers.py index f01d6f0b27..e555505048 100644 --- a/koku/api/report/serializers.py +++ b/koku/api/report/serializers.py @@ -20,10 +20,6 @@ from masu.processor import check_group_by_limit from reporting.provider.ocp.models import OpenshiftCostCategory -# URL path fragments for reports that rank with filter[limit]/offset using implicit group_by -# from the provider map (no group_by query params required). See report_type "group_by". -_FILTER_LIMIT_IMPLICIT_GROUP_BY_PATH_MARKERS = frozenset(("instance-types", "mig_profiles")) - def handle_invalid_fields(this, data): """Validate incoming data. @@ -418,9 +414,11 @@ def validate(self, data): filter_limit = data.get("filter", {}).get("limit") filter_offset = data.get("filter", {}).get("offset") - path = self.context["request"].path - implicit_group_by_for_limit = any(m in path for m in _FILTER_LIMIT_IMPLICIT_GROUP_BY_PATH_MARKERS) - if not implicit_group_by_for_limit and (filter_limit or filter_offset) and not data.get("group_by"): + if ( + "instance-types" not in self.context["request"].path + and (filter_limit or filter_offset) + and not data.get("group_by") + ): error = {"error": "filter[limit] and filter[offset] requires a valid group_by param."} raise serializers.ValidationError(error) diff --git a/koku/api/report/test/ocp/view/test_gpu_view.py b/koku/api/report/test/ocp/view/test_gpu_view.py index 6e3de3f146..db96314094 100644 --- a/koku/api/report/test/ocp/view/test_gpu_view.py +++ b/koku/api/report/test/ocp/view/test_gpu_view.py @@ -255,40 +255,6 @@ def test_mig_profiles_endpoint_with_valid_filters(self, mock_unleash): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIn("data", response.data) - @patch("api.report.ocp.view.is_feature_flag_enabled_by_schema", return_value=True) - def test_mig_profiles_endpoint_accepts_filter_limit_without_group_by(self, mock_unleash): - """filter[limit] ranks by implicit group_by[mig_profile]; must not require explicit group_by.""" - url = reverse("reports-openshift-gpu-mig-profiles") - query_params = { - "filter[gpu_vendor]": "nvidia", - "filter[gpu_model]": "A100", - "filter[node]": "gpu_node_0", - "filter[limit]": "2", - } - url = url + "?" + urlencode(query_params, doseq=True) - response = self.client.get(url, **self.headers) - err = getattr(response, "data", response.content) - self.assertEqual(response.status_code, status.HTTP_200_OK, err) - self.assertIn("data", response.data) - self._assert_mig_profiles_response_has_no_other_bucket(response.data) - - def _assert_mig_profiles_response_has_no_other_bucket(self, payload): - """MIG profiles with filter[limit] must not add a synthetic Other(s) mig_profile row.""" - - def walk(obj): - if isinstance(obj, dict): - mp = obj.get("mig_profile") - if isinstance(mp, str) and mp in ("Other", "Others"): - yield mp - for v in obj.values(): - yield from walk(v) - elif isinstance(obj, list): - for item in obj: - yield from walk(item) - - bad = list(walk(payload)) - self.assertEqual(bad, [], f"Unexpected Other bucket in mig_profiles response: {bad}") - @patch("api.report.ocp.view.is_feature_flag_enabled_by_schema", return_value=True) def test_mig_profiles_endpoint_accepts_exact_project_filter(self, mock_unleash): """MIG profiles accepts filter[exact:project] (UI parity with other OCP reports).""" From e8313d93e5941428c1e84f03def5037858c1af21 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 16:33:56 +0300 Subject: [PATCH 040/106] Use DateHelper.month_end instead of manual calendar.monthrange in exchange rates Made-with: Cursor --- koku/api/report/queries.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index a8b3f53442..36ad8e295b 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """Query Handling for Reports.""" -import calendar import copy import logging import random @@ -1091,9 +1090,7 @@ def _get_exchange_rates_applied(self, start_date, end_date, target_currency): "rate": str(rate["exchange_rate"]), "type": rate["rate_type"], "start_date": str(rate["effective_date"]), - "end_date": str(rate["effective_date"].replace( - day=calendar.monthrange(rate["effective_date"].year, rate["effective_date"].month)[1] - )), + "end_date": str(self.dh.month_end(rate["effective_date"])), } for rate in rates ] From 4c0de874a8118aeb82c1491742ea5409ef5ddbe4 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 17:04:20 +0300 Subject: [PATCH 041/106] COST-7252: Skip Others bucket for MIG profile ranking and fix GROUP BY annotation conflict MIG profiles should return exact top-N results without synthesizing an "Others" bucket. Also prevent Django ValueError when the rank key is already a GROUP BY column by removing it from rank_annotations. Made-with: Cursor --- koku/api/report/ocp/provider_map.py | 2 ++ koku/api/report/queries.py | 21 ++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/koku/api/report/ocp/provider_map.py b/koku/api/report/ocp/provider_map.py index 68f9dd6904..b666995a52 100644 --- a/koku/api/report/ocp/provider_map.py +++ b/koku/api/report/ocp/provider_map.py @@ -1167,6 +1167,8 @@ def __init__(self, provider, report_type, schema_name): {"field": "mig_profile", "operation": "gt", "parameter": ""}, ], "group_by": ["mig_profile"], + # Do not synthesize an "Other(s)" bucket when filter[limit] is used; return top-N only. + "rank_limit_include_others": False, "cost_units_key": "raw_currency", "sum_columns": [], }, diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 36ad8e295b..7ee5f27694 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -1051,6 +1051,7 @@ def _apply_group_by(self, query_data, group_by=None): def _initialize_response_output(self, parameters): """Initialize output response object.""" output = copy.deepcopy(parameters.parameters) + # remove access from the output output.pop("access") if self.currency: @@ -1365,6 +1366,11 @@ def _group_by_ranks(self, query, data): # noqa: C901 if self.order_field == "subscription_name": group_by_value.append("subscription_name") + # Do not re-annotate names that are already GROUP BY columns (often the rank key). + # Otherwise Django raises ValueError + for grouped_col in group_by_value: + rank_annotations.pop(grouped_col, None) + ranks = ( query.annotate(**self.annotations) .values(*group_by_value) @@ -1469,13 +1475,14 @@ def _ranked_list(self, data_list, ranks, rank_fields=None): # noqa C901 (data_frame["rank"] > self._offset) & (data_frame["rank"] <= (self._offset + self._limit)) ] else: - # Get others category - others_data_frame = self._aggregate_ranks_over_limit(data_frame, group_by) - # Reduce data to limit - data_frame = data_frame[data_frame["rank"] <= self._limit] - - # Add the others category to the data set - data_frame = pd.concat([data_frame, others_data_frame]) + include_others = self._mapper.report_type_map.get("rank_limit_include_others", True) + if include_others: + # Aggregate rank > limit before trimming; _aggregate_ranks_over_limit needs those rows. + others_data_frame = self._aggregate_ranks_over_limit(data_frame, group_by) + data_frame = data_frame[data_frame["rank"] <= self._limit] + data_frame = pd.concat([data_frame, others_data_frame]) + else: + data_frame = data_frame[data_frame["rank"] <= self._limit] # Replace NaN with 0 numeric_columns = [col for col in self.report_annotations if "unit" not in col] From 73086428e686be41ffb0fc080b6061eb863db250 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 17:45:22 +0300 Subject: [PATCH 042/106] Revert MIG-related changes from constant-currency branch Remove MIG profile fixes that don't belong in this PR: - Restore _FILTER_LIMIT_IMPLICIT_GROUP_BY_PATH_MARKERS in serializers - Restore MIG filter limit test in test_gpu_view.py Made-with: Cursor --- koku/api/report/serializers.py | 12 ++++--- .../api/report/test/ocp/view/test_gpu_view.py | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/koku/api/report/serializers.py b/koku/api/report/serializers.py index ab90233717..0da9ac1fe9 100644 --- a/koku/api/report/serializers.py +++ b/koku/api/report/serializers.py @@ -20,6 +20,10 @@ from masu.processor import check_group_by_limit from reporting.provider.ocp.models import OpenshiftCostCategory +# URL path fragments for reports that rank with filter[limit]/offset using implicit group_by +# from the provider map (no group_by query params required). See report_type "group_by". +_FILTER_LIMIT_IMPLICIT_GROUP_BY_PATH_MARKERS = frozenset(("instance-types", "mig_profiles")) + def handle_invalid_fields(this, data): """Validate incoming data. @@ -421,11 +425,9 @@ def validate(self, data): filter_limit = data.get("filter", {}).get("limit") filter_offset = data.get("filter", {}).get("offset") - if ( - "instance-types" not in self.context["request"].path - and (filter_limit or filter_offset) - and not data.get("group_by") - ): + path = self.context["request"].path + implicit_group_by_for_limit = any(m in path for m in _FILTER_LIMIT_IMPLICIT_GROUP_BY_PATH_MARKERS) + if not implicit_group_by_for_limit and (filter_limit or filter_offset) and not data.get("group_by"): error = {"error": "filter[limit] and filter[offset] requires a valid group_by param."} raise serializers.ValidationError(error) diff --git a/koku/api/report/test/ocp/view/test_gpu_view.py b/koku/api/report/test/ocp/view/test_gpu_view.py index db96314094..6e3de3f146 100644 --- a/koku/api/report/test/ocp/view/test_gpu_view.py +++ b/koku/api/report/test/ocp/view/test_gpu_view.py @@ -255,6 +255,40 @@ def test_mig_profiles_endpoint_with_valid_filters(self, mock_unleash): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIn("data", response.data) + @patch("api.report.ocp.view.is_feature_flag_enabled_by_schema", return_value=True) + def test_mig_profiles_endpoint_accepts_filter_limit_without_group_by(self, mock_unleash): + """filter[limit] ranks by implicit group_by[mig_profile]; must not require explicit group_by.""" + url = reverse("reports-openshift-gpu-mig-profiles") + query_params = { + "filter[gpu_vendor]": "nvidia", + "filter[gpu_model]": "A100", + "filter[node]": "gpu_node_0", + "filter[limit]": "2", + } + url = url + "?" + urlencode(query_params, doseq=True) + response = self.client.get(url, **self.headers) + err = getattr(response, "data", response.content) + self.assertEqual(response.status_code, status.HTTP_200_OK, err) + self.assertIn("data", response.data) + self._assert_mig_profiles_response_has_no_other_bucket(response.data) + + def _assert_mig_profiles_response_has_no_other_bucket(self, payload): + """MIG profiles with filter[limit] must not add a synthetic Other(s) mig_profile row.""" + + def walk(obj): + if isinstance(obj, dict): + mp = obj.get("mig_profile") + if isinstance(mp, str) and mp in ("Other", "Others"): + yield mp + for v in obj.values(): + yield from walk(v) + elif isinstance(obj, list): + for item in obj: + yield from walk(item) + + bad = list(walk(payload)) + self.assertEqual(bad, [], f"Unexpected Other bucket in mig_profiles response: {bad}") + @patch("api.report.ocp.view.is_feature_flag_enabled_by_schema", return_value=True) def test_mig_profiles_endpoint_accepts_exact_project_filter(self, mock_unleash): """MIG profiles accepts filter[exact:project] (UI parity with other OCP reports).""" From da45e1db98da124e0d46755b13fdda0b9b80a29a Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 18:16:53 +0300 Subject: [PATCH 043/106] Move ExchangeRateDictionary import to module level Made-with: Cursor --- koku/masu/celery/tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index b37c77d69b..1fb7677d1e 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -16,6 +16,7 @@ from urllib3.util.retry import Retry from api.common import log_json +from api.currency.models import ExchangeRateDictionary from api.currency.models import ExchangeRates from api.currency.utils import exchange_dictionary from api.iam.models import Tenant @@ -341,8 +342,6 @@ def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): @celery_app.task(name="masu.celery.tasks.get_daily_currency_rates", queue=DEFAULT) def get_daily_currency_rates(): """Task to get latest daily conversion rates and upsert MonthlyExchangeRate per tenant.""" - from api.currency.models import ExchangeRateDictionary - url = settings.CURRENCY_URL if not url: LOG.info(log_json(msg="CURRENCY_URL not configured; skipping dynamic exchange rate fetch")) From ca74b372645643fdc9edb947eb4ff3fb65ae3f6c Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 19:51:42 +0300 Subject: [PATCH 044/106] COST-7252: Refactor exchange rate helpers for clarity Move imports to module level and rename helpers to better reflect their purpose: _fetch_and_store_exchange_rates (fetches + persists to public ExchangeRates) and _upsert_tenant_dynamic_exchange_rates (tenant-scoped MonthlyExchangeRate sync). Made-with: Cursor --- koku/masu/celery/tasks.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index 1fb7677d1e..3d62961e38 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -26,7 +26,11 @@ from common.queues import DownloadQueue from common.queues import PriorityQueue from common.queues import SummaryQueue +from cost_models.models import CurrencyConfig +from cost_models.models import MonthlyExchangeRate +from cost_models.models import RateType from koku import celery_app +from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types from koku.notifications import NotificationService from masu.config import Config from masu.database.cost_model_db_accessor import CostModelDBAccessor @@ -263,7 +267,7 @@ def autovacuum_tune_schemas(): autovacuum_tune_schema.delay(schema_name) -def _fetch_exchange_rates(url): +def _fetch_and_store_exchange_rates(url): """Fetch exchange rates from the configured URL. Returns rate_metrics dict or None on failure.""" retries = Retry( total=5, @@ -283,7 +287,9 @@ def _fetch_exchange_rates(url): return None rate_metrics = {} - for curr_type, value in response.json()["rates"].items(): + data = response.json() + rates = data["rates"] + for curr_type, value in rates.items(): try: exchange = ExchangeRates.objects.get(currency_type=curr_type.lower()) LOG.info(f"Updating currency {curr_type} to {value}") @@ -297,13 +303,8 @@ def _fetch_exchange_rates(url): return rate_metrics -def _upsert_tenant_exchange_rates(schema_name, exchange_dict, current_month): +def _upsert_tenant_dynamic_exchange_rates(schema_name, exchange_dict, current_month): """Sync CurrencyConfig and upsert dynamic MonthlyExchangeRate rows for one tenant.""" - from cost_models.models import CurrencyConfig - from cost_models.models import MonthlyExchangeRate - from cost_models.models import RateType - from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types - with schema_context(schema_name): existing_codes = set(CurrencyConfig.objects.values_list("currency_code", flat=True)) new_currencies = set(exchange_dict.keys()) - existing_codes @@ -347,7 +348,7 @@ def get_daily_currency_rates(): LOG.info(log_json(msg="CURRENCY_URL not configured; skipping dynamic exchange rate fetch")) return {} - rate_metrics = _fetch_exchange_rates(url) + rate_metrics = _fetch_and_store_exchange_rates(url) if rate_metrics is None: return {} @@ -357,7 +358,7 @@ def get_daily_currency_rates(): current_month = DateHelper().this_month_start.date() for tenant in Tenant.objects.exclude(schema_name="public"): - _upsert_tenant_exchange_rates(tenant.schema_name, erd.currency_exchange_dictionary, current_month) + _upsert_tenant_dynamic_exchange_rates(tenant.schema_name, erd.currency_exchange_dictionary, current_month) return rate_metrics From ea6bfa0ecd90cb8934a86020d94fac9ff9e775ba Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 20:06:50 +0300 Subject: [PATCH 045/106] COST-7252: Replace manual get_queryset with DjangoFilterBackend in StaticExchangeRateViewSet Made-with: Cursor --- koku/cost_models/static_exchange_rate_view.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index 2430628aad..94b58eb398 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -5,6 +5,10 @@ """View for StaticExchangeRate CRUD operations.""" from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache +from django_filters import CharFilter +from django_filters import DateFilter +from django_filters import FilterSet +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status from rest_framework import viewsets from rest_framework.response import Response @@ -14,6 +18,19 @@ from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer +class StaticExchangeRateFilter(FilterSet): + """Filters for static exchange rate lookups.""" + + base_currency = CharFilter(field_name="base_currency", lookup_expr="iexact") + target_currency = CharFilter(field_name="target_currency", lookup_expr="iexact") + start_date = DateFilter(field_name="end_date", lookup_expr="gte") + end_date = DateFilter(field_name="start_date", lookup_expr="lte") + + class Meta: + model = StaticExchangeRate + fields = ["base_currency", "target_currency", "start_date", "end_date"] + + class StaticExchangeRateViewSet(viewsets.ModelViewSet): """CRUD for static exchange rate pairs.""" @@ -22,19 +39,8 @@ class StaticExchangeRateViewSet(viewsets.ModelViewSet): lookup_field = "uuid" permission_classes = (CostModelsAccessPermission,) http_method_names = ["get", "post", "put", "delete", "head"] - - def get_queryset(self): - qs = super().get_queryset() - params = self.request.query_params - if base := params.get("base_currency"): - qs = qs.filter(base_currency=base.upper()) - if target := params.get("target_currency"): - qs = qs.filter(target_currency=target.upper()) - if start_date := params.get("start_date"): - qs = qs.filter(end_date__gte=start_date) - if end_date := params.get("end_date"): - qs = qs.filter(start_date__lte=end_date) - return qs + filter_backends = (DjangoFilterBackend,) + filterset_class = StaticExchangeRateFilter @method_decorator(never_cache) def list(self, request, *args, **kwargs): From d0aaede2ccfda937c3295d23433c875ec43e604c Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 20:24:42 +0300 Subject: [PATCH 046/106] COST-7252: Remove redundant never_cache wrappers from StaticExchangeRateViewSet The serializer already invalidates the view cache on create, update, and delete, making the per-method never_cache decorators unnecessary. Made-with: Cursor --- koku/cost_models/static_exchange_rate_view.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index 94b58eb398..3624a56927 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -3,8 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """View for StaticExchangeRate CRUD operations.""" -from django.utils.decorators import method_decorator -from django.views.decorators.cache import never_cache from django_filters import CharFilter from django_filters import DateFilter from django_filters import FilterSet @@ -42,23 +40,6 @@ class StaticExchangeRateViewSet(viewsets.ModelViewSet): filter_backends = (DjangoFilterBackend,) filterset_class = StaticExchangeRateFilter - @method_decorator(never_cache) - def list(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) - - @method_decorator(never_cache) - def retrieve(self, request, *args, **kwargs): - return super().retrieve(request, *args, **kwargs) - - @method_decorator(never_cache) - def create(self, request, *args, **kwargs): - return super().create(request, *args, **kwargs) - - @method_decorator(never_cache) - def update(self, request, *args, **kwargs): - return super().update(request, *args, **kwargs) - - @method_decorator(never_cache) def destroy(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance) From 84034ede0ec0d3f4433f276728a10a2bcb70bef8 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 20:27:38 +0300 Subject: [PATCH 047/106] COST-7252: Use perform_destroy hook instead of overriding destroy in StaticExchangeRateViewSet Made-with: Cursor --- koku/cost_models/static_exchange_rate_view.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index 3624a56927..14c34539e3 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -7,9 +7,7 @@ from django_filters import DateFilter from django_filters import FilterSet from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import status from rest_framework import viewsets -from rest_framework.response import Response from api.common.permissions.cost_models_access import CostModelsAccessPermission from cost_models.models import StaticExchangeRate @@ -40,8 +38,6 @@ class StaticExchangeRateViewSet(viewsets.ModelViewSet): filter_backends = (DjangoFilterBackend,) filterset_class = StaticExchangeRateFilter - def destroy(self, request, *args, **kwargs): - instance = self.get_object() + def perform_destroy(self, instance): serializer = self.get_serializer(instance) serializer.delete(instance) - return Response(status=status.HTTP_204_NO_CONTENT) From a8f058f349a6dda9997de4910ce5c8826ba59698 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 20:39:08 +0300 Subject: [PATCH 048/106] COST-7252: Extract exchange rate helpers into static_exchange_rate_utils module Move MonthlyExchangeRate side-effect logic (iter_months, ensure_currencies_enabled, upsert_monthly_rates, remove_static_and_backfill_dynamic) out of the serializer into a dedicated utils module, following the project convention of keeping business logic separate from serialization. The view's perform_destroy now imports directly from utils. Made-with: Cursor --- .../static_exchange_rate_serializer.py | 95 ++----------------- .../cost_models/static_exchange_rate_utils.py | 74 +++++++++++++++ koku/cost_models/static_exchange_rate_view.py | 21 +++- .../test_static_exchange_rate_serializer.py | 12 ++- 4 files changed, 110 insertions(+), 92 deletions(-) create mode 100644 koku/cost_models/static_exchange_rate_utils.py diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index a1c0d333f7..f751b960ec 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -6,85 +6,20 @@ import calendar import logging -from dateutil.relativedelta import relativedelta from django.db import transaction from rest_framework import serializers from api.common import log_json from api.currency.currencies import get_all_currency_codes -from api.currency.models import ExchangeRateDictionary -from cost_models.models import CurrencyConfig -from cost_models.models import MonthlyExchangeRate -from cost_models.models import RateType from cost_models.models import StaticExchangeRate +from cost_models.static_exchange_rate_utils import ensure_currencies_enabled +from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic +from cost_models.static_exchange_rate_utils import upsert_monthly_rates from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types LOG = logging.getLogger(__name__) -def _iter_months(start_date, end_date): - """Yield the first day of each month between start_date and end_date inclusive.""" - current = start_date.replace(day=1) - end = end_date.replace(day=1) - while current <= end: - yield current - current += relativedelta(months=1) - - -def _ensure_currencies_enabled(*currency_codes): - """Ensure CurrencyConfig rows exist and are enabled for the given currency codes.""" - for code in currency_codes: - CurrencyConfig.objects.update_or_create( - currency_code=code, - defaults={"enabled": True}, - ) - - -def _upsert_monthly_rates(static_rate): - """Upsert MonthlyExchangeRate rows with rate_type=static for each month in the validity period.""" - for month_start in _iter_months(static_rate.start_date, static_rate.end_date): - MonthlyExchangeRate.objects.update_or_create( - effective_date=month_start, - base_currency=static_rate.base_currency, - target_currency=static_rate.target_currency, - defaults={ - "exchange_rate": static_rate.exchange_rate, - "rate_type": RateType.STATIC, - }, - ) - - -def _remove_static_and_backfill_dynamic(base_currency, target_currency, start_date, end_date): - """Remove static rows for affected months and backfill with dynamic rates from ExchangeRateDictionary.""" - MonthlyExchangeRate.objects.filter( - effective_date__gte=start_date.replace(day=1), - effective_date__lte=end_date.replace(day=1), - base_currency=base_currency, - target_currency=target_currency, - rate_type=RateType.STATIC, - ).delete() - - erd = ExchangeRateDictionary.objects.first() - if not erd or not erd.currency_exchange_dictionary: - return - - exchange_dict = erd.currency_exchange_dictionary - rate = exchange_dict.get(base_currency, {}).get(target_currency) - if rate is None: - return - - for month_start in _iter_months(start_date, end_date): - MonthlyExchangeRate.objects.update_or_create( - effective_date=month_start, - base_currency=base_currency, - target_currency=target_currency, - defaults={ - "exchange_rate": rate, - "rate_type": RateType.DYNAMIC, - }, - ) - - class StaticExchangeRateSerializer(serializers.ModelSerializer): name = serializers.SerializerMethodField() @@ -165,8 +100,8 @@ def _get_schema_name(self): @transaction.atomic def create(self, validated_data): instance = StaticExchangeRate.objects.create(**validated_data) - _ensure_currencies_enabled(instance.base_currency, instance.target_currency) - _upsert_monthly_rates(instance) + ensure_currencies_enabled(instance.base_currency, instance.target_currency) + upsert_monthly_rates(instance) schema_name = self._get_schema_name() if schema_name: invalidate_view_cache_for_tenant_and_all_source_types(schema_name) @@ -194,10 +129,10 @@ def update(self, instance, validated_data): instance.save() if old_base != instance.base_currency or old_target != instance.target_currency: - _remove_static_and_backfill_dynamic(old_base, old_target, old_start, old_end) + remove_static_and_backfill_dynamic(old_base, old_target, old_start, old_end) - _ensure_currencies_enabled(instance.base_currency, instance.target_currency) - _upsert_monthly_rates(instance) + ensure_currencies_enabled(instance.base_currency, instance.target_currency) + upsert_monthly_rates(instance) schema_name = self._get_schema_name() if schema_name: @@ -211,17 +146,3 @@ def update(self, instance, validated_data): ) return instance - @transaction.atomic - def delete(self, instance): - _remove_static_and_backfill_dynamic( - instance.base_currency, - instance.target_currency, - instance.start_date, - instance.end_date, - ) - pair_name = instance.name - instance.delete() - schema_name = self._get_schema_name() - if schema_name: - invalidate_view_cache_for_tenant_and_all_source_types(schema_name) - LOG.info(log_json(msg="Static exchange rate deleted", pair=pair_name)) diff --git a/koku/cost_models/static_exchange_rate_utils.py b/koku/cost_models/static_exchange_rate_utils.py new file mode 100644 index 0000000000..a7c2ae649a --- /dev/null +++ b/koku/cost_models/static_exchange_rate_utils.py @@ -0,0 +1,74 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Utilities for managing MonthlyExchangeRate side effects of StaticExchangeRate operations.""" +from dateutil.relativedelta import relativedelta + +from api.currency.models import ExchangeRateDictionary +from cost_models.models import CurrencyConfig +from cost_models.models import MonthlyExchangeRate +from cost_models.models import RateType + + +def iter_months(start_date, end_date): + """Yield the first day of each month between start_date and end_date inclusive.""" + current = start_date.replace(day=1) + end = end_date.replace(day=1) + while current <= end: + yield current + current += relativedelta(months=1) + + +def ensure_currencies_enabled(*currency_codes): + """Ensure CurrencyConfig rows exist and are enabled for the given currency codes.""" + for code in currency_codes: + CurrencyConfig.objects.update_or_create( + currency_code=code, + defaults={"enabled": True}, + ) + + +def upsert_monthly_rates(static_rate): + """Upsert MonthlyExchangeRate rows with rate_type=static for each month in the validity period.""" + for month_start in iter_months(static_rate.start_date, static_rate.end_date): + MonthlyExchangeRate.objects.update_or_create( + effective_date=month_start, + base_currency=static_rate.base_currency, + target_currency=static_rate.target_currency, + defaults={ + "exchange_rate": static_rate.exchange_rate, + "rate_type": RateType.STATIC, + }, + ) + + +def remove_static_and_backfill_dynamic(base_currency, target_currency, start_date, end_date): + """Remove static rows for affected months and backfill with dynamic rates from ExchangeRateDictionary.""" + MonthlyExchangeRate.objects.filter( + effective_date__gte=start_date.replace(day=1), + effective_date__lte=end_date.replace(day=1), + base_currency=base_currency, + target_currency=target_currency, + rate_type=RateType.STATIC, + ).delete() + + erd = ExchangeRateDictionary.objects.first() + if not erd or not erd.currency_exchange_dictionary: + return + + exchange_dict = erd.currency_exchange_dictionary + rate = exchange_dict.get(base_currency, {}).get(target_currency) + if rate is None: + return + + for month_start in iter_months(start_date, end_date): + MonthlyExchangeRate.objects.update_or_create( + effective_date=month_start, + base_currency=base_currency, + target_currency=target_currency, + defaults={ + "exchange_rate": rate, + "rate_type": RateType.DYNAMIC, + }, + ) diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index 14c34539e3..645b3fb894 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -3,15 +3,23 @@ # SPDX-License-Identifier: Apache-2.0 # """View for StaticExchangeRate CRUD operations.""" +import logging + +from django.db import transaction from django_filters import CharFilter from django_filters import DateFilter from django_filters import FilterSet from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets +from api.common import log_json from api.common.permissions.cost_models_access import CostModelsAccessPermission from cost_models.models import StaticExchangeRate from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer +from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic +from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types + +LOG = logging.getLogger(__name__) class StaticExchangeRateFilter(FilterSet): @@ -38,6 +46,15 @@ class StaticExchangeRateViewSet(viewsets.ModelViewSet): filter_backends = (DjangoFilterBackend,) filterset_class = StaticExchangeRateFilter + @transaction.atomic def perform_destroy(self, instance): - serializer = self.get_serializer(instance) - serializer.delete(instance) + remove_static_and_backfill_dynamic( + instance.base_currency, + instance.target_currency, + instance.start_date, + instance.end_date, + ) + pair_name = instance.name + instance.delete() + invalidate_view_cache_for_tenant_and_all_source_types(self.request.user.customer.schema_name) + LOG.info(log_json(msg="Static exchange rate deleted", pair=pair_name)) diff --git a/koku/cost_models/test/test_static_exchange_rate_serializer.py b/koku/cost_models/test/test_static_exchange_rate_serializer.py index e9a0b43dff..f4ee7f71d9 100644 --- a/koku/cost_models/test/test_static_exchange_rate_serializer.py +++ b/koku/cost_models/test/test_static_exchange_rate_serializer.py @@ -13,6 +13,7 @@ from cost_models.models import RateType from cost_models.models import StaticExchangeRate from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer +from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic from masu.test import MasuTestCase @@ -141,8 +142,7 @@ def test_update_increments_version(self, mock_invalidate): updated = serializer2.save() self.assertEqual(updated.version, 2) - @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") - def test_delete_removes_static_monthly_rates(self, mock_invalidate): + def test_delete_removes_static_monthly_rates(self): """Test that deleting a static rate removes static MonthlyExchangeRate rows.""" with tenant_context(self.tenant): serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) @@ -155,7 +155,13 @@ def test_delete_removes_static_monthly_rates(self, mock_invalidate): ).exists() ) - serializer.delete(instance) + remove_static_and_backfill_dynamic( + instance.base_currency, + instance.target_currency, + instance.start_date, + instance.end_date, + ) + instance.delete() self.assertFalse( MonthlyExchangeRate.objects.filter( From 2296f7eda33bdaf4c464cde18f830a65188ff49b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 20:51:53 +0300 Subject: [PATCH 049/106] COST-7252: Validate static exchange rate currencies against ISO 4217 instead of CurrencyConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The serializer validated currency codes against CurrencyConfig, which is populated by the daily Celery task or by ensure_currencies_enabled() — but ensure_currencies_enabled() only runs after validation passes. This created a chicken-and-egg problem: on-prem deployments without dynamic rates had an empty CurrencyConfig and could never create static rates. Validate against babel's ISO 4217 registry instead, which requires no database state. ensure_currencies_enabled() still adds the currencies to CurrencyConfig after saving. Made-with: Cursor --- koku/api/currency/currencies.py | 14 ++++++++++++++ .../static_exchange_rate_serializer.py | 15 +++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index 08736359ad..2bc15a26db 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -49,6 +49,20 @@ def to_internal_value(self, data): return value +def is_valid_iso_currency(code): + """Check whether *code* is a valid ISO 4217 currency using babel's registry. + + Unlike ``get_all_currency_codes`` this does NOT require the currency to + already exist in ``CurrencyConfig``, so it can be used to validate codes + *before* they are inserted (e.g. when creating static exchange rates). + """ + try: + get_currency_name(code.upper(), locale="en_US") + return True + except UnknownCurrencyError: + return False + + def get_currency_info(code): """Return a dict with code, name, symbol, and description for a currency. diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index f751b960ec..cecc9ac010 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -10,7 +10,7 @@ from rest_framework import serializers from api.common import log_json -from api.currency.currencies import get_all_currency_codes +from api.currency.currencies import is_valid_iso_currency from cost_models.models import StaticExchangeRate from cost_models.static_exchange_rate_utils import ensure_currencies_enabled from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic @@ -42,17 +42,17 @@ class Meta: def get_name(self, obj): return f"{obj.base_currency}-{obj.target_currency}" - def validate_base_currency(self, value): + def _validate_currency_code(self, value): value = value.upper() - if value not in get_all_currency_codes(): + if not is_valid_iso_currency(value): raise serializers.ValidationError(f"Invalid currency code: {value}") return value + def validate_base_currency(self, value): + return self._validate_currency_code(value) + def validate_target_currency(self, value): - value = value.upper() - if value not in get_all_currency_codes(): - raise serializers.ValidationError(f"Invalid currency code: {value}") - return value + return self._validate_currency_code(value) def validate_start_date(self, value): if value.day != 1: @@ -145,4 +145,3 @@ def update(self, instance, validated_data): ) ) return instance - From 95581fee78a5a31cd414262db1fb0d46ebaa4e6a Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 21:01:40 +0300 Subject: [PATCH 050/106] COST-7252: Remove ensure_currencies_enabled from static exchange rate serializer Currency enablement is a user-facing concern, not an admin concern. The admin adding/removing static rates should not force-enable currencies as a side effect. Made-with: Cursor --- koku/cost_models/static_exchange_rate_serializer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index cecc9ac010..c718668794 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -12,7 +12,6 @@ from api.common import log_json from api.currency.currencies import is_valid_iso_currency from cost_models.models import StaticExchangeRate -from cost_models.static_exchange_rate_utils import ensure_currencies_enabled from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic from cost_models.static_exchange_rate_utils import upsert_monthly_rates from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types @@ -100,7 +99,6 @@ def _get_schema_name(self): @transaction.atomic def create(self, validated_data): instance = StaticExchangeRate.objects.create(**validated_data) - ensure_currencies_enabled(instance.base_currency, instance.target_currency) upsert_monthly_rates(instance) schema_name = self._get_schema_name() if schema_name: @@ -131,7 +129,6 @@ def update(self, instance, validated_data): if old_base != instance.base_currency or old_target != instance.target_currency: remove_static_and_backfill_dynamic(old_base, old_target, old_start, old_end) - ensure_currencies_enabled(instance.base_currency, instance.target_currency) upsert_monthly_rates(instance) schema_name = self._get_schema_name() From c7d3118a0063db53cb595a6b2d7b4ba111cb4de7 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 21:07:35 +0300 Subject: [PATCH 051/106] COST-7252: Inline schema_name access following project convention Remove _get_schema_name helper and defensive None checks; the middleware guarantees request.user.customer is always set before the serializer runs. Made-with: Cursor --- .../static_exchange_rate_serializer.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index c718668794..eebae09036 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -90,19 +90,12 @@ def validate(self, data): return data - def _get_schema_name(self): - request = self.context.get("request") - if request and hasattr(request, "user") and hasattr(request.user, "customer"): - return request.user.customer.schema_name - return None - @transaction.atomic def create(self, validated_data): instance = StaticExchangeRate.objects.create(**validated_data) upsert_monthly_rates(instance) - schema_name = self._get_schema_name() - if schema_name: - invalidate_view_cache_for_tenant_and_all_source_types(schema_name) + schema_name = self.context["request"].user.customer.schema_name + invalidate_view_cache_for_tenant_and_all_source_types(schema_name) LOG.info( log_json( msg="Static exchange rate created", @@ -131,9 +124,8 @@ def update(self, instance, validated_data): upsert_monthly_rates(instance) - schema_name = self._get_schema_name() - if schema_name: - invalidate_view_cache_for_tenant_and_all_source_types(schema_name) + schema_name = self.context["request"].user.customer.schema_name + invalidate_view_cache_for_tenant_and_all_source_types(schema_name) LOG.info( log_json( msg="Static exchange rate updated", From a146485a5d1966e6551e206a91a46219b180b1a8 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 21:09:58 +0300 Subject: [PATCH 052/106] COST-7252: Rename upsert_monthly_rates to upsert_static_monthly_rates for clarity Made-with: Cursor --- koku/cost_models/static_exchange_rate_serializer.py | 6 +++--- koku/cost_models/static_exchange_rate_utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index eebae09036..a71b8b6789 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -13,7 +13,7 @@ from api.currency.currencies import is_valid_iso_currency from cost_models.models import StaticExchangeRate from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic -from cost_models.static_exchange_rate_utils import upsert_monthly_rates +from cost_models.static_exchange_rate_utils import upsert_static_monthly_rates from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types LOG = logging.getLogger(__name__) @@ -93,7 +93,7 @@ def validate(self, data): @transaction.atomic def create(self, validated_data): instance = StaticExchangeRate.objects.create(**validated_data) - upsert_monthly_rates(instance) + upsert_static_monthly_rates(instance) schema_name = self.context["request"].user.customer.schema_name invalidate_view_cache_for_tenant_and_all_source_types(schema_name) LOG.info( @@ -122,7 +122,7 @@ def update(self, instance, validated_data): if old_base != instance.base_currency or old_target != instance.target_currency: remove_static_and_backfill_dynamic(old_base, old_target, old_start, old_end) - upsert_monthly_rates(instance) + upsert_static_monthly_rates(instance) schema_name = self.context["request"].user.customer.schema_name invalidate_view_cache_for_tenant_and_all_source_types(schema_name) diff --git a/koku/cost_models/static_exchange_rate_utils.py b/koku/cost_models/static_exchange_rate_utils.py index a7c2ae649a..da9f5db1c0 100644 --- a/koku/cost_models/static_exchange_rate_utils.py +++ b/koku/cost_models/static_exchange_rate_utils.py @@ -29,7 +29,7 @@ def ensure_currencies_enabled(*currency_codes): ) -def upsert_monthly_rates(static_rate): +def upsert_static_monthly_rates(static_rate): """Upsert MonthlyExchangeRate rows with rate_type=static for each month in the validity period.""" for month_start in iter_months(static_rate.start_date, static_rate.end_date): MonthlyExchangeRate.objects.update_or_create( From 2a99c7cd8af083cd4f74846502e9c912158ff098 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Sun, 26 Apr 2026 21:15:39 +0300 Subject: [PATCH 053/106] COST-7252: Remove dead ensure_currencies_enabled function from utils Made-with: Cursor --- koku/cost_models/static_exchange_rate_utils.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/koku/cost_models/static_exchange_rate_utils.py b/koku/cost_models/static_exchange_rate_utils.py index da9f5db1c0..42f4ff9ee3 100644 --- a/koku/cost_models/static_exchange_rate_utils.py +++ b/koku/cost_models/static_exchange_rate_utils.py @@ -6,12 +6,11 @@ from dateutil.relativedelta import relativedelta from api.currency.models import ExchangeRateDictionary -from cost_models.models import CurrencyConfig from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType -def iter_months(start_date, end_date): +def _iter_months(start_date, end_date): """Yield the first day of each month between start_date and end_date inclusive.""" current = start_date.replace(day=1) end = end_date.replace(day=1) @@ -20,18 +19,9 @@ def iter_months(start_date, end_date): current += relativedelta(months=1) -def ensure_currencies_enabled(*currency_codes): - """Ensure CurrencyConfig rows exist and are enabled for the given currency codes.""" - for code in currency_codes: - CurrencyConfig.objects.update_or_create( - currency_code=code, - defaults={"enabled": True}, - ) - - def upsert_static_monthly_rates(static_rate): """Upsert MonthlyExchangeRate rows with rate_type=static for each month in the validity period.""" - for month_start in iter_months(static_rate.start_date, static_rate.end_date): + for month_start in _iter_months(static_rate.start_date, static_rate.end_date): MonthlyExchangeRate.objects.update_or_create( effective_date=month_start, base_currency=static_rate.base_currency, @@ -62,7 +52,7 @@ def remove_static_and_backfill_dynamic(base_currency, target_currency, start_dat if rate is None: return - for month_start in iter_months(start_date, end_date): + for month_start in _iter_months(start_date, end_date): MonthlyExchangeRate.objects.update_or_create( effective_date=month_start, base_currency=base_currency, From f241684dc35bf5e0a4272c8b1561b9d4238304a4 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 27 Apr 2026 14:29:55 +0300 Subject: [PATCH 054/106] COST-7252: Use set-based ISO 4217 lookup and inline currency validation Replace try/except UnknownCurrencyError with a direct set membership check against babel's all_currencies registry for faster validation. Inline the shared _validate_currency_code helper into each field validator since the indirection added no value. Add merge migration for 0013/0016 branch convergence. Made-with: Cursor --- koku/api/currency/currencies.py | 9 ++++----- .../migrations/0017_merge_20260427_1058.py | 13 +++++++++++++ koku/cost_models/static_exchange_rate_serializer.py | 12 +++++------- 3 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 koku/cost_models/migrations/0017_merge_20260427_1058.py diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index 2bc15a26db..14fe157709 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -10,6 +10,7 @@ Name, symbol, and description are computed at response time via babel. """ +from babel.core import get_global from babel.numbers import get_currency_name from babel.numbers import get_currency_symbol from babel.numbers import UnknownCurrencyError @@ -17,6 +18,8 @@ from cost_models.models import CurrencyConfig +_ISO_4217_CURRENCIES = get_global("all_currencies") + def get_enabled_currency_codes(): """Return the set of currency codes that are currently enabled. @@ -56,11 +59,7 @@ def is_valid_iso_currency(code): already exist in ``CurrencyConfig``, so it can be used to validate codes *before* they are inserted (e.g. when creating static exchange rates). """ - try: - get_currency_name(code.upper(), locale="en_US") - return True - except UnknownCurrencyError: - return False + return code.upper() in _ISO_4217_CURRENCIES def get_currency_info(code): diff --git a/koku/cost_models/migrations/0017_merge_20260427_1058.py b/koku/cost_models/migrations/0017_merge_20260427_1058.py new file mode 100644 index 0000000000..7692b4cfe5 --- /dev/null +++ b/koku/cost_models/migrations/0017_merge_20260427_1058.py @@ -0,0 +1,13 @@ +# Generated by Django 5.2.13 on 2026-04-27 10:58 +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cost_models', '0013_normalize_rates_to_rate_table'), + ('cost_models', '0016_rename_enabledcurrency_to_currencyconfig'), + ] + + operations = [ + ] diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index a71b8b6789..f59bb58431 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -41,17 +41,15 @@ class Meta: def get_name(self, obj): return f"{obj.base_currency}-{obj.target_currency}" - def _validate_currency_code(self, value): - value = value.upper() + def validate_base_currency(self, value): if not is_valid_iso_currency(value): raise serializers.ValidationError(f"Invalid currency code: {value}") - return value - - def validate_base_currency(self, value): - return self._validate_currency_code(value) + return value.upper() def validate_target_currency(self, value): - return self._validate_currency_code(value) + if not is_valid_iso_currency(value): + raise serializers.ValidationError(f"Invalid currency code: {value}") + return value.upper() def validate_start_date(self, value): if value.day != 1: From b7cfde8269fe1f7adffd2b351b644a064368cd1a Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 27 Apr 2026 17:28:18 +0300 Subject: [PATCH 055/106] COST-7252: Replace CurrencyConfig with EnabledCurrency and use Babel ISO 4217 registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify the currency enablement model: presence in EnabledCurrency means enabled (no `enabled` flag needed). Currency discovery is removed from the Celery task — all known currencies come from Babel's ISO 4217 registry at runtime. Consolidate migrations 0013–0017 into a single 0014 migration. Add AllCurrencyView (GET settings/currency/) returning all ISO 4217 currencies with an enabled flag, and replace CurrencyConfigView with EnabledCurrencyConfigView (POST settings/currency/config/) for atomic bulk-set of enabled currencies. Made-with: Cursor --- docs/architecture/constant-currency/README.md | 24 ++--- .../constant-currency/api-and-frontend.md | 51 ++++----- .../constant-currency/data-model.md | 54 ++++------ .../constant-currency/phased-delivery.md | 19 ++-- .../constant-currency/pipeline-changes.md | 42 +++----- .../constant-currency/risk-register.md | 2 +- koku/api/currency/currencies.py | 27 ++--- koku/api/currency/test/test_views.py | 15 +-- koku/api/currency/view.py | 6 +- koku/api/settings/currency_views.py | 69 +++++++----- koku/api/urls.py | 10 +- .../migrations/0013_staticexchangerate.py | 39 ------- .../migrations/0014_constant_currency.py | 76 +++++++++++++ .../migrations/0014_monthlyexchangerate.py | 68 ------------ .../migrations/0015_enabledcurrency.py | 33 ------ ...ename_enabledcurrency_to_currencyconfig.py | 23 ---- .../migrations/0017_merge_20260427_1058.py | 13 --- koku/cost_models/models.py | 8 +- .../cost_models/test/test_enabled_currency.py | 100 +++++++++++++----- .../test/test_monthly_exchange_rate.py | 33 ++---- koku/masu/celery/tasks.py | 17 +-- 21 files changed, 313 insertions(+), 416 deletions(-) delete mode 100644 koku/cost_models/migrations/0013_staticexchangerate.py create mode 100644 koku/cost_models/migrations/0014_constant_currency.py delete mode 100644 koku/cost_models/migrations/0014_monthlyexchangerate.py delete mode 100644 koku/cost_models/migrations/0015_enabledcurrency.py delete mode 100644 koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py delete mode 100644 koku/cost_models/migrations/0017_merge_20260427_1058.py diff --git a/docs/architecture/constant-currency/README.md b/docs/architecture/constant-currency/README.md index bdddc91f1b..2e35484d97 100644 --- a/docs/architecture/constant-currency/README.md +++ b/docs/architecture/constant-currency/README.md @@ -126,24 +126,24 @@ again. **Problem**: Should all currencies returned by the exchange rate API be immediately available for use, or should an administrator explicitly enable them? -**Resolution**: Explicit enablement. Currencies fetched from the dynamic exchange -rate API arrive in Cost Management as **disabled** by default (stored in the -`CurrencyConfig` table with `enabled=False`). An administrator must explicitly -enable currencies through the Settings UI before they appear in the target -currency dropdown. +**Resolution**: Explicit enablement. The full list of known currencies comes from +Babel's ISO 4217 registry. Only currencies that an administrator has explicitly +enabled are stored in the `EnabledCurrency` table. An administrator must enable +currencies through the Settings API (`POST settings/currency/config/`) before +they appear in the target currency dropdown. All currencies are always stored in `MonthlyExchangeRate` regardless of their -enabled status — the `enabled` flag only controls dropdown visibility, not -data storage. This ensures the underlying data is complete and +enabled status — the `EnabledCurrency` table only controls dropdown visibility, +not data storage. This ensures the underlying data is complete and ready when an administrator enables a currency. **Rationale**: Explicit enablement gives administrators control over which currencies appear in their UI. In on-premise environments, customers may only -need a small subset of the ~170 currencies available from the API. Showing all -currencies by default would clutter the dropdown. +need a small subset of the ~300 ISO 4217 currencies. Showing all currencies by +default would clutter the dropdown. **Exception**: Static exchange rate pairs always make their currencies available -in the dropdown, regardless of `CurrencyConfig` status. If an administrator +in the dropdown, regardless of `EnabledCurrency` status. If an administrator defines a `USD→EUR` static rate, both `USD` and `EUR` are immediately available. ### IQ-6: Rate resolution without `CURRENCY_URL` — RESOLVED @@ -254,7 +254,7 @@ graph LR API["open.er-api.com
(or custom URL)"] -->|"daily fetch
(skipped if no URL)"| CT["Celery Task:
get_daily_currency_rates"] CT -->|upsert| ER["ExchangeRates
(public schema)"] CT -->|rebuild| ERD["ExchangeRateDictionary
(public schema)"] - CT -->|"discover currencies
create as disabled"| EC["CurrencyConfig
(tenant schema)
enabled/disabled per currency"] + CT -->|"no currency discovery"| EC["EnabledCurrency
(tenant schema)
admin-managed"] CT -->|"Writer 1: per-tenant
skip static pairs
all currencies"| MER["MonthlyExchangeRate
(tenant schema)
single source of truth"] MER -->|"all months:
per-month rates"| QH["QueryHandler
Subquery annotation"] QH -->|"per-month rates +
rate metadata"| REPORT["Report Response
+ exchange_rates_applied"] @@ -296,7 +296,7 @@ graph LR | 11 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. | | 12 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback, error if neither). Documentation references `open.er-api.com` (free tier) as the production example | | 13 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection | -| 14 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `CurrencyConfig` status | +| 14 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `EnabledCurrency` status | --- diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 549de49e0d..2a9f77febb 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -149,55 +149,50 @@ allows an administrator to enable or disable currencies. ### View -**File**: Extend existing settings views in `koku/api/settings/` or add a new -`CurrencyConfigViewSet`. +**File**: `koku/api/settings/currency_views.py` — `AllCurrencyView` (GET) and +`EnabledCurrencyConfigView` (POST). **Permission**: Cost Management Administrator role (same permission level as other Settings operations). -### Example: GET Response +### Example: GET `settings/currency/` Response + +Returns all ISO 4217 currencies (from Babel) with an `enabled` flag based on +`EnabledCurrency` table membership. ```json { - "meta": { "count": 5 }, + "meta": { "count": 300 }, "data": [ - { "currency_code": "USD", "enabled": true }, - { "currency_code": "EUR", "enabled": true }, - { "currency_code": "GBP", "enabled": false }, - { "currency_code": "CNY", "enabled": false }, - { "currency_code": "JPY", "enabled": false } + { "code": "AED", "name": "UAE Dirham", "symbol": "AED", "description": "...", "enabled": false }, + { "code": "EUR", "name": "Euro", "symbol": "€", "description": "...", "enabled": true }, + { "code": "USD", "name": "US Dollar", "symbol": "$", "description": "...", "enabled": true } ] } ``` -Currencies with `enabled: false` were discovered by the daily exchange rate API -fetch but have not been enabled by an administrator. They will not appear in the -target currency dropdown until enabled. All currencies are always stored -regardless of their enabled status. +### Example: POST `settings/currency/config/` Request (Bulk Set) -### Example: PUT Request (Enable/Disable) +Replaces all enabled currencies atomically with the submitted list. ```json { - "currencies": [ - { "currency_code": "GBP", "enabled": true }, - { "currency_code": "CNY", "enabled": true } - ] + "currencies": ["USD", "EUR", "GBP"] } ``` +Response: `204 No Content` + **Side effects**: Enabling or disabling a currency only affects its visibility in the target currency dropdown. It does not affect the `MonthlyExchangeRate`, -`ExchangeRateDictionary`, `ExchangeRates`, or `StaticExchangeRate` -tables — all currencies are always stored regardless of their enabled status. +`ExchangeRateDictionary`, `ExchangeRates`, or `StaticExchangeRate` tables. ### No `CURRENCY_URL` Configured -When no `CURRENCY_URL` is configured, no dynamic currencies are discovered by the -Celery task, so the `CurrencyConfig` table will have no dynamically-discovered -rows. The GET response will return either an empty list or only currencies that -were manually added. Previously fetched dynamic currencies (if the URL was -removed later) remain in the table. +When no `CURRENCY_URL` is configured, no dynamic exchange rates are fetched by +the Celery task. The `EnabledCurrency` table only contains currencies that an +administrator has explicitly enabled via the Settings API. The full list of +ISO 4217 currencies is always available from Babel. --- @@ -210,8 +205,8 @@ currencies from two sources: | Source | Rule | Example | |--------|------|---------| -| **Dynamic** | Currency has `enabled=True` in `CurrencyConfig` | USD, EUR enabled → appear in dropdown | -| **Static** | Currency appears in any `StaticExchangeRate` pair (as base or target) | Static rate EUR→CHF defined → both EUR and CHF appear in dropdown regardless of `CurrencyConfig` status | +| **Dynamic** | Currency exists in `EnabledCurrency` table | USD, EUR enabled → appear in dropdown | +| **Static** | Currency appears in any `StaticExchangeRate` pair (as base or target) | Static rate EUR→CHF defined → both EUR and CHF appear in dropdown regardless of `EnabledCurrency` status | ### Dropdown Endpoint @@ -242,7 +237,7 @@ static rates, or both. This is informational for the frontend. When **no currencies are available at all** — meaning: -- All dynamic currencies are disabled in `CurrencyConfig` (or none exist), **and** +- No currencies exist in `EnabledCurrency` (none enabled), **and** - No `StaticExchangeRate` rows exist (no static rates) Then the currency dropdown should either be **hidden** or show a message: diff --git a/docs/architecture/constant-currency/data-model.md b/docs/architecture/constant-currency/data-model.md index fa4025eb81..7f4df88368 100644 --- a/docs/architecture/constant-currency/data-model.md +++ b/docs/architecture/constant-currency/data-model.md @@ -113,57 +113,49 @@ defined, each uses its own rate. (see [api-and-frontend.md](./api-and-frontend.md)) and has side effects on `MonthlyExchangeRate` via the serializer. -### `CurrencyConfig` +### `EnabledCurrency` -Tracks which currencies are visible in the target currency dropdown. Currencies -must be explicitly enabled by an administrator before they appear in the -dropdown. All currencies are always stored in `MonthlyExchangeRate` regardless -of their enabled status — the `enabled` flag only controls dropdown visibility. +Tracks which currencies are enabled for the target currency dropdown. Only +enabled currencies are stored — presence in this table means the currency is +enabled. The full list of known currencies comes from Babel's ISO 4217 registry +at runtime. ```python -class CurrencyConfig(models.Model): +class EnabledCurrency(models.Model): currency_code = models.CharField(max_length=5, unique=True) - enabled = models.BooleanField(default=False) created_timestamp = models.DateTimeField(auto_now_add=True) - updated_timestamp = models.DateTimeField(auto_now=True) class Meta: - db_table = "currency_config" + db_table = "enabled_currency" ordering = ["currency_code"] ``` -**Example `currency_config` rows**: +**Example `enabled_currency` rows**: -| id | currency_code | enabled | created_timestamp | updated_timestamp | -|----|---------------|---------|-------------------|-------------------| -| 1 | `USD` | `true` | `2026-01-01 06:00:00+00` | `2026-01-01 06:00:00+00` | -| 2 | `EUR` | `true` | `2026-01-01 06:00:00+00` | `2026-01-15 10:30:00+00` | -| 3 | `GBP` | `false` | `2026-01-01 06:00:00+00` | `2026-01-01 06:00:00+00` | -| 4 | `CNY` | `false` | `2026-01-01 06:00:00+00` | `2026-01-01 06:00:00+00` | -| 5 | `JPY` | `false` | `2026-01-01 06:00:00+00` | `2026-01-01 06:00:00+00` | +| id | currency_code | created_timestamp | +|----|---------------|-------------------| +| 1 | `USD` | `2026-01-01 06:00:00+00` | +| 2 | `EUR` | `2026-01-15 10:30:00+00` | -In this example, `USD` and `EUR` are enabled and will appear in the target -currency dropdown. `GBP`, `CNY`, and `JPY` were discovered by the daily Celery -task (fetched from the exchange rate API) but have not yet been enabled by an -administrator — they are stored in `MonthlyExchangeRate` but hidden from the -dropdown. +In this example, only `USD` and `EUR` are enabled and will appear in the target +currency dropdown. All other ISO 4217 currencies are known via Babel but not +enabled. **Lifecycle**: | Event | Action | |-------|--------| -| Daily Celery task fetches from exchange rate API | Creates `CurrencyConfig` rows with `enabled=False` for any newly discovered currencies not already in the table | -| Administrator enables a currency in Settings | Sets `enabled=True` | -| Administrator disables a currency in Settings | Sets `enabled=False` | +| Administrator enables currencies via `POST settings/currency/config/` | Replaces all `EnabledCurrency` rows with the submitted list | +| `GET settings/currency/` | Returns all ISO 4217 currencies (from Babel) with `enabled` flag based on `EnabledCurrency` table membership | **How currencies become "available" in dropdowns**: A currency is visible in the target currency dropdown if **any** of the following are true: -1. It has `enabled=True` in `CurrencyConfig` +1. It exists in the `EnabledCurrency` table 2. It appears in any `StaticExchangeRate` pair (static rates make their currencies - visible regardless of the `CurrencyConfig` status) + visible regardless of `EnabledCurrency` status) **Corner case — no usable rate**: A currency may be available in the dropdown but have no exchange rate path from the bill's source currency. In this case, the API @@ -337,7 +329,7 @@ If `ExchangeRateDictionary` is empty (e.g., fresh deployment with no rates to seed, and the table starts empty. The daily Celery task and static rate CRUD will populate it going forward. -### M3: Create `currency_config` Table +### M3: Create `enabled_currency` Table | Field | Value | |-------|-------| @@ -375,12 +367,12 @@ changes required. | Version | Date | Summary | |---------|------|---------| | v1.0 | 2026-03-19 | Initial data model design | -| v1.1 | 2026-03-24 | Added `CurrencyConfig` model, M4 migration | -| v1.2 | 2026-03-24 | Simplified enablement: `enabled` flag only controls dropdown visibility, not snapshotting | +| v1.1 | 2026-03-24 | Added `EnabledCurrency` model, M4 migration | +| v1.2 | 2026-03-24 | Simplified enablement: `EnabledCurrency` table controls dropdown visibility, not snapshotting | | v1.3 | 2026-03-24 | Removed airgapped mode concept. Rate resolution: static first, dynamic fallback, error if neither. | | v1.4 | 2026-03-26 | Clarified `StaticExchangeRateDictionary` as source of truth for static rates; `MonthlyExchangeRateSnapshot` as historical rate storage for reports. | | v1.5 | 2026-03-29 | Replaced `year_month` CharField with `effective_date` DateField on `MonthlyExchangeRateSnapshot` for consistency with existing date field patterns (`usage_start`, `billing_period_start`). | -| v1.6 | 2026-03-30 | Renamed `MonthlyExchangeRateSnapshot` → `MonthlyExchangeRate` and promoted it to single source of truth for all months (current and past). Removed `StaticExchangeRateDictionary` — no longer needed since query handlers read from `MonthlyExchangeRate` for all months. Renumbered migrations (M3 is now `currency_config`; old M3 removed). | +| v1.6 | 2026-03-30 | Renamed `MonthlyExchangeRateSnapshot` → `MonthlyExchangeRate` and promoted it to single source of truth for all months (current and past). Removed `StaticExchangeRateDictionary` — no longer needed since query handlers read from `MonthlyExchangeRate` for all months. Renumbered migrations (M3 is now `enabled_currency`; old M3 removed). | | v1.7 | 2026-03-30 | M2 now seeds current-month data from `ExchangeRateDictionary` during migration. Eliminates `ExchangeRateDictionary` fallback in query handler. | | v1.8 | 2026-04-12 | Updated reader description to reflect `Subquery`-based rate resolution (replaces `Case`/`When`). | | v1.9 | 2026-04-13 | Fixed `ExchangeRates` model description: actual fields are `currency_type` (CharField) and `exchange_rate` (FloatField), not `base_currency`/`exchange_rates` JSONField. Removed non-existent `updated_timestamp` column from `ExchangeRateDictionary` example. | diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index b6c086e89e..0f54521b25 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -35,10 +35,10 @@ pairs. Show rate provenance in report responses. |----------|------|-------------| | `StaticExchangeRate` model | `koku/cost_models/models.py` | User-defined rate pairs with validity periods | | `MonthlyExchangeRate` model | `koku/cost_models/models.py` | Single source of truth: per-month, per-pair rate storage for all months | -| `CurrencyConfig` model | `koku/cost_models/models.py` | Tracks enabled/disabled currencies per tenant | +| `EnabledCurrency` model | `koku/cost_models/models.py` | Tracks enabled currencies per tenant (presence = enabled) | | Migration M1 | `koku/cost_models/migrations/XXXX_*.py` | Create `static_exchange_rate` table | | Migration M2 | `koku/cost_models/migrations/XXXX_*.py` | Create `monthly_exchange_rate` table + seed current-month data from `ExchangeRateDictionary` | -| Migration M3 | `koku/cost_models/migrations/XXXX_*.py` | Create `currency_config` table | +| Migration M3 | `koku/cost_models/migrations/XXXX_*.py` | Create `enabled_currency` table | | Serializer | `koku/cost_models/static_exchange_rate_serializer.py` | Validation + `MonthlyExchangeRate` upsert side-effects | | ViewSet | `koku/cost_models/static_exchange_rate_view.py` | CRUD API for static rates | | Currency enablement view | `koku/api/settings/` or new file | Settings API for enable/disable currencies | @@ -75,16 +75,15 @@ pairs. Show rate provenance in report responses. - [ ] Consecutive months with same rate/type collapsed into one period string - [ ] Unit tests pass for serializer, view, MonthlyExchangeRate logic, query handler - [ ] On-prem mode: full functionality without Trino -- [ ] **Currency enablement**: Dynamic currencies arrive as disabled in `CurrencyConfig` -- [ ] **Currency enablement**: Administrator can enable/disable currencies via Settings API +- [ ] **Currency enablement**: Administrator can enable currencies via `POST settings/currency/config/` - [ ] **Currency enablement**: All currencies are stored in `MonthlyExchangeRate` regardless of enabled status; `enabled` flag only controls dropdown visibility - [ ] **Rate resolution**: Static rates take precedence over dynamic rates; error returned if neither exists for a given pair - [ ] **No `CURRENCY_URL`**: Celery task skips API fetch; system works with whatever rates are available -- [ ] **Available currencies**: Dropdown shows only enabled dynamic currencies and static rate currencies (disabled currencies are stored but hidden) -- [ ] **Available currencies**: Static rate currencies appear regardless of `CurrencyConfig` status +- [ ] **Available currencies**: Dropdown shows only enabled currencies and static rate currencies +- [ ] **Available currencies**: Static rate currencies appear regardless of `EnabledCurrency` status - [ ] **No-rate corner case**: Selecting a target currency with no conversion path returns HTTP 400 with actionable error - [ ] **No currencies available**: Dropdown hidden or shows "No exchange rates available" when no currencies are available -- [ ] **Currency discovery**: New currencies from API are created as disabled `CurrencyConfig` rows +- [ ] **Currency list**: `GET settings/currency/` returns all ISO 4217 currencies with enabled flag ### Rollback @@ -100,7 +99,7 @@ pairs. Show rate provenance in report responses. available-currencies endpoints) 7. Drop tables via reverse migration (`migrate_schemas` runs `DeleteModel` for all three new tables: `static_exchange_rate`, `monthly_exchange_rate`, - `currency_config`) + `enabled_currency`) 8. Remove new files: serializer, view, currency enablement views, test files 9. Revert OpenAPI changes @@ -172,8 +171,8 @@ design would be needed to handle path prioritization. | Version | Date | Summary | |---------|------|---------| | v1.0 | 2026-03-19 | Initial phased delivery plan | -| v1.1 | 2026-03-24 | Added CurrencyConfig artifacts (M4, views, tests), currency enablement and airgapped validation items, R7/R8 risks, updated rollback steps | -| v1.2 | 2026-03-24 | Simplified enablement: `enabled` flag only controls dropdown visibility. All currencies always stored and snapshotted. | +| v1.1 | 2026-03-24 | Added EnabledCurrency artifacts (M4, views, tests), currency enablement and airgapped validation items, R7/R8 risks, updated rollback steps | +| v1.2 | 2026-03-24 | Simplified enablement: `EnabledCurrency` table controls dropdown visibility. All currencies always stored and snapshotted. | | v1.3 | 2026-03-24 | Removed airgapped mode concept. Rate resolution: static first, dynamic fallback, error if neither. | | v1.4 | 2026-03-26 | Updated artifacts and validation to reflect two-tier rate resolution (dictionaries + snapshots). | | v1.5 | 2026-03-29 | Updated future scalability section: `year_month` CharField replaced by `effective_date` DateField. | diff --git a/docs/architecture/constant-currency/pipeline-changes.md b/docs/architecture/constant-currency/pipeline-changes.md index bff4039c2e..60a9fb69ae 100644 --- a/docs/architecture/constant-currency/pipeline-changes.md +++ b/docs/architecture/constant-currency/pipeline-changes.md @@ -83,9 +83,7 @@ configuring their own on-premise deployment. 3. Fetches rates from `CURRENCY_URL` *(unchanged when URL is set)* 4. Upserts `ExchangeRates` rows *(unchanged)* 5. Rebuilds `ExchangeRateDictionary` *(unchanged)* -6. **NEW**: Per-tenant currency discovery — creates `CurrencyConfig` rows - with `enabled=False` for newly discovered currencies -7. **NEW**: Per-tenant upsert into `MonthlyExchangeRate` for all currencies +6. **NEW**: Per-tenant upsert into `MonthlyExchangeRate` for all currencies returned by the API At query time: @@ -152,26 +150,17 @@ if not settings.CURRENCY_URL: Fetch rates from `CURRENCY_URL`, upsert `ExchangeRates`, rebuild `ExchangeRateDictionary`. This logic is unchanged from today. -### Step 3: Currency discovery + `MonthlyExchangeRate` upsert (new) +### Step 3: `MonthlyExchangeRate` upsert (new) -After rebuilding `ExchangeRateDictionary`, add per-tenant currency discovery -and `MonthlyExchangeRate` upsert: +After rebuilding `ExchangeRateDictionary`, upsert per-tenant +`MonthlyExchangeRate` rows: ```python current_month = dh.this_month_start # date(2026, 3, 1) exchange_dict = ExchangeRateDictionary.objects.first().currency_exchange_dictionary -all_api_currencies = set(exchange_dict.keys()) for tenant in Tenant.objects.exclude(schema_name="public"): with schema_context(tenant.schema_name): - # Currency discovery: create CurrencyConfig rows for newly seen currencies - existing_codes = set(CurrencyConfig.objects.values_list("currency_code", flat=True)) - new_currencies = all_api_currencies - existing_codes - CurrencyConfig.objects.bulk_create( - [CurrencyConfig(currency_code=code, enabled=False) for code in new_currencies], - ignore_conflicts=True, - ) - # Pre-fetch all static pairs for this month in a single query static_pairs = set( MonthlyExchangeRate.objects.filter( @@ -180,7 +169,7 @@ for tenant in Tenant.objects.exclude(schema_name="public"): ).values_list("base_currency", "target_currency") ) - # Upsert ALL currencies — enabled flag only controls dropdown visibility + # Upsert all currencies — EnabledCurrency controls dropdown visibility for base_cur, targets in exchange_dict.items(): for target_cur, rate in targets.items(): if base_cur == target_cur: @@ -199,12 +188,9 @@ for tenant in Tenant.objects.exclude(schema_name="public"): - **URL check**: If `CURRENCY_URL` is not configured, the task skips the API fetch. Dynamic rates are simply not fetched; the system uses whatever rates are available (static first, dynamic fallback, error if neither exists). -- **Currency discovery**: Creates `CurrencyConfig` rows with `enabled=False` - for any new currencies returned by the API. These appear in Settings as - disabled currencies that the administrator can enable. - **All currencies stored**: Upserts dynamic rates for all currency pairs - returned by the API. The `enabled` flag on `CurrencyConfig` only controls - dropdown visibility, not rate storage. + returned by the API. The `EnabledCurrency` table controls dropdown visibility, + not rate storage. Administrators enable currencies via the Settings API. - Runs daily; overwrites current month's dynamic rows with latest rate - Skips pairs with `rate_type=RateType.STATIC` (static takes precedence) - Past months' rows are never updated (automatic finalization) @@ -508,23 +494,21 @@ def _validate_exchange_rates(self, queryset): ### New: Available Currency Resolution The query handler (or a shared utility) computes the list of currencies -visible in the target currency dropdown. All currencies are stored in -`MonthlyExchangeRate` regardless of their enabled status — the `enabled` flag only -controls dropdown visibility. A currency is **visible in the dropdown** if any -of the following are true: +visible in the target currency dropdown. A currency is **visible in the +dropdown** if any of the following are true: -1. It has `enabled=True` in `CurrencyConfig` +1. It exists in the `EnabledCurrency` table 2. It appears as either `base_currency` or `target_currency` in any `StaticExchangeRate` row (static rates make their currencies visible - regardless of `CurrencyConfig` status) + regardless of `EnabledCurrency` status) ```python @cached_property def available_currencies(self): """Currencies visible in the target currency dropdown.""" - # Dynamic: enabled currencies (all are stored; enabled controls visibility only) + # Enabled currencies (presence in EnabledCurrency table = enabled) enabled_codes = set( - CurrencyConfig.objects.filter(enabled=True).values_list("currency_code", flat=True) + EnabledCurrency.objects.values_list("currency_code", flat=True) ) # Static: all currencies appearing in any static exchange rate pair diff --git a/docs/architecture/constant-currency/risk-register.md b/docs/architecture/constant-currency/risk-register.md index 9f58f0a557..4f445e2515 100644 --- a/docs/architecture/constant-currency/risk-register.md +++ b/docs/architecture/constant-currency/risk-register.md @@ -201,7 +201,7 @@ currency dropdown is either hidden or shows *"No exchange rates available"*. 3. Enable discovered currencies in Settings to make them visible in the dropdown **Linked from**: [pipeline-changes.md § Writer 1](./pipeline-changes.md#modified-get_daily_currency_rates--writer-1), -[data-model.md § CurrencyConfig](./data-model.md#currencyconfig) +[data-model.md § EnabledCurrency](./data-model.md#enabledcurrency) --- diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index 14fe157709..ed0b96a97c 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -2,11 +2,11 @@ # Copyright 2021 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Currency helpers backed by the CurrencyConfig table. +"""Currency helpers backed by the EnabledCurrency table. -No hardcoded currency list. Currencies are discovered dynamically by the -daily Celery task and managed via the CurrencyConfig table (tenant schema). -Administrators enable currencies through the Settings UI. +All known currencies come from babel's ISO 4217 registry. Only the +currencies that an administrator has explicitly enabled are stored in +the ``EnabledCurrency`` table (tenant schema). Name, symbol, and description are computed at response time via babel. """ @@ -16,7 +16,7 @@ from babel.numbers import UnknownCurrencyError from rest_framework import serializers -from cost_models.models import CurrencyConfig +from cost_models.models import EnabledCurrency _ISO_4217_CURRENCIES = get_global("all_currencies") @@ -27,15 +27,7 @@ def get_enabled_currency_codes(): Requires tenant schema context (set by django-tenants middleware for requests or by ``schema_context()`` in tasks). """ - return set(CurrencyConfig.objects.filter(enabled=True).values_list("currency_code", flat=True)) - - -def get_all_currency_codes(): - """Return the set of all known currency codes (enabled or not). - - Requires tenant schema context. - """ - return set(CurrencyConfig.objects.values_list("currency_code", flat=True)) + return set(EnabledCurrency.objects.values_list("currency_code", flat=True)) class CurrencyField(serializers.CharField): @@ -53,12 +45,7 @@ def to_internal_value(self, data): def is_valid_iso_currency(code): - """Check whether *code* is a valid ISO 4217 currency using babel's registry. - - Unlike ``get_all_currency_codes`` this does NOT require the currency to - already exist in ``CurrencyConfig``, so it can be used to validate codes - *before* they are inserted (e.g. when creating static exchange rates). - """ + """Check whether *code* is a valid ISO 4217 currency using babel's registry.""" return code.upper() in _ISO_4217_CURRENCIES diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index 6dce8d47ff..d7a7528487 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -9,7 +9,7 @@ from rest_framework.test import APIClient from api.iam.test.iam_test_case import IamTestCase -from cost_models.models import CurrencyConfig +from cost_models.models import EnabledCurrency class CurrencyViewTest(IamTestCase): @@ -17,10 +17,9 @@ class CurrencyViewTest(IamTestCase): def setUp(self): super().setUp() - CurrencyConfig.objects.all().delete() - CurrencyConfig.objects.create(currency_code="USD", enabled=True) - CurrencyConfig.objects.create(currency_code="EUR", enabled=True) - CurrencyConfig.objects.create(currency_code="GBP", enabled=False) + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD") + EnabledCurrency.objects.create(currency_code="EUR") @patch( "api.currency.view.get_currency_info", @@ -45,11 +44,13 @@ def test_supported_currencies(self, _mock_display): ] self.assertEqual(data.get("data"), expected) - def test_disabled_currencies_excluded(self): - """Test that disabled currencies do not appear in the response.""" + def test_non_enabled_currencies_excluded(self): + """Test that currencies not in the EnabledCurrency table do not appear in the response.""" url = reverse("currency") + "?limit=25" client = APIClient() response = client.get(url, **self.headers) codes = [c["code"] for c in response.data["data"]] + self.assertIn("USD", codes) + self.assertIn("EUR", codes) self.assertNotIn("GBP", codes) diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index 7560871260..10218a4040 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -14,7 +14,7 @@ from api.common.pagination import ListPaginator from api.currency.currencies import get_currency_info from api.currency.models import ExchangeRateDictionary -from cost_models.models import CurrencyConfig +from cost_models.models import EnabledCurrency @api_view(("GET",)) @@ -24,10 +24,10 @@ def get_currency(request): """Get available currencies. Returns currencies that have been enabled by an administrator via - the CurrencyConfig table. Name, symbol, and description are + the EnabledCurrency table. Name, symbol, and description are computed at response time via babel. """ - enabled_codes = CurrencyConfig.objects.filter(enabled=True).values_list("currency_code", flat=True) + enabled_codes = EnabledCurrency.objects.values_list("currency_code", flat=True) available = [get_currency_info(code) for code in sorted(enabled_codes)] return ListPaginator(available, request).paginated_response diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 4513de30ad..f81ed7144b 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -5,6 +5,7 @@ """Views for currency enablement.""" import logging +from django.db import transaction from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from rest_framework import serializers @@ -15,47 +16,57 @@ from api.common import log_json from api.common.pagination import ListPaginator from api.common.permissions.settings_access import SettingsAccessPermission -from cost_models.models import CurrencyConfig +from api.currency.currencies import _ISO_4217_CURRENCIES +from api.currency.currencies import get_currency_info +from api.currency.currencies import is_valid_iso_currency +from cost_models.models import EnabledCurrency LOG = logging.getLogger(__name__) -class CurrencyConfigItemSerializer(serializers.Serializer): - currency_code = serializers.CharField(max_length=5) - enabled = serializers.BooleanField() +class EnabledCurrencySerializer(serializers.Serializer): + """Accepts a list of ISO 4217 currency codes to enable.""" + currencies = serializers.ListField(child=serializers.CharField(max_length=5), allow_empty=True) -class CurrencyConfigUpdateSerializer(serializers.Serializer): - currencies = CurrencyConfigItemSerializer(many=True) + def validate_currencies(self, value): + invalid = [code for code in value if not is_valid_iso_currency(code)] + if invalid: + raise serializers.ValidationError(f"Invalid ISO 4217 currency codes: {', '.join(invalid)}") + return [code.upper() for code in value] -class CurrencyConfigView(APIView): - """List and update enabled/disabled currencies for a tenant.""" +class EnabledCurrencyConfigView(APIView): + """Bulk-set enabled currencies for a tenant.""" permission_classes = [SettingsAccessPermission] @method_decorator(never_cache) - def get(self, request, *args, **kwargs): - currencies = CurrencyConfig.objects.all().values("currency_code", "enabled") - data = list(currencies) - paginator = ListPaginator(data, request) - return paginator.get_paginated_response(data) - - @method_decorator(never_cache) - def put(self, request, *args, **kwargs): - serializer = CurrencyConfigUpdateSerializer(data=request.data) + def post(self, request, *args, **kwargs): + serializer = EnabledCurrencySerializer(data=request.data) serializer.is_valid(raise_exception=True) - for item in serializer.validated_data["currencies"]: - CurrencyConfig.objects.update_or_create( - currency_code=item["currency_code"].upper(), - defaults={"enabled": item["enabled"]}, - ) - - LOG.info( - log_json( - msg="Currency configuration updated", - count=len(serializer.validated_data["currencies"]), - ) - ) + codes = serializer.validated_data["currencies"] + with transaction.atomic(): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.bulk_create([EnabledCurrency(currency_code=code) for code in codes]) + + LOG.info(log_json(msg="Enabled currencies updated", count=len(codes))) return Response(status=status.HTTP_204_NO_CONTENT) + + +class AllCurrencyView(APIView): + """List all ISO 4217 currencies with an enabled flag.""" + + permission_classes = [SettingsAccessPermission] + + @method_decorator(never_cache) + def get(self, request, *args, **kwargs): + enabled_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + result = [] + for code in sorted(_ISO_4217_CURRENCIES): + info = get_currency_info(code) + info["enabled"] = code in enabled_codes + result.append(info) + paginator = ListPaginator(result, request) + return paginator.get_paginated_response(result) diff --git a/koku/api/urls.py b/koku/api/urls.py index a5b1c79aba..3109cc59f1 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -9,7 +9,8 @@ from rest_framework.routers import DefaultRouter from api.common.deprecate_view import SunsetView -from api.settings.currency_views import CurrencyConfigView +from api.settings.currency_views import AllCurrencyView +from api.settings.currency_views import EnabledCurrencyConfigView from api.views import AccountSettings from api.views import AWSAccountRegionView from api.views import AWSAccountView @@ -427,9 +428,14 @@ ), path( "settings/currency/config/", - CurrencyConfigView.as_view(), + EnabledCurrencyConfigView.as_view(), name="currency-config", ), + path( + "settings/currency/", + AllCurrencyView.as_view(), + name="all-currencies", + ), path( "settings/currency/static-exchange-rates/", StaticExchangeRateViewSet.as_view({"get": "list", "post": "create"}), diff --git a/koku/cost_models/migrations/0013_staticexchangerate.py b/koku/cost_models/migrations/0013_staticexchangerate.py deleted file mode 100644 index e8ef4b648b..0000000000 --- a/koku/cost_models/migrations/0013_staticexchangerate.py +++ /dev/null @@ -1,39 +0,0 @@ -# -# Copyright 2026 Red Hat Inc. -# SPDX-License-Identifier: Apache-2.0 -# -import uuid - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [ - ("cost_models", "0012_add_rate_model"), - ] - - operations = [ - migrations.CreateModel( - name="StaticExchangeRate", - fields=[ - ( - "uuid", - models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), - ), - ("base_currency", models.CharField(max_length=5)), - ("target_currency", models.CharField(max_length=5)), - ("exchange_rate", models.DecimalField(decimal_places=15, max_digits=33)), - ("start_date", models.DateField()), - ("end_date", models.DateField()), - ("version", models.IntegerField(default=1)), - ("created_timestamp", models.DateTimeField(auto_now_add=True)), - ("updated_timestamp", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "static_exchange_rate", - "ordering": ["-updated_timestamp"], - }, - ), - ] diff --git a/koku/cost_models/migrations/0014_constant_currency.py b/koku/cost_models/migrations/0014_constant_currency.py new file mode 100644 index 0000000000..41c770dde4 --- /dev/null +++ b/koku/cost_models/migrations/0014_constant_currency.py @@ -0,0 +1,76 @@ +# +# Copyright 2026 Red Hat Inc. +# SPDX-License-Identifier: Apache-2.0 +# +"""Add StaticExchangeRate, EnabledCurrency, and MonthlyExchangeRate models for constant currency.""" +import uuid + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cost_models", "0013_normalize_rates_to_rate_table"), + ] + + operations = [ + migrations.CreateModel( + name="StaticExchangeRate", + fields=[ + ( + "uuid", + models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + ("base_currency", models.CharField(max_length=5)), + ("target_currency", models.CharField(max_length=5)), + ("exchange_rate", models.DecimalField(decimal_places=15, max_digits=33)), + ("start_date", models.DateField()), + ("end_date", models.DateField()), + ("version", models.IntegerField(default=1)), + ("created_timestamp", models.DateTimeField(auto_now_add=True)), + ("updated_timestamp", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "static_exchange_rate", + "ordering": ["-updated_timestamp"], + "unique_together": {("base_currency", "target_currency", "start_date", "end_date", "version")}, + }, + ), + migrations.CreateModel( + name="EnabledCurrency", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("currency_code", models.CharField(max_length=5, unique=True)), + ("created_timestamp", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "enabled_currency", + "ordering": ["currency_code"], + }, + ), + migrations.CreateModel( + name="MonthlyExchangeRate", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("effective_date", models.DateField()), + ("base_currency", models.CharField(max_length=5)), + ("target_currency", models.CharField(max_length=5)), + ("exchange_rate", models.DecimalField(decimal_places=15, max_digits=33)), + ("rate_type", models.CharField(choices=[("static", "Static"), ("dynamic", "Dynamic")], max_length=10)), + ("created_timestamp", models.DateTimeField(auto_now_add=True)), + ("updated_timestamp", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "monthly_exchange_rate", + "unique_together": {("effective_date", "base_currency", "target_currency")}, + }, + ), + ] diff --git a/koku/cost_models/migrations/0014_monthlyexchangerate.py b/koku/cost_models/migrations/0014_monthlyexchangerate.py deleted file mode 100644 index f3edd73e50..0000000000 --- a/koku/cost_models/migrations/0014_monthlyexchangerate.py +++ /dev/null @@ -1,68 +0,0 @@ -# -# Copyright 2026 Red Hat Inc. -# SPDX-License-Identifier: Apache-2.0 -# -from datetime import date - -from django.db import migrations -from django.db import models - - -def seed_current_month(apps, schema_editor): - """Seed MonthlyExchangeRate with current-month rates from ExchangeRateDictionary.""" - ExchangeRateDictionary = apps.get_model("api", "ExchangeRateDictionary") - MonthlyExchangeRate = apps.get_model("cost_models", "MonthlyExchangeRate") - - erd = ExchangeRateDictionary.objects.first() - if not erd or not erd.currency_exchange_dictionary: - return - - current_month = date.today().replace(day=1) - - rows = [] - for base_cur, targets in erd.currency_exchange_dictionary.items(): - for target_cur, rate in targets.items(): - if base_cur == target_cur: - continue - rows.append( - MonthlyExchangeRate( - effective_date=current_month, - base_currency=base_cur, - target_currency=target_cur, - exchange_rate=rate, - rate_type="dynamic", - ) - ) - MonthlyExchangeRate.objects.bulk_create(rows, ignore_conflicts=True) - - -class Migration(migrations.Migration): - - dependencies = [ - ("cost_models", "0013_staticexchangerate"), - ("api", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="MonthlyExchangeRate", - fields=[ - ( - "id", - models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), - ), - ("effective_date", models.DateField()), - ("base_currency", models.CharField(max_length=5)), - ("target_currency", models.CharField(max_length=5)), - ("exchange_rate", models.DecimalField(decimal_places=15, max_digits=33)), - ("rate_type", models.CharField(choices=[("static", "Static"), ("dynamic", "Dynamic")], max_length=10)), - ("created_timestamp", models.DateTimeField(auto_now_add=True)), - ("updated_timestamp", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "monthly_exchange_rate", - "unique_together": {("effective_date", "base_currency", "target_currency")}, - }, - ), - migrations.RunPython(seed_current_month, migrations.RunPython.noop), - ] diff --git a/koku/cost_models/migrations/0015_enabledcurrency.py b/koku/cost_models/migrations/0015_enabledcurrency.py deleted file mode 100644 index 57c22954c1..0000000000 --- a/koku/cost_models/migrations/0015_enabledcurrency.py +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright 2026 Red Hat Inc. -# SPDX-License-Identifier: Apache-2.0 -# -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [ - ("cost_models", "0014_monthlyexchangerate"), - ] - - operations = [ - migrations.CreateModel( - name="EnabledCurrency", - fields=[ - ( - "id", - models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), - ), - ("currency_code", models.CharField(max_length=5, unique=True)), - ("enabled", models.BooleanField(default=False)), - ("created_timestamp", models.DateTimeField(auto_now_add=True)), - ("updated_timestamp", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "enabled_currency", - "ordering": ["currency_code"], - }, - ), - ] diff --git a/koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py b/koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py deleted file mode 100644 index 633d2d6528..0000000000 --- a/koku/cost_models/migrations/0016_rename_enabledcurrency_to_currencyconfig.py +++ /dev/null @@ -1,23 +0,0 @@ -# -# Copyright 2026 Red Hat Inc. -# SPDX-License-Identifier: Apache-2.0 -# -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("cost_models", "0015_enabledcurrency"), - ] - - operations = [ - migrations.RenameModel( - old_name="EnabledCurrency", - new_name="CurrencyConfig", - ), - migrations.AlterModelTable( - name="currencyconfig", - table="currency_config", - ), - ] diff --git a/koku/cost_models/migrations/0017_merge_20260427_1058.py b/koku/cost_models/migrations/0017_merge_20260427_1058.py deleted file mode 100644 index 7692b4cfe5..0000000000 --- a/koku/cost_models/migrations/0017_merge_20260427_1058.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 5.2.13 on 2026-04-27 10:58 -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('cost_models', '0013_normalize_rates_to_rate_table'), - ('cost_models', '0016_rename_enabledcurrency_to_currencyconfig'), - ] - - operations = [ - ] diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index aa31adff73..7b053a0a76 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -227,17 +227,15 @@ def name(self): return f"{self.base_currency}-{self.target_currency}" -class CurrencyConfig(models.Model): - """Per-tenant currency configuration: tracks which currencies are visible in the target currency dropdown.""" +class EnabledCurrency(models.Model): + """Per-tenant enabled currencies: presence in this table means the currency is enabled.""" class Meta: - db_table = "currency_config" + db_table = "enabled_currency" ordering = ["currency_code"] currency_code = models.CharField(max_length=5, unique=True) - enabled = models.BooleanField(default=False) created_timestamp = models.DateTimeField(auto_now_add=True) - updated_timestamp = models.DateTimeField(auto_now=True) class MonthlyExchangeRate(models.Model): diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index 0b9d9043ef..f96890095b 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -2,51 +2,101 @@ # Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Tests for CurrencyConfig views.""" +"""Tests for EnabledCurrency views.""" from django.urls import reverse from django_tenants.utils import tenant_context from rest_framework import status from rest_framework.test import APIClient from api.iam.test.iam_test_case import IamTestCase -from cost_models.models import CurrencyConfig +from cost_models.models import EnabledCurrency -class CurrencyConfigViewTest(IamTestCase): - """Tests for CurrencyConfigView.""" +class EnabledCurrencyConfigViewTest(IamTestCase): + """Tests for EnabledCurrencyConfigView (POST settings/currency/config/).""" def setUp(self): super().setUp() self.client = APIClient() self.url = reverse("currency-config") - def test_get_enabled_currencies_empty(self): + def test_post_sets_enabled_currencies(self): with tenant_context(self.tenant): - CurrencyConfig.objects.all().delete() - response = self.client.get(self.url, **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) + EnabledCurrency.objects.all().delete() + data = {"currencies": ["USD", "EUR", "GBP"]} + response = self.client.post(self.url, data=data, format="json", **self.headers) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(EnabledCurrency.objects.count(), 3) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="EUR").exists()) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="GBP").exists()) - def test_get_enabled_currencies(self): + def test_post_replaces_existing(self): with tenant_context(self.tenant): - CurrencyConfig.objects.all().delete() - CurrencyConfig.objects.create(currency_code="USD", enabled=True) - CurrencyConfig.objects.create(currency_code="EUR", enabled=False) - response = self.client.get(self.url, **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="JPY") + data = {"currencies": ["USD"]} + response = self.client.post(self.url, data=data, format="json", **self.headers) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(EnabledCurrency.objects.count(), 1) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) + self.assertFalse(EnabledCurrency.objects.filter(currency_code="JPY").exists()) - def test_put_enable_currencies(self): + def test_post_empty_list_clears_all(self): with tenant_context(self.tenant): - CurrencyConfig.objects.all().delete() - CurrencyConfig.objects.create(currency_code="GBP", enabled=False) - data = {"currencies": [{"currency_code": "GBP", "enabled": True}]} - response = self.client.put(self.url, data=data, format="json", **self.headers) + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD") + data = {"currencies": []} + response = self.client.post(self.url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertTrue(CurrencyConfig.objects.get(currency_code="GBP").enabled) + self.assertEqual(EnabledCurrency.objects.count(), 0) - def test_put_creates_new_currency(self): + def test_post_invalid_currency_code(self): + data = {"currencies": ["INVALID"]} + response = self.client.post(self.url, data=data, format="json", **self.headers) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_post_normalizes_to_uppercase(self): with tenant_context(self.tenant): - CurrencyConfig.objects.all().delete() - data = {"currencies": [{"currency_code": "JPY", "enabled": True}]} - response = self.client.put(self.url, data=data, format="json", **self.headers) + EnabledCurrency.objects.all().delete() + data = {"currencies": ["usd", "eur"]} + response = self.client.post(self.url, data=data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertTrue(CurrencyConfig.objects.filter(currency_code="JPY", enabled=True).exists()) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="EUR").exists()) + + +class AllCurrencyViewTest(IamTestCase): + """Tests for AllCurrencyView (GET settings/currency/).""" + + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("all-currencies") + + def test_get_all_currencies_with_enabled_flag(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD") + EnabledCurrency.objects.create(currency_code="EUR") + + response = self.client.get(self.url + "?limit=500", **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.data["data"] + self.assertGreater(len(data), 100) + + usd = next(c for c in data if c["code"] == "USD") + eur = next(c for c in data if c["code"] == "EUR") + gbp = next(c for c in data if c["code"] == "GBP") + self.assertTrue(usd["enabled"]) + self.assertTrue(eur["enabled"]) + self.assertFalse(gbp["enabled"]) + + def test_get_all_currencies_none_enabled(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + + response = self.client.get(self.url + "?limit=500", **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.data["data"] + self.assertTrue(all(not c["enabled"] for c in data)) diff --git a/koku/cost_models/test/test_monthly_exchange_rate.py b/koku/cost_models/test/test_monthly_exchange_rate.py index 5c2484edfc..b004d153e5 100644 --- a/koku/cost_models/test/test_monthly_exchange_rate.py +++ b/koku/cost_models/test/test_monthly_exchange_rate.py @@ -2,14 +2,14 @@ # Copyright 2026 Red Hat Inc. # SPDX-License-Identifier: Apache-2.0 # -"""Tests for the MonthlyExchangeRate and CurrencyConfig models.""" +"""Tests for the MonthlyExchangeRate and EnabledCurrency models.""" from datetime import date from decimal import Decimal from django.db import IntegrityError from django_tenants.utils import tenant_context -from cost_models.models import CurrencyConfig +from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType from masu.test import MasuTestCase @@ -91,31 +91,20 @@ def test_update_or_create_overwrites_dynamic_with_static(self): self.assertEqual(rate.exchange_rate, Decimal("0.920000000000000")) -class CurrencyConfigTest(MasuTestCase): - """Tests for CurrencyConfig model.""" +class EnabledCurrencyTest(MasuTestCase): + """Tests for EnabledCurrency model.""" - def test_create_disabled_currency(self): - """Test creating a disabled currency entry.""" + def test_create_enabled_currency(self): + """Test creating an enabled currency entry.""" with tenant_context(self.tenant): - CurrencyConfig.objects.all().delete() - ec = CurrencyConfig.objects.create(currency_code="JPY", enabled=False) + EnabledCurrency.objects.all().delete() + ec = EnabledCurrency.objects.create(currency_code="JPY") self.assertEqual(ec.currency_code, "JPY") - self.assertFalse(ec.enabled) - - def test_enable_currency(self): - """Test enabling a currency.""" - with tenant_context(self.tenant): - CurrencyConfig.objects.all().delete() - ec = CurrencyConfig.objects.create(currency_code="GBP", enabled=False) - ec.enabled = True - ec.save() - ec.refresh_from_db() - self.assertTrue(ec.enabled) def test_unique_currency_code(self): """Test that duplicate currency codes raise IntegrityError.""" with tenant_context(self.tenant): - CurrencyConfig.objects.all().delete() - CurrencyConfig.objects.create(currency_code="CNY", enabled=False) + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="CNY") with self.assertRaises(IntegrityError): - CurrencyConfig.objects.create(currency_code="CNY", enabled=True) + EnabledCurrency.objects.create(currency_code="CNY") diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index 3d62961e38..7921b7eabe 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -26,7 +26,6 @@ from common.queues import DownloadQueue from common.queues import PriorityQueue from common.queues import SummaryQueue -from cost_models.models import CurrencyConfig from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType from koku import celery_app @@ -304,22 +303,8 @@ def _fetch_and_store_exchange_rates(url): def _upsert_tenant_dynamic_exchange_rates(schema_name, exchange_dict, current_month): - """Sync CurrencyConfig and upsert dynamic MonthlyExchangeRate rows for one tenant.""" + """Upsert dynamic MonthlyExchangeRate rows for one tenant.""" with schema_context(schema_name): - existing_codes = set(CurrencyConfig.objects.values_list("currency_code", flat=True)) - new_currencies = set(exchange_dict.keys()) - existing_codes - if new_currencies: - CurrencyConfig.objects.bulk_create( - [ - CurrencyConfig( - currency_code=code, - enabled=False, - ) - for code in new_currencies - ], - ignore_conflicts=True, - ) - static_pairs = set( MonthlyExchangeRate.objects.filter( effective_date=current_month, From c7a198cf0c271cd618168f327c884591ad6bc317 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 27 Apr 2026 17:37:57 +0300 Subject: [PATCH 056/106] COST-7252: Rename currency config endpoint to settings/currency/enabled/ Made-with: Cursor --- koku/api/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koku/api/urls.py b/koku/api/urls.py index 3109cc59f1..8e690fcfad 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -427,7 +427,7 @@ name="settings-aws-category-keys-disable", ), path( - "settings/currency/config/", + "settings/currency/enabled/", EnabledCurrencyConfigView.as_view(), name="currency-config", ), From 5c577acdc1277ed2045d4bc3c6a05f6211d17f1e Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 27 Apr 2026 17:42:17 +0300 Subject: [PATCH 057/106] COST-7252: Rename EnabledCurrencyConfigView to EnabledCurrencyView Made-with: Cursor --- koku/api/settings/currency_views.py | 2 +- koku/api/urls.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index f81ed7144b..4c440f8da8 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -36,7 +36,7 @@ def validate_currencies(self, value): return [code.upper() for code in value] -class EnabledCurrencyConfigView(APIView): +class EnabledCurrencyView(APIView): """Bulk-set enabled currencies for a tenant.""" permission_classes = [SettingsAccessPermission] diff --git a/koku/api/urls.py b/koku/api/urls.py index 8e690fcfad..5f85f298a0 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -10,7 +10,7 @@ from api.common.deprecate_view import SunsetView from api.settings.currency_views import AllCurrencyView -from api.settings.currency_views import EnabledCurrencyConfigView +from api.settings.currency_views import EnabledCurrencyView from api.views import AccountSettings from api.views import AWSAccountRegionView from api.views import AWSAccountView @@ -428,7 +428,7 @@ ), path( "settings/currency/enabled/", - EnabledCurrencyConfigView.as_view(), + EnabledCurrencyView.as_view(), name="currency-config", ), path( From 14a64cb8dc73bf4abbcdfb2f9b2e3fbd7d978e28 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 27 Apr 2026 17:51:29 +0300 Subject: [PATCH 058/106] COST-7252: Move EnabledCurrencySerializer to settings/serializers.py Made-with: Cursor --- koku/api/settings/currency_views.py | 15 +-------------- koku/api/settings/serializers.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 4c440f8da8..74dae56f9a 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -8,7 +8,6 @@ from django.db import transaction from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache -from rest_framework import serializers from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView @@ -18,24 +17,12 @@ from api.common.permissions.settings_access import SettingsAccessPermission from api.currency.currencies import _ISO_4217_CURRENCIES from api.currency.currencies import get_currency_info -from api.currency.currencies import is_valid_iso_currency +from api.settings.serializers import EnabledCurrencySerializer from cost_models.models import EnabledCurrency LOG = logging.getLogger(__name__) -class EnabledCurrencySerializer(serializers.Serializer): - """Accepts a list of ISO 4217 currency codes to enable.""" - - currencies = serializers.ListField(child=serializers.CharField(max_length=5), allow_empty=True) - - def validate_currencies(self, value): - invalid = [code for code in value if not is_valid_iso_currency(code)] - if invalid: - raise serializers.ValidationError(f"Invalid ISO 4217 currency codes: {', '.join(invalid)}") - return [code.upper() for code in value] - - class EnabledCurrencyView(APIView): """Bulk-set enabled currencies for a tenant.""" diff --git a/koku/api/settings/serializers.py b/koku/api/settings/serializers.py index f051b47a44..acc73de11a 100644 --- a/koku/api/settings/serializers.py +++ b/koku/api/settings/serializers.py @@ -6,6 +6,7 @@ from rest_framework import serializers from api.currency.currencies import CurrencyField +from api.currency.currencies import is_valid_iso_currency from api.settings.settings import COST_TYPE_CHOICES from reporting.tenant_settings.models import TenantSettings from reporting.user_settings.models import UserSettings @@ -39,3 +40,15 @@ class TenantSettingsSerializer(serializers.Serializer): min_value=TenantSettings.MIN_RETENTION_MONTHS, max_value=TenantSettings.MAX_RETENTION_MONTHS, ) + + +class EnabledCurrencySerializer(serializers.Serializer): + """Accepts a list of ISO 4217 currency codes to enable.""" + + currencies = serializers.ListField(child=serializers.CharField(max_length=5), allow_empty=True) + + def validate_currencies(self, value): + invalid = [code for code in value if not is_valid_iso_currency(code)] + if invalid: + raise serializers.ValidationError(f"Invalid ISO 4217 currency codes: {', '.join(invalid)}") + return [code.upper() for code in value] From e80100c85a19f81e7a204021c274acdf8617a07b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 13:34:19 +0300 Subject: [PATCH 059/106] COST-7252: Replace bulk currency enablement with single-currency toggle Change the EnabledCurrency API from a bulk-replace POST to a per-currency PUT at settings/currency//. The request body is {"enabled": true/false} and the currency code in the URL is validated against ISO 4217 and normalized to uppercase. Made-with: Cursor --- docs/architecture/api-settings-endpoints.md | 78 +++++++++++++++++++ koku/api/settings/currency_views.py | 21 +++-- koku/api/settings/serializers.py | 11 +-- koku/api/urls.py | 10 +-- .../cost_models/test/test_enabled_currency.py | 61 ++++++++------- 5 files changed, 133 insertions(+), 48 deletions(-) diff --git a/docs/architecture/api-settings-endpoints.md b/docs/architecture/api-settings-endpoints.md index a08ac23034..7a7b4c31b3 100644 --- a/docs/architecture/api-settings-endpoints.md +++ b/docs/architecture/api-settings-endpoints.md @@ -516,6 +516,84 @@ PUT /settings/aws_category_keys/disable/ --- +## Currency Enablement + +### **Purpose** +Control which ISO 4217 currencies are available for selection across the tenant. Only enabled currencies can be used in account settings, cost models, and report filters. + +### **Endpoints** + +#### List All Currencies +``` +GET /settings/currency/ +``` + +Returns every ISO 4217 currency with an `enabled` flag indicating whether the tenant has enabled it. + +**Query Parameters:** +- `limit` (integer) - Results per page +- `offset` (integer) - Pagination offset + +**Response:** +```json +{ + "meta": { + "count": 160 + }, + "data": [ + { + "code": "USD", + "name": "US Dollar", + "symbol": "$", + "description": "USD ($) - US Dollar", + "enabled": true + }, + { + "code": "EUR", + "name": "Euro", + "symbol": "€", + "description": "EUR (€) - Euro", + "enabled": false + } + ] +} +``` + +#### Toggle a Currency +``` +PUT /settings/currency// +``` + +Enable or disable a single currency by its ISO 4217 code in the URL path. + +**Path Parameters:** +- `code` (string) - ISO 4217 currency code (case-insensitive, normalized to uppercase) + +**Request Body:** +```json +{ + "enabled": true +} +``` + +**Response:** `204 No Content` + +**Error Responses:** + +**Invalid Currency Code (400 Bad Request):** +```json +{ + "error": "Invalid ISO 4217 currency code: INVALID" +} +``` + +**Behavior:** +- `enabled: true` — idempotently creates an `EnabledCurrency` row for the code +- `enabled: false` — idempotently deletes the `EnabledCurrency` row if it exists +- Currency code in the URL is case-insensitive (`/settings/currency/usd/` enables `USD`) + +--- + ## Cost Groups (OpenShift) ### **Purpose** diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 74dae56f9a..73351d9e66 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -5,7 +5,6 @@ """Views for currency enablement.""" import logging -from django.db import transaction from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from rest_framework import status @@ -17,6 +16,7 @@ from api.common.permissions.settings_access import SettingsAccessPermission from api.currency.currencies import _ISO_4217_CURRENCIES from api.currency.currencies import get_currency_info +from api.currency.currencies import is_valid_iso_currency from api.settings.serializers import EnabledCurrencySerializer from cost_models.models import EnabledCurrency @@ -24,21 +24,26 @@ class EnabledCurrencyView(APIView): - """Bulk-set enabled currencies for a tenant.""" + """Toggle a single currency's enabled state for a tenant.""" permission_classes = [SettingsAccessPermission] @method_decorator(never_cache) - def post(self, request, *args, **kwargs): + def put(self, request, *args, **kwargs): + code = kwargs["code"].upper() + if not is_valid_iso_currency(code): + return Response({"code": [f"Invalid ISO 4217 currency code: {code}"]}, status=status.HTTP_400_BAD_REQUEST) + serializer = EnabledCurrencySerializer(data=request.data) serializer.is_valid(raise_exception=True) - codes = serializer.validated_data["currencies"] - with transaction.atomic(): - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.bulk_create([EnabledCurrency(currency_code=code) for code in codes]) + if serializer.validated_data["enabled"]: + EnabledCurrency.objects.get_or_create(currency_code=code) + LOG.info(log_json(msg="Currency enabled", currency=code)) + else: + EnabledCurrency.objects.filter(currency_code=code).delete() + LOG.info(log_json(msg="Currency disabled", currency=code)) - LOG.info(log_json(msg="Enabled currencies updated", count=len(codes))) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/koku/api/settings/serializers.py b/koku/api/settings/serializers.py index acc73de11a..710e29c486 100644 --- a/koku/api/settings/serializers.py +++ b/koku/api/settings/serializers.py @@ -6,7 +6,6 @@ from rest_framework import serializers from api.currency.currencies import CurrencyField -from api.currency.currencies import is_valid_iso_currency from api.settings.settings import COST_TYPE_CHOICES from reporting.tenant_settings.models import TenantSettings from reporting.user_settings.models import UserSettings @@ -43,12 +42,6 @@ class TenantSettingsSerializer(serializers.Serializer): class EnabledCurrencySerializer(serializers.Serializer): - """Accepts a list of ISO 4217 currency codes to enable.""" + """Accepts an enabled flag for a single currency (code comes from the URL path).""" - currencies = serializers.ListField(child=serializers.CharField(max_length=5), allow_empty=True) - - def validate_currencies(self, value): - invalid = [code for code in value if not is_valid_iso_currency(code)] - if invalid: - raise serializers.ValidationError(f"Invalid ISO 4217 currency codes: {', '.join(invalid)}") - return [code.upper() for code in value] + enabled = serializers.BooleanField() diff --git a/koku/api/urls.py b/koku/api/urls.py index 5f85f298a0..dfb73f373e 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -426,11 +426,6 @@ SettingsDisableAWSCategoryKeyView.as_view(), name="settings-aws-category-keys-disable", ), - path( - "settings/currency/enabled/", - EnabledCurrencyView.as_view(), - name="currency-config", - ), path( "settings/currency/", AllCurrencyView.as_view(), @@ -446,6 +441,11 @@ StaticExchangeRateViewSet.as_view({"get": "retrieve", "put": "update", "delete": "destroy"}), name="static-exchange-rates-detail", ), + path( + "settings/currency//", + EnabledCurrencyView.as_view(), + name="currency-config", + ), path("settings/tags/", SettingsTagView.as_view(), name="settings-tags"), path("settings/tags/enable/", SettingsEnableTagView.as_view(), name="tags-enable"), path("settings/tags/disable/", SettingsDisableTagView.as_view(), name="tags-disable"), diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index f96890095b..9ae10c27c9 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -13,57 +13,66 @@ class EnabledCurrencyConfigViewTest(IamTestCase): - """Tests for EnabledCurrencyConfigView (POST settings/currency/config/).""" + """Tests for EnabledCurrencyView (PUT settings/currency//).""" def setUp(self): super().setUp() self.client = APIClient() - self.url = reverse("currency-config") - def test_post_sets_enabled_currencies(self): + def _url(self, code): + return reverse("currency-config", kwargs={"code": code}) + + def test_put_enables_currency(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - data = {"currencies": ["USD", "EUR", "GBP"]} - response = self.client.post(self.url, data=data, format="json", **self.headers) + response = self.client.put(self._url("USD"), data={"enabled": True}, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(EnabledCurrency.objects.count(), 3) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) - self.assertTrue(EnabledCurrency.objects.filter(currency_code="EUR").exists()) - self.assertTrue(EnabledCurrency.objects.filter(currency_code="GBP").exists()) - def test_post_replaces_existing(self): + def test_put_disables_currency(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="JPY") - data = {"currencies": ["USD"]} - response = self.client.post(self.url, data=data, format="json", **self.headers) + EnabledCurrency.objects.create(currency_code="USD") + response = self.client.put(self._url("USD"), data={"enabled": False}, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(EnabledCurrency.objects.count(), 1) - self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) - self.assertFalse(EnabledCurrency.objects.filter(currency_code="JPY").exists()) + self.assertFalse(EnabledCurrency.objects.filter(currency_code="USD").exists()) - def test_post_empty_list_clears_all(self): + def test_put_enable_is_idempotent(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() EnabledCurrency.objects.create(currency_code="USD") - data = {"currencies": []} - response = self.client.post(self.url, data=data, format="json", **self.headers) + response = self.client.put(self._url("USD"), data={"enabled": True}, format="json", **self.headers) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(EnabledCurrency.objects.filter(currency_code="USD").count(), 1) + + def test_put_disable_is_idempotent(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + response = self.client.put(self._url("USD"), data={"enabled": False}, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(EnabledCurrency.objects.count(), 0) + self.assertFalse(EnabledCurrency.objects.filter(currency_code="USD").exists()) - def test_post_invalid_currency_code(self): - data = {"currencies": ["INVALID"]} - response = self.client.post(self.url, data=data, format="json", **self.headers) + def test_put_does_not_affect_other_currencies(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="EUR") + EnabledCurrency.objects.create(currency_code="GBP") + response = self.client.put(self._url("USD"), data={"enabled": True}, format="json", **self.headers) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="EUR").exists()) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="GBP").exists()) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) + + def test_put_invalid_currency_code(self): + response = self.client.put(self._url("INVALID"), data={"enabled": True}, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - def test_post_normalizes_to_uppercase(self): + def test_put_normalizes_to_uppercase(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - data = {"currencies": ["usd", "eur"]} - response = self.client.post(self.url, data=data, format="json", **self.headers) + response = self.client.put(self._url("usd"), data={"enabled": True}, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) - self.assertTrue(EnabledCurrency.objects.filter(currency_code="EUR").exists()) class AllCurrencyViewTest(IamTestCase): From 61c340436f4fc6f9549500ca019ee78982cd4850 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 15:08:51 +0300 Subject: [PATCH 060/106] COST-7252: Consolidate currency endpoints under settings/currency/exchange_rate/ - Merge AllCurrencyView and static-exchange-rates into a single exchange_rate endpoint that returns currencies grouped by target currency with enabled status and nested exchange rates - Move enable/disable to POST/DELETE on exchange_rate//enable/ (no request body needed) - Remove redundant GET retrieve on detail endpoint (data already in list response) - Remove AllCurrencyView, EnabledCurrencySerializer (no longer needed) Made-with: Cursor --- .../constant-currency/api-and-frontend.md | 4 +- koku/api/settings/currency_views.py | 59 +++++++---------- koku/api/settings/serializers.py | 6 -- koku/api/urls.py | 18 ++--- .../static_exchange_rate_serializer.py | 38 +++++++++++ koku/cost_models/static_exchange_rate_view.py | 10 +++ .../cost_models/test/test_enabled_currency.py | 66 +++++-------------- .../test/test_static_exchange_rate_view.py | 47 +++++++++---- 8 files changed, 130 insertions(+), 118 deletions(-) diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 2a9f77febb..a07697e2cc 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -149,8 +149,8 @@ allows an administrator to enable or disable currencies. ### View -**File**: `koku/api/settings/currency_views.py` — `AllCurrencyView` (GET) and -`EnabledCurrencyConfigView` (POST). +**File**: `koku/api/settings/currency_views.py` — `EnabledCurrencyView` (PUT to toggle enabled/disabled). +**File**: `koku/cost_models/static_exchange_rate_view.py` — `StaticExchangeRateViewSet` (GET list returns currencies grouped with exchange rates and enabled status). **Permission**: Cost Management Administrator role (same permission level as other Settings operations). diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 73351d9e66..7a9343e517 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -12,53 +12,44 @@ from rest_framework.views import APIView from api.common import log_json -from api.common.pagination import ListPaginator from api.common.permissions.settings_access import SettingsAccessPermission -from api.currency.currencies import _ISO_4217_CURRENCIES -from api.currency.currencies import get_currency_info from api.currency.currencies import is_valid_iso_currency -from api.settings.serializers import EnabledCurrencySerializer from cost_models.models import EnabledCurrency LOG = logging.getLogger(__name__) class EnabledCurrencyView(APIView): - """Toggle a single currency's enabled state for a tenant.""" + """Enable or disable a single currency for a tenant. + + POST enables the currency; DELETE disables it. No request body required. + """ permission_classes = [SettingsAccessPermission] - @method_decorator(never_cache) - def put(self, request, *args, **kwargs): - code = kwargs["code"].upper() + def _validate_code(self, code): + code = code.upper() if not is_valid_iso_currency(code): - return Response({"code": [f"Invalid ISO 4217 currency code: {code}"]}, status=status.HTTP_400_BAD_REQUEST) - - serializer = EnabledCurrencySerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - if serializer.validated_data["enabled"]: - EnabledCurrency.objects.get_or_create(currency_code=code) - LOG.info(log_json(msg="Currency enabled", currency=code)) - else: - EnabledCurrency.objects.filter(currency_code=code).delete() - LOG.info(log_json(msg="Currency disabled", currency=code)) + return None, Response( + {"code": [f"Invalid ISO 4217 currency code: {code}"]}, + status=status.HTTP_400_BAD_REQUEST, + ) + return code, None + @method_decorator(never_cache) + def post(self, request, *args, **kwargs): + code, error = self._validate_code(kwargs["code"]) + if error: + return error + EnabledCurrency.objects.get_or_create(currency_code=code) + LOG.info(log_json(msg="Currency enabled", currency=code)) return Response(status=status.HTTP_204_NO_CONTENT) - -class AllCurrencyView(APIView): - """List all ISO 4217 currencies with an enabled flag.""" - - permission_classes = [SettingsAccessPermission] - @method_decorator(never_cache) - def get(self, request, *args, **kwargs): - enabled_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) - result = [] - for code in sorted(_ISO_4217_CURRENCIES): - info = get_currency_info(code) - info["enabled"] = code in enabled_codes - result.append(info) - paginator = ListPaginator(result, request) - return paginator.get_paginated_response(result) + def delete(self, request, *args, **kwargs): + code, error = self._validate_code(kwargs["code"]) + if error: + return error + EnabledCurrency.objects.filter(currency_code=code).delete() + LOG.info(log_json(msg="Currency disabled", currency=code)) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/koku/api/settings/serializers.py b/koku/api/settings/serializers.py index 710e29c486..f051b47a44 100644 --- a/koku/api/settings/serializers.py +++ b/koku/api/settings/serializers.py @@ -39,9 +39,3 @@ class TenantSettingsSerializer(serializers.Serializer): min_value=TenantSettings.MIN_RETENTION_MONTHS, max_value=TenantSettings.MAX_RETENTION_MONTHS, ) - - -class EnabledCurrencySerializer(serializers.Serializer): - """Accepts an enabled flag for a single currency (code comes from the URL path).""" - - enabled = serializers.BooleanField() diff --git a/koku/api/urls.py b/koku/api/urls.py index dfb73f373e..ed774d2dc5 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -9,7 +9,6 @@ from rest_framework.routers import DefaultRouter from api.common.deprecate_view import SunsetView -from api.settings.currency_views import AllCurrencyView from api.settings.currency_views import EnabledCurrencyView from api.views import AccountSettings from api.views import AWSAccountRegionView @@ -427,22 +426,17 @@ name="settings-aws-category-keys-disable", ), path( - "settings/currency/", - AllCurrencyView.as_view(), - name="all-currencies", - ), - path( - "settings/currency/static-exchange-rates/", + "settings/currency/exchange_rate/", StaticExchangeRateViewSet.as_view({"get": "list", "post": "create"}), - name="static-exchange-rates-list", + name="exchange-rate-list", ), path( - "settings/currency/static-exchange-rates//", - StaticExchangeRateViewSet.as_view({"get": "retrieve", "put": "update", "delete": "destroy"}), - name="static-exchange-rates-detail", + "settings/currency/exchange_rate//", + StaticExchangeRateViewSet.as_view({"put": "update", "delete": "destroy"}), + name="exchange-rate-detail", ), path( - "settings/currency//", + "settings/currency/exchange_rate//enable/", EnabledCurrencyView.as_view(), name="currency-config", ), diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index f59bb58431..50ffe936e6 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -10,7 +10,9 @@ from rest_framework import serializers from api.common import log_json +from api.currency.currencies import get_currency_info from api.currency.currencies import is_valid_iso_currency +from cost_models.models import EnabledCurrency from cost_models.models import StaticExchangeRate from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic from cost_models.static_exchange_rate_utils import upsert_static_monthly_rates @@ -132,3 +134,39 @@ def update(self, instance, validated_data): ) ) return instance + + +class CurrencyExchangeRateSerializer(serializers.Serializer): + """Read-only serializer for a currency grouped with its static exchange rates.""" + + code = serializers.CharField() + name = serializers.CharField() + symbol = serializers.CharField() + enabled = serializers.BooleanField() + exchange_rates = StaticExchangeRateSerializer(many=True) + + @classmethod + def build_grouped_response(cls, queryset): + """Group exchange rates by target_currency and attach currency metadata + enabled flag.""" + enabled_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + + grouped = {} + for rate in queryset: + code = rate.target_currency + if code not in grouped: + info = get_currency_info(code) + grouped[code] = { + "code": info["code"], + "name": info["name"], + "symbol": info["symbol"], + "enabled": code in enabled_codes, + "exchange_rates": [], + } + grouped[code]["exchange_rates"].append(rate) + + result = [] + for code in sorted(grouped): + entry = grouped[code] + entry["exchange_rates"] = StaticExchangeRateSerializer(entry["exchange_rates"], many=True).data + result.append(entry) + return result diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index 645b3fb894..1688df74cd 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -11,10 +11,13 @@ from django_filters import FilterSet from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets +from rest_framework.response import Response from api.common import log_json +from api.common.pagination import ListPaginator from api.common.permissions.cost_models_access import CostModelsAccessPermission from cost_models.models import StaticExchangeRate +from cost_models.static_exchange_rate_serializer import CurrencyExchangeRateSerializer from cost_models.static_exchange_rate_serializer import StaticExchangeRateSerializer from cost_models.static_exchange_rate_utils import remove_static_and_backfill_dynamic from koku.cache import invalidate_view_cache_for_tenant_and_all_source_types @@ -46,6 +49,13 @@ class StaticExchangeRateViewSet(viewsets.ModelViewSet): filter_backends = (DjangoFilterBackend,) filterset_class = StaticExchangeRateFilter + def list(self, request, *args, **kwargs): + """Return exchange rates grouped by target currency with enabled status.""" + queryset = self.filter_queryset(self.get_queryset()) + result = CurrencyExchangeRateSerializer.build_grouped_response(queryset) + paginator = ListPaginator(result, request) + return paginator.get_paginated_response(result) + @transaction.atomic def perform_destroy(self, instance): remove_static_and_backfill_dynamic( diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index 9ae10c27c9..c7667ae5ff 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -13,7 +13,7 @@ class EnabledCurrencyConfigViewTest(IamTestCase): - """Tests for EnabledCurrencyView (PUT settings/currency//).""" + """Tests for EnabledCurrencyView (POST/DELETE settings/currency/exchange_rate//enable/).""" def setUp(self): super().setUp() @@ -22,90 +22,54 @@ def setUp(self): def _url(self, code): return reverse("currency-config", kwargs={"code": code}) - def test_put_enables_currency(self): + def test_post_enables_currency(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - response = self.client.put(self._url("USD"), data={"enabled": True}, format="json", **self.headers) + response = self.client.post(self._url("USD"), **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) - def test_put_disables_currency(self): + def test_delete_disables_currency(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() EnabledCurrency.objects.create(currency_code="USD") - response = self.client.put(self._url("USD"), data={"enabled": False}, format="json", **self.headers) + response = self.client.delete(self._url("USD"), **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse(EnabledCurrency.objects.filter(currency_code="USD").exists()) - def test_put_enable_is_idempotent(self): + def test_post_enable_is_idempotent(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() EnabledCurrency.objects.create(currency_code="USD") - response = self.client.put(self._url("USD"), data={"enabled": True}, format="json", **self.headers) + response = self.client.post(self._url("USD"), **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(EnabledCurrency.objects.filter(currency_code="USD").count(), 1) - def test_put_disable_is_idempotent(self): + def test_delete_disable_is_idempotent(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - response = self.client.put(self._url("USD"), data={"enabled": False}, format="json", **self.headers) + response = self.client.delete(self._url("USD"), **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse(EnabledCurrency.objects.filter(currency_code="USD").exists()) - def test_put_does_not_affect_other_currencies(self): + def test_post_does_not_affect_other_currencies(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() EnabledCurrency.objects.create(currency_code="EUR") EnabledCurrency.objects.create(currency_code="GBP") - response = self.client.put(self._url("USD"), data={"enabled": True}, format="json", **self.headers) + response = self.client.post(self._url("USD"), **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.filter(currency_code="EUR").exists()) self.assertTrue(EnabledCurrency.objects.filter(currency_code="GBP").exists()) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) - def test_put_invalid_currency_code(self): - response = self.client.put(self._url("INVALID"), data={"enabled": True}, format="json", **self.headers) + def test_post_invalid_currency_code(self): + response = self.client.post(self._url("INVALID"), **self.headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - def test_put_normalizes_to_uppercase(self): + def test_post_normalizes_to_uppercase(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - response = self.client.put(self._url("usd"), data={"enabled": True}, format="json", **self.headers) + response = self.client.post(self._url("usd"), **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) - - -class AllCurrencyViewTest(IamTestCase): - """Tests for AllCurrencyView (GET settings/currency/).""" - - def setUp(self): - super().setUp() - self.client = APIClient() - self.url = reverse("all-currencies") - - def test_get_all_currencies_with_enabled_flag(self): - with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="USD") - EnabledCurrency.objects.create(currency_code="EUR") - - response = self.client.get(self.url + "?limit=500", **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - data = response.data["data"] - self.assertGreater(len(data), 100) - - usd = next(c for c in data if c["code"] == "USD") - eur = next(c for c in data if c["code"] == "EUR") - gbp = next(c for c in data if c["code"] == "GBP") - self.assertTrue(usd["enabled"]) - self.assertTrue(eur["enabled"]) - self.assertFalse(gbp["enabled"]) - - def test_get_all_currencies_none_enabled(self): - with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - - response = self.client.get(self.url + "?limit=500", **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - data = response.data["data"] - self.assertTrue(all(not c["enabled"] for c in data)) diff --git a/koku/cost_models/test/test_static_exchange_rate_view.py b/koku/cost_models/test/test_static_exchange_rate_view.py index d5c3fe36e7..07a2028809 100644 --- a/koku/cost_models/test/test_static_exchange_rate_view.py +++ b/koku/cost_models/test/test_static_exchange_rate_view.py @@ -11,6 +11,7 @@ from rest_framework.test import APIClient from api.iam.test.iam_test_case import IamTestCase +from cost_models.models import EnabledCurrency from cost_models.models import StaticExchangeRate @@ -20,7 +21,7 @@ class StaticExchangeRateViewSetTest(IamTestCase): def setUp(self): super().setUp() self.client = APIClient() - self.list_url = reverse("static-exchange-rates-list") + self.list_url = reverse("exchange-rate-list") self.valid_data = { "base_currency": "USD", "target_currency": "EUR", @@ -42,24 +43,44 @@ def test_create_static_rate(self, mock_invalidate): self.assertEqual(data["version"], 1) @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") - def test_list_static_rates(self, mock_invalidate): - """Test listing static exchange rates.""" + def test_list_returns_grouped_by_currency(self, mock_invalidate): + """Test that GET list returns exchange rates grouped by target currency with enabled flag.""" with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="EUR") self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) + response = self.client.get(self.list_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertGreaterEqual(len(response.data["data"]), 1) + + data = response.data["data"] + self.assertEqual(len(data), 1) + + eur_entry = data[0] + self.assertEqual(eur_entry["code"], "EUR") + self.assertEqual(eur_entry["enabled"], True) + self.assertIn("name", eur_entry) + self.assertIn("symbol", eur_entry) + self.assertEqual(len(eur_entry["exchange_rates"]), 1) + + rate = eur_entry["exchange_rates"][0] + self.assertEqual(rate["base_currency"], "USD") + self.assertEqual(rate["target_currency"], "EUR") + self.assertIn("uuid", rate) @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") - def test_retrieve_static_rate(self, mock_invalidate): - """Test retrieving a single static exchange rate.""" + def test_list_disabled_currency_shows_enabled_false(self, mock_invalidate): + """Test that a currency without EnabledCurrency row shows enabled=False.""" with tenant_context(self.tenant): - create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) - uuid = create_response.data["uuid"] - detail_url = reverse("static-exchange-rates-detail", kwargs={"uuid": uuid}) - response = self.client.get(detail_url, **self.headers) + EnabledCurrency.objects.filter(currency_code="EUR").delete() + self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) + + response = self.client.get(self.list_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["uuid"], uuid) + + eur_entry = response.data["data"][0] + self.assertEqual(eur_entry["code"], "EUR") + self.assertEqual(eur_entry["enabled"], False) @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") def test_update_static_rate(self, mock_invalidate): @@ -67,7 +88,7 @@ def test_update_static_rate(self, mock_invalidate): with tenant_context(self.tenant): create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] - detail_url = reverse("static-exchange-rates-detail", kwargs={"uuid": uuid}) + detail_url = reverse("exchange-rate-detail", kwargs={"uuid": uuid}) update_data = self.valid_data.copy() update_data["exchange_rate"] = "0.900000000000000" @@ -81,7 +102,7 @@ def test_delete_static_rate(self, mock_invalidate): with tenant_context(self.tenant): create_response = self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) uuid = create_response.data["uuid"] - detail_url = reverse("static-exchange-rates-detail", kwargs={"uuid": uuid}) + detail_url = reverse("exchange-rate-detail", kwargs={"uuid": uuid}) response = self.client.delete(detail_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse(StaticExchangeRate.objects.filter(uuid=uuid).exists()) From c416fb27ed0cf224d7ac764495a8be032a4f98e8 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 15:18:14 +0300 Subject: [PATCH 061/106] COST-7252: Sync constant-currency architecture docs with new URL structure Update all docs to reflect consolidated exchange_rate endpoint, POST/DELETE currency enablement, and grouped list response format. Made-with: Cursor --- docs/architecture/constant-currency/README.md | 3 +- .../constant-currency/api-and-frontend.md | 212 ++++++++---------- .../constant-currency/data-model.md | 6 +- .../constant-currency/phased-delivery.md | 17 +- .../constant-currency/risk-register.md | 3 +- 5 files changed, 110 insertions(+), 131 deletions(-) diff --git a/docs/architecture/constant-currency/README.md b/docs/architecture/constant-currency/README.md index 2e35484d97..396964ffd6 100644 --- a/docs/architecture/constant-currency/README.md +++ b/docs/architecture/constant-currency/README.md @@ -129,7 +129,7 @@ immediately available for use, or should an administrator explicitly enable them **Resolution**: Explicit enablement. The full list of known currencies comes from Babel's ISO 4217 registry. Only currencies that an administrator has explicitly enabled are stored in the `EnabledCurrency` table. An administrator must enable -currencies through the Settings API (`POST settings/currency/config/`) before +currencies through the Settings API (`POST settings/currency/exchange_rate/{code}/enable/`) before they appear in the target currency dropdown. All currencies are always stored in `MonthlyExchangeRate` regardless of their @@ -313,3 +313,4 @@ graph LR | v1.6 | 2026-03-30 | Removed `ExchangeRateDictionary` fallback from query handler. M2 seeds current-month data. Decision #9 updated. | | v1.7 | 2026-04-12 | Updated data flow diagram: query handler uses `Subquery` annotation instead of `Case`/`When`. | | v1.8 | 2026-04-13 | Synced pre-deployment month references: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). | +| v1.9 | 2026-04-28 | Updated currency enablement URL reference to `settings/currency/exchange_rate/{code}/enable/`. | diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index a07697e2cc..917199bad5 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -7,25 +7,22 @@ OpenAPI updates. --- -## New CRUD Endpoint: Exchange Rate Pairs +## Exchange Rate Endpoint ### URL ``` -GET/POST /api/cost-management/v1/exchange-rate-pairs/ -GET/PUT/DELETE /api/cost-management/v1/exchange-rate-pairs/{uuid}/ +GET/POST /api/cost-management/v1/settings/currency/exchange_rate/ +PUT/DELETE /api/cost-management/v1/settings/currency/exchange_rate/{uuid}/ +POST/DELETE /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/ ``` ### Registration -**File**: `koku/cost_models/urls.py` +**File**: `koku/api/urls.py` -Register `StaticExchangeRateViewSet` on the existing `DefaultRouter` as -`"exchange-rate-pairs"`. - -```python -router.register(r"exchange-rate-pairs", StaticExchangeRateViewSet, basename="exchange-rate-pairs") -``` +Registered as explicit `path()` entries mapping to `StaticExchangeRateViewSet` +and `EnabledCurrencyView`. ### Query Parameters @@ -38,22 +35,24 @@ router.register(r"exchange-rate-pairs", StaticExchangeRateViewSet, basename="exc ### View -**File**: New `koku/cost_models/static_exchange_rate_view.py` +**File**: `koku/cost_models/static_exchange_rate_view.py` -```python -class StaticExchangeRateViewSet(viewsets.ModelViewSet): - queryset = StaticExchangeRate.objects.all() - serializer_class = StaticExchangeRateSerializer - lookup_field = "uuid" - permission_classes = (CostModelsAccessPermission,) -``` - -Follows the pattern from `CostModelViewSet` in `koku/cost_models/view.py`. -All operations run under tenant context (handled by `django-tenants` middleware). +The `StaticExchangeRateViewSet` handles CRUD for exchange rates. The `list` +action returns exchange rates grouped by target currency with enabled status +(via `CurrencyExchangeRateSerializer`). All other actions use the flat +`StaticExchangeRateSerializer`. **Permission**: `CostModelsAccessPermission` — requires the **Price List Administrator** role. Same permission used for cost model CRUD. +**File**: `koku/api/settings/currency_views.py` + +The `EnabledCurrencyView` handles currency enablement via POST (enable) and +DELETE (disable). No request body required. + +**Permission**: `SettingsAccessPermission` — requires the **Cost Management +Administrator** role. + ### Serializer **File**: New `koku/cost_models/static_exchange_rate_serializer.py` @@ -84,35 +83,55 @@ together with the `StaticExchangeRate` write. If any side effect fails, the proactively populates `rate_type=RateType.DYNAMIC` rows from the current `ExchangeRateDictionary` to avoid a data gap until the next daily Celery run -### Example: List Response +### Example: GET List Response + +The list endpoint returns exchange rates grouped by target currency. Each +currency entry includes its enabled status and a nested list of exchange rates. +Only currencies with at least one `StaticExchangeRate` record appear. ```json { "meta": { "count": 2 }, "data": [ { - "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "name": "USD-EUR", - "base_currency": "USD", - "target_currency": "EUR", - "exchange_rate": "0.870000000000000", - "start_date": "2026-01-01", - "end_date": "2026-03-31", - "version": 1, - "created_timestamp": "2026-01-15T10:30:00Z", - "updated_timestamp": "2026-01-15T10:30:00Z" + "code": "EUR", + "name": "Euro", + "symbol": "€", + "enabled": true, + "exchange_rates": [ + { + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "USD-EUR", + "base_currency": "USD", + "target_currency": "EUR", + "exchange_rate": "0.870000000000000", + "start_date": "2026-01-01", + "end_date": "2026-03-31", + "version": 1, + "created_timestamp": "2026-01-15T10:30:00Z", + "updated_timestamp": "2026-01-15T10:30:00Z" + } + ] }, { - "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", - "name": "USD-GBP", - "base_currency": "USD", - "target_currency": "GBP", - "exchange_rate": "0.740000000000000", - "start_date": "2026-01-01", - "end_date": "2026-06-30", - "version": 2, - "created_timestamp": "2026-01-15T10:30:00Z", - "updated_timestamp": "2026-02-01T14:00:00Z" + "code": "GBP", + "name": "British Pound", + "symbol": "£", + "enabled": false, + "exchange_rates": [ + { + "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "name": "USD-GBP", + "base_currency": "USD", + "target_currency": "GBP", + "exchange_rate": "0.740000000000000", + "start_date": "2026-01-01", + "end_date": "2026-06-30", + "version": 2, + "created_timestamp": "2026-01-15T10:30:00Z", + "updated_timestamp": "2026-02-01T14:00:00Z" + } + ] } ] } @@ -136,52 +155,30 @@ This endpoint is always available. No Unleash feature flag gating. --- -## Currency Enablement Settings API +## Currency Enablement + +Currency enablement is managed via the exchange rate endpoint. The enabled +status for each currency is visible in the GET list response and can be +toggled individually. ### URL ``` -GET/PUT /api/cost-management/v1/settings/currency/config/ +POST /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/ +DELETE /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/ ``` -This endpoint lists all known currencies and their enabled/disabled status, and -allows an administrator to enable or disable currencies. - -### View - -**File**: `koku/api/settings/currency_views.py` — `EnabledCurrencyView` (PUT to toggle enabled/disabled). -**File**: `koku/cost_models/static_exchange_rate_view.py` — `StaticExchangeRateViewSet` (GET list returns currencies grouped with exchange rates and enabled status). - -**Permission**: Cost Management Administrator role (same permission level as -other Settings operations). +- **POST**: Enables the currency (creates an `EnabledCurrency` row). No request body. +- **DELETE**: Disables the currency (removes the `EnabledCurrency` row). No request body. -### Example: GET `settings/currency/` Response +Both return `204 No Content`. -Returns all ISO 4217 currencies (from Babel) with an `enabled` flag based on -`EnabledCurrency` table membership. - -```json -{ - "meta": { "count": 300 }, - "data": [ - { "code": "AED", "name": "UAE Dirham", "symbol": "AED", "description": "...", "enabled": false }, - { "code": "EUR", "name": "Euro", "symbol": "€", "description": "...", "enabled": true }, - { "code": "USD", "name": "US Dollar", "symbol": "$", "description": "...", "enabled": true } - ] -} -``` - -### Example: POST `settings/currency/config/` Request (Bulk Set) - -Replaces all enabled currencies atomically with the submitted list. +### View -```json -{ - "currencies": ["USD", "EUR", "GBP"] -} -``` +**File**: `koku/api/settings/currency_views.py` — `EnabledCurrencyView` -Response: `204 No Content` +**Permission**: `SettingsAccessPermission` — requires the **Cost Management +Administrator** role. **Side effects**: Enabling or disabling a currency only affects its visibility in the target currency dropdown. It does not affect the `MonthlyExchangeRate`, @@ -191,14 +188,14 @@ in the target currency dropdown. It does not affect the `MonthlyExchangeRate`, When no `CURRENCY_URL` is configured, no dynamic exchange rates are fetched by the Celery task. The `EnabledCurrency` table only contains currencies that an -administrator has explicitly enabled via the Settings API. The full list of -ISO 4217 currencies is always available from Babel. +administrator has explicitly enabled. The full list of ISO 4217 currencies is +always available from Babel. --- ## Available Currencies for Dropdown -The target currency dropdown in the UI must compute its list of available +The target currency dropdown in the UI computes its list of available currencies from two sources: ### Availability Rules @@ -208,30 +205,11 @@ currencies from two sources: | **Dynamic** | Currency exists in `EnabledCurrency` table | USD, EUR enabled → appear in dropdown | | **Static** | Currency appears in any `StaticExchangeRate` pair (as base or target) | Static rate EUR→CHF defined → both EUR and CHF appear in dropdown regardless of `EnabledCurrency` status | -### Dropdown Endpoint - -**File**: New endpoint or extend existing currency-related views. - -``` -GET /api/cost-management/v1/settings/currency/available-currencies/ -``` - -Returns the currencies visible to the user — the union of enabled dynamic -currencies and static rate currencies: - -```json -{ - "data": [ - { "currency_code": "USD", "source": "dynamic" }, - { "currency_code": "EUR", "source": "both" }, - { "currency_code": "CHF", "source": "static" }, - { "currency_code": "GBP", "source": "dynamic" } - ] -} -``` +### Data Source -The `source` field indicates whether the currency is available via dynamic rates, -static rates, or both. This is informational for the frontend. +The `GET settings/currency/exchange_rate/` endpoint returns all currencies that +have exchange rates, along with their `enabled` status. The frontend uses this +to populate the dropdown — no separate available-currencies endpoint is needed. ### No Currencies Available @@ -350,14 +328,12 @@ respectively). Add endpoint definitions for: -- `GET /api/cost-management/v1/exchange-rate-pairs/` — list with filters -- `POST /api/cost-management/v1/exchange-rate-pairs/` — create -- `GET /api/cost-management/v1/exchange-rate-pairs/{uuid}/` — retrieve -- `PUT /api/cost-management/v1/exchange-rate-pairs/{uuid}/` — update -- `DELETE /api/cost-management/v1/exchange-rate-pairs/{uuid}/` — delete -- `GET /api/cost-management/v1/settings/currency/config/` — list enabled/disabled currencies -- `PUT /api/cost-management/v1/settings/currency/config/` — enable/disable currencies -- `GET /api/cost-management/v1/settings/currency/available-currencies/` — list available target currencies +- `GET /api/cost-management/v1/settings/currency/exchange_rate/` — list exchange rates grouped by currency (with enabled status) +- `POST /api/cost-management/v1/settings/currency/exchange_rate/` — create exchange rate +- `PUT /api/cost-management/v1/settings/currency/exchange_rate/{uuid}/` — update exchange rate +- `DELETE /api/cost-management/v1/settings/currency/exchange_rate/{uuid}/` — delete exchange rate +- `POST /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/` — enable currency +- `DELETE /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/` — disable currency Add `exchange_rates_applied` to report response schemas. @@ -373,14 +349,15 @@ and will consume the APIs defined above. The frontend will: -- Add a currency exchange rate table in the Settings "Currency" tab +- Add a currency exchange rate table in the Settings "Currency" tab, using + `GET settings/currency/exchange_rate/` (grouped response with enabled status) - Allow Price List Administrators to add, edit, and remove rate pairs - Display validity periods (start/end month) - Show a note explaining dynamic rates are used when no static rate is defined -- **Add a currency enablement section** in Settings for enabling/disabling - currencies discovered from the exchange rate API -- **Populate the target currency dropdown** from the available-currencies - endpoint (union of enabled dynamic currencies + static rate currencies). +- **Add a currency enablement toggle** using + `POST/DELETE settings/currency/exchange_rate/{code}/enable/` +- **Populate the target currency dropdown** from the exchange rate list response + (union of enabled dynamic currencies + static rate currencies). Disabled currencies are stored but hidden from this dropdown. - **Handle the no-rate error**: When the user selects a target currency that has no conversion path from the bill currency, display the error message @@ -413,3 +390,4 @@ The frontend will: | v1.5 | 2026-04-09 | Replaced stale `MonthlyExchangeRateSnapshot` → `MonthlyExchangeRate`, removed `StaticExchangeRateDictionary` references (removed in pipeline-changes v1.6). | | v1.6 | 2026-04-12 | Updated `exchange_rates_applied` implementation to reflect `Subquery`-based rate resolution (removed `effective_exchange_rates` reference). | | v1.7 | 2026-04-13 | Removed stale "snapshotted" terminology (remnant from `MonthlyExchangeRateSnapshot` rename). | +| v1.8 | 2026-04-28 | Consolidated endpoints under `settings/currency/exchange_rate/`. List returns grouped response with enabled status. Currency enablement via POST/DELETE (no body). Removed separate `AllCurrencyView` and `available-currencies` endpoints. | diff --git a/docs/architecture/constant-currency/data-model.md b/docs/architecture/constant-currency/data-model.md index 7f4df88368..fd03ffe77b 100644 --- a/docs/architecture/constant-currency/data-model.md +++ b/docs/architecture/constant-currency/data-model.md @@ -145,8 +145,9 @@ enabled. | Event | Action | |-------|--------| -| Administrator enables currencies via `POST settings/currency/config/` | Replaces all `EnabledCurrency` rows with the submitted list | -| `GET settings/currency/` | Returns all ISO 4217 currencies (from Babel) with `enabled` flag based on `EnabledCurrency` table membership | +| Administrator enables a currency via `POST settings/currency/exchange_rate/{code}/enable/` | Creates an `EnabledCurrency` row for that currency | +| Administrator disables a currency via `DELETE settings/currency/exchange_rate/{code}/enable/` | Removes the `EnabledCurrency` row for that currency | +| `GET settings/currency/exchange_rate/` | Returns currencies that have exchange rates, grouped by target currency, with `enabled` flag based on `EnabledCurrency` table membership | **How currencies become "available" in dropdowns**: @@ -376,3 +377,4 @@ changes required. | v1.7 | 2026-03-30 | M2 now seeds current-month data from `ExchangeRateDictionary` during migration. Eliminates `ExchangeRateDictionary` fallback in query handler. | | v1.8 | 2026-04-12 | Updated reader description to reflect `Subquery`-based rate resolution (replaces `Case`/`When`). | | v1.9 | 2026-04-13 | Fixed `ExchangeRates` model description: actual fields are `currency_type` (CharField) and `exchange_rate` (FloatField), not `base_currency`/`exchange_rates` JSONField. Removed non-existent `updated_timestamp` column from `ExchangeRateDictionary` example. | +| v2.0 | 2026-04-28 | Updated `EnabledCurrency` lifecycle to reflect POST/DELETE per-currency enablement at `settings/currency/exchange_rate/{code}/enable/`. | diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index 0f54521b25..3c2e78d2b3 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -41,16 +41,14 @@ pairs. Show rate provenance in report responses. | Migration M3 | `koku/cost_models/migrations/XXXX_*.py` | Create `enabled_currency` table | | Serializer | `koku/cost_models/static_exchange_rate_serializer.py` | Validation + `MonthlyExchangeRate` upsert side-effects | | ViewSet | `koku/cost_models/static_exchange_rate_view.py` | CRUD API for static rates | -| Currency enablement view | `koku/api/settings/` or new file | Settings API for enable/disable currencies | -| Available currencies view | `koku/api/settings/` or new file | Returns available target currencies for dropdown | -| URL registration | `koku/cost_models/urls.py` | Router entry for `exchange-rate-pairs` | -| Settings URL registration | `koku/api/urls.py` or `koku/api/settings/urls.py` | Routes for currency enablement and available currencies endpoints | +| Currency enablement view | `koku/api/settings/currency_views.py` | POST/DELETE enablement for individual currencies | +| URL registration | `koku/api/urls.py` | Routes for `settings/currency/exchange_rate/` (list/create, detail, enable) | | Celery task update | `koku/masu/celery/tasks.py` | Currency discovery, `MonthlyExchangeRate` upsert for all currencies per tenant (skips fetch if no `CURRENCY_URL`) | | Query handler update | `koku/api/query_handler.py` | Read from `MonthlyExchangeRate` for all months (no fallback; M2 seeds current month) | | OCP handler update | `koku/api/report/ocp/query_handler.py` | OCP-specific rate resolution from `MonthlyExchangeRate` | | Forecast handler update | `koku/forecast/forecast.py` | Rate resolution from `MonthlyExchangeRate` | | Report meta update | `koku/api/report/queries.py` | `exchange_rates_applied` metadata, no-rate error handling | -| OpenAPI update | `koku/docs/specs/openapi.json` | New endpoint definitions (exchange-rate-pairs, currency-config, available-currencies) | +| OpenAPI update | `koku/docs/specs/openapi.json` | New endpoint definitions (exchange_rate list/create/update/delete, enable/disable) | | Serializer tests | `koku/cost_models/test/test_static_exchange_rate_serializer.py` | Validation tests | | View tests | `koku/cost_models/test/test_static_exchange_rate_view.py` | CRUD tests | | MonthlyExchangeRate tests | `koku/cost_models/test/test_monthly_exchange_rate.py` | Rate creation, query, locking tests | @@ -75,7 +73,7 @@ pairs. Show rate provenance in report responses. - [ ] Consecutive months with same rate/type collapsed into one period string - [ ] Unit tests pass for serializer, view, MonthlyExchangeRate logic, query handler - [ ] On-prem mode: full functionality without Trino -- [ ] **Currency enablement**: Administrator can enable currencies via `POST settings/currency/config/` +- [ ] **Currency enablement**: Administrator can enable currencies via `POST settings/currency/exchange_rate/{code}/enable/` - [ ] **Currency enablement**: All currencies are stored in `MonthlyExchangeRate` regardless of enabled status; `enabled` flag only controls dropdown visibility - [ ] **Rate resolution**: Static rates take precedence over dynamic rates; error returned if neither exists for a given pair - [ ] **No `CURRENCY_URL`**: Celery task skips API fetch; system works with whatever rates are available @@ -83,7 +81,7 @@ pairs. Show rate provenance in report responses. - [ ] **Available currencies**: Static rate currencies appear regardless of `EnabledCurrency` status - [ ] **No-rate corner case**: Selecting a target currency with no conversion path returns HTTP 400 with actionable error - [ ] **No currencies available**: Dropdown hidden or shows "No exchange rates available" when no currencies are available -- [ ] **Currency list**: `GET settings/currency/` returns all ISO 4217 currencies with enabled flag +- [ ] **Currency list**: `GET settings/currency/exchange_rate/` returns currencies grouped by target currency with enabled flag and nested exchange rates ### Rollback @@ -94,9 +92,7 @@ pairs. Show rate provenance in report responses. `MonthlyExchangeRate` upsert, currency discovery) 4. Revert report meta changes in `koku/api/report/queries.py` (remove `exchange_rates_applied` metadata and no-rate error handling) -5. Revert URL registration in `koku/cost_models/urls.py` -6. Revert Settings URL registration (remove currency enablement and - available-currencies endpoints) +5. Revert URL registration in `koku/api/urls.py` (remove `settings/currency/exchange_rate/` routes) 7. Drop tables via reverse migration (`migrate_schemas` runs `DeleteModel` for all three new tables: `static_exchange_rate`, `monthly_exchange_rate`, `enabled_currency`) @@ -181,3 +177,4 @@ design would be needed to handle path prioritization. | v1.8 | 2026-04-12 | Fixed R6 status from "Low" to "Mitigated" to match risk-register.md. | | v1.9 | 2026-04-12 | R5 mitigated (Subquery replaces Case/When). Updated validation to reflect Subquery approach. | | v2.0 | 2026-04-13 | Updated pre-deployment month validation item: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). | +| v2.1 | 2026-04-28 | Updated URL references to `settings/currency/exchange_rate/`. Consolidated URL registration to `koku/api/urls.py`. Removed separate available-currencies endpoint. | diff --git a/docs/architecture/constant-currency/risk-register.md b/docs/architecture/constant-currency/risk-register.md index 4f445e2515..85c9037b8b 100644 --- a/docs/architecture/constant-currency/risk-register.md +++ b/docs/architecture/constant-currency/risk-register.md @@ -196,7 +196,7 @@ currency dropdown is either hidden or shows *"No exchange rates available"*. **Recovery path**: The administrator can: -1. Define static exchange rates via the CRUD API (`/exchange-rate-pairs/`) +1. Define static exchange rates via the CRUD API (`/settings/currency/exchange_rate/`) 2. Configure `CURRENCY_URL` to fetch dynamic rates from an exchange rate API 3. Enable discovered currencies in Settings to make them visible in the dropdown @@ -233,3 +233,4 @@ R8 ✓ | v1.5 | 2026-03-30 | R4 resolved: M2 seeds current-month data, eliminating need for `ExchangeRateDictionary` fallback. | | v1.6 | 2026-04-12 | R5 mitigated: `Subquery` approach replaces `Case`/`When`, eliminating O(months × currencies) scaling concern. | | v1.7 | 2026-04-13 | R4: updated to reflect earliest-available-rate fallback for pre-deployment months (aligns with pipeline-changes.md v2.1). | +| v1.8 | 2026-04-28 | R8: updated CRUD API URL to `settings/currency/exchange_rate/`. | From a401aa29dfe688c38875a7b899ae9df0b48774e9 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 15:32:10 +0300 Subject: [PATCH 062/106] COST-7252: Remove version field from StaticExchangeRate The version field added unnecessary complexity without providing meaningful value at this stage. Audit history is deferred to Phase 2. Made-with: Cursor --- docs/architecture/constant-currency/README.md | 15 +++++++-------- .../constant-currency/api-and-frontend.md | 3 --- .../constant-currency/data-model.md | 14 ++++++-------- .../constant-currency/phased-delivery.md | 1 - koku/cost_models/models.py | 3 +-- .../static_exchange_rate_serializer.py | 6 +----- .../test_static_exchange_rate_serializer.py | 18 ------------------ .../test/test_static_exchange_rate_view.py | 2 -- 8 files changed, 15 insertions(+), 47 deletions(-) diff --git a/docs/architecture/constant-currency/README.md b/docs/architecture/constant-currency/README.md index 396964ffd6..133fdd3268 100644 --- a/docs/architecture/constant-currency/README.md +++ b/docs/architecture/constant-currency/README.md @@ -289,14 +289,13 @@ graph LR | 4 | **No multi-hop conversion** | No chain conversion (e.g., USD→EUR→CNY) to avoid prioritization complexity | | 5 | **Bidirectional implicit inverse** | USD→EUR at 0.87 implies EUR→USD = 1/0.87 unless explicitly defined | | 6 | **Natural month boundaries** | Start/end dates must align to first/last day of month; no mid-month validity periods | -| 7 | **Simple integer versioning** | Auto-increment on `StaticExchangeRate.version`; Phase 2 adds full audit history | -| 8 | **Automatic finalized month locking** | Dynamic rows overwritten daily during current month; untouched after month ends | -| 9 | **Forward-only with current-month seed** | M2 migration seeds current-month data from `ExchangeRateDictionary`; pre-deployment months fall back to earliest available rate | -| 10 | **Per-pair rows, not JSON blob** | Enables `unique_together` constraint, simpler queries, cleaner ORM integration | -| 11 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. | -| 12 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback, error if neither). Documentation references `open.er-api.com` (free tier) as the production example | -| 13 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection | -| 14 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `EnabledCurrency` status | +| 7 | **Automatic finalized month locking** | Dynamic rows overwritten daily during current month; untouched after month ends | +| 8 | **Forward-only with current-month seed** | M2 migration seeds current-month data from `ExchangeRateDictionary`; pre-deployment months fall back to earliest available rate | +| 9 | **Per-pair rows, not JSON blob** | Enables `unique_together` constraint, simpler queries, cleaner ORM integration | +| 10 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. | +| 11 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback, error if neither). Documentation references `open.er-api.com` (free tier) as the production example | +| 12 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection | +| 13 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `EnabledCurrency` status | --- diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 917199bad5..8aaa679700 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -65,7 +65,6 @@ Administrator** role. | Different currencies | `base_currency != target_currency` | | Month boundaries | `start_date` must be 1st of month; `end_date` must be last day of a month | | No overlap | No overlapping validity periods for same directional `(base, target)` pair | -| Version | Auto-increment `version` on update | | Name | Read-only computed field: `"{base_currency}-{target_currency}"` | **Side effects** (see [pipeline-changes.md § Writer 2](./pipeline-changes.md#static-rate--monthlyexchangerate-upsert--writer-2)): @@ -107,7 +106,6 @@ Only currencies with at least one `StaticExchangeRate` record appear. "exchange_rate": "0.870000000000000", "start_date": "2026-01-01", "end_date": "2026-03-31", - "version": 1, "created_timestamp": "2026-01-15T10:30:00Z", "updated_timestamp": "2026-01-15T10:30:00Z" } @@ -127,7 +125,6 @@ Only currencies with at least one `StaticExchangeRate` record appear. "exchange_rate": "0.740000000000000", "start_date": "2026-01-01", "end_date": "2026-06-30", - "version": 2, "created_timestamp": "2026-01-15T10:30:00Z", "updated_timestamp": "2026-02-01T14:00:00Z" } diff --git a/docs/architecture/constant-currency/data-model.md b/docs/architecture/constant-currency/data-model.md index fd03ffe77b..0cd8db76ea 100644 --- a/docs/architecture/constant-currency/data-model.md +++ b/docs/architecture/constant-currency/data-model.md @@ -69,7 +69,6 @@ class StaticExchangeRate(models.Model): exchange_rate = models.DecimalField(max_digits=33, decimal_places=15) start_date = models.DateField() # first day of a natural month end_date = models.DateField() # last day of a natural month (or later) - version = models.IntegerField(default=1) created_timestamp = models.DateTimeField(auto_now_add=True) updated_timestamp = models.DateTimeField(auto_now=True) @@ -80,15 +79,15 @@ class StaticExchangeRate(models.Model): **Example `static_exchange_rate` rows**: -| uuid | base_currency | target_currency | exchange_rate | start_date | end_date | version | created_timestamp | updated_timestamp | -|------|---------------|-----------------|---------------|------------|----------|---------|-------------------|-------------------| -| `a1b2c3d4-...` | `USD` | `EUR` | `0.920000000000000` | `2026-01-01` | `2026-03-31` | 1 | `2026-01-15 10:30:00+00` | `2026-01-15 10:30:00+00` | -| `e5f6a7b8-...` | `USD` | `GBP` | `0.780000000000000` | `2026-01-01` | `2026-01-31` | 2 | `2026-01-10 08:00:00+00` | `2026-01-20 14:22:00+00` | -| `c9d0e1f2-...` | `EUR` | `GBP` | `0.848000000000000` | `2026-02-01` | `2026-06-30` | 1 | `2026-02-01 09:00:00+00` | `2026-02-01 09:00:00+00` | +| uuid | base_currency | target_currency | exchange_rate | start_date | end_date | created_timestamp | updated_timestamp | +|------|---------------|-----------------|---------------|------------|----------|-------------------|-------------------| +| `a1b2c3d4-...` | `USD` | `EUR` | `0.920000000000000` | `2026-01-01` | `2026-03-31` | `2026-01-15 10:30:00+00` | `2026-01-15 10:30:00+00` | +| `e5f6a7b8-...` | `USD` | `GBP` | `0.780000000000000` | `2026-01-01` | `2026-01-31` | `2026-01-10 08:00:00+00` | `2026-01-20 14:22:00+00` | +| `c9d0e1f2-...` | `EUR` | `GBP` | `0.848000000000000` | `2026-02-01` | `2026-06-30` | `2026-02-01 09:00:00+00` | `2026-02-01 09:00:00+00` | In this example: - The `USD→EUR` rate of `0.92` applies for Jan–Mar 2026 (overrides dynamic rates for those months) -- The `USD→GBP` rate was updated once (`version=2`) and only covers January +- The `USD→GBP` rate only covers January - The `EUR→GBP` rate covers Feb–Jun 2026 **Constraints** (enforced in serializer validation): @@ -99,7 +98,6 @@ In this example: that same month or a later month - No overlapping validity periods for the same `(base_currency, target_currency)` directional pair -- `version` auto-increments on update (managed by serializer, not DB trigger) **Computed properties**: diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index 3c2e78d2b3..f7718c2e5a 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -60,7 +60,6 @@ pairs. Show rate provenance in report responses. - [ ] Static rate CRUD: create, read, update, delete via API - [ ] Overlapping validity period rejection returns 400 - [ ] Natural month boundary enforcement (mid-month dates rejected) -- [ ] Auto-increment version on update - [ ] Bidirectional inverse rate resolution (1/rate when reverse undefined) - [ ] Dynamic rate daily `MonthlyExchangeRate` upsert per tenant - [ ] Static rate precedence: task skips pairs with existing static rates diff --git a/koku/cost_models/models.py b/koku/cost_models/models.py index 7b053a0a76..fd2e55d576 100644 --- a/koku/cost_models/models.py +++ b/koku/cost_models/models.py @@ -210,7 +210,7 @@ class StaticExchangeRate(models.Model): class Meta: db_table = "static_exchange_rate" ordering = ["-updated_timestamp"] - unique_together = [("base_currency", "target_currency", "start_date", "end_date", "version")] + unique_together = [("base_currency", "target_currency", "start_date", "end_date")] uuid = models.UUIDField(primary_key=True, default=uuid4) base_currency = models.CharField(max_length=5) @@ -218,7 +218,6 @@ class Meta: exchange_rate = models.DecimalField(max_digits=33, decimal_places=15) start_date = models.DateField() end_date = models.DateField() - version = models.IntegerField(default=1) created_timestamp = models.DateTimeField(auto_now_add=True) updated_timestamp = models.DateTimeField(auto_now=True) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 50ffe936e6..32411d076a 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -34,11 +34,10 @@ class Meta: "exchange_rate", "start_date", "end_date", - "version", "created_timestamp", "updated_timestamp", ] - read_only_fields = ["uuid", "name", "version", "created_timestamp", "updated_timestamp"] + read_only_fields = ["uuid", "name", "created_timestamp", "updated_timestamp"] def get_name(self, obj): return f"{obj.base_currency}-{obj.target_currency}" @@ -113,8 +112,6 @@ def update(self, instance, validated_data): old_base = instance.base_currency old_target = instance.target_currency - validated_data["version"] = instance.version + 1 - for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() @@ -130,7 +127,6 @@ def update(self, instance, validated_data): log_json( msg="Static exchange rate updated", pair=instance.name, - version=instance.version, ) ) return instance diff --git a/koku/cost_models/test/test_static_exchange_rate_serializer.py b/koku/cost_models/test/test_static_exchange_rate_serializer.py index f4ee7f71d9..47e2b6e184 100644 --- a/koku/cost_models/test/test_static_exchange_rate_serializer.py +++ b/koku/cost_models/test/test_static_exchange_rate_serializer.py @@ -94,7 +94,6 @@ def test_create_upserts_monthly_exchange_rate(self, mock_invalidate): instance = serializer.save() self.assertIsNotNone(instance.uuid) - self.assertEqual(instance.version, 1) self.assertEqual(instance.name, "USD-EUR") monthly_rates = MonthlyExchangeRate.objects.filter( @@ -125,23 +124,6 @@ def test_overlap_detection(self, mock_invalidate): serializer2 = StaticExchangeRateSerializer(data=overlap_data, context=self._make_request_context()) self.assertFalse(serializer2.is_valid()) - @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") - def test_update_increments_version(self, mock_invalidate): - """Test that updating a static rate increments the version.""" - with tenant_context(self.tenant): - serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - self.assertEqual(instance.version, 1) - - update_data = {"exchange_rate": "0.900000000000000"} - serializer2 = StaticExchangeRateSerializer( - instance=instance, data=update_data, partial=True, context=self._make_request_context() - ) - serializer2.is_valid(raise_exception=True) - updated = serializer2.save() - self.assertEqual(updated.version, 2) - def test_delete_removes_static_monthly_rates(self): """Test that deleting a static rate removes static MonthlyExchangeRate rows.""" with tenant_context(self.tenant): diff --git a/koku/cost_models/test/test_static_exchange_rate_view.py b/koku/cost_models/test/test_static_exchange_rate_view.py index 07a2028809..499b5a7416 100644 --- a/koku/cost_models/test/test_static_exchange_rate_view.py +++ b/koku/cost_models/test/test_static_exchange_rate_view.py @@ -40,7 +40,6 @@ def test_create_static_rate(self, mock_invalidate): self.assertEqual(data["base_currency"], "USD") self.assertEqual(data["target_currency"], "EUR") self.assertEqual(data["name"], "USD-EUR") - self.assertEqual(data["version"], 1) @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") def test_list_returns_grouped_by_currency(self, mock_invalidate): @@ -94,7 +93,6 @@ def test_update_static_rate(self, mock_invalidate): update_data["exchange_rate"] = "0.900000000000000" response = self.client.put(detail_url, data=update_data, format="json", **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["version"], 2) @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") def test_delete_static_rate(self, mock_invalidate): From 7a904d93c53ec833277af933b1392075b0330ed5 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 15:45:17 +0300 Subject: [PATCH 063/106] COST-7252: Fix currency endpoint docs to match implementation The list endpoint is at /currency/ (not /settings/currency/) and only returns enabled currencies, so the `enabled` field was removed from the response. The toggle endpoint uses POST/DELETE at the actual URL path rather than PUT with a request body. Made-with: Cursor --- docs/architecture/api-settings-endpoints.md | 48 ++++++++++----------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/docs/architecture/api-settings-endpoints.md b/docs/architecture/api-settings-endpoints.md index 7a7b4c31b3..9c9d0b7ac7 100644 --- a/docs/architecture/api-settings-endpoints.md +++ b/docs/architecture/api-settings-endpoints.md @@ -523,12 +523,12 @@ Control which ISO 4217 currencies are available for selection across the tenant. ### **Endpoints** -#### List All Currencies +#### List Enabled Currencies ``` -GET /settings/currency/ +GET /currency/ ``` -Returns every ISO 4217 currency with an `enabled` flag indicating whether the tenant has enabled it. +Returns only the currencies that an administrator has enabled via the `EnabledCurrency` table. Metadata (name, symbol, description) is computed at response time via babel. **Query Parameters:** - `limit` (integer) - Results per page @@ -538,30 +538,33 @@ Returns every ISO 4217 currency with an `enabled` flag indicating whether the te ```json { "meta": { - "count": 160 + "count": 2 }, "data": [ - { - "code": "USD", - "name": "US Dollar", - "symbol": "$", - "description": "USD ($) - US Dollar", - "enabled": true - }, { "code": "EUR", "name": "Euro", "symbol": "€", - "description": "EUR (€) - Euro", - "enabled": false + "description": "EUR (€) - Euro" + }, + { + "code": "USD", + "name": "US Dollar", + "symbol": "$", + "description": "USD ($) - US Dollar" } ] } ``` -#### Toggle a Currency +#### Enable a Currency +``` +POST /settings/currency/exchange_rate//enable/ +``` + +#### Disable a Currency ``` -PUT /settings/currency// +DELETE /settings/currency/exchange_rate//enable/ ``` Enable or disable a single currency by its ISO 4217 code in the URL path. @@ -569,13 +572,6 @@ Enable or disable a single currency by its ISO 4217 code in the URL path. **Path Parameters:** - `code` (string) - ISO 4217 currency code (case-insensitive, normalized to uppercase) -**Request Body:** -```json -{ - "enabled": true -} -``` - **Response:** `204 No Content` **Error Responses:** @@ -583,14 +579,14 @@ Enable or disable a single currency by its ISO 4217 code in the URL path. **Invalid Currency Code (400 Bad Request):** ```json { - "error": "Invalid ISO 4217 currency code: INVALID" + "code": ["Invalid ISO 4217 currency code: INVALID"] } ``` **Behavior:** -- `enabled: true` — idempotently creates an `EnabledCurrency` row for the code -- `enabled: false` — idempotently deletes the `EnabledCurrency` row if it exists -- Currency code in the URL is case-insensitive (`/settings/currency/usd/` enables `USD`) +- `POST` — idempotently creates an `EnabledCurrency` row for the code +- `DELETE` — idempotently deletes the `EnabledCurrency` row if it exists +- Currency code in the URL is case-insensitive (`/settings/currency/exchange_rate/usd/enable/` enables `USD`) --- From b62ad98b0c5f72f091e8d1fda040bd4420db8c67 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 16:13:10 +0300 Subject: [PATCH 064/106] COST-7252: Add enabled_only flag to CurrencyField Allow callers to opt out of the enabled-currency check by passing enabled_only=False. The parameter is keyword-only with no default, so every call site must declare its intent explicitly. Made-with: Cursor --- koku/api/currency/currencies.py | 5 +++-- koku/api/forecast/serializers.py | 2 +- koku/api/report/serializers.py | 2 +- koku/api/settings/serializers.py | 2 +- koku/cost_models/serializers.py | 6 +++--- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index ed0b96a97c..e5ca93e9ab 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -33,13 +33,14 @@ def get_enabled_currency_codes(): class CurrencyField(serializers.CharField): """CharField that normalizes to uppercase and validates against enabled currencies.""" - def __init__(self, **kwargs): + def __init__(self, *, enabled_only, **kwargs): kwargs.setdefault("max_length", 5) + self.enabled_only = enabled_only super().__init__(**kwargs) def to_internal_value(self, data): value = super().to_internal_value(data).upper() - if value not in get_enabled_currency_codes(): + if self.enabled_only and value not in get_enabled_currency_codes(): raise serializers.ValidationError(f'"{value}" is not an enabled currency.') return value diff --git a/koku/api/forecast/serializers.py b/koku/api/forecast/serializers.py index 7a59527360..ab9bd0f5d1 100644 --- a/koku/api/forecast/serializers.py +++ b/koku/api/forecast/serializers.py @@ -17,7 +17,7 @@ class ForecastParamSerializer(serializers.Serializer): limit = serializers.IntegerField(required=False, min_value=1) offset = serializers.IntegerField(required=False, min_value=0) - currency = CurrencyField(required=False) + currency = CurrencyField(required=False, enabled_only=True) def __init__(self, *args, **kwargs): """Initialize the BaseSerializer.""" diff --git a/koku/api/report/serializers.py b/koku/api/report/serializers.py index 0da9ac1fe9..5304def228 100644 --- a/koku/api/report/serializers.py +++ b/koku/api/report/serializers.py @@ -353,7 +353,7 @@ def _schema_name(self): start_date = serializers.DateField(required=False) end_date = serializers.DateField(required=False) - currency = CurrencyField(required=False) + currency = CurrencyField(required=False, enabled_only=True) category = StringOrListField(child=serializers.CharField(), required=False) order_by_allowlist = ( diff --git a/koku/api/settings/serializers.py b/koku/api/settings/serializers.py index f051b47a44..46fe46944e 100644 --- a/koku/api/settings/serializers.py +++ b/koku/api/settings/serializers.py @@ -29,7 +29,7 @@ class UserSettingUpdateCostTypeSerializer(serializers.Serializer): class UserSettingUpdateCurrencySerializer(serializers.Serializer): """Serializer for setting currency.""" - currency = CurrencyField() + currency = CurrencyField(enabled_only=True) class TenantSettingsSerializer(serializers.Serializer): diff --git a/koku/cost_models/serializers.py b/koku/cost_models/serializers.py index 64d57e42b4..5d7a2adf9f 100644 --- a/koku/cost_models/serializers.py +++ b/koku/cost_models/serializers.py @@ -95,7 +95,7 @@ class TieredRateSerializer(serializers.Serializer): value = serializers.DecimalField(required=False, max_digits=19, decimal_places=10) usage = serializers.DictField(required=False) - unit = CurrencyField() + unit = CurrencyField(enabled_only=True) def validate_value(self, value): """Check that value is a positive value.""" @@ -133,7 +133,7 @@ class TagRateValueSerializer(serializers.Serializer): DECIMALS = ("value", "usage_start", "usage_end") tag_value = serializers.CharField(max_length=100) - unit = CurrencyField() + unit = CurrencyField(enabled_only=True) usage = serializers.DictField(required=False) value = serializers.DecimalField(required=False, max_digits=19, decimal_places=10) description = serializers.CharField(allow_blank=True, max_length=500) @@ -459,7 +459,7 @@ class Meta: distribution_info = DistributionSerializer(required=False) - currency = CurrencyField(required=False) + currency = CurrencyField(required=False, enabled_only=True) price_list_uuids = serializers.ListField(child=serializers.UUIDField(), required=False) From 1aef5bad7a3d7604835fdd11c2c062a33c8e5958 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 17:23:10 +0300 Subject: [PATCH 065/106] COST-7252: Update Pipfile.lock to include Babel dependency Made-with: Cursor --- Pipfile.lock | 1697 +++++++++++-------------------- dev/containers/minio/.gitignore | 5 - 2 files changed, 615 insertions(+), 1087 deletions(-) delete mode 100644 dev/containers/minio/.gitignore diff --git a/Pipfile.lock b/Pipfile.lock index 21e9ababee..3619423ae2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8355828638d3b0648e8f215a63312b8cc8edc9f80fd3dfb07e960556aa6086a0" + "sha256": "076741f1d4c52a3ea84a9a18600a5de48c6106574cacae7db51b8e50808f33a1" }, "pipfile-spec": 6, "requires": { @@ -115,6 +115,15 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, + "babel": { + "hashes": [ + "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", + "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.18.0" + }, "beautifulsoup4": { "hashes": [ "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", @@ -134,29 +143,29 @@ }, "boto3": { "hashes": [ - "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", - "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" + "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", + "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "botocore": { "hashes": [ - "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", - "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" + "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", + "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" ], "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "cachetools": { "hashes": [ - "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", - "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" + "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", + "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.5" + "version": "==7.0.6" }, "celery": { "hashes": [ @@ -169,11 +178,11 @@ }, "certifi": { "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" + "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", + "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" ], "markers": "python_version >= '3.7'", - "version": "==2026.2.25" + "version": "==2026.4.22" }, "cffi": { "hashes": [ @@ -262,7 +271,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.9'", "version": "==2.0.0" }, "charset-normalizer": { @@ -473,11 +482,11 @@ }, "click": { "hashes": [ - "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", - "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" + "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", + "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" ], "markers": "python_version >= '3.10'", - "version": "==8.3.2" + "version": "==8.3.3" }, "click-didyoumean": { "hashes": [ @@ -547,58 +556,58 @@ }, "cryptography": { "hashes": [ - "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", - "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", - "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", - "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", - "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", - "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", - "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", - "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", - "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", - "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", - "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", - "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", - "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", - "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", - "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", - "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", - "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", - "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", - "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", - "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", - "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", - "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", - "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", - "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", - "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", - "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", - "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", - "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", - "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", - "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", - "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", - "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", - "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", - "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", - "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", - "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", - "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", - "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", - "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", - "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", - "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", - "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", - "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", - "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", - "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", - "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", - "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", - "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", - "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" + "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", + "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", + "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", + "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", + "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", + "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", + "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", + "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", + "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", + "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", + "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", + "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", + "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", + "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", + "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", + "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", + "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", + "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", + "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", + "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", + "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", + "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", + "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", + "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", + "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", + "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", + "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", + "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", + "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", + "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", + "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", + "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", + "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", + "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", + "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", + "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", + "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", + "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", + "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", + "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", + "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", + "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", + "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", + "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", + "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", + "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", + "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", + "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", + "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.7" + "version": "==47.0.0" }, "django": { "hashes": [ @@ -701,11 +710,11 @@ "grpc" ], "hashes": [ - "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", - "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" + "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", + "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" ], "markers": "python_version >= '3.9'", - "version": "==2.30.2" + "version": "==2.30.3" }, "google-api-python-client": { "hashes": [ @@ -718,12 +727,12 @@ }, "google-auth": { "hashes": [ - "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", - "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" + "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", + "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.1" + "version": "==2.49.2" }, "google-auth-httplib2": { "hashes": [ @@ -814,6 +823,71 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, + "greenlet": { + "hashes": [ + "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", + "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", + "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", + "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", + "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", + "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", + "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", + "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", + "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", + "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", + "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", + "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", + "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", + "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", + "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", + "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", + "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", + "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", + "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", + "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", + "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", + "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", + "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", + "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", + "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", + "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", + "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", + "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", + "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", + "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", + "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", + "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", + "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", + "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", + "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", + "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", + "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", + "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", + "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", + "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", + "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", + "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", + "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", + "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", + "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", + "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", + "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", + "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", + "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", + "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", + "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", + "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", + "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", + "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", + "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", + "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", + "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", + "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", + "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564" + ], + "markers": "python_version >= '3.10'", + "version": "==3.5.0" + }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -908,11 +982,11 @@ }, "idna": { "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", + "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" ], "markers": "python_version >= '3.8'", - "version": "==3.11" + "version": "==3.13" }, "importlib-metadata": { "hashes": [ @@ -1413,17 +1487,17 @@ "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.11.8" }, "packaging": { "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.0" + "version": "==26.2" }, "pandas": { "hashes": [ @@ -1574,134 +1648,134 @@ }, "psycopg2-binary": { "hashes": [ - "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", - "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", - "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", - "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", - "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", - "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", - "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", - "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", - "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", - "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", - "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", - "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", - "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", - "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", - "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", - "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", - "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", - "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", - "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", - "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", - "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", - "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", - "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", - "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", - "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", - "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", - "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", - "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", - "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", - "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", - "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", - "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", - "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", - "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", - "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", - "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", - "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", - "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", - "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", - "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", - "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", - "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", - "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", - "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", - "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", - "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", - "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", - "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", - "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", - "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", - "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", - "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", - "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", - "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", - "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", - "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", - "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", - "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", - "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", - "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", - "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", - "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", - "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", - "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", - "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", - "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", - "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" + "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", + "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964", + "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", + "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", + "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115", + "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", + "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", + "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", + "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", + "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", + "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", + "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", + "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", + "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", + "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", + "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", + "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", + "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", + "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", + "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", + "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf", + "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", + "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", + "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", + "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", + "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", + "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", + "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", + "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94", + "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", + "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", + "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", + "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", + "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", + "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", + "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", + "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", + "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", + "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", + "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", + "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", + "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", + "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", + "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", + "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915", + "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", + "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", + "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe", + "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", + "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", + "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", + "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86", + "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", + "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", + "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", + "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", + "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03", + "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", + "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", + "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", + "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", + "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab", + "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", + "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10", + "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", + "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2", + "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.9.11" + "version": "==2.9.12" }, "pyarrow": { "hashes": [ - "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", - "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", - "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", - "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", - "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", - "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", - "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", - "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", - "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", - "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", - "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", - "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", - "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", - "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", - "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", - "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", - "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", - "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", - "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", - "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", - "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", - "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", - "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", - "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", - "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", - "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", - "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", - "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", - "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", - "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", - "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", - "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", - "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", - "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", - "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", - "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", - "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", - "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", - "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", - "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", - "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", - "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", - "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", - "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", - "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", - "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", - "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", - "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", - "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", - "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd" + "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", + "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", + "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", + "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", + "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", + "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", + "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", + "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", + "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", + "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", + "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", + "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", + "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", + "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", + "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", + "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", + "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", + "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", + "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", + "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", + "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", + "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", + "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", + "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", + "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", + "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", + "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", + "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", + "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", + "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", + "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", + "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", + "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", + "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", + "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", + "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", + "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", + "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", + "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", + "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", + "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", + "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", + "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", + "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", + "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", + "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", + "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", + "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", + "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", + "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==23.0.1" + "version": "==24.0.0" }, "pyasn1": { "hashes": [ @@ -1724,144 +1798,143 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "implementation_name != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.0" }, "pydantic": { "hashes": [ - "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", - "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" + "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", + "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.12.5" + "version": "==2.13.3" }, "pydantic-core": { "hashes": [ - "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", - "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", - "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", - "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", - "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", - "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", - "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", - "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", - "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", - "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", - "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", - "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", - "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", - "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", - "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", - "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", - "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", - "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", - "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", - "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", - "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", - "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", - "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", - "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", - "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", - "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", - "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", - "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", - "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", - "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", - "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", - "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", - "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", - "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", - "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", - "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", - "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", - "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", - "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", - "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", - "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", - "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", - "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", - "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", - "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", - "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", - "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", - "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", - "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", - "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", - "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", - "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", - "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", - "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", - "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", - "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", - "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", - "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", - "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", - "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", - "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", - "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", - "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", - "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", - "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", - "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", - "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", - "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", - "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", - "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", - "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", - "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", - "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", - "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", - "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", - "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", - "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", - "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", - "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", - "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", - "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", - "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", - "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", - "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", - "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", - "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", - "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", - "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", - "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", - "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", - "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", - "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", - "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", - "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", - "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", - "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", - "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", - "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", - "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", - "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", - "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", - "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", - "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", - "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", - "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", - "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", - "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", - "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", - "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", - "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", - "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", - "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", - "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", - "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", - "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", - "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", - "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", - "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", - "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", - "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", - "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" + "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", + "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", + "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", + "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", + "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", + "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", + "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", + "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", + "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", + "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", + "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", + "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", + "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", + "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", + "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", + "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", + "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", + "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", + "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", + "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", + "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", + "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", + "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", + "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495", + "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", + "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0", + "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd", + "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", + "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", + "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", + "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", + "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", + "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", + "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", + "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", + "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", + "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", + "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", + "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", + "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", + "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720", + "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", + "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c", + "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", + "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", + "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", + "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", + "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", + "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", + "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873", + "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", + "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", + "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", + "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", + "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", + "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd", + "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", + "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", + "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", + "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d", + "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", + "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", + "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", + "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", + "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168", + "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", + "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13", + "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", + "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", + "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", + "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", + "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", + "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", + "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", + "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", + "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", + "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", + "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", + "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", + "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", + "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", + "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", + "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", + "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", + "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", + "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", + "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", + "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", + "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", + "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", + "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", + "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", + "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", + "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", + "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", + "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", + "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", + "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6", + "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79", + "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a", + "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", + "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", + "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", + "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", + "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", + "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", + "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", + "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", + "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", + "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", + "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", + "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", + "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", + "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", + "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb", + "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", + "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", + "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", + "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", + "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56" ], "markers": "python_version >= '3.9'", - "version": "==2.41.5" + "version": "==2.46.3" }, "pyjwt": { "extras": [ @@ -1888,7 +1961,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pytz": { @@ -1926,11 +1999,11 @@ }, "s3transfer": { "hashes": [ - "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", - "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" + "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", + "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" ], "markers": "python_version >= '3.9'", - "version": "==0.16.0" + "version": "==0.16.1" }, "scipy": { "hashes": [ @@ -2010,19 +2083,19 @@ }, "sentry-sdk": { "hashes": [ - "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", - "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585" + "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", + "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.57.0" + "version": "==2.58.0" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.17.0" }, "soupsieve": { @@ -2182,11 +2255,11 @@ }, "tzdata": { "hashes": [ - "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", - "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" + "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", + "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" ], "markers": "python_version >= '2'", - "version": "==2026.1" + "version": "==2026.2" }, "tzlocal": { "hashes": [ @@ -2311,11 +2384,11 @@ }, "zipp": { "hashes": [ - "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", + "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110" ], "markers": "python_version >= '3.9'", - "version": "==3.23.0" + "version": "==3.23.1" }, "zstandard": { "hashes": [ @@ -2432,30 +2505,6 @@ "markers": "python_version >= '3.6'", "version": "==5.3.1" }, - "anyio": { - "hashes": [ - "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", - "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" - ], - "markers": "python_version >= '3.10'", - "version": "==4.13.0" - }, - "anytree": { - "hashes": [ - "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", - "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.9.2'", - "version": "==2.13.0" - }, - "app-common-python": { - "hashes": [ - "sha256:74125371e2865bcba7a7c34f0700d4b8b97a0c0a9ff67bbb06fdb20a3da4b75c", - "sha256:861bd93482edabd00a85eb6512e0d7df08597be6f76b864be8e53d6294f81389" - ], - "index": "pypi", - "version": "==0.2.9" - }, "argh": { "hashes": [ "sha256:2edac856ff50126f6e47d884751328c9f466bacbbb6cbfdac322053d94705494", @@ -2499,14 +2548,6 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, - "backoff": { - "hashes": [ - "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", - "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" - ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==2.2.1" - }, "billiard": { "hashes": [ "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", @@ -2517,37 +2558,29 @@ }, "boto3": { "hashes": [ - "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", - "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" + "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", + "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "botocore": { "hashes": [ - "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", - "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" + "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", + "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" ], "markers": "python_version >= '3.9'", - "version": "==1.42.87" - }, - "cached-property": { - "hashes": [ - "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", - "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb" - ], - "markers": "python_version >= '3.8'", - "version": "==2.0.1" + "version": "==1.42.97" }, "cachetools": { "hashes": [ - "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", - "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" + "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", + "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.5" + "version": "==7.0.6" }, "celery": { "hashes": [ @@ -2560,11 +2593,11 @@ }, "certifi": { "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" + "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", + "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" ], "markers": "python_version >= '3.7'", - "version": "==2026.2.25" + "version": "==2026.4.22" }, "cffi": { "hashes": [ @@ -2653,7 +2686,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.9'", "version": "==2.0.0" }, "cfgv": { @@ -2666,41 +2699,45 @@ }, "chardet": { "hashes": [ - "sha256:0487a6a6846740f39f9fcd71e3acff2982bae8bca3507ee986d5155cd458e044", - "sha256:04b9be0d786b9a3bbd7860a82d27e843f22211be51b9b84d85fe8d9864e2f535", - "sha256:0848015eb1471e1499963dff2776557af05f99c38ba2a14f34ea078f8668c6a9", - "sha256:0de8d636391f9050e4e048ca8a9f98b25e67eff389705f8c6ff1ab9593f7339b", - "sha256:19bcd1de4a0c1a5802f9d2d370b6696668bddc166a3c89c113cf109313b3d99f", - "sha256:277ce1174ea054415a3c2ad5f51aa089a96dda16999de56e4ac1bc366d0d535e", - "sha256:2da446b920064ca9574504c29a07ef5eae91a1948a302a25043a16fb79ec2397", - "sha256:35ade2a6f93e5d2bdff541b584126cfe066eac5c9457572ff97cabc8068bece1", - "sha256:3886f8f9bb3500bd8c421b2de9b4878a0c183f80bc289338cdda869dfd4397fb", - "sha256:3d66d2949754ad924865a47e81857a0792dc8edc651094285116b6df2e218445", - "sha256:4ececf9631f7932a2cef728746303d71ae8204923190253d5382ee37739dd46e", - "sha256:5d86402a506631af2fb36e3d1c72021477b228fb0dcdb44400b9b681f14b14c0", - "sha256:5e686e5a0d8155cfbf5b1a579f5790bb01bf1a0a52e7f98b38801c09a0c63fcd", - "sha256:62b25b3ea5ef8e1672726e4c1601f6636ce3b76b9de92af669c8000711d7fe13", - "sha256:6b5c03330b9108124f8174a596284b737e95f1cf6a99953c37cea7e2583212e7", - "sha256:7a1c2be068ab91a472fd74617d9371605897c3061ca4f2e5df63d9f3d9c11f8e", - "sha256:80de820fa1df95a2e7c9898867f7d6ff3dc3d52a109938b215b37ab54474d307", - "sha256:8befb12c263cb14e26f065a3f99d76ef2610b0266cb70d827bb61528e9e60f28", - "sha256:9381b3d9075c8a2e622b4d46db5e4229c94aebc71d4c8e620d9cf2cea2930824", - "sha256:a9322fd3ffd359b49b2d608725a15975ebc0d66f2dcedefa7ddb5847e54a6f9c", - "sha256:a98c361b73c6ce4a44beaecf5cfb389ec69b566dd7f3f4ea5d1bde6487e7054d", - "sha256:b726b0b2684d29cd08f602bb4266334386c58741ff34c9e2f6cdf97ad604e235", - "sha256:be39708b300a80a9f78ef8f81018e2e9c6274a71c0823a4d6e493c72f7b3d2a2", - "sha256:bee1d665ca5810d8e3cf11122619e85c23b209075cbddb91f213675248f0e522", - "sha256:c03925738670199d253b8c79828d8a68d404f629a2dbf1b4b5aabd8c8b0249ab", - "sha256:c820c95d8b4de8aea99b54083d38f10f763686646962b5627e8e2b2db113a37b", - "sha256:c98e1044785ab71f0fee70f64b8d56f69df9de1b593793022e001ba2f6b76dd0", - "sha256:caf0715b8a5e20fc3faf21a24abd3ae513f8f58206dd32d1b87eca6351e105ed", - "sha256:cda41132a45dfbf6984dade1f531a4098c813caf266c66cc446d90bb9369cabd", - "sha256:d8aa2bae7d0523963395f802ae2212e8b2248d4503a14a691de86edf716b22d3", - "sha256:e53cc280a1ab616f191ac7ebdd1f38f2aa78b1411dd2677dc556c6f0fa085913", - "sha256:fcaed03cefa53f62346091ef92da7a6f44bae6830a6f4c6b097a70cdc31b1199" + "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", + "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", + "sha256:23163921dccf3103ce59540b0443c106d2c0a0ff2e0503e05196f5e6fdea453f", + "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", + "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", + "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", + "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", + "sha256:3990fffcc6a6045f2234ab72752ad037e3b2d48c72037f244d42738db397eb75", + "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", + "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", + "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", + "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", + "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", + "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", + "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", + "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", + "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", + "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", + "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", + "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", + "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", + "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", + "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", + "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", + "sha256:bba8bea1b28d927b3e99e47deafe53658d34497c0a891d95ff1ba8ff6663f01c", + "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", + "sha256:c0c79b13c9908ac7dfe0a74116ebc9a0f28b2319d23c32f3dfcdfbe1279c7eaf", + "sha256:c7116b0452994734ccff35e154b44240090eb0f4f74b9106292668133557c175", + "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", + "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", + "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", + "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", + "sha256:cfb54563fe5f130da17c44c6a4e2e8052ba628e5ab4eab7ef8190f736f0f8f72", + "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", + "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", + "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131" ], "markers": "python_version >= '3.10'", - "version": "==7.4.1" + "version": "==7.4.3" }, "charset-normalizer": { "hashes": [ @@ -2839,11 +2876,11 @@ }, "click": { "hashes": [ - "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", - "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" + "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", + "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" ], "markers": "python_version >= '3.10'", - "version": "==8.3.2" + "version": "==8.3.3" }, "click-didyoumean": { "hashes": [ @@ -3069,67 +3106,67 @@ }, "crc-bonfire": { "hashes": [ - "sha256:7fa3b2e739a92c2ffdc3f43ba590201dff64d6993603fc43deb10f3f0c2d903a", - "sha256:d9a32ad91714dd0c00c56a405add63df4896dfeb58994ce5f9688dde7cd4298a" + "sha256:51df052702fb2e24a2ccbbe7fe9089f8f984140ba1ede206ca91d56bfca469aa", + "sha256:6fa9fda6582e8eb69d1654e4b440991133ef1b2109483e73888d5474bf9ee695" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.9.1" + "markers": "python_version >= '3.10'", + "version": "==6.11.0" }, "cryptography": { "hashes": [ - "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", - "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", - "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", - "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", - "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", - "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", - "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", - "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", - "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", - "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", - "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", - "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", - "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", - "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", - "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", - "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", - "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", - "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", - "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", - "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", - "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", - "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", - "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", - "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", - "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", - "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", - "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", - "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", - "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", - "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", - "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", - "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", - "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", - "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", - "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", - "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", - "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", - "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", - "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", - "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", - "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", - "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", - "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", - "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", - "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", - "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", - "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", - "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", - "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" + "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", + "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", + "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", + "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", + "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", + "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", + "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", + "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", + "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", + "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", + "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", + "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", + "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", + "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", + "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", + "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", + "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", + "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", + "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", + "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", + "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", + "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", + "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", + "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", + "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", + "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", + "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", + "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", + "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", + "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", + "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", + "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", + "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", + "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", + "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", + "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", + "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", + "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", + "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", + "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", + "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", + "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", + "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", + "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", + "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", + "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", + "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", + "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", + "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.7" + "version": "==47.0.0" }, "cycler": { "hashes": [ @@ -3208,20 +3245,20 @@ }, "faker": { "hashes": [ - "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", - "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019" + "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", + "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==40.13.0" + "version": "==40.15.0" }, "filelock": { "hashes": [ - "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", - "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70" + "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", + "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258" ], "markers": "python_version >= '3.10'", - "version": "==3.25.2" + "version": "==3.29.0" }, "flake8": { "hashes": [ @@ -3302,20 +3339,20 @@ "grpc" ], "hashes": [ - "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", - "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" + "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", + "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" ], "markers": "python_version >= '3.9'", - "version": "==2.30.2" + "version": "==2.30.3" }, "google-auth": { "hashes": [ - "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", - "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" + "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", + "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.1" + "version": "==2.49.2" }, "google-cloud-bigquery": { "hashes": [ @@ -3398,25 +3435,6 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, - "gql": { - "extras": [ - "requests" - ], - "hashes": [ - "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", - "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479" - ], - "markers": "python_full_version >= '3.8.1'", - "version": "==4.0.0" - }, - "graphql-core": { - "hashes": [ - "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", - "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==3.2.8" - }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -3502,19 +3520,19 @@ }, "identify": { "hashes": [ - "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", - "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737" + "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", + "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842" ], "markers": "python_version >= '3.10'", - "version": "==2.6.18" + "version": "==2.6.19" }, "idna": { "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", + "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" ], "markers": "python_version >= '3.8'", - "version": "==3.11" + "version": "==3.13" }, "isodate": { "hashes": [ @@ -3540,14 +3558,6 @@ "markers": "python_version >= '3.9'", "version": "==1.1.0" }, - "junitparser": { - "hashes": [ - "sha256:9e279f2214dc74b6a86b22db757abda2e8e66e819fe882dad5b392d57024cd26", - "sha256:f15e292877258d7c5755d672ce86f82c3622c7ea4c2f44f55de44ed7518484d3" - ], - "markers": "python_version >= '3.10'", - "version": "==5.0.0" - }, "kiwisolver": { "hashes": [ "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", @@ -3673,12 +3683,12 @@ }, "koku-nise": { "hashes": [ - "sha256:5ac277879ad686c3f0b9fbd8084e48868a949948f293783d61ad8c219f9a650c", - "sha256:b189f3aad01b698228459a757bfd14af23ec177f582bcf528b897fe02bbae893" + "sha256:2d8c03b9a27f8f9f5caf87ca68b4a6ba23b4d7e6291f69ad0b01655d96e64c51", + "sha256:42b6db673060c9ed23c4bc2badb57fb840a8dccf09d2ec933194aa37e926439d" ], "index": "pypi", "markers": "python_version >= '3.11'", - "version": "==5.4.0" + "version": "==5.4.1" }, "kombu": { "hashes": [ @@ -3794,65 +3804,65 @@ }, "matplotlib": { "hashes": [ - "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", - "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", - "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", - "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", - "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", - "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", - "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", - "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", - "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", - "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", - "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", - "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", - "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", - "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", - "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", - "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", - "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", - "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", - "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", - "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", - "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", - "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", - "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", - "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", - "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", - "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", - "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", - "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", - "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", - "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", - "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", - "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", - "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", - "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", - "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", - "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", - "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", - "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", - "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", - "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", - "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", - "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", - "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", - "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", - "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", - "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", - "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", - "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", - "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", - "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", - "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", - "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", - "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", - "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", - "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7" + "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", + "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", + "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", + "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", + "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", + "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", + "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", + "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", + "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", + "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", + "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", + "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", + "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", + "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", + "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", + "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", + "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", + "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", + "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", + "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", + "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", + "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", + "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", + "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", + "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", + "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", + "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", + "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", + "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", + "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", + "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", + "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", + "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", + "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", + "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", + "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", + "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", + "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", + "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", + "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", + "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", + "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", + "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", + "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", + "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", + "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", + "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", + "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", + "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", + "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", + "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", + "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", + "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", + "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", + "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==3.10.8" + "version": "==3.10.9" }, "mccabe": { "hashes": [ @@ -3871,158 +3881,6 @@ "markers": "python_version >= '3.10'", "version": "==1.23.4" }, - "multidict": { - "hashes": [ - "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", - "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", - "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", - "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", - "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", - "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", - "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", - "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", - "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", - "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", - "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", - "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", - "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", - "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", - "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", - "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", - "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", - "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", - "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", - "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", - "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", - "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", - "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", - "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", - "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", - "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", - "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", - "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", - "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", - "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", - "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", - "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", - "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", - "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", - "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", - "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", - "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", - "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", - "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", - "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", - "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", - "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", - "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", - "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", - "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", - "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", - "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", - "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", - "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", - "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", - "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", - "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", - "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", - "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", - "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", - "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", - "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", - "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", - "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", - "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", - "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", - "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", - "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", - "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", - "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", - "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", - "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", - "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", - "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", - "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", - "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", - "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", - "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", - "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", - "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", - "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", - "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", - "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", - "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", - "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", - "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", - "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", - "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", - "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", - "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", - "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", - "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", - "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", - "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", - "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", - "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", - "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", - "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", - "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", - "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", - "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", - "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", - "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", - "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", - "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", - "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", - "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", - "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", - "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", - "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", - "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", - "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", - "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", - "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", - "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", - "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", - "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", - "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", - "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", - "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", - "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", - "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", - "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", - "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", - "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", - "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", - "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", - "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", - "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", - "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", - "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", - "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", - "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", - "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", - "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", - "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", - "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", - "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", - "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", - "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", - "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", - "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", - "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", - "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", - "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", - "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", - "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", - "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", - "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", - "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", - "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" - ], - "markers": "python_version >= '3.9'", - "version": "==6.7.1" - }, "nodeenv": { "hashes": [ "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", @@ -4117,29 +3975,14 @@ "markers": "python_version >= '3.8'", "version": "==3.3.1" }, - "ocviapy": { - "hashes": [ - "sha256:77602d07d1f124e6f020e08ce21379e8a12cbefa98b168e697fdc202c770f9e2", - "sha256:e5558ec5c46d2793949c0b2fd1c99759d5b5ee564e2ac9ec56fd200c94dcaef4" - ], - "markers": "python_version >= '3.6'", - "version": "==1.6.0" - }, "packaging": { "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.0" - }, - "parsedatetime": { - "hashes": [ - "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", - "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" - ], - "version": "==2.6" + "version": "==26.2" }, "pillow": { "hashes": [ @@ -4265,12 +4108,12 @@ }, "pre-commit": { "hashes": [ - "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", - "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61" + "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", + "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==4.5.1" + "version": "==4.6.0" }, "prometheus-client": { "hashes": [ @@ -4289,134 +4132,6 @@ "markers": "python_version >= '3.8'", "version": "==3.0.52" }, - "propcache": { - "hashes": [ - "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", - "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", - "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", - "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", - "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", - "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", - "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", - "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", - "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", - "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", - "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", - "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", - "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", - "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", - "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", - "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", - "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", - "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", - "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", - "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", - "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", - "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", - "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", - "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", - "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", - "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", - "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", - "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", - "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", - "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", - "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", - "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", - "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", - "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", - "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", - "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", - "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", - "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", - "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", - "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", - "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", - "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", - "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", - "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", - "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", - "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", - "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", - "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", - "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", - "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", - "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", - "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", - "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", - "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", - "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", - "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", - "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", - "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", - "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", - "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", - "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", - "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", - "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", - "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", - "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", - "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", - "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", - "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", - "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", - "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", - "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", - "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", - "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", - "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", - "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", - "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", - "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", - "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", - "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", - "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", - "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", - "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", - "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", - "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", - "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", - "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", - "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", - "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", - "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", - "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", - "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", - "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", - "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", - "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", - "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", - "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", - "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", - "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", - "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", - "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", - "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", - "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", - "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", - "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", - "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", - "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", - "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", - "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", - "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", - "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", - "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", - "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", - "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", - "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", - "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", - "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", - "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", - "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", - "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", - "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", - "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", - "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.1" - }, "proto-plus": { "hashes": [ "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", @@ -4470,7 +4185,7 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "implementation_name != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.0" }, "pydocstyle": { @@ -4512,7 +4227,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-discovery": { @@ -4523,14 +4238,6 @@ "markers": "python_version >= '3.8'", "version": "==1.2.2" }, - "python-dotenv": { - "hashes": [ - "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", - "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" - ], - "markers": "python_version >= '3.10'", - "version": "==1.2.2" - }, "pytz": { "hashes": [ "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", @@ -4644,14 +4351,6 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, "responses": { "hashes": [ "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", @@ -4663,26 +4362,18 @@ }, "s3transfer": { "hashes": [ - "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", - "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" + "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", + "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" ], "markers": "python_version >= '3.9'", - "version": "==0.16.0" - }, - "sh": { - "hashes": [ - "sha256:653227a7c41a284ec5302173fbc044ee817c7bad5e6e4d8d55741b9aeb9eb65b", - "sha256:e0b15b4ae8ffcd399bc8ffddcbd770a43c7a70a24b16773fbb34c001ad5d52af" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", - "version": "==2.2.2" + "version": "==0.16.1" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.17.0" }, "snakeviz": { @@ -4699,7 +4390,7 @@ "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895" ], - "markers": "python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version not in '3.0, 3.1, 3.2'", "version": "==3.0.1" }, "sqlparse": { @@ -4711,14 +4402,6 @@ "markers": "python_version >= '3.8'", "version": "==0.5.5" }, - "tabulate": { - "hashes": [ - "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", - "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3" - ], - "markers": "python_version >= '3.10'", - "version": "==0.10.0" - }, "tblib": { "hashes": [ "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", @@ -4753,14 +4436,6 @@ "markers": "python_version >= '3.9'", "version": "==4.26.0" }, - "truststore": { - "hashes": [ - "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", - "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981" - ], - "markers": "python_version >= '3.10'", - "version": "==0.10.4" - }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", @@ -4771,11 +4446,11 @@ }, "tzdata": { "hashes": [ - "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", - "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" + "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", + "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" ], "markers": "python_version >= '2'", - "version": "==2026.1" + "version": "==2026.2" }, "tzlocal": { "hashes": [ @@ -4803,19 +4478,11 @@ }, "virtualenv": { "hashes": [ - "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", - "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2" + "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", + "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e" ], "markers": "python_version >= '3.8'", - "version": "==21.2.1" - }, - "wait-for": { - "hashes": [ - "sha256:1129f3350e29b0600889e24328d685a6bff048c8f4cabce28ef7632ed40c5d91", - "sha256:5642975f1fc5850acb55684b2d7842bd820fb068e725cd4ffff4bf3eba8e2788" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" + "version": "==21.3.0" }, "watchdog": { "hashes": [ @@ -4869,140 +4536,6 @@ ], "markers": "python_version >= '3.9'", "version": "==1.9.0" - }, - "yarl": { - "hashes": [ - "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", - "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", - "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", - "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", - "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", - "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", - "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", - "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", - "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", - "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", - "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", - "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", - "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", - "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", - "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", - "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", - "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", - "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", - "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", - "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", - "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", - "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", - "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", - "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", - "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", - "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", - "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", - "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", - "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", - "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", - "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", - "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", - "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", - "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", - "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", - "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", - "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", - "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", - "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", - "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", - "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", - "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", - "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", - "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", - "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", - "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", - "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", - "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", - "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", - "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", - "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", - "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", - "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", - "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", - "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", - "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", - "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", - "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", - "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", - "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", - "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", - "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", - "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", - "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", - "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", - "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", - "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", - "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", - "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", - "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", - "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", - "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", - "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", - "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", - "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", - "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", - "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", - "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", - "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", - "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", - "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", - "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", - "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", - "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", - "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", - "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", - "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", - "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", - "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", - "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", - "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", - "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", - "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", - "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", - "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", - "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", - "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", - "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", - "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", - "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", - "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", - "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", - "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", - "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", - "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", - "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", - "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", - "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", - "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", - "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", - "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", - "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", - "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", - "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", - "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", - "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", - "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", - "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", - "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", - "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", - "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", - "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", - "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", - "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", - "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", - "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", - "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", - "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d" - ], - "markers": "python_version >= '3.10'", - "version": "==1.23.0" } } } diff --git a/dev/containers/minio/.gitignore b/dev/containers/minio/.gitignore deleted file mode 100644 index 668823aafa..0000000000 --- a/dev/containers/minio/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Ignore all files in this dir... -* - -# ... except for this one. -!.gitignore From cd56221251e98883f339585cbad43a58c3fcc1fa Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 17:26:08 +0300 Subject: [PATCH 066/106] COST-7252: Restore dev/containers/minio/.gitignore Made-with: Cursor --- dev/containers/minio/.gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 dev/containers/minio/.gitignore diff --git a/dev/containers/minio/.gitignore b/dev/containers/minio/.gitignore new file mode 100644 index 0000000000..668823aafa --- /dev/null +++ b/dev/containers/minio/.gitignore @@ -0,0 +1,5 @@ +# Ignore all files in this dir... +* + +# ... except for this one. +!.gitignore From e51bb780d416dc640e0cffd0109de63b3ed6725d Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 17:38:19 +0300 Subject: [PATCH 067/106] COST-7252: Sync migrations with model changes Remove version field from StaticExchangeRate migration and add migration to allow blank currency_type on ExchangeRates. Made-with: Cursor --- .../0072_alter_exchangerates_currency_type.py | 18 ++++++++++++++++++ .../migrations/0014_constant_currency.py | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 koku/api/migrations/0072_alter_exchangerates_currency_type.py diff --git a/koku/api/migrations/0072_alter_exchangerates_currency_type.py b/koku/api/migrations/0072_alter_exchangerates_currency_type.py new file mode 100644 index 0000000000..bd071a5658 --- /dev/null +++ b/koku/api/migrations/0072_alter_exchangerates_currency_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.13 on 2026-04-28 14:34 +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0071_sources_updated_timestamp'), + ] + + operations = [ + migrations.AlterField( + model_name='exchangerates', + name='currency_type', + field=models.CharField(blank=True, max_length=5), + ), + ] diff --git a/koku/cost_models/migrations/0014_constant_currency.py b/koku/cost_models/migrations/0014_constant_currency.py index 41c770dde4..7cf8ab5583 100644 --- a/koku/cost_models/migrations/0014_constant_currency.py +++ b/koku/cost_models/migrations/0014_constant_currency.py @@ -28,14 +28,13 @@ class Migration(migrations.Migration): ("exchange_rate", models.DecimalField(decimal_places=15, max_digits=33)), ("start_date", models.DateField()), ("end_date", models.DateField()), - ("version", models.IntegerField(default=1)), ("created_timestamp", models.DateTimeField(auto_now_add=True)), ("updated_timestamp", models.DateTimeField(auto_now=True)), ], options={ "db_table": "static_exchange_rate", "ordering": ["-updated_timestamp"], - "unique_together": {("base_currency", "target_currency", "start_date", "end_date", "version")}, + "unique_together": {("base_currency", "target_currency", "start_date", "end_date")}, }, ), migrations.CreateModel( From 13506eb7ee21e097e8b072a23702fc7bb1e5b808 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 17:41:59 +0300 Subject: [PATCH 068/106] COST-7252: Remove unnecessary blank=True from ExchangeRates.currency_type The field is always populated with a currency code; blank values are never used. Also removes the choices constraint to stop migration churn when new currencies are added. Made-with: Cursor --- koku/api/currency/models.py | 2 +- koku/api/migrations/0072_alter_exchangerates_currency_type.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/koku/api/currency/models.py b/koku/api/currency/models.py index 096379ec5d..a9ac27e353 100644 --- a/koku/api/currency/models.py +++ b/koku/api/currency/models.py @@ -11,7 +11,7 @@ class ExchangeRates(models.Model): - currency_type = models.CharField(max_length=5, unique=False, blank=True) + currency_type = models.CharField(max_length=5, unique=False) exchange_rate = models.FloatField(default=0) diff --git a/koku/api/migrations/0072_alter_exchangerates_currency_type.py b/koku/api/migrations/0072_alter_exchangerates_currency_type.py index bd071a5658..63047c64f6 100644 --- a/koku/api/migrations/0072_alter_exchangerates_currency_type.py +++ b/koku/api/migrations/0072_alter_exchangerates_currency_type.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='exchangerates', name='currency_type', - field=models.CharField(blank=True, max_length=5), + field=models.CharField(max_length=5), ), ] From 55cfb398aff17dfb108eae5452b3d8bc40cba45b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 17:47:14 +0300 Subject: [PATCH 069/106] COST-7252: Fix pre-commit violations (black formatting, unused imports) Made-with: Cursor --- .../migrations/0072_alter_exchangerates_currency_type.py | 6 +++--- koku/api/report/queries.py | 1 - koku/cost_models/exchange_rate_annotations.py | 2 +- koku/cost_models/static_exchange_rate_view.py | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/koku/api/migrations/0072_alter_exchangerates_currency_type.py b/koku/api/migrations/0072_alter_exchangerates_currency_type.py index 63047c64f6..9af285dfd2 100644 --- a/koku/api/migrations/0072_alter_exchangerates_currency_type.py +++ b/koku/api/migrations/0072_alter_exchangerates_currency_type.py @@ -6,13 +6,13 @@ class Migration(migrations.Migration): dependencies = [ - ('api', '0071_sources_updated_timestamp'), + ("api", "0071_sources_updated_timestamp"), ] operations = [ migrations.AlterField( - model_name='exchangerates', - name='currency_type', + model_name="exchangerates", + name="currency_type", field=models.CharField(max_length=5), ), ] diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 7ee5f27694..3f84144a62 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -21,7 +21,6 @@ import ciso8601 import numpy as np import pandas as pd -from dateutil.relativedelta import relativedelta from django.contrib.postgres.aggregates import ArrayAgg from django.db.models import Case from django.db.models import CharField diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 4f9dc7703e..70822c47df 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -73,7 +73,7 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): cost_model_currency = Subquery( CostModel.objects.filter( costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), - ).values("currency")[:1] + ).values("currency")[:1], ) return { diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index 1688df74cd..d4cfa302bb 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -11,7 +11,6 @@ from django_filters import FilterSet from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets -from rest_framework.response import Response from api.common import log_json from api.common.pagination import ListPaginator From ddf9f79d44c6b47bd3e76b3191d4f1b073b65eed Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 17:50:19 +0300 Subject: [PATCH 070/106] COST-7252: Remove static-rate enablement bypass from architecture docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Report dropdown is governed solely by EnabledCurrency — defining a static exchange rate does not automatically make its currencies available. The settings admin page shows static rates regardless for management. Made-with: Cursor --- docs/architecture/constant-currency/README.md | 11 ++-- .../constant-currency/api-and-frontend.md | 38 ++++++-------- .../constant-currency/data-model.md | 22 ++++---- .../constant-currency/phased-delivery.md | 4 +- .../constant-currency/pipeline-changes.md | 52 ++++++------------- 5 files changed, 50 insertions(+), 77 deletions(-) diff --git a/docs/architecture/constant-currency/README.md b/docs/architecture/constant-currency/README.md index 133fdd3268..a8d9624e06 100644 --- a/docs/architecture/constant-currency/README.md +++ b/docs/architecture/constant-currency/README.md @@ -142,9 +142,6 @@ currencies appear in their UI. In on-premise environments, customers may only need a small subset of the ~300 ISO 4217 currencies. Showing all currencies by default would clutter the dropdown. -**Exception**: Static exchange rate pairs always make their currencies available -in the dropdown, regardless of `EnabledCurrency` status. If an administrator -defines a `USD→EUR` static rate, both `USD` and `EUR` are immediately available. ### IQ-6: Rate resolution without `CURRENCY_URL` — RESOLVED @@ -263,8 +260,7 @@ graph LR USER["Price List Admin"] -->|CRUD| SER["Serializer"] SER -->|"write canonical
rate record"| STATIC["StaticExchangeRate
(tenant schema)"] SER -->|"Writer 2: upsert
rate_type=static"| MER - EC -->|"dropdown filter:
enabled dynamic ∪
static rate currencies"| DD["Target Currency
Dropdown"] - STATIC -->|"static currencies
always available"| DD + EC -->|"dropdown filter:
enabled currencies only"| DD["Target Currency
Dropdown"] ``` **Key changes**: @@ -274,7 +270,7 @@ graph LR 3. **Rate resolution**: All months read from `MonthlyExchangeRate`; M2 migration seeds current-month data at deployment; pre-deployment months fall back to earliest available rate; error if no rate exists at all for a currency pair 4. Report responses include rate provenance metadata 5. **Currency enablement**: Dynamic currencies arrive as disabled; administrator enables them via Settings to make them visible in the dropdown (all currencies are always stored) -6. **Dropdown visibility**: Target currency dropdown shows only the union of enabled dynamic currencies and static rate currencies (disabled currencies are stored but hidden from the dropdown) +6. **Dropdown visibility**: Target currency dropdown shows only currencies that an administrator has explicitly enabled (static rate currencies still require enablement) 7. **No-rate error**: If user selects a currency with no conversion path from the bill currency, an actionable error is returned --- @@ -295,7 +291,7 @@ graph LR | 10 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. | | 11 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback, error if neither). Documentation references `open.er-api.com` (free tier) as the production example | | 12 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection | -| 13 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `EnabledCurrency` status | +| 13 | **Enablement is always required for reports** | Static exchange rate currencies must still be explicitly enabled to appear in the report dropdown. The settings admin page shows them regardless for management purposes. | --- @@ -313,3 +309,4 @@ graph LR | v1.7 | 2026-04-12 | Updated data flow diagram: query handler uses `Subquery` annotation instead of `Case`/`When`. | | v1.8 | 2026-04-13 | Synced pre-deployment month references: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). | | v1.9 | 2026-04-28 | Updated currency enablement URL reference to `settings/currency/exchange_rate/{code}/enable/`. | +| v2.0 | 2026-04-28 | Removed static-rate enablement bypass (decision #13). Report dropdown governed solely by `EnabledCurrency`; settings admin page shows static rates regardless. | diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 8aaa679700..9f825d45bd 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -192,39 +192,35 @@ always available from Babel. ## Available Currencies for Dropdown -The target currency dropdown in the UI computes its list of available -currencies from two sources: +The target currency dropdown in the UI shows only currencies that an +administrator has explicitly enabled. -### Availability Rules +### Availability Rule | Source | Rule | Example | |--------|------|---------| -| **Dynamic** | Currency exists in `EnabledCurrency` table | USD, EUR enabled → appear in dropdown | -| **Static** | Currency appears in any `StaticExchangeRate` pair (as base or target) | Static rate EUR→CHF defined → both EUR and CHF appear in dropdown regardless of `EnabledCurrency` status | +| **EnabledCurrency** | Currency exists in `EnabledCurrency` table | USD, EUR enabled → appear in dropdown | -### Data Source +Defining a static exchange rate does **not** automatically make its currencies +available in the report dropdown. The administrator must explicitly enable them. -The `GET settings/currency/exchange_rate/` endpoint returns all currencies that -have exchange rates, along with their `enabled` status. The frontend uses this -to populate the dropdown — no separate available-currencies endpoint is needed. +The settings admin page (`GET settings/currency/exchange_rate/`) shows all +currencies with static rates regardless of enabled status, so the administrator +can see and manage exchange rates without needing to enable currencies first. ### No Currencies Available -When **no currencies are available at all** — meaning: - -- No currencies exist in `EnabledCurrency` (none enabled), **and** -- No `StaticExchangeRate` rows exist (no static rates) - -Then the currency dropdown should either be **hidden** or show a message: +When no currencies exist in `EnabledCurrency` (none enabled), the currency +dropdown should either be **hidden** or show a message: *"No exchange rates available."* Whichever approach is simpler to implement. --- ## Corner Case: No Exchange Rate -A currency may appear in the dropdown (because it has static or enabled dynamic -rates) but have **no exchange rate path** from the bill's source currency to -the selected target currency. +A currency may appear in the dropdown (because it is enabled) but have **no +exchange rate path** from the bill's source currency to the selected target +currency. **Example**: - Cloud bill arrives in `USD` @@ -353,9 +349,8 @@ The frontend will: - Show a note explaining dynamic rates are used when no static rate is defined - **Add a currency enablement toggle** using `POST/DELETE settings/currency/exchange_rate/{code}/enable/` -- **Populate the target currency dropdown** from the exchange rate list response - (union of enabled dynamic currencies + static rate currencies). - Disabled currencies are stored but hidden from this dropdown. +- **Populate the target currency dropdown** from enabled currencies only. + Disabled currencies are stored but hidden from the report dropdown. - **Handle the no-rate error**: When the user selects a target currency that has no conversion path from the bill currency, display the error message returned by the API @@ -388,3 +383,4 @@ The frontend will: | v1.6 | 2026-04-12 | Updated `exchange_rates_applied` implementation to reflect `Subquery`-based rate resolution (removed `effective_exchange_rates` reference). | | v1.7 | 2026-04-13 | Removed stale "snapshotted" terminology (remnant from `MonthlyExchangeRateSnapshot` rename). | | v1.8 | 2026-04-28 | Consolidated endpoints under `settings/currency/exchange_rate/`. List returns grouped response with enabled status. Currency enablement via POST/DELETE (no body). Removed separate `AllCurrencyView` and `available-currencies` endpoints. | +| v1.9 | 2026-04-28 | Removed static-rate enablement bypass. Report dropdown governed solely by `EnabledCurrency`. Settings admin page shows static rates regardless for management. | diff --git a/docs/architecture/constant-currency/data-model.md b/docs/architecture/constant-currency/data-model.md index 0cd8db76ea..a419722a86 100644 --- a/docs/architecture/constant-currency/data-model.md +++ b/docs/architecture/constant-currency/data-model.md @@ -147,19 +147,20 @@ enabled. | Administrator disables a currency via `DELETE settings/currency/exchange_rate/{code}/enable/` | Removes the `EnabledCurrency` row for that currency | | `GET settings/currency/exchange_rate/` | Returns currencies that have exchange rates, grouped by target currency, with `enabled` flag based on `EnabledCurrency` table membership | -**How currencies become "available" in dropdowns**: +**How currencies become "available" in the report dropdown**: -A currency is visible in the target currency dropdown if **any** of the -following are true: +A currency is visible in the target currency dropdown only if it exists in the +`EnabledCurrency` table. Defining a static exchange rate does **not** automatically +make its currencies available — the administrator must explicitly enable them. -1. It exists in the `EnabledCurrency` table -2. It appears in any `StaticExchangeRate` pair (static rates make their currencies - visible regardless of `EnabledCurrency` status) +The settings admin page (`GET settings/currency/exchange_rate/`) shows all +currencies with static rates regardless of enabled status, so the administrator +can see and manage them. -**Corner case — no usable rate**: A currency may be available in the dropdown but -have no exchange rate path from the bill's source currency. In this case, the API -returns an error: *"No exchange rate available. Ask your administrator to configure -static exchange rates or enable dynamic exchange rates."* See +**Corner case — no usable rate**: A currency may be enabled but have no exchange +rate path from the bill's source currency. In this case, the API returns an +error: *"No exchange rate available. Ask your administrator to configure static +exchange rates or enable dynamic exchange rates."* See [api-and-frontend.md § Corner Case: No Exchange Rate](./api-and-frontend.md#corner-case-no-exchange-rate). **No `CURRENCY_URL` configured**: When the URL is not set, no dynamic currencies @@ -376,3 +377,4 @@ changes required. | v1.8 | 2026-04-12 | Updated reader description to reflect `Subquery`-based rate resolution (replaces `Case`/`When`). | | v1.9 | 2026-04-13 | Fixed `ExchangeRates` model description: actual fields are `currency_type` (CharField) and `exchange_rate` (FloatField), not `base_currency`/`exchange_rates` JSONField. Removed non-existent `updated_timestamp` column from `ExchangeRateDictionary` example. | | v2.0 | 2026-04-28 | Updated `EnabledCurrency` lifecycle to reflect POST/DELETE per-currency enablement at `settings/currency/exchange_rate/{code}/enable/`. | +| v2.1 | 2026-04-28 | Removed static-rate enablement bypass. Report dropdown governed solely by `EnabledCurrency`. | diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index f7718c2e5a..b5465c1c7a 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -76,8 +76,7 @@ pairs. Show rate provenance in report responses. - [ ] **Currency enablement**: All currencies are stored in `MonthlyExchangeRate` regardless of enabled status; `enabled` flag only controls dropdown visibility - [ ] **Rate resolution**: Static rates take precedence over dynamic rates; error returned if neither exists for a given pair - [ ] **No `CURRENCY_URL`**: Celery task skips API fetch; system works with whatever rates are available -- [ ] **Available currencies**: Dropdown shows only enabled currencies and static rate currencies -- [ ] **Available currencies**: Static rate currencies appear regardless of `EnabledCurrency` status +- [ ] **Available currencies**: Report dropdown shows only enabled currencies (static rates do not bypass enablement) - [ ] **No-rate corner case**: Selecting a target currency with no conversion path returns HTTP 400 with actionable error - [ ] **No currencies available**: Dropdown hidden or shows "No exchange rates available" when no currencies are available - [ ] **Currency list**: `GET settings/currency/exchange_rate/` returns currencies grouped by target currency with enabled flag and nested exchange rates @@ -177,3 +176,4 @@ design would be needed to handle path prioritization. | v1.9 | 2026-04-12 | R5 mitigated (Subquery replaces Case/When). Updated validation to reflect Subquery approach. | | v2.0 | 2026-04-13 | Updated pre-deployment month validation item: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). | | v2.1 | 2026-04-28 | Updated URL references to `settings/currency/exchange_rate/`. Consolidated URL registration to `koku/api/urls.py`. Removed separate available-currencies endpoint. | +| v2.2 | 2026-04-28 | Removed static-rate enablement bypass from validation checklist. Report dropdown governed solely by `EnabledCurrency`. | diff --git a/docs/architecture/constant-currency/pipeline-changes.md b/docs/architecture/constant-currency/pipeline-changes.md index 60a9fb69ae..b9b71ff813 100644 --- a/docs/architecture/constant-currency/pipeline-changes.md +++ b/docs/architecture/constant-currency/pipeline-changes.md @@ -93,9 +93,8 @@ At query time: 9. **CHANGED**: Per-month rate resolution via `Subquery` annotation (replaces single-rate `Case`/`When`) 10. **NEW**: Report response includes `exchange_rates_applied` metadata -11. **NEW**: Available currencies for dropdown computed from enabled dynamic - currencies + static rate currencies (`enabled` flag controls dropdown - visibility only, not storage) +11. **NEW**: Available currencies for dropdown computed from `EnabledCurrency` + table only (static rates do not bypass enablement) ### Single Source of Truth: `MonthlyExchangeRate` @@ -493,46 +492,24 @@ def _validate_exchange_rates(self, queryset): ### New: Available Currency Resolution -The query handler (or a shared utility) computes the list of currencies -visible in the target currency dropdown. A currency is **visible in the -dropdown** if any of the following are true: +The report dropdown shows only currencies that an administrator has explicitly +enabled via the `EnabledCurrency` table. Defining a static exchange rate does +**not** automatically make its currencies available in the report dropdown — the +administrator must still enable them. -1. It exists in the `EnabledCurrency` table -2. It appears as either `base_currency` or `target_currency` in any - `StaticExchangeRate` row (static rates make their currencies visible - regardless of `EnabledCurrency` status) +The settings admin page (`GET settings/currency/exchange_rate/`) shows all +currencies with static rates regardless of enabled status, so the administrator +can manage them without needing to enable them first. -```python -@cached_property -def available_currencies(self): - """Currencies visible in the target currency dropdown.""" - # Enabled currencies (presence in EnabledCurrency table = enabled) - enabled_codes = set( - EnabledCurrency.objects.values_list("currency_code", flat=True) - ) - - # Static: all currencies appearing in any static exchange rate pair - static_currencies = set( - StaticExchangeRate.objects.values_list("base_currency", flat=True) - ) | set( - StaticExchangeRate.objects.values_list("target_currency", flat=True) - ) - - return enabled_codes | static_currencies -``` - -When the user selects a target currency that is "available" but has **no -exchange rate path** from the bill's source currency (e.g., bill is in USD, user -selects EUR, but no USD→EUR rate exists — only EUR↔CHF and CNY↔SAR are defined), -the API returns an error rather than silently showing zero or unconverted costs: +When the user selects a currency that is enabled but has **no exchange rate +path** from the bill's source currency, the API returns an error rather than +silently showing zero or unconverted costs: > *"No exchange rate available. Ask your administrator to configure static > exchange rates or enable dynamic exchange rates."* -When **no currencies are visible** (no dynamic currencies enabled and no -static rates defined), the frontend either hides the currency dropdown -entirely or shows *"No exchange rates available."* — whichever is simpler to -implement. +When **no currencies are enabled**, the frontend either hides the currency +dropdown entirely or shows *"No exchange rates available."* See [api-and-frontend.md § Corner Case: No Exchange Rate](./api-and-frontend.md#corner-case-no-exchange-rate) for the full UX specification. @@ -669,3 +646,4 @@ per-source-type would miss cross-provider reports (e.g., OCP-on-AWS). | v2.0 | 2026-04-12 | Adopted `Subquery` approach for rate resolution (replaces `Case`/`When`). Removed `effective_exchange_rates` property. OCP uses nested `Subquery` for `source_uuid` → cost model currency resolution. R5 mitigated. | | v2.1 | 2026-04-12 | Pre-deployment months now fall back to earliest available rate instead of defaulting to 1. Added post-query validation that raises `ExchangeRateNotFound` when no rate exists for a currency pair. Removed `Value(Decimal("1"))` from `Coalesce` in both base and OCP annotations. | | v2.2 | 2026-04-13 | Fixed current pipeline description: `ExchangeRates` upserts per target currency (not base). Fixed "stored and stored" typo in available currency resolution. | +| v2.3 | 2026-04-28 | Removed static-rate enablement bypass from available currency resolution. Report dropdown governed solely by `EnabledCurrency`. | From 37bdbe3d75a44701265faaa78143d9d855da214b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 17:50:35 +0300 Subject: [PATCH 071/106] COST-7252: Fix black formatting for exchange_rate_annotations Made-with: Cursor --- koku/cost_models/exchange_rate_annotations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 70822c47df..5d6a61698c 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -71,9 +71,9 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - infra_exchange_rate: cloud bill currency (raw_currency column) """ cost_model_currency = Subquery( - CostModel.objects.filter( - costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), - ).values("currency")[:1], + CostModel.objects.filter(costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")),).values( + "currency" + )[:1], ) return { From 7640313f58763b5bd4dc665fd2a6db9b85ceb262 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 28 Apr 2026 18:53:34 +0300 Subject: [PATCH 072/106] COST-7252: Add migration seed, exchange rate validation, and costs-as-is support - Add RunPython seed step to migration 0014 to populate MonthlyExchangeRate with current-month dynamic rates from ExchangeRateDictionary at deploy time - Add ExchangeRateNotFound exception and pre-query validation in report queries and forecasts (returns HTTP 400 when target currency has no rates) - Skip validation when MonthlyExchangeRate is empty (feature not configured), allowing costs to be returned as-is in their original bill currency - Update architecture docs to document the costs-as-is use case, replace stale post-query validation pseudocode with actual implementation, and document the two-level validation behavior (empty table vs missing target) Made-with: Cursor --- docs/architecture/constant-currency/README.md | 24 +++--- .../constant-currency/api-and-frontend.md | 23 ++++-- .../constant-currency/data-model.md | 21 ++++-- .../constant-currency/phased-delivery.md | 6 +- .../constant-currency/pipeline-changes.md | 73 ++++++++----------- .../constant-currency/risk-register.md | 26 ++++--- koku/api/forecast/views.py | 6 +- koku/api/report/queries.py | 14 ++++ koku/api/report/view.py | 6 +- koku/cost_models/exchange_rate_annotations.py | 12 +++ .../migrations/0014_constant_currency.py | 30 ++++++++ koku/forecast/forecast.py | 8 ++ 12 files changed, 172 insertions(+), 77 deletions(-) diff --git a/docs/architecture/constant-currency/README.md b/docs/architecture/constant-currency/README.md index a8d9624e06..c3b1f4e7a8 100644 --- a/docs/architecture/constant-currency/README.md +++ b/docs/architecture/constant-currency/README.md @@ -150,15 +150,16 @@ configured (e.g., airgapped or disconnected deployments)? **Resolution**: The system does not require `CURRENCY_URL` to function. Rate resolution follows a simple priority: **static rates first, dynamic rates as -fallback, error if neither exists** for a given currency pair. When -`CURRENCY_URL` is empty or unset: +fallback**. When `CURRENCY_URL` is empty or unset: - The daily Celery task skips the API fetch step (no dynamic rates are fetched) - Static exchange rates defined via the CRUD API work normally - If dynamic rates were previously fetched (before the URL was removed), they remain available as fallback -- If no rate exists for a given pair (static or dynamic), the API returns an - actionable error +- If `MonthlyExchangeRate` is completely empty (no rates configured at all), + the feature is inactive — validation is skipped and costs are returned as-is + in their original bill currency +- If rates exist but not for a given pair, the API returns an actionable error The `CURRENCY_URL` setting is documented with the production API URL (`open.er-api.com`) as a reference example. Only the free tier of the Open @@ -167,7 +168,9 @@ Exchange Rates API is supported in this design. **Rationale**: The system should work with whatever data is available rather than treating the absence of `CURRENCY_URL` as a special mode. Customers can define their own exchange rates via the CRUD API regardless of whether dynamic -rates are being fetched. +rates are being fetched. Deployments that never configure exchange rates +continue to work exactly as before — costs are returned in their original +currency with no conversion. ### IQ-7: No-rate corner case — RESOLVED @@ -255,7 +258,7 @@ graph LR CT -->|"Writer 1: per-tenant
skip static pairs
all currencies"| MER["MonthlyExchangeRate
(tenant schema)
single source of truth"] MER -->|"all months:
per-month rates"| QH["QueryHandler
Subquery annotation"] QH -->|"per-month rates +
rate metadata"| REPORT["Report Response
+ exchange_rates_applied"] - QH -->|"no rate? →
actionable error"| ERR["Error: no exchange rate
available"] + QH -->|"no rate +
feature active? →
actionable error"| ERR["Error: no exchange rate
available"] ADMIN["CM Admin"] -->|"enable/disable
currencies"| EC USER["Price List Admin"] -->|CRUD| SER["Serializer"] SER -->|"write canonical
rate record"| STATIC["StaticExchangeRate
(tenant schema)"] @@ -267,11 +270,11 @@ graph LR 1. **Single source of truth**: `MonthlyExchangeRate` stores rates for all months (current and past); query handlers read from this one table 2. **Two writers**: Celery task writes dynamic rates daily for the current month; CRUD serializer writes static rates for affected months -3. **Rate resolution**: All months read from `MonthlyExchangeRate`; M2 migration seeds current-month data at deployment; pre-deployment months fall back to earliest available rate; error if no rate exists at all for a currency pair +3. **Rate resolution**: All months read from `MonthlyExchangeRate`; M2 migration seeds current-month data at deployment; pre-deployment months fall back to earliest available rate. When `MonthlyExchangeRate` is empty (feature not configured), costs are returned as-is; when rows exist but not for the target currency, an actionable error is returned 4. Report responses include rate provenance metadata 5. **Currency enablement**: Dynamic currencies arrive as disabled; administrator enables them via Settings to make them visible in the dropdown (all currencies are always stored) 6. **Dropdown visibility**: Target currency dropdown shows only currencies that an administrator has explicitly enabled (static rate currencies still require enablement) -7. **No-rate error**: If user selects a currency with no conversion path from the bill currency, an actionable error is returned +7. **No-rate handling**: If `MonthlyExchangeRate` is empty, the feature is inactive and costs are returned as-is. If rows exist but not for the selected currency, an actionable error is returned --- @@ -289,8 +292,8 @@ graph LR | 8 | **Forward-only with current-month seed** | M2 migration seeds current-month data from `ExchangeRateDictionary`; pre-deployment months fall back to earliest available rate | | 9 | **Per-pair rows, not JSON blob** | Enables `unique_together` constraint, simpler queries, cleaner ORM integration | | 10 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. | -| 11 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback, error if neither). Documentation references `open.er-api.com` (free tier) as the production example | -| 12 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection | +| 11 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback). Documentation references `open.er-api.com` (free tier) as the production example | +| 12 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection. When `MonthlyExchangeRate` is empty (no rates configured at all), the feature is inactive and costs are returned as-is | | 13 | **Enablement is always required for reports** | Static exchange rate currencies must still be explicitly enabled to appear in the report dropdown. The settings admin page shows them regardless for management purposes. | --- @@ -310,3 +313,4 @@ graph LR | v1.8 | 2026-04-13 | Synced pre-deployment month references: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). | | v1.9 | 2026-04-28 | Updated currency enablement URL reference to `settings/currency/exchange_rate/{code}/enable/`. | | v2.0 | 2026-04-28 | Removed static-rate enablement bypass (decision #13). Report dropdown governed solely by `EnabledCurrency`; settings admin page shows static rates regardless. | +| v2.1 | 2026-04-28 | Added "costs as-is" behavior: when `MonthlyExchangeRate` is empty, feature is inactive, validation skipped, costs returned in original currency. Updated IQ-6, decision #12, data flow key changes. | diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 9f825d45bd..6ecd640f7f 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -228,17 +228,26 @@ currency. - User wants to see costs in `EUR` - There is no `USD→EUR` rate (static or dynamic) -### Behavior (Preferred Approach) +### Behavior -**Make all available currencies visible** in the dropdown (`EUR`, `CHF`, `CNY`, -`SAR`), but when the user selects a target currency for which no conversion rate -exists from the bill currency, the API returns an error: +There are two distinct cases: + +**1. Feature not configured** (`MonthlyExchangeRate` is empty): When no exchange +rates have been configured at all (no `CURRENCY_URL`, no static rates, no Celery +task run), the constant currency feature is inactive. Validation is skipped and +costs are returned as-is in their original bill currency. The +`Coalesce("exchange_rate", Value(1))` fallback in provider maps ensures NULL +annotations resolve to `1` (no conversion). This is the default state for fresh +deployments. + +**2. Feature active but target currency has no rates** (`MonthlyExchangeRate` +has rows but none for the target): The API returns an error: ```json { "errors": [ { - "detail": "No exchange rate available between USD and EUR. Ask your administrator to configure static exchange rates or enable dynamic exchange rates.", + "detail": "No exchange rate available for EUR. Ask your administrator to configure static exchange rates or enable dynamic exchange rates.", "status": 400, "source": "currency" } @@ -250,6 +259,9 @@ The frontend should display this error message to the user. The report data is **not** returned with unconverted amounts — the request fails with a clear, actionable error. +**Make all available currencies visible** in the dropdown (`EUR`, `CHF`, `CNY`, +`SAR`). + **Rationale**: This approach was preferred over filtering the dropdown to only show currencies with available conversion paths because: @@ -384,3 +396,4 @@ The frontend will: | v1.7 | 2026-04-13 | Removed stale "snapshotted" terminology (remnant from `MonthlyExchangeRateSnapshot` rename). | | v1.8 | 2026-04-28 | Consolidated endpoints under `settings/currency/exchange_rate/`. List returns grouped response with enabled status. Currency enablement via POST/DELETE (no body). Removed separate `AllCurrencyView` and `available-currencies` endpoints. | | v1.9 | 2026-04-28 | Removed static-rate enablement bypass. Report dropdown governed solely by `EnabledCurrency`. Settings admin page shows static rates regardless for management. | +| v2.0 | 2026-04-28 | Added "costs as-is" behavior to Corner Case section: when `MonthlyExchangeRate` is empty, feature is inactive, costs returned in original currency. | diff --git a/docs/architecture/constant-currency/data-model.md b/docs/architecture/constant-currency/data-model.md index a419722a86..050bc2f8a5 100644 --- a/docs/architecture/constant-currency/data-model.md +++ b/docs/architecture/constant-currency/data-model.md @@ -158,18 +158,24 @@ currencies with static rates regardless of enabled status, so the administrator can see and manage them. **Corner case — no usable rate**: A currency may be enabled but have no exchange -rate path from the bill's source currency. In this case, the API returns an -error: *"No exchange rate available. Ask your administrator to configure static -exchange rates or enable dynamic exchange rates."* See +rate path from the bill's source currency. If `MonthlyExchangeRate` has rows +(the feature is active) but none for the requested target currency, the API +returns an error: *"No exchange rate available. Ask your administrator to +configure static exchange rates or enable dynamic exchange rates."* See [api-and-frontend.md § Corner Case: No Exchange Rate](./api-and-frontend.md#corner-case-no-exchange-rate). +**No rates configured at all**: When `MonthlyExchangeRate` is completely empty +(no `CURRENCY_URL` configured, no static rates defined, no Celery task run), +the constant currency feature is inactive. Validation is skipped and costs are +returned as-is in their original bill currency. The `Coalesce(..., Value(1))` +fallback in provider maps ensures exchange rate annotations resolve to `1` (no +conversion). This is the default state for fresh deployments. + **No `CURRENCY_URL` configured**: When the URL is not set, no dynamic currencies are discovered by the Celery task, so no rows are created automatically. The table may still contain previously fetched currencies or manually-created rows. -The system does not treat this as a special mode — it uses whatever rates are -available (static first, dynamic fallback, error if neither exists). If no -currencies are visible (all disabled and no static rates), the currency dropdown -is hidden or shows *"No exchange rates available."* +If no currencies are visible (all disabled and no static rates), the currency +dropdown is hidden or shows *"No exchange rates available."* **Registration points**: None. Accessed via the Settings API (see [api-and-frontend.md § Currency Enablement](./api-and-frontend.md#currency-enablement-settings-api)). @@ -378,3 +384,4 @@ changes required. | v1.9 | 2026-04-13 | Fixed `ExchangeRates` model description: actual fields are `currency_type` (CharField) and `exchange_rate` (FloatField), not `base_currency`/`exchange_rates` JSONField. Removed non-existent `updated_timestamp` column from `ExchangeRateDictionary` example. | | v2.0 | 2026-04-28 | Updated `EnabledCurrency` lifecycle to reflect POST/DELETE per-currency enablement at `settings/currency/exchange_rate/{code}/enable/`. | | v2.1 | 2026-04-28 | Removed static-rate enablement bypass. Report dropdown governed solely by `EnabledCurrency`. | +| v2.2 | 2026-04-28 | Added "costs as-is" behavior: when `MonthlyExchangeRate` is empty, feature is inactive, costs returned in original currency. | diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index b5465c1c7a..ce260082b6 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -74,10 +74,11 @@ pairs. Show rate provenance in report responses. - [ ] On-prem mode: full functionality without Trino - [ ] **Currency enablement**: Administrator can enable currencies via `POST settings/currency/exchange_rate/{code}/enable/` - [ ] **Currency enablement**: All currencies are stored in `MonthlyExchangeRate` regardless of enabled status; `enabled` flag only controls dropdown visibility -- [ ] **Rate resolution**: Static rates take precedence over dynamic rates; error returned if neither exists for a given pair +- [ ] **Rate resolution**: Static rates take precedence over dynamic rates; when `MonthlyExchangeRate` is empty (feature not configured), costs returned as-is; when rows exist but not for target, error returned - [ ] **No `CURRENCY_URL`**: Celery task skips API fetch; system works with whatever rates are available - [ ] **Available currencies**: Report dropdown shows only enabled currencies (static rates do not bypass enablement) -- [ ] **No-rate corner case**: Selecting a target currency with no conversion path returns HTTP 400 with actionable error +- [ ] **Costs as-is**: When no exchange rates are configured at all (`MonthlyExchangeRate` empty), validation skipped, costs returned in original bill currency +- [ ] **No-rate corner case**: Selecting a target currency with no conversion path (when rates exist for other currencies) returns HTTP 400 with actionable error - [ ] **No currencies available**: Dropdown hidden or shows "No exchange rates available" when no currencies are available - [ ] **Currency list**: `GET settings/currency/exchange_rate/` returns currencies grouped by target currency with enabled flag and nested exchange rates @@ -177,3 +178,4 @@ design would be needed to handle path prioritization. | v2.0 | 2026-04-13 | Updated pre-deployment month validation item: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). | | v2.1 | 2026-04-28 | Updated URL references to `settings/currency/exchange_rate/`. Consolidated URL registration to `koku/api/urls.py`. Removed separate available-currencies endpoint. | | v2.2 | 2026-04-28 | Removed static-rate enablement bypass from validation checklist. Report dropdown governed solely by `EnabledCurrency`. | +| v2.3 | 2026-04-28 | Added "costs as-is" validation item: when `MonthlyExchangeRate` is empty, feature inactive, costs returned as-is. Updated rate resolution and no-rate validation items. | diff --git a/docs/architecture/constant-currency/pipeline-changes.md b/docs/architecture/constant-currency/pipeline-changes.md index b9b71ff813..5df0a2b058 100644 --- a/docs/architecture/constant-currency/pipeline-changes.md +++ b/docs/architecture/constant-currency/pipeline-changes.md @@ -186,7 +186,9 @@ for tenant in Tenant.objects.exclude(schema_name="public"): - **URL check**: If `CURRENCY_URL` is not configured, the task skips the API fetch. Dynamic rates are simply not fetched; the system uses whatever rates - are available (static first, dynamic fallback, error if neither exists). + are available (static first, dynamic fallback). If `MonthlyExchangeRate` is + completely empty (no rates configured), the feature is inactive and costs are + returned as-is in their original bill currency. - **All currencies stored**: Upserts dynamic rates for all currency pairs returned by the API. The `EnabledCurrency` table controls dropdown visibility, not rate storage. Administrators enable currencies via the Settings API. @@ -440,55 +442,41 @@ an exception. **Risk linkage**: See [risk-register.md § R5](./risk-register.md#r5--query-handler-performance) -### Post-Query Exchange Rate Validation +### Pre-Query Exchange Rate Validation -After the queryset is evaluated but before the response is serialized, the query -handler checks for `NULL` exchange rate annotations. A `NULL` value means both -the exact-month and earliest-available subqueries returned no result — indicating -that `MonthlyExchangeRate` has no data at all for that currency pair. This is a -system configuration error, not normal operation. +Before executing the report query, the query handler validates that exchange +rate data exists for the requested target currency. This validation has two +levels: ```python -def _validate_exchange_rates(self, queryset): - """Raise if any rows have NULL exchange rates (no rate data for currency pair).""" - if not self.currency: - return - - null_rate_filter = Q(exchange_rate__isnull=True) - # OCP has a second annotation to check - if hasattr(self, "_mapper") and hasattr(self._mapper, "cost_units_key"): - null_rate_filter |= Q(infra_exchange_rate__isnull=True) - - if queryset.filter(null_rate_filter).exists(): - missing_currencies = ( - queryset.filter(null_rate_filter) - .values_list(self._mapper.cost_units_key, flat=True) - .distinct() - ) - raise ExchangeRateNotFound( - f"No exchange rates found for base currencies " - f"{list(missing_currencies)} → {self.currency}. " - f"MonthlyExchangeRate table may not be seeded." - ) +def _validate_exchange_rates(self, target_currency): + """Raise ExchangeRateNotFound if no MonthlyExchangeRate rows exist for the target currency. + + Skips validation when MonthlyExchangeRate is completely empty (feature not configured). + The Coalesce(..., Value(1)) fallback in provider maps ensures costs are returned as-is. + """ + with tenant_context(self.tenant): + if not MonthlyExchangeRate.objects.exists(): + return # feature not configured, costs returned as-is + if not MonthlyExchangeRate.objects.filter(target_currency=target_currency).exists(): + raise ExchangeRateNotFound(target_currency) ``` **Key design choices**: -- **Post-query, not pre-query**: The validation runs after the query because - the base currency varies per row (it comes from the data, not the request). - A pre-query check could only verify that *some* rates exist for the target - currency, but would miss cases where rates exist for one base currency but - not another. -- **Performance**: The `.filter(...).exists()` check is a lightweight query - that only adds overhead when the annotation produced `NULL` values. The - `.values_list().distinct()` for the error message only runs on the error - path. +- **Feature activation**: When `MonthlyExchangeRate` is completely empty (no + rates configured at all), the feature is inactive. Validation is skipped and + costs are returned as-is in their original bill currency. The + `Coalesce("exchange_rate", Value(1))` fallback in provider maps ensures NULL + annotations resolve to `1` (no conversion). +- **Pre-query check**: The validation runs before the query executes. It + verifies that *any* `MonthlyExchangeRate` row exists for the target currency. + Per-row mismatches (e.g., a specific base currency with no rate) are handled + gracefully by the `Coalesce` fallback to `1`. - **Exception type**: `ExchangeRateNotFound` is a custom exception caught by - the view layer and returned as an appropriate HTTP error response. This is a - system configuration issue (missing rate data), not a user input error. -- **OCP dual annotations**: The OCP query handler must validate both - `exchange_rate` and `infra_exchange_rate` since they resolve against - different base currencies (cost model currency vs cloud bill currency). + the view layer and returned as HTTP 400 with an actionable error message. + This only fires when rates are configured but not for the requested currency + — indicating an administrator configuration gap, not a system error. ### New: Available Currency Resolution @@ -647,3 +635,4 @@ per-source-type would miss cross-provider reports (e.g., OCP-on-AWS). | v2.1 | 2026-04-12 | Pre-deployment months now fall back to earliest available rate instead of defaulting to 1. Added post-query validation that raises `ExchangeRateNotFound` when no rate exists for a currency pair. Removed `Value(Decimal("1"))` from `Coalesce` in both base and OCP annotations. | | v2.2 | 2026-04-13 | Fixed current pipeline description: `ExchangeRates` upserts per target currency (not base). Fixed "stored and stored" typo in available currency resolution. | | v2.3 | 2026-04-28 | Removed static-rate enablement bypass from available currency resolution. Report dropdown governed solely by `EnabledCurrency`. | +| v2.4 | 2026-04-28 | Replaced post-query validation pseudocode with actual pre-query implementation. Added "costs as-is" behavior: when `MonthlyExchangeRate` is empty, feature inactive, validation skipped. | diff --git a/docs/architecture/constant-currency/risk-register.md b/docs/architecture/constant-currency/risk-register.md index 85c9037b8b..d977b79c8d 100644 --- a/docs/architecture/constant-currency/risk-register.md +++ b/docs/architecture/constant-currency/risk-register.md @@ -16,7 +16,7 @@ Single source of truth for risks related to the Constant Currency feature | **R5** | Query handler subquery performance with many months/currencies | Mitigated | 1 | `Subquery` approach uses indexed lookups instead of growing `CASE` expressions | | **R6** | Static rate deletion leaves gap before dynamic rate fills in | Mitigated | 1 | Serializer proactively populates dynamic rows on static rate deletion; no gap | | **R7** | User selects a target currency with no conversion path from bill currency | Mitigated | 1 | Show actionable error; currencies remain visible in dropdown | -| **R8** | No rates configured (static or dynamic) for a currency pair | Accepted | 1 | Static first, dynamic fallback, error if neither; hide dropdown when no currencies visible | +| **R8** | No rates configured (static or dynamic) for a currency pair | Accepted | 1 | If `MonthlyExchangeRate` is empty: feature inactive, costs as-is. If rows exist but not for target: error. | --- @@ -180,16 +180,23 @@ with unconverted or zero amounts. - No dynamic exchange rate exists for the pair (either `CURRENCY_URL` was never configured, the API never returned that pair, or no rates have been fetched yet) -The system does not treat this as a special mode — it simply has no data for -that conversion. +**Behavior**: Two distinct cases: -**Behavior**: Rate resolution follows a simple priority: +1. **`MonthlyExchangeRate` is completely empty** (feature not configured): The + constant currency feature is inactive. Validation is skipped entirely. The + `Coalesce("exchange_rate", Value(1))` fallback in provider maps ensures all + exchange rate annotations resolve to `1`, so costs are returned as-is in + their original bill currency. This is the default state for fresh deployments + without `CURRENCY_URL` or static rates. -1. **Static rates** — used if defined for the pair -2. **Dynamic rates** — used as fallback if no static rate exists -3. **Error** — if neither exists, the API returns an actionable error: - *"No exchange rate available. Ask your administrator to configure static - exchange rates or enable dynamic exchange rates."* +2. **`MonthlyExchangeRate` has rows but none for the target currency**: The + feature is active but the specific currency pair is missing. Rate resolution + follows the priority: + 1. **Static rates** — used if defined for the pair + 2. **Dynamic rates** — used as fallback if no static rate exists + 3. **Error** — if neither exists, the API returns an actionable error: + *"No exchange rate available. Ask your administrator to configure static + exchange rates or enable dynamic exchange rates."* If no currencies are visible at all (all disabled and no static rates), the currency dropdown is either hidden or shows *"No exchange rates available"*. @@ -234,3 +241,4 @@ R8 ✓ | v1.6 | 2026-04-12 | R5 mitigated: `Subquery` approach replaces `Case`/`When`, eliminating O(months × currencies) scaling concern. | | v1.7 | 2026-04-13 | R4: updated to reflect earliest-available-rate fallback for pre-deployment months (aligns with pipeline-changes.md v2.1). | | v1.8 | 2026-04-28 | R8: updated CRUD API URL to `settings/currency/exchange_rate/`. | +| v1.9 | 2026-04-28 | R8: added "costs as-is" behavior — when `MonthlyExchangeRate` is empty, feature is inactive, validation skipped, costs returned in original currency. | diff --git a/koku/api/forecast/views.py b/koku/api/forecast/views.py index f593753b27..9a3fd5c921 100644 --- a/koku/api/forecast/views.py +++ b/koku/api/forecast/views.py @@ -28,6 +28,7 @@ from api.forecast.serializers import OCPGCPCostForecastParamSerializer from api.provider.models import Provider from api.query_params import QueryParameters +from cost_models.exchange_rate_annotations import ExchangeRateNotFound from forecast import AWSForecast from forecast import AzureForecast from forecast import GCPForecast @@ -56,7 +57,10 @@ def get(self, request, **kwargs): return Response(data=exc.detail, status=status.HTTP_400_BAD_REQUEST) handler = self.query_handler(params) - output = handler.predict() + try: + output = handler.predict() + except ExchangeRateNotFound as exc: + return Response(data={"currency": [str(exc)]}, status=status.HTTP_400_BAD_REQUEST) LOG.debug(f"DATA: {output}") cost_type = params.parameters.get("cost_type") paginator = ForecastListPaginator(output, request, cost_type) diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index 3f84144a62..e2f722f204 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -44,6 +44,7 @@ from api.report.constants import AWS_CATEGORY_PREFIX from api.report.constants import TAG_PREFIX from api.report.constants import URL_ENCODED_SAFE +from cost_models.exchange_rate_annotations import ExchangeRateNotFound from cost_models.models import MonthlyExchangeRate LOG = logging.getLogger(__name__) @@ -1047,6 +1048,18 @@ def _apply_group_by(self, query_data, group_by=None): bucket_by_date[date] = grouped return bucket_by_date + def _validate_exchange_rates(self, target_currency): + """Raise ExchangeRateNotFound if no MonthlyExchangeRate rows exist for the target currency. + + Skips validation when MonthlyExchangeRate is completely empty (feature not configured). + The Coalesce(..., Value(1)) fallback in provider maps ensures costs are returned as-is. + """ + with tenant_context(self.tenant): + if not MonthlyExchangeRate.objects.exists(): + return + if not MonthlyExchangeRate.objects.filter(target_currency=target_currency).exists(): + raise ExchangeRateNotFound(target_currency) + def _initialize_response_output(self, parameters): """Initialize output response object.""" output = copy.deepcopy(parameters.parameters) @@ -1054,6 +1067,7 @@ def _initialize_response_output(self, parameters): output.pop("access") if self.currency: + self._validate_exchange_rates(self.currency) output["currency"] = self.currency start = self.start_datetime end = self.end_datetime diff --git a/koku/api/report/view.py b/koku/api/report/view.py index 77ad0a51d6..03f18ea64d 100644 --- a/koku/api/report/view.py +++ b/koku/api/report/view.py @@ -18,6 +18,7 @@ from api.common.pagination import ReportPagination from api.common.pagination import ReportRankedPagination from api.query_params import QueryParameters +from cost_models.exchange_rate_annotations import ExchangeRateNotFound LOG = logging.getLogger(__name__) @@ -71,7 +72,10 @@ def get(self, request, **kwargs): return Response(data=exc.detail, status=status.HTTP_400_BAD_REQUEST) handler = self.query_handler(params) - output = handler.execute_query() + try: + output = handler.execute_query() + except ExchangeRateNotFound as exc: + return Response(data={"currency": [str(exc)]}, status=status.HTTP_400_BAD_REQUEST) # reset the meta when order_by[date] is used if output.get("cost_explorer_order_by"): diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 5d6a61698c..34a1b2cc76 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -14,6 +14,18 @@ from cost_models.models import MonthlyExchangeRate +class ExchangeRateNotFound(Exception): + """Raised when no exchange rate exists for a required currency pair.""" + + def __init__(self, target_currency): + self.target_currency = target_currency + super().__init__( + f"No exchange rate available for {target_currency}. " + "Ask your administrator to configure static exchange rates " + "or enable dynamic exchange rates." + ) + + def _build_monthly_rate_annotation(base_currency, target_currency): """Build a Coalesce annotation that resolves exchange rates per month. diff --git a/koku/cost_models/migrations/0014_constant_currency.py b/koku/cost_models/migrations/0014_constant_currency.py index 7cf8ab5583..3bc03e491d 100644 --- a/koku/cost_models/migrations/0014_constant_currency.py +++ b/koku/cost_models/migrations/0014_constant_currency.py @@ -4,11 +4,40 @@ # """Add StaticExchangeRate, EnabledCurrency, and MonthlyExchangeRate models for constant currency.""" import uuid +from datetime import date from django.db import migrations from django.db import models +def seed_current_month(apps, schema_editor): + """Seed MonthlyExchangeRate with current-month dynamic rates from ExchangeRateDictionary.""" + ExchangeRateDictionary = apps.get_model("api", "ExchangeRateDictionary") + MonthlyExchangeRate = apps.get_model("cost_models", "MonthlyExchangeRate") + + erd = ExchangeRateDictionary.objects.first() + if not erd or not erd.currency_exchange_dictionary: + return + + current_month = date.today().replace(day=1) + + rows = [] + for base_cur, targets in erd.currency_exchange_dictionary.items(): + for target_cur, rate in targets.items(): + if base_cur == target_cur: + continue + rows.append( + MonthlyExchangeRate( + effective_date=current_month, + base_currency=base_cur, + target_currency=target_cur, + exchange_rate=rate, + rate_type="dynamic", + ) + ) + MonthlyExchangeRate.objects.bulk_create(rows, ignore_conflicts=True) + + class Migration(migrations.Migration): dependencies = [ @@ -72,4 +101,5 @@ class Migration(migrations.Migration): "unique_together": {("effective_date", "base_currency", "target_currency")}, }, ), + migrations.RunPython(code=seed_current_month, reverse_code=migrations.RunPython.noop), ] diff --git a/koku/forecast/forecast.py b/koku/forecast/forecast.py index 2521784e58..9061fef250 100644 --- a/koku/forecast/forecast.py +++ b/koku/forecast/forecast.py @@ -41,6 +41,8 @@ from api.utils import get_cost_type from cost_models.exchange_rate_annotations import build_exchange_rate_annotation_dict from cost_models.exchange_rate_annotations import build_ocp_exchange_rate_annotation_dict +from cost_models.exchange_rate_annotations import ExchangeRateNotFound +from cost_models.models import MonthlyExchangeRate from reporting.provider.aws.models import AWSOrganizationalUnit LOG = logging.getLogger(__name__) @@ -168,6 +170,12 @@ def predict(self): """Define ORM query to run forecast and return prediction.""" cost_predictions = {} with tenant_context(self.params.tenant): + if ( + self.currency + and MonthlyExchangeRate.objects.exists() + and not MonthlyExchangeRate.objects.filter(target_currency=self.currency).exists() + ): + raise ExchangeRateNotFound(self.currency) data = self.get_data() for fieldname in COST_FIELD_NAMES: From 3f212a5bd432c6499c5af5783f2bfb2065424968 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 12:09:31 +0300 Subject: [PATCH 073/106] COST-7252: Filter exchange_rates_applied to currencies present in report data Add _get_base_currencies_in_data() to ReportQueryHandler so the exchange_rates_applied metadata only includes rates whose base_currency actually appears in the query range. OCP overrides it to also consider cost model currencies. Update architecture docs to reflect per-month (ungrouped) rate entries and the currency filtering behavior. Made-with: Cursor --- Pipfile | 2 +- Pipfile.lock | 2 +- .../constant-currency/api-and-frontend.md | 29 ++++++++++++--- koku/api/report/ocp/query_handler.py | 21 +++++++++++ koku/api/report/queries.py | 35 +++++++++++++++++-- 5 files changed, 80 insertions(+), 9 deletions(-) diff --git a/Pipfile b/Pipfile index 6b2626d1eb..c82fb2e651 100644 --- a/Pipfile +++ b/Pipfile @@ -46,7 +46,6 @@ pydantic = ">=2" sqlparse = "*" packaging = "*" psycopg2-binary = "*" -Babel = "*" watchtower = "*" unleashclient = "*" kombu = "*" @@ -54,6 +53,7 @@ pandas = "<3.0" scipy = ">=1.16" boto3 = "*" sqlalchemy = ">=2.0.0" +babel = "*" [dev-packages] argh = ">=0.26.2" diff --git a/Pipfile.lock b/Pipfile.lock index 3619423ae2..efa3175337 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "076741f1d4c52a3ea84a9a18600a5de48c6106574cacae7db51b8e50808f33a1" + "sha256": "c219b511c58c2080e516c303829afc3977cc55f356cd020499f5d0d974a6da92" }, "pipfile-spec": 6, "requires": { diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 6ecd640f7f..283424fa28 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -295,6 +295,22 @@ transparency on which rates (static vs dynamic) were used and for which periods. "rate": "0.870000000000000", "type": "static", "start_date": "2026-01-01", + "end_date": "2026-01-31" + }, + { + "base_currency": "USD", + "target_currency": "EUR", + "rate": "0.870000000000000", + "type": "static", + "start_date": "2026-02-01", + "end_date": "2026-02-28" + }, + { + "base_currency": "USD", + "target_currency": "EUR", + "rate": "0.870000000000000", + "type": "static", + "start_date": "2026-03-01", "end_date": "2026-03-31" }, { @@ -320,10 +336,15 @@ transparency on which rates (static vs dynamic) were used and for which periods. **Implementation**: The response formatter queries `MonthlyExchangeRate` for the report's date range and target currency -(see [pipeline-changes.md § Rate Resolution](./pipeline-changes.md#rate-resolution-strategy)), -then groups consecutive months with the same rate and type into a single entry -with `start_date` / `end_date` boundaries (first-of-month and last-day-of-month -respectively). +(see [pipeline-changes.md § Rate Resolution](./pipeline-changes.md#rate-resolution-strategy)). +Each `MonthlyExchangeRate` row produces one entry in the array with `start_date` +set to the first of the month and `end_date` to the last day of that month. +Consecutive months with the same rate and type are **not** grouped — each month +is returned as a separate entry. + +Only base currencies that actually appear in the report's cost data are included. +For OCP reports, this means currencies from both `raw_currency` (infrastructure +costs) and cost model currencies are considered. --- diff --git a/koku/api/report/ocp/query_handler.py b/koku/api/report/ocp/query_handler.py index 5e33aa164f..0d0136a733 100644 --- a/koku/api/report/ocp/query_handler.py +++ b/koku/api/report/ocp/query_handler.py @@ -27,6 +27,7 @@ from api.report.queries import is_grouped_by_project from api.report.queries import ReportQueryHandler from cost_models.exchange_rate_annotations import build_ocp_exchange_rate_annotation_dict +from cost_models.models import CostModel LOG = logging.getLogger(__name__) @@ -166,6 +167,26 @@ def exchange_rate_annotation_dict(self): """ return build_ocp_exchange_rate_annotation_dict(self._mapper.cost_units_key, self.currency) + def _get_base_currencies_in_data(self): + """Return base currencies from both raw_currency (infra) and cost model currencies.""" + base_currencies = super()._get_base_currencies_in_data() + base_table = self._mapper.query_table + with tenant_context(self.tenant): + source_uuids = ( + base_table.objects.filter( + usage_start__gte=self.start_datetime.date(), + usage_start__lte=self.end_datetime.date(), + ) + .values_list("source_uuid", flat=True) + .distinct() + ) + cm_currencies = set( + CostModel.objects.filter( + costmodelmap__provider_uuid__in=source_uuids, + ).values_list("currency", flat=True) + ) + return base_currencies | cm_currencies + def format_tags(self, tags_iterable): """ Formats the tags into our standard format. diff --git a/koku/api/report/queries.py b/koku/api/report/queries.py index e2f722f204..a25715bbd9 100644 --- a/koku/api/report/queries.py +++ b/koku/api/report/queries.py @@ -1079,10 +1079,38 @@ def _initialize_response_output(self, parameters): return output + def _get_base_currencies_in_data(self): + """Return the set of base currencies present in the report data for the query range. + + Uses the mapper's base query table (e.g. OCPUsageLineItemDailySummary) + rather than the resolved view/summary table, because the base table is + always populated and is the source of truth for which currencies exist. + + Subclasses (e.g. OCP) can override to include additional currency + sources such as cost model currencies. + """ + cost_units_key = self._mapper.cost_units_key + base_table = self._mapper.query_table + with tenant_context(self.tenant): + return set( + base_table.objects.filter( + usage_start__gte=self.start_datetime.date(), + usage_start__lte=self.end_datetime.date(), + ) + .values_list(cost_units_key, flat=True) + .distinct() + ) + def _get_exchange_rates_applied(self, start_date, end_date, target_currency): - """Build exchange_rates_applied metadata from MonthlyExchangeRate for the query range.""" - # MonthlyExchangeRate stores one rate per month with effective_date on the 1st, - # so snap query bounds to month starts to avoid excluding overlapping months. + """Build exchange_rates_applied metadata from MonthlyExchangeRate for the query range. + + Only includes rates whose base_currency actually appears in the + report data, so the response stays small and relevant. + """ + base_currencies = self._get_base_currencies_in_data() + if not base_currencies: + return [] + start_month = start_date.replace(day=1) end_month = end_date.replace(day=1) @@ -1092,6 +1120,7 @@ def _get_exchange_rates_applied(self, start_date, end_date, target_currency): effective_date__gte=start_month, effective_date__lte=end_month, target_currency=target_currency, + base_currency__in=base_currencies, ) .order_by("base_currency", "effective_date") .values("base_currency", "target_currency", "exchange_rate", "rate_type", "effective_date") From a732a5a4457da13d6c71722a9203d2a5fc6e9485 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 12:18:28 +0300 Subject: [PATCH 074/106] COST-7252: Capitalize Babel package name in Pipfile Made-with: Cursor --- Pipfile | 2 +- Pipfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index c82fb2e651..ed01b67fa1 100644 --- a/Pipfile +++ b/Pipfile @@ -53,7 +53,7 @@ pandas = "<3.0" scipy = ">=1.16" boto3 = "*" sqlalchemy = ">=2.0.0" -babel = "*" +Babel = "*" [dev-packages] argh = ">=0.26.2" diff --git a/Pipfile.lock b/Pipfile.lock index efa3175337..e63df1cbd9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c219b511c58c2080e516c303829afc3977cc55f356cd020499f5d0d974a6da92" + "sha256": "ef9280eefcd630256299ba0a9b3d01bbb7ef2e6563664d55459ff6b3d6f699f8" }, "pipfile-spec": 6, "requires": { From c39512a62af69fc94d2b4adc6356153aa2cdc15b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 12:48:59 +0300 Subject: [PATCH 075/106] COST-7252: Regenerate Pipfile.lock with Python 3.11 The previous commit set the Pipfile.lock hash using Python 3.14, but CI runs check-pipfile-lock with Python 3.11, which computes a different hash. Regenerated with pipenv lock under Python 3.11. Made-with: Cursor --- Pipfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index e63df1cbd9..3b22547e0c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ef9280eefcd630256299ba0a9b3d01bbb7ef2e6563664d55459ff6b3d6f699f8" + "sha256": "076741f1d4c52a3ea84a9a18600a5de48c6106574cacae7db51b8e50808f33a1" }, "pipfile-spec": 6, "requires": { @@ -3106,12 +3106,12 @@ }, "crc-bonfire": { "hashes": [ - "sha256:51df052702fb2e24a2ccbbe7fe9089f8f984140ba1ede206ca91d56bfca469aa", - "sha256:6fa9fda6582e8eb69d1654e4b440991133ef1b2109483e73888d5474bf9ee695" + "sha256:54c60db35846eeffc515013b7966d6313e1b12d4b1df433271fd29cfd581207a", + "sha256:cceb257faf44b7116976d822518d12ab3ec329951078199aa577dff189a61e77" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==6.11.0" + "version": "==6.12.0" }, "cryptography": { "hashes": [ From 86d0b5b89c87b2ca6cd79e759d60cdf2aa55b2d1 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 15:19:29 +0300 Subject: [PATCH 076/106] Revert Pipfile.lock to main branch version Made-with: Cursor --- Pipfile.lock | 1697 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 1082 insertions(+), 615 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 3b22547e0c..21e9ababee 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "076741f1d4c52a3ea84a9a18600a5de48c6106574cacae7db51b8e50808f33a1" + "sha256": "8355828638d3b0648e8f215a63312b8cc8edc9f80fd3dfb07e960556aa6086a0" }, "pipfile-spec": 6, "requires": { @@ -115,15 +115,6 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, - "babel": { - "hashes": [ - "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", - "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.18.0" - }, "beautifulsoup4": { "hashes": [ "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", @@ -143,29 +134,29 @@ }, "boto3": { "hashes": [ - "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", - "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" + "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", + "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" }, "botocore": { "hashes": [ - "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", - "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" + "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", + "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" ], "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" }, "cachetools": { "hashes": [ - "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", - "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" + "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", + "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.6" + "version": "==7.0.5" }, "celery": { "hashes": [ @@ -178,11 +169,11 @@ }, "certifi": { "hashes": [ - "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", - "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2026.4.22" + "version": "==2026.2.25" }, "cffi": { "hashes": [ @@ -271,7 +262,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_version >= '3.9'", + "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", "version": "==2.0.0" }, "charset-normalizer": { @@ -482,11 +473,11 @@ }, "click": { "hashes": [ - "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", - "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" + "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", + "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" ], "markers": "python_version >= '3.10'", - "version": "==8.3.3" + "version": "==8.3.2" }, "click-didyoumean": { "hashes": [ @@ -556,58 +547,58 @@ }, "cryptography": { "hashes": [ - "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", - "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", - "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", - "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", - "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", - "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", - "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", - "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", - "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", - "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", - "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", - "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", - "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", - "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", - "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", - "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", - "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", - "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", - "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", - "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", - "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", - "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", - "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", - "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", - "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", - "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", - "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", - "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", - "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", - "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", - "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", - "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", - "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", - "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", - "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", - "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", - "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", - "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", - "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", - "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", - "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", - "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", - "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", - "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", - "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", - "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", - "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", - "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", - "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" + "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", + "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", + "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", + "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", + "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", + "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", + "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", + "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", + "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", + "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", + "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", + "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", + "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", + "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", + "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", + "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", + "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", + "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", + "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", + "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", + "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", + "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", + "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", + "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", + "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", + "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", + "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", + "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", + "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", + "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", + "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", + "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", + "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", + "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", + "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", + "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", + "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", + "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", + "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", + "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", + "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", + "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", + "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", + "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", + "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", + "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", + "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", + "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", + "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==47.0.0" + "version": "==46.0.7" }, "django": { "hashes": [ @@ -710,11 +701,11 @@ "grpc" ], "hashes": [ - "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", - "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" + "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", + "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" ], "markers": "python_version >= '3.9'", - "version": "==2.30.3" + "version": "==2.30.2" }, "google-api-python-client": { "hashes": [ @@ -727,12 +718,12 @@ }, "google-auth": { "hashes": [ - "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", - "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" + "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", + "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.2" + "version": "==2.49.1" }, "google-auth-httplib2": { "hashes": [ @@ -823,71 +814,6 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, - "greenlet": { - "hashes": [ - "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", - "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", - "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", - "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", - "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", - "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", - "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", - "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", - "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", - "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", - "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", - "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", - "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", - "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", - "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", - "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", - "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", - "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", - "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", - "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", - "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", - "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", - "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", - "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", - "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", - "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", - "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", - "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", - "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", - "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", - "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", - "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", - "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", - "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", - "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", - "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", - "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", - "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", - "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", - "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", - "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", - "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", - "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", - "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", - "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", - "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", - "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", - "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", - "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", - "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", - "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", - "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", - "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", - "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", - "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", - "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", - "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", - "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", - "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564" - ], - "markers": "python_version >= '3.10'", - "version": "==3.5.0" - }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -982,11 +908,11 @@ }, "idna": { "hashes": [ - "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", - "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" ], "markers": "python_version >= '3.8'", - "version": "==3.13" + "version": "==3.11" }, "importlib-metadata": { "hashes": [ @@ -1487,17 +1413,17 @@ "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6" ], - "markers": "python_version >= '3.10'", + "markers": "platform_python_implementation != 'PyPy'", "version": "==3.11.8" }, "packaging": { "hashes": [ - "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", - "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" + "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", + "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.2" + "version": "==26.0" }, "pandas": { "hashes": [ @@ -1648,134 +1574,134 @@ }, "psycopg2-binary": { "hashes": [ - "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", - "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964", - "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", - "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", - "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115", - "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", - "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", - "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", - "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", - "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", - "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", - "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", - "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", - "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", - "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", - "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", - "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", - "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", - "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", - "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", - "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf", - "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", - "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", - "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", - "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", - "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", - "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", - "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", - "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94", - "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", - "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", - "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", - "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", - "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", - "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", - "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", - "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", - "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", - "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", - "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", - "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", - "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", - "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", - "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", - "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915", - "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", - "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", - "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe", - "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", - "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", - "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", - "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86", - "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", - "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", - "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", - "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", - "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03", - "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", - "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", - "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", - "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", - "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab", - "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", - "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10", - "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", - "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2", - "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e" + "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", + "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", + "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", + "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", + "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", + "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", + "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", + "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", + "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", + "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", + "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", + "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", + "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", + "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", + "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", + "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", + "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", + "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", + "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", + "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", + "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", + "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", + "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", + "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", + "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", + "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", + "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", + "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", + "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", + "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", + "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", + "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", + "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", + "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", + "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", + "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", + "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", + "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", + "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", + "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", + "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", + "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", + "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", + "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", + "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", + "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", + "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", + "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", + "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", + "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", + "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", + "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", + "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", + "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", + "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", + "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", + "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", + "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", + "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", + "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", + "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", + "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", + "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", + "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", + "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", + "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", + "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.9.12" + "version": "==2.9.11" }, "pyarrow": { "hashes": [ - "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", - "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", - "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", - "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", - "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", - "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", - "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", - "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", - "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", - "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", - "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", - "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", - "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", - "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", - "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", - "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", - "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", - "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", - "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", - "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", - "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", - "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", - "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", - "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", - "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", - "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", - "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", - "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", - "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", - "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", - "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", - "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", - "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", - "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", - "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", - "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", - "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", - "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", - "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", - "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", - "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", - "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", - "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", - "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", - "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", - "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", - "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", - "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", - "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", - "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072" + "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", + "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", + "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", + "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", + "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", + "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", + "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", + "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", + "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", + "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", + "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", + "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", + "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", + "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", + "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", + "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", + "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", + "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", + "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", + "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", + "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", + "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", + "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", + "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", + "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", + "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", + "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", + "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", + "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", + "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", + "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", + "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", + "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", + "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", + "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", + "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", + "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", + "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", + "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", + "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", + "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", + "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", + "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", + "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", + "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", + "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", + "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", + "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", + "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", + "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==24.0.0" + "version": "==23.0.1" }, "pyasn1": { "hashes": [ @@ -1798,143 +1724,144 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "python_version >= '3.10'", + "markers": "implementation_name != 'PyPy'", "version": "==3.0" }, "pydantic": { "hashes": [ - "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", - "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d" + "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", + "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.13.3" + "version": "==2.12.5" }, "pydantic-core": { "hashes": [ - "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", - "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", - "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", - "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", - "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", - "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", - "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", - "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", - "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", - "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", - "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", - "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", - "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", - "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", - "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", - "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", - "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", - "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", - "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", - "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", - "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", - "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", - "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", - "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495", - "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", - "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0", - "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd", - "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", - "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", - "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", - "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", - "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", - "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", - "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", - "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", - "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", - "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", - "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", - "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", - "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", - "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720", - "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", - "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c", - "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", - "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", - "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", - "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", - "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", - "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", - "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873", - "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", - "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", - "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", - "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", - "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", - "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd", - "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", - "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", - "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", - "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d", - "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", - "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", - "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", - "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", - "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168", - "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", - "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13", - "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", - "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", - "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", - "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", - "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", - "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", - "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", - "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", - "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", - "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", - "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", - "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", - "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", - "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", - "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", - "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", - "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", - "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", - "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", - "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", - "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", - "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", - "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", - "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", - "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", - "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", - "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", - "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", - "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", - "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", - "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6", - "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79", - "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a", - "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", - "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", - "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", - "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", - "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", - "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", - "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", - "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", - "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", - "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", - "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", - "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", - "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", - "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", - "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb", - "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", - "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", - "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", - "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", - "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56" + "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", + "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", + "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", + "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", + "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", + "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", + "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", + "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", + "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", + "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", + "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", + "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", + "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", + "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", + "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", + "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", + "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", + "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", + "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", + "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", + "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", + "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", + "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", + "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", + "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", + "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", + "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", + "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", + "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", + "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", + "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", + "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", + "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", + "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", + "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", + "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", + "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", + "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", + "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", + "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", + "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", + "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", + "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", + "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", + "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", + "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", + "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", + "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", + "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", + "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", + "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", + "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", + "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", + "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", + "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", + "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", + "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", + "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", + "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", + "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", + "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", + "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", + "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", + "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", + "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", + "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", + "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", + "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", + "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", + "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", + "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", + "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", + "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", + "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", + "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", + "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", + "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", + "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", + "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", + "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", + "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", + "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", + "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", + "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", + "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", + "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", + "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", + "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", + "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", + "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", + "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", + "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", + "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", + "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", + "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", + "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", + "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", + "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", + "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", + "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", + "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", + "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", + "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", + "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", + "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", + "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", + "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", + "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", + "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", + "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", + "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", + "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", + "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", + "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", + "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", + "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", + "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", + "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", + "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", + "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", + "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" ], "markers": "python_version >= '3.9'", - "version": "==2.46.3" + "version": "==2.41.5" }, "pyjwt": { "extras": [ @@ -1961,7 +1888,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, "pytz": { @@ -1999,11 +1926,11 @@ }, "s3transfer": { "hashes": [ - "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", - "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" + "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", + "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" ], "markers": "python_version >= '3.9'", - "version": "==0.16.1" + "version": "==0.16.0" }, "scipy": { "hashes": [ @@ -2083,19 +2010,19 @@ }, "sentry-sdk": { "hashes": [ - "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", - "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f" + "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", + "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.58.0" + "version": "==2.57.0" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.17.0" }, "soupsieve": { @@ -2255,11 +2182,11 @@ }, "tzdata": { "hashes": [ - "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", - "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" + "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", + "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" ], "markers": "python_version >= '2'", - "version": "==2026.2" + "version": "==2026.1" }, "tzlocal": { "hashes": [ @@ -2384,11 +2311,11 @@ }, "zipp": { "hashes": [ - "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", - "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110" + "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", + "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" ], "markers": "python_version >= '3.9'", - "version": "==3.23.1" + "version": "==3.23.0" }, "zstandard": { "hashes": [ @@ -2505,6 +2432,30 @@ "markers": "python_version >= '3.6'", "version": "==5.3.1" }, + "anyio": { + "hashes": [ + "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", + "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" + ], + "markers": "python_version >= '3.10'", + "version": "==4.13.0" + }, + "anytree": { + "hashes": [ + "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", + "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714" + ], + "markers": "python_version < '4.0' and python_full_version >= '3.9.2'", + "version": "==2.13.0" + }, + "app-common-python": { + "hashes": [ + "sha256:74125371e2865bcba7a7c34f0700d4b8b97a0c0a9ff67bbb06fdb20a3da4b75c", + "sha256:861bd93482edabd00a85eb6512e0d7df08597be6f76b864be8e53d6294f81389" + ], + "index": "pypi", + "version": "==0.2.9" + }, "argh": { "hashes": [ "sha256:2edac856ff50126f6e47d884751328c9f466bacbbb6cbfdac322053d94705494", @@ -2548,6 +2499,14 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, + "backoff": { + "hashes": [ + "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", + "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.2.1" + }, "billiard": { "hashes": [ "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", @@ -2558,29 +2517,37 @@ }, "boto3": { "hashes": [ - "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", - "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" + "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", + "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" }, "botocore": { "hashes": [ - "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", - "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" + "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", + "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" ], "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" + }, + "cached-property": { + "hashes": [ + "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", + "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb" + ], + "markers": "python_version >= '3.8'", + "version": "==2.0.1" }, "cachetools": { "hashes": [ - "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", - "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" + "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", + "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.6" + "version": "==7.0.5" }, "celery": { "hashes": [ @@ -2593,11 +2560,11 @@ }, "certifi": { "hashes": [ - "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", - "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2026.4.22" + "version": "==2026.2.25" }, "cffi": { "hashes": [ @@ -2686,7 +2653,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_version >= '3.9'", + "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", "version": "==2.0.0" }, "cfgv": { @@ -2699,45 +2666,41 @@ }, "chardet": { "hashes": [ - "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", - "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", - "sha256:23163921dccf3103ce59540b0443c106d2c0a0ff2e0503e05196f5e6fdea453f", - "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", - "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", - "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", - "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", - "sha256:3990fffcc6a6045f2234ab72752ad037e3b2d48c72037f244d42738db397eb75", - "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", - "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", - "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", - "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", - "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", - "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", - "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", - "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", - "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", - "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", - "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", - "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", - "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", - "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", - "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", - "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", - "sha256:bba8bea1b28d927b3e99e47deafe53658d34497c0a891d95ff1ba8ff6663f01c", - "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", - "sha256:c0c79b13c9908ac7dfe0a74116ebc9a0f28b2319d23c32f3dfcdfbe1279c7eaf", - "sha256:c7116b0452994734ccff35e154b44240090eb0f4f74b9106292668133557c175", - "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", - "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", - "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", - "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", - "sha256:cfb54563fe5f130da17c44c6a4e2e8052ba628e5ab4eab7ef8190f736f0f8f72", - "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", - "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", - "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131" + "sha256:0487a6a6846740f39f9fcd71e3acff2982bae8bca3507ee986d5155cd458e044", + "sha256:04b9be0d786b9a3bbd7860a82d27e843f22211be51b9b84d85fe8d9864e2f535", + "sha256:0848015eb1471e1499963dff2776557af05f99c38ba2a14f34ea078f8668c6a9", + "sha256:0de8d636391f9050e4e048ca8a9f98b25e67eff389705f8c6ff1ab9593f7339b", + "sha256:19bcd1de4a0c1a5802f9d2d370b6696668bddc166a3c89c113cf109313b3d99f", + "sha256:277ce1174ea054415a3c2ad5f51aa089a96dda16999de56e4ac1bc366d0d535e", + "sha256:2da446b920064ca9574504c29a07ef5eae91a1948a302a25043a16fb79ec2397", + "sha256:35ade2a6f93e5d2bdff541b584126cfe066eac5c9457572ff97cabc8068bece1", + "sha256:3886f8f9bb3500bd8c421b2de9b4878a0c183f80bc289338cdda869dfd4397fb", + "sha256:3d66d2949754ad924865a47e81857a0792dc8edc651094285116b6df2e218445", + "sha256:4ececf9631f7932a2cef728746303d71ae8204923190253d5382ee37739dd46e", + "sha256:5d86402a506631af2fb36e3d1c72021477b228fb0dcdb44400b9b681f14b14c0", + "sha256:5e686e5a0d8155cfbf5b1a579f5790bb01bf1a0a52e7f98b38801c09a0c63fcd", + "sha256:62b25b3ea5ef8e1672726e4c1601f6636ce3b76b9de92af669c8000711d7fe13", + "sha256:6b5c03330b9108124f8174a596284b737e95f1cf6a99953c37cea7e2583212e7", + "sha256:7a1c2be068ab91a472fd74617d9371605897c3061ca4f2e5df63d9f3d9c11f8e", + "sha256:80de820fa1df95a2e7c9898867f7d6ff3dc3d52a109938b215b37ab54474d307", + "sha256:8befb12c263cb14e26f065a3f99d76ef2610b0266cb70d827bb61528e9e60f28", + "sha256:9381b3d9075c8a2e622b4d46db5e4229c94aebc71d4c8e620d9cf2cea2930824", + "sha256:a9322fd3ffd359b49b2d608725a15975ebc0d66f2dcedefa7ddb5847e54a6f9c", + "sha256:a98c361b73c6ce4a44beaecf5cfb389ec69b566dd7f3f4ea5d1bde6487e7054d", + "sha256:b726b0b2684d29cd08f602bb4266334386c58741ff34c9e2f6cdf97ad604e235", + "sha256:be39708b300a80a9f78ef8f81018e2e9c6274a71c0823a4d6e493c72f7b3d2a2", + "sha256:bee1d665ca5810d8e3cf11122619e85c23b209075cbddb91f213675248f0e522", + "sha256:c03925738670199d253b8c79828d8a68d404f629a2dbf1b4b5aabd8c8b0249ab", + "sha256:c820c95d8b4de8aea99b54083d38f10f763686646962b5627e8e2b2db113a37b", + "sha256:c98e1044785ab71f0fee70f64b8d56f69df9de1b593793022e001ba2f6b76dd0", + "sha256:caf0715b8a5e20fc3faf21a24abd3ae513f8f58206dd32d1b87eca6351e105ed", + "sha256:cda41132a45dfbf6984dade1f531a4098c813caf266c66cc446d90bb9369cabd", + "sha256:d8aa2bae7d0523963395f802ae2212e8b2248d4503a14a691de86edf716b22d3", + "sha256:e53cc280a1ab616f191ac7ebdd1f38f2aa78b1411dd2677dc556c6f0fa085913", + "sha256:fcaed03cefa53f62346091ef92da7a6f44bae6830a6f4c6b097a70cdc31b1199" ], "markers": "python_version >= '3.10'", - "version": "==7.4.3" + "version": "==7.4.1" }, "charset-normalizer": { "hashes": [ @@ -2876,11 +2839,11 @@ }, "click": { "hashes": [ - "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", - "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" + "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", + "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" ], "markers": "python_version >= '3.10'", - "version": "==8.3.3" + "version": "==8.3.2" }, "click-didyoumean": { "hashes": [ @@ -3106,67 +3069,67 @@ }, "crc-bonfire": { "hashes": [ - "sha256:54c60db35846eeffc515013b7966d6313e1b12d4b1df433271fd29cfd581207a", - "sha256:cceb257faf44b7116976d822518d12ab3ec329951078199aa577dff189a61e77" + "sha256:7fa3b2e739a92c2ffdc3f43ba590201dff64d6993603fc43deb10f3f0c2d903a", + "sha256:d9a32ad91714dd0c00c56a405add63df4896dfeb58994ce5f9688dde7cd4298a" ], "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==6.12.0" + "markers": "python_version >= '3.6'", + "version": "==6.9.1" }, "cryptography": { "hashes": [ - "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", - "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", - "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", - "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", - "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", - "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", - "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", - "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", - "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", - "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", - "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", - "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", - "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", - "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", - "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", - "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", - "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", - "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", - "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", - "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", - "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", - "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", - "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", - "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", - "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", - "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", - "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", - "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", - "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", - "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", - "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", - "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", - "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", - "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", - "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", - "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", - "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", - "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", - "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", - "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", - "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", - "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", - "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", - "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", - "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", - "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", - "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", - "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", - "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" + "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", + "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", + "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", + "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", + "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", + "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", + "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", + "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", + "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", + "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", + "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", + "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", + "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", + "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", + "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", + "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", + "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", + "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", + "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", + "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", + "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", + "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", + "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", + "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", + "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", + "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", + "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", + "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", + "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", + "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", + "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", + "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", + "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", + "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", + "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", + "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", + "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", + "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", + "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", + "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", + "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", + "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", + "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", + "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", + "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", + "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", + "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", + "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", + "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==47.0.0" + "version": "==46.0.7" }, "cycler": { "hashes": [ @@ -3245,20 +3208,20 @@ }, "faker": { "hashes": [ - "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", - "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318" + "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", + "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==40.15.0" + "version": "==40.13.0" }, "filelock": { "hashes": [ - "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", - "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258" + "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", + "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70" ], "markers": "python_version >= '3.10'", - "version": "==3.29.0" + "version": "==3.25.2" }, "flake8": { "hashes": [ @@ -3339,20 +3302,20 @@ "grpc" ], "hashes": [ - "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", - "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" + "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", + "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" ], "markers": "python_version >= '3.9'", - "version": "==2.30.3" + "version": "==2.30.2" }, "google-auth": { "hashes": [ - "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", - "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" + "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", + "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.2" + "version": "==2.49.1" }, "google-cloud-bigquery": { "hashes": [ @@ -3435,6 +3398,25 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, + "gql": { + "extras": [ + "requests" + ], + "hashes": [ + "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", + "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479" + ], + "markers": "python_full_version >= '3.8.1'", + "version": "==4.0.0" + }, + "graphql-core": { + "hashes": [ + "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", + "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c" + ], + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==3.2.8" + }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -3520,19 +3502,19 @@ }, "identify": { "hashes": [ - "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", - "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842" + "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", + "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737" ], "markers": "python_version >= '3.10'", - "version": "==2.6.19" + "version": "==2.6.18" }, "idna": { "hashes": [ - "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", - "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" ], "markers": "python_version >= '3.8'", - "version": "==3.13" + "version": "==3.11" }, "isodate": { "hashes": [ @@ -3558,6 +3540,14 @@ "markers": "python_version >= '3.9'", "version": "==1.1.0" }, + "junitparser": { + "hashes": [ + "sha256:9e279f2214dc74b6a86b22db757abda2e8e66e819fe882dad5b392d57024cd26", + "sha256:f15e292877258d7c5755d672ce86f82c3622c7ea4c2f44f55de44ed7518484d3" + ], + "markers": "python_version >= '3.10'", + "version": "==5.0.0" + }, "kiwisolver": { "hashes": [ "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", @@ -3683,12 +3673,12 @@ }, "koku-nise": { "hashes": [ - "sha256:2d8c03b9a27f8f9f5caf87ca68b4a6ba23b4d7e6291f69ad0b01655d96e64c51", - "sha256:42b6db673060c9ed23c4bc2badb57fb840a8dccf09d2ec933194aa37e926439d" + "sha256:5ac277879ad686c3f0b9fbd8084e48868a949948f293783d61ad8c219f9a650c", + "sha256:b189f3aad01b698228459a757bfd14af23ec177f582bcf528b897fe02bbae893" ], "index": "pypi", "markers": "python_version >= '3.11'", - "version": "==5.4.1" + "version": "==5.4.0" }, "kombu": { "hashes": [ @@ -3804,65 +3794,65 @@ }, "matplotlib": { "hashes": [ - "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", - "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", - "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", - "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", - "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", - "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", - "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", - "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", - "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", - "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", - "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", - "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", - "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", - "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", - "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", - "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", - "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", - "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", - "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", - "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", - "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", - "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", - "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", - "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", - "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", - "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", - "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", - "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", - "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", - "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", - "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", - "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", - "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", - "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", - "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", - "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", - "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", - "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", - "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", - "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", - "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", - "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", - "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", - "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", - "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", - "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", - "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", - "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", - "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", - "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", - "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", - "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", - "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", - "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", - "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358" + "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", + "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", + "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", + "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", + "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", + "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", + "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", + "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", + "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", + "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", + "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", + "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", + "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", + "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", + "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", + "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", + "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", + "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", + "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", + "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", + "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", + "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", + "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", + "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", + "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", + "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", + "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", + "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", + "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", + "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", + "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", + "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", + "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", + "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", + "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", + "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", + "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", + "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", + "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", + "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", + "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", + "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", + "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", + "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", + "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", + "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", + "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", + "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", + "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", + "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", + "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", + "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", + "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", + "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", + "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==3.10.9" + "version": "==3.10.8" }, "mccabe": { "hashes": [ @@ -3881,6 +3871,158 @@ "markers": "python_version >= '3.10'", "version": "==1.23.4" }, + "multidict": { + "hashes": [ + "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", + "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", + "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", + "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", + "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", + "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", + "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", + "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", + "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", + "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", + "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", + "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", + "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", + "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", + "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", + "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", + "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", + "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", + "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", + "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", + "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", + "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", + "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", + "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", + "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", + "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", + "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", + "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", + "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", + "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", + "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", + "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", + "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", + "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", + "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", + "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", + "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", + "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", + "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", + "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", + "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", + "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", + "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", + "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", + "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", + "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", + "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", + "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", + "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", + "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", + "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", + "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", + "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", + "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", + "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", + "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", + "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", + "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", + "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", + "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", + "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", + "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", + "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", + "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", + "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", + "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", + "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", + "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", + "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", + "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", + "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", + "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", + "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", + "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", + "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", + "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", + "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", + "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", + "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", + "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", + "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", + "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", + "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", + "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", + "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", + "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", + "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", + "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", + "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", + "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", + "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", + "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", + "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", + "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", + "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", + "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", + "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", + "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", + "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", + "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", + "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", + "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", + "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", + "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", + "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", + "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", + "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", + "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", + "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", + "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", + "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", + "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", + "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", + "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", + "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", + "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", + "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", + "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", + "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", + "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", + "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", + "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", + "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", + "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", + "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", + "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", + "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", + "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", + "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", + "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", + "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", + "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", + "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", + "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", + "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", + "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", + "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", + "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", + "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", + "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", + "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", + "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", + "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", + "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", + "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", + "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" + ], + "markers": "python_version >= '3.9'", + "version": "==6.7.1" + }, "nodeenv": { "hashes": [ "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", @@ -3975,14 +4117,29 @@ "markers": "python_version >= '3.8'", "version": "==3.3.1" }, + "ocviapy": { + "hashes": [ + "sha256:77602d07d1f124e6f020e08ce21379e8a12cbefa98b168e697fdc202c770f9e2", + "sha256:e5558ec5c46d2793949c0b2fd1c99759d5b5ee564e2ac9ec56fd200c94dcaef4" + ], + "markers": "python_version >= '3.6'", + "version": "==1.6.0" + }, "packaging": { "hashes": [ - "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", - "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" + "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", + "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.2" + "version": "==26.0" + }, + "parsedatetime": { + "hashes": [ + "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", + "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" + ], + "version": "==2.6" }, "pillow": { "hashes": [ @@ -4108,12 +4265,12 @@ }, "pre-commit": { "hashes": [ - "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", - "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b" + "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", + "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==4.6.0" + "version": "==4.5.1" }, "prometheus-client": { "hashes": [ @@ -4132,6 +4289,134 @@ "markers": "python_version >= '3.8'", "version": "==3.0.52" }, + "propcache": { + "hashes": [ + "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", + "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", + "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", + "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", + "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", + "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", + "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", + "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", + "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", + "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", + "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", + "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", + "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", + "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", + "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", + "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", + "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", + "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", + "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", + "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", + "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", + "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", + "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", + "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", + "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", + "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", + "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", + "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", + "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", + "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", + "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", + "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", + "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", + "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", + "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", + "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", + "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", + "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", + "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", + "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", + "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", + "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", + "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", + "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", + "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", + "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", + "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", + "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", + "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", + "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", + "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", + "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", + "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", + "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", + "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", + "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", + "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", + "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", + "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", + "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", + "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", + "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", + "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", + "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", + "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", + "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", + "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", + "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", + "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", + "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", + "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", + "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", + "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", + "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", + "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", + "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", + "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", + "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", + "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", + "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", + "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", + "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", + "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", + "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", + "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", + "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", + "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", + "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", + "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", + "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", + "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", + "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", + "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", + "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", + "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", + "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", + "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", + "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", + "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", + "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", + "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", + "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", + "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", + "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", + "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", + "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", + "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", + "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", + "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", + "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", + "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", + "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", + "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", + "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", + "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", + "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", + "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", + "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", + "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", + "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", + "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", + "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" + ], + "markers": "python_version >= '3.9'", + "version": "==0.4.1" + }, "proto-plus": { "hashes": [ "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", @@ -4185,7 +4470,7 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "python_version >= '3.10'", + "markers": "implementation_name != 'PyPy'", "version": "==3.0" }, "pydocstyle": { @@ -4227,7 +4512,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, "python-discovery": { @@ -4238,6 +4523,14 @@ "markers": "python_version >= '3.8'", "version": "==1.2.2" }, + "python-dotenv": { + "hashes": [ + "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", + "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" + ], + "markers": "python_version >= '3.10'", + "version": "==1.2.2" + }, "pytz": { "hashes": [ "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", @@ -4351,6 +4644,14 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, + "requests-toolbelt": { + "hashes": [ + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, "responses": { "hashes": [ "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", @@ -4362,18 +4663,26 @@ }, "s3transfer": { "hashes": [ - "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", - "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" + "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", + "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" ], "markers": "python_version >= '3.9'", - "version": "==0.16.1" + "version": "==0.16.0" + }, + "sh": { + "hashes": [ + "sha256:653227a7c41a284ec5302173fbc044ee817c7bad5e6e4d8d55741b9aeb9eb65b", + "sha256:e0b15b4ae8ffcd399bc8ffddcbd770a43c7a70a24b16773fbb34c001ad5d52af" + ], + "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", + "version": "==2.2.2" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.17.0" }, "snakeviz": { @@ -4390,7 +4699,7 @@ "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895" ], - "markers": "python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.1" }, "sqlparse": { @@ -4402,6 +4711,14 @@ "markers": "python_version >= '3.8'", "version": "==0.5.5" }, + "tabulate": { + "hashes": [ + "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", + "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3" + ], + "markers": "python_version >= '3.10'", + "version": "==0.10.0" + }, "tblib": { "hashes": [ "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", @@ -4436,6 +4753,14 @@ "markers": "python_version >= '3.9'", "version": "==4.26.0" }, + "truststore": { + "hashes": [ + "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", + "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981" + ], + "markers": "python_version >= '3.10'", + "version": "==0.10.4" + }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", @@ -4446,11 +4771,11 @@ }, "tzdata": { "hashes": [ - "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", - "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" + "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", + "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" ], "markers": "python_version >= '2'", - "version": "==2026.2" + "version": "==2026.1" }, "tzlocal": { "hashes": [ @@ -4478,11 +4803,19 @@ }, "virtualenv": { "hashes": [ - "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", - "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e" + "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", + "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2" ], "markers": "python_version >= '3.8'", - "version": "==21.3.0" + "version": "==21.2.1" + }, + "wait-for": { + "hashes": [ + "sha256:1129f3350e29b0600889e24328d685a6bff048c8f4cabce28ef7632ed40c5d91", + "sha256:5642975f1fc5850acb55684b2d7842bd820fb068e725cd4ffff4bf3eba8e2788" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" }, "watchdog": { "hashes": [ @@ -4536,6 +4869,140 @@ ], "markers": "python_version >= '3.9'", "version": "==1.9.0" + }, + "yarl": { + "hashes": [ + "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", + "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", + "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", + "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", + "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", + "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", + "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", + "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", + "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", + "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", + "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", + "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", + "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", + "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", + "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", + "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", + "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", + "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", + "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", + "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", + "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", + "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", + "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", + "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", + "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", + "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", + "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", + "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", + "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", + "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", + "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", + "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", + "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", + "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", + "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", + "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", + "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", + "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", + "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", + "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", + "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", + "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", + "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", + "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", + "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", + "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", + "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", + "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", + "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", + "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", + "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", + "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", + "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", + "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", + "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", + "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", + "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", + "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", + "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", + "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", + "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", + "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", + "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", + "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", + "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", + "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", + "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", + "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", + "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", + "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", + "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", + "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", + "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", + "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", + "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", + "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", + "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", + "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", + "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", + "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", + "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", + "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", + "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", + "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", + "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", + "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", + "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", + "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", + "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", + "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", + "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", + "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", + "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", + "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", + "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", + "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", + "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", + "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", + "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", + "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", + "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", + "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", + "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", + "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", + "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", + "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", + "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", + "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", + "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", + "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", + "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", + "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", + "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", + "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", + "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", + "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", + "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", + "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", + "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", + "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", + "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", + "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", + "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", + "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", + "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", + "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", + "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", + "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d" + ], + "markers": "python_version >= '3.10'", + "version": "==1.23.0" } } } From d1a0a0d710c9715433df441667c6dac44e5204ec Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 15:28:49 +0300 Subject: [PATCH 077/106] Downgrade tox to 4.11.4 to resolve platformdirs conflict fcache 0.6.0 (via unleashclient) pins platformdirs~=3.0 (<4.0), while tox>=4.12 requires platformdirs>=4.1. Downgrade tox to the latest version compatible with platformdirs 3.x. Made-with: Cursor --- Pipfile | 2 +- Pipfile.lock | 1713 ++++++++++++++++++-------------------------------- 2 files changed, 620 insertions(+), 1095 deletions(-) diff --git a/Pipfile b/Pipfile index ed01b67fa1..d168ea1e54 100644 --- a/Pipfile +++ b/Pipfile @@ -74,7 +74,7 @@ requests-mock = ">=1.7" responses = ">=0.10" snakeviz = "*" tblib = ">=1.6" -tox = "==4.26.0" # requires a higher cachetools version than google-auth at the moment +tox = "==4.11.4" # fcache (via unleashclient) pins platformdirs~=3.0; tox>=4.12 needs platformdirs>=4.1 watchdog = ">=2.1.1" polyfactory = "*" koku-nise = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 21e9ababee..dd8674fcf7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8355828638d3b0648e8f215a63312b8cc8edc9f80fd3dfb07e960556aa6086a0" + "sha256": "c55e9044eabfcec9a67223c3788b50fde7c1939457816c381743d391751c933b" }, "pipfile-spec": 6, "requires": { @@ -115,6 +115,15 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, + "babel": { + "hashes": [ + "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", + "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.18.0" + }, "beautifulsoup4": { "hashes": [ "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", @@ -134,29 +143,29 @@ }, "boto3": { "hashes": [ - "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", - "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" + "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", + "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "botocore": { "hashes": [ - "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", - "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" + "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", + "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" ], "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "cachetools": { "hashes": [ - "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", - "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" + "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", + "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.5" + "version": "==7.0.6" }, "celery": { "hashes": [ @@ -169,11 +178,11 @@ }, "certifi": { "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" + "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", + "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" ], "markers": "python_version >= '3.7'", - "version": "==2026.2.25" + "version": "==2026.4.22" }, "cffi": { "hashes": [ @@ -262,7 +271,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.9'", "version": "==2.0.0" }, "charset-normalizer": { @@ -473,11 +482,11 @@ }, "click": { "hashes": [ - "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", - "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" + "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", + "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" ], "markers": "python_version >= '3.10'", - "version": "==8.3.2" + "version": "==8.3.3" }, "click-didyoumean": { "hashes": [ @@ -547,58 +556,58 @@ }, "cryptography": { "hashes": [ - "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", - "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", - "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", - "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", - "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", - "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", - "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", - "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", - "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", - "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", - "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", - "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", - "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", - "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", - "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", - "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", - "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", - "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", - "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", - "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", - "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", - "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", - "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", - "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", - "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", - "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", - "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", - "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", - "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", - "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", - "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", - "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", - "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", - "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", - "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", - "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", - "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", - "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", - "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", - "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", - "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", - "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", - "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", - "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", - "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", - "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", - "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", - "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", - "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" + "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", + "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", + "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", + "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", + "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", + "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", + "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", + "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", + "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", + "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", + "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", + "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", + "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", + "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", + "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", + "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", + "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", + "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", + "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", + "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", + "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", + "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", + "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", + "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", + "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", + "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", + "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", + "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", + "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", + "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", + "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", + "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", + "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", + "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", + "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", + "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", + "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", + "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", + "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", + "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", + "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", + "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", + "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", + "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", + "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", + "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", + "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", + "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", + "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.7" + "version": "==47.0.0" }, "django": { "hashes": [ @@ -701,11 +710,11 @@ "grpc" ], "hashes": [ - "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", - "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" + "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", + "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" ], "markers": "python_version >= '3.9'", - "version": "==2.30.2" + "version": "==2.30.3" }, "google-api-python-client": { "hashes": [ @@ -718,12 +727,12 @@ }, "google-auth": { "hashes": [ - "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", - "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" + "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", + "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.1" + "version": "==2.49.2" }, "google-auth-httplib2": { "hashes": [ @@ -814,6 +823,71 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, + "greenlet": { + "hashes": [ + "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", + "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", + "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", + "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", + "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", + "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", + "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", + "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", + "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", + "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", + "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", + "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", + "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", + "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", + "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", + "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", + "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", + "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", + "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", + "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", + "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", + "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", + "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", + "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", + "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", + "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", + "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", + "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", + "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", + "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", + "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", + "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", + "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", + "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", + "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", + "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", + "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", + "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", + "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", + "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", + "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", + "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", + "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", + "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", + "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", + "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", + "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", + "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", + "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", + "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", + "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", + "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", + "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", + "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", + "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", + "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", + "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", + "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", + "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564" + ], + "markers": "python_version >= '3.10'", + "version": "==3.5.0" + }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -908,11 +982,11 @@ }, "idna": { "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", + "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" ], "markers": "python_version >= '3.8'", - "version": "==3.11" + "version": "==3.13" }, "importlib-metadata": { "hashes": [ @@ -1413,17 +1487,17 @@ "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.11.8" }, "packaging": { "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.0" + "version": "==26.2" }, "pandas": { "hashes": [ @@ -1574,134 +1648,134 @@ }, "psycopg2-binary": { "hashes": [ - "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", - "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", - "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", - "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", - "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", - "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", - "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", - "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", - "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", - "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", - "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", - "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", - "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", - "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", - "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", - "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", - "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", - "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", - "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", - "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", - "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", - "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", - "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", - "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", - "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", - "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", - "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", - "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", - "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", - "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", - "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", - "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", - "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", - "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", - "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", - "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", - "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", - "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", - "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", - "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", - "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", - "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", - "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", - "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", - "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", - "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", - "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", - "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", - "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", - "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", - "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", - "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", - "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", - "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", - "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", - "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", - "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", - "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", - "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", - "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", - "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", - "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", - "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", - "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", - "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", - "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", - "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" + "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", + "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964", + "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", + "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", + "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115", + "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", + "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", + "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", + "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", + "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", + "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", + "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", + "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", + "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", + "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", + "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", + "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", + "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", + "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", + "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", + "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf", + "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", + "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", + "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", + "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", + "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", + "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", + "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", + "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94", + "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", + "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", + "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", + "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", + "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", + "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", + "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", + "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", + "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", + "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", + "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", + "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", + "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", + "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", + "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", + "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915", + "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", + "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", + "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe", + "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", + "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", + "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", + "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86", + "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", + "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", + "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", + "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", + "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03", + "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", + "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", + "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", + "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", + "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab", + "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", + "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10", + "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", + "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2", + "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.9.11" + "version": "==2.9.12" }, "pyarrow": { "hashes": [ - "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", - "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", - "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", - "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", - "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", - "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", - "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", - "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", - "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", - "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", - "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", - "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", - "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", - "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", - "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", - "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", - "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", - "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", - "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", - "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", - "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", - "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", - "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", - "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", - "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", - "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", - "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", - "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", - "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", - "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", - "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", - "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", - "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", - "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", - "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", - "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", - "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", - "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", - "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", - "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", - "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", - "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", - "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", - "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", - "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", - "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", - "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", - "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", - "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", - "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd" + "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", + "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", + "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", + "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", + "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", + "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", + "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", + "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", + "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", + "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", + "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", + "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", + "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", + "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", + "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", + "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", + "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", + "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", + "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", + "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", + "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", + "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", + "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", + "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", + "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", + "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", + "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", + "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", + "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", + "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", + "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", + "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", + "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", + "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", + "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", + "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", + "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", + "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", + "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", + "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", + "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", + "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", + "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", + "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", + "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", + "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", + "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", + "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", + "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", + "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==23.0.1" + "version": "==24.0.0" }, "pyasn1": { "hashes": [ @@ -1724,144 +1798,143 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "implementation_name != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.0" }, "pydantic": { "hashes": [ - "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", - "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" + "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", + "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.12.5" + "version": "==2.13.3" }, "pydantic-core": { "hashes": [ - "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", - "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", - "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", - "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", - "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", - "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", - "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", - "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", - "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", - "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", - "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", - "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", - "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", - "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", - "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", - "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", - "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", - "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", - "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", - "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", - "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", - "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", - "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", - "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", - "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", - "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", - "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", - "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", - "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", - "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", - "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", - "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", - "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", - "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", - "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", - "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", - "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", - "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", - "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", - "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", - "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", - "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", - "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", - "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", - "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", - "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", - "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", - "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", - "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", - "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", - "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", - "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", - "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", - "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", - "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", - "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", - "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", - "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", - "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", - "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", - "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", - "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", - "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", - "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", - "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", - "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", - "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", - "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", - "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", - "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", - "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", - "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", - "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", - "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", - "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", - "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", - "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", - "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", - "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", - "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", - "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", - "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", - "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", - "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", - "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", - "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", - "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", - "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", - "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", - "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", - "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", - "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", - "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", - "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", - "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", - "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", - "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", - "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", - "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", - "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", - "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", - "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", - "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", - "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", - "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", - "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", - "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", - "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", - "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", - "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", - "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", - "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", - "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", - "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", - "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", - "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", - "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", - "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", - "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", - "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", - "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" + "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", + "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", + "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", + "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", + "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", + "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", + "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", + "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", + "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", + "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", + "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", + "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", + "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", + "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", + "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", + "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", + "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", + "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", + "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", + "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", + "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", + "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", + "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", + "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495", + "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", + "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0", + "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd", + "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", + "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", + "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", + "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", + "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", + "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", + "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", + "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", + "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", + "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", + "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", + "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", + "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", + "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720", + "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", + "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c", + "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", + "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", + "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", + "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", + "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", + "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", + "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873", + "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", + "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", + "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", + "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", + "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", + "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd", + "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", + "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", + "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", + "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d", + "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", + "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", + "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", + "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", + "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168", + "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", + "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13", + "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", + "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", + "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", + "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", + "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", + "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", + "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", + "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", + "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", + "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", + "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", + "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", + "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", + "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", + "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", + "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", + "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", + "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", + "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", + "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", + "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", + "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", + "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", + "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", + "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", + "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", + "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", + "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", + "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", + "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", + "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6", + "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79", + "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a", + "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", + "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", + "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", + "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", + "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", + "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", + "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", + "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", + "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", + "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", + "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", + "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", + "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", + "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", + "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb", + "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", + "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", + "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", + "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", + "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56" ], "markers": "python_version >= '3.9'", - "version": "==2.41.5" + "version": "==2.46.3" }, "pyjwt": { "extras": [ @@ -1888,7 +1961,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pytz": { @@ -1926,11 +1999,11 @@ }, "s3transfer": { "hashes": [ - "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", - "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" + "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", + "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" ], "markers": "python_version >= '3.9'", - "version": "==0.16.0" + "version": "==0.16.1" }, "scipy": { "hashes": [ @@ -2010,19 +2083,19 @@ }, "sentry-sdk": { "hashes": [ - "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", - "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585" + "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", + "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.57.0" + "version": "==2.58.0" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.17.0" }, "soupsieve": { @@ -2182,11 +2255,11 @@ }, "tzdata": { "hashes": [ - "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", - "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" + "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", + "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" ], "markers": "python_version >= '2'", - "version": "==2026.1" + "version": "==2026.2" }, "tzlocal": { "hashes": [ @@ -2311,11 +2384,11 @@ }, "zipp": { "hashes": [ - "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", + "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110" ], "markers": "python_version >= '3.9'", - "version": "==3.23.0" + "version": "==3.23.1" }, "zstandard": { "hashes": [ @@ -2432,30 +2505,6 @@ "markers": "python_version >= '3.6'", "version": "==5.3.1" }, - "anyio": { - "hashes": [ - "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", - "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" - ], - "markers": "python_version >= '3.10'", - "version": "==4.13.0" - }, - "anytree": { - "hashes": [ - "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", - "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.9.2'", - "version": "==2.13.0" - }, - "app-common-python": { - "hashes": [ - "sha256:74125371e2865bcba7a7c34f0700d4b8b97a0c0a9ff67bbb06fdb20a3da4b75c", - "sha256:861bd93482edabd00a85eb6512e0d7df08597be6f76b864be8e53d6294f81389" - ], - "index": "pypi", - "version": "==0.2.9" - }, "argh": { "hashes": [ "sha256:2edac856ff50126f6e47d884751328c9f466bacbbb6cbfdac322053d94705494", @@ -2499,14 +2548,6 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, - "backoff": { - "hashes": [ - "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", - "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" - ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==2.2.1" - }, "billiard": { "hashes": [ "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", @@ -2517,37 +2558,29 @@ }, "boto3": { "hashes": [ - "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", - "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" + "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", + "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "botocore": { "hashes": [ - "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", - "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" + "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", + "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" ], "markers": "python_version >= '3.9'", - "version": "==1.42.87" - }, - "cached-property": { - "hashes": [ - "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", - "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb" - ], - "markers": "python_version >= '3.8'", - "version": "==2.0.1" + "version": "==1.42.97" }, "cachetools": { "hashes": [ - "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", - "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" + "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", + "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.5" + "version": "==7.0.6" }, "celery": { "hashes": [ @@ -2560,11 +2593,11 @@ }, "certifi": { "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" + "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", + "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" ], "markers": "python_version >= '3.7'", - "version": "==2026.2.25" + "version": "==2026.4.22" }, "cffi": { "hashes": [ @@ -2653,7 +2686,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.9'", "version": "==2.0.0" }, "cfgv": { @@ -2666,41 +2699,45 @@ }, "chardet": { "hashes": [ - "sha256:0487a6a6846740f39f9fcd71e3acff2982bae8bca3507ee986d5155cd458e044", - "sha256:04b9be0d786b9a3bbd7860a82d27e843f22211be51b9b84d85fe8d9864e2f535", - "sha256:0848015eb1471e1499963dff2776557af05f99c38ba2a14f34ea078f8668c6a9", - "sha256:0de8d636391f9050e4e048ca8a9f98b25e67eff389705f8c6ff1ab9593f7339b", - "sha256:19bcd1de4a0c1a5802f9d2d370b6696668bddc166a3c89c113cf109313b3d99f", - "sha256:277ce1174ea054415a3c2ad5f51aa089a96dda16999de56e4ac1bc366d0d535e", - "sha256:2da446b920064ca9574504c29a07ef5eae91a1948a302a25043a16fb79ec2397", - "sha256:35ade2a6f93e5d2bdff541b584126cfe066eac5c9457572ff97cabc8068bece1", - "sha256:3886f8f9bb3500bd8c421b2de9b4878a0c183f80bc289338cdda869dfd4397fb", - "sha256:3d66d2949754ad924865a47e81857a0792dc8edc651094285116b6df2e218445", - "sha256:4ececf9631f7932a2cef728746303d71ae8204923190253d5382ee37739dd46e", - "sha256:5d86402a506631af2fb36e3d1c72021477b228fb0dcdb44400b9b681f14b14c0", - "sha256:5e686e5a0d8155cfbf5b1a579f5790bb01bf1a0a52e7f98b38801c09a0c63fcd", - "sha256:62b25b3ea5ef8e1672726e4c1601f6636ce3b76b9de92af669c8000711d7fe13", - "sha256:6b5c03330b9108124f8174a596284b737e95f1cf6a99953c37cea7e2583212e7", - "sha256:7a1c2be068ab91a472fd74617d9371605897c3061ca4f2e5df63d9f3d9c11f8e", - "sha256:80de820fa1df95a2e7c9898867f7d6ff3dc3d52a109938b215b37ab54474d307", - "sha256:8befb12c263cb14e26f065a3f99d76ef2610b0266cb70d827bb61528e9e60f28", - "sha256:9381b3d9075c8a2e622b4d46db5e4229c94aebc71d4c8e620d9cf2cea2930824", - "sha256:a9322fd3ffd359b49b2d608725a15975ebc0d66f2dcedefa7ddb5847e54a6f9c", - "sha256:a98c361b73c6ce4a44beaecf5cfb389ec69b566dd7f3f4ea5d1bde6487e7054d", - "sha256:b726b0b2684d29cd08f602bb4266334386c58741ff34c9e2f6cdf97ad604e235", - "sha256:be39708b300a80a9f78ef8f81018e2e9c6274a71c0823a4d6e493c72f7b3d2a2", - "sha256:bee1d665ca5810d8e3cf11122619e85c23b209075cbddb91f213675248f0e522", - "sha256:c03925738670199d253b8c79828d8a68d404f629a2dbf1b4b5aabd8c8b0249ab", - "sha256:c820c95d8b4de8aea99b54083d38f10f763686646962b5627e8e2b2db113a37b", - "sha256:c98e1044785ab71f0fee70f64b8d56f69df9de1b593793022e001ba2f6b76dd0", - "sha256:caf0715b8a5e20fc3faf21a24abd3ae513f8f58206dd32d1b87eca6351e105ed", - "sha256:cda41132a45dfbf6984dade1f531a4098c813caf266c66cc446d90bb9369cabd", - "sha256:d8aa2bae7d0523963395f802ae2212e8b2248d4503a14a691de86edf716b22d3", - "sha256:e53cc280a1ab616f191ac7ebdd1f38f2aa78b1411dd2677dc556c6f0fa085913", - "sha256:fcaed03cefa53f62346091ef92da7a6f44bae6830a6f4c6b097a70cdc31b1199" + "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", + "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", + "sha256:23163921dccf3103ce59540b0443c106d2c0a0ff2e0503e05196f5e6fdea453f", + "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", + "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", + "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", + "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", + "sha256:3990fffcc6a6045f2234ab72752ad037e3b2d48c72037f244d42738db397eb75", + "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", + "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", + "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", + "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", + "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", + "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", + "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", + "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", + "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", + "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", + "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", + "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", + "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", + "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", + "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", + "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", + "sha256:bba8bea1b28d927b3e99e47deafe53658d34497c0a891d95ff1ba8ff6663f01c", + "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", + "sha256:c0c79b13c9908ac7dfe0a74116ebc9a0f28b2319d23c32f3dfcdfbe1279c7eaf", + "sha256:c7116b0452994734ccff35e154b44240090eb0f4f74b9106292668133557c175", + "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", + "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", + "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", + "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", + "sha256:cfb54563fe5f130da17c44c6a4e2e8052ba628e5ab4eab7ef8190f736f0f8f72", + "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", + "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", + "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131" ], "markers": "python_version >= '3.10'", - "version": "==7.4.1" + "version": "==7.4.3" }, "charset-normalizer": { "hashes": [ @@ -2839,11 +2876,11 @@ }, "click": { "hashes": [ - "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", - "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" + "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", + "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" ], "markers": "python_version >= '3.10'", - "version": "==8.3.2" + "version": "==8.3.3" }, "click-didyoumean": { "hashes": [ @@ -3069,67 +3106,67 @@ }, "crc-bonfire": { "hashes": [ - "sha256:7fa3b2e739a92c2ffdc3f43ba590201dff64d6993603fc43deb10f3f0c2d903a", - "sha256:d9a32ad91714dd0c00c56a405add63df4896dfeb58994ce5f9688dde7cd4298a" + "sha256:54c60db35846eeffc515013b7966d6313e1b12d4b1df433271fd29cfd581207a", + "sha256:cceb257faf44b7116976d822518d12ab3ec329951078199aa577dff189a61e77" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.9.1" + "markers": "python_version >= '3.10'", + "version": "==6.12.0" }, "cryptography": { "hashes": [ - "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", - "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", - "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", - "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", - "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", - "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", - "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", - "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", - "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", - "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", - "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", - "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", - "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", - "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", - "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", - "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", - "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", - "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", - "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", - "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", - "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", - "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", - "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", - "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", - "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", - "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", - "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", - "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", - "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", - "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", - "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", - "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", - "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", - "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", - "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", - "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", - "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", - "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", - "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", - "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", - "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", - "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", - "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", - "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", - "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", - "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", - "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", - "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", - "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" + "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", + "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", + "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", + "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", + "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", + "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", + "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", + "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", + "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", + "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", + "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", + "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", + "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", + "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", + "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", + "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", + "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", + "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", + "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", + "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", + "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", + "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", + "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", + "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", + "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", + "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", + "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", + "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", + "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", + "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", + "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", + "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", + "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", + "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", + "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", + "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", + "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", + "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", + "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", + "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", + "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", + "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", + "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", + "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", + "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", + "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", + "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", + "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", + "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.7" + "version": "==47.0.0" }, "cycler": { "hashes": [ @@ -3208,20 +3245,20 @@ }, "faker": { "hashes": [ - "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", - "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019" + "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", + "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==40.13.0" + "version": "==40.15.0" }, "filelock": { "hashes": [ - "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", - "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70" + "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", + "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258" ], "markers": "python_version >= '3.10'", - "version": "==3.25.2" + "version": "==3.29.0" }, "flake8": { "hashes": [ @@ -3302,20 +3339,20 @@ "grpc" ], "hashes": [ - "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", - "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" + "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", + "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" ], "markers": "python_version >= '3.9'", - "version": "==2.30.2" + "version": "==2.30.3" }, "google-auth": { "hashes": [ - "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", - "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" + "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", + "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.1" + "version": "==2.49.2" }, "google-cloud-bigquery": { "hashes": [ @@ -3398,25 +3435,6 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, - "gql": { - "extras": [ - "requests" - ], - "hashes": [ - "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", - "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479" - ], - "markers": "python_full_version >= '3.8.1'", - "version": "==4.0.0" - }, - "graphql-core": { - "hashes": [ - "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", - "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==3.2.8" - }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -3502,19 +3520,19 @@ }, "identify": { "hashes": [ - "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", - "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737" + "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", + "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842" ], "markers": "python_version >= '3.10'", - "version": "==2.6.18" + "version": "==2.6.19" }, "idna": { "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", + "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" ], "markers": "python_version >= '3.8'", - "version": "==3.11" + "version": "==3.13" }, "isodate": { "hashes": [ @@ -3540,14 +3558,6 @@ "markers": "python_version >= '3.9'", "version": "==1.1.0" }, - "junitparser": { - "hashes": [ - "sha256:9e279f2214dc74b6a86b22db757abda2e8e66e819fe882dad5b392d57024cd26", - "sha256:f15e292877258d7c5755d672ce86f82c3622c7ea4c2f44f55de44ed7518484d3" - ], - "markers": "python_version >= '3.10'", - "version": "==5.0.0" - }, "kiwisolver": { "hashes": [ "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", @@ -3673,12 +3683,12 @@ }, "koku-nise": { "hashes": [ - "sha256:5ac277879ad686c3f0b9fbd8084e48868a949948f293783d61ad8c219f9a650c", - "sha256:b189f3aad01b698228459a757bfd14af23ec177f582bcf528b897fe02bbae893" + "sha256:2d8c03b9a27f8f9f5caf87ca68b4a6ba23b4d7e6291f69ad0b01655d96e64c51", + "sha256:42b6db673060c9ed23c4bc2badb57fb840a8dccf09d2ec933194aa37e926439d" ], "index": "pypi", "markers": "python_version >= '3.11'", - "version": "==5.4.0" + "version": "==5.4.1" }, "kombu": { "hashes": [ @@ -3794,65 +3804,65 @@ }, "matplotlib": { "hashes": [ - "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", - "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", - "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", - "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", - "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", - "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", - "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", - "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", - "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", - "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", - "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", - "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", - "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", - "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", - "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", - "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", - "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", - "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", - "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", - "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", - "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", - "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", - "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", - "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", - "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", - "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", - "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", - "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", - "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", - "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", - "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", - "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", - "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", - "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", - "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", - "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", - "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", - "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", - "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", - "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", - "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", - "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", - "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", - "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", - "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", - "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", - "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", - "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", - "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", - "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", - "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", - "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", - "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", - "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", - "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7" + "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", + "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", + "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", + "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", + "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", + "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", + "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", + "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", + "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", + "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", + "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", + "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", + "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", + "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", + "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", + "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", + "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", + "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", + "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", + "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", + "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", + "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", + "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", + "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", + "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", + "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", + "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", + "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", + "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", + "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", + "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", + "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", + "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", + "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", + "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", + "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", + "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", + "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", + "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", + "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", + "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", + "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", + "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", + "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", + "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", + "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", + "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", + "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", + "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", + "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", + "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", + "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", + "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", + "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", + "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==3.10.8" + "version": "==3.10.9" }, "mccabe": { "hashes": [ @@ -3871,158 +3881,6 @@ "markers": "python_version >= '3.10'", "version": "==1.23.4" }, - "multidict": { - "hashes": [ - "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", - "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", - "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", - "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", - "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", - "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", - "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", - "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", - "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", - "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", - "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", - "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", - "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", - "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", - "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", - "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", - "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", - "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", - "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", - "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", - "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", - "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", - "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", - "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", - "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", - "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", - "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", - "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", - "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", - "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", - "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", - "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", - "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", - "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", - "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", - "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", - "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", - "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", - "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", - "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", - "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", - "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", - "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", - "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", - "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", - "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", - "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", - "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", - "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", - "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", - "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", - "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", - "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", - "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", - "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", - "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", - "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", - "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", - "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", - "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", - "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", - "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", - "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", - "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", - "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", - "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", - "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", - "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", - "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", - "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", - "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", - "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", - "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", - "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", - "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", - "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", - "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", - "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", - "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", - "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", - "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", - "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", - "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", - "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", - "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", - "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", - "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", - "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", - "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", - "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", - "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", - "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", - "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", - "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", - "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", - "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", - "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", - "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", - "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", - "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", - "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", - "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", - "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", - "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", - "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", - "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", - "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", - "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", - "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", - "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", - "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", - "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", - "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", - "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", - "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", - "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", - "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", - "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", - "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", - "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", - "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", - "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", - "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", - "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", - "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", - "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", - "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", - "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", - "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", - "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", - "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", - "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", - "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", - "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", - "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", - "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", - "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", - "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", - "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", - "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", - "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", - "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", - "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", - "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", - "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", - "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" - ], - "markers": "python_version >= '3.9'", - "version": "==6.7.1" - }, "nodeenv": { "hashes": [ "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", @@ -4117,29 +3975,14 @@ "markers": "python_version >= '3.8'", "version": "==3.3.1" }, - "ocviapy": { - "hashes": [ - "sha256:77602d07d1f124e6f020e08ce21379e8a12cbefa98b168e697fdc202c770f9e2", - "sha256:e5558ec5c46d2793949c0b2fd1c99759d5b5ee564e2ac9ec56fd200c94dcaef4" - ], - "markers": "python_version >= '3.6'", - "version": "==1.6.0" - }, "packaging": { "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.0" - }, - "parsedatetime": { - "hashes": [ - "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", - "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" - ], - "version": "==2.6" + "version": "==26.2" }, "pillow": { "hashes": [ @@ -4265,12 +4108,12 @@ }, "pre-commit": { "hashes": [ - "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", - "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61" + "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", + "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==4.5.1" + "version": "==4.6.0" }, "prometheus-client": { "hashes": [ @@ -4289,134 +4132,6 @@ "markers": "python_version >= '3.8'", "version": "==3.0.52" }, - "propcache": { - "hashes": [ - "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", - "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", - "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", - "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", - "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", - "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", - "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", - "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", - "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", - "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", - "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", - "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", - "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", - "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", - "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", - "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", - "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", - "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", - "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", - "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", - "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", - "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", - "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", - "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", - "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", - "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", - "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", - "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", - "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", - "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", - "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", - "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", - "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", - "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", - "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", - "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", - "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", - "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", - "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", - "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", - "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", - "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", - "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", - "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", - "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", - "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", - "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", - "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", - "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", - "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", - "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", - "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", - "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", - "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", - "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", - "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", - "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", - "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", - "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", - "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", - "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", - "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", - "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", - "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", - "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", - "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", - "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", - "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", - "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", - "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", - "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", - "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", - "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", - "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", - "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", - "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", - "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", - "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", - "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", - "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", - "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", - "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", - "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", - "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", - "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", - "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", - "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", - "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", - "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", - "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", - "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", - "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", - "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", - "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", - "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", - "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", - "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", - "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", - "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", - "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", - "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", - "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", - "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", - "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", - "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", - "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", - "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", - "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", - "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", - "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", - "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", - "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", - "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", - "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", - "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", - "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", - "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", - "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", - "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", - "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", - "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", - "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.1" - }, "proto-plus": { "hashes": [ "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", @@ -4470,7 +4185,7 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "implementation_name != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.0" }, "pydocstyle": { @@ -4512,25 +4227,9 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, - "python-discovery": { - "hashes": [ - "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", - "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a" - ], - "markers": "python_version >= '3.8'", - "version": "==1.2.2" - }, - "python-dotenv": { - "hashes": [ - "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", - "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" - ], - "markers": "python_version >= '3.10'", - "version": "==1.2.2" - }, "pytz": { "hashes": [ "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", @@ -4644,14 +4343,6 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, "responses": { "hashes": [ "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", @@ -4663,26 +4354,18 @@ }, "s3transfer": { "hashes": [ - "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", - "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" + "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", + "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" ], "markers": "python_version >= '3.9'", - "version": "==0.16.0" - }, - "sh": { - "hashes": [ - "sha256:653227a7c41a284ec5302173fbc044ee817c7bad5e6e4d8d55741b9aeb9eb65b", - "sha256:e0b15b4ae8ffcd399bc8ffddcbd770a43c7a70a24b16773fbb34c001ad5d52af" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", - "version": "==2.2.2" + "version": "==0.16.1" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.17.0" }, "snakeviz": { @@ -4699,7 +4382,7 @@ "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895" ], - "markers": "python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version not in '3.0, 3.1, 3.2'", "version": "==3.0.1" }, "sqlparse": { @@ -4711,14 +4394,6 @@ "markers": "python_version >= '3.8'", "version": "==0.5.5" }, - "tabulate": { - "hashes": [ - "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", - "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3" - ], - "markers": "python_version >= '3.10'", - "version": "==0.10.0" - }, "tblib": { "hashes": [ "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", @@ -4746,20 +4421,12 @@ }, "tox": { "hashes": [ - "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", - "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca" + "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229", + "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==4.26.0" - }, - "truststore": { - "hashes": [ - "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", - "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981" - ], - "markers": "python_version >= '3.10'", - "version": "==0.10.4" + "markers": "python_version >= '3.8'", + "version": "==4.11.4" }, "typing-extensions": { "hashes": [ @@ -4771,11 +4438,11 @@ }, "tzdata": { "hashes": [ - "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", - "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" + "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", + "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" ], "markers": "python_version >= '2'", - "version": "==2026.1" + "version": "==2026.2" }, "tzlocal": { "hashes": [ @@ -4803,19 +4470,11 @@ }, "virtualenv": { "hashes": [ - "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", - "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2" + "sha256:a00332e0b089ba64edefbf94ef34e6db33f45fe5184df0652cf0b754dcd36190", + "sha256:c551ea1072d7717a273dd9a4a0adad8f45571af134b0f63a12430e85f6ac1c08" ], "markers": "python_version >= '3.8'", - "version": "==21.2.1" - }, - "wait-for": { - "hashes": [ - "sha256:1129f3350e29b0600889e24328d685a6bff048c8f4cabce28ef7632ed40c5d91", - "sha256:5642975f1fc5850acb55684b2d7842bd820fb068e725cd4ffff4bf3eba8e2788" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" + "version": "==20.39.1" }, "watchdog": { "hashes": [ @@ -4869,140 +4528,6 @@ ], "markers": "python_version >= '3.9'", "version": "==1.9.0" - }, - "yarl": { - "hashes": [ - "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", - "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", - "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", - "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", - "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", - "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", - "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", - "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", - "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", - "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", - "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", - "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", - "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", - "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", - "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", - "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", - "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", - "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", - "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", - "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", - "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", - "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", - "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", - "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", - "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", - "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", - "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", - "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", - "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", - "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", - "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", - "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", - "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", - "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", - "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", - "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", - "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", - "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", - "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", - "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", - "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", - "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", - "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", - "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", - "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", - "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", - "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", - "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", - "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", - "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", - "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", - "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", - "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", - "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", - "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", - "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", - "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", - "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", - "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", - "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", - "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", - "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", - "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", - "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", - "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", - "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", - "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", - "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", - "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", - "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", - "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", - "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", - "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", - "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", - "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", - "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", - "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", - "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", - "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", - "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", - "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", - "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", - "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", - "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", - "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", - "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", - "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", - "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", - "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", - "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", - "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", - "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", - "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", - "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", - "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", - "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", - "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", - "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", - "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", - "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", - "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", - "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", - "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", - "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", - "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", - "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", - "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", - "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", - "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", - "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", - "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", - "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", - "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", - "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", - "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", - "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", - "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", - "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", - "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", - "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", - "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", - "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", - "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", - "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", - "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", - "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", - "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", - "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d" - ], - "markers": "python_version >= '3.10'", - "version": "==1.23.0" } } } From b989ff77b73415733117ee53a2ecd17177373103 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 15:39:46 +0300 Subject: [PATCH 078/106] revert Pipfile.lock --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index d168ea1e54..ed01b67fa1 100644 --- a/Pipfile +++ b/Pipfile @@ -74,7 +74,7 @@ requests-mock = ">=1.7" responses = ">=0.10" snakeviz = "*" tblib = ">=1.6" -tox = "==4.11.4" # fcache (via unleashclient) pins platformdirs~=3.0; tox>=4.12 needs platformdirs>=4.1 +tox = "==4.26.0" # requires a higher cachetools version than google-auth at the moment watchdog = ">=2.1.1" polyfactory = "*" koku-nise = "*" From 4160b2ac6031e76fd9a5f1cbd89083a71895bbb3 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 15:53:27 +0300 Subject: [PATCH 079/106] revert Pipfile.lock --- Pipfile | 3 +- Pipfile.lock | 1715 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 1096 insertions(+), 622 deletions(-) diff --git a/Pipfile b/Pipfile index ed01b67fa1..c4ca341d87 100644 --- a/Pipfile +++ b/Pipfile @@ -53,7 +53,6 @@ pandas = "<3.0" scipy = ">=1.16" boto3 = "*" sqlalchemy = ">=2.0.0" -Babel = "*" [dev-packages] argh = ">=0.26.2" @@ -80,4 +79,4 @@ polyfactory = "*" koku-nise = "*" [requires] -python_version = "3.11" +python_version = "3.11" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index dd8674fcf7..13850feb96 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c55e9044eabfcec9a67223c3788b50fde7c1939457816c381743d391751c933b" + "sha256": "8355828638d3b0648e8f215a63312b8cc8edc9f80fd3dfb07e960556aa6086a0" }, "pipfile-spec": 6, "requires": { @@ -115,15 +115,6 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, - "babel": { - "hashes": [ - "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", - "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.18.0" - }, "beautifulsoup4": { "hashes": [ "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", @@ -143,29 +134,29 @@ }, "boto3": { "hashes": [ - "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", - "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" + "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", + "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" }, "botocore": { "hashes": [ - "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", - "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" + "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", + "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" ], "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" }, "cachetools": { "hashes": [ - "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", - "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" + "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", + "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.6" + "version": "==7.0.5" }, "celery": { "hashes": [ @@ -178,11 +169,11 @@ }, "certifi": { "hashes": [ - "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", - "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2026.4.22" + "version": "==2026.2.25" }, "cffi": { "hashes": [ @@ -271,7 +262,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_version >= '3.9'", + "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", "version": "==2.0.0" }, "charset-normalizer": { @@ -482,11 +473,11 @@ }, "click": { "hashes": [ - "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", - "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" + "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", + "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" ], "markers": "python_version >= '3.10'", - "version": "==8.3.3" + "version": "==8.3.2" }, "click-didyoumean": { "hashes": [ @@ -556,58 +547,58 @@ }, "cryptography": { "hashes": [ - "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", - "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", - "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", - "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", - "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", - "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", - "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", - "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", - "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", - "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", - "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", - "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", - "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", - "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", - "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", - "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", - "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", - "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", - "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", - "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", - "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", - "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", - "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", - "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", - "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", - "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", - "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", - "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", - "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", - "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", - "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", - "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", - "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", - "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", - "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", - "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", - "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", - "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", - "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", - "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", - "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", - "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", - "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", - "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", - "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", - "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", - "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", - "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", - "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" + "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", + "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", + "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", + "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", + "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", + "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", + "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", + "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", + "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", + "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", + "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", + "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", + "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", + "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", + "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", + "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", + "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", + "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", + "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", + "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", + "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", + "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", + "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", + "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", + "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", + "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", + "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", + "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", + "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", + "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", + "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", + "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", + "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", + "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", + "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", + "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", + "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", + "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", + "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", + "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", + "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", + "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", + "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", + "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", + "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", + "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", + "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", + "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", + "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==47.0.0" + "version": "==46.0.7" }, "django": { "hashes": [ @@ -710,11 +701,11 @@ "grpc" ], "hashes": [ - "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", - "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" + "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", + "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" ], "markers": "python_version >= '3.9'", - "version": "==2.30.3" + "version": "==2.30.2" }, "google-api-python-client": { "hashes": [ @@ -727,12 +718,12 @@ }, "google-auth": { "hashes": [ - "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", - "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" + "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", + "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.2" + "version": "==2.49.1" }, "google-auth-httplib2": { "hashes": [ @@ -823,71 +814,6 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, - "greenlet": { - "hashes": [ - "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", - "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", - "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", - "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", - "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", - "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", - "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", - "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", - "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", - "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", - "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", - "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", - "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", - "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", - "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", - "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", - "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", - "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", - "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", - "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", - "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", - "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", - "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", - "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", - "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", - "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", - "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", - "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", - "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", - "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", - "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", - "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", - "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", - "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", - "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", - "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", - "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", - "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", - "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", - "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", - "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", - "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", - "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", - "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", - "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", - "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", - "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", - "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", - "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", - "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", - "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", - "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", - "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", - "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", - "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", - "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", - "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", - "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", - "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564" - ], - "markers": "python_version >= '3.10'", - "version": "==3.5.0" - }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -982,11 +908,11 @@ }, "idna": { "hashes": [ - "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", - "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" ], "markers": "python_version >= '3.8'", - "version": "==3.13" + "version": "==3.11" }, "importlib-metadata": { "hashes": [ @@ -1487,17 +1413,17 @@ "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6" ], - "markers": "python_version >= '3.10'", + "markers": "platform_python_implementation != 'PyPy'", "version": "==3.11.8" }, "packaging": { "hashes": [ - "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", - "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" + "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", + "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.2" + "version": "==26.0" }, "pandas": { "hashes": [ @@ -1648,134 +1574,134 @@ }, "psycopg2-binary": { "hashes": [ - "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", - "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964", - "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", - "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", - "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115", - "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", - "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", - "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", - "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", - "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", - "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", - "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", - "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", - "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", - "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", - "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", - "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", - "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", - "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", - "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", - "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf", - "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", - "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", - "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", - "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", - "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", - "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", - "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", - "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94", - "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", - "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", - "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", - "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", - "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", - "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", - "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", - "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", - "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", - "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", - "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", - "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", - "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", - "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", - "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", - "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915", - "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", - "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", - "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe", - "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", - "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", - "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", - "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86", - "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", - "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", - "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", - "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", - "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03", - "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", - "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", - "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", - "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", - "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab", - "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", - "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10", - "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", - "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2", - "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e" + "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", + "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", + "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", + "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", + "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", + "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", + "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", + "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", + "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", + "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", + "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", + "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", + "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", + "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", + "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", + "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", + "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", + "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", + "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", + "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", + "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", + "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", + "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", + "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", + "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", + "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", + "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", + "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", + "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", + "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", + "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", + "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", + "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", + "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", + "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", + "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", + "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", + "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", + "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", + "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", + "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", + "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", + "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", + "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", + "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", + "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", + "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", + "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", + "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", + "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", + "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", + "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", + "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", + "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", + "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", + "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", + "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", + "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", + "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", + "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", + "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", + "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", + "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", + "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", + "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", + "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", + "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.9.12" + "version": "==2.9.11" }, "pyarrow": { "hashes": [ - "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", - "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", - "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", - "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", - "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", - "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", - "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", - "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", - "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", - "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", - "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", - "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", - "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", - "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", - "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", - "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", - "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", - "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", - "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", - "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", - "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", - "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", - "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", - "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", - "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", - "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", - "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", - "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", - "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", - "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", - "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", - "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", - "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", - "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", - "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", - "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", - "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", - "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", - "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", - "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", - "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", - "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", - "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", - "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", - "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", - "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", - "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", - "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", - "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", - "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072" + "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", + "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", + "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", + "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", + "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", + "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", + "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", + "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", + "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", + "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", + "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", + "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", + "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", + "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", + "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", + "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", + "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", + "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", + "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", + "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", + "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", + "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", + "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", + "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", + "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", + "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", + "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", + "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", + "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", + "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", + "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", + "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", + "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", + "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", + "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", + "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", + "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", + "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", + "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", + "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", + "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", + "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", + "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", + "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", + "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", + "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", + "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", + "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", + "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", + "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==24.0.0" + "version": "==23.0.1" }, "pyasn1": { "hashes": [ @@ -1798,143 +1724,144 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "python_version >= '3.10'", + "markers": "implementation_name != 'PyPy'", "version": "==3.0" }, "pydantic": { "hashes": [ - "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", - "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d" + "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", + "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.13.3" + "version": "==2.12.5" }, "pydantic-core": { "hashes": [ - "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", - "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", - "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", - "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", - "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", - "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", - "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", - "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", - "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", - "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", - "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", - "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", - "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", - "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", - "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", - "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", - "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", - "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", - "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", - "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", - "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", - "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", - "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", - "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495", - "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", - "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0", - "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd", - "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", - "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", - "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", - "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", - "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", - "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", - "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", - "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", - "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", - "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", - "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", - "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", - "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", - "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720", - "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", - "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c", - "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", - "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", - "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", - "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", - "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", - "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", - "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873", - "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", - "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", - "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", - "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", - "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", - "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd", - "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", - "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", - "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", - "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d", - "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", - "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", - "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", - "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", - "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168", - "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", - "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13", - "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", - "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", - "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", - "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", - "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", - "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", - "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", - "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", - "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", - "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", - "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", - "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", - "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", - "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", - "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", - "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", - "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", - "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", - "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", - "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", - "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", - "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", - "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", - "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", - "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", - "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", - "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", - "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", - "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", - "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", - "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6", - "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79", - "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a", - "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", - "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", - "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", - "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", - "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", - "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", - "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", - "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", - "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", - "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", - "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", - "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", - "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", - "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", - "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb", - "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", - "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", - "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", - "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", - "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56" + "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", + "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", + "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", + "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", + "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", + "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", + "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", + "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", + "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", + "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", + "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", + "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", + "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", + "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", + "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", + "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", + "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", + "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", + "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", + "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", + "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", + "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", + "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", + "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", + "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", + "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", + "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", + "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", + "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", + "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", + "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", + "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", + "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", + "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", + "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", + "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", + "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", + "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", + "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", + "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", + "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", + "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", + "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", + "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", + "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", + "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", + "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", + "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", + "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", + "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", + "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", + "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", + "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", + "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", + "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", + "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", + "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", + "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", + "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", + "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", + "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", + "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", + "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", + "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", + "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", + "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", + "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", + "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", + "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", + "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", + "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", + "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", + "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", + "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", + "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", + "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", + "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", + "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", + "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", + "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", + "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", + "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", + "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", + "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", + "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", + "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", + "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", + "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", + "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", + "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", + "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", + "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", + "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", + "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", + "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", + "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", + "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", + "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", + "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", + "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", + "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", + "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", + "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", + "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", + "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", + "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", + "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", + "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", + "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", + "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", + "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", + "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", + "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", + "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", + "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", + "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", + "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", + "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", + "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", + "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", + "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" ], "markers": "python_version >= '3.9'", - "version": "==2.46.3" + "version": "==2.41.5" }, "pyjwt": { "extras": [ @@ -1961,7 +1888,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, "pytz": { @@ -1999,11 +1926,11 @@ }, "s3transfer": { "hashes": [ - "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", - "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" + "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", + "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" ], "markers": "python_version >= '3.9'", - "version": "==0.16.1" + "version": "==0.16.0" }, "scipy": { "hashes": [ @@ -2083,19 +2010,19 @@ }, "sentry-sdk": { "hashes": [ - "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", - "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f" + "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", + "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.58.0" + "version": "==2.57.0" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.17.0" }, "soupsieve": { @@ -2255,11 +2182,11 @@ }, "tzdata": { "hashes": [ - "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", - "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" + "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", + "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" ], "markers": "python_version >= '2'", - "version": "==2026.2" + "version": "==2026.1" }, "tzlocal": { "hashes": [ @@ -2384,11 +2311,11 @@ }, "zipp": { "hashes": [ - "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", - "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110" + "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", + "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" ], "markers": "python_version >= '3.9'", - "version": "==3.23.1" + "version": "==3.23.0" }, "zstandard": { "hashes": [ @@ -2505,6 +2432,30 @@ "markers": "python_version >= '3.6'", "version": "==5.3.1" }, + "anyio": { + "hashes": [ + "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", + "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" + ], + "markers": "python_version >= '3.10'", + "version": "==4.13.0" + }, + "anytree": { + "hashes": [ + "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", + "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714" + ], + "markers": "python_version < '4.0' and python_full_version >= '3.9.2'", + "version": "==2.13.0" + }, + "app-common-python": { + "hashes": [ + "sha256:74125371e2865bcba7a7c34f0700d4b8b97a0c0a9ff67bbb06fdb20a3da4b75c", + "sha256:861bd93482edabd00a85eb6512e0d7df08597be6f76b864be8e53d6294f81389" + ], + "index": "pypi", + "version": "==0.2.9" + }, "argh": { "hashes": [ "sha256:2edac856ff50126f6e47d884751328c9f466bacbbb6cbfdac322053d94705494", @@ -2548,6 +2499,14 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, + "backoff": { + "hashes": [ + "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", + "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==2.2.1" + }, "billiard": { "hashes": [ "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", @@ -2558,29 +2517,37 @@ }, "boto3": { "hashes": [ - "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", - "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" + "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", + "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" }, "botocore": { "hashes": [ - "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", - "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" + "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", + "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" ], "markers": "python_version >= '3.9'", - "version": "==1.42.97" + "version": "==1.42.87" + }, + "cached-property": { + "hashes": [ + "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", + "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb" + ], + "markers": "python_version >= '3.8'", + "version": "==2.0.1" }, "cachetools": { "hashes": [ - "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", - "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" + "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", + "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.6" + "version": "==7.0.5" }, "celery": { "hashes": [ @@ -2593,11 +2560,11 @@ }, "certifi": { "hashes": [ - "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", - "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" + "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", + "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" ], "markers": "python_version >= '3.7'", - "version": "==2026.4.22" + "version": "==2026.2.25" }, "cffi": { "hashes": [ @@ -2686,7 +2653,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_version >= '3.9'", + "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", "version": "==2.0.0" }, "cfgv": { @@ -2699,45 +2666,41 @@ }, "chardet": { "hashes": [ - "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", - "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", - "sha256:23163921dccf3103ce59540b0443c106d2c0a0ff2e0503e05196f5e6fdea453f", - "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", - "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", - "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", - "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", - "sha256:3990fffcc6a6045f2234ab72752ad037e3b2d48c72037f244d42738db397eb75", - "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", - "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", - "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", - "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", - "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", - "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", - "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", - "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", - "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", - "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", - "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", - "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", - "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", - "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", - "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", - "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", - "sha256:bba8bea1b28d927b3e99e47deafe53658d34497c0a891d95ff1ba8ff6663f01c", - "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", - "sha256:c0c79b13c9908ac7dfe0a74116ebc9a0f28b2319d23c32f3dfcdfbe1279c7eaf", - "sha256:c7116b0452994734ccff35e154b44240090eb0f4f74b9106292668133557c175", - "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", - "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", - "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", - "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", - "sha256:cfb54563fe5f130da17c44c6a4e2e8052ba628e5ab4eab7ef8190f736f0f8f72", - "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", - "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", - "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131" + "sha256:0487a6a6846740f39f9fcd71e3acff2982bae8bca3507ee986d5155cd458e044", + "sha256:04b9be0d786b9a3bbd7860a82d27e843f22211be51b9b84d85fe8d9864e2f535", + "sha256:0848015eb1471e1499963dff2776557af05f99c38ba2a14f34ea078f8668c6a9", + "sha256:0de8d636391f9050e4e048ca8a9f98b25e67eff389705f8c6ff1ab9593f7339b", + "sha256:19bcd1de4a0c1a5802f9d2d370b6696668bddc166a3c89c113cf109313b3d99f", + "sha256:277ce1174ea054415a3c2ad5f51aa089a96dda16999de56e4ac1bc366d0d535e", + "sha256:2da446b920064ca9574504c29a07ef5eae91a1948a302a25043a16fb79ec2397", + "sha256:35ade2a6f93e5d2bdff541b584126cfe066eac5c9457572ff97cabc8068bece1", + "sha256:3886f8f9bb3500bd8c421b2de9b4878a0c183f80bc289338cdda869dfd4397fb", + "sha256:3d66d2949754ad924865a47e81857a0792dc8edc651094285116b6df2e218445", + "sha256:4ececf9631f7932a2cef728746303d71ae8204923190253d5382ee37739dd46e", + "sha256:5d86402a506631af2fb36e3d1c72021477b228fb0dcdb44400b9b681f14b14c0", + "sha256:5e686e5a0d8155cfbf5b1a579f5790bb01bf1a0a52e7f98b38801c09a0c63fcd", + "sha256:62b25b3ea5ef8e1672726e4c1601f6636ce3b76b9de92af669c8000711d7fe13", + "sha256:6b5c03330b9108124f8174a596284b737e95f1cf6a99953c37cea7e2583212e7", + "sha256:7a1c2be068ab91a472fd74617d9371605897c3061ca4f2e5df63d9f3d9c11f8e", + "sha256:80de820fa1df95a2e7c9898867f7d6ff3dc3d52a109938b215b37ab54474d307", + "sha256:8befb12c263cb14e26f065a3f99d76ef2610b0266cb70d827bb61528e9e60f28", + "sha256:9381b3d9075c8a2e622b4d46db5e4229c94aebc71d4c8e620d9cf2cea2930824", + "sha256:a9322fd3ffd359b49b2d608725a15975ebc0d66f2dcedefa7ddb5847e54a6f9c", + "sha256:a98c361b73c6ce4a44beaecf5cfb389ec69b566dd7f3f4ea5d1bde6487e7054d", + "sha256:b726b0b2684d29cd08f602bb4266334386c58741ff34c9e2f6cdf97ad604e235", + "sha256:be39708b300a80a9f78ef8f81018e2e9c6274a71c0823a4d6e493c72f7b3d2a2", + "sha256:bee1d665ca5810d8e3cf11122619e85c23b209075cbddb91f213675248f0e522", + "sha256:c03925738670199d253b8c79828d8a68d404f629a2dbf1b4b5aabd8c8b0249ab", + "sha256:c820c95d8b4de8aea99b54083d38f10f763686646962b5627e8e2b2db113a37b", + "sha256:c98e1044785ab71f0fee70f64b8d56f69df9de1b593793022e001ba2f6b76dd0", + "sha256:caf0715b8a5e20fc3faf21a24abd3ae513f8f58206dd32d1b87eca6351e105ed", + "sha256:cda41132a45dfbf6984dade1f531a4098c813caf266c66cc446d90bb9369cabd", + "sha256:d8aa2bae7d0523963395f802ae2212e8b2248d4503a14a691de86edf716b22d3", + "sha256:e53cc280a1ab616f191ac7ebdd1f38f2aa78b1411dd2677dc556c6f0fa085913", + "sha256:fcaed03cefa53f62346091ef92da7a6f44bae6830a6f4c6b097a70cdc31b1199" ], "markers": "python_version >= '3.10'", - "version": "==7.4.3" + "version": "==7.4.1" }, "charset-normalizer": { "hashes": [ @@ -2876,11 +2839,11 @@ }, "click": { "hashes": [ - "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", - "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" + "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", + "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" ], "markers": "python_version >= '3.10'", - "version": "==8.3.3" + "version": "==8.3.2" }, "click-didyoumean": { "hashes": [ @@ -3106,67 +3069,67 @@ }, "crc-bonfire": { "hashes": [ - "sha256:54c60db35846eeffc515013b7966d6313e1b12d4b1df433271fd29cfd581207a", - "sha256:cceb257faf44b7116976d822518d12ab3ec329951078199aa577dff189a61e77" + "sha256:7fa3b2e739a92c2ffdc3f43ba590201dff64d6993603fc43deb10f3f0c2d903a", + "sha256:d9a32ad91714dd0c00c56a405add63df4896dfeb58994ce5f9688dde7cd4298a" ], "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==6.12.0" + "markers": "python_version >= '3.6'", + "version": "==6.9.1" }, "cryptography": { "hashes": [ - "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", - "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", - "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", - "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", - "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", - "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", - "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", - "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", - "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", - "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", - "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", - "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", - "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", - "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", - "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", - "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", - "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", - "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", - "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", - "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", - "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", - "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", - "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", - "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", - "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", - "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", - "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", - "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", - "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", - "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", - "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", - "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", - "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", - "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", - "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", - "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", - "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", - "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", - "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", - "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", - "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", - "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", - "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", - "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", - "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", - "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", - "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", - "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", - "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" + "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", + "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", + "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", + "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", + "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", + "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", + "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", + "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", + "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", + "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", + "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", + "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", + "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", + "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", + "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", + "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", + "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", + "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", + "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", + "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", + "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", + "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", + "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", + "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", + "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", + "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", + "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", + "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", + "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", + "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", + "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", + "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", + "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", + "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", + "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", + "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", + "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", + "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", + "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", + "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", + "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", + "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", + "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", + "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", + "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", + "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", + "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", + "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", + "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==47.0.0" + "version": "==46.0.7" }, "cycler": { "hashes": [ @@ -3245,20 +3208,20 @@ }, "faker": { "hashes": [ - "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", - "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318" + "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", + "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==40.15.0" + "version": "==40.13.0" }, "filelock": { "hashes": [ - "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", - "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258" + "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", + "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70" ], "markers": "python_version >= '3.10'", - "version": "==3.29.0" + "version": "==3.25.2" }, "flake8": { "hashes": [ @@ -3339,20 +3302,20 @@ "grpc" ], "hashes": [ - "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", - "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" + "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", + "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" ], "markers": "python_version >= '3.9'", - "version": "==2.30.3" + "version": "==2.30.2" }, "google-auth": { "hashes": [ - "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", - "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" + "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", + "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.2" + "version": "==2.49.1" }, "google-cloud-bigquery": { "hashes": [ @@ -3435,6 +3398,25 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, + "gql": { + "extras": [ + "requests" + ], + "hashes": [ + "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", + "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479" + ], + "markers": "python_full_version >= '3.8.1'", + "version": "==4.0.0" + }, + "graphql-core": { + "hashes": [ + "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", + "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c" + ], + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==3.2.8" + }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -3520,19 +3502,19 @@ }, "identify": { "hashes": [ - "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", - "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842" + "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", + "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737" ], "markers": "python_version >= '3.10'", - "version": "==2.6.19" + "version": "==2.6.18" }, "idna": { "hashes": [ - "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", - "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" ], "markers": "python_version >= '3.8'", - "version": "==3.13" + "version": "==3.11" }, "isodate": { "hashes": [ @@ -3558,6 +3540,14 @@ "markers": "python_version >= '3.9'", "version": "==1.1.0" }, + "junitparser": { + "hashes": [ + "sha256:9e279f2214dc74b6a86b22db757abda2e8e66e819fe882dad5b392d57024cd26", + "sha256:f15e292877258d7c5755d672ce86f82c3622c7ea4c2f44f55de44ed7518484d3" + ], + "markers": "python_version >= '3.10'", + "version": "==5.0.0" + }, "kiwisolver": { "hashes": [ "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", @@ -3683,12 +3673,12 @@ }, "koku-nise": { "hashes": [ - "sha256:2d8c03b9a27f8f9f5caf87ca68b4a6ba23b4d7e6291f69ad0b01655d96e64c51", - "sha256:42b6db673060c9ed23c4bc2badb57fb840a8dccf09d2ec933194aa37e926439d" + "sha256:5ac277879ad686c3f0b9fbd8084e48868a949948f293783d61ad8c219f9a650c", + "sha256:b189f3aad01b698228459a757bfd14af23ec177f582bcf528b897fe02bbae893" ], "index": "pypi", "markers": "python_version >= '3.11'", - "version": "==5.4.1" + "version": "==5.4.0" }, "kombu": { "hashes": [ @@ -3804,65 +3794,65 @@ }, "matplotlib": { "hashes": [ - "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", - "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", - "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", - "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", - "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", - "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", - "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", - "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", - "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", - "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", - "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", - "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", - "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", - "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", - "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", - "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", - "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", - "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", - "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", - "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", - "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", - "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", - "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", - "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", - "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", - "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", - "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", - "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", - "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", - "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", - "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", - "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", - "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", - "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", - "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", - "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", - "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", - "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", - "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", - "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", - "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", - "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", - "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", - "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", - "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", - "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", - "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", - "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", - "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", - "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", - "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", - "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", - "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", - "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", - "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358" + "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", + "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", + "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", + "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", + "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", + "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", + "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", + "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", + "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", + "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", + "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", + "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", + "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", + "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", + "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", + "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", + "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", + "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", + "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", + "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", + "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", + "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", + "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", + "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", + "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", + "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", + "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", + "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", + "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", + "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", + "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", + "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", + "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", + "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", + "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", + "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", + "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", + "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", + "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", + "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", + "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", + "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", + "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", + "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", + "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", + "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", + "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", + "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", + "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", + "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", + "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", + "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", + "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", + "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", + "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==3.10.9" + "version": "==3.10.8" }, "mccabe": { "hashes": [ @@ -3881,6 +3871,158 @@ "markers": "python_version >= '3.10'", "version": "==1.23.4" }, + "multidict": { + "hashes": [ + "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", + "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", + "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", + "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", + "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", + "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", + "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", + "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", + "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", + "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", + "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", + "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", + "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", + "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", + "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", + "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", + "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", + "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", + "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", + "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", + "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", + "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", + "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", + "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", + "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", + "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", + "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", + "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", + "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", + "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", + "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", + "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", + "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", + "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", + "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", + "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", + "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", + "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", + "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", + "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", + "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", + "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", + "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", + "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", + "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", + "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", + "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", + "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", + "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", + "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", + "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", + "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", + "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", + "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", + "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", + "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", + "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", + "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", + "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", + "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", + "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", + "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", + "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", + "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", + "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", + "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", + "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", + "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", + "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", + "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", + "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", + "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", + "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", + "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", + "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", + "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", + "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", + "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", + "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", + "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", + "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", + "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", + "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", + "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", + "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", + "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", + "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", + "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", + "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", + "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", + "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", + "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", + "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", + "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", + "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", + "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", + "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", + "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", + "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", + "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", + "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", + "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", + "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", + "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", + "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", + "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", + "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", + "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", + "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", + "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", + "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", + "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", + "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", + "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", + "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", + "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", + "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", + "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", + "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", + "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", + "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", + "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", + "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", + "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", + "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", + "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", + "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", + "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", + "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", + "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", + "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", + "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", + "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", + "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", + "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", + "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", + "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", + "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", + "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", + "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", + "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", + "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", + "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", + "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", + "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", + "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" + ], + "markers": "python_version >= '3.9'", + "version": "==6.7.1" + }, "nodeenv": { "hashes": [ "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", @@ -3975,14 +4117,29 @@ "markers": "python_version >= '3.8'", "version": "==3.3.1" }, + "ocviapy": { + "hashes": [ + "sha256:77602d07d1f124e6f020e08ce21379e8a12cbefa98b168e697fdc202c770f9e2", + "sha256:e5558ec5c46d2793949c0b2fd1c99759d5b5ee564e2ac9ec56fd200c94dcaef4" + ], + "markers": "python_version >= '3.6'", + "version": "==1.6.0" + }, "packaging": { "hashes": [ - "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", - "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" + "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", + "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.2" + "version": "==26.0" + }, + "parsedatetime": { + "hashes": [ + "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", + "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" + ], + "version": "==2.6" }, "pillow": { "hashes": [ @@ -4108,12 +4265,12 @@ }, "pre-commit": { "hashes": [ - "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", - "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b" + "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", + "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==4.6.0" + "version": "==4.5.1" }, "prometheus-client": { "hashes": [ @@ -4132,6 +4289,134 @@ "markers": "python_version >= '3.8'", "version": "==3.0.52" }, + "propcache": { + "hashes": [ + "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", + "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", + "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", + "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", + "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", + "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", + "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", + "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", + "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", + "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", + "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", + "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", + "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", + "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", + "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", + "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", + "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", + "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", + "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", + "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", + "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", + "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", + "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", + "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", + "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", + "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", + "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", + "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", + "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", + "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", + "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", + "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", + "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", + "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", + "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", + "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", + "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", + "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", + "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", + "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", + "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", + "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", + "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", + "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", + "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", + "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", + "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", + "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", + "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", + "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", + "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", + "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", + "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", + "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", + "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", + "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", + "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", + "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", + "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", + "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", + "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", + "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", + "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", + "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", + "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", + "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", + "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", + "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", + "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", + "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", + "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", + "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", + "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", + "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", + "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", + "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", + "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", + "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", + "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", + "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", + "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", + "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", + "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", + "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", + "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", + "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", + "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", + "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", + "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", + "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", + "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", + "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", + "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", + "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", + "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", + "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", + "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", + "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", + "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", + "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", + "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", + "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", + "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", + "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", + "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", + "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", + "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", + "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", + "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", + "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", + "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", + "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", + "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", + "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", + "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", + "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", + "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", + "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", + "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", + "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", + "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", + "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" + ], + "markers": "python_version >= '3.9'", + "version": "==0.4.1" + }, "proto-plus": { "hashes": [ "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", @@ -4185,7 +4470,7 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "python_version >= '3.10'", + "markers": "implementation_name != 'PyPy'", "version": "==3.0" }, "pydocstyle": { @@ -4227,9 +4512,25 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, + "python-discovery": { + "hashes": [ + "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", + "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.2.2" + }, + "python-dotenv": { + "hashes": [ + "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", + "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" + ], + "markers": "python_version >= '3.10'", + "version": "==1.2.2" + }, "pytz": { "hashes": [ "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", @@ -4343,6 +4644,14 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, + "requests-toolbelt": { + "hashes": [ + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, "responses": { "hashes": [ "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", @@ -4354,18 +4663,26 @@ }, "s3transfer": { "hashes": [ - "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", - "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" + "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", + "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" ], "markers": "python_version >= '3.9'", - "version": "==0.16.1" + "version": "==0.16.0" + }, + "sh": { + "hashes": [ + "sha256:653227a7c41a284ec5302173fbc044ee817c7bad5e6e4d8d55741b9aeb9eb65b", + "sha256:e0b15b4ae8ffcd399bc8ffddcbd770a43c7a70a24b16773fbb34c001ad5d52af" + ], + "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", + "version": "==2.2.2" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.17.0" }, "snakeviz": { @@ -4382,7 +4699,7 @@ "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895" ], - "markers": "python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.1" }, "sqlparse": { @@ -4394,6 +4711,14 @@ "markers": "python_version >= '3.8'", "version": "==0.5.5" }, + "tabulate": { + "hashes": [ + "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", + "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3" + ], + "markers": "python_version >= '3.10'", + "version": "==0.10.0" + }, "tblib": { "hashes": [ "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", @@ -4421,12 +4746,20 @@ }, "tox": { "hashes": [ - "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229", - "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1" + "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", + "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.11.4" + "markers": "python_version >= '3.9'", + "version": "==4.26.0" + }, + "truststore": { + "hashes": [ + "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", + "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981" + ], + "markers": "python_version >= '3.10'", + "version": "==0.10.4" }, "typing-extensions": { "hashes": [ @@ -4438,11 +4771,11 @@ }, "tzdata": { "hashes": [ - "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", - "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" + "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", + "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" ], "markers": "python_version >= '2'", - "version": "==2026.2" + "version": "==2026.1" }, "tzlocal": { "hashes": [ @@ -4470,11 +4803,19 @@ }, "virtualenv": { "hashes": [ - "sha256:a00332e0b089ba64edefbf94ef34e6db33f45fe5184df0652cf0b754dcd36190", - "sha256:c551ea1072d7717a273dd9a4a0adad8f45571af134b0f63a12430e85f6ac1c08" + "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", + "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2" ], "markers": "python_version >= '3.8'", - "version": "==20.39.1" + "version": "==21.2.1" + }, + "wait-for": { + "hashes": [ + "sha256:1129f3350e29b0600889e24328d685a6bff048c8f4cabce28ef7632ed40c5d91", + "sha256:5642975f1fc5850acb55684b2d7842bd820fb068e725cd4ffff4bf3eba8e2788" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" }, "watchdog": { "hashes": [ @@ -4528,6 +4869,140 @@ ], "markers": "python_version >= '3.9'", "version": "==1.9.0" + }, + "yarl": { + "hashes": [ + "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", + "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", + "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", + "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", + "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", + "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", + "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", + "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", + "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", + "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", + "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", + "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", + "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", + "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", + "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", + "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", + "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", + "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", + "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", + "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", + "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", + "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", + "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", + "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", + "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", + "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", + "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", + "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", + "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", + "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", + "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", + "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", + "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", + "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", + "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", + "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", + "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", + "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", + "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", + "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", + "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", + "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", + "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", + "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", + "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", + "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", + "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", + "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", + "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", + "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", + "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", + "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", + "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", + "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", + "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", + "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", + "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", + "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", + "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", + "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", + "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", + "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", + "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", + "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", + "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", + "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", + "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", + "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", + "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", + "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", + "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", + "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", + "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", + "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", + "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", + "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", + "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", + "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", + "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", + "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", + "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", + "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", + "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", + "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", + "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", + "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", + "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", + "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", + "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", + "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", + "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", + "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", + "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", + "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", + "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", + "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", + "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", + "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", + "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", + "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", + "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", + "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", + "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", + "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", + "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", + "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", + "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", + "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", + "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", + "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", + "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", + "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", + "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", + "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", + "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", + "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", + "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", + "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", + "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", + "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", + "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", + "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", + "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", + "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", + "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", + "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", + "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", + "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d" + ], + "markers": "python_version >= '3.10'", + "version": "==1.23.0" } } -} +} \ No newline at end of file From 81fd01f3e71606074ff72baeb3e5c47a195e0809 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 15:54:13 +0300 Subject: [PATCH 080/106] revert Pipfile.lock --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index c4ca341d87..99b8042ddb 100644 --- a/Pipfile +++ b/Pipfile @@ -79,4 +79,4 @@ polyfactory = "*" koku-nise = "*" [requires] -python_version = "3.11" \ No newline at end of file +python_version = "3.11" From 637a69682a92d5fb59fa97186a559777878e4d11 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 15:54:48 +0300 Subject: [PATCH 081/106] revert Pipfile.lock --- Pipfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile.lock b/Pipfile.lock index 13850feb96..21e9ababee 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -5005,4 +5005,4 @@ "version": "==1.23.0" } } -} \ No newline at end of file +} From fe2a812752151fd697d2ebd826ab64c891217b10 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 16:29:06 +0300 Subject: [PATCH 082/106] installing Babel --- Pipfile | 1 + Pipfile.lock | 1705 ++++++++++++++++++-------------------------------- 2 files changed, 624 insertions(+), 1082 deletions(-) diff --git a/Pipfile b/Pipfile index 99b8042ddb..ed01b67fa1 100644 --- a/Pipfile +++ b/Pipfile @@ -53,6 +53,7 @@ pandas = "<3.0" scipy = ">=1.16" boto3 = "*" sqlalchemy = ">=2.0.0" +Babel = "*" [dev-packages] argh = ">=0.26.2" diff --git a/Pipfile.lock b/Pipfile.lock index 21e9ababee..93492cf498 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8355828638d3b0648e8f215a63312b8cc8edc9f80fd3dfb07e960556aa6086a0" + "sha256": "076741f1d4c52a3ea84a9a18600a5de48c6106574cacae7db51b8e50808f33a1" }, "pipfile-spec": 6, "requires": { @@ -56,6 +56,14 @@ "markers": "python_version >= '3.9'", "version": "==3.11.1" }, + "async-timeout": { + "hashes": [ + "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", + "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" + ], + "markers": "python_version >= '3.8'", + "version": "==5.0.1" + }, "azure-common": { "hashes": [ "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", @@ -115,6 +123,15 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, + "babel": { + "hashes": [ + "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", + "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.18.0" + }, "beautifulsoup4": { "hashes": [ "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", @@ -134,29 +151,29 @@ }, "boto3": { "hashes": [ - "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", - "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" + "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", + "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "botocore": { "hashes": [ - "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", - "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" + "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", + "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" ], "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "cachetools": { "hashes": [ - "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", - "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" + "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", + "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.5" + "version": "==7.0.6" }, "celery": { "hashes": [ @@ -169,11 +186,11 @@ }, "certifi": { "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" + "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", + "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" ], "markers": "python_version >= '3.7'", - "version": "==2026.2.25" + "version": "==2026.4.22" }, "cffi": { "hashes": [ @@ -262,7 +279,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.9'", "version": "==2.0.0" }, "charset-normalizer": { @@ -473,11 +490,11 @@ }, "click": { "hashes": [ - "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", - "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" + "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", + "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" ], "markers": "python_version >= '3.10'", - "version": "==8.3.2" + "version": "==8.3.3" }, "click-didyoumean": { "hashes": [ @@ -547,58 +564,58 @@ }, "cryptography": { "hashes": [ - "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", - "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", - "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", - "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", - "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", - "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", - "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", - "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", - "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", - "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", - "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", - "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", - "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", - "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", - "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", - "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", - "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", - "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", - "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", - "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", - "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", - "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", - "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", - "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", - "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", - "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", - "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", - "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", - "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", - "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", - "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", - "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", - "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", - "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", - "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", - "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", - "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", - "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", - "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", - "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", - "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", - "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", - "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", - "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", - "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", - "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", - "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", - "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", - "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" + "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", + "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", + "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", + "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", + "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", + "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", + "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", + "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", + "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", + "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", + "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", + "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", + "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", + "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", + "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", + "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", + "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", + "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", + "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", + "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", + "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", + "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", + "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", + "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", + "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", + "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", + "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", + "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", + "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", + "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", + "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", + "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", + "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", + "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", + "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", + "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", + "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", + "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", + "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", + "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", + "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", + "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", + "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", + "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", + "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", + "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", + "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", + "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", + "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.7" + "version": "==47.0.0" }, "django": { "hashes": [ @@ -701,11 +718,11 @@ "grpc" ], "hashes": [ - "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", - "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" + "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", + "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" ], "markers": "python_version >= '3.9'", - "version": "==2.30.2" + "version": "==2.30.3" }, "google-api-python-client": { "hashes": [ @@ -718,12 +735,12 @@ }, "google-auth": { "hashes": [ - "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", - "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" + "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", + "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.1" + "version": "==2.49.2" }, "google-auth-httplib2": { "hashes": [ @@ -814,6 +831,71 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, + "greenlet": { + "hashes": [ + "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", + "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", + "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", + "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", + "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", + "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", + "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", + "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", + "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", + "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", + "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", + "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", + "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", + "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", + "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", + "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", + "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", + "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", + "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", + "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", + "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", + "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", + "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", + "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", + "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", + "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", + "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", + "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", + "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", + "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", + "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", + "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", + "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", + "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", + "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", + "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", + "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", + "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", + "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", + "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", + "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", + "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", + "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", + "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", + "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", + "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", + "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", + "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", + "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", + "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", + "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", + "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", + "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", + "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", + "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", + "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", + "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", + "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", + "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564" + ], + "markers": "python_version >= '3.10'", + "version": "==3.5.0" + }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -908,11 +990,11 @@ }, "idna": { "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", + "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" ], "markers": "python_version >= '3.8'", - "version": "==3.11" + "version": "==3.13" }, "importlib-metadata": { "hashes": [ @@ -1413,17 +1495,17 @@ "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.11.8" }, "packaging": { "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.0" + "version": "==26.2" }, "pandas": { "hashes": [ @@ -1574,134 +1656,134 @@ }, "psycopg2-binary": { "hashes": [ - "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", - "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", - "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", - "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", - "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", - "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", - "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", - "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", - "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", - "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", - "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", - "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", - "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", - "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", - "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", - "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", - "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", - "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", - "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", - "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", - "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", - "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", - "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", - "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", - "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", - "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", - "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", - "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", - "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", - "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", - "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", - "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", - "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", - "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", - "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", - "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", - "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", - "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", - "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", - "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", - "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", - "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", - "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", - "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", - "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", - "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", - "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", - "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", - "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", - "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", - "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", - "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", - "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", - "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", - "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", - "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", - "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", - "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", - "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", - "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", - "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", - "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", - "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", - "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", - "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", - "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", - "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" + "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", + "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964", + "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", + "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", + "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115", + "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", + "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", + "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", + "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", + "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", + "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", + "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", + "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", + "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", + "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", + "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", + "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", + "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", + "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", + "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", + "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf", + "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", + "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", + "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", + "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", + "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", + "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", + "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", + "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94", + "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", + "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", + "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", + "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", + "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", + "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", + "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", + "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", + "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", + "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", + "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", + "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", + "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", + "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", + "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", + "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915", + "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", + "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", + "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe", + "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", + "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", + "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", + "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86", + "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", + "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", + "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", + "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", + "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03", + "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", + "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", + "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", + "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", + "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab", + "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", + "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10", + "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", + "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2", + "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.9.11" + "version": "==2.9.12" }, "pyarrow": { "hashes": [ - "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", - "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", - "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", - "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", - "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", - "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", - "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", - "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", - "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", - "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", - "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", - "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", - "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", - "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", - "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", - "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", - "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", - "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", - "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", - "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", - "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", - "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", - "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", - "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", - "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", - "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", - "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", - "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", - "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", - "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", - "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", - "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", - "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", - "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", - "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", - "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", - "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", - "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", - "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", - "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", - "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", - "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", - "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", - "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", - "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", - "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", - "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", - "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", - "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", - "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd" + "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", + "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", + "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", + "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", + "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", + "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", + "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", + "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", + "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", + "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", + "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", + "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", + "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", + "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", + "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", + "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", + "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", + "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", + "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", + "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", + "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", + "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", + "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", + "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", + "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", + "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", + "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", + "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", + "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", + "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", + "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", + "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", + "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", + "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", + "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", + "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", + "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", + "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", + "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", + "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", + "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", + "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", + "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", + "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", + "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", + "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", + "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", + "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", + "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", + "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==23.0.1" + "version": "==24.0.0" }, "pyasn1": { "hashes": [ @@ -1724,144 +1806,143 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "implementation_name != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.0" }, "pydantic": { "hashes": [ - "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", - "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" + "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", + "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.12.5" + "version": "==2.13.3" }, "pydantic-core": { "hashes": [ - "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", - "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", - "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", - "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", - "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", - "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", - "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", - "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", - "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", - "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", - "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", - "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", - "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", - "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", - "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", - "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", - "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", - "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", - "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", - "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", - "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", - "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", - "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", - "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", - "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", - "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", - "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", - "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", - "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", - "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", - "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", - "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", - "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", - "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", - "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", - "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", - "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", - "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", - "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", - "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", - "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", - "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", - "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", - "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", - "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", - "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", - "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", - "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", - "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", - "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", - "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", - "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", - "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", - "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", - "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", - "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", - "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", - "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", - "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", - "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", - "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", - "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", - "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", - "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", - "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", - "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", - "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", - "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", - "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", - "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", - "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", - "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", - "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", - "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", - "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", - "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", - "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", - "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", - "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", - "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", - "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", - "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", - "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", - "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", - "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", - "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", - "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", - "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", - "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", - "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", - "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", - "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", - "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", - "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", - "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", - "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", - "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", - "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", - "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", - "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", - "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", - "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", - "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", - "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", - "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", - "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", - "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", - "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", - "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", - "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", - "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", - "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", - "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", - "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", - "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", - "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", - "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", - "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", - "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", - "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", - "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" + "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", + "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", + "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", + "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", + "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", + "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", + "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", + "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", + "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", + "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", + "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", + "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", + "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", + "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", + "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", + "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", + "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", + "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", + "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", + "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", + "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", + "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", + "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", + "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495", + "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", + "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0", + "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd", + "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", + "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", + "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", + "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", + "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", + "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", + "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", + "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", + "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", + "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", + "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", + "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", + "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", + "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720", + "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", + "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c", + "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", + "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", + "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", + "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", + "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", + "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", + "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873", + "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", + "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", + "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", + "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", + "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", + "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd", + "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", + "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", + "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", + "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d", + "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", + "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", + "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", + "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", + "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168", + "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", + "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13", + "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", + "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", + "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", + "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", + "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", + "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", + "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", + "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", + "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", + "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", + "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", + "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", + "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", + "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", + "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", + "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", + "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", + "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", + "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", + "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", + "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", + "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", + "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", + "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", + "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", + "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", + "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", + "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", + "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", + "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", + "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6", + "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79", + "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a", + "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", + "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", + "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", + "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", + "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", + "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", + "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", + "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", + "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", + "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", + "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", + "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", + "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", + "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", + "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb", + "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", + "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", + "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", + "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", + "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56" ], "markers": "python_version >= '3.9'", - "version": "==2.41.5" + "version": "==2.46.3" }, "pyjwt": { "extras": [ @@ -1888,7 +1969,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pytz": { @@ -1926,11 +2007,11 @@ }, "s3transfer": { "hashes": [ - "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", - "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" + "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", + "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" ], "markers": "python_version >= '3.9'", - "version": "==0.16.0" + "version": "==0.16.1" }, "scipy": { "hashes": [ @@ -2010,19 +2091,19 @@ }, "sentry-sdk": { "hashes": [ - "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", - "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585" + "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", + "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.57.0" + "version": "==2.58.0" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.17.0" }, "soupsieve": { @@ -2182,11 +2263,11 @@ }, "tzdata": { "hashes": [ - "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", - "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" + "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", + "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" ], "markers": "python_version >= '2'", - "version": "==2026.1" + "version": "==2026.2" }, "tzlocal": { "hashes": [ @@ -2311,11 +2392,11 @@ }, "zipp": { "hashes": [ - "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", + "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110" ], "markers": "python_version >= '3.9'", - "version": "==3.23.0" + "version": "==3.23.1" }, "zstandard": { "hashes": [ @@ -2432,30 +2513,6 @@ "markers": "python_version >= '3.6'", "version": "==5.3.1" }, - "anyio": { - "hashes": [ - "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", - "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" - ], - "markers": "python_version >= '3.10'", - "version": "==4.13.0" - }, - "anytree": { - "hashes": [ - "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", - "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.9.2'", - "version": "==2.13.0" - }, - "app-common-python": { - "hashes": [ - "sha256:74125371e2865bcba7a7c34f0700d4b8b97a0c0a9ff67bbb06fdb20a3da4b75c", - "sha256:861bd93482edabd00a85eb6512e0d7df08597be6f76b864be8e53d6294f81389" - ], - "index": "pypi", - "version": "==0.2.9" - }, "argh": { "hashes": [ "sha256:2edac856ff50126f6e47d884751328c9f466bacbbb6cbfdac322053d94705494", @@ -2499,14 +2556,6 @@ "markers": "python_version >= '3.9'", "version": "==12.28.0" }, - "backoff": { - "hashes": [ - "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", - "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" - ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==2.2.1" - }, "billiard": { "hashes": [ "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", @@ -2517,37 +2566,29 @@ }, "boto3": { "hashes": [ - "sha256:15cc1404b3ccbcfe17bd5834d467b1b28d53d9aca44e3798dc44876ac57362e6", - "sha256:b5b86a826f8f12c7d38679f35bd0135807a6867a21eb8be6dea7c27aeb14ec14" + "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", + "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.42.87" + "version": "==1.42.97" }, "botocore": { "hashes": [ - "sha256:1c6cc9555c1feec50b290a42de70ba6f04826c009562c2c12bb9990d0258482d", - "sha256:32832df27c039cc1a518289afe0f6006296d3df26603b5f66e911457e67f0146" + "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", + "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc" ], "markers": "python_version >= '3.9'", - "version": "==1.42.87" - }, - "cached-property": { - "hashes": [ - "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", - "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb" - ], - "markers": "python_version >= '3.8'", - "version": "==2.0.1" + "version": "==1.42.97" }, "cachetools": { "hashes": [ - "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", - "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114" + "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", + "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==7.0.5" + "version": "==7.0.6" }, "celery": { "hashes": [ @@ -2560,11 +2601,11 @@ }, "certifi": { "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" + "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", + "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" ], "markers": "python_version >= '3.7'", - "version": "==2026.2.25" + "version": "==2026.4.22" }, "cffi": { "hashes": [ @@ -2653,7 +2694,7 @@ "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" ], - "markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.9'", "version": "==2.0.0" }, "cfgv": { @@ -2666,41 +2707,45 @@ }, "chardet": { "hashes": [ - "sha256:0487a6a6846740f39f9fcd71e3acff2982bae8bca3507ee986d5155cd458e044", - "sha256:04b9be0d786b9a3bbd7860a82d27e843f22211be51b9b84d85fe8d9864e2f535", - "sha256:0848015eb1471e1499963dff2776557af05f99c38ba2a14f34ea078f8668c6a9", - "sha256:0de8d636391f9050e4e048ca8a9f98b25e67eff389705f8c6ff1ab9593f7339b", - "sha256:19bcd1de4a0c1a5802f9d2d370b6696668bddc166a3c89c113cf109313b3d99f", - "sha256:277ce1174ea054415a3c2ad5f51aa089a96dda16999de56e4ac1bc366d0d535e", - "sha256:2da446b920064ca9574504c29a07ef5eae91a1948a302a25043a16fb79ec2397", - "sha256:35ade2a6f93e5d2bdff541b584126cfe066eac5c9457572ff97cabc8068bece1", - "sha256:3886f8f9bb3500bd8c421b2de9b4878a0c183f80bc289338cdda869dfd4397fb", - "sha256:3d66d2949754ad924865a47e81857a0792dc8edc651094285116b6df2e218445", - "sha256:4ececf9631f7932a2cef728746303d71ae8204923190253d5382ee37739dd46e", - "sha256:5d86402a506631af2fb36e3d1c72021477b228fb0dcdb44400b9b681f14b14c0", - "sha256:5e686e5a0d8155cfbf5b1a579f5790bb01bf1a0a52e7f98b38801c09a0c63fcd", - "sha256:62b25b3ea5ef8e1672726e4c1601f6636ce3b76b9de92af669c8000711d7fe13", - "sha256:6b5c03330b9108124f8174a596284b737e95f1cf6a99953c37cea7e2583212e7", - "sha256:7a1c2be068ab91a472fd74617d9371605897c3061ca4f2e5df63d9f3d9c11f8e", - "sha256:80de820fa1df95a2e7c9898867f7d6ff3dc3d52a109938b215b37ab54474d307", - "sha256:8befb12c263cb14e26f065a3f99d76ef2610b0266cb70d827bb61528e9e60f28", - "sha256:9381b3d9075c8a2e622b4d46db5e4229c94aebc71d4c8e620d9cf2cea2930824", - "sha256:a9322fd3ffd359b49b2d608725a15975ebc0d66f2dcedefa7ddb5847e54a6f9c", - "sha256:a98c361b73c6ce4a44beaecf5cfb389ec69b566dd7f3f4ea5d1bde6487e7054d", - "sha256:b726b0b2684d29cd08f602bb4266334386c58741ff34c9e2f6cdf97ad604e235", - "sha256:be39708b300a80a9f78ef8f81018e2e9c6274a71c0823a4d6e493c72f7b3d2a2", - "sha256:bee1d665ca5810d8e3cf11122619e85c23b209075cbddb91f213675248f0e522", - "sha256:c03925738670199d253b8c79828d8a68d404f629a2dbf1b4b5aabd8c8b0249ab", - "sha256:c820c95d8b4de8aea99b54083d38f10f763686646962b5627e8e2b2db113a37b", - "sha256:c98e1044785ab71f0fee70f64b8d56f69df9de1b593793022e001ba2f6b76dd0", - "sha256:caf0715b8a5e20fc3faf21a24abd3ae513f8f58206dd32d1b87eca6351e105ed", - "sha256:cda41132a45dfbf6984dade1f531a4098c813caf266c66cc446d90bb9369cabd", - "sha256:d8aa2bae7d0523963395f802ae2212e8b2248d4503a14a691de86edf716b22d3", - "sha256:e53cc280a1ab616f191ac7ebdd1f38f2aa78b1411dd2677dc556c6f0fa085913", - "sha256:fcaed03cefa53f62346091ef92da7a6f44bae6830a6f4c6b097a70cdc31b1199" + "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", + "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", + "sha256:23163921dccf3103ce59540b0443c106d2c0a0ff2e0503e05196f5e6fdea453f", + "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", + "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", + "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", + "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", + "sha256:3990fffcc6a6045f2234ab72752ad037e3b2d48c72037f244d42738db397eb75", + "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", + "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", + "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", + "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", + "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", + "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", + "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", + "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", + "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", + "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", + "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", + "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", + "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", + "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", + "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", + "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", + "sha256:bba8bea1b28d927b3e99e47deafe53658d34497c0a891d95ff1ba8ff6663f01c", + "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", + "sha256:c0c79b13c9908ac7dfe0a74116ebc9a0f28b2319d23c32f3dfcdfbe1279c7eaf", + "sha256:c7116b0452994734ccff35e154b44240090eb0f4f74b9106292668133557c175", + "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", + "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", + "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", + "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", + "sha256:cfb54563fe5f130da17c44c6a4e2e8052ba628e5ab4eab7ef8190f736f0f8f72", + "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", + "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", + "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131" ], "markers": "python_version >= '3.10'", - "version": "==7.4.1" + "version": "==7.4.3" }, "charset-normalizer": { "hashes": [ @@ -2839,11 +2884,11 @@ }, "click": { "hashes": [ - "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", - "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d" + "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", + "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" ], "markers": "python_version >= '3.10'", - "version": "==8.3.2" + "version": "==8.3.3" }, "click-didyoumean": { "hashes": [ @@ -3069,67 +3114,67 @@ }, "crc-bonfire": { "hashes": [ - "sha256:7fa3b2e739a92c2ffdc3f43ba590201dff64d6993603fc43deb10f3f0c2d903a", - "sha256:d9a32ad91714dd0c00c56a405add63df4896dfeb58994ce5f9688dde7cd4298a" + "sha256:54c60db35846eeffc515013b7966d6313e1b12d4b1df433271fd29cfd581207a", + "sha256:cceb257faf44b7116976d822518d12ab3ec329951078199aa577dff189a61e77" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==6.9.1" + "markers": "python_version >= '3.10'", + "version": "==6.12.0" }, "cryptography": { "hashes": [ - "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", - "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", - "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", - "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", - "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", - "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", - "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", - "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", - "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", - "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", - "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", - "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", - "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", - "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", - "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", - "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", - "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", - "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", - "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", - "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", - "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", - "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", - "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", - "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", - "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", - "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", - "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", - "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", - "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", - "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", - "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", - "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", - "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", - "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", - "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", - "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", - "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", - "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", - "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", - "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", - "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", - "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", - "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", - "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", - "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", - "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", - "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", - "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", - "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce" + "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", + "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", + "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", + "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", + "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", + "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", + "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", + "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", + "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", + "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", + "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", + "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", + "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", + "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", + "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", + "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", + "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", + "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", + "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", + "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", + "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", + "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", + "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", + "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", + "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", + "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", + "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", + "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", + "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", + "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", + "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", + "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", + "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", + "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", + "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", + "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", + "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", + "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", + "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", + "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", + "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", + "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", + "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", + "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", + "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", + "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", + "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", + "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", + "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736" ], "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.7" + "version": "==47.0.0" }, "cycler": { "hashes": [ @@ -3208,20 +3253,20 @@ }, "faker": { "hashes": [ - "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", - "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019" + "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", + "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==40.13.0" + "version": "==40.15.0" }, "filelock": { "hashes": [ - "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", - "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70" + "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", + "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258" ], "markers": "python_version >= '3.10'", - "version": "==3.25.2" + "version": "==3.29.0" }, "flake8": { "hashes": [ @@ -3302,20 +3347,20 @@ "grpc" ], "hashes": [ - "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", - "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594" + "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", + "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b" ], "markers": "python_version >= '3.9'", - "version": "==2.30.2" + "version": "==2.30.3" }, "google-auth": { "hashes": [ - "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", - "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7" + "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", + "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.49.1" + "version": "==2.49.2" }, "google-cloud-bigquery": { "hashes": [ @@ -3398,25 +3443,6 @@ "markers": "python_version >= '3.9'", "version": "==1.74.0" }, - "gql": { - "extras": [ - "requests" - ], - "hashes": [ - "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", - "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479" - ], - "markers": "python_full_version >= '3.8.1'", - "version": "==4.0.0" - }, - "graphql-core": { - "hashes": [ - "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", - "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==3.2.8" - }, "grpcio": { "hashes": [ "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", @@ -3502,19 +3528,19 @@ }, "identify": { "hashes": [ - "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", - "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737" + "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", + "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842" ], "markers": "python_version >= '3.10'", - "version": "==2.6.18" + "version": "==2.6.19" }, "idna": { "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", + "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" ], "markers": "python_version >= '3.8'", - "version": "==3.11" + "version": "==3.13" }, "isodate": { "hashes": [ @@ -3540,14 +3566,6 @@ "markers": "python_version >= '3.9'", "version": "==1.1.0" }, - "junitparser": { - "hashes": [ - "sha256:9e279f2214dc74b6a86b22db757abda2e8e66e819fe882dad5b392d57024cd26", - "sha256:f15e292877258d7c5755d672ce86f82c3622c7ea4c2f44f55de44ed7518484d3" - ], - "markers": "python_version >= '3.10'", - "version": "==5.0.0" - }, "kiwisolver": { "hashes": [ "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", @@ -3673,12 +3691,12 @@ }, "koku-nise": { "hashes": [ - "sha256:5ac277879ad686c3f0b9fbd8084e48868a949948f293783d61ad8c219f9a650c", - "sha256:b189f3aad01b698228459a757bfd14af23ec177f582bcf528b897fe02bbae893" + "sha256:2d8c03b9a27f8f9f5caf87ca68b4a6ba23b4d7e6291f69ad0b01655d96e64c51", + "sha256:42b6db673060c9ed23c4bc2badb57fb840a8dccf09d2ec933194aa37e926439d" ], "index": "pypi", "markers": "python_version >= '3.11'", - "version": "==5.4.0" + "version": "==5.4.1" }, "kombu": { "hashes": [ @@ -3794,65 +3812,65 @@ }, "matplotlib": { "hashes": [ - "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", - "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", - "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", - "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", - "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", - "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", - "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", - "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", - "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", - "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", - "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", - "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", - "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", - "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", - "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", - "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", - "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", - "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", - "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", - "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", - "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", - "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", - "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", - "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", - "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", - "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", - "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", - "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", - "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", - "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", - "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", - "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", - "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", - "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", - "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", - "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", - "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", - "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", - "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", - "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", - "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", - "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", - "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", - "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", - "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", - "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", - "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", - "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", - "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", - "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", - "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", - "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", - "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", - "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", - "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7" + "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", + "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", + "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", + "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", + "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", + "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", + "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", + "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", + "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", + "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", + "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", + "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", + "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", + "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", + "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", + "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", + "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", + "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", + "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", + "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", + "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", + "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", + "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", + "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", + "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", + "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", + "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", + "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", + "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", + "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", + "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", + "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", + "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", + "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", + "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", + "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", + "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", + "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", + "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", + "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", + "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", + "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", + "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", + "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", + "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", + "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", + "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", + "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", + "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", + "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", + "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", + "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", + "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", + "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", + "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==3.10.8" + "version": "==3.10.9" }, "mccabe": { "hashes": [ @@ -3871,158 +3889,6 @@ "markers": "python_version >= '3.10'", "version": "==1.23.4" }, - "multidict": { - "hashes": [ - "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", - "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", - "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", - "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", - "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", - "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", - "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", - "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", - "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", - "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", - "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", - "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", - "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", - "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", - "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", - "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", - "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", - "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", - "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", - "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", - "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", - "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", - "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", - "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", - "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", - "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", - "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", - "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", - "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", - "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", - "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", - "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", - "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", - "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", - "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", - "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", - "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", - "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", - "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", - "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", - "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", - "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", - "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", - "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", - "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", - "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", - "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", - "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", - "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", - "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", - "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", - "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", - "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", - "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", - "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", - "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", - "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", - "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", - "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", - "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", - "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", - "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", - "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", - "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", - "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", - "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", - "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", - "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", - "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", - "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", - "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", - "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", - "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", - "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", - "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", - "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", - "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", - "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", - "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", - "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", - "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", - "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", - "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", - "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", - "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", - "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", - "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", - "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", - "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", - "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", - "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", - "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", - "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", - "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", - "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", - "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", - "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", - "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", - "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", - "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", - "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", - "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", - "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", - "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", - "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", - "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", - "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", - "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", - "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", - "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", - "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", - "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", - "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", - "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", - "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", - "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", - "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", - "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", - "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", - "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", - "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", - "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", - "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", - "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", - "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", - "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", - "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", - "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", - "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", - "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", - "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", - "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", - "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", - "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", - "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", - "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", - "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", - "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", - "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", - "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", - "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", - "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", - "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", - "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", - "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", - "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" - ], - "markers": "python_version >= '3.9'", - "version": "==6.7.1" - }, "nodeenv": { "hashes": [ "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", @@ -4117,29 +3983,14 @@ "markers": "python_version >= '3.8'", "version": "==3.3.1" }, - "ocviapy": { - "hashes": [ - "sha256:77602d07d1f124e6f020e08ce21379e8a12cbefa98b168e697fdc202c770f9e2", - "sha256:e5558ec5c46d2793949c0b2fd1c99759d5b5ee564e2ac9ec56fd200c94dcaef4" - ], - "markers": "python_version >= '3.6'", - "version": "==1.6.0" - }, "packaging": { "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==26.0" - }, - "parsedatetime": { - "hashes": [ - "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", - "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" - ], - "version": "==2.6" + "version": "==26.2" }, "pillow": { "hashes": [ @@ -4265,12 +4116,12 @@ }, "pre-commit": { "hashes": [ - "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", - "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61" + "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", + "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==4.5.1" + "version": "==4.6.0" }, "prometheus-client": { "hashes": [ @@ -4289,134 +4140,6 @@ "markers": "python_version >= '3.8'", "version": "==3.0.52" }, - "propcache": { - "hashes": [ - "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", - "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", - "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", - "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", - "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", - "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", - "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", - "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", - "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", - "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", - "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", - "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", - "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", - "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", - "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", - "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", - "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", - "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", - "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", - "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", - "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", - "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", - "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", - "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", - "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", - "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", - "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", - "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", - "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", - "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", - "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", - "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", - "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", - "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", - "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", - "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", - "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", - "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", - "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", - "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", - "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", - "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", - "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", - "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", - "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", - "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", - "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", - "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", - "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", - "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", - "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", - "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", - "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", - "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", - "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", - "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", - "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", - "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", - "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", - "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", - "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", - "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", - "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", - "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", - "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", - "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", - "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", - "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", - "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", - "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", - "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", - "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", - "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", - "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", - "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", - "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", - "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", - "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", - "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", - "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", - "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", - "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", - "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", - "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", - "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", - "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", - "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", - "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", - "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", - "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", - "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", - "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", - "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", - "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", - "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", - "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", - "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", - "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", - "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", - "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", - "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", - "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", - "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", - "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", - "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", - "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", - "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", - "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", - "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", - "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", - "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", - "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", - "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", - "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", - "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", - "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", - "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", - "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", - "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", - "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", - "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", - "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.1" - }, "proto-plus": { "hashes": [ "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", @@ -4470,7 +4193,7 @@ "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" ], - "markers": "implementation_name != 'PyPy'", + "markers": "python_version >= '3.10'", "version": "==3.0" }, "pydocstyle": { @@ -4512,7 +4235,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-discovery": { @@ -4523,14 +4246,6 @@ "markers": "python_version >= '3.8'", "version": "==1.2.2" }, - "python-dotenv": { - "hashes": [ - "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", - "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" - ], - "markers": "python_version >= '3.10'", - "version": "==1.2.2" - }, "pytz": { "hashes": [ "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", @@ -4644,14 +4359,6 @@ "markers": "python_version >= '3.4'", "version": "==2.0.0" }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, "responses": { "hashes": [ "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", @@ -4663,26 +4370,18 @@ }, "s3transfer": { "hashes": [ - "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", - "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920" + "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", + "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524" ], "markers": "python_version >= '3.9'", - "version": "==0.16.0" - }, - "sh": { - "hashes": [ - "sha256:653227a7c41a284ec5302173fbc044ee817c7bad5e6e4d8d55741b9aeb9eb65b", - "sha256:e0b15b4ae8ffcd399bc8ffddcbd770a43c7a70a24b16773fbb34c001ad5d52af" - ], - "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", - "version": "==2.2.2" + "version": "==0.16.1" }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.17.0" }, "snakeviz": { @@ -4699,7 +4398,7 @@ "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895" ], - "markers": "python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version not in '3.0, 3.1, 3.2'", "version": "==3.0.1" }, "sqlparse": { @@ -4711,14 +4410,6 @@ "markers": "python_version >= '3.8'", "version": "==0.5.5" }, - "tabulate": { - "hashes": [ - "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", - "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3" - ], - "markers": "python_version >= '3.10'", - "version": "==0.10.0" - }, "tblib": { "hashes": [ "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", @@ -4753,14 +4444,6 @@ "markers": "python_version >= '3.9'", "version": "==4.26.0" }, - "truststore": { - "hashes": [ - "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", - "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981" - ], - "markers": "python_version >= '3.10'", - "version": "==0.10.4" - }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", @@ -4771,11 +4454,11 @@ }, "tzdata": { "hashes": [ - "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", - "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98" + "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", + "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" ], "markers": "python_version >= '2'", - "version": "==2026.1" + "version": "==2026.2" }, "tzlocal": { "hashes": [ @@ -4803,19 +4486,11 @@ }, "virtualenv": { "hashes": [ - "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", - "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2" + "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", + "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e" ], "markers": "python_version >= '3.8'", - "version": "==21.2.1" - }, - "wait-for": { - "hashes": [ - "sha256:1129f3350e29b0600889e24328d685a6bff048c8f4cabce28ef7632ed40c5d91", - "sha256:5642975f1fc5850acb55684b2d7842bd820fb068e725cd4ffff4bf3eba8e2788" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" + "version": "==21.3.0" }, "watchdog": { "hashes": [ @@ -4869,140 +4544,6 @@ ], "markers": "python_version >= '3.9'", "version": "==1.9.0" - }, - "yarl": { - "hashes": [ - "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", - "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", - "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", - "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", - "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", - "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", - "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", - "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", - "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", - "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", - "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", - "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", - "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", - "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", - "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", - "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", - "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", - "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", - "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", - "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", - "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", - "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", - "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", - "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", - "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", - "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", - "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", - "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", - "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", - "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", - "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", - "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", - "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", - "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", - "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", - "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", - "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", - "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", - "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", - "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", - "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", - "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", - "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", - "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", - "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", - "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", - "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", - "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", - "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", - "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", - "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", - "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", - "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", - "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", - "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", - "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", - "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", - "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", - "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", - "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", - "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", - "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", - "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", - "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", - "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", - "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", - "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", - "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", - "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", - "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", - "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", - "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", - "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", - "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", - "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", - "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", - "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", - "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", - "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", - "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", - "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", - "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", - "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", - "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", - "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", - "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", - "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", - "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", - "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", - "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", - "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", - "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", - "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", - "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", - "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", - "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", - "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", - "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", - "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", - "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", - "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", - "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", - "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", - "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", - "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", - "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", - "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", - "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", - "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", - "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", - "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", - "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", - "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", - "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", - "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", - "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", - "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", - "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", - "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", - "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", - "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", - "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", - "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", - "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", - "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", - "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", - "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", - "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d" - ], - "markers": "python_version >= '3.10'", - "version": "==1.23.0" } } } From ab94e0572f3108ab39d5c437515f818cec6ea50b Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 17:15:50 +0300 Subject: [PATCH 083/106] Group exchange rates by base_currency in settings list endpoint The admin manages rates FROM a source currency, so grouping by base_currency better matches the mental model for rate management. Made-with: Cursor --- .../constant-currency/api-and-frontend.md | 22 +++++----------- .../static_exchange_rate_serializer.py | 4 +-- koku/cost_models/static_exchange_rate_view.py | 2 +- .../test/test_static_exchange_rate_view.py | 26 +++++++++---------- 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 283424fa28..52b18afa51 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -38,7 +38,7 @@ and `EnabledCurrencyView`. **File**: `koku/cost_models/static_exchange_rate_view.py` The `StaticExchangeRateViewSet` handles CRUD for exchange rates. The `list` -action returns exchange rates grouped by target currency with enabled status +action returns exchange rates grouped by base currency with enabled status (via `CurrencyExchangeRateSerializer`). All other actions use the flat `StaticExchangeRateSerializer`. @@ -84,18 +84,18 @@ together with the `StaticExchangeRate` write. If any side effect fails, the ### Example: GET List Response -The list endpoint returns exchange rates grouped by target currency. Each +The list endpoint returns exchange rates grouped by base currency. Each currency entry includes its enabled status and a nested list of exchange rates. Only currencies with at least one `StaticExchangeRate` record appear. ```json { - "meta": { "count": 2 }, + "meta": { "count": 1 }, "data": [ { - "code": "EUR", - "name": "Euro", - "symbol": "€", + "code": "USD", + "name": "US Dollar", + "symbol": "$", "enabled": true, "exchange_rates": [ { @@ -108,15 +108,7 @@ Only currencies with at least one `StaticExchangeRate` record appear. "end_date": "2026-03-31", "created_timestamp": "2026-01-15T10:30:00Z", "updated_timestamp": "2026-01-15T10:30:00Z" - } - ] - }, - { - "code": "GBP", - "name": "British Pound", - "symbol": "£", - "enabled": false, - "exchange_rates": [ + }, { "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "USD-GBP", diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 32411d076a..5c01bc756d 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -143,12 +143,12 @@ class CurrencyExchangeRateSerializer(serializers.Serializer): @classmethod def build_grouped_response(cls, queryset): - """Group exchange rates by target_currency and attach currency metadata + enabled flag.""" + """Group exchange rates by base_currency and attach currency metadata + enabled flag.""" enabled_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) grouped = {} for rate in queryset: - code = rate.target_currency + code = rate.base_currency if code not in grouped: info = get_currency_info(code) grouped[code] = { diff --git a/koku/cost_models/static_exchange_rate_view.py b/koku/cost_models/static_exchange_rate_view.py index d4cfa302bb..e56317df48 100644 --- a/koku/cost_models/static_exchange_rate_view.py +++ b/koku/cost_models/static_exchange_rate_view.py @@ -49,7 +49,7 @@ class StaticExchangeRateViewSet(viewsets.ModelViewSet): filterset_class = StaticExchangeRateFilter def list(self, request, *args, **kwargs): - """Return exchange rates grouped by target currency with enabled status.""" + """Return exchange rates grouped by base currency with enabled status.""" queryset = self.filter_queryset(self.get_queryset()) result = CurrencyExchangeRateSerializer.build_grouped_response(queryset) paginator = ListPaginator(result, request) diff --git a/koku/cost_models/test/test_static_exchange_rate_view.py b/koku/cost_models/test/test_static_exchange_rate_view.py index 499b5a7416..6adcf046b5 100644 --- a/koku/cost_models/test/test_static_exchange_rate_view.py +++ b/koku/cost_models/test/test_static_exchange_rate_view.py @@ -43,10 +43,10 @@ def test_create_static_rate(self, mock_invalidate): @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") def test_list_returns_grouped_by_currency(self, mock_invalidate): - """Test that GET list returns exchange rates grouped by target currency with enabled flag.""" + """Test that GET list returns exchange rates grouped by base currency with enabled flag.""" with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="EUR") + EnabledCurrency.objects.create(currency_code="USD") self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) response = self.client.get(self.list_url, **self.headers) @@ -55,14 +55,14 @@ def test_list_returns_grouped_by_currency(self, mock_invalidate): data = response.data["data"] self.assertEqual(len(data), 1) - eur_entry = data[0] - self.assertEqual(eur_entry["code"], "EUR") - self.assertEqual(eur_entry["enabled"], True) - self.assertIn("name", eur_entry) - self.assertIn("symbol", eur_entry) - self.assertEqual(len(eur_entry["exchange_rates"]), 1) + usd_entry = data[0] + self.assertEqual(usd_entry["code"], "USD") + self.assertEqual(usd_entry["enabled"], True) + self.assertIn("name", usd_entry) + self.assertIn("symbol", usd_entry) + self.assertEqual(len(usd_entry["exchange_rates"]), 1) - rate = eur_entry["exchange_rates"][0] + rate = usd_entry["exchange_rates"][0] self.assertEqual(rate["base_currency"], "USD") self.assertEqual(rate["target_currency"], "EUR") self.assertIn("uuid", rate) @@ -71,15 +71,15 @@ def test_list_returns_grouped_by_currency(self, mock_invalidate): def test_list_disabled_currency_shows_enabled_false(self, mock_invalidate): """Test that a currency without EnabledCurrency row shows enabled=False.""" with tenant_context(self.tenant): - EnabledCurrency.objects.filter(currency_code="EUR").delete() + EnabledCurrency.objects.filter(currency_code="USD").delete() self.client.post(self.list_url, data=self.valid_data, format="json", **self.headers) response = self.client.get(self.list_url, **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - eur_entry = response.data["data"][0] - self.assertEqual(eur_entry["code"], "EUR") - self.assertEqual(eur_entry["enabled"], False) + usd_entry = response.data["data"][0] + self.assertEqual(usd_entry["code"], "USD") + self.assertEqual(usd_entry["enabled"], False) @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") def test_update_static_rate(self, mock_invalidate): From bb4fac14341dacc6bd93d68f5b651f84ec3431cb Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 17:26:51 +0300 Subject: [PATCH 084/106] Move enabled-currency endpoint to settings/currency/enabled-currencies/ Replaces the old settings/currency/exchange_rate//enable/ route with RESTful settings/currency/enabled-currencies/ (GET list) and settings/currency/enabled-currencies// (POST enable, DELETE disable). Made-with: Cursor --- koku/api/settings/currency_views.py | 10 ++++ koku/api/urls.py | 9 +++- .../cost_models/test/test_enabled_currency.py | 49 +++++++++++++++++-- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 7a9343e517..c32bdab8ea 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -12,7 +12,9 @@ from rest_framework.views import APIView from api.common import log_json +from api.common.pagination import ListPaginator from api.common.permissions.settings_access import SettingsAccessPermission +from api.currency.currencies import get_currency_info from api.currency.currencies import is_valid_iso_currency from cost_models.models import EnabledCurrency @@ -36,6 +38,14 @@ def _validate_code(self, code): ) return code, None + @method_decorator(never_cache) + def get(self, request, *args, **kwargs): + if "code" in kwargs: + return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) + enabled_codes = EnabledCurrency.objects.values_list("currency_code", flat=True) + available = [get_currency_info(code) for code in sorted(enabled_codes)] + return ListPaginator(available, request).paginated_response + @method_decorator(never_cache) def post(self, request, *args, **kwargs): code, error = self._validate_code(kwargs["code"]) diff --git a/koku/api/urls.py b/koku/api/urls.py index ed774d2dc5..ec874ac443 100644 --- a/koku/api/urls.py +++ b/koku/api/urls.py @@ -436,9 +436,14 @@ name="exchange-rate-detail", ), path( - "settings/currency/exchange_rate//enable/", + "settings/currency/enabled-currencies/", EnabledCurrencyView.as_view(), - name="currency-config", + name="enabled-currencies-list", + ), + path( + "settings/currency/enabled-currencies//", + EnabledCurrencyView.as_view(), + name="enabled-currencies-detail", ), path("settings/tags/", SettingsTagView.as_view(), name="settings-tags"), path("settings/tags/enable/", SettingsEnableTagView.as_view(), name="tags-enable"), diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index c7667ae5ff..25f59bd2b8 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -3,6 +3,8 @@ # SPDX-License-Identifier: Apache-2.0 # """Tests for EnabledCurrency views.""" +from unittest.mock import patch + from django.urls import reverse from django_tenants.utils import tenant_context from rest_framework import status @@ -12,15 +14,15 @@ from cost_models.models import EnabledCurrency -class EnabledCurrencyConfigViewTest(IamTestCase): - """Tests for EnabledCurrencyView (POST/DELETE settings/currency/exchange_rate//enable/).""" +class EnabledCurrencyDetailViewTest(IamTestCase): + """Tests for POST/DELETE on settings/currency/enabled-currencies//.""" def setUp(self): super().setUp() self.client = APIClient() def _url(self, code): - return reverse("currency-config", kwargs={"code": code}) + return reverse("enabled-currencies-detail", kwargs={"code": code}) def test_post_enables_currency(self): with tenant_context(self.tenant): @@ -73,3 +75,44 @@ def test_post_normalizes_to_uppercase(self): response = self.client.post(self._url("usd"), **self.headers) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) + + def test_get_on_detail_returns_405(self): + """GET on the detail route (with a code) is not allowed.""" + response = self.client.get(self._url("USD"), **self.headers) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + +class EnabledCurrencyListViewTest(IamTestCase): + """Tests for GET on settings/currency/enabled-currencies/.""" + + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("enabled-currencies-list") + + @patch( + "api.settings.currency_views.get_currency_info", + side_effect=lambda c: { + "USD": {"code": "USD", "name": "US Dollar", "symbol": "$", "description": "USD ($) - US Dollar"}, + "EUR": {"code": "EUR", "name": "Euro", "symbol": "\u20ac", "description": "EUR (\u20ac) - Euro"}, + }[c], + ) + def test_get_returns_enabled_currencies(self, _mock): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD") + EnabledCurrency.objects.create(currency_code="EUR") + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.data["data"] + codes = [c["code"] for c in data] + self.assertEqual(codes, ["EUR", "USD"]) + self.assertEqual(data[0]["name"], "Euro") + self.assertEqual(data[1]["symbol"], "$") + + def test_get_returns_empty_list_when_none_enabled(self): + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"], []) From 25d8d1fc98bf2e25ed318cd787ddec8d20cd16c6 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 18:17:20 +0300 Subject: [PATCH 085/106] Seed EnabledCurrency in test DB and filter unsupported currencies The EnabledCurrency validation gate rejects currencies not present in the tenant's enabled_currency table. Seed USD for all three test tenant schemas so the existing test suite passes. Also skip non-ISO-4217 currency codes in _fetch_and_store_exchange_rates to match the test expectation that made-up codes like "FOO" are dropped. Made-with: Cursor --- koku/koku/koku_test_runner.py | 7 +++++++ koku/masu/celery/tasks.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/koku/koku/koku_test_runner.py b/koku/koku/koku_test_runner.py index e00432d72c..6979adbcdb 100644 --- a/koku/koku/koku_test_runner.py +++ b/koku/koku/koku_test_runner.py @@ -13,11 +13,13 @@ from django.db import connections from django.test.runner import DiscoverRunner from django.test.utils import get_unique_databases_and_mirrors +from django_tenants.utils import schema_context from api.models import Customer from api.models import Provider from api.models import Tenant from api.report.test.util.model_bakery_loader import ModelBakeryDataLoader +from cost_models.models import EnabledCurrency from koku.env import ENVIRONMENT @@ -94,6 +96,9 @@ def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, paral day_list = tree_yaml["account_structure"]["days"] bakery_data_loader = ModelBakeryDataLoader(KokuTestRunner.schema, customer) + with schema_context(KokuTestRunner.schema): + EnabledCurrency.objects.get_or_create(currency_code="USD") + ocp_on_aws_ocp_provider, ocp_on_aws_report_periods = bakery_data_loader.load_openshift_data( OCP_ON_AWS_CLUSTER_ID, on_cloud=True ) @@ -138,6 +143,8 @@ def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, paral Customer.objects.get_or_create( account_id=account[0], org_id=account[2], schema_name=account[1] ) + with schema_context(account[1]): + EnabledCurrency.objects.get_or_create(currency_code="USD") except Exception as err: LOG.error(err) raise err diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index 7921b7eabe..424b419ec3 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -16,6 +16,7 @@ from urllib3.util.retry import Retry from api.common import log_json +from api.currency.currencies import is_valid_iso_currency from api.currency.models import ExchangeRateDictionary from api.currency.models import ExchangeRates from api.currency.utils import exchange_dictionary @@ -289,6 +290,9 @@ def _fetch_and_store_exchange_rates(url): data = response.json() rates = data["rates"] for curr_type, value in rates.items(): + if not is_valid_iso_currency(curr_type): + LOG.warning(f"Skipping unsupported currency {curr_type}") + continue try: exchange = ExchangeRates.objects.get(currency_type=curr_type.lower()) LOG.info(f"Updating currency {curr_type} to {value}") From ae35f2cba2ce7206575db83264f6e7eff22dfe82 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 29 Apr 2026 18:28:02 +0300 Subject: [PATCH 086/106] add to in model --- koku/api/currency/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koku/api/currency/models.py b/koku/api/currency/models.py index a9ac27e353..096379ec5d 100644 --- a/koku/api/currency/models.py +++ b/koku/api/currency/models.py @@ -11,7 +11,7 @@ class ExchangeRates(models.Model): - currency_type = models.CharField(max_length=5, unique=False) + currency_type = models.CharField(max_length=5, unique=False, blank=True) exchange_rate = models.FloatField(default=0) From c47b46595dd9a6a46462d4862adab694b2f31ace Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 30 Apr 2026 13:00:03 +0300 Subject: [PATCH 087/106] Fix tenant context for EnabledCurrency in tests - Wrap EnabledCurrency operations in schema_context in CurrencyViewTest and CostModelSerializerTest - Mock get_enabled_currency_codes in RateSerializer test to avoid DB hit - Seed multiple currencies (USD, EUR, JPY, AUD, GBP, CAD) across all test schemas in KokuTestRunner instead of only USD Made-with: Cursor --- koku/api/currency/test/test_views.py | 8 +++++--- koku/cost_models/test/test_rate_serializer.py | 4 +++- koku/cost_models/test/test_serializers.py | 7 ++++--- koku/koku/koku_test_runner.py | 12 +++++++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index d7a7528487..56a58aec97 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -5,6 +5,7 @@ from unittest.mock import patch from django.urls import reverse +from django_tenants.utils import schema_context from rest_framework import status from rest_framework.test import APIClient @@ -17,9 +18,10 @@ class CurrencyViewTest(IamTestCase): def setUp(self): super().setUp() - EnabledCurrency.objects.all().delete() - EnabledCurrency.objects.create(currency_code="USD") - EnabledCurrency.objects.create(currency_code="EUR") + with schema_context(self.schema_name): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD") + EnabledCurrency.objects.create(currency_code="EUR") @patch( "api.currency.view.get_currency_info", diff --git a/koku/cost_models/test/test_rate_serializer.py b/koku/cost_models/test/test_rate_serializer.py index 861300b0c7..25d11edbb4 100644 --- a/koku/cost_models/test/test_rate_serializer.py +++ b/koku/cost_models/test/test_rate_serializer.py @@ -4,6 +4,7 @@ # """Tests for RateSerializer rate_id and custom_name output.""" from unittest import TestCase +from unittest.mock import patch from uuid import uuid4 from cost_models.serializers import RateSerializer @@ -151,7 +152,8 @@ def test_is_valid_rejects_malformed_rate_id(self): self.assertFalse(serializer.is_valid()) self.assertIn("rate_id", serializer.errors) - def test_is_valid_accepts_valid_rate_id(self): + @patch("api.currency.currencies.get_enabled_currency_codes", return_value={"USD"}) + def test_is_valid_accepts_valid_rate_id(self, _mock): """Test that a valid UUID string passes field validation.""" data = { "metric": {"name": "cpu_core_usage_per_hour"}, diff --git a/koku/cost_models/test/test_serializers.py b/koku/cost_models/test/test_serializers.py index e73c65320d..768e80fb43 100644 --- a/koku/cost_models/test/test_serializers.py +++ b/koku/cost_models/test/test_serializers.py @@ -807,9 +807,10 @@ def test_cost_model_currency(self): def test_invalid_currency(self): """Test failure while handling invalid cost_type.""" self.ocp_data["currency"] = "invalid" - serializer = CostModelSerializer(data=self.ocp_data, context=self.request_context) - with self.assertRaises(serializers.ValidationError): - serializer.is_valid(raise_exception=True) + with tenant_context(self.tenant): + serializer = CostModelSerializer(data=self.ocp_data, context=self.request_context) + with self.assertRaises(serializers.ValidationError): + serializer.is_valid(raise_exception=True) def test_tiered_not_matching_currency(self): """Test if tiered rates do not match currency raises a validation error.""" diff --git a/koku/koku/koku_test_runner.py b/koku/koku/koku_test_runner.py index 6979adbcdb..00a5dc4965 100644 --- a/koku/koku/koku_test_runner.py +++ b/koku/koku/koku_test_runner.py @@ -96,9 +96,6 @@ def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, paral day_list = tree_yaml["account_structure"]["days"] bakery_data_loader = ModelBakeryDataLoader(KokuTestRunner.schema, customer) - with schema_context(KokuTestRunner.schema): - EnabledCurrency.objects.get_or_create(currency_code="USD") - ocp_on_aws_ocp_provider, ocp_on_aws_report_periods = bakery_data_loader.load_openshift_data( OCP_ON_AWS_CLUSTER_ID, on_cloud=True ) @@ -143,8 +140,13 @@ def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, paral Customer.objects.get_or_create( account_id=account[0], org_id=account[2], schema_name=account[1] ) - with schema_context(account[1]): - EnabledCurrency.objects.get_or_create(currency_code="USD") + + _TEST_CURRENCIES = ("USD", "EUR", "JPY", "AUD", "GBP", "CAD") + for _schema in (KokuTestRunner.schema, "org2222222", "org3333333"): + with schema_context(_schema): + for code in _TEST_CURRENCIES: + EnabledCurrency.objects.get_or_create(currency_code=code) + except Exception as err: LOG.error(err) raise err From 0396c3cb5dfe84d6871e08e5f93b076214e8bcba Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 30 Apr 2026 13:05:43 +0300 Subject: [PATCH 088/106] Add missing migration for ExchangeRates.currency_type blank=True Made-with: Cursor --- .../0073_alter_exchangerates_currency_type.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 koku/api/migrations/0073_alter_exchangerates_currency_type.py diff --git a/koku/api/migrations/0073_alter_exchangerates_currency_type.py b/koku/api/migrations/0073_alter_exchangerates_currency_type.py new file mode 100644 index 0000000000..f3ab5be469 --- /dev/null +++ b/koku/api/migrations/0073_alter_exchangerates_currency_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.13 on 2026-04-30 10:05 +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0072_alter_exchangerates_currency_type"), + ] + + operations = [ + migrations.AlterField( + model_name="exchangerates", + name="currency_type", + field=models.CharField(blank=True, max_length=5), + ), + ] From b652370d77211481efa90c20c5e9eb3e145e5290 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Fri, 1 May 2026 01:38:08 +0300 Subject: [PATCH 089/106] Update constant-currency docs: fix enablement URL and clarify costs-as-is behavior Fix currency enablement URL references to settings/currency/enabled-currencies/{code}/. Clarify that "costs as-is" works via serializer rejecting non-enabled currencies (two-level validation: serializer then query handler). Made-with: Cursor --- docs/architecture/constant-currency/README.md | 16 ++++---- .../constant-currency/api-and-frontend.md | 33 ++++++++-------- .../constant-currency/data-model.md | 19 ++++++---- .../constant-currency/phased-delivery.md | 13 ++++--- .../constant-currency/pipeline-changes.md | 38 ++++++++++++------- .../constant-currency/risk-register.md | 12 +++--- 6 files changed, 77 insertions(+), 54 deletions(-) diff --git a/docs/architecture/constant-currency/README.md b/docs/architecture/constant-currency/README.md index c3b1f4e7a8..3d8824e15d 100644 --- a/docs/architecture/constant-currency/README.md +++ b/docs/architecture/constant-currency/README.md @@ -129,7 +129,7 @@ immediately available for use, or should an administrator explicitly enable them **Resolution**: Explicit enablement. The full list of known currencies comes from Babel's ISO 4217 registry. Only currencies that an administrator has explicitly enabled are stored in the `EnabledCurrency` table. An administrator must enable -currencies through the Settings API (`POST settings/currency/exchange_rate/{code}/enable/`) before +currencies through the Settings API (`POST settings/currency/enabled-currencies/{code}/`) before they appear in the target currency dropdown. All currencies are always stored in `MonthlyExchangeRate` regardless of their @@ -157,8 +157,9 @@ fallback**. When `CURRENCY_URL` is empty or unset: - If dynamic rates were previously fetched (before the URL was removed), they remain available as fallback - If `MonthlyExchangeRate` is completely empty (no rates configured at all), - the feature is inactive — validation is skipped and costs are returned as-is - in their original bill currency + the feature is inactive — no currencies are enabled, so the user cannot + select a target currency and costs are returned as-is in their original + bill currency - If rates exist but not for a given pair, the API returns an actionable error The `CURRENCY_URL` setting is documented with the production API URL @@ -270,11 +271,11 @@ graph LR 1. **Single source of truth**: `MonthlyExchangeRate` stores rates for all months (current and past); query handlers read from this one table 2. **Two writers**: Celery task writes dynamic rates daily for the current month; CRUD serializer writes static rates for affected months -3. **Rate resolution**: All months read from `MonthlyExchangeRate`; M2 migration seeds current-month data at deployment; pre-deployment months fall back to earliest available rate. When `MonthlyExchangeRate` is empty (feature not configured), costs are returned as-is; when rows exist but not for the target currency, an actionable error is returned +3. **Rate resolution**: All months read from `MonthlyExchangeRate`; M2 migration seeds current-month data at deployment; pre-deployment months fall back to earliest available rate. When `MonthlyExchangeRate` is empty (feature not configured), no currencies are enabled and costs are returned as-is; when rows exist but not for the target currency, an actionable error is returned 4. Report responses include rate provenance metadata 5. **Currency enablement**: Dynamic currencies arrive as disabled; administrator enables them via Settings to make them visible in the dropdown (all currencies are always stored) 6. **Dropdown visibility**: Target currency dropdown shows only currencies that an administrator has explicitly enabled (static rate currencies still require enablement) -7. **No-rate handling**: If `MonthlyExchangeRate` is empty, the feature is inactive and costs are returned as-is. If rows exist but not for the selected currency, an actionable error is returned +7. **No-rate handling**: If `MonthlyExchangeRate` is empty, the feature is inactive — no currencies are enabled, so costs are returned as-is. If rows exist but not for the selected currency, an actionable error is returned --- @@ -293,7 +294,7 @@ graph LR | 9 | **Per-pair rows, not JSON blob** | Enables `unique_together` constraint, simpler queries, cleaner ORM integration | | 10 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. | | 11 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback). Documentation references `open.er-api.com` (free tier) as the production example | -| 12 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection. When `MonthlyExchangeRate` is empty (no rates configured at all), the feature is inactive and costs are returned as-is | +| 12 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection. When `MonthlyExchangeRate` is empty (no rates configured at all), the feature is inactive — no currencies are enabled and costs are returned as-is | | 13 | **Enablement is always required for reports** | Static exchange rate currencies must still be explicitly enabled to appear in the report dropdown. The settings admin page shows them regardless for management purposes. | --- @@ -311,6 +312,7 @@ graph LR | v1.6 | 2026-03-30 | Removed `ExchangeRateDictionary` fallback from query handler. M2 seeds current-month data. Decision #9 updated. | | v1.7 | 2026-04-12 | Updated data flow diagram: query handler uses `Subquery` annotation instead of `Case`/`When`. | | v1.8 | 2026-04-13 | Synced pre-deployment month references: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). | -| v1.9 | 2026-04-28 | Updated currency enablement URL reference to `settings/currency/exchange_rate/{code}/enable/`. | +| v1.9 | 2026-04-28 | Updated currency enablement URL reference to `settings/currency/enabled-currencies/{code}/`. | | v2.0 | 2026-04-28 | Removed static-rate enablement bypass (decision #13). Report dropdown governed solely by `EnabledCurrency`; settings admin page shows static rates regardless. | | v2.1 | 2026-04-28 | Added "costs as-is" behavior: when `MonthlyExchangeRate` is empty, feature is inactive, validation skipped, costs returned in original currency. Updated IQ-6, decision #12, data flow key changes. | +| v2.2 | 2026-04-30 | Fixed currency enablement URL to `settings/currency/enabled-currencies/{code}/`. Clarified "costs as-is": no currencies enabled means serializer blocks currency selection; costs returned as-is. | diff --git a/docs/architecture/constant-currency/api-and-frontend.md b/docs/architecture/constant-currency/api-and-frontend.md index 52b18afa51..531e782fc7 100644 --- a/docs/architecture/constant-currency/api-and-frontend.md +++ b/docs/architecture/constant-currency/api-and-frontend.md @@ -14,7 +14,7 @@ OpenAPI updates. ``` GET/POST /api/cost-management/v1/settings/currency/exchange_rate/ PUT/DELETE /api/cost-management/v1/settings/currency/exchange_rate/{uuid}/ -POST/DELETE /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/ +POST/DELETE /api/cost-management/v1/settings/currency/enabled-currencies/{code}/ ``` ### Registration @@ -146,15 +146,16 @@ This endpoint is always available. No Unleash feature flag gating. ## Currency Enablement -Currency enablement is managed via the exchange rate endpoint. The enabled -status for each currency is visible in the GET list response and can be -toggled individually. +Currency enablement is managed via a dedicated endpoint under +`settings/currency/enabled-currencies/`. The enabled status for each currency +is also visible in the `GET settings/currency/exchange_rate/` list response and +can be toggled individually. ### URL ``` -POST /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/ -DELETE /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/ +POST /api/cost-management/v1/settings/currency/enabled-currencies/{code}/ +DELETE /api/cost-management/v1/settings/currency/enabled-currencies/{code}/ ``` - **POST**: Enables the currency (creates an `EnabledCurrency` row). No request body. @@ -226,11 +227,12 @@ There are two distinct cases: **1. Feature not configured** (`MonthlyExchangeRate` is empty): When no exchange rates have been configured at all (no `CURRENCY_URL`, no static rates, no Celery -task run), the constant currency feature is inactive. Validation is skipped and -costs are returned as-is in their original bill currency. The -`Coalesce("exchange_rate", Value(1))` fallback in provider maps ensures NULL -annotations resolve to `1` (no conversion). This is the default state for fresh -deployments. +task run), the constant currency feature is inactive. No currencies are enabled, +so the serializer rejects any explicit `currency` parameter — the user cannot +select a target currency. Without a `currency` parameter, costs are returned +as-is in their original bill currency. The `Coalesce("exchange_rate", Value(1))` +fallback in provider maps ensures NULL annotations resolve to `1` (no +conversion). This is the default state for fresh deployments. **2. Feature active but target currency has no rates** (`MonthlyExchangeRate` has rows but none for the target): The API returns an error: @@ -350,8 +352,8 @@ Add endpoint definitions for: - `POST /api/cost-management/v1/settings/currency/exchange_rate/` — create exchange rate - `PUT /api/cost-management/v1/settings/currency/exchange_rate/{uuid}/` — update exchange rate - `DELETE /api/cost-management/v1/settings/currency/exchange_rate/{uuid}/` — delete exchange rate -- `POST /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/` — enable currency -- `DELETE /api/cost-management/v1/settings/currency/exchange_rate/{code}/enable/` — disable currency +- `POST /api/cost-management/v1/settings/currency/enabled-currencies/{code}/` — enable currency +- `DELETE /api/cost-management/v1/settings/currency/enabled-currencies/{code}/` — disable currency Add `exchange_rates_applied` to report response schemas. @@ -373,7 +375,7 @@ The frontend will: - Display validity periods (start/end month) - Show a note explaining dynamic rates are used when no static rate is defined - **Add a currency enablement toggle** using - `POST/DELETE settings/currency/exchange_rate/{code}/enable/` + `POST/DELETE settings/currency/enabled-currencies/{code}/` - **Populate the target currency dropdown** from enabled currencies only. Disabled currencies are stored but hidden from the report dropdown. - **Handle the no-rate error**: When the user selects a target currency that @@ -407,6 +409,7 @@ The frontend will: | v1.5 | 2026-04-09 | Replaced stale `MonthlyExchangeRateSnapshot` → `MonthlyExchangeRate`, removed `StaticExchangeRateDictionary` references (removed in pipeline-changes v1.6). | | v1.6 | 2026-04-12 | Updated `exchange_rates_applied` implementation to reflect `Subquery`-based rate resolution (removed `effective_exchange_rates` reference). | | v1.7 | 2026-04-13 | Removed stale "snapshotted" terminology (remnant from `MonthlyExchangeRateSnapshot` rename). | -| v1.8 | 2026-04-28 | Consolidated endpoints under `settings/currency/exchange_rate/`. List returns grouped response with enabled status. Currency enablement via POST/DELETE (no body). Removed separate `AllCurrencyView` and `available-currencies` endpoints. | +| v1.8 | 2026-04-28 | Consolidated endpoints under `settings/currency/exchange_rate/`. List returns grouped response with enabled status. Currency enablement via POST/DELETE at `settings/currency/enabled-currencies/{code}/` (no body). Removed separate `AllCurrencyView` and `available-currencies` endpoints. | | v1.9 | 2026-04-28 | Removed static-rate enablement bypass. Report dropdown governed solely by `EnabledCurrency`. Settings admin page shows static rates regardless for management. | | v2.0 | 2026-04-28 | Added "costs as-is" behavior to Corner Case section: when `MonthlyExchangeRate` is empty, feature is inactive, costs returned in original currency. | +| v2.1 | 2026-04-30 | Fixed currency enablement URLs to `settings/currency/enabled-currencies/{code}/`. Clarified "costs as-is" Corner Case: serializer enforces enabled currencies before query handler validation. | diff --git a/docs/architecture/constant-currency/data-model.md b/docs/architecture/constant-currency/data-model.md index 050bc2f8a5..98cceb0559 100644 --- a/docs/architecture/constant-currency/data-model.md +++ b/docs/architecture/constant-currency/data-model.md @@ -143,9 +143,9 @@ enabled. | Event | Action | |-------|--------| -| Administrator enables a currency via `POST settings/currency/exchange_rate/{code}/enable/` | Creates an `EnabledCurrency` row for that currency | -| Administrator disables a currency via `DELETE settings/currency/exchange_rate/{code}/enable/` | Removes the `EnabledCurrency` row for that currency | -| `GET settings/currency/exchange_rate/` | Returns currencies that have exchange rates, grouped by target currency, with `enabled` flag based on `EnabledCurrency` table membership | +| Administrator enables a currency via `POST settings/currency/enabled-currencies/{code}/` | Creates an `EnabledCurrency` row for that currency | +| Administrator disables a currency via `DELETE settings/currency/enabled-currencies/{code}/` | Removes the `EnabledCurrency` row for that currency | +| `GET settings/currency/exchange_rate/` | Returns currencies that have exchange rates, grouped by base currency, with `enabled` flag based on `EnabledCurrency` table membership | **How currencies become "available" in the report dropdown**: @@ -166,10 +166,12 @@ configure static exchange rates or enable dynamic exchange rates."* See **No rates configured at all**: When `MonthlyExchangeRate` is completely empty (no `CURRENCY_URL` configured, no static rates defined, no Celery task run), -the constant currency feature is inactive. Validation is skipped and costs are -returned as-is in their original bill currency. The `Coalesce(..., Value(1))` -fallback in provider maps ensures exchange rate annotations resolve to `1` (no -conversion). This is the default state for fresh deployments. +the constant currency feature is inactive. No currencies are enabled, so the +serializer rejects any explicit `currency` parameter. Without a `currency` +parameter, costs are returned as-is in their original bill currency. The +`Coalesce(..., Value(1))` fallback in provider maps ensures exchange rate +annotations resolve to `1` (no conversion). This is the default state for +fresh deployments. **No `CURRENCY_URL` configured**: When the URL is not set, no dynamic currencies are discovered by the Celery task, so no rows are created automatically. The @@ -382,6 +384,7 @@ changes required. | v1.7 | 2026-03-30 | M2 now seeds current-month data from `ExchangeRateDictionary` during migration. Eliminates `ExchangeRateDictionary` fallback in query handler. | | v1.8 | 2026-04-12 | Updated reader description to reflect `Subquery`-based rate resolution (replaces `Case`/`When`). | | v1.9 | 2026-04-13 | Fixed `ExchangeRates` model description: actual fields are `currency_type` (CharField) and `exchange_rate` (FloatField), not `base_currency`/`exchange_rates` JSONField. Removed non-existent `updated_timestamp` column from `ExchangeRateDictionary` example. | -| v2.0 | 2026-04-28 | Updated `EnabledCurrency` lifecycle to reflect POST/DELETE per-currency enablement at `settings/currency/exchange_rate/{code}/enable/`. | +| v2.0 | 2026-04-28 | Updated `EnabledCurrency` lifecycle to reflect POST/DELETE per-currency enablement at `settings/currency/enabled-currencies/{code}/`. | | v2.1 | 2026-04-28 | Removed static-rate enablement bypass. Report dropdown governed solely by `EnabledCurrency`. | | v2.2 | 2026-04-28 | Added "costs as-is" behavior: when `MonthlyExchangeRate` is empty, feature is inactive, costs returned in original currency. | +| v2.3 | 2026-04-30 | Fixed `EnabledCurrency` lifecycle URLs to `settings/currency/enabled-currencies/{code}/`. Clarified "costs as-is" behavior: serializer blocks non-enabled currencies. | diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index ce260082b6..65e890e3f3 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -42,7 +42,7 @@ pairs. Show rate provenance in report responses. | Serializer | `koku/cost_models/static_exchange_rate_serializer.py` | Validation + `MonthlyExchangeRate` upsert side-effects | | ViewSet | `koku/cost_models/static_exchange_rate_view.py` | CRUD API for static rates | | Currency enablement view | `koku/api/settings/currency_views.py` | POST/DELETE enablement for individual currencies | -| URL registration | `koku/api/urls.py` | Routes for `settings/currency/exchange_rate/` (list/create, detail, enable) | +| URL registration | `koku/api/urls.py` | Routes for `settings/currency/exchange_rate/` (list/create, detail) and `settings/currency/enabled-currencies/` (enable/disable) | | Celery task update | `koku/masu/celery/tasks.py` | Currency discovery, `MonthlyExchangeRate` upsert for all currencies per tenant (skips fetch if no `CURRENCY_URL`) | | Query handler update | `koku/api/query_handler.py` | Read from `MonthlyExchangeRate` for all months (no fallback; M2 seeds current month) | | OCP handler update | `koku/api/report/ocp/query_handler.py` | OCP-specific rate resolution from `MonthlyExchangeRate` | @@ -72,15 +72,15 @@ pairs. Show rate provenance in report responses. - [ ] Consecutive months with same rate/type collapsed into one period string - [ ] Unit tests pass for serializer, view, MonthlyExchangeRate logic, query handler - [ ] On-prem mode: full functionality without Trino -- [ ] **Currency enablement**: Administrator can enable currencies via `POST settings/currency/exchange_rate/{code}/enable/` +- [ ] **Currency enablement**: Administrator can enable currencies via `POST settings/currency/enabled-currencies/{code}/` - [ ] **Currency enablement**: All currencies are stored in `MonthlyExchangeRate` regardless of enabled status; `enabled` flag only controls dropdown visibility -- [ ] **Rate resolution**: Static rates take precedence over dynamic rates; when `MonthlyExchangeRate` is empty (feature not configured), costs returned as-is; when rows exist but not for target, error returned +- [ ] **Rate resolution**: Static rates take precedence over dynamic rates; when `MonthlyExchangeRate` is empty (feature not configured), no currencies are enabled and costs returned as-is; when rows exist but not for target, error returned - [ ] **No `CURRENCY_URL`**: Celery task skips API fetch; system works with whatever rates are available - [ ] **Available currencies**: Report dropdown shows only enabled currencies (static rates do not bypass enablement) -- [ ] **Costs as-is**: When no exchange rates are configured at all (`MonthlyExchangeRate` empty), validation skipped, costs returned in original bill currency +- [ ] **Costs as-is**: When no exchange rates are configured at all (`MonthlyExchangeRate` empty), no currencies are enabled — serializer rejects any `currency` parameter and costs returned in original bill currency - [ ] **No-rate corner case**: Selecting a target currency with no conversion path (when rates exist for other currencies) returns HTTP 400 with actionable error - [ ] **No currencies available**: Dropdown hidden or shows "No exchange rates available" when no currencies are available -- [ ] **Currency list**: `GET settings/currency/exchange_rate/` returns currencies grouped by target currency with enabled flag and nested exchange rates +- [ ] **Currency list**: `GET settings/currency/exchange_rate/` returns currencies grouped by base currency with enabled flag and nested exchange rates ### Rollback @@ -91,7 +91,7 @@ pairs. Show rate provenance in report responses. `MonthlyExchangeRate` upsert, currency discovery) 4. Revert report meta changes in `koku/api/report/queries.py` (remove `exchange_rates_applied` metadata and no-rate error handling) -5. Revert URL registration in `koku/api/urls.py` (remove `settings/currency/exchange_rate/` routes) +5. Revert URL registration in `koku/api/urls.py` (remove `settings/currency/exchange_rate/` and `settings/currency/enabled-currencies/` routes) 7. Drop tables via reverse migration (`migrate_schemas` runs `DeleteModel` for all three new tables: `static_exchange_rate`, `monthly_exchange_rate`, `enabled_currency`) @@ -179,3 +179,4 @@ design would be needed to handle path prioritization. | v2.1 | 2026-04-28 | Updated URL references to `settings/currency/exchange_rate/`. Consolidated URL registration to `koku/api/urls.py`. Removed separate available-currencies endpoint. | | v2.2 | 2026-04-28 | Removed static-rate enablement bypass from validation checklist. Report dropdown governed solely by `EnabledCurrency`. | | v2.3 | 2026-04-28 | Added "costs as-is" validation item: when `MonthlyExchangeRate` is empty, feature inactive, costs returned as-is. Updated rate resolution and no-rate validation items. | +| v2.4 | 2026-04-30 | Fixed currency enablement URL to `settings/currency/enabled-currencies/{code}/`. Clarified "costs as-is": serializer blocks non-enabled currencies before query handler. | diff --git a/docs/architecture/constant-currency/pipeline-changes.md b/docs/architecture/constant-currency/pipeline-changes.md index 5df0a2b058..12277e180e 100644 --- a/docs/architecture/constant-currency/pipeline-changes.md +++ b/docs/architecture/constant-currency/pipeline-changes.md @@ -187,8 +187,9 @@ for tenant in Tenant.objects.exclude(schema_name="public"): - **URL check**: If `CURRENCY_URL` is not configured, the task skips the API fetch. Dynamic rates are simply not fetched; the system uses whatever rates are available (static first, dynamic fallback). If `MonthlyExchangeRate` is - completely empty (no rates configured), the feature is inactive and costs are - returned as-is in their original bill currency. + completely empty (no rates configured), the feature is inactive — no + currencies are enabled and costs are returned as-is in their original bill + currency. - **All currencies stored**: Upserts dynamic rates for all currency pairs returned by the API. The `EnabledCurrency` table controls dropdown visibility, not rate storage. Administrators enable currencies via the Settings API. @@ -444,9 +445,18 @@ an exception. ### Pre-Query Exchange Rate Validation -Before executing the report query, the query handler validates that exchange -rate data exists for the requested target currency. This validation has two -levels: +Validation happens at two levels before the report query executes: + +**Level 1 — Serializer** (`CurrencyField(enabled_only=True)`): The `currency` +query parameter is validated against the `EnabledCurrency` table. Only +currencies that an administrator has explicitly enabled are accepted. When no +currencies are enabled (feature not configured), any `currency` parameter is +rejected — the user cannot select a target currency and costs are returned +as-is in their default currency. + +**Level 2 — Query handler** (`_validate_exchange_rates`): If the serializer +passes, the query handler checks that `MonthlyExchangeRate` has rows for the +target currency: ```python def _validate_exchange_rates(self, target_currency): @@ -465,14 +475,15 @@ def _validate_exchange_rates(self, target_currency): **Key design choices**: - **Feature activation**: When `MonthlyExchangeRate` is completely empty (no - rates configured at all), the feature is inactive. Validation is skipped and - costs are returned as-is in their original bill currency. The - `Coalesce("exchange_rate", Value(1))` fallback in provider maps ensures NULL - annotations resolve to `1` (no conversion). -- **Pre-query check**: The validation runs before the query executes. It - verifies that *any* `MonthlyExchangeRate` row exists for the target currency. - Per-row mismatches (e.g., a specific base currency with no rate) are handled - gracefully by the `Coalesce` fallback to `1`. + rates configured at all), the feature is inactive. No currencies are enabled, + so the serializer rejects any `currency` parameter before the query handler + runs. Without a `currency` parameter, the `Coalesce("exchange_rate", + Value(1))` fallback in provider maps ensures NULL annotations resolve to `1` + (no conversion). +- **Pre-query check**: The query handler validation runs before the query + executes. It verifies that *any* `MonthlyExchangeRate` row exists for the + target currency. Per-row mismatches (e.g., a specific base currency with no + rate) are handled gracefully by the `Coalesce` fallback to `1`. - **Exception type**: `ExchangeRateNotFound` is a custom exception caught by the view layer and returned as HTTP 400 with an actionable error message. This only fires when rates are configured but not for the requested currency @@ -636,3 +647,4 @@ per-source-type would miss cross-provider reports (e.g., OCP-on-AWS). | v2.2 | 2026-04-13 | Fixed current pipeline description: `ExchangeRates` upserts per target currency (not base). Fixed "stored and stored" typo in available currency resolution. | | v2.3 | 2026-04-28 | Removed static-rate enablement bypass from available currency resolution. Report dropdown governed solely by `EnabledCurrency`. | | v2.4 | 2026-04-28 | Replaced post-query validation pseudocode with actual pre-query implementation. Added "costs as-is" behavior: when `MonthlyExchangeRate` is empty, feature inactive, validation skipped. | +| v2.5 | 2026-04-30 | Documented two-level validation: serializer (`CurrencyField(enabled_only=True)`) blocks non-enabled currencies before query handler checks `MonthlyExchangeRate`. | diff --git a/docs/architecture/constant-currency/risk-register.md b/docs/architecture/constant-currency/risk-register.md index d977b79c8d..4274d3a4b3 100644 --- a/docs/architecture/constant-currency/risk-register.md +++ b/docs/architecture/constant-currency/risk-register.md @@ -183,11 +183,12 @@ with unconverted or zero amounts. **Behavior**: Two distinct cases: 1. **`MonthlyExchangeRate` is completely empty** (feature not configured): The - constant currency feature is inactive. Validation is skipped entirely. The - `Coalesce("exchange_rate", Value(1))` fallback in provider maps ensures all - exchange rate annotations resolve to `1`, so costs are returned as-is in - their original bill currency. This is the default state for fresh deployments - without `CURRENCY_URL` or static rates. + constant currency feature is inactive. No currencies are enabled, so the + serializer rejects any explicit `currency` parameter. Without a `currency` + parameter, the `Coalesce("exchange_rate", Value(1))` fallback in provider + maps ensures all exchange rate annotations resolve to `1`, so costs are + returned as-is in their original bill currency. This is the default state + for fresh deployments without `CURRENCY_URL` or static rates. 2. **`MonthlyExchangeRate` has rows but none for the target currency**: The feature is active but the specific currency pair is missing. Rate resolution @@ -242,3 +243,4 @@ R8 ✓ | v1.7 | 2026-04-13 | R4: updated to reflect earliest-available-rate fallback for pre-deployment months (aligns with pipeline-changes.md v2.1). | | v1.8 | 2026-04-28 | R8: updated CRUD API URL to `settings/currency/exchange_rate/`. | | v1.9 | 2026-04-28 | R8: added "costs as-is" behavior — when `MonthlyExchangeRate` is empty, feature is inactive, validation skipped, costs returned in original currency. | +| v2.0 | 2026-04-30 | R8: clarified "costs as-is" — serializer blocks non-enabled currencies; query handler skip is secondary defense. | From b24bca8cceeca0cd3ec01f5dde484a24981cdc1d Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Fri, 1 May 2026 01:55:00 +0300 Subject: [PATCH 090/106] [COST-7252] Add bidirectional inverse for static exchange rates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a static rate is defined (e.g. USD→EUR), automatically write the inverse direction (EUR→USD = 1/rate) into MonthlyExchangeRate. This ensures airgapped deployments with only static rates have both directions available at query time without requiring CURRENCY_URL. The inverse is only written when no explicit StaticExchangeRate defines the reverse pair, so user-defined rates always take precedence. Made-with: Cursor --- .../constant-currency/phased-delivery.md | 2 +- .../cost_models/static_exchange_rate_utils.py | 84 ++++++++-- .../test_static_exchange_rate_serializer.py | 149 ++++++++++++++++++ 3 files changed, 220 insertions(+), 15 deletions(-) diff --git a/docs/architecture/constant-currency/phased-delivery.md b/docs/architecture/constant-currency/phased-delivery.md index 65e890e3f3..3f598d0ed2 100644 --- a/docs/architecture/constant-currency/phased-delivery.md +++ b/docs/architecture/constant-currency/phased-delivery.md @@ -60,7 +60,7 @@ pairs. Show rate provenance in report responses. - [ ] Static rate CRUD: create, read, update, delete via API - [ ] Overlapping validity period rejection returns 400 - [ ] Natural month boundary enforcement (mid-month dates rejected) -- [ ] Bidirectional inverse rate resolution (1/rate when reverse undefined) +- [x] Bidirectional inverse rate resolution (1/rate when reverse undefined) - [ ] Dynamic rate daily `MonthlyExchangeRate` upsert per tenant - [ ] Static rate precedence: task skips pairs with existing static rates - [ ] Finalized month immutability: past month rows never overwritten diff --git a/koku/cost_models/static_exchange_rate_utils.py b/koku/cost_models/static_exchange_rate_utils.py index 42f4ff9ee3..c6bac356a9 100644 --- a/koku/cost_models/static_exchange_rate_utils.py +++ b/koku/cost_models/static_exchange_rate_utils.py @@ -3,11 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 # """Utilities for managing MonthlyExchangeRate side effects of StaticExchangeRate operations.""" +from decimal import Decimal + from dateutil.relativedelta import relativedelta from api.currency.models import ExchangeRateDictionary from cost_models.models import MonthlyExchangeRate from cost_models.models import RateType +from cost_models.models import StaticExchangeRate def _iter_months(start_date, end_date): @@ -19,8 +22,25 @@ def _iter_months(start_date, end_date): current += relativedelta(months=1) +def _explicit_static_rate_exists(base_currency, target_currency, month_start): + """Check if a StaticExchangeRate explicitly defines this direction covering the given month.""" + return StaticExchangeRate.objects.filter( + base_currency=base_currency, + target_currency=target_currency, + start_date__lte=month_start, + end_date__gte=month_start, + ).exists() + + def upsert_static_monthly_rates(static_rate): - """Upsert MonthlyExchangeRate rows with rate_type=static for each month in the validity period.""" + """Upsert MonthlyExchangeRate rows for each month, including the inverse direction. + + The forward direction is always written unconditionally. The inverse (1/rate) + is written only when no explicit StaticExchangeRate defines the reverse pair + for that month, ensuring explicit user-defined rates always take precedence. + """ + inverse_rate = Decimal(1) / static_rate.exchange_rate + for month_start in _iter_months(static_rate.start_date, static_rate.end_date): MonthlyExchangeRate.objects.update_or_create( effective_date=month_start, @@ -32,9 +52,24 @@ def upsert_static_monthly_rates(static_rate): }, ) + if not _explicit_static_rate_exists(static_rate.target_currency, static_rate.base_currency, month_start): + MonthlyExchangeRate.objects.update_or_create( + effective_date=month_start, + base_currency=static_rate.target_currency, + target_currency=static_rate.base_currency, + defaults={ + "exchange_rate": inverse_rate, + "rate_type": RateType.STATIC, + }, + ) + def remove_static_and_backfill_dynamic(base_currency, target_currency, start_date, end_date): - """Remove static rows for affected months and backfill with dynamic rates from ExchangeRateDictionary.""" + """Remove static rows for affected months and backfill with dynamic rates from ExchangeRateDictionary. + + Also removes auto-generated inverse rows unless an explicit StaticExchangeRate + defines the reverse direction for that month. + """ MonthlyExchangeRate.objects.filter( effective_date__gte=start_date.replace(day=1), effective_date__lte=end_date.replace(day=1), @@ -43,22 +78,43 @@ def remove_static_and_backfill_dynamic(base_currency, target_currency, start_dat rate_type=RateType.STATIC, ).delete() + for month_start in _iter_months(start_date, end_date): + if not _explicit_static_rate_exists(target_currency, base_currency, month_start): + MonthlyExchangeRate.objects.filter( + effective_date=month_start, + base_currency=target_currency, + target_currency=base_currency, + rate_type=RateType.STATIC, + ).delete() + erd = ExchangeRateDictionary.objects.first() if not erd or not erd.currency_exchange_dictionary: return exchange_dict = erd.currency_exchange_dictionary rate = exchange_dict.get(base_currency, {}).get(target_currency) - if rate is None: - return + if rate is not None: + for month_start in _iter_months(start_date, end_date): + MonthlyExchangeRate.objects.update_or_create( + effective_date=month_start, + base_currency=base_currency, + target_currency=target_currency, + defaults={ + "exchange_rate": rate, + "rate_type": RateType.DYNAMIC, + }, + ) - for month_start in _iter_months(start_date, end_date): - MonthlyExchangeRate.objects.update_or_create( - effective_date=month_start, - base_currency=base_currency, - target_currency=target_currency, - defaults={ - "exchange_rate": rate, - "rate_type": RateType.DYNAMIC, - }, - ) + inverse_rate = exchange_dict.get(target_currency, {}).get(base_currency) + if inverse_rate is not None: + for month_start in _iter_months(start_date, end_date): + if not _explicit_static_rate_exists(target_currency, base_currency, month_start): + MonthlyExchangeRate.objects.update_or_create( + effective_date=month_start, + base_currency=target_currency, + target_currency=base_currency, + defaults={ + "exchange_rate": inverse_rate, + "rate_type": RateType.DYNAMIC, + }, + ) diff --git a/koku/cost_models/test/test_static_exchange_rate_serializer.py b/koku/cost_models/test/test_static_exchange_rate_serializer.py index 47e2b6e184..91ceb4b224 100644 --- a/koku/cost_models/test/test_static_exchange_rate_serializer.py +++ b/koku/cost_models/test/test_static_exchange_rate_serializer.py @@ -158,3 +158,152 @@ def test_name_computed_field(self): data = self.valid_data.copy() serializer = StaticExchangeRateSerializer(data=data, context=self._make_request_context()) self.assertTrue(serializer.is_valid(), serializer.errors) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_create_upserts_inverse_monthly_rate(self, mock_invalidate): + """Test that creating USD->EUR also creates the inverse EUR->USD = 1/rate.""" + with tenant_context(self.tenant): + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + self.assertTrue(serializer.is_valid(), serializer.errors) + serializer.save() + + inverse_rates = MonthlyExchangeRate.objects.filter( + base_currency="EUR", + target_currency="USD", + rate_type=RateType.STATIC, + ) + self.assertEqual(inverse_rates.count(), 3) + + expected_inverse = Decimal(1) / Decimal("0.870000000000000") + for rate in inverse_rates: + self.assertAlmostEqual(rate.exchange_rate, expected_inverse, places=15) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_create_does_not_overwrite_explicit_reverse_rate(self, mock_invalidate): + """Test that auto-generated inverse does not overwrite an explicit reverse StaticExchangeRate.""" + with tenant_context(self.tenant): + reverse_data = { + "base_currency": "EUR", + "target_currency": "USD", + "exchange_rate": "1.150000000000000", + "start_date": "2026-01-01", + "end_date": "2026-03-31", + } + reverse_serializer = StaticExchangeRateSerializer(data=reverse_data, context=self._make_request_context()) + self.assertTrue(reverse_serializer.is_valid(), reverse_serializer.errors) + reverse_serializer.save() + + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + self.assertTrue(serializer.is_valid(), serializer.errors) + serializer.save() + + inverse_rates = MonthlyExchangeRate.objects.filter( + base_currency="EUR", + target_currency="USD", + rate_type=RateType.STATIC, + ) + for rate in inverse_rates: + self.assertEqual( + rate.exchange_rate, + Decimal("1.150000000000000"), + "Auto-generated inverse should not overwrite explicit reverse rate", + ) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_explicit_forward_overwrites_auto_generated_inverse(self, mock_invalidate): + """Test that creating an explicit rate overwrites a previously auto-generated inverse.""" + with tenant_context(self.tenant): + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + self.assertTrue(serializer.is_valid(), serializer.errors) + serializer.save() + + expected_inverse = Decimal(1) / Decimal("0.870000000000000") + inverse_rates = MonthlyExchangeRate.objects.filter( + base_currency="EUR", target_currency="USD", rate_type=RateType.STATIC + ) + for rate in inverse_rates: + self.assertAlmostEqual(rate.exchange_rate, expected_inverse, places=15) + + explicit_reverse_data = { + "base_currency": "EUR", + "target_currency": "USD", + "exchange_rate": "1.150000000000000", + "start_date": "2026-01-01", + "end_date": "2026-03-31", + } + reverse_serializer = StaticExchangeRateSerializer( + data=explicit_reverse_data, context=self._make_request_context() + ) + self.assertTrue(reverse_serializer.is_valid(), reverse_serializer.errors) + reverse_serializer.save() + + inverse_rates = MonthlyExchangeRate.objects.filter( + base_currency="EUR", target_currency="USD", rate_type=RateType.STATIC + ) + for rate in inverse_rates: + self.assertEqual( + rate.exchange_rate, + Decimal("1.150000000000000"), + "Explicit forward should overwrite auto-generated inverse", + ) + + def test_delete_removes_inverse_monthly_rate(self): + """Test that deleting a static rate also removes its auto-generated inverse rows.""" + with tenant_context(self.tenant): + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + + self.assertTrue( + MonthlyExchangeRate.objects.filter( + base_currency="EUR", target_currency="USD", rate_type=RateType.STATIC + ).exists() + ) + + remove_static_and_backfill_dynamic( + instance.base_currency, + instance.target_currency, + instance.start_date, + instance.end_date, + ) + instance.delete() + + self.assertFalse( + MonthlyExchangeRate.objects.filter( + base_currency="EUR", target_currency="USD", rate_type=RateType.STATIC + ).exists() + ) + + @patch("cost_models.static_exchange_rate_serializer.invalidate_view_cache_for_tenant_and_all_source_types") + def test_delete_preserves_explicit_reverse_rate(self, mock_invalidate): + """Test that deleting USD->EUR does not remove EUR->USD rows from an explicit StaticExchangeRate.""" + with tenant_context(self.tenant): + reverse_data = { + "base_currency": "EUR", + "target_currency": "USD", + "exchange_rate": "1.150000000000000", + "start_date": "2026-01-01", + "end_date": "2026-03-31", + } + reverse_serializer = StaticExchangeRateSerializer(data=reverse_data, context=self._make_request_context()) + self.assertTrue(reverse_serializer.is_valid(), reverse_serializer.errors) + reverse_serializer.save() + + serializer = StaticExchangeRateSerializer(data=self.valid_data, context=self._make_request_context()) + self.assertTrue(serializer.is_valid(), serializer.errors) + forward_instance = serializer.save() + + remove_static_and_backfill_dynamic( + forward_instance.base_currency, + forward_instance.target_currency, + forward_instance.start_date, + forward_instance.end_date, + ) + forward_instance.delete() + + inverse_rates = MonthlyExchangeRate.objects.filter( + base_currency="EUR", target_currency="USD", rate_type=RateType.STATIC + ) + self.assertEqual(inverse_rates.count(), 3, "Explicit reverse rates should be preserved") + for rate in inverse_rates: + self.assertEqual(rate.exchange_rate, Decimal("1.150000000000000")) From c1bf96dc29908b610c70348481baf2f54f00deaf Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 7 May 2026 16:37:23 +0300 Subject: [PATCH 091/106] [COST-7252] Fix static exchange rate update to clean up old rates on date changes Previously, `remove_static_and_backfill_dynamic` was only called when the base or target currency changed. This left stale monthly rates when only `start_date` or `end_date` was modified. Co-authored-by: Cursor --- koku/cost_models/static_exchange_rate_serializer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index 5c01bc756d..e3f64e913e 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -116,7 +116,12 @@ def update(self, instance, validated_data): setattr(instance, attr, value) instance.save() - if old_base != instance.base_currency or old_target != instance.target_currency: + if ( + old_base != instance.base_currency + or old_target != instance.target_currency + or old_start != instance.start_date + or old_end != instance.end_date + ): remove_static_and_backfill_dynamic(old_base, old_target, old_start, old_end) upsert_static_monthly_rates(instance) From 2ff0317404ca4b802c59d4cd858fd151970a1e0d Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 7 May 2026 17:33:02 +0300 Subject: [PATCH 092/106] [COST-7252] Require authentication for currency endpoint The get_currency endpoint was open to unauthenticated requests. Change permission from AllowAny to IsAuthenticated to enforce proper access control. Co-authored-by: Cursor --- koku/api/currency/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index 10218a4040..b1731fea23 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -18,7 +18,7 @@ @api_view(("GET",)) -@permission_classes((permissions.AllowAny,)) +@permission_classes((permissions.IsAuthenticated,)) @renderer_classes([JSONRenderer] + api_settings.DEFAULT_RENDERER_CLASSES) def get_currency(request): """Get available currencies. From 2003b9bd2febd7aed02f29987ad44c94d0893056 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 7 May 2026 17:49:16 +0300 Subject: [PATCH 093/106] [COST-7252] Seed EnabledCurrency in migration instead of test runner Move the default enabled currencies into the migration as a data seed so they are available in all environments, not just tests. Co-authored-by: Cursor --- .../migrations/0014_constant_currency.py | 36 +++++++++++++++++++ koku/koku/koku_test_runner.py | 8 ----- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/koku/cost_models/migrations/0014_constant_currency.py b/koku/cost_models/migrations/0014_constant_currency.py index 3bc03e491d..1954079acc 100644 --- a/koku/cost_models/migrations/0014_constant_currency.py +++ b/koku/cost_models/migrations/0014_constant_currency.py @@ -10,6 +10,41 @@ from django.db import models +DEFAULT_ENABLED_CURRENCIES = ( + "AED", + "AUD", + "BRL", + "CAD", + "CHF", + "CNY", + "CZK", + "DKK", + "EUR", + "GBP", + "HKD", + "INR", + "JPY", + "NGN", + "NOK", + "NZD", + "SAR", + "SEK", + "SGD", + "TWD", + "USD", + "ZAR", +) + + +def seed_enabled_currencies(apps, schema_editor): + """Seed EnabledCurrency with the default set that was previously hardcoded.""" + EnabledCurrency = apps.get_model("cost_models", "EnabledCurrency") + EnabledCurrency.objects.bulk_create( + [EnabledCurrency(currency_code=code) for code in DEFAULT_ENABLED_CURRENCIES], + ignore_conflicts=True, + ) + + def seed_current_month(apps, schema_editor): """Seed MonthlyExchangeRate with current-month dynamic rates from ExchangeRateDictionary.""" ExchangeRateDictionary = apps.get_model("api", "ExchangeRateDictionary") @@ -101,5 +136,6 @@ class Migration(migrations.Migration): "unique_together": {("effective_date", "base_currency", "target_currency")}, }, ), + migrations.RunPython(code=seed_enabled_currencies, reverse_code=migrations.RunPython.noop), migrations.RunPython(code=seed_current_month, reverse_code=migrations.RunPython.noop), ] diff --git a/koku/koku/koku_test_runner.py b/koku/koku/koku_test_runner.py index 00a5dc4965..2266ff3b7b 100644 --- a/koku/koku/koku_test_runner.py +++ b/koku/koku/koku_test_runner.py @@ -13,13 +13,11 @@ from django.db import connections from django.test.runner import DiscoverRunner from django.test.utils import get_unique_databases_and_mirrors -from django_tenants.utils import schema_context from api.models import Customer from api.models import Provider from api.models import Tenant from api.report.test.util.model_bakery_loader import ModelBakeryDataLoader -from cost_models.models import EnabledCurrency from koku.env import ENVIRONMENT @@ -141,12 +139,6 @@ def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, paral account_id=account[0], org_id=account[2], schema_name=account[1] ) - _TEST_CURRENCIES = ("USD", "EUR", "JPY", "AUD", "GBP", "CAD") - for _schema in (KokuTestRunner.schema, "org2222222", "org3333333"): - with schema_context(_schema): - for code in _TEST_CURRENCIES: - EnabledCurrency.objects.get_or_create(currency_code=code) - except Exception as err: LOG.error(err) raise err From c62525d1f20c3190ea45ac33ec801d8f14a13de5 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Thu, 7 May 2026 18:13:44 +0300 Subject: [PATCH 094/106] [COST-7252] Add is_authenticated property to User model DRF permission classes check request.user.is_authenticated, which defaults to False on non-Django-auth models. Returning True is safe because User instances are only created after middleware authentication. Co-authored-by: Cursor --- koku/api/iam/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/koku/api/iam/models.py b/koku/api/iam/models.py index 6d8fd48d46..42f35a6ed3 100644 --- a/koku/api/iam/models.py +++ b/koku/api/iam/models.py @@ -71,6 +71,11 @@ def __init__(self, *args, **kwargs): self.identity_header = None self.beta = False + @property + def is_authenticated(self): + """Always True — User instances only exist after middleware authentication.""" + return True + class Meta: ordering = ["username"] From 3b3d51781d505442f4f716120dd88124a17df10d Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 11 May 2026 15:53:31 +0300 Subject: [PATCH 095/106] [COST-7252] Skip currencies with invalid exchange rate values Validate that exchange rate values are non-null and positive before storing them, preventing bad data from corrupting the rates table. Co-authored-by: Cursor --- koku/masu/celery/tasks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index 424b419ec3..96741090f2 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -293,6 +293,9 @@ def _fetch_and_store_exchange_rates(url): if not is_valid_iso_currency(curr_type): LOG.warning(f"Skipping unsupported currency {curr_type}") continue + if not value or value <= 0: + LOG.warning(f"Skipping currency {curr_type} with invalid rate {value}") + continue try: exchange = ExchangeRates.objects.get(currency_type=curr_type.lower()) LOG.info(f"Updating currency {curr_type} to {value}") From fe1f7a49cd421c73ed924c89e824f65fb2ce0014 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 11 May 2026 18:30:06 +0300 Subject: [PATCH 096/106] squash migrations into one --- .../0072_alter_exchangerates_currency_type.py | 2 +- .../0073_alter_exchangerates_currency_type.py | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 koku/api/migrations/0073_alter_exchangerates_currency_type.py diff --git a/koku/api/migrations/0072_alter_exchangerates_currency_type.py b/koku/api/migrations/0072_alter_exchangerates_currency_type.py index 9af285dfd2..c6415cc93f 100644 --- a/koku/api/migrations/0072_alter_exchangerates_currency_type.py +++ b/koku/api/migrations/0072_alter_exchangerates_currency_type.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="exchangerates", name="currency_type", - field=models.CharField(max_length=5), + field=models.CharField(blank=True, max_length=5), ), ] diff --git a/koku/api/migrations/0073_alter_exchangerates_currency_type.py b/koku/api/migrations/0073_alter_exchangerates_currency_type.py deleted file mode 100644 index f3ab5be469..0000000000 --- a/koku/api/migrations/0073_alter_exchangerates_currency_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.13 on 2026-04-30 10:05 -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [ - ("api", "0072_alter_exchangerates_currency_type"), - ] - - operations = [ - migrations.AlterField( - model_name="exchangerates", - name="currency_type", - field=models.CharField(blank=True, max_length=5), - ), - ] From 1da21f0a020b5d3e1bc3b76382d18d18d8fb3de8 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 18 May 2026 12:15:58 +0300 Subject: [PATCH 097/106] Conditionalize exchange rate error message based on CURRENCY_URL Only suggest enabling dynamic exchange rates when CURRENCY_URL is configured. On-prem deployments without a rate API get a simpler message pointing to static exchange rates only. Co-authored-by: Cursor --- koku/cost_models/exchange_rate_annotations.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 34a1b2cc76..21ea429aae 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # """Shared exchange rate annotation builders for query handlers and forecasts.""" +from django.conf import settings from django.db.models import DecimalField from django.db.models import OuterRef from django.db.models import Subquery @@ -19,11 +20,18 @@ class ExchangeRateNotFound(Exception): def __init__(self, target_currency): self.target_currency = target_currency - super().__init__( - f"No exchange rate available for {target_currency}. " - "Ask your administrator to configure static exchange rates " - "or enable dynamic exchange rates." - ) + if settings.CURRENCY_URL: + msg = ( + f"No exchange rate available for {target_currency}. " + "Ask your administrator to configure static exchange rates " + "or enable dynamic exchange rates." + ) + else: + msg = ( + f"No exchange rate available for {target_currency}. " + "Ask your administrator to configure static exchange rates." + ) + super().__init__(msg) def _build_monthly_rate_annotation(base_currency, target_currency): From f06954272a813b5b52700a3b1c3bd0945378dc35 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 18 May 2026 12:20:59 +0300 Subject: [PATCH 098/106] removed self.target_currency Co-authored-by: Cursor --- koku/cost_models/exchange_rate_annotations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 21ea429aae..0023685bf3 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # """Shared exchange rate annotation builders for query handlers and forecasts.""" + from django.conf import settings from django.db.models import DecimalField from django.db.models import OuterRef @@ -19,7 +20,6 @@ class ExchangeRateNotFound(Exception): """Raised when no exchange rate exists for a required currency pair.""" def __init__(self, target_currency): - self.target_currency = target_currency if settings.CURRENCY_URL: msg = ( f"No exchange rate available for {target_currency}. " @@ -91,7 +91,9 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - infra_exchange_rate: cloud bill currency (raw_currency column) """ cost_model_currency = Subquery( - CostModel.objects.filter(costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")),).values( + CostModel.objects.filter( + costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), + ).values( "currency" )[:1], ) From 2bb4ecfe33f9bfe7bad8633b00a0f53c49023d8c Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 18 May 2026 13:09:57 +0300 Subject: [PATCH 099/106] Warn when enabling a currency without a dynamic exchange rate Return a warning in the POST response when CURRENCY_URL is configured but the requested currency has no dynamic rate available, prompting the admin to configure a static rate. Also default CURRENCY_URL to None in on-prem mode since external rate APIs are typically unavailable. Co-authored-by: Cursor --- koku/api/currency/utils.py | 20 ++++++ koku/api/settings/currency_views.py | 7 +- .../cost_models/test/test_enabled_currency.py | 68 +++++++++++++++++-- koku/koku/settings.py | 4 +- 4 files changed, 93 insertions(+), 6 deletions(-) diff --git a/koku/api/currency/utils.py b/koku/api/currency/utils.py index 9ddee9fbee..589b984a77 100644 --- a/koku/api/currency/utils.py +++ b/koku/api/currency/utils.py @@ -4,6 +4,8 @@ # from decimal import Decimal +from django.conf import settings + from api.currency.models import ExchangeRateDictionary @@ -16,6 +18,24 @@ def build_exchange_dictionary(rates): return exchanged_rates +def get_missing_rate_warning(code): + """Return a warning string if CURRENCY_URL is configured but code has no dynamic rate.""" + if not settings.CURRENCY_URL: + return None + erd = ExchangeRateDictionary.objects.first() + if not erd or not erd.currency_exchange_dictionary: + return ( + f"No exchange rate data is available yet. " + f"You may need to configure a static rate for {code} before users can use it." + ) + if code not in erd.currency_exchange_dictionary: + return ( + f"{code} has no dynamic exchange rate available. " + f"You will need to configure a static rate before users can use it." + ) + return None + + def exchange_dictionary(rates): """Posts exchange rates dictionary to DB""" exchange_data = build_exchange_dictionary(rates) diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index c32bdab8ea..75480cc085 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -16,6 +16,7 @@ from api.common.permissions.settings_access import SettingsAccessPermission from api.currency.currencies import get_currency_info from api.currency.currencies import is_valid_iso_currency +from api.currency.utils import get_missing_rate_warning from cost_models.models import EnabledCurrency LOG = logging.getLogger(__name__) @@ -53,7 +54,11 @@ def post(self, request, *args, **kwargs): return error EnabledCurrency.objects.get_or_create(currency_code=code) LOG.info(log_json(msg="Currency enabled", currency=code)) - return Response(status=status.HTTP_204_NO_CONTENT) + + warning = get_missing_rate_warning(code) + if warning: + LOG.warning(log_json(msg="Currency enabled with warning", currency=code, warning=warning)) + return Response({"warning": warning}, status=status.HTTP_200_OK) @method_decorator(never_cache) def delete(self, request, *args, **kwargs): diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index 25f59bd2b8..92b833ba1a 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # """Tests for EnabledCurrency views.""" +from decimal import Decimal from unittest.mock import patch from django.urls import reverse @@ -10,6 +11,7 @@ from rest_framework import status from rest_framework.test import APIClient +from api.currency.models import ExchangeRateDictionary from api.iam.test.iam_test_case import IamTestCase from cost_models.models import EnabledCurrency @@ -28,7 +30,7 @@ def test_post_enables_currency(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() response = self.client.post(self._url("USD"), **self.headers) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) def test_delete_disables_currency(self): @@ -44,7 +46,7 @@ def test_post_enable_is_idempotent(self): EnabledCurrency.objects.all().delete() EnabledCurrency.objects.create(currency_code="USD") response = self.client.post(self._url("USD"), **self.headers) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(EnabledCurrency.objects.filter(currency_code="USD").count(), 1) def test_delete_disable_is_idempotent(self): @@ -60,7 +62,7 @@ def test_post_does_not_affect_other_currencies(self): EnabledCurrency.objects.create(currency_code="EUR") EnabledCurrency.objects.create(currency_code="GBP") response = self.client.post(self._url("USD"), **self.headers) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(EnabledCurrency.objects.filter(currency_code="EUR").exists()) self.assertTrue(EnabledCurrency.objects.filter(currency_code="GBP").exists()) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) @@ -73,7 +75,7 @@ def test_post_normalizes_to_uppercase(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() response = self.client.post(self._url("usd"), **self.headers) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(EnabledCurrency.objects.filter(currency_code="USD").exists()) def test_get_on_detail_returns_405(self): @@ -81,6 +83,64 @@ def test_get_on_detail_returns_405(self): response = self.client.get(self._url("USD"), **self.headers) self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + @patch("api.currency.utils.settings") + def test_post_returns_warning_when_no_dynamic_rate(self, mock_settings): + """When CURRENCY_URL is set but the currency has no dynamic rate, return a warning.""" + mock_settings.CURRENCY_URL = "https://example.com/rates" + ExchangeRateDictionary.objects.all().delete() + ExchangeRateDictionary.objects.create( + currency_exchange_dictionary={ + "USD": {"USD": Decimal(1), "EUR": Decimal("0.92")}, + "EUR": {"USD": Decimal("1.087"), "EUR": Decimal(1)}, + } + ) + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + response = self.client.post(self._url("JPY"), **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("warning", response.data) + self.assertIn("JPY", response.data["warning"]) + self.assertTrue(EnabledCurrency.objects.filter(currency_code="JPY").exists()) + + @patch("api.currency.utils.settings") + def test_post_returns_warning_when_no_exchange_data(self, mock_settings): + """When CURRENCY_URL is set but ExchangeRateDictionary is empty, return a warning.""" + mock_settings.CURRENCY_URL = "https://example.com/rates" + ExchangeRateDictionary.objects.all().delete() + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + response = self.client.post(self._url("EUR"), **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("warning", response.data) + self.assertIn("static rate", response.data["warning"]) + + @patch("api.currency.utils.settings") + def test_post_no_warning_when_dynamic_rate_exists(self, mock_settings): + """When CURRENCY_URL is set and the currency has a dynamic rate, no warning.""" + mock_settings.CURRENCY_URL = "https://example.com/rates" + ExchangeRateDictionary.objects.all().delete() + ExchangeRateDictionary.objects.create( + currency_exchange_dictionary={ + "USD": {"USD": Decimal(1), "EUR": Decimal("0.92")}, + "EUR": {"USD": Decimal("1.087"), "EUR": Decimal(1)}, + } + ) + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + response = self.client.post(self._url("EUR"), **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsNone(response.data["warning"]) + + @patch("api.currency.utils.settings") + def test_post_no_warning_when_currency_url_not_configured(self, mock_settings): + """When CURRENCY_URL is None (on-prem default), no warning.""" + mock_settings.CURRENCY_URL = None + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + response = self.client.post(self._url("JPY"), **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsNone(response.data["warning"]) + class EnabledCurrencyListViewTest(IamTestCase): """Tests for GET on settings/currency/enabled-currencies/.""" diff --git a/koku/koku/settings.py b/koku/koku/settings.py index e8176eb71f..67128a8bc6 100644 --- a/koku/koku/settings.py +++ b/koku/koku/settings.py @@ -174,7 +174,9 @@ MAX_GROUP_BY = ENVIRONMENT.int("MAX_GROUP_BY_OVERRIDE", default=3) ### Currency URL -CURRENCY_URL = ENVIRONMENT.get_value("CURRENCY_URL", default="https://open.er-api.com/v6/latest/USD") +CURRENCY_URL = ENVIRONMENT.get_value( + "CURRENCY_URL", default=None if ONPREM else "https://open.er-api.com/v6/latest/USD" +) ### End Middleware From ba70bbbd9cb2bb70441e256c9b38d1861e308379 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 18 May 2026 14:28:15 +0300 Subject: [PATCH 100/106] Fix pre-commit formatting in exchange_rate_annotations.py Co-authored-by: Cursor --- koku/cost_models/exchange_rate_annotations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 0023685bf3..47d5b270d5 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # """Shared exchange rate annotation builders for query handlers and forecasts.""" - from django.conf import settings from django.db.models import DecimalField from django.db.models import OuterRef @@ -91,9 +90,7 @@ def build_ocp_exchange_rate_annotation_dict(cost_units_key, target_currency): - infra_exchange_rate: cloud bill currency (raw_currency column) """ cost_model_currency = Subquery( - CostModel.objects.filter( - costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")), - ).values( + CostModel.objects.filter(costmodelmap__provider_uuid=OuterRef(OuterRef("source_uuid")),).values( "currency" )[:1], ) From 1210532cfdd06cff71f16a2d6c86ad2182d1e235 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 19 May 2026 13:15:23 +0300 Subject: [PATCH 101/106] Fix exchange rate error message logic Swap the error messages so that when CURRENCY_URL is configured, we only suggest static rates (dynamic is already available), and when it's not configured, we suggest both options. Co-authored-by: Cursor --- koku/cost_models/exchange_rate_annotations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/koku/cost_models/exchange_rate_annotations.py b/koku/cost_models/exchange_rate_annotations.py index 47d5b270d5..c3966462fc 100644 --- a/koku/cost_models/exchange_rate_annotations.py +++ b/koku/cost_models/exchange_rate_annotations.py @@ -22,13 +22,13 @@ def __init__(self, target_currency): if settings.CURRENCY_URL: msg = ( f"No exchange rate available for {target_currency}. " - "Ask your administrator to configure static exchange rates " - "or enable dynamic exchange rates." + "Ask your administrator to configure static exchange rates." ) else: msg = ( f"No exchange rate available for {target_currency}. " - "Ask your administrator to configure static exchange rates." + "Ask your administrator to configure static exchange rates " + "or enable dynamic exchange rates." ) super().__init__(msg) From 58d9cd01418a21527446378670bb9d60b7a63f03 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Wed, 20 May 2026 15:20:08 +0300 Subject: [PATCH 102/106] Warn admin when enabling a currency with no exchange rates Check MonthlyExchangeRate (the actual source of truth for reports) instead of ExchangeRateDictionary, and always warn regardless of whether CURRENCY_URL is configured. Co-authored-by: Cursor --- koku/api/currency/utils.py | 23 +++---- .../cost_models/test/test_enabled_currency.py | 68 ++++++------------- 2 files changed, 32 insertions(+), 59 deletions(-) diff --git a/koku/api/currency/utils.py b/koku/api/currency/utils.py index 589b984a77..c99aa9cf21 100644 --- a/koku/api/currency/utils.py +++ b/koku/api/currency/utils.py @@ -7,6 +7,7 @@ from django.conf import settings from api.currency.models import ExchangeRateDictionary +from cost_models.models import MonthlyExchangeRate def build_exchange_dictionary(rates): @@ -19,21 +20,19 @@ def build_exchange_dictionary(rates): def get_missing_rate_warning(code): - """Return a warning string if CURRENCY_URL is configured but code has no dynamic rate.""" - if not settings.CURRENCY_URL: + """Return a warning string if the currency has no exchange rates configured.""" + has_rate = MonthlyExchangeRate.objects.filter(target_currency=code).exists() + if has_rate: return None - erd = ExchangeRateDictionary.objects.first() - if not erd or not erd.currency_exchange_dictionary: + if settings.CURRENCY_URL: return ( - f"No exchange rate data is available yet. " - f"You may need to configure a static rate for {code} before users can use it." + f"No exchange rate available for {code}. " + f"You may need to configure a static rate before users can use it." ) - if code not in erd.currency_exchange_dictionary: - return ( - f"{code} has no dynamic exchange rate available. " - f"You will need to configure a static rate before users can use it." - ) - return None + return ( + f"No exchange rate available for {code}. " + f"Configure a static rate or enable dynamic exchange rates before users can use it." + ) def exchange_dictionary(rates): diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index 92b833ba1a..94c3026d9b 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # """Tests for EnabledCurrency views.""" +from datetime import date from decimal import Decimal from unittest.mock import patch @@ -11,9 +12,10 @@ from rest_framework import status from rest_framework.test import APIClient -from api.currency.models import ExchangeRateDictionary from api.iam.test.iam_test_case import IamTestCase from cost_models.models import EnabledCurrency +from cost_models.models import MonthlyExchangeRate +from cost_models.models import RateType class EnabledCurrencyDetailViewTest(IamTestCase): @@ -26,6 +28,16 @@ def setUp(self): def _url(self, code): return reverse("enabled-currencies-detail", kwargs={"code": code}) + def _create_rate(self, target_currency): + """Helper to create a MonthlyExchangeRate for the given target currency.""" + MonthlyExchangeRate.objects.create( + effective_date=date(2026, 1, 1), + base_currency="USD", + target_currency=target_currency, + exchange_rate=Decimal("1.000000000000000"), + rate_type=RateType.STATIC, + ) + def test_post_enables_currency(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() @@ -83,64 +95,26 @@ def test_get_on_detail_returns_405(self): response = self.client.get(self._url("USD"), **self.headers) self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - @patch("api.currency.utils.settings") - def test_post_returns_warning_when_no_dynamic_rate(self, mock_settings): - """When CURRENCY_URL is set but the currency has no dynamic rate, return a warning.""" - mock_settings.CURRENCY_URL = "https://example.com/rates" - ExchangeRateDictionary.objects.all().delete() - ExchangeRateDictionary.objects.create( - currency_exchange_dictionary={ - "USD": {"USD": Decimal(1), "EUR": Decimal("0.92")}, - "EUR": {"USD": Decimal("1.087"), "EUR": Decimal(1)}, - } - ) + def test_post_warns_no_rate(self): + """When no exchange rate exists for the currency, return a warning.""" with tenant_context(self.tenant): + MonthlyExchangeRate.objects.all().delete() EnabledCurrency.objects.all().delete() response = self.client.post(self._url("JPY"), **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("warning", response.data) - self.assertIn("JPY", response.data["warning"]) + self.assertIsNotNone(response.data["warning"]) self.assertTrue(EnabledCurrency.objects.filter(currency_code="JPY").exists()) - @patch("api.currency.utils.settings") - def test_post_returns_warning_when_no_exchange_data(self, mock_settings): - """When CURRENCY_URL is set but ExchangeRateDictionary is empty, return a warning.""" - mock_settings.CURRENCY_URL = "https://example.com/rates" - ExchangeRateDictionary.objects.all().delete() - with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - response = self.client.post(self._url("EUR"), **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("warning", response.data) - self.assertIn("static rate", response.data["warning"]) - - @patch("api.currency.utils.settings") - def test_post_no_warning_when_dynamic_rate_exists(self, mock_settings): - """When CURRENCY_URL is set and the currency has a dynamic rate, no warning.""" - mock_settings.CURRENCY_URL = "https://example.com/rates" - ExchangeRateDictionary.objects.all().delete() - ExchangeRateDictionary.objects.create( - currency_exchange_dictionary={ - "USD": {"USD": Decimal(1), "EUR": Decimal("0.92")}, - "EUR": {"USD": Decimal("1.087"), "EUR": Decimal(1)}, - } - ) + def test_post_no_warning_when_rate_exists(self): + """No warning when MonthlyExchangeRate has a row for the target currency.""" with tenant_context(self.tenant): + MonthlyExchangeRate.objects.all().delete() + self._create_rate("EUR") EnabledCurrency.objects.all().delete() response = self.client.post(self._url("EUR"), **self.headers) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIsNone(response.data["warning"]) - @patch("api.currency.utils.settings") - def test_post_no_warning_when_currency_url_not_configured(self, mock_settings): - """When CURRENCY_URL is None (on-prem default), no warning.""" - mock_settings.CURRENCY_URL = None - with tenant_context(self.tenant): - EnabledCurrency.objects.all().delete() - response = self.client.post(self._url("JPY"), **self.headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIsNone(response.data["warning"]) - class EnabledCurrencyListViewTest(IamTestCase): """Tests for GET on settings/currency/enabled-currencies/.""" From 0b52cbc191f2bdacbb511f1809bf84f787790186 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 25 May 2026 13:23:22 +0300 Subject: [PATCH 103/106] Add has_dynamic_rate flag to currency info response Expose whether each currency has a dynamic exchange rate in the ExchangeRates table, allowing the UI to indicate which currencies support live rate conversion. Co-authored-by: Cursor --- koku/api/currency/currencies.py | 7 +++- koku/api/currency/test/test_views.py | 32 +++++++++++++++--- .../cost_models/test/test_enabled_currency.py | 33 +++++++++++++++++-- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index e5ca93e9ab..e4d9b34e8f 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -16,6 +16,7 @@ from babel.numbers import UnknownCurrencyError from rest_framework import serializers +from api.currency.models import ExchangeRates from cost_models.models import EnabledCurrency _ISO_4217_CURRENCIES = get_global("all_currencies") @@ -51,7 +52,7 @@ def is_valid_iso_currency(code): def get_currency_info(code): - """Return a dict with code, name, symbol, and description for a currency. + """Return a dict with code, name, symbol, description, and dynamic rate availability. All metadata is resolved via babel at call time. Falls back to the code itself for currencies babel does not recognise. @@ -63,9 +64,13 @@ def get_currency_info(code): except UnknownCurrencyError: name = code symbol = code + + has_dynamic_rate = ExchangeRates.objects.filter(currency_type=code.lower()).exists() + return { "code": code, "name": name, "symbol": symbol, "description": f"{code} ({symbol}) - {name}", + "has_dynamic_rate": has_dynamic_rate, } diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index 56a58aec97..57d0a5bc4e 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -26,8 +26,20 @@ def setUp(self): @patch( "api.currency.view.get_currency_info", side_effect=lambda c: { - "USD": {"code": "USD", "name": "US Dollar", "symbol": "$", "description": "USD ($) - US Dollar"}, - "EUR": {"code": "EUR", "name": "Euro", "symbol": "€", "description": "EUR (€) - Euro"}, + "USD": { + "code": "USD", + "name": "US Dollar", + "symbol": "$", + "description": "USD ($) - US Dollar", + "has_dynamic_rate": True, + }, + "EUR": { + "code": "EUR", + "name": "Euro", + "symbol": "€", + "description": "EUR (€) - Euro", + "has_dynamic_rate": False, + }, }[c], ) def test_supported_currencies(self, _mock_display): @@ -41,8 +53,20 @@ def test_supported_currencies(self, _mock_display): data = response.data expected = [ - {"code": "EUR", "name": "Euro", "symbol": "€", "description": "EUR (€) - Euro"}, - {"code": "USD", "name": "US Dollar", "symbol": "$", "description": "USD ($) - US Dollar"}, + { + "code": "EUR", + "name": "Euro", + "symbol": "€", + "description": "EUR (€) - Euro", + "has_dynamic_rate": False, + }, + { + "code": "USD", + "name": "US Dollar", + "symbol": "$", + "description": "USD ($) - US Dollar", + "has_dynamic_rate": True, + }, ] self.assertEqual(data.get("data"), expected) diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index 94c3026d9b..8b79c10a7c 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -12,6 +12,7 @@ from rest_framework import status from rest_framework.test import APIClient +from api.currency.models import ExchangeRates from api.iam.test.iam_test_case import IamTestCase from cost_models.models import EnabledCurrency from cost_models.models import MonthlyExchangeRate @@ -127,8 +128,20 @@ def setUp(self): @patch( "api.settings.currency_views.get_currency_info", side_effect=lambda c: { - "USD": {"code": "USD", "name": "US Dollar", "symbol": "$", "description": "USD ($) - US Dollar"}, - "EUR": {"code": "EUR", "name": "Euro", "symbol": "\u20ac", "description": "EUR (\u20ac) - Euro"}, + "USD": { + "code": "USD", + "name": "US Dollar", + "symbol": "$", + "description": "USD ($) - US Dollar", + "has_dynamic_rate": True, + }, + "EUR": { + "code": "EUR", + "name": "Euro", + "symbol": "\u20ac", + "description": "EUR (\u20ac) - Euro", + "has_dynamic_rate": False, + }, }[c], ) def test_get_returns_enabled_currencies(self, _mock): @@ -144,6 +157,22 @@ def test_get_returns_enabled_currencies(self, _mock): self.assertEqual(data[0]["name"], "Euro") self.assertEqual(data[1]["symbol"], "$") + def test_get_has_dynamic_rate_flag(self): + """Test that has_dynamic_rate reflects ExchangeRates table.""" + with tenant_context(self.tenant): + EnabledCurrency.objects.all().delete() + EnabledCurrency.objects.create(currency_code="USD") + EnabledCurrency.objects.create(currency_code="EUR") + ExchangeRates.objects.all().delete() + ExchangeRates.objects.create(currency_type="usd", exchange_rate=1.0) + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.data["data"] + eur_entry = next(c for c in data if c["code"] == "EUR") + usd_entry = next(c for c in data if c["code"] == "USD") + self.assertTrue(usd_entry["has_dynamic_rate"]) + self.assertFalse(eur_entry["has_dynamic_rate"]) + def test_get_returns_empty_list_when_none_enabled(self): with tenant_context(self.tenant): EnabledCurrency.objects.all().delete() From 78c07f5505bb3d0389021a5383f6b36d5f1351bc Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 25 May 2026 15:06:34 +0300 Subject: [PATCH 104/106] Eliminate N+1 queries in currency info by batching dynamic rate lookup Extract get_dynamic_rate_currencies() to fetch all exchange rate codes in a single query, then pass the set into get_currency_info() instead of issuing a per-currency EXISTS query. Co-authored-by: Cursor --- koku/api/currency/currencies.py | 9 +++++++-- koku/api/currency/test/test_views.py | 2 +- koku/api/currency/view.py | 4 +++- koku/api/settings/currency_views.py | 4 +++- .../cost_models/static_exchange_rate_serializer.py | 14 ++++++-------- koku/cost_models/test/test_enabled_currency.py | 2 +- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/koku/api/currency/currencies.py b/koku/api/currency/currencies.py index e4d9b34e8f..4700edc4d4 100644 --- a/koku/api/currency/currencies.py +++ b/koku/api/currency/currencies.py @@ -51,7 +51,12 @@ def is_valid_iso_currency(code): return code.upper() in _ISO_4217_CURRENCIES -def get_currency_info(code): +def get_dynamic_rate_currencies(): + """Return the set of currency codes that have a dynamic exchange rate available.""" + return set(ExchangeRates.objects.values_list("currency_type", flat=True).distinct()) + + +def get_currency_info(code, dynamic_rate_codes): """Return a dict with code, name, symbol, description, and dynamic rate availability. All metadata is resolved via babel at call time. Falls back to the @@ -65,7 +70,7 @@ def get_currency_info(code): name = code symbol = code - has_dynamic_rate = ExchangeRates.objects.filter(currency_type=code.lower()).exists() + has_dynamic_rate = code.lower() in dynamic_rate_codes return { "code": code, diff --git a/koku/api/currency/test/test_views.py b/koku/api/currency/test/test_views.py index 57d0a5bc4e..eed5cc14de 100644 --- a/koku/api/currency/test/test_views.py +++ b/koku/api/currency/test/test_views.py @@ -25,7 +25,7 @@ def setUp(self): @patch( "api.currency.view.get_currency_info", - side_effect=lambda c: { + side_effect=lambda c, dynamic_rate_codes: { "USD": { "code": "USD", "name": "US Dollar", diff --git a/koku/api/currency/view.py b/koku/api/currency/view.py index b1731fea23..2f4c0d9656 100644 --- a/koku/api/currency/view.py +++ b/koku/api/currency/view.py @@ -13,6 +13,7 @@ from api.common.pagination import ListPaginator from api.currency.currencies import get_currency_info +from api.currency.currencies import get_dynamic_rate_currencies from api.currency.models import ExchangeRateDictionary from cost_models.models import EnabledCurrency @@ -28,7 +29,8 @@ def get_currency(request): computed at response time via babel. """ enabled_codes = EnabledCurrency.objects.values_list("currency_code", flat=True) - available = [get_currency_info(code) for code in sorted(enabled_codes)] + dynamic_codes = get_dynamic_rate_currencies() + available = [get_currency_info(code, dynamic_rate_codes=dynamic_codes) for code in sorted(enabled_codes)] return ListPaginator(available, request).paginated_response diff --git a/koku/api/settings/currency_views.py b/koku/api/settings/currency_views.py index 75480cc085..ba7ff65bdc 100644 --- a/koku/api/settings/currency_views.py +++ b/koku/api/settings/currency_views.py @@ -15,6 +15,7 @@ from api.common.pagination import ListPaginator from api.common.permissions.settings_access import SettingsAccessPermission from api.currency.currencies import get_currency_info +from api.currency.currencies import get_dynamic_rate_currencies from api.currency.currencies import is_valid_iso_currency from api.currency.utils import get_missing_rate_warning from cost_models.models import EnabledCurrency @@ -44,7 +45,8 @@ def get(self, request, *args, **kwargs): if "code" in kwargs: return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) enabled_codes = EnabledCurrency.objects.values_list("currency_code", flat=True) - available = [get_currency_info(code) for code in sorted(enabled_codes)] + dynamic_codes = get_dynamic_rate_currencies() + available = [get_currency_info(code, dynamic_rate_codes=dynamic_codes) for code in sorted(enabled_codes)] return ListPaginator(available, request).paginated_response @method_decorator(never_cache) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index e3f64e913e..e73db49fa9 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -11,6 +11,7 @@ from api.common import log_json from api.currency.currencies import get_currency_info +from api.currency.currencies import get_dynamic_rate_currencies from api.currency.currencies import is_valid_iso_currency from cost_models.models import EnabledCurrency from cost_models.models import StaticExchangeRate @@ -150,19 +151,16 @@ class CurrencyExchangeRateSerializer(serializers.Serializer): def build_grouped_response(cls, queryset): """Group exchange rates by base_currency and attach currency metadata + enabled flag.""" enabled_codes = set(EnabledCurrency.objects.values_list("currency_code", flat=True)) + dynamic_codes = get_dynamic_rate_currencies() grouped = {} for rate in queryset: code = rate.base_currency if code not in grouped: - info = get_currency_info(code) - grouped[code] = { - "code": info["code"], - "name": info["name"], - "symbol": info["symbol"], - "enabled": code in enabled_codes, - "exchange_rates": [], - } + info = get_currency_info(code, dynamic_codes) + info["enabled"] = code in enabled_codes + info["exchange_rates"] = [] + grouped[code] = info grouped[code]["exchange_rates"].append(rate) result = [] diff --git a/koku/cost_models/test/test_enabled_currency.py b/koku/cost_models/test/test_enabled_currency.py index 8b79c10a7c..347696b58c 100644 --- a/koku/cost_models/test/test_enabled_currency.py +++ b/koku/cost_models/test/test_enabled_currency.py @@ -127,7 +127,7 @@ def setUp(self): @patch( "api.settings.currency_views.get_currency_info", - side_effect=lambda c: { + side_effect=lambda c, dynamic_rate_codes: { "USD": { "code": "USD", "name": "US Dollar", From c21e1f3ac77d7bbf5177910e2fbc1efce637fcc3 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Mon, 25 May 2026 17:32:21 +0300 Subject: [PATCH 105/106] Simplify get_daily_currency_rates early-return logic Combine the None and empty-dict checks into a single falsy check, and remove the redundant early return when ExchangeRateDictionary is absent so tenant dynamic rates are always upserted. Co-authored-by: Cursor --- koku/masu/celery/tasks.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/koku/masu/celery/tasks.py b/koku/masu/celery/tasks.py index 3f5ce5624b..57c1eb1347 100644 --- a/koku/masu/celery/tasks.py +++ b/koku/masu/celery/tasks.py @@ -341,13 +341,10 @@ def get_daily_currency_rates(): return {} rate_metrics = _fetch_and_store_exchange_rates(url) - if rate_metrics is None: + if not rate_metrics: return {} erd = ExchangeRateDictionary.objects.first() - if not erd or not erd.currency_exchange_dictionary: - return rate_metrics - current_month = DateHelper().this_month_start.date() for tenant in Tenant.objects.exclude(schema_name="public"): _upsert_tenant_dynamic_exchange_rates(tenant.schema_name, erd.currency_exchange_dictionary, current_month) From edafa8997e9d22632586bc4ef2500856ffaa71a2 Mon Sep 17 00:00:00 2001 From: ELK4N4 Date: Tue, 26 May 2026 11:25:02 +0300 Subject: [PATCH 106/106] Validate exchange_rate is positive before computing inverse Prevents DivisionByZero in upsert_static_monthly_rates when exchange_rate is zero, and rejects negative rates at the API boundary. Co-authored-by: Cursor --- koku/cost_models/static_exchange_rate_serializer.py | 5 +++++ koku/cost_models/static_exchange_rate_utils.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/koku/cost_models/static_exchange_rate_serializer.py b/koku/cost_models/static_exchange_rate_serializer.py index e73db49fa9..f87fa27d04 100644 --- a/koku/cost_models/static_exchange_rate_serializer.py +++ b/koku/cost_models/static_exchange_rate_serializer.py @@ -53,6 +53,11 @@ def validate_target_currency(self, value): raise serializers.ValidationError(f"Invalid currency code: {value}") return value.upper() + def validate_exchange_rate(self, value): + if value <= 0: + raise serializers.ValidationError("exchange_rate must be greater than zero.") + return value + def validate_start_date(self, value): if value.day != 1: raise serializers.ValidationError("start_date must be the first day of a month.") diff --git a/koku/cost_models/static_exchange_rate_utils.py b/koku/cost_models/static_exchange_rate_utils.py index c6bac356a9..d190e45eb7 100644 --- a/koku/cost_models/static_exchange_rate_utils.py +++ b/koku/cost_models/static_exchange_rate_utils.py @@ -39,6 +39,8 @@ def upsert_static_monthly_rates(static_rate): is written only when no explicit StaticExchangeRate defines the reverse pair for that month, ensuring explicit user-defined rates always take precedence. """ + if static_rate.exchange_rate <= 0: + raise ValueError(f"exchange_rate must be positive, got {static_rate.exchange_rate}") inverse_rate = Decimal(1) / static_rate.exchange_rate for month_start in _iter_months(static_rate.start_date, static_rate.end_date):