From ce38ab787e743ec20ce071ac8515bd8f39eb8358 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Thu, 28 Mar 2024 08:30:26 -0400 Subject: [PATCH] fix: API usage alerting in production (#3507) Co-authored-by: Matthew Elwell Co-authored-by: Kim Gustyr --- api/app/pagination.py | 4 +- api/app_analytics/influxdb_wrapper.py | 5 +- .../identities/edge_identity_service.py | 7 +- api/edge_api/identities/models.py | 3 +- api/edge_api/identities/views.py | 6 +- .../dynamodb/wrappers/identity_wrapper.py | 7 +- api/integrations/flagsmith/client.py | 18 +- api/organisations/tasks.py | 10 + api/poetry.lock | 254 ++++++++++++------ api/pyproject.toml | 9 +- api/tests/conftest.py | 1 - .../flagsmith/test_unit_flagsmith_client.py | 2 +- .../test_unit_organisations_tasks.py | 51 ++++ .../mappers/test_unit_mappers_dynamodb.py | 2 + .../util/mappers/test_unit_mappers_engine.py | 38 +-- api/util/mappers/dynamodb.py | 4 +- 16 files changed, 288 insertions(+), 133 deletions(-) diff --git a/api/app/pagination.py b/api/app/pagination.py index ce39dbe2bc57..ba2d5b7d5221 100644 --- a/api/app/pagination.py +++ b/api/app/pagination.py @@ -4,7 +4,7 @@ from drf_yasg import openapi from drf_yasg.inspectors import PaginatorInspector -from flag_engine.identities.builders import build_identity_model +from flag_engine.identities.models import IdentityModel from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response @@ -75,7 +75,7 @@ def paginate_queryset(self, dynamo_queryset, request, view=None): ) return [ - build_identity_model(identity_document) + IdentityModel.model_validate(identity_document) for identity_document in dynamo_queryset["Items"] ] diff --git a/api/app_analytics/influxdb_wrapper.py b/api/app_analytics/influxdb_wrapper.py index 1779bfc4739c..e25d6164d0e4 100644 --- a/api/app_analytics/influxdb_wrapper.py +++ b/api/app_analytics/influxdb_wrapper.py @@ -336,8 +336,7 @@ def get_current_api_usage(organisation_id: int, date_range: str) -> int: ), drop_columns=("_start", "_stop", "_time"), extra='|> sum() \ - |> group() \ - |> sort(columns: ["_value"], desc: true) ', + |> sort(columns: ["_value"], desc: true) ', ) for result in results: @@ -346,7 +345,7 @@ def get_current_api_usage(organisation_id: int, date_range: str) -> int: return 0 # There should only be one matching result due to the - # group part of the query. + # sum part of the query. assert len(result.records) == 1 return result.records[0].get_value() diff --git a/api/edge_api/identities/edge_identity_service.py b/api/edge_api/identities/edge_identity_service.py index ef2b471f7c73..3c1edc453252 100644 --- a/api/edge_api/identities/edge_identity_service.py +++ b/api/edge_api/identities/edge_identity_service.py @@ -15,4 +15,9 @@ def get_edge_identity_overrides( environment_id=environment_id, feature_id=feature_id ) ) - return [IdentityOverrideV2.parse_obj(item) for item in override_items] + return [ + IdentityOverrideV2.model_validate( + {**item, "environment_id": str(item["environment_id"])} + ) + for item in override_items + ] diff --git a/api/edge_api/identities/models.py b/api/edge_api/identities/models.py index 7e12395999f4..85f7e087aa91 100644 --- a/api/edge_api/identities/models.py +++ b/api/edge_api/identities/models.py @@ -4,7 +4,6 @@ from django.db.models import Prefetch, Q from flag_engine.features.models import FeatureStateModel -from flag_engine.identities.builders import build_identity_model from flag_engine.identities.models import IdentityFeaturesList, IdentityModel from api_keys.models import MasterAPIKey @@ -32,7 +31,7 @@ def __init__(self, engine_identity_model: IdentityModel): @classmethod def from_identity_document(cls, identity_document: dict) -> "EdgeIdentity": - return EdgeIdentity(build_identity_model(identity_document)) + return EdgeIdentity(IdentityModel.model_validate(identity_document)) @property def django_id(self) -> int: diff --git a/api/edge_api/identities/views.py b/api/edge_api/identities/views.py index f0045901a9a1..09d900cce45a 100644 --- a/api/edge_api/identities/views.py +++ b/api/edge_api/identities/views.py @@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema -from flag_engine.identities.builders import build_identity_model +from flag_engine.identities.models import IdentityModel from flag_engine.identities.traits.models import TraitModel from pyngo import drf_error_details from rest_framework import status, viewsets @@ -166,7 +166,7 @@ def perform_destroy(self, instance): ) @action(detail=True, methods=["get"], url_path="list-traits") def get_traits(self, request, *args, **kwargs): - identity = build_identity_model(self.get_object()) + identity = IdentityModel.model_validate(self.get_object()) data = [trait.dict() for trait in identity.identity_traits] return Response(data=data, status=status.HTTP_200_OK) @@ -180,7 +180,7 @@ def update_traits(self, request, *args, **kwargs): environment = self.get_environment_from_request() if not environment.project.organisation.persist_trait_data: raise TraitPersistenceError() - identity = build_identity_model(self.get_object()) + identity = IdentityModel.model_validate(self.get_object()) try: trait = TraitModel(**request.data) except pydantic.ValidationError as validation_error: diff --git a/api/environments/dynamodb/wrappers/identity_wrapper.py b/api/environments/dynamodb/wrappers/identity_wrapper.py index 6351419d0fc7..9bf91c6d9516 100644 --- a/api/environments/dynamodb/wrappers/identity_wrapper.py +++ b/api/environments/dynamodb/wrappers/identity_wrapper.py @@ -6,8 +6,7 @@ from boto3.dynamodb.conditions import Key from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from flag_engine.environments.builders import build_environment_model -from flag_engine.identities.builders import build_identity_model +from flag_engine.environments.models import EnvironmentModel from flag_engine.identities.models import IdentityModel from flag_engine.segments.evaluator import get_identity_segments from rest_framework.exceptions import NotFound @@ -153,11 +152,11 @@ def get_segment_ids( raise ValueError("Must provide one of identity_pk or identity_model.") with suppress(ObjectDoesNotExist): - identity = identity_model or build_identity_model( + identity = identity_model or IdentityModel.model_validate( self.get_item_from_uuid(identity_pk) ) environment_wrapper = DynamoEnvironmentWrapper() - environment = build_environment_model( + environment = EnvironmentModel.model_validate( environment_wrapper.get_item(identity.environment_api_key) ) segments = get_identity_segments(environment, identity) diff --git a/api/integrations/flagsmith/client.py b/api/integrations/flagsmith/client.py index 3d88a1e5211f..ffb518592dba 100644 --- a/api/integrations/flagsmith/client.py +++ b/api/integrations/flagsmith/client.py @@ -8,9 +8,6 @@ environment_flags = get_client().get_environment_flags() identity_flags = get_client().get_identity_flags() ``` - -Possible extensions: - - Allow for multiple clients? """ import typing @@ -22,14 +19,19 @@ from integrations.flagsmith.exceptions import FlagsmithIntegrationError from integrations.flagsmith.flagsmith_service import ENVIRONMENT_JSON_PATH -_flagsmith_client: typing.Optional[Flagsmith] = None +_flagsmith_clients: dict[str, Flagsmith] = {} -def get_client() -> Flagsmith: - global _flagsmith_client +def get_client(name: str = "default", local_eval: bool = False) -> Flagsmith: + global _flagsmith_clients - if not _flagsmith_client: - _flagsmith_client = Flagsmith(**_get_client_kwargs()) + try: + _flagsmith_client = _flagsmith_clients[name] + except (KeyError, TypeError): + kwargs = _get_client_kwargs() + kwargs["enable_local_evaluation"] = local_eval + _flagsmith_client = Flagsmith(**kwargs) + _flagsmith_clients[name] = _flagsmith_client return _flagsmith_client diff --git a/api/organisations/tasks.py b/api/organisations/tasks.py index 5d3581868f1f..279894ec3b2d 100644 --- a/api/organisations/tasks.py +++ b/api/organisations/tasks.py @@ -8,6 +8,7 @@ from django.template.loader import render_to_string from django.utils import timezone +from integrations.flagsmith.client import get_client from organisations import subscription_info_cache from organisations.models import ( OranisationAPIUsageNotification, @@ -164,12 +165,21 @@ def _handle_api_usage_notifications(organisation: Organisation): def handle_api_usage_notifications(): + flagsmith_client = get_client("local", local_eval=True) + for organisation in Organisation.objects.filter( subscription_information_cache__current_billing_term_starts_at__isnull=False, subscription_information_cache__current_billing_term_ends_at__isnull=False, ).select_related( "subscription_information_cache", ): + feature_enabled = flagsmith_client.get_identity_flags( + f"org.{organisation.id}.{organisation.name}", + traits={"organisation_id": organisation.id}, + ).is_feature_enabled("api_usage_alerting") + if not feature_enabled: + continue + try: _handle_api_usage_notifications(organisation) except RuntimeError: diff --git a/api/poetry.lock b/api/poetry.lock index 8709849faaa8..0e32838a904a 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -124,6 +124,17 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "appdirs" version = "1.4.4" @@ -178,8 +189,8 @@ files = [ lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, + {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, ] [[package]] @@ -802,8 +813,8 @@ openapi-spec-validator = ">=0.2.8,<=0.5.7" packaging = "*" prance = ">=0.18.2" pydantic = [ - {version = ">=1.9.0,<3.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.10.0,<3.0", extras = ["email"], markers = "python_version >= \"3.11\" and python_version < \"4.0\""}, + {version = ">=1.9.0,<3.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, ] PySnooper = ">=0.4.1,<2.0.0" toml = ">=0.10.0,<1.0.0" @@ -1480,33 +1491,35 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p [[package]] name = "flagsmith" -version = "3.4.0" +version = "3.6.0" description = "Flagsmith Python SDK" optional = false -python-versions = ">=3.7.0,<4" +python-versions = ">=3.8.1,<4" files = [ - {file = "flagsmith-3.4.0.tar.gz", hash = "sha256:247ed4a225a5017bf805f137496dd5760361582c338b689db59e6c112e6e9281"}, + {file = "flagsmith-3.6.0.tar.gz", hash = "sha256:b189bea8def2a01f2fc2c53a71dcf917ed088a68d8478f7aed4575c488782632"}, ] [package.dependencies] -flagsmith-flag-engine = ">=4.0.0,<5.0.0" +flagsmith-flag-engine = ">=5.1.0,<6.0.0" +pytz = ">=2023.4,<2024.0" requests = ">=2.27.1,<3.0.0" requests-futures = ">=1.0.0,<2.0.0" +sseclient-py = ">=1.8.0,<2.0.0" [[package]] name = "flagsmith-flag-engine" -version = "4.1.0" +version = "5.1.1" description = "Flag engine for the Flagsmith API." optional = false python-versions = "*" files = [ - {file = "flagsmith-flag-engine-4.1.0.tar.gz", hash = "sha256:09ad8b0cf22c420830db4c6c198088b72ef0b1ed5fe5fea2c7d25c3e0c126e9c"}, + {file = "flagsmith-flag-engine-5.1.1.tar.gz", hash = "sha256:a97d001ac50fcddee273a25d8c88442b33797fde5b4d657f3e83e1493aa4f536"}, ] [package.dependencies] -pydantic = ">=1.10.8,<2" -pydantic-collections = ">=0.4.0,<1" -semver = "2.13.0" +pydantic = ">=2.3.0,<3" +pydantic-collections = ">=0.5.1,<1" +semver = ">=3.0.1" [[package]] name = "flagsmith-ldap" @@ -2935,70 +2948,129 @@ files = [ [[package]] name = "pydantic" -version = "1.10.13" -description = "Data validation and settings management using python type hints" +version = "2.6.4" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, - {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, - {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, - {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, - {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, - {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, - {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, - {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, - {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, - {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, -] - -[package.dependencies] -email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} -typing-extensions = ">=4.2.0" + {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, + {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-collections" -version = "0.4.1" +version = "0.5.4" description = "Collections of pydantic models" optional = false python-versions = "*" files = [ - {file = "pydantic-collections-0.4.1.tar.gz", hash = "sha256:a9bbfe927c561ec9a15639f0f715ea9d905baebfe84132caece5aacb8203eb7d"}, - {file = "pydantic_collections-0.4.1-py3-none-any.whl", hash = "sha256:73d7ec8f3b7cc65e201785b343dab04d256c4cbdc5801fd76f59870d32e9a0e0"}, + {file = "pydantic-collections-0.5.4.tar.gz", hash = "sha256:5bce65519456b4829f918c2456d58aac3620a866603461a702aafffe08845966"}, + {file = "pydantic_collections-0.5.4-py3-none-any.whl", hash = "sha256:5d107170c89fb17de229f5e8c4b4355af27594444fd0f93086048ccafa69238b"}, +] + +[package.dependencies] +pydantic = ">=1.8.2,<3.0" +typing-extensions = ">=4.7.1" + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] -pydantic = ">=1.8.2,<2.0" +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyflakes" @@ -3060,8 +3132,8 @@ files = [ astroid = ">=2.14.2,<=2.16.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, + {version = ">=0.2", markers = "python_version < \"3.11\""}, ] isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" @@ -3086,23 +3158,23 @@ files = [ [[package]] name = "pyngo" -version = "1.6.0" +version = "2.0.1" description = "Pydantic Package for Adding Models into a Django or Django Rest Framework Project ✨" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "pyngo-1.6.0-py3-none-any.whl", hash = "sha256:9a32b171fa285e1d41b725ef59cadb9f808839aba4606a0c75deaad8267051e9"}, - {file = "pyngo-1.6.0.tar.gz", hash = "sha256:26f0b178ddacfef49c7925d14d1f9f6aedc0113491e5b6865fe90f1f3c0f672f"}, + {file = "pyngo-2.0.1-py3-none-any.whl", hash = "sha256:1f69b383d0eab613bb571729b19cbcd3c410a9ebe19f4239a7d7b2d95d85f64b"}, + {file = "pyngo-2.0.1.tar.gz", hash = "sha256:afe347920300e1dbeca23bd76367dc84c631295a0188bc79bcb98760de772132"}, ] [package.dependencies] -django = ">=3.2.0,<5.0.0" -pydantic = ">=1.8.2,<2.0.0" -typing-extensions = ">=3.7.4,<4.6.0" +django = ">=3.2.0,<6.0.0" +pydantic = ">=2.5.3" +typing-extensions = ">=3.7.4,<4.10.0" [package.extras] -lint = ["mypy (==1.1.1)", "pre-commit (==3.2.1)"] -test = ["codecov (==2.1.12)", "django-stubs", "pytest (==7.2.2)", "pytest-asyncio (==0.21.0)", "pytest-cov (==4.0.0)", "pytest-pretty"] +lint = ["mypy (==1.8.0)", "pre-commit (==3.6.0)", "ruff (==0.1.14)"] +test = ["django-stubs", "pytest (==7.4.4)", "pytest-asyncio (==0.23.3)", "pytest-cov (==4.1.0)", "pytest-pretty"] [[package]] name = "pyopenssl" @@ -3449,13 +3521,13 @@ postgresql = ["psycopg2"] [[package]] name = "pytz" -version = "2023.3" +version = "2023.4" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pytz-2023.4-py2.py3-none-any.whl", hash = "sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a"}, + {file = "pytz-2023.4.tar.gz", hash = "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40"}, ] [[package]] @@ -3506,7 +3578,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -3818,13 +3889,13 @@ test = ["flake8 (==3.7.9)", "mock (==2.0.0)", "pylint (==2.8.0)"] [[package]] name = "semver" -version = "2.13.0" -description = "Python helper for Semantic Versioning (http://semver.org/)" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" files = [ - {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, - {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, ] [[package]] @@ -4113,6 +4184,17 @@ dev = ["build", "flake8"] doc = ["sphinx"] test = ["pytest", "pytest-cov"] +[[package]] +name = "sseclient-py" +version = "1.8.0" +description = "SSE client for Python" +optional = false +python-versions = "*" +files = [ + {file = "sseclient-py-1.8.0.tar.gz", hash = "sha256:c547c5c1a7633230a38dc599a21a2dc638f9b5c297286b48b46b935c71fac3e8"}, + {file = "sseclient_py-1.8.0-py2.py3-none-any.whl", hash = "sha256:4ecca6dc0b9f963f8384e9d7fd529bf93dd7d708144c4fb5da0e0a1a926fee83"}, +] + [[package]] name = "toml" version = "0.10.2" @@ -4177,13 +4259,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] @@ -4543,4 +4625,4 @@ requests = ">=2.7,<3.0" [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "7ca1a719ad30f6236cd31b9007742df9f0a056e1969d62876987247b6d43b755" +content-hash = "32fbb6d66e409a78848a6095939889557bfb40d3f6e8bfb1f9c1ef10339bdb7b" diff --git a/api/pyproject.toml b/api/pyproject.toml index 9e86eff8399d..6be8dabd3e89 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -79,11 +79,10 @@ environs = "~9.2.0" django-lifecycle = "~1.0.0" drf-writable-nested = "~0.6.2" django-filter = "~2.4.0" -flagsmith-flag-engine = "^4.1.0" +flagsmith-flag-engine = "^5.1.1" boto3 = "~1.28.78" slack-sdk = "~3.9.0" asgiref = "~3.5.0" -semver = "~2.13.0" opencensus-ext-azure = "~1.1.4" opencensus-ext-django = "~0.7.6" djangorestframework-api-key = "~2.2.0" @@ -99,9 +98,9 @@ influxdb-client = "~1.28.0" django-ordered-model = "~3.4.1" django-ses = "~3.5.0" django-axes = "~5.32.0" -pydantic = "~1.10.9" -pyngo = "~1.6.0" -flagsmith = "^3.4.0" +pydantic = "^2.3.0" +pyngo = "~2.0.1" +flagsmith = "^3.6.0" python-gnupg = "^0.5.1" django-redis = "^5.4.0" hubspot-api-client = "^8.2.1" diff --git a/api/tests/conftest.py b/api/tests/conftest.py index ac7d67a7af0a..00d7a393108d 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -92,6 +92,5 @@ def dynamo_environment_wrapper( flagsmith_environment_table: Table, ) -> DynamoEnvironmentWrapper: wrapper = DynamoEnvironmentWrapper() - wrapper.table_name = flagsmith_environment_table.name return wrapper diff --git a/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py b/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py index 1e7450c08cf2..5cbf659c0b0b 100644 --- a/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py +++ b/api/tests/unit/integrations/flagsmith/test_unit_flagsmith_client.py @@ -12,7 +12,7 @@ @pytest.fixture(autouse=True) def reset_globals(mocker: MockerFixture) -> None: - mocker.patch("integrations.flagsmith.client._flagsmith_client", None) + mocker.patch("integrations.flagsmith.client._flagsmith_clients", {}) yield diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index 86577b979cbb..437e9841ce55 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -1,5 +1,6 @@ import uuid from datetime import timedelta +from unittest.mock import MagicMock import pytest from django.core.mail.message import EmailMultiAlternatives @@ -227,6 +228,46 @@ def test_send_org_subscription_cancelled_alert(db: None, mocker: MockerFixture) ) +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") +def test_handle_api_usage_notifications_when_feature_flag_is_off( + mocker: MockerFixture, + organisation: Organisation, + mailoutbox: list[EmailMultiAlternatives], +) -> None: + # Given + now = timezone.now() + OrganisationSubscriptionInformationCache.objects.create( + organisation=organisation, + allowed_seats=10, + allowed_projects=3, + allowed_30d_api_calls=100, + chargebee_email="test@example.com", + current_billing_term_starts_at=now - timedelta(days=45), + current_billing_term_ends_at=now + timedelta(days=320), + ) + mock_api_usage = mocker.patch( + "organisations.tasks.get_current_api_usage", + ) + get_client_mock = mocker.patch("organisations.tasks.get_client") + client_mock = MagicMock() + get_client_mock.return_value = client_mock + client_mock.get_identity_flags.return_value.is_feature_enabled.return_value = False + + # When + handle_api_usage_notifications() + + # Then + mock_api_usage.assert_not_called() + + assert len(mailoutbox) == 0 + assert ( + OranisationAPIUsageNotification.objects.filter( + organisation=organisation, + ).count() + == 0 + ) + + @pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_handle_api_usage_notifications_below_100( mocker: MockerFixture, @@ -248,6 +289,11 @@ def test_handle_api_usage_notifications_below_100( "organisations.tasks.get_current_api_usage", ) mock_api_usage.return_value = 91 + get_client_mock = mocker.patch("organisations.tasks.get_client") + client_mock = MagicMock() + get_client_mock.return_value = client_mock + client_mock.get_identity_flags.return_value.is_feature_enabled.return_value = True + assert not OranisationAPIUsageNotification.objects.filter( organisation=organisation, ).exists() @@ -336,6 +382,11 @@ def test_handle_api_usage_notifications_above_100( ) mock_api_usage.return_value = 105 + get_client_mock = mocker.patch("organisations.tasks.get_client") + client_mock = MagicMock() + get_client_mock.return_value = client_mock + client_mock.get_identity_flags.return_value.is_feature_enabled.return_value = True + assert not OranisationAPIUsageNotification.objects.filter( organisation=organisation, ).exists() diff --git a/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py b/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py index fd414f6828d1..2492b1d47c76 100644 --- a/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py +++ b/api/tests/unit/util/mappers/test_unit_mappers_dynamodb.py @@ -50,6 +50,7 @@ def test_map_environment_to_environment_document__call_expected( "multivariate_feature_state_values": [], } ], + "identity_overrides": [], "heap_config": None, "hide_disabled_flags": None, "hide_sensitive_data": False, @@ -153,6 +154,7 @@ def test_map_environment_to_environment_v2_document__call_expected( "allow_client_traits": True, "amplitude_config": None, "dynatrace_config": None, + "identity_overrides": [], "feature_states": [ { "django_id": Decimal(feature_state.pk), diff --git a/api/tests/unit/util/mappers/test_unit_mappers_engine.py b/api/tests/unit/util/mappers/test_unit_mappers_engine.py index 1f1295599ea6..9c6e364036b4 100644 --- a/api/tests/unit/util/mappers/test_unit_mappers_engine.py +++ b/api/tests/unit/util/mappers/test_unit_mappers_engine.py @@ -16,7 +16,11 @@ MultivariateFeatureOptionModel, MultivariateFeatureStateValueModel, ) -from flag_engine.identities.models import IdentityModel, TraitModel +from flag_engine.identities.models import ( + IdentityFeaturesList, + IdentityModel, + TraitModel, +) from flag_engine.organisations.models import OrganisationModel from flag_engine.projects.models import ProjectModel from flag_engine.segments.models import ( @@ -451,21 +455,23 @@ def test_map_identity_to_engine__return_expected( identifier=identity.identifier, environment_api_key=environment_api_key, created_date=identity.created_date, - identity_features=[ - FeatureStateModel( - feature=FeatureModel( - id=feature.pk, - name=feature.name, - type=feature.type, - ), - enabled=identity_featurestate.enabled, - django_id=identity_featurestate.pk, - feature_segment=identity_featurestate.feature_segment, - featurestate_uuid=identity_featurestate.uuid, - feature_state_value=identity_featurestate.get_feature_state_value(), - multivariate_feature_state_values=[], - ) - ], + identity_features=IdentityFeaturesList( + [ + FeatureStateModel( + feature=FeatureModel( + id=feature.pk, + name=feature.name, + type=feature.type, + ), + enabled=identity_featurestate.enabled, + django_id=identity_featurestate.pk, + feature_segment=identity_featurestate.feature_segment, + featurestate_uuid=identity_featurestate.uuid, + feature_state_value=identity_featurestate.get_feature_state_value(), + multivariate_feature_state_values=[], + ) + ] + ), identity_traits=[ TraitModel( trait_key=trait.trait_key, diff --git a/api/util/mappers/dynamodb.py b/api/util/mappers/dynamodb.py index 2e9c293da51d..06036153be4b 100644 --- a/api/util/mappers/dynamodb.py +++ b/api/util/mappers/dynamodb.py @@ -77,10 +77,12 @@ def map_environment_api_key_to_environment_api_key_document( def map_engine_identity_to_identity_document( engine_identity: "IdentityModel", ) -> Document: - return { + response = { field_name: _map_value_to_document_value(value) for field_name, value in engine_identity } + response["composite_key"] = engine_identity.composite_key + return response def map_identity_to_identity_document(