diff --git a/.github/workflows/manual-e2e-tests.yml b/.github/workflows/manual-e2e-tests.yml new file mode 100644 index 000000000000..7bf9fec3799e --- /dev/null +++ b/.github/workflows/manual-e2e-tests.yml @@ -0,0 +1,38 @@ +name: 'Manual E2E tests' + +on: + workflow_dispatch: + inputs: + e2e-token: + description: 'The authentication token used by the E2E process' + required: true + e2e-concurrency: + description: 'The concurrency value to use when running the E2E process' + default: 3 + type: number + api-url: + description: 'Which database service to use to run the API against' + default: 'https://api.flagsmith.com/api/v1/' + +jobs: + run-e2e-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + cache: npm + node-version-file: frontend/.nvmrc + cache-dependency-path: frontend/package-lock.json + + - name: Run tests + working-directory: frontend + env: + E2E_TEST_AUTH_TOKEN: ${{ inputs.e2e-token }} + FLAGSMITH_API_URL: ${{ inputs.api-url }} + E2E_CONCURRENCY: ${{ inputs.e2e-concurrency }} + run: | + npm ci + npm run env + npm run test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82ff55d1f8f2..52254fc999dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,13 +26,13 @@ repos: - id: check-json - id: check-toml - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + - repo: local hooks: - id: prettier + name: prettier exclude: ^(frontend/|CHANGELOG.md|.github/docker_build_comment_template.md) - additional_dependencies: - - prettier@3.3.3 # SEE: https://github.com/pre-commit/pre-commit/issues/3133 + language: system + entry: npx prettier --check - repo: https://github.com/python-poetry/poetry rev: 1.8.0 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1670b75b9e3a..7beabe708fd1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.154.0" + ".": "2.155.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b999b0a7f54..2cc89e66f633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [2.155.0](https://github.com/Flagsmith/flagsmith/compare/v2.154.0...v2.155.0) (2024-12-03) + + +### Features + +* Create change requests for segments ([#4265](https://github.com/Flagsmith/flagsmith/issues/4265)) ([355f4e2](https://github.com/Flagsmith/flagsmith/commit/355f4e2827dc0a0cba169a320c7a0db8b522089e)) +* **environment/views:** Add get-by-uuid action ([#4875](https://github.com/Flagsmith/flagsmith/issues/4875)) ([db8433b](https://github.com/Flagsmith/flagsmith/commit/db8433ba0a307b3e27f078aadf94d6ef4e717391)) +* **org/view:** Add api to get organisation by uuid ([#4878](https://github.com/Flagsmith/flagsmith/issues/4878)) ([06ed466](https://github.com/Flagsmith/flagsmith/commit/06ed466864d723ac43bad9ec8fa738a8e9d0812d)) + + +### Bug Fixes + +* Environment Ready Checker ([#4865](https://github.com/Flagsmith/flagsmith/issues/4865)) ([2392222](https://github.com/Flagsmith/flagsmith/commit/23922223d4367792b196d798cb2571dc8f589ee1)) +* prevent enabling versioning from affecting scheduled change requests ([#4872](https://github.com/Flagsmith/flagsmith/issues/4872)) ([c7aa30b](https://github.com/Flagsmith/flagsmith/commit/c7aa30bb217deca7ea6a0b0657d1a08076f2ab44)) + ## [2.154.0](https://github.com/Flagsmith/flagsmith/compare/v2.153.0...v2.154.0) (2024-11-21) diff --git a/api/Makefile b/api/Makefile index eca334728e26..5c5fd7ea3e12 100644 --- a/api/Makefile +++ b/api/Makefile @@ -12,7 +12,7 @@ POETRY_VERSION ?= 1.8.3 GUNICORN_LOGGER_CLASS ?= util.logging.GunicornJsonCapableLogger SAML_REVISION ?= v1.6.4 -RBAC_REVISION ?= v0.11.0 +RBAC_REVISION ?= v0.11.1 -include .env-local -include $(DOTENV_OVERRIDE_FILE) diff --git a/api/app/settings/common.py b/api/app/settings/common.py index aaf7c9f1b265..eb1b5cefea89 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -1274,3 +1274,27 @@ # subscriptions created before this date full audit log and versioning # history. VERSIONING_RELEASE_DATE = env.date("VERSIONING_RELEASE_DATE", default=None) + +SUBSCRIPTION_LICENCE_PUBLIC_KEY = env.str( + "SUBSCRIPTION_LICENCE_PUBLIC_KEY", + """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs1H23Xv1IlhyTbUP9Z4e +zN3t6oa97ybLufhqSRCoPxHWAY/pqqjdwiC00AnRbL/guDi1FLPEkLza2gAKfU+f +04SsNTfYL5MTPnaFtf+B+hlYmlrT1C6n05t+uQW2OQm6mWoqBssmoyR8T5FXfBls +FrT8dsZg5XG7JaWAyGbbVscHrXHXqVcLbFGO8CcO2BG2whl+7hzm4edNCsxLJqmN +uASR9KtntdulkRar0A9x+hAQUlrDKv77nMMdljNIqkcCcWrbhiDoTVCDbE99mhMq +LeC/+C54/ZiCb3r9woq/kpsbRj0Ys2b4czfjWioXooSxA0w3BE6/lV0+hVltjRO6 +5QIDAQAB +-----END PUBLIC KEY----- +""", +) + +# For the matching private key to the public key added above +# search for "Flagsmith licence private key" in Bitwarden. +SUBSCRIPTION_LICENCE_PRIVATE_KEY = env.str("SUBSCRIPTION_LICENCE_PRIVATE_KEY", None) + +LICENSING_INSTALLED = importlib.util.find_spec("licensing") is not None + +if LICENSING_INSTALLED: # pragma: no cover + INSTALLED_APPS.append("licensing") diff --git a/api/app/settings/test.py b/api/app/settings/test.py index 1fb33fe5c0a4..3faad310c58f 100644 --- a/api/app/settings/test.py +++ b/api/app/settings/test.py @@ -18,3 +18,46 @@ RETRY_WEBHOOKS = True INFLUXDB_BUCKET = "test_bucket" + +SUBSCRIPTION_LICENCE_PRIVATE_KEY = """ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0o6Q+J6ArJZ2x +RyZQ5e9ue6dB4bgH7I7DYYb9t9eIb55z0vZZWVLLmIr+ngCfCxIePqCclrAen9gr +rCRhyAXD+XZYjRP0w2wlqA367HJXbti1adXnQnM4QXITNJhRnGoqiRVx7vQ/Klup ++yMBJOU4IkkSsQaAgp0eTdPlGlA+KAfCH39rsqIHNXuS1qfspI2RyaR6130NvR6D +4p07XJls1AYOs8xphdWl8b4hzbJTvC0IqRhvX+z4kEyQjprdcfwOG4qrqtIb4asm +21imOtE8CGRvHUl/cV+1l/hgv1fdbeCFzM89q16Z/KXIAWJMYfkWuOWGVEmf7yjB +9aMrfM3fAgMBAAECggEAKqGwQocBkw1GoS8kiNUrY8zFFZRa5Wvb6ZqbzEdWE7oc +EEPKph2hn7E5pIvPo7luJjsrlqktmZyp3Oy8jWMykSTP3Gg3PH3eiSiXXA/vkFj1 +xiLbO8AAB1fSv1ubUy9yEuXVbNUzSbEKfxxpD30Qp+XXjxS+bxfkUuGVT62dIH3V +j251CEsCIZzwOriGP52OKK5HR24Y9/c+uGLu1CLY6qdrMgWAXTYqEoUw7ku8Sm8B +o6fuu9i0mAEJUl6qcVz3yH0QYe9pM6jDQ9oZeSkVYyspCwysTVs2jsCTMYUpK/kD +WU9sniHRgly3C9Ge3PrE4qUeMRTNk0Vd4RETtznwqQKBgQD3UiJ11FUd3g1FXL9N +iLOIayACrX37cBuc8M5iAzasNDjSVNoCrQun1091xzYK6/F7As4PtH1YUaDgXdBp +efHHl3DPTFkeztPMOKOd8tCpLai/23sbCBNc51x0LCuWWnNaAuidId1rpvFq7AMJ +jE4HPJkoL6udOzlKUHebp02ILQKBgQC6+nT2A+AZeheUiw2wBl0BRitQCxA+TN+L +vkAwLa0u/OqeNc8W50lybzHCS9nEVZ0Lp3Qk9Cl++X/k5o6k2byqJxtmMvLGqjjw +UNuZWHSoUzfdzs8yBjroLM4HsBgbEaG9E2e2zuqKBvwLqZ3fv/fXvmJDIu+aCWXC +ADtlrAvJuwKBgQDq+CW1PJ4BWk3RcGRwDUhEe0JWSO5ATCpv2Hi7tcHjqVmyutrF +YBKKy4y6oSE/DxrFe8y6LwhHOIZXo8m17B1BOyf6StcA5g9jHwyTq3WCxdZlMOis +red3hHfaB30Bw72D7u+BGgN7m4gRxVi9YYdgaLo569Bn+TRc3kZEo5aNoQKBgH7z +aJBU50ZFCFeZ5iw61dD0pJnPOTMjnLBT917+1FRP8riCzl29obep2b4TJANTIbL0 ++j3Q7Y/BtV1kUTuKfreEn+zO8NmEX+6C5+cBEQvsnMTkEvfjFQHo0eaUYHmYihlH +YKbVbJdU0LLWclOmEpAQOsVcphQPB2EmKS4KF2LbAoGAOqVsQg61S1u7s4NF4JCN +EiJvBDjjwTycNCmhY7bV1R7LX+Qk/Mq9fgK3yccKV/Bl69C9Fmeopivbu20urNhn +q/sgOPDK0zJUSVh76gFon1gx7OfaHV31TrvIl0T7WnyfDvAv20F+dmmXkjnPBNNm +dXzo4kXwDOlWCJI8VhYfH/0= +-----END PRIVATE KEY----- +""" + +SUBSCRIPTION_LICENCE_PUBLIC_KEY = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtKOkPiegKyWdsUcmUOXv +bnunQeG4B+yOw2GG/bfXiG+ec9L2WVlSy5iK/p4AnwsSHj6gnJawHp/YK6wkYcgF +w/l2WI0T9MNsJagN+uxyV27YtWnV50JzOEFyEzSYUZxqKokVce70PypbqfsjASTl +OCJJErEGgIKdHk3T5RpQPigHwh9/a7KiBzV7ktan7KSNkcmketd9Db0eg+KdO1yZ +bNQGDrPMaYXVpfG+Ic2yU7wtCKkYb1/s+JBMkI6a3XH8DhuKq6rSG+GrJttYpjrR +PAhkbx1Jf3FftZf4YL9X3W3ghczPPatemfylyAFiTGH5FrjlhlRJn+8owfWjK3zN +3wIDAQAB +-----END PUBLIC KEY----- +""" diff --git a/api/environments/migrations/0037_add_uuid_field.py b/api/environments/migrations/0037_add_uuid_field.py new file mode 100644 index 000000000000..eaf32b1c642b --- /dev/null +++ b/api/environments/migrations/0037_add_uuid_field.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.23 on 2024-11-27 08:46 + +from django.db import migrations, models +import uuid + +from core.migration_helpers import AddDefaultUUIDs + + +class Migration(migrations.Migration): + dependencies = [ + ("environments", "0036_add_is_creating_field"), + ] + + operations = [ + migrations.AddField( + model_name="environment", + name="uuid", + field=models.UUIDField(editable=False, null=True), + ), + migrations.RunPython( + AddDefaultUUIDs("environments", "environment"), + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name="environment", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/api/environments/models.py b/api/environments/models.py index f1cf5adc5132..b01b15c89453 100644 --- a/api/environments/models.py +++ b/api/environments/models.py @@ -1,5 +1,6 @@ import logging import typing +import uuid from copy import deepcopy from core.models import abstract_base_auditable_model_factory @@ -63,7 +64,8 @@ class Environment( LifecycleModel, abstract_base_auditable_model_factory( - change_details_excluded_fields=["updated_at"] + change_details_excluded_fields=["updated_at"], + historical_records_excluded_fields=["uuid"], ), SoftDeleteObject, ): @@ -71,6 +73,7 @@ class Environment( related_object_type = RelatedObjectType.ENVIRONMENT name = models.CharField(max_length=2000) + uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) created_date = models.DateTimeField("DateCreated", auto_now_add=True) description = models.TextField(null=True, blank=True, max_length=20000) project = models.ForeignKey( @@ -178,6 +181,7 @@ def clone( """ clone = deepcopy(self) clone.id = None + clone.uuid = uuid.uuid4() clone.name = name clone.api_key = api_key if api_key else create_hash() clone.is_creating = True diff --git a/api/environments/serializers.py b/api/environments/serializers.py index 4d2836732859..70ac6f9be8cc 100644 --- a/api/environments/serializers.py +++ b/api/environments/serializers.py @@ -44,6 +44,7 @@ class Meta: model = Environment fields = ( "id", + "uuid", "name", "api_key", "description", diff --git a/api/environments/views.py b/api/environments/views.py index b60b8a1ab929..c035e8bc6218 100644 --- a/api/environments/views.py +++ b/api/environments/views.py @@ -1,13 +1,17 @@ import logging -from common.environments.permissions import TAG_SUPPORTED_PERMISSIONS +from common.environments.permissions import ( + TAG_SUPPORTED_PERMISSIONS, + VIEW_ENVIRONMENT, +) from django.db.models import Count, Q from django.utils.decorators import method_decorator from drf_yasg import openapi from drf_yasg.utils import no_body, swagger_auto_schema from rest_framework import mixins, status, viewsets from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError +from rest_framework.generics import get_object_or_404 from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response @@ -142,6 +146,20 @@ def perform_create(self, serializer): user=self.request.user, environment=environment, admin=True ) + @action( + detail=False, + url_path=r"get-by-uuid/(?P[0-9a-f-]+)", + methods=["get"], + ) + def get_by_uuid(self, request, uuid): + qs = self.get_queryset() + environment = get_object_or_404(qs, uuid=uuid) + if not request.user.has_environment_permission(VIEW_ENVIRONMENT, environment): + raise PermissionDenied() + + serializer = self.get_serializer(environment) + return Response(serializer.data) + @action(detail=True, methods=["GET"], url_path="trait-keys") def trait_keys(self, request, *args, **kwargs): keys = [ diff --git a/api/features/versioning/migrations/0005_fix_scheduled_fs_data_issue_caused_by_enabling_versioning.py b/api/features/versioning/migrations/0005_fix_scheduled_fs_data_issue_caused_by_enabling_versioning.py new file mode 100644 index 000000000000..88bb9efa66b4 --- /dev/null +++ b/api/features/versioning/migrations/0005_fix_scheduled_fs_data_issue_caused_by_enabling_versioning.py @@ -0,0 +1,71 @@ +# Generated by Django 4.2.16 on 2024-11-28 13:45 +from django.apps.registry import Apps +from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.models import F + + +def fix_corrupted_feature_states(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + feature_state_model_class = apps.get_model("features", "FeatureState") + environment_feature_version_model_class = apps.get_model( + "feature_versioning", "EnvironmentFeatureVersion" + ) + + feature_states_to_update = [] + environment_feature_versions_to_update = [] + + for feature_state in feature_state_model_class.objects.filter( + # We're looking for feature states that live in environments + # that don't have versioning enabled, but have an environment + # feature version associated to them. Moreover, those environment + # feature versions should have a change request associated with + # them. See this PR for further details of the bug we're looking + # to clean up after: https://github.com/Flagsmith/flagsmith/pull/4872 + environment__use_v2_feature_versioning=False, + environment_feature_version__isnull=False, + environment_feature_version__change_request__isnull=False, + ).exclude( + # Just to be safe, we exclude feature states for which the version + # belongs to the same feature and environment. This will prevent us + # from accidentally modifying any feature states that belong to + # legitimate environment feature versions but perhaps versioning + # has been switched off for the environment. + environment_feature_version__feature=F("feature"), + environment_feature_version__environment=F("environment") + ).select_related("environment_feature_version"): + environment_feature_version = feature_state.environment_feature_version + + # 1. move the change request back to the feature state + feature_state.change_request = environment_feature_version.change_request + + # 2. remove the change request from the EnvironmentFeatureVersion + environment_feature_version.change_request = None + + # 3. remove the environment feature version from the feature state + feature_state.environment_feature_version = None + + feature_states_to_update.append(feature_state) + environment_feature_versions_to_update.append(environment_feature_version) + + feature_state_model_class.objects.bulk_update( + feature_states_to_update, + ["environment_feature_version", "change_request"], + ) + environment_feature_version_model_class.objects.bulk_update( + environment_feature_versions_to_update, + ["change_request"], + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("feature_versioning", "0004_add_version_change_set"), + ] + + operations = [ + migrations.RunPython( + fix_corrupted_feature_states, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/api/features/versioning/tasks.py b/api/features/versioning/tasks.py index 5850c369d0c0..c5d8e1748ce4 100644 --- a/api/features/versioning/tasks.py +++ b/api/features/versioning/tasks.py @@ -109,7 +109,9 @@ def _create_initial_feature_versions(environment: "Environment"): change_request__isnull=False, change_request__committed_at__isnull=False, change_request__deleted_at__isnull=True, + environment=environment, ).select_related("change_request") + for feature_state in scheduled_feature_states: ef_version = EnvironmentFeatureVersion.objects.create( feature=feature, diff --git a/api/integrations/flagsmith/data/environment.json b/api/integrations/flagsmith/data/environment.json index f843b8ee43e1..044a1d59c92c 100644 --- a/api/integrations/flagsmith/data/environment.json +++ b/api/integrations/flagsmith/data/environment.json @@ -15,7 +15,7 @@ "multivariate_feature_state_values": [] }, { - "django_id": 627339, + "django_id": 638869, "enabled": true, "feature": { "id": 96656, @@ -24,11 +24,11 @@ }, "feature_segment": null, "feature_state_value": null, - "featurestate_uuid": "566228a6-78b0-4696-a023-feb149b61fd2", + "featurestate_uuid": "768e6102-7a0c-49f6-9d9f-097e376ed76c", "multivariate_feature_state_values": [] }, { - "django_id": 627341, + "django_id": 638871, "enabled": true, "feature": { "id": 96657, @@ -37,7 +37,7 @@ }, "feature_segment": null, "feature_state_value": null, - "featurestate_uuid": "4f7cae64-afed-416c-ada8-e6b034dff436", + "featurestate_uuid": "346cc272-dabe-4993-9cfa-76afd0d848f1", "multivariate_feature_state_values": [] }, { diff --git a/api/integrations/lead_tracking/hubspot/services.py b/api/integrations/lead_tracking/hubspot/services.py index 285fbe5a74d6..324af4f8bfcd 100644 --- a/api/integrations/lead_tracking/hubspot/services.py +++ b/api/integrations/lead_tracking/hubspot/services.py @@ -9,23 +9,21 @@ def register_hubspot_tracker(request: Request) -> None: - hubspot_cookie = request.COOKIES.get(HUBSPOT_COOKIE_NAME) + hubspot_cookie = request.data.get(HUBSPOT_COOKIE_NAME) - # TODO: Remove this temporary debugging logger statement. - logger.info(f"Request cookies for user {request.user.email}: {request.COOKIES}") - - if hubspot_cookie: - logger.info( - f"Creating HubspotTracker instance for user {request.user.email} with cookie {hubspot_cookie}" - ) - - HubspotTracker.objects.update_or_create( - user=request.user, - defaults={ - "hubspot_cookie": hubspot_cookie, - }, - ) - else: + if not hubspot_cookie: logger.info( - f"Could not create HubspotTracker instance for user {request.user.email} since no cookie" + f"Request did not included Hubspot data for user {request.user.email}" ) + return + + logger.info( + f"Creating HubspotTracker instance for user {request.user.email} with cookie {hubspot_cookie}" + ) + + HubspotTracker.objects.update_or_create( + user=request.user, + defaults={ + "hubspot_cookie": hubspot_cookie, + }, + ) diff --git a/api/organisations/admin.py b/api/organisations/admin.py index 4fe6260ef5b2..f8de3f3563be 100644 --- a/api/organisations/admin.py +++ b/api/organisations/admin.py @@ -4,7 +4,12 @@ from django.contrib import admin from django.db.models import Count, Q -from organisations.models import Organisation, Subscription, UserOrganisation +from organisations.models import ( + Organisation, + OrganisationSubscriptionInformationCache, + Subscription, + UserOrganisation, +) from projects.models import Project @@ -13,6 +18,8 @@ class ProjectInline(admin.StackedInline): extra = 0 show_change_link = True + classes = ("collapse",) + class SubscriptionInline(admin.StackedInline): model = Subscription @@ -29,12 +36,73 @@ class UserOrganisationInline(admin.TabularInline): verbose_name_plural = "Users" +class OrganisationSubscriptionInformationCacheInline(admin.StackedInline): + model = OrganisationSubscriptionInformationCache + extra = 0 + show_change_link = False + classes = ("collapse",) + + fieldsets = ( + ( + None, + { + "fields": [], + "description": "This data is relevant in SaaS only. It should all be managed automatically via " + "webhooks from Chargebee and recurring tasks but may need to be edited in certain " + "situtations.", + }, + ), + ( + "Usage Information", + { + "classes": ["collapse"], + "fields": ["api_calls_24h", "api_calls_7d", "api_calls_30d"], + }, + ), + ( + "Billing Information", + { + "classes": ["collapse"], + "fields": [ + "current_billing_term_starts_at", + "current_billing_term_ends_at", + "chargebee_email", + ], + }, + ), + ( + "Allowances", + { + "description": "These fields shouldn't need to be edited, as it should be managed automatically, " + "but sometimes things get out of sync - in which case, we can edit them here.", + "fields": [ + "allowed_seats", + "allowed_30d_api_calls", + "allowed_projects", + "audit_log_visibility_days", + "feature_history_visibility_days", + ], + }, + ), + ) + + readonly_fields = ( + "api_calls_24h", + "api_calls_7d", + "api_calls_30d", + "current_billing_term_starts_at", + "current_billing_term_ends_at", + "chargebee_email", + ) + + @admin.register(Organisation) class OrganisationAdmin(admin.ModelAdmin): inlines = [ ProjectInline, SubscriptionInline, UserOrganisationInline, + OrganisationSubscriptionInformationCacheInline, ] list_display = ( "id", diff --git a/api/organisations/models.py b/api/organisations/models.py index 3c81188e63bf..1e5637df3e8e 100644 --- a/api/organisations/models.py +++ b/api/organisations/models.py @@ -415,16 +415,29 @@ def _get_subscription_metadata_for_chargebee(self) -> ChargebeeObjMetadata: return cb_metadata def _get_subscription_metadata_for_self_hosted(self) -> BaseSubscriptionMetadata: - if not is_enterprise(): - return FREE_PLAN_SUBSCRIPTION_METADATA + if is_enterprise() and hasattr( + self.organisation, "licence" + ): # pragma: no cover + licence_information = self.organisation.licence.get_licence_information() + return BaseSubscriptionMetadata( + seats=licence_information.num_seats, + projects=licence_information.num_projects, + audit_log_visibility_days=None, + feature_history_visibility_days=None, + ) + # TODO: Once we've successfully rolled out licences to enterprises + # remove this branch to force them into the free plan + # if they don't have a licence. + elif is_enterprise(): # pragma: no cover + return BaseSubscriptionMetadata( + seats=self.max_seats, + api_calls=self.max_api_calls, + projects=None, + audit_log_visibility_days=None, + feature_history_visibility_days=None, + ) - return BaseSubscriptionMetadata( - seats=self.max_seats, - api_calls=self.max_api_calls, - projects=None, - audit_log_visibility_days=None, - feature_history_visibility_days=None, - ) + return FREE_PLAN_SUBSCRIPTION_METADATA def add_single_seat(self): if not self.can_auto_upgrade_seats: diff --git a/api/organisations/serializers.py b/api/organisations/serializers.py index fbb09b1eb23c..8a14bd924817 100644 --- a/api/organisations/serializers.py +++ b/api/organisations/serializers.py @@ -36,6 +36,7 @@ class Meta: model = Organisation fields = ( "id", + "uuid", "name", "created_date", "webhook_notification_email", diff --git a/api/organisations/subscriptions/metadata.py b/api/organisations/subscriptions/metadata.py index 893cb729bc60..fdb3033ab55d 100644 --- a/api/organisations/subscriptions/metadata.py +++ b/api/organisations/subscriptions/metadata.py @@ -9,13 +9,13 @@ class BaseSubscriptionMetadata: def __init__( self, seats: int = 0, - api_calls: int = 0, - projects: typing.Optional[int] = None, - chargebee_email: str = None, + api_calls: None | int = None, + projects: None | int = None, + chargebee_email: None | str = None, audit_log_visibility_days: int | None = 0, feature_history_visibility_days: int | None = DEFAULT_VERSION_LIMIT_DAYS, **kwargs, # allows for extra unknown attrs from CB json metadata - ): + ) -> None: self.seats = seats self.api_calls = api_calls self.projects = projects diff --git a/api/organisations/urls.py b/api/organisations/urls.py index 56faeedab455..600beededdf0 100644 --- a/api/organisations/urls.py +++ b/api/organisations/urls.py @@ -154,6 +154,20 @@ ), ] +if settings.LICENSING_INSTALLED: # pragma: no cover + from licensing.views import create_or_update_licence + + urlpatterns.extend( + [ + path( + "/licence", + create_or_update_licence, + name="create-or-update-licence", + ), + ] + ) + + if settings.IS_RBAC_INSTALLED: from rbac.views import ( GroupRoleViewSet, diff --git a/api/organisations/views.py b/api/organisations/views.py index 33bd581441a9..7a7be7d61255 100644 --- a/api/organisations/views.py +++ b/api/organisations/views.py @@ -16,7 +16,7 @@ from rest_framework.authentication import BasicAuthentication from rest_framework.decorators import action, api_view, authentication_classes from rest_framework.exceptions import ValidationError -from rest_framework.generics import ListAPIView +from rest_framework.generics import ListAPIView, get_object_or_404 from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response @@ -124,6 +124,17 @@ def create(self, request, **kwargs): else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @action( + detail=False, + url_path=r"get-by-uuid/(?P[0-9a-f-]+)", + methods=["get"], + ) + def get_by_uuid(self, request, uuid): + qs = self.get_queryset() + organisation = get_object_or_404(qs, uuid=uuid) + serializer = self.get_serializer(organisation) + return Response(serializer.data) + @action(detail=True, permission_classes=[IsAuthenticated]) def projects(self, request, pk): organisation = self.get_object() diff --git a/api/poetry.lock b/api/poetry.lock index 7b3141e0863b..374c8fd49316 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -220,13 +220,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.35.67" -description = "Type annotations for boto3 1.35.67 generated with mypy-boto3-builder 8.3.1" +version = "1.35.73" +description = "Type annotations for boto3 1.35.73 generated with mypy-boto3-builder 8.5.0" optional = false python-versions = ">=3.8" files = [ - {file = "boto3_stubs-1.35.67-py3-none-any.whl", hash = "sha256:d10d274821319ca568dde9459904c0b3ef3406c1adf92c385906ab5d9d7c09c7"}, - {file = "boto3_stubs-1.35.67.tar.gz", hash = "sha256:17eecb4fb4c65fb12eb2fb9b57d44161e1b20ecce4f0241c4d6051a7a0ce3370"}, + {file = "boto3_stubs-1.35.73-py3-none-any.whl", hash = "sha256:b935f0b62be1e18445f63cd9f5bbb4fe9a792d99efa9eb7f37b641ed4a6e70e0"}, + {file = "boto3_stubs-1.35.73.tar.gz", hash = "sha256:d1c072dfa59fbe0d91ba8e8966e844d9eb79ccc5f59e49914f796f29cd96a14d"}, ] [package.dependencies] @@ -239,7 +239,7 @@ accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.35.0,<1.36.0)"] account = ["mypy-boto3-account (>=1.35.0,<1.36.0)"] acm = ["mypy-boto3-acm (>=1.35.0,<1.36.0)"] acm-pca = ["mypy-boto3-acm-pca (>=1.35.0,<1.36.0)"] -all = ["mypy-boto3-accessanalyzer (>=1.35.0,<1.36.0)", "mypy-boto3-account (>=1.35.0,<1.36.0)", "mypy-boto3-acm (>=1.35.0,<1.36.0)", "mypy-boto3-acm-pca (>=1.35.0,<1.36.0)", "mypy-boto3-amp (>=1.35.0,<1.36.0)", "mypy-boto3-amplify (>=1.35.0,<1.36.0)", "mypy-boto3-amplifybackend (>=1.35.0,<1.36.0)", "mypy-boto3-amplifyuibuilder (>=1.35.0,<1.36.0)", "mypy-boto3-apigateway (>=1.35.0,<1.36.0)", "mypy-boto3-apigatewaymanagementapi (>=1.35.0,<1.36.0)", "mypy-boto3-apigatewayv2 (>=1.35.0,<1.36.0)", "mypy-boto3-appconfig (>=1.35.0,<1.36.0)", "mypy-boto3-appconfigdata (>=1.35.0,<1.36.0)", "mypy-boto3-appfabric (>=1.35.0,<1.36.0)", "mypy-boto3-appflow (>=1.35.0,<1.36.0)", "mypy-boto3-appintegrations (>=1.35.0,<1.36.0)", "mypy-boto3-application-autoscaling (>=1.35.0,<1.36.0)", "mypy-boto3-application-insights (>=1.35.0,<1.36.0)", "mypy-boto3-application-signals (>=1.35.0,<1.36.0)", "mypy-boto3-applicationcostprofiler (>=1.35.0,<1.36.0)", "mypy-boto3-appmesh (>=1.35.0,<1.36.0)", "mypy-boto3-apprunner (>=1.35.0,<1.36.0)", "mypy-boto3-appstream (>=1.35.0,<1.36.0)", "mypy-boto3-appsync (>=1.35.0,<1.36.0)", "mypy-boto3-apptest (>=1.35.0,<1.36.0)", "mypy-boto3-arc-zonal-shift (>=1.35.0,<1.36.0)", "mypy-boto3-artifact (>=1.35.0,<1.36.0)", "mypy-boto3-athena (>=1.35.0,<1.36.0)", "mypy-boto3-auditmanager (>=1.35.0,<1.36.0)", "mypy-boto3-autoscaling (>=1.35.0,<1.36.0)", "mypy-boto3-autoscaling-plans (>=1.35.0,<1.36.0)", "mypy-boto3-b2bi (>=1.35.0,<1.36.0)", "mypy-boto3-backup (>=1.35.0,<1.36.0)", "mypy-boto3-backup-gateway (>=1.35.0,<1.36.0)", "mypy-boto3-batch (>=1.35.0,<1.36.0)", "mypy-boto3-bcm-data-exports (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-agent (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-agent-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-billing (>=1.35.0,<1.36.0)", "mypy-boto3-billingconductor (>=1.35.0,<1.36.0)", "mypy-boto3-braket (>=1.35.0,<1.36.0)", "mypy-boto3-budgets (>=1.35.0,<1.36.0)", "mypy-boto3-ce (>=1.35.0,<1.36.0)", "mypy-boto3-chatbot (>=1.35.0,<1.36.0)", "mypy-boto3-chime (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-identity (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-meetings (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-messaging (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-voice (>=1.35.0,<1.36.0)", "mypy-boto3-cleanrooms (>=1.35.0,<1.36.0)", "mypy-boto3-cleanroomsml (>=1.35.0,<1.36.0)", "mypy-boto3-cloud9 (>=1.35.0,<1.36.0)", "mypy-boto3-cloudcontrol (>=1.35.0,<1.36.0)", "mypy-boto3-clouddirectory (>=1.35.0,<1.36.0)", "mypy-boto3-cloudformation (>=1.35.0,<1.36.0)", "mypy-boto3-cloudfront (>=1.35.0,<1.36.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.35.0,<1.36.0)", "mypy-boto3-cloudhsm (>=1.35.0,<1.36.0)", "mypy-boto3-cloudhsmv2 (>=1.35.0,<1.36.0)", "mypy-boto3-cloudsearch (>=1.35.0,<1.36.0)", "mypy-boto3-cloudsearchdomain (>=1.35.0,<1.36.0)", "mypy-boto3-cloudtrail (>=1.35.0,<1.36.0)", "mypy-boto3-cloudtrail-data (>=1.35.0,<1.36.0)", "mypy-boto3-cloudwatch (>=1.35.0,<1.36.0)", "mypy-boto3-codeartifact (>=1.35.0,<1.36.0)", "mypy-boto3-codebuild (>=1.35.0,<1.36.0)", "mypy-boto3-codecatalyst (>=1.35.0,<1.36.0)", "mypy-boto3-codecommit (>=1.35.0,<1.36.0)", "mypy-boto3-codeconnections (>=1.35.0,<1.36.0)", "mypy-boto3-codedeploy (>=1.35.0,<1.36.0)", "mypy-boto3-codeguru-reviewer (>=1.35.0,<1.36.0)", "mypy-boto3-codeguru-security (>=1.35.0,<1.36.0)", "mypy-boto3-codeguruprofiler (>=1.35.0,<1.36.0)", "mypy-boto3-codepipeline (>=1.35.0,<1.36.0)", "mypy-boto3-codestar-connections (>=1.35.0,<1.36.0)", "mypy-boto3-codestar-notifications (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-identity (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-idp (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-sync (>=1.35.0,<1.36.0)", "mypy-boto3-comprehend (>=1.35.0,<1.36.0)", "mypy-boto3-comprehendmedical (>=1.35.0,<1.36.0)", "mypy-boto3-compute-optimizer (>=1.35.0,<1.36.0)", "mypy-boto3-config (>=1.35.0,<1.36.0)", "mypy-boto3-connect (>=1.35.0,<1.36.0)", "mypy-boto3-connect-contact-lens (>=1.35.0,<1.36.0)", "mypy-boto3-connectcampaigns (>=1.35.0,<1.36.0)", "mypy-boto3-connectcampaignsv2 (>=1.35.0,<1.36.0)", "mypy-boto3-connectcases (>=1.35.0,<1.36.0)", "mypy-boto3-connectparticipant (>=1.35.0,<1.36.0)", "mypy-boto3-controlcatalog (>=1.35.0,<1.36.0)", "mypy-boto3-controltower (>=1.35.0,<1.36.0)", "mypy-boto3-cost-optimization-hub (>=1.35.0,<1.36.0)", "mypy-boto3-cur (>=1.35.0,<1.36.0)", "mypy-boto3-customer-profiles (>=1.35.0,<1.36.0)", "mypy-boto3-databrew (>=1.35.0,<1.36.0)", "mypy-boto3-dataexchange (>=1.35.0,<1.36.0)", "mypy-boto3-datapipeline (>=1.35.0,<1.36.0)", "mypy-boto3-datasync (>=1.35.0,<1.36.0)", "mypy-boto3-datazone (>=1.35.0,<1.36.0)", "mypy-boto3-dax (>=1.35.0,<1.36.0)", "mypy-boto3-deadline (>=1.35.0,<1.36.0)", "mypy-boto3-detective (>=1.35.0,<1.36.0)", "mypy-boto3-devicefarm (>=1.35.0,<1.36.0)", "mypy-boto3-devops-guru (>=1.35.0,<1.36.0)", "mypy-boto3-directconnect (>=1.35.0,<1.36.0)", "mypy-boto3-discovery (>=1.35.0,<1.36.0)", "mypy-boto3-dlm (>=1.35.0,<1.36.0)", "mypy-boto3-dms (>=1.35.0,<1.36.0)", "mypy-boto3-docdb (>=1.35.0,<1.36.0)", "mypy-boto3-docdb-elastic (>=1.35.0,<1.36.0)", "mypy-boto3-drs (>=1.35.0,<1.36.0)", "mypy-boto3-ds (>=1.35.0,<1.36.0)", "mypy-boto3-ds-data (>=1.35.0,<1.36.0)", "mypy-boto3-dynamodb (>=1.35.0,<1.36.0)", "mypy-boto3-dynamodbstreams (>=1.35.0,<1.36.0)", "mypy-boto3-ebs (>=1.35.0,<1.36.0)", "mypy-boto3-ec2 (>=1.35.0,<1.36.0)", "mypy-boto3-ec2-instance-connect (>=1.35.0,<1.36.0)", "mypy-boto3-ecr (>=1.35.0,<1.36.0)", "mypy-boto3-ecr-public (>=1.35.0,<1.36.0)", "mypy-boto3-ecs (>=1.35.0,<1.36.0)", "mypy-boto3-efs (>=1.35.0,<1.36.0)", "mypy-boto3-eks (>=1.35.0,<1.36.0)", "mypy-boto3-eks-auth (>=1.35.0,<1.36.0)", "mypy-boto3-elastic-inference (>=1.35.0,<1.36.0)", "mypy-boto3-elasticache (>=1.35.0,<1.36.0)", "mypy-boto3-elasticbeanstalk (>=1.35.0,<1.36.0)", "mypy-boto3-elastictranscoder (>=1.35.0,<1.36.0)", "mypy-boto3-elb (>=1.35.0,<1.36.0)", "mypy-boto3-elbv2 (>=1.35.0,<1.36.0)", "mypy-boto3-emr (>=1.35.0,<1.36.0)", "mypy-boto3-emr-containers (>=1.35.0,<1.36.0)", "mypy-boto3-emr-serverless (>=1.35.0,<1.36.0)", "mypy-boto3-entityresolution (>=1.35.0,<1.36.0)", "mypy-boto3-es (>=1.35.0,<1.36.0)", "mypy-boto3-events (>=1.35.0,<1.36.0)", "mypy-boto3-evidently (>=1.35.0,<1.36.0)", "mypy-boto3-finspace (>=1.35.0,<1.36.0)", "mypy-boto3-finspace-data (>=1.35.0,<1.36.0)", "mypy-boto3-firehose (>=1.35.0,<1.36.0)", "mypy-boto3-fis (>=1.35.0,<1.36.0)", "mypy-boto3-fms (>=1.35.0,<1.36.0)", "mypy-boto3-forecast (>=1.35.0,<1.36.0)", "mypy-boto3-forecastquery (>=1.35.0,<1.36.0)", "mypy-boto3-frauddetector (>=1.35.0,<1.36.0)", "mypy-boto3-freetier (>=1.35.0,<1.36.0)", "mypy-boto3-fsx (>=1.35.0,<1.36.0)", "mypy-boto3-gamelift (>=1.35.0,<1.36.0)", "mypy-boto3-geo-maps (>=1.35.0,<1.36.0)", "mypy-boto3-geo-places (>=1.35.0,<1.36.0)", "mypy-boto3-geo-routes (>=1.35.0,<1.36.0)", "mypy-boto3-glacier (>=1.35.0,<1.36.0)", "mypy-boto3-globalaccelerator (>=1.35.0,<1.36.0)", "mypy-boto3-glue (>=1.35.0,<1.36.0)", "mypy-boto3-grafana (>=1.35.0,<1.36.0)", "mypy-boto3-greengrass (>=1.35.0,<1.36.0)", "mypy-boto3-greengrassv2 (>=1.35.0,<1.36.0)", "mypy-boto3-groundstation (>=1.35.0,<1.36.0)", "mypy-boto3-guardduty (>=1.35.0,<1.36.0)", "mypy-boto3-health (>=1.35.0,<1.36.0)", "mypy-boto3-healthlake (>=1.35.0,<1.36.0)", "mypy-boto3-iam (>=1.35.0,<1.36.0)", "mypy-boto3-identitystore (>=1.35.0,<1.36.0)", "mypy-boto3-imagebuilder (>=1.35.0,<1.36.0)", "mypy-boto3-importexport (>=1.35.0,<1.36.0)", "mypy-boto3-inspector (>=1.35.0,<1.36.0)", "mypy-boto3-inspector-scan (>=1.35.0,<1.36.0)", "mypy-boto3-inspector2 (>=1.35.0,<1.36.0)", "mypy-boto3-internetmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-iot (>=1.35.0,<1.36.0)", "mypy-boto3-iot-data (>=1.35.0,<1.36.0)", "mypy-boto3-iot-jobs-data (>=1.35.0,<1.36.0)", "mypy-boto3-iot1click-devices (>=1.35.0,<1.36.0)", "mypy-boto3-iot1click-projects (>=1.35.0,<1.36.0)", "mypy-boto3-iotanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-iotdeviceadvisor (>=1.35.0,<1.36.0)", "mypy-boto3-iotevents (>=1.35.0,<1.36.0)", "mypy-boto3-iotevents-data (>=1.35.0,<1.36.0)", "mypy-boto3-iotfleethub (>=1.35.0,<1.36.0)", "mypy-boto3-iotfleetwise (>=1.35.0,<1.36.0)", "mypy-boto3-iotsecuretunneling (>=1.35.0,<1.36.0)", "mypy-boto3-iotsitewise (>=1.35.0,<1.36.0)", "mypy-boto3-iotthingsgraph (>=1.35.0,<1.36.0)", "mypy-boto3-iottwinmaker (>=1.35.0,<1.36.0)", "mypy-boto3-iotwireless (>=1.35.0,<1.36.0)", "mypy-boto3-ivs (>=1.35.0,<1.36.0)", "mypy-boto3-ivs-realtime (>=1.35.0,<1.36.0)", "mypy-boto3-ivschat (>=1.35.0,<1.36.0)", "mypy-boto3-kafka (>=1.35.0,<1.36.0)", "mypy-boto3-kafkaconnect (>=1.35.0,<1.36.0)", "mypy-boto3-kendra (>=1.35.0,<1.36.0)", "mypy-boto3-kendra-ranking (>=1.35.0,<1.36.0)", "mypy-boto3-keyspaces (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-archived-media (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-media (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-signaling (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisvideo (>=1.35.0,<1.36.0)", "mypy-boto3-kms (>=1.35.0,<1.36.0)", "mypy-boto3-lakeformation (>=1.35.0,<1.36.0)", "mypy-boto3-lambda (>=1.35.0,<1.36.0)", "mypy-boto3-launch-wizard (>=1.35.0,<1.36.0)", "mypy-boto3-lex-models (>=1.35.0,<1.36.0)", "mypy-boto3-lex-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-lexv2-models (>=1.35.0,<1.36.0)", "mypy-boto3-lexv2-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.35.0,<1.36.0)", "mypy-boto3-lightsail (>=1.35.0,<1.36.0)", "mypy-boto3-location (>=1.35.0,<1.36.0)", "mypy-boto3-logs (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutequipment (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutmetrics (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutvision (>=1.35.0,<1.36.0)", "mypy-boto3-m2 (>=1.35.0,<1.36.0)", "mypy-boto3-machinelearning (>=1.35.0,<1.36.0)", "mypy-boto3-macie2 (>=1.35.0,<1.36.0)", "mypy-boto3-mailmanager (>=1.35.0,<1.36.0)", "mypy-boto3-managedblockchain (>=1.35.0,<1.36.0)", "mypy-boto3-managedblockchain-query (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-agreement (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-catalog (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-deployment (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-entitlement (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-reporting (>=1.35.0,<1.36.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-mediaconnect (>=1.35.0,<1.36.0)", "mypy-boto3-mediaconvert (>=1.35.0,<1.36.0)", "mypy-boto3-medialive (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackage (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackage-vod (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackagev2 (>=1.35.0,<1.36.0)", "mypy-boto3-mediastore (>=1.35.0,<1.36.0)", "mypy-boto3-mediastore-data (>=1.35.0,<1.36.0)", "mypy-boto3-mediatailor (>=1.35.0,<1.36.0)", "mypy-boto3-medical-imaging (>=1.35.0,<1.36.0)", "mypy-boto3-memorydb (>=1.35.0,<1.36.0)", "mypy-boto3-meteringmarketplace (>=1.35.0,<1.36.0)", "mypy-boto3-mgh (>=1.35.0,<1.36.0)", "mypy-boto3-mgn (>=1.35.0,<1.36.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhub-config (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhuborchestrator (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhubstrategy (>=1.35.0,<1.36.0)", "mypy-boto3-mq (>=1.35.0,<1.36.0)", "mypy-boto3-mturk (>=1.35.0,<1.36.0)", "mypy-boto3-mwaa (>=1.35.0,<1.36.0)", "mypy-boto3-neptune (>=1.35.0,<1.36.0)", "mypy-boto3-neptune-graph (>=1.35.0,<1.36.0)", "mypy-boto3-neptunedata (>=1.35.0,<1.36.0)", "mypy-boto3-network-firewall (>=1.35.0,<1.36.0)", "mypy-boto3-networkmanager (>=1.35.0,<1.36.0)", "mypy-boto3-networkmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-notifications (>=1.35.0,<1.36.0)", "mypy-boto3-notificationscontacts (>=1.35.0,<1.36.0)", "mypy-boto3-oam (>=1.35.0,<1.36.0)", "mypy-boto3-omics (>=1.35.0,<1.36.0)", "mypy-boto3-opensearch (>=1.35.0,<1.36.0)", "mypy-boto3-opensearchserverless (>=1.35.0,<1.36.0)", "mypy-boto3-opsworks (>=1.35.0,<1.36.0)", "mypy-boto3-opsworkscm (>=1.35.0,<1.36.0)", "mypy-boto3-organizations (>=1.35.0,<1.36.0)", "mypy-boto3-osis (>=1.35.0,<1.36.0)", "mypy-boto3-outposts (>=1.35.0,<1.36.0)", "mypy-boto3-panorama (>=1.35.0,<1.36.0)", "mypy-boto3-partnercentral-selling (>=1.35.0,<1.36.0)", "mypy-boto3-payment-cryptography (>=1.35.0,<1.36.0)", "mypy-boto3-payment-cryptography-data (>=1.35.0,<1.36.0)", "mypy-boto3-pca-connector-ad (>=1.35.0,<1.36.0)", "mypy-boto3-pca-connector-scep (>=1.35.0,<1.36.0)", "mypy-boto3-pcs (>=1.35.0,<1.36.0)", "mypy-boto3-personalize (>=1.35.0,<1.36.0)", "mypy-boto3-personalize-events (>=1.35.0,<1.36.0)", "mypy-boto3-personalize-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-pi (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-email (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-sms-voice (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.35.0,<1.36.0)", "mypy-boto3-pipes (>=1.35.0,<1.36.0)", "mypy-boto3-polly (>=1.35.0,<1.36.0)", "mypy-boto3-pricing (>=1.35.0,<1.36.0)", "mypy-boto3-privatenetworks (>=1.35.0,<1.36.0)", "mypy-boto3-proton (>=1.35.0,<1.36.0)", "mypy-boto3-qapps (>=1.35.0,<1.36.0)", "mypy-boto3-qbusiness (>=1.35.0,<1.36.0)", "mypy-boto3-qconnect (>=1.35.0,<1.36.0)", "mypy-boto3-qldb (>=1.35.0,<1.36.0)", "mypy-boto3-qldb-session (>=1.35.0,<1.36.0)", "mypy-boto3-quicksight (>=1.35.0,<1.36.0)", "mypy-boto3-ram (>=1.35.0,<1.36.0)", "mypy-boto3-rbin (>=1.35.0,<1.36.0)", "mypy-boto3-rds (>=1.35.0,<1.36.0)", "mypy-boto3-rds-data (>=1.35.0,<1.36.0)", "mypy-boto3-redshift (>=1.35.0,<1.36.0)", "mypy-boto3-redshift-data (>=1.35.0,<1.36.0)", "mypy-boto3-redshift-serverless (>=1.35.0,<1.36.0)", "mypy-boto3-rekognition (>=1.35.0,<1.36.0)", "mypy-boto3-repostspace (>=1.35.0,<1.36.0)", "mypy-boto3-resiliencehub (>=1.35.0,<1.36.0)", "mypy-boto3-resource-explorer-2 (>=1.35.0,<1.36.0)", "mypy-boto3-resource-groups (>=1.35.0,<1.36.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.35.0,<1.36.0)", "mypy-boto3-robomaker (>=1.35.0,<1.36.0)", "mypy-boto3-rolesanywhere (>=1.35.0,<1.36.0)", "mypy-boto3-route53 (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-cluster (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-control-config (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-readiness (>=1.35.0,<1.36.0)", "mypy-boto3-route53domains (>=1.35.0,<1.36.0)", "mypy-boto3-route53profiles (>=1.35.0,<1.36.0)", "mypy-boto3-route53resolver (>=1.35.0,<1.36.0)", "mypy-boto3-rum (>=1.35.0,<1.36.0)", "mypy-boto3-s3 (>=1.35.0,<1.36.0)", "mypy-boto3-s3control (>=1.35.0,<1.36.0)", "mypy-boto3-s3outposts (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-edge (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-geospatial (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-metrics (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-savingsplans (>=1.35.0,<1.36.0)", "mypy-boto3-scheduler (>=1.35.0,<1.36.0)", "mypy-boto3-schemas (>=1.35.0,<1.36.0)", "mypy-boto3-sdb (>=1.35.0,<1.36.0)", "mypy-boto3-secretsmanager (>=1.35.0,<1.36.0)", "mypy-boto3-securityhub (>=1.35.0,<1.36.0)", "mypy-boto3-securitylake (>=1.35.0,<1.36.0)", "mypy-boto3-serverlessrepo (>=1.35.0,<1.36.0)", "mypy-boto3-service-quotas (>=1.35.0,<1.36.0)", "mypy-boto3-servicecatalog (>=1.35.0,<1.36.0)", "mypy-boto3-servicecatalog-appregistry (>=1.35.0,<1.36.0)", "mypy-boto3-servicediscovery (>=1.35.0,<1.36.0)", "mypy-boto3-ses (>=1.35.0,<1.36.0)", "mypy-boto3-sesv2 (>=1.35.0,<1.36.0)", "mypy-boto3-shield (>=1.35.0,<1.36.0)", "mypy-boto3-signer (>=1.35.0,<1.36.0)", "mypy-boto3-simspaceweaver (>=1.35.0,<1.36.0)", "mypy-boto3-sms (>=1.35.0,<1.36.0)", "mypy-boto3-sms-voice (>=1.35.0,<1.36.0)", "mypy-boto3-snow-device-management (>=1.35.0,<1.36.0)", "mypy-boto3-snowball (>=1.35.0,<1.36.0)", "mypy-boto3-sns (>=1.35.0,<1.36.0)", "mypy-boto3-socialmessaging (>=1.35.0,<1.36.0)", "mypy-boto3-sqs (>=1.35.0,<1.36.0)", "mypy-boto3-ssm (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-contacts (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-incidents (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-quicksetup (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-sap (>=1.35.0,<1.36.0)", "mypy-boto3-sso (>=1.35.0,<1.36.0)", "mypy-boto3-sso-admin (>=1.35.0,<1.36.0)", "mypy-boto3-sso-oidc (>=1.35.0,<1.36.0)", "mypy-boto3-stepfunctions (>=1.35.0,<1.36.0)", "mypy-boto3-storagegateway (>=1.35.0,<1.36.0)", "mypy-boto3-sts (>=1.35.0,<1.36.0)", "mypy-boto3-supplychain (>=1.35.0,<1.36.0)", "mypy-boto3-support (>=1.35.0,<1.36.0)", "mypy-boto3-support-app (>=1.35.0,<1.36.0)", "mypy-boto3-swf (>=1.35.0,<1.36.0)", "mypy-boto3-synthetics (>=1.35.0,<1.36.0)", "mypy-boto3-taxsettings (>=1.35.0,<1.36.0)", "mypy-boto3-textract (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-influxdb (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-query (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-write (>=1.35.0,<1.36.0)", "mypy-boto3-tnb (>=1.35.0,<1.36.0)", "mypy-boto3-transcribe (>=1.35.0,<1.36.0)", "mypy-boto3-transfer (>=1.35.0,<1.36.0)", "mypy-boto3-translate (>=1.35.0,<1.36.0)", "mypy-boto3-trustedadvisor (>=1.35.0,<1.36.0)", "mypy-boto3-verifiedpermissions (>=1.35.0,<1.36.0)", "mypy-boto3-voice-id (>=1.35.0,<1.36.0)", "mypy-boto3-vpc-lattice (>=1.35.0,<1.36.0)", "mypy-boto3-waf (>=1.35.0,<1.36.0)", "mypy-boto3-waf-regional (>=1.35.0,<1.36.0)", "mypy-boto3-wafv2 (>=1.35.0,<1.36.0)", "mypy-boto3-wellarchitected (>=1.35.0,<1.36.0)", "mypy-boto3-wisdom (>=1.35.0,<1.36.0)", "mypy-boto3-workdocs (>=1.35.0,<1.36.0)", "mypy-boto3-workmail (>=1.35.0,<1.36.0)", "mypy-boto3-workmailmessageflow (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces-thin-client (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces-web (>=1.35.0,<1.36.0)", "mypy-boto3-xray (>=1.35.0,<1.36.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.35.0,<1.36.0)", "mypy-boto3-account (>=1.35.0,<1.36.0)", "mypy-boto3-acm (>=1.35.0,<1.36.0)", "mypy-boto3-acm-pca (>=1.35.0,<1.36.0)", "mypy-boto3-amp (>=1.35.0,<1.36.0)", "mypy-boto3-amplify (>=1.35.0,<1.36.0)", "mypy-boto3-amplifybackend (>=1.35.0,<1.36.0)", "mypy-boto3-amplifyuibuilder (>=1.35.0,<1.36.0)", "mypy-boto3-apigateway (>=1.35.0,<1.36.0)", "mypy-boto3-apigatewaymanagementapi (>=1.35.0,<1.36.0)", "mypy-boto3-apigatewayv2 (>=1.35.0,<1.36.0)", "mypy-boto3-appconfig (>=1.35.0,<1.36.0)", "mypy-boto3-appconfigdata (>=1.35.0,<1.36.0)", "mypy-boto3-appfabric (>=1.35.0,<1.36.0)", "mypy-boto3-appflow (>=1.35.0,<1.36.0)", "mypy-boto3-appintegrations (>=1.35.0,<1.36.0)", "mypy-boto3-application-autoscaling (>=1.35.0,<1.36.0)", "mypy-boto3-application-insights (>=1.35.0,<1.36.0)", "mypy-boto3-application-signals (>=1.35.0,<1.36.0)", "mypy-boto3-applicationcostprofiler (>=1.35.0,<1.36.0)", "mypy-boto3-appmesh (>=1.35.0,<1.36.0)", "mypy-boto3-apprunner (>=1.35.0,<1.36.0)", "mypy-boto3-appstream (>=1.35.0,<1.36.0)", "mypy-boto3-appsync (>=1.35.0,<1.36.0)", "mypy-boto3-apptest (>=1.35.0,<1.36.0)", "mypy-boto3-arc-zonal-shift (>=1.35.0,<1.36.0)", "mypy-boto3-artifact (>=1.35.0,<1.36.0)", "mypy-boto3-athena (>=1.35.0,<1.36.0)", "mypy-boto3-auditmanager (>=1.35.0,<1.36.0)", "mypy-boto3-autoscaling (>=1.35.0,<1.36.0)", "mypy-boto3-autoscaling-plans (>=1.35.0,<1.36.0)", "mypy-boto3-b2bi (>=1.35.0,<1.36.0)", "mypy-boto3-backup (>=1.35.0,<1.36.0)", "mypy-boto3-backup-gateway (>=1.35.0,<1.36.0)", "mypy-boto3-batch (>=1.35.0,<1.36.0)", "mypy-boto3-bcm-data-exports (>=1.35.0,<1.36.0)", "mypy-boto3-bcm-pricing-calculator (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-agent (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-agent-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-billing (>=1.35.0,<1.36.0)", "mypy-boto3-billingconductor (>=1.35.0,<1.36.0)", "mypy-boto3-braket (>=1.35.0,<1.36.0)", "mypy-boto3-budgets (>=1.35.0,<1.36.0)", "mypy-boto3-ce (>=1.35.0,<1.36.0)", "mypy-boto3-chatbot (>=1.35.0,<1.36.0)", "mypy-boto3-chime (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-identity (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-meetings (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-messaging (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-voice (>=1.35.0,<1.36.0)", "mypy-boto3-cleanrooms (>=1.35.0,<1.36.0)", "mypy-boto3-cleanroomsml (>=1.35.0,<1.36.0)", "mypy-boto3-cloud9 (>=1.35.0,<1.36.0)", "mypy-boto3-cloudcontrol (>=1.35.0,<1.36.0)", "mypy-boto3-clouddirectory (>=1.35.0,<1.36.0)", "mypy-boto3-cloudformation (>=1.35.0,<1.36.0)", "mypy-boto3-cloudfront (>=1.35.0,<1.36.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.35.0,<1.36.0)", "mypy-boto3-cloudhsm (>=1.35.0,<1.36.0)", "mypy-boto3-cloudhsmv2 (>=1.35.0,<1.36.0)", "mypy-boto3-cloudsearch (>=1.35.0,<1.36.0)", "mypy-boto3-cloudsearchdomain (>=1.35.0,<1.36.0)", "mypy-boto3-cloudtrail (>=1.35.0,<1.36.0)", "mypy-boto3-cloudtrail-data (>=1.35.0,<1.36.0)", "mypy-boto3-cloudwatch (>=1.35.0,<1.36.0)", "mypy-boto3-codeartifact (>=1.35.0,<1.36.0)", "mypy-boto3-codebuild (>=1.35.0,<1.36.0)", "mypy-boto3-codecatalyst (>=1.35.0,<1.36.0)", "mypy-boto3-codecommit (>=1.35.0,<1.36.0)", "mypy-boto3-codeconnections (>=1.35.0,<1.36.0)", "mypy-boto3-codedeploy (>=1.35.0,<1.36.0)", "mypy-boto3-codeguru-reviewer (>=1.35.0,<1.36.0)", "mypy-boto3-codeguru-security (>=1.35.0,<1.36.0)", "mypy-boto3-codeguruprofiler (>=1.35.0,<1.36.0)", "mypy-boto3-codepipeline (>=1.35.0,<1.36.0)", "mypy-boto3-codestar-connections (>=1.35.0,<1.36.0)", "mypy-boto3-codestar-notifications (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-identity (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-idp (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-sync (>=1.35.0,<1.36.0)", "mypy-boto3-comprehend (>=1.35.0,<1.36.0)", "mypy-boto3-comprehendmedical (>=1.35.0,<1.36.0)", "mypy-boto3-compute-optimizer (>=1.35.0,<1.36.0)", "mypy-boto3-config (>=1.35.0,<1.36.0)", "mypy-boto3-connect (>=1.35.0,<1.36.0)", "mypy-boto3-connect-contact-lens (>=1.35.0,<1.36.0)", "mypy-boto3-connectcampaigns (>=1.35.0,<1.36.0)", "mypy-boto3-connectcampaignsv2 (>=1.35.0,<1.36.0)", "mypy-boto3-connectcases (>=1.35.0,<1.36.0)", "mypy-boto3-connectparticipant (>=1.35.0,<1.36.0)", "mypy-boto3-controlcatalog (>=1.35.0,<1.36.0)", "mypy-boto3-controltower (>=1.35.0,<1.36.0)", "mypy-boto3-cost-optimization-hub (>=1.35.0,<1.36.0)", "mypy-boto3-cur (>=1.35.0,<1.36.0)", "mypy-boto3-customer-profiles (>=1.35.0,<1.36.0)", "mypy-boto3-databrew (>=1.35.0,<1.36.0)", "mypy-boto3-dataexchange (>=1.35.0,<1.36.0)", "mypy-boto3-datapipeline (>=1.35.0,<1.36.0)", "mypy-boto3-datasync (>=1.35.0,<1.36.0)", "mypy-boto3-datazone (>=1.35.0,<1.36.0)", "mypy-boto3-dax (>=1.35.0,<1.36.0)", "mypy-boto3-deadline (>=1.35.0,<1.36.0)", "mypy-boto3-detective (>=1.35.0,<1.36.0)", "mypy-boto3-devicefarm (>=1.35.0,<1.36.0)", "mypy-boto3-devops-guru (>=1.35.0,<1.36.0)", "mypy-boto3-directconnect (>=1.35.0,<1.36.0)", "mypy-boto3-discovery (>=1.35.0,<1.36.0)", "mypy-boto3-dlm (>=1.35.0,<1.36.0)", "mypy-boto3-dms (>=1.35.0,<1.36.0)", "mypy-boto3-docdb (>=1.35.0,<1.36.0)", "mypy-boto3-docdb-elastic (>=1.35.0,<1.36.0)", "mypy-boto3-drs (>=1.35.0,<1.36.0)", "mypy-boto3-ds (>=1.35.0,<1.36.0)", "mypy-boto3-ds-data (>=1.35.0,<1.36.0)", "mypy-boto3-dynamodb (>=1.35.0,<1.36.0)", "mypy-boto3-dynamodbstreams (>=1.35.0,<1.36.0)", "mypy-boto3-ebs (>=1.35.0,<1.36.0)", "mypy-boto3-ec2 (>=1.35.0,<1.36.0)", "mypy-boto3-ec2-instance-connect (>=1.35.0,<1.36.0)", "mypy-boto3-ecr (>=1.35.0,<1.36.0)", "mypy-boto3-ecr-public (>=1.35.0,<1.36.0)", "mypy-boto3-ecs (>=1.35.0,<1.36.0)", "mypy-boto3-efs (>=1.35.0,<1.36.0)", "mypy-boto3-eks (>=1.35.0,<1.36.0)", "mypy-boto3-eks-auth (>=1.35.0,<1.36.0)", "mypy-boto3-elastic-inference (>=1.35.0,<1.36.0)", "mypy-boto3-elasticache (>=1.35.0,<1.36.0)", "mypy-boto3-elasticbeanstalk (>=1.35.0,<1.36.0)", "mypy-boto3-elastictranscoder (>=1.35.0,<1.36.0)", "mypy-boto3-elb (>=1.35.0,<1.36.0)", "mypy-boto3-elbv2 (>=1.35.0,<1.36.0)", "mypy-boto3-emr (>=1.35.0,<1.36.0)", "mypy-boto3-emr-containers (>=1.35.0,<1.36.0)", "mypy-boto3-emr-serverless (>=1.35.0,<1.36.0)", "mypy-boto3-entityresolution (>=1.35.0,<1.36.0)", "mypy-boto3-es (>=1.35.0,<1.36.0)", "mypy-boto3-events (>=1.35.0,<1.36.0)", "mypy-boto3-evidently (>=1.35.0,<1.36.0)", "mypy-boto3-finspace (>=1.35.0,<1.36.0)", "mypy-boto3-finspace-data (>=1.35.0,<1.36.0)", "mypy-boto3-firehose (>=1.35.0,<1.36.0)", "mypy-boto3-fis (>=1.35.0,<1.36.0)", "mypy-boto3-fms (>=1.35.0,<1.36.0)", "mypy-boto3-forecast (>=1.35.0,<1.36.0)", "mypy-boto3-forecastquery (>=1.35.0,<1.36.0)", "mypy-boto3-frauddetector (>=1.35.0,<1.36.0)", "mypy-boto3-freetier (>=1.35.0,<1.36.0)", "mypy-boto3-fsx (>=1.35.0,<1.36.0)", "mypy-boto3-gamelift (>=1.35.0,<1.36.0)", "mypy-boto3-geo-maps (>=1.35.0,<1.36.0)", "mypy-boto3-geo-places (>=1.35.0,<1.36.0)", "mypy-boto3-geo-routes (>=1.35.0,<1.36.0)", "mypy-boto3-glacier (>=1.35.0,<1.36.0)", "mypy-boto3-globalaccelerator (>=1.35.0,<1.36.0)", "mypy-boto3-glue (>=1.35.0,<1.36.0)", "mypy-boto3-grafana (>=1.35.0,<1.36.0)", "mypy-boto3-greengrass (>=1.35.0,<1.36.0)", "mypy-boto3-greengrassv2 (>=1.35.0,<1.36.0)", "mypy-boto3-groundstation (>=1.35.0,<1.36.0)", "mypy-boto3-guardduty (>=1.35.0,<1.36.0)", "mypy-boto3-health (>=1.35.0,<1.36.0)", "mypy-boto3-healthlake (>=1.35.0,<1.36.0)", "mypy-boto3-iam (>=1.35.0,<1.36.0)", "mypy-boto3-identitystore (>=1.35.0,<1.36.0)", "mypy-boto3-imagebuilder (>=1.35.0,<1.36.0)", "mypy-boto3-importexport (>=1.35.0,<1.36.0)", "mypy-boto3-inspector (>=1.35.0,<1.36.0)", "mypy-boto3-inspector-scan (>=1.35.0,<1.36.0)", "mypy-boto3-inspector2 (>=1.35.0,<1.36.0)", "mypy-boto3-internetmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-invoicing (>=1.35.0,<1.36.0)", "mypy-boto3-iot (>=1.35.0,<1.36.0)", "mypy-boto3-iot-data (>=1.35.0,<1.36.0)", "mypy-boto3-iot-jobs-data (>=1.35.0,<1.36.0)", "mypy-boto3-iot1click-devices (>=1.35.0,<1.36.0)", "mypy-boto3-iot1click-projects (>=1.35.0,<1.36.0)", "mypy-boto3-iotanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-iotdeviceadvisor (>=1.35.0,<1.36.0)", "mypy-boto3-iotevents (>=1.35.0,<1.36.0)", "mypy-boto3-iotevents-data (>=1.35.0,<1.36.0)", "mypy-boto3-iotfleethub (>=1.35.0,<1.36.0)", "mypy-boto3-iotfleetwise (>=1.35.0,<1.36.0)", "mypy-boto3-iotsecuretunneling (>=1.35.0,<1.36.0)", "mypy-boto3-iotsitewise (>=1.35.0,<1.36.0)", "mypy-boto3-iotthingsgraph (>=1.35.0,<1.36.0)", "mypy-boto3-iottwinmaker (>=1.35.0,<1.36.0)", "mypy-boto3-iotwireless (>=1.35.0,<1.36.0)", "mypy-boto3-ivs (>=1.35.0,<1.36.0)", "mypy-boto3-ivs-realtime (>=1.35.0,<1.36.0)", "mypy-boto3-ivschat (>=1.35.0,<1.36.0)", "mypy-boto3-kafka (>=1.35.0,<1.36.0)", "mypy-boto3-kafkaconnect (>=1.35.0,<1.36.0)", "mypy-boto3-kendra (>=1.35.0,<1.36.0)", "mypy-boto3-kendra-ranking (>=1.35.0,<1.36.0)", "mypy-boto3-keyspaces (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-archived-media (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-media (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-signaling (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisvideo (>=1.35.0,<1.36.0)", "mypy-boto3-kms (>=1.35.0,<1.36.0)", "mypy-boto3-lakeformation (>=1.35.0,<1.36.0)", "mypy-boto3-lambda (>=1.35.0,<1.36.0)", "mypy-boto3-launch-wizard (>=1.35.0,<1.36.0)", "mypy-boto3-lex-models (>=1.35.0,<1.36.0)", "mypy-boto3-lex-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-lexv2-models (>=1.35.0,<1.36.0)", "mypy-boto3-lexv2-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.35.0,<1.36.0)", "mypy-boto3-lightsail (>=1.35.0,<1.36.0)", "mypy-boto3-location (>=1.35.0,<1.36.0)", "mypy-boto3-logs (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutequipment (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutmetrics (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutvision (>=1.35.0,<1.36.0)", "mypy-boto3-m2 (>=1.35.0,<1.36.0)", "mypy-boto3-machinelearning (>=1.35.0,<1.36.0)", "mypy-boto3-macie2 (>=1.35.0,<1.36.0)", "mypy-boto3-mailmanager (>=1.35.0,<1.36.0)", "mypy-boto3-managedblockchain (>=1.35.0,<1.36.0)", "mypy-boto3-managedblockchain-query (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-agreement (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-catalog (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-deployment (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-entitlement (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-reporting (>=1.35.0,<1.36.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-mediaconnect (>=1.35.0,<1.36.0)", "mypy-boto3-mediaconvert (>=1.35.0,<1.36.0)", "mypy-boto3-medialive (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackage (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackage-vod (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackagev2 (>=1.35.0,<1.36.0)", "mypy-boto3-mediastore (>=1.35.0,<1.36.0)", "mypy-boto3-mediastore-data (>=1.35.0,<1.36.0)", "mypy-boto3-mediatailor (>=1.35.0,<1.36.0)", "mypy-boto3-medical-imaging (>=1.35.0,<1.36.0)", "mypy-boto3-memorydb (>=1.35.0,<1.36.0)", "mypy-boto3-meteringmarketplace (>=1.35.0,<1.36.0)", "mypy-boto3-mgh (>=1.35.0,<1.36.0)", "mypy-boto3-mgn (>=1.35.0,<1.36.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhub-config (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhuborchestrator (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhubstrategy (>=1.35.0,<1.36.0)", "mypy-boto3-mq (>=1.35.0,<1.36.0)", "mypy-boto3-mturk (>=1.35.0,<1.36.0)", "mypy-boto3-mwaa (>=1.35.0,<1.36.0)", "mypy-boto3-neptune (>=1.35.0,<1.36.0)", "mypy-boto3-neptune-graph (>=1.35.0,<1.36.0)", "mypy-boto3-neptunedata (>=1.35.0,<1.36.0)", "mypy-boto3-network-firewall (>=1.35.0,<1.36.0)", "mypy-boto3-networkflowmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-networkmanager (>=1.35.0,<1.36.0)", "mypy-boto3-networkmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-notifications (>=1.35.0,<1.36.0)", "mypy-boto3-notificationscontacts (>=1.35.0,<1.36.0)", "mypy-boto3-oam (>=1.35.0,<1.36.0)", "mypy-boto3-observabilityadmin (>=1.35.0,<1.36.0)", "mypy-boto3-omics (>=1.35.0,<1.36.0)", "mypy-boto3-opensearch (>=1.35.0,<1.36.0)", "mypy-boto3-opensearchserverless (>=1.35.0,<1.36.0)", "mypy-boto3-opsworks (>=1.35.0,<1.36.0)", "mypy-boto3-opsworkscm (>=1.35.0,<1.36.0)", "mypy-boto3-organizations (>=1.35.0,<1.36.0)", "mypy-boto3-osis (>=1.35.0,<1.36.0)", "mypy-boto3-outposts (>=1.35.0,<1.36.0)", "mypy-boto3-panorama (>=1.35.0,<1.36.0)", "mypy-boto3-partnercentral-selling (>=1.35.0,<1.36.0)", "mypy-boto3-payment-cryptography (>=1.35.0,<1.36.0)", "mypy-boto3-payment-cryptography-data (>=1.35.0,<1.36.0)", "mypy-boto3-pca-connector-ad (>=1.35.0,<1.36.0)", "mypy-boto3-pca-connector-scep (>=1.35.0,<1.36.0)", "mypy-boto3-pcs (>=1.35.0,<1.36.0)", "mypy-boto3-personalize (>=1.35.0,<1.36.0)", "mypy-boto3-personalize-events (>=1.35.0,<1.36.0)", "mypy-boto3-personalize-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-pi (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-email (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-sms-voice (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.35.0,<1.36.0)", "mypy-boto3-pipes (>=1.35.0,<1.36.0)", "mypy-boto3-polly (>=1.35.0,<1.36.0)", "mypy-boto3-pricing (>=1.35.0,<1.36.0)", "mypy-boto3-privatenetworks (>=1.35.0,<1.36.0)", "mypy-boto3-proton (>=1.35.0,<1.36.0)", "mypy-boto3-qapps (>=1.35.0,<1.36.0)", "mypy-boto3-qbusiness (>=1.35.0,<1.36.0)", "mypy-boto3-qconnect (>=1.35.0,<1.36.0)", "mypy-boto3-qldb (>=1.35.0,<1.36.0)", "mypy-boto3-qldb-session (>=1.35.0,<1.36.0)", "mypy-boto3-quicksight (>=1.35.0,<1.36.0)", "mypy-boto3-ram (>=1.35.0,<1.36.0)", "mypy-boto3-rbin (>=1.35.0,<1.36.0)", "mypy-boto3-rds (>=1.35.0,<1.36.0)", "mypy-boto3-rds-data (>=1.35.0,<1.36.0)", "mypy-boto3-redshift (>=1.35.0,<1.36.0)", "mypy-boto3-redshift-data (>=1.35.0,<1.36.0)", "mypy-boto3-redshift-serverless (>=1.35.0,<1.36.0)", "mypy-boto3-rekognition (>=1.35.0,<1.36.0)", "mypy-boto3-repostspace (>=1.35.0,<1.36.0)", "mypy-boto3-resiliencehub (>=1.35.0,<1.36.0)", "mypy-boto3-resource-explorer-2 (>=1.35.0,<1.36.0)", "mypy-boto3-resource-groups (>=1.35.0,<1.36.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.35.0,<1.36.0)", "mypy-boto3-robomaker (>=1.35.0,<1.36.0)", "mypy-boto3-rolesanywhere (>=1.35.0,<1.36.0)", "mypy-boto3-route53 (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-cluster (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-control-config (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-readiness (>=1.35.0,<1.36.0)", "mypy-boto3-route53domains (>=1.35.0,<1.36.0)", "mypy-boto3-route53profiles (>=1.35.0,<1.36.0)", "mypy-boto3-route53resolver (>=1.35.0,<1.36.0)", "mypy-boto3-rum (>=1.35.0,<1.36.0)", "mypy-boto3-s3 (>=1.35.0,<1.36.0)", "mypy-boto3-s3control (>=1.35.0,<1.36.0)", "mypy-boto3-s3outposts (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-edge (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-geospatial (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-metrics (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-savingsplans (>=1.35.0,<1.36.0)", "mypy-boto3-scheduler (>=1.35.0,<1.36.0)", "mypy-boto3-schemas (>=1.35.0,<1.36.0)", "mypy-boto3-sdb (>=1.35.0,<1.36.0)", "mypy-boto3-secretsmanager (>=1.35.0,<1.36.0)", "mypy-boto3-security-ir (>=1.35.0,<1.36.0)", "mypy-boto3-securityhub (>=1.35.0,<1.36.0)", "mypy-boto3-securitylake (>=1.35.0,<1.36.0)", "mypy-boto3-serverlessrepo (>=1.35.0,<1.36.0)", "mypy-boto3-service-quotas (>=1.35.0,<1.36.0)", "mypy-boto3-servicecatalog (>=1.35.0,<1.36.0)", "mypy-boto3-servicecatalog-appregistry (>=1.35.0,<1.36.0)", "mypy-boto3-servicediscovery (>=1.35.0,<1.36.0)", "mypy-boto3-ses (>=1.35.0,<1.36.0)", "mypy-boto3-sesv2 (>=1.35.0,<1.36.0)", "mypy-boto3-shield (>=1.35.0,<1.36.0)", "mypy-boto3-signer (>=1.35.0,<1.36.0)", "mypy-boto3-simspaceweaver (>=1.35.0,<1.36.0)", "mypy-boto3-sms (>=1.35.0,<1.36.0)", "mypy-boto3-sms-voice (>=1.35.0,<1.36.0)", "mypy-boto3-snow-device-management (>=1.35.0,<1.36.0)", "mypy-boto3-snowball (>=1.35.0,<1.36.0)", "mypy-boto3-sns (>=1.35.0,<1.36.0)", "mypy-boto3-socialmessaging (>=1.35.0,<1.36.0)", "mypy-boto3-sqs (>=1.35.0,<1.36.0)", "mypy-boto3-ssm (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-contacts (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-incidents (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-quicksetup (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-sap (>=1.35.0,<1.36.0)", "mypy-boto3-sso (>=1.35.0,<1.36.0)", "mypy-boto3-sso-admin (>=1.35.0,<1.36.0)", "mypy-boto3-sso-oidc (>=1.35.0,<1.36.0)", "mypy-boto3-stepfunctions (>=1.35.0,<1.36.0)", "mypy-boto3-storagegateway (>=1.35.0,<1.36.0)", "mypy-boto3-sts (>=1.35.0,<1.36.0)", "mypy-boto3-supplychain (>=1.35.0,<1.36.0)", "mypy-boto3-support (>=1.35.0,<1.36.0)", "mypy-boto3-support-app (>=1.35.0,<1.36.0)", "mypy-boto3-swf (>=1.35.0,<1.36.0)", "mypy-boto3-synthetics (>=1.35.0,<1.36.0)", "mypy-boto3-taxsettings (>=1.35.0,<1.36.0)", "mypy-boto3-textract (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-influxdb (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-query (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-write (>=1.35.0,<1.36.0)", "mypy-boto3-tnb (>=1.35.0,<1.36.0)", "mypy-boto3-transcribe (>=1.35.0,<1.36.0)", "mypy-boto3-transfer (>=1.35.0,<1.36.0)", "mypy-boto3-translate (>=1.35.0,<1.36.0)", "mypy-boto3-trustedadvisor (>=1.35.0,<1.36.0)", "mypy-boto3-verifiedpermissions (>=1.35.0,<1.36.0)", "mypy-boto3-voice-id (>=1.35.0,<1.36.0)", "mypy-boto3-vpc-lattice (>=1.35.0,<1.36.0)", "mypy-boto3-waf (>=1.35.0,<1.36.0)", "mypy-boto3-waf-regional (>=1.35.0,<1.36.0)", "mypy-boto3-wafv2 (>=1.35.0,<1.36.0)", "mypy-boto3-wellarchitected (>=1.35.0,<1.36.0)", "mypy-boto3-wisdom (>=1.35.0,<1.36.0)", "mypy-boto3-workdocs (>=1.35.0,<1.36.0)", "mypy-boto3-workmail (>=1.35.0,<1.36.0)", "mypy-boto3-workmailmessageflow (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces-thin-client (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces-web (>=1.35.0,<1.36.0)", "mypy-boto3-xray (>=1.35.0,<1.36.0)"] amp = ["mypy-boto3-amp (>=1.35.0,<1.36.0)"] amplify = ["mypy-boto3-amplify (>=1.35.0,<1.36.0)"] amplifybackend = ["mypy-boto3-amplifybackend (>=1.35.0,<1.36.0)"] @@ -272,13 +272,14 @@ backup = ["mypy-boto3-backup (>=1.35.0,<1.36.0)"] backup-gateway = ["mypy-boto3-backup-gateway (>=1.35.0,<1.36.0)"] batch = ["mypy-boto3-batch (>=1.35.0,<1.36.0)"] bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.35.0,<1.36.0)"] +bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.35.0,<1.36.0)"] bedrock = ["mypy-boto3-bedrock (>=1.35.0,<1.36.0)"] bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.35.0,<1.36.0)"] bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.35.0,<1.36.0)"] bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.35.0,<1.36.0)"] billing = ["mypy-boto3-billing (>=1.35.0,<1.36.0)"] billingconductor = ["mypy-boto3-billingconductor (>=1.35.0,<1.36.0)"] -boto3 = ["boto3 (==1.35.67)", "botocore (==1.35.67)"] +boto3 = ["boto3 (==1.35.73)", "botocore (==1.35.73)"] braket = ["mypy-boto3-braket (>=1.35.0,<1.36.0)"] budgets = ["mypy-boto3-budgets (>=1.35.0,<1.36.0)"] ce = ["mypy-boto3-ce (>=1.35.0,<1.36.0)"] @@ -411,6 +412,7 @@ inspector = ["mypy-boto3-inspector (>=1.35.0,<1.36.0)"] inspector-scan = ["mypy-boto3-inspector-scan (>=1.35.0,<1.36.0)"] inspector2 = ["mypy-boto3-inspector2 (>=1.35.0,<1.36.0)"] internetmonitor = ["mypy-boto3-internetmonitor (>=1.35.0,<1.36.0)"] +invoicing = ["mypy-boto3-invoicing (>=1.35.0,<1.36.0)"] iot = ["mypy-boto3-iot (>=1.35.0,<1.36.0)"] iot-data = ["mypy-boto3-iot-data (>=1.35.0,<1.36.0)"] iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.35.0,<1.36.0)"] @@ -497,11 +499,13 @@ neptune = ["mypy-boto3-neptune (>=1.35.0,<1.36.0)"] neptune-graph = ["mypy-boto3-neptune-graph (>=1.35.0,<1.36.0)"] neptunedata = ["mypy-boto3-neptunedata (>=1.35.0,<1.36.0)"] network-firewall = ["mypy-boto3-network-firewall (>=1.35.0,<1.36.0)"] +networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.35.0,<1.36.0)"] networkmanager = ["mypy-boto3-networkmanager (>=1.35.0,<1.36.0)"] networkmonitor = ["mypy-boto3-networkmonitor (>=1.35.0,<1.36.0)"] notifications = ["mypy-boto3-notifications (>=1.35.0,<1.36.0)"] notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.35.0,<1.36.0)"] oam = ["mypy-boto3-oam (>=1.35.0,<1.36.0)"] +observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.35.0,<1.36.0)"] omics = ["mypy-boto3-omics (>=1.35.0,<1.36.0)"] opensearch = ["mypy-boto3-opensearch (>=1.35.0,<1.36.0)"] opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.35.0,<1.36.0)"] @@ -574,6 +578,7 @@ scheduler = ["mypy-boto3-scheduler (>=1.35.0,<1.36.0)"] schemas = ["mypy-boto3-schemas (>=1.35.0,<1.36.0)"] sdb = ["mypy-boto3-sdb (>=1.35.0,<1.36.0)"] secretsmanager = ["mypy-boto3-secretsmanager (>=1.35.0,<1.36.0)"] +security-ir = ["mypy-boto3-security-ir (>=1.35.0,<1.36.0)"] securityhub = ["mypy-boto3-securityhub (>=1.35.0,<1.36.0)"] securitylake = ["mypy-boto3-securitylake (>=1.35.0,<1.36.0)"] serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.35.0,<1.36.0)"] @@ -656,13 +661,13 @@ crt = ["awscrt (==0.16.26)"] [[package]] name = "botocore-stubs" -version = "1.35.67" +version = "1.35.73" description = "Type annotations and code completion for botocore" optional = false python-versions = ">=3.8" files = [ - {file = "botocore_stubs-1.35.67-py3-none-any.whl", hash = "sha256:2c10c6c34473e33c5ff1569aef44b8c4ce4afcb0a502a99f7f192dc31006e9f2"}, - {file = "botocore_stubs-1.35.67.tar.gz", hash = "sha256:21886510f4fd8f5804e4bb83661ba0e005e4c5236b89f70dd428e6cea68b0ee6"}, + {file = "botocore_stubs-1.35.73-py3-none-any.whl", hash = "sha256:54f7bcc325382050ae6aa839163f93f5c4e777db9c0fd2da3ad0744720895fbe"}, + {file = "botocore_stubs-1.35.73.tar.gz", hash = "sha256:e9a20b0a29621674b46225fdb88bf00a0bca5216413d717895b75ba2dd63c6cc"}, ] [package.dependencies] @@ -999,38 +1004,38 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.1" +version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] [package.dependencies] @@ -1043,7 +1048,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -1822,7 +1827,7 @@ sseclient-py = ">=1.8.0,<2.0.0" [[package]] name = "flagsmith-auth-controller" -version = "0.1.1" +version = "0.1.3" description = "Flagsmith auth controller." optional = false python-versions = ">=3.11,<4.0" @@ -1835,8 +1840,8 @@ django-multiselectfield = "0.1.12" [package.source] type = "git" url = "https://github.com/flagsmith/flagsmith-auth-controller" -reference = "v0.1.2" -resolved_reference = "d0f73840b4d5a078077c2bb108458356476d0ee5" +reference = "v0.1.3" +resolved_reference = "303954ba54fd2f402c75806b3b9ba7f2d42aa426" [[package]] name = "flagsmith-common" @@ -1857,8 +1862,8 @@ flagsmith-flag-engine = "*" [package.source] type = "git" url = "https://github.com/Flagsmith/flagsmith-common" -reference = "v1.1.0" -resolved_reference = "27fbd8b7d889dc1529df08972a8c1bfaba5a7e03" +reference = "v1.2.0" +resolved_reference = "ccb6c0603168c91cfb8f9f24bdfa21741eb5916e" [[package]] name = "flagsmith-flag-engine" @@ -4455,13 +4460,13 @@ files = [ [[package]] name = "types-awscrt" -version = "0.23.0" +version = "0.23.1" description = "Type annotations and code completion for awscrt" optional = false python-versions = ">=3.8" files = [ - {file = "types_awscrt-0.23.0-py3-none-any.whl", hash = "sha256:517d9d06f19cf58d778ca90ad01e52e0489466bf70dcf78c7f47f74fdf151a60"}, - {file = "types_awscrt-0.23.0.tar.gz", hash = "sha256:3fd1edeac923d1956c0e907c973fb83bda465beae7f054716b371b293f9b5fdc"}, + {file = "types_awscrt-0.23.1-py3-none-any.whl", hash = "sha256:0d362a5d62d68ca4216f458172f41c1123ec04791d68364de8ee8b61b528b262"}, + {file = "types_awscrt-0.23.1.tar.gz", hash = "sha256:a20b425dabb258bc3d07a5e7de503fd9558dd1542d72de796e74e402c6d493b2"}, ] [[package]] @@ -4676,14 +4681,14 @@ develop = false [package.dependencies] djangorestframework = "*" djangorestframework-recursive = "*" -flagsmith-common = {git = "https://github.com/Flagsmith/flagsmith-common", tag = "v1.1.0"} +flagsmith-common = {git = "https://github.com/Flagsmith/flagsmith-common", tag = "v1.2.0"} flagsmith-flag-engine = "*" [package.source] type = "git" url = "https://github.com/flagsmith/flagsmith-workflows" -reference = "v2.6.0" -resolved_reference = "06b03d428484f5aed2f400cef431ab5aae0d0df0" +reference = "v2.7.0" +resolved_reference = "3bd546a451e7f2359aa9787ec8ffa5e4315d12cf" [[package]] name = "wrapt" @@ -4802,4 +4807,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.11, <3.13" -content-hash = "2b4397d1c347df51b3dd218c5f429f2b35c0bc27bb5ff62bf9beb6c8b5886a34" +content-hash = "ec45c0a37a04c7295c3e9b52294ac7f28eb60e1b80fe86da367c2e7b11534a60" diff --git a/api/pyproject.toml b/api/pyproject.toml index 23dd2c5cd864..faf9a924e55f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -180,7 +180,7 @@ hubspot-api-client = "^8.2.1" djangorestframework-dataclasses = "^1.3.1" pyotp = "^2.9.0" flagsmith-task-processor = { git = "https://github.com/Flagsmith/flagsmith-task-processor", tag = "v1.0.2" } -flagsmith-common = { git = "https://github.com/Flagsmith/flagsmith-common", tag = "v1.1.0" } +flagsmith-common = { git = "https://github.com/Flagsmith/flagsmith-common", tag = "v1.2.0" } tzdata = "^2024.1" djangorestframework-simplejwt = "^5.3.1" @@ -188,7 +188,7 @@ djangorestframework-simplejwt = "^5.3.1" optional = true [tool.poetry.group.auth-controller.dependencies] -flagsmith-auth-controller = { git = "https://github.com/flagsmith/flagsmith-auth-controller", tag = "v0.1.2" } +flagsmith-auth-controller = { git = "https://github.com/flagsmith/flagsmith-auth-controller", tag = "v0.1.3" } [tool.poetry.group.saml] optional = true @@ -206,7 +206,7 @@ flagsmith-ldap = { git = "https://github.com/flagsmith/flagsmith-ldap", tag = "v optional = true [tool.poetry.group.workflows.dependencies] -workflows-logic = { git = "https://github.com/flagsmith/flagsmith-workflows", tag = "v2.6.0" } +workflows-logic = { git = "https://github.com/flagsmith/flagsmith-workflows", tag = "v2.7.0" } [tool.poetry.group.dev.dependencies] django-test-migrations = "~1.2.0" diff --git a/api/scripts/mypy_baseline.txt b/api/scripts/mypy_baseline.txt index 1092b3baef75..ab0cfe56444a 100644 --- a/api/scripts/mypy_baseline.txt +++ b/api/scripts/mypy_baseline.txt @@ -1533,3 +1533,81 @@ api/urls/v1.py:78: error: Cannot find implementation or library stub for module util/mappers/sdk.py:51: error: Argument "exclude" to "model_dump" of "BaseModel" has incompatible type "list[str]"; expected "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" [arg-type] util/pydantic.py:17: note: def [Model: BaseModel] create_model(str, /, *, __config__: ConfigDict | None = ..., __doc__: str | None = ..., __base__: type[Model] | tuple[type[Model], ...], __module__: str = ..., __validators__: dict[str, classmethod[Any, Any, Any]] | None = ..., __cls_kwargs__: dict[str, Any] | None = ..., **field_definitions: Any) -> type[Model] util/pydantic.py:17: note: def create_model(str, /, *, __config__: ConfigDict | None = ..., __doc__: str | None = ..., __base__: None = ..., __module__: str = ..., __validators__: dict[str, classmethod[Any, Any, Any]] | None = ..., __cls_kwargs__: dict[str, Any] | None = ..., **field_definitions: Any) -> type[BaseModel] +environments/models.py:142: error: Need type annotation for "is_creating" [var-annotated] +environments/serializers.py:87: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +environments/models.py:100: error: Need type annotation for "webhook_url" [var-annotated] +environments/models.py:175: error: Incompatible default for argument "api_key" (default has type "None", argument has type "str") [assignment] +environments/models.py:23: error: Skipping analyzing "softdelete.models": module is installed, but missing library stubs or py.typed marker [import-untyped] +environments/models.py:447: note: Suggestion: use type[...] instead of type(...) +environments/models.py:502: error: Need type annotation for "environment" [var-annotated] +environments/models.py:102: error: Need type annotation for "allow_client_traits" [var-annotated] +environments/models.py:79: error: Need type annotation for "project" [var-annotated] +environments/models.py:507: error: Need type annotation for "name" [var-annotated] +environments/models.py:93: error: Need type annotation for "api_key" [var-annotated] +environments/models.py:175: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +environments/serializers.py:130: error: Item "None" of "Any | Sequence[Any] | None" has no attribute "project" [union-attr] +environments/models.py:175: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True +environments/models.py:292: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +environments/models.py:137: error: Need type annotation for "use_identity_overrides_in_local_eval" [var-annotated] +organisations/subscriptions/metadata.py:34: error: Unsupported left operand type for + ("None") [operator] +environments/models.py:75: error: Need type annotation for "name" [var-annotated] +organisations/serializers.py:222: error: Need type annotation for "events_list" [var-annotated] +environments/views.py:309: error: Incompatible types in assignment (expression has type "type[Webhook]", base class "NestedEnvironmentViewSet" defined the type as "None") [assignment] +integrations/lead_tracking/hubspot/services.py:16: error: Item "AbstractBaseUser" of "AbstractBaseUser | AnonymousUser" has no attribute "email" [union-attr] +environments/models.py:77: error: Need type annotation for "created_date" [var-annotated] +environments/models.py:78: error: Need type annotation for "description" [var-annotated] +environments/models.py:246: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True +environments/models.py:450: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +environments/serializers.py:89: error: Item "Sequence[Any]" of "Any | Sequence[Any]" has no attribute "project" [union-attr] +environments/models.py:448: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True +environments/models.py:450: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True +environments/models.py:283: error: Item "None" of "Project | None" has no attribute "enable_dynamo_db" [union-attr] +organisations/views.py:313: error: Incompatible types in assignment (expression has type "WebhookType", base class "TriggerSampleWebhookMixin" defined the type as "None") [assignment] +organisations/serializers.py:118: error: Need type annotation for "emails" [var-annotated] +organisations/urls.py:158: error: Cannot find implementation or library stub for module named "licensing.views" [import-not-found] +integrations/lead_tracking/hubspot/services.py:21: error: Item "AbstractBaseUser" of "AbstractBaseUser | AnonymousUser" has no attribute "email" [union-attr] +environments/models.py:109: error: Need type annotation for "banner_text" [var-annotated] +environments/serializers.py:102: error: Cannot determine type of "fields" [has-type] +environments/models.py:123: error: Need type annotation for "use_identity_composite_key_for_hashing" [var-annotated] +environments/serializers.py:87: error: Incompatible default for argument "validated_data" (default has type "None", argument has type "dict[Any, Any]") [assignment] +environments/serializers.py:87: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True +environments/models.py:447: error: Invalid type comment or annotation [valid-type] +environments/serializers.py:130: error: Item "Sequence[Any]" of "Any | Sequence[Any] | None" has no attribute "project" [union-attr] +environments/views.py:324: error: Incompatible types in assignment (expression has type "type[EnvironmentAPIKey]", base class "NestedEnvironmentViewSet" defined the type as "None") [assignment] +environments/models.py:131: error: Need type annotation for "hide_sensitive_data" [var-annotated] +organisations/subscriptions/metadata.py:34: error: Unsupported operand types for + ("int" and "None") [operator] +environments/models.py:288: error: Item "None" of "Project | None" has no attribute "edge_v2_environments_migrated" [union-attr] +environments/views.py:311: error: Incompatible types in assignment (expression has type "WebhookType", base class "TriggerSampleWebhookMixin" defined the type as "None") [assignment] +environments/models.py:448: error: Incompatible default for argument "identity_id" (default has type "None", argument has type "int | str") [assignment] +organisations/subscription_info_cache.py:131: error: Incompatible types in assignment (expression has type "int | None", variable has type "float | int | str | Combinable") [assignment] +environments/views.py:10: error: Skipping analyzing "drf_yasg.utils": module is installed, but missing library stubs or py.typed marker [import-untyped] +environments/models.py:508: error: Need type annotation for "expires_at" [var-annotated] +environments/models.py:449: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +environments/models.py:246: error: Incompatible default for argument "environment_id" (default has type "None", argument has type "int") [assignment] +integrations/lead_tracking/hubspot/services.py:16: error: Item "AnonymousUser" of "AbstractBaseUser | AnonymousUser" has no attribute "email" [union-attr] +environments/models.py:136: error: Need type annotation for "use_v2_feature_versioning" [var-annotated] +integrations/lead_tracking/hubspot/services.py:21: error: Item "AnonymousUser" of "AbstractBaseUser | AnonymousUser" has no attribute "email" [union-attr] +environments/models.py:292: error: Incompatible default for argument "filter_kwargs" (default has type "None", argument has type "dict[Any, Any]") [assignment] +environments/models.py:246: error: Incompatible default for argument "project_id" (default has type "None", argument has type "int") [assignment] +environments/models.py:449: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True +organisations/urls.py:172: error: Cannot find implementation or library stub for module named "rbac.views" [import-not-found] +environments/models.py:448: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +environments/models.py:115: error: Need type annotation for "hide_disabled_flags" [var-annotated] +environments/models.py:449: error: Incompatible default for argument "identity_identifier" (default has type "None", argument has type "str") [assignment] +environments/models.py:76: error: Need type annotation for "uuid" [var-annotated] +environments/models.py:14: error: Skipping analyzing "django_lifecycle": module is installed, but missing library stubs or py.typed marker [import-untyped] +environments/models.py:505: error: Need type annotation for "key" [var-annotated] +environments/models.py:292: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True +environments/models.py:450: error: Incompatible default for argument "feature_segment" (default has type "None", argument has type "FeatureSegment") [assignment] +environments/models.py:66: error: Unsupported dynamic base class "abstract_base_auditable_model_factory" [misc] +environments/models.py:509: error: Need type annotation for "active" [var-annotated] +environments/views.py:9: error: Skipping analyzing "drf_yasg": module is installed, but missing library stubs or py.typed marker [import-untyped] +environments/models.py:110: error: Need type annotation for "banner_colour" [var-annotated] +environments/models.py:246: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +environments/models.py:506: error: Need type annotation for "created_at" [var-annotated] +environments/models.py:99: error: Need type annotation for "webhooks_enabled" [var-annotated] +organisations/subscriptions/metadata.py:34: error: Unsupported operand types for + ("None" and "int") [operator] +organisations/subscriptions/metadata.py:34: note: Both left and right operands are unions +environments/models.py:105: error: Need type annotation for "updated_at" [var-annotated] +environments/serializers.py:85: error: Incompatible types in assignment (expression has type tuple[str, str, ... <16 more items>], base class "Meta" defined the type as tuple[str, str, ... <15 more items>]) [assignment] +environments/models.py:97: error: Need type annotation for "minimum_change_request_approvals" [var-annotated] diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index bbe4102330f7..41c39911b4e6 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -76,6 +76,43 @@ def test_retrieve_environment( ) +def test_get_by_uuid_returns_environment( + staff_client: APIClient, + environment: Environment, + with_environment_permissions: WithEnvironmentPermissionsCallable, +) -> None: + # Given + with_environment_permissions([VIEW_ENVIRONMENT]) + + url = reverse( + "api-v1:environments:environment-get-by-uuid", + args=[environment.uuid], + ) + + # When + response = staff_client.get(url) + + # Then + assert response.status_code == status.HTTP_200_OK + assert response.json()["uuid"] == str(environment.uuid) + + +def test_get_by_uuid_returns_403_for_user_without_permission( + staff_client: APIClient, environment: Environment +) -> None: + # Given + url = reverse( + "api-v1:environments:environment-get-by-uuid", + args=[environment.uuid], + ) + + # When + response = staff_client.get(url) + + # Then + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_user_with_view_environment_permission_can_retrieve_environment( staff_client: APIClient, environment: Environment, diff --git a/api/tests/unit/features/versioning/test_unit_versioning_migrations.py b/api/tests/unit/features/versioning/test_unit_versioning_migrations.py new file mode 100644 index 000000000000..88fc94b4d246 --- /dev/null +++ b/api/tests/unit/features/versioning/test_unit_versioning_migrations.py @@ -0,0 +1,154 @@ +from datetime import timedelta + +from django.utils import timezone +from django_test_migrations.migrator import Migrator + + +def test_fix_scheduled_fs_data_issue_caused_by_enabling_versioning( + migrator: Migrator +) -> None: + # Given + now = timezone.now() + one_hour_from_now = now + timedelta(hours=1) + + initial_state = migrator.apply_initial_migration(("feature_versioning", "0004_add_version_change_set")) + + organisation_model_class = initial_state.apps.get_model("organisations", "Organisation") + project_model_class = initial_state.apps.get_model("projects", "Project") + environment_model_class = initial_state.apps.get_model("environments", "Environment") + change_request_model_class = initial_state.apps.get_model("workflows_core", "ChangeRequest") + environment_feature_version_model_class = initial_state.apps.get_model( + "feature_versioning", "EnvironmentFeatureVersion" + ) + feature_state_model_class = initial_state.apps.get_model("features", "FeatureState") + feature_model_class = initial_state.apps.get_model("features", "Feature") + + organisation = organisation_model_class.objects.create(name="Test Organisation") + project = project_model_class.objects.create(name="Test Project", organisation=organisation) + environment_1 = environment_model_class.objects.create(name="Environment 1", project=project) + feature = feature_model_class.objects.create(name="test_feature", project=project) + + # First, let's create some regular data that should be left untouched for a + # non-versioned environment + # Note: because migrations don't trigger the signals, we need to manually create + # the default state + environment_1_default_feature_state = feature_state_model_class.objects.create( + feature=feature, + environment=environment_1, + version=1, + ) + environment_1_live_cr = change_request_model_class.objects.create( + title="Live CR for Environment 1", + environment=environment_1, + ) + environment_1_live_feature_state = feature_state_model_class.objects.create( + feature=feature, + environment=environment_1, + enabled=True, + live_from=one_hour_from_now, + version=2, + change_request=environment_1_live_cr, + ) + + # ... and a versioned environment + versioned_environment = environment_model_class.objects.create( + name="Versioned Environment", + project=project, + use_v2_feature_versioning=True, + ) + versioned_environment_version_1 = environment_feature_version_model_class.objects.create( + feature=feature, + environment=versioned_environment, + ) + versioned_environment_default_feature_state = feature_state_model_class.objects.create( + feature=feature, + environment=versioned_environment, + environment_feature_version=versioned_environment_version_1, + ) + versioned_environment_cr = change_request_model_class.objects.create( + environment=versioned_environment, + title="Versioned Environment CR", + ) + versioned_environment_cr_version = environment_feature_version_model_class.objects.create( + feature=feature, + environment=versioned_environment, + change_request=versioned_environment_cr, + ) + versioned_environment_change_request_feature_state = feature_state_model_class.objects.create( + feature=feature, + environment=versioned_environment, + environment_feature_version=versioned_environment_cr_version, + ) + + # Now, let's create the corrupt data that we want to correct + environment_2 = environment_model_class.objects.create(name="Environment 2", project=project) + # Note: because migrations don't trigger the signals, we need to manually create + # the default state + feature_state_model_class.objects.create( + feature=feature, + environment=environment_2, + version=1, + ) + environment_2_scheduled_cr = change_request_model_class.objects.create( + title="Environment 2 Scheduled CR", + environment=environment_2, + ) + environment_2_scheduled_feature_state = feature_state_model_class.objects.create( + feature=feature, + environment=environment_2, + live_from=one_hour_from_now, + version=2, + change_request=environment_2_scheduled_cr, + ) + + # and let's explicitly write out the corruption steps as they would have occurred + # when any environment has feature versioning enabled. + # 1. The scheduled feature state for environment 2 would be associated with a new + # environment feature version in that environment. + new_version = environment_feature_version_model_class.objects.create( + environment=versioned_environment, + feature=feature, + ) + environment_2_scheduled_feature_state.environment_feature_version = new_version + + # 2. The change request would be removed from the feature state + environment_2_scheduled_feature_state.change_request = None + + # 3. The change request would be added to the new version instead + new_version.change_request = environment_2_scheduled_cr + + # 4. Let's save the models with the modifications + new_version.save() + environment_2_scheduled_feature_state.save() + + # When + new_state = migrator.apply_tested_migration( + ( + "feature_versioning", + "0005_fix_scheduled_fs_data_issue_caused_by_enabling_versioning", + ) + ) + + # Then + feature_state_model_class = new_state.apps.get_model("features", "FeatureState") + + # Let's check that the regular data is untouched + assert feature_state_model_class.objects.get( + pk=environment_1_default_feature_state.pk + ).environment_feature_version is None + assert feature_state_model_class.objects.get( + pk=environment_1_live_feature_state.pk + ).environment_feature_version is None + assert feature_state_model_class.objects.get( + pk=versioned_environment_default_feature_state.pk + ).environment_feature_version_id == versioned_environment_version_1.pk + assert feature_state_model_class.objects.get( + pk=versioned_environment_change_request_feature_state.pk + ).environment_feature_version_id == versioned_environment_cr_version.pk + + # And let's check the corrupted data issue has been solved + new_environment_2_scheduled_feature_state = feature_state_model_class.objects.get( + pk=environment_2_scheduled_feature_state.pk + ) + assert new_environment_2_scheduled_feature_state.environment_feature_version is None + assert new_environment_2_scheduled_feature_state.change_request_id == environment_2_scheduled_cr.pk diff --git a/api/tests/unit/features/versioning/test_unit_versioning_tasks.py b/api/tests/unit/features/versioning/test_unit_versioning_tasks.py index bc701c2156b9..2ae8ce082a56 100644 --- a/api/tests/unit/features/versioning/test_unit_versioning_tasks.py +++ b/api/tests/unit/features/versioning/test_unit_versioning_tasks.py @@ -32,6 +32,7 @@ get_environment_flags_queryset, ) from features.workflows.core.models import ChangeRequest +from organisations.models import Organisation from projects.models import Project from segments.models import Segment from users.models import FFAdminUser @@ -233,6 +234,36 @@ def test_enable_v2_versioning_for_scheduled_changes( version=None, ) + # and a published, scheduled feature state associated with a different project + # Note: this additional test clause is to verify an issue found in testing + # where enabling feature versioning 'stole' scheduled feature states from other + # projects. + another_organisation = Organisation.objects.create(name="another organisation") + staff_user.add_organisation(another_organisation) + another_project = Project.objects.create( + name="another project", organisation=another_organisation + ) + another_environment = Environment.objects.create( + name="another environment", project=another_project + ) + another_feature = Feature.objects.create( + name="another_feature", project=another_project + ) + published_scheduled_cr_another_environment = ChangeRequest.objects.create( + environment=another_environment, + title="Published, scheduled change in another environment", + user=staff_user, + ) + another_environment_fs = FeatureState.objects.create( + feature=another_feature, + enabled=True, + environment=another_environment, + live_from=two_hours_from_now, + change_request=published_scheduled_cr_another_environment, + version=None, + ) + published_scheduled_cr_another_environment.commit(staff_user) + # When enable_v2_versioning(environment.id) @@ -260,6 +291,13 @@ def test_enable_v2_versioning_for_scheduled_changes( == scheduled_feature_state ) + another_environment_fs.refresh_from_db() + assert another_environment_fs.environment_feature_version is None + assert ( + another_environment_fs.change_request + == published_scheduled_cr_another_environment + ) + def test_publish_version_change_set_sends_email_to_change_request_owner_if_conflicts_when_scheduled( feature: Feature, diff --git a/api/tests/unit/features/versioning/test_unit_versioning_views.py b/api/tests/unit/features/versioning/test_unit_versioning_views.py index 7eba620a1b06..074b0665d63f 100644 --- a/api/tests/unit/features/versioning/test_unit_versioning_views.py +++ b/api/tests/unit/features/versioning/test_unit_versioning_views.py @@ -1639,8 +1639,7 @@ def test_list_versions_always_returns_current_version_even_if_outside_limit( @pytest.mark.freeze_time(now - timedelta(days=DEFAULT_VERSION_LIMIT_DAYS + 1)) -@pytest.mark.parametrize("is_saas", (True, False)) -def test_list_versions_returns_all_versions_for_enterprise_plan( +def test_list_versions_returns_all_versions_for_enterprise_plan_when_saas( feature: Feature, environment_v2_versioning: Environment, staff_user: FFAdminUser, @@ -1649,10 +1648,10 @@ def test_list_versions_returns_all_versions_for_enterprise_plan( with_project_permissions: WithProjectPermissionsCallable, subscription: Subscription, freezer: FrozenDateTimeFactory, - is_saas: bool, mocker: MockerFixture, ) -> None: # Given + is_saas = True with_environment_permissions([VIEW_ENVIRONMENT]) with_project_permissions([VIEW_PROJECT]) @@ -1668,11 +1667,10 @@ def test_list_versions_returns_all_versions_for_enterprise_plan( subscription.plan = "enterprise" subscription.save() - if is_saas: - OrganisationSubscriptionInformationCache.objects.update_or_create( - organisation=subscription.organisation, - defaults={"feature_history_visibility_days": None}, - ) + OrganisationSubscriptionInformationCache.objects.update_or_create( + organisation=subscription.organisation, + defaults={"feature_history_visibility_days": None}, + ) initial_version = EnvironmentFeatureVersion.objects.get( feature=feature, environment=environment_v2_versioning diff --git a/api/tests/unit/organisations/invites/test_unit_invites_views.py b/api/tests/unit/organisations/invites/test_unit_invites_views.py index bdaecdb0f5e0..b9082f0d9dfb 100644 --- a/api/tests/unit/organisations/invites/test_unit_invites_views.py +++ b/api/tests/unit/organisations/invites/test_unit_invites_views.py @@ -14,7 +14,7 @@ from organisations.invites.models import Invite, InviteLink from organisations.models import Organisation, OrganisationRole, Subscription -from users.models import FFAdminUser +from users.models import FFAdminUser, HubspotTracker def test_create_invite_link( @@ -166,13 +166,17 @@ def test_join_organisation_with_permission_groups( subscription.save() url = reverse("api-v1:users:user-join-organisation", args=[invite.hash]) + data = {"hubspotutk": "somehubspotdata"} # When - response = test_user_client.post(url) + response = test_user_client.post(url, data) test_user.refresh_from_db() # Then assert response.status_code == status.HTTP_200_OK + hubspot_tracker = HubspotTracker.objects.first() + assert hubspot_tracker.user == test_user + assert organisation in test_user.organisations.all() assert user_permission_group in test_user.permission_groups.all() # and invite is deleted diff --git a/api/tests/unit/organisations/test_unit_organisations_models.py b/api/tests/unit/organisations/test_unit_organisations_models.py index aa76002cd5cd..a3ace82a8e31 100644 --- a/api/tests/unit/organisations/test_unit_organisations_models.py +++ b/api/tests/unit/organisations/test_unit_organisations_models.py @@ -336,50 +336,6 @@ def test_organisation_subscription_get_subscription_metadata_returns_free_plan_m assert subscription_metadata == FREE_PLAN_SUBSCRIPTION_METADATA -@pytest.mark.parametrize( - "subscription_id, plan, max_seats, expected_seats, expected_projects", - ( - ( - None, - "free", - 10, - MAX_SEATS_IN_FREE_PLAN, - settings.MAX_PROJECTS_IN_FREE_PLAN, - ), - ("anything", "enterprise", 20, 20, None), - (TRIAL_SUBSCRIPTION_ID, "enterprise", 20, 20, None), - ), -) -def test_organisation_get_subscription_metadata_for_enterprise_self_hosted_licenses( - organisation: Organisation, - subscription_id: str | None, - plan: str, - max_seats: int, - expected_seats: int, - expected_projects: int | None, - mocker: MockerFixture, -) -> None: - """ - Specific test to make sure that we can manually add subscriptions to - enterprise self-hosted deployments and the values stored in the django - database will be correctly used. - """ - # Given - Subscription.objects.filter(organisation=organisation).update( - subscription_id=subscription_id, plan=plan, max_seats=max_seats - ) - organisation.subscription.refresh_from_db() - mocker.patch("organisations.models.is_saas", return_value=False) - mocker.patch("organisations.models.is_enterprise", return_value=True) - - # When - subscription_metadata = organisation.subscription.get_subscription_metadata() - - # Then - assert subscription_metadata.projects == expected_projects - assert subscription_metadata.seats == expected_seats - - @pytest.mark.parametrize( "subscription_id, plan, max_seats, max_api_calls, expected_seats, " "expected_api_calls, expected_projects", diff --git a/api/tests/unit/organisations/test_unit_organisations_views.py b/api/tests/unit/organisations/test_unit_organisations_views.py index d55b28e4fb20..2f0fd2cdd123 100644 --- a/api/tests/unit/organisations/test_unit_organisations_views.py +++ b/api/tests/unit/organisations/test_unit_organisations_views.py @@ -71,6 +71,42 @@ def test_should_return_organisation_list_when_requested( assert response.data["results"][0]["name"] == organisation.name +def test_get_by_uuid_returns_organisation( + admin_client: APIClient, + organisation: Organisation, +) -> None: + # Given + url = reverse( + "api-v1:organisations:organisation-get-by-uuid", + args=[organisation.uuid], + ) + + # When + response = admin_client.get(url) + + # Then + assert response.status_code == status.HTTP_200_OK + assert response.json()["uuid"] == str(organisation.uuid) + + +def test_get_by_uuid_returns_404_for_organisation_that_does_not_belong_to_the_user( + admin_client: APIClient, + organisation: Organisation, +) -> None: + # Given + different_org = Organisation.objects.create(name="Different org") + url = reverse( + "api-v1:organisations:organisation-get-by-uuid", + args=[different_org.uuid], + ) + + # When + response = admin_client.get(url) + + # Then + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_non_superuser_can_create_new_organisation_by_default( staff_client: APIClient, staff_user: FFAdminUser, @@ -82,8 +118,9 @@ def test_non_superuser_can_create_new_organisation_by_default( data = { "name": org_name, "webhook_notification_email": webhook_notification_email, + HUBSPOT_COOKIE_NAME: "test_cookie_tracker", } - staff_client.cookies[HUBSPOT_COOKIE_NAME] = "test_cookie_tracker" + assert not HubspotTracker.objects.filter(user=staff_user).exists() # When diff --git a/api/tests/unit/users/test_unit_users_views.py b/api/tests/unit/users/test_unit_users_views.py index f66f8cf01c71..2856d3afbfa3 100644 --- a/api/tests/unit/users/test_unit_users_views.py +++ b/api/tests/unit/users/test_unit_users_views.py @@ -35,11 +35,11 @@ def test_join_organisation( organisation = Organisation.objects.create(name="test org") invite = Invite.objects.create(email=staff_user.email, organisation=organisation) url = reverse("api-v1:users:user-join-organisation", args=[invite.hash]) - staff_client.cookies[HUBSPOT_COOKIE_NAME] = "test_cookie_tracker" + data = {HUBSPOT_COOKIE_NAME: "test_cookie_tracker"} assert not HubspotTracker.objects.filter(user=staff_user).exists() # When - response = staff_client.post(url) + response = staff_client.post(url, data) staff_user.refresh_from_db() # Then @@ -56,11 +56,11 @@ def test_join_organisation_via_link( organisation = Organisation.objects.create(name="test org") invite = InviteLink.objects.create(organisation=organisation) url = reverse("api-v1:users:user-join-organisation-link", args=[invite.hash]) - staff_client.cookies[HUBSPOT_COOKIE_NAME] = "test_cookie_tracker" + data = {HUBSPOT_COOKIE_NAME: "test_cookie_tracker"} assert not HubspotTracker.objects.filter(user=staff_user).exists() # When - response = staff_client.post(url) + response = staff_client.post(url, data) staff_user.refresh_from_db() # Then diff --git a/docker-compose.pgpool.yml b/docker-compose.pgpool.yml new file mode 100644 index 000000000000..454f812196f4 --- /dev/null +++ b/docker-compose.pgpool.yml @@ -0,0 +1,62 @@ +services: + pg-0: + extends: + file: ./docker/common/db/.pgpool.yml + service: pg-0 + volumes: + - pg_0_data:/bitnami/postgresql + + pg-1: + extends: + file: ./docker/common/db/.pgpool.yml + service: pg-1 + volumes: + - pg_1_data:/bitnami/postgresql + + pgpool: + extends: + file: ./docker/common/db/.pgpool.yml + service: pgpool + + flagsmith: + image: flagsmith/flagsmith:latest + platform: linux/arm64 + environment: + DATABASE_URL: postgresql://flagsmith:password@pgpool:5432/flagsmith + USE_POSTGRES_FOR_ANALYTICS: 'true' # Store API and Flag Analytics data in Postgres + + ENVIRONMENT: production # set to 'production' in production. + DJANGO_ALLOWED_HOSTS: '*' # Change this in production + ALLOW_ADMIN_INITIATION_VIA_CLI: 'true' # Change this in production + FLAGSMITH_DOMAIN: localhost:8000 # Change this in production + DJANGO_SECRET_KEY: secret # Change this in production + ENABLE_ADMIN_ACCESS_USER_PASS: 'true' + TASK_RUN_METHOD: TASK_PROCESSOR # other options are: SYNCHRONOUSLY, SEPARATE_THREAD (default) + ports: + - 8000:8000 + healthcheck: + test: ['CMD-SHELL', 'python /app/scripts/healthcheck.py'] + interval: 2s + timeout: 2s + retries: 20 + start_period: 20s + depends_on: + pgpool: + condition: service_healthy + + flagsmith_processor: + image: flagsmith/flagsmith:latest + platform: linux/arm64 + environment: + DATABASE_URL: postgresql://flagsmith:password@pgpool:5432/flagsmith + USE_POSTGRES_FOR_ANALYTICS: 'true' + depends_on: + flagsmith: + condition: service_healthy + command: run-task-processor + +volumes: + pg_0_data: + driver: local + pg_1_data: + driver: local diff --git a/docker/common/db/.pgpool.yml b/docker/common/db/.pgpool.yml new file mode 100644 index 000000000000..404c286d2d8c --- /dev/null +++ b/docker/common/db/.pgpool.yml @@ -0,0 +1,62 @@ +# Copyright Broadcom, Inc. All Rights Reserved. +# SPDX-License-Identifier: APACHE-2.0 + +services: + pg-0: + image: docker.io/bitnami/postgresql-repmgr:15 + ports: + - 5432 + environment: + - POSTGRESQL_POSTGRES_PASSWORD=password + - POSTGRESQL_USERNAME=flagsmith + - POSTGRESQL_PASSWORD=password + - POSTGRESQL_DATABASE=flagsmith + - POSTGRESQL_NUM_SYNCHRONOUS_REPLICAS=1 + - REPMGR_PRIMARY_HOST=pg-0 + - REPMGR_PARTNER_NODES=pg-1,pg-0 + - REPMGR_NODE_NAME=pg-0 + - REPMGR_NODE_NETWORK_NAME=pg-0 + - REPMGR_USERNAME=repmgr + - REPMGR_PASSWORD=repmgrpassword + + pg-1: + image: docker.io/bitnami/postgresql-repmgr:15 + ports: + - 5432 + environment: + - POSTGRESQL_POSTGRES_PASSWORD=password + - POSTGRESQL_USERNAME=flagsmith + - POSTGRESQL_PASSWORD=password + - POSTGRESQL_DATABASE=flagsmith + - POSTGRESQL_NUM_SYNCHRONOUS_REPLICAS=1 + - REPMGR_PRIMARY_HOST=pg-0 + - REPMGR_PARTNER_NODES=pg-0,pg-1 + - REPMGR_NODE_NAME=pg-1 + - REPMGR_NODE_NETWORK_NAME=pg-1 + - REPMGR_USERNAME=repmgr + - REPMGR_PASSWORD=repmgrpassword + + pgpool: + image: docker.io/bitnami/pgpool:4 + ports: + - 5432:5432 + environment: + - PGPOOL_BACKEND_NODES=0:pg-0:5432,1:pg-1:5432 + - PGPOOL_SR_CHECK_USER=repmgr + - PGPOOL_SR_CHECK_PASSWORD=repmgrpassword + - PGPOOL_ENABLE_LDAP=no + - PGPOOL_POSTGRES_USERNAME=postgres + - PGPOOL_POSTGRES_PASSWORD=password + - PGPOOL_ADMIN_USERNAME=admin + - PGPOOL_ADMIN_PASSWORD=adminpassword + - PGPOOL_ENABLE_LOAD_BALANCING=yes + - PGPOOL_POSTGRES_CUSTOM_USERS=flagsmith + - PGPOOL_POSTGRES_CUSTOM_PASSWORDS=password + healthcheck: + test: ['CMD', '/opt/bitnami/scripts/pgpool/healthcheck.sh'] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + - pg-0 + - pg-1 diff --git a/docker/common/e2e/.e2e-tests.yml b/docker/common/e2e/.e2e-tests.yml new file mode 100644 index 000000000000..dfe9592189a4 --- /dev/null +++ b/docker/common/e2e/.e2e-tests.yml @@ -0,0 +1,42 @@ +services: + flagsmith-api: + image: ${API_IMAGE:-ghcr.io/flagsmith/flagsmith-api:dev} + build: + context: ../../../ + target: oss-api + environment: + E2E_TEST_AUTH_TOKEN: some-token + ENABLE_FE_E2E: 'True' + DJANGO_ALLOWED_HOSTS: '*' + DISABLE_ANALYTICS_FEATURES: 'true' + EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend + ACCESS_LOG_LOCATION: /dev/shm/log.txt + ports: + - 8000:8000 + healthcheck: + test: '[ -e /dev/shm/log.txt ] && exit 0 || exit 1' + start_period: 60s + interval: 10s + timeout: 3s + retries: 30 + + frontend: + image: ${E2E_IMAGE:-ghcr.io/flagsmith/flagsmith-e2e:dev} + build: + context: ../../../ + dockerfile: frontend/Dockerfile.e2e + environment: + E2E_TEST_TOKEN_DEV: some-token + DISABLE_ANALYTICS_FEATURES: 'true' + FLAGSMITH_API: flagsmith-api:8000/api/v1/ + SLACK_TOKEN: ${SLACK_TOKEN} + GITHUB_ACTION_URL: ${GITHUB_ACTION_URL} + ports: + - 8080:8080 + depends_on: + flagsmith-api: + condition: service_healthy + + links: + - flagsmith-api:flagsmith-api + command: [npm, run, test] diff --git a/docker/db.yml b/docker/db.yml deleted file mode 100644 index a98f1f82053c..000000000000 --- a/docker/db.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: flagsmith - -volumes: - pg_11_data: - -services: - db: - image: postgres:15.5-alpine - pull_policy: always - restart: unless-stopped - volumes: - - pg_11_data:/var/lib/postgresql/data - ports: - - 5432:5432 - environment: - POSTGRES_DB: flagsmith - POSTGRES_PASSWORD: password diff --git a/docs/docs/clients/client-side/ios.mdx b/docs/docs/clients/client-side/ios.mdx index 499a0b0d2516..a08dfe773189 100644 --- a/docs/docs/clients/client-side/ios.mdx +++ b/docs/docs/clients/client-side/ios.mdx @@ -90,8 +90,8 @@ Flagsmith.shared.getFeatureFlags() { (result) in Note that you can use: -- `flag.value?.stringValue` -- `flag.value?.intValue` +- `flag.value?.stringValue` +- `flag.value?.intValue` Based on your desired type. diff --git a/docs/docs/clients/server-side.mdx b/docs/docs/clients/server-side.mdx index 242595ffa63f..61379725c70d 100644 --- a/docs/docs/clients/server-side.mdx +++ b/docs/docs/clients/server-side.mdx @@ -8,7 +8,13 @@ import CodeBlock from '@theme/CodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -import { JavaVersion, RustVersion, DotnetVersion, ElixirVersion } from '@site/src/components/SdkVersions.js'; +import { + JavaVersion, + RustVersion, + DotnetVersion, + ElixirVersion, + NodejsVersion, +} from '@site/src/components/SdkVersions.js'; # Server Side SDKs @@ -24,57 +30,56 @@ Server Side SDKs can run in 2 different modes: Local Evaluation and Remote Evalu -- Version Compatibility: **Python 3.8+** -- Source Code: https://github.com/Flagsmith/flagsmith-python-client +- Version Compatibility: **Python 3.8+** +- Source Code: https://github.com/Flagsmith/flagsmith-python-client -- Version Compatibility: **JDK 11+** -- Source Code: https://github.com/Flagsmith/flagsmith-java-client +- Version Compatibility: **JDK 11+** +- Source Code: https://github.com/Flagsmith/flagsmith-java-client -- Version Compatibility: **.NET core 6.0+** -- Source Code: https://github.com/Flagsmith/flagsmith-dotnet-client +- Version Compatibility: **.NET core 6.0+** +- Source Code: https://github.com/Flagsmith/flagsmith-dotnet-client - + -- Version Compatibility: **Node 14+** -- Source Code: - - https://github.com/Flagsmith/flagsmith-nodejs-client +- Version Compatibility: **Node 18+** +- Source Code: https://github.com/Flagsmith/flagsmith-nodejs-client -- Version Compatibility: **Ruby 2.4+** -- Source Code: https://github.com/Flagsmith/flagsmith-ruby-client +- Version Compatibility: **Ruby 2.4+** +- Source Code: https://github.com/Flagsmith/flagsmith-ruby-client -- Version Compatibility: **php 7.4+** -- Source Code: https://github.com/Flagsmith/flagsmith-php-client +- Version Compatibility: **php 7.4+** +- Source Code: https://github.com/Flagsmith/flagsmith-php-client -- Version Compatibility: **Go 1.18+** -- Source Code: https://github.com/Flagsmith/flagsmith-go-client +- Version Compatibility: **Go 1.18+** +- Source Code: https://github.com/Flagsmith/flagsmith-go-client -- Version Compatibility: **2021 edition (1.56.0)+** -- Source Code: https://github.com/Flagsmith/flagsmith-rust-client +- Version Compatibility: **2021 edition (1.56.0)+** +- Source Code: https://github.com/Flagsmith/flagsmith-rust-client -- Version Compatibility: **Elixir 1.12+** -- Source Code: https://github.com/Flagsmith/flagsmith-elixir-client +- Version Compatibility: **Elixir 1.12+** +- Source Code: https://github.com/Flagsmith/flagsmith-elixir-client @@ -140,11 +145,10 @@ pip install flagsmith - + ```bash -# Via NPM -npm i flagsmith-nodejs --save +npm install flagsmith-nodejs ``` @@ -245,10 +249,10 @@ _flagsmithClient = new("FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY"); ``` - + ```javascript -const Flagsmith = require('flagsmith-nodejs'); +import { Flagsmith } from 'flagsmith-nodejs'; const flagsmith = new Flagsmith({ environmentKey: 'FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY', @@ -367,12 +371,12 @@ var buttonData = await flags.GetFeatureValue("secret_button"); ``` - + ```javascript const flags = await flagsmith.getEnvironmentFlags(); -var showButton = flags.isFeatureEnabled('secret_button'); -var buttonData = flags.getFeatureValue('secret_button'); +const showButton = flags.isFeatureEnabled('secret_button'); +const buttonData = flags.getFeatureValue('secret_button'); ``` @@ -479,7 +483,7 @@ var showButton = await flags.IsFeatureEnabled("secret_button"); ``` - + ```javascript const identifier = 'delboy@trotterstraders.co.uk'; @@ -572,19 +576,19 @@ secret_button_feature_value = Flagsmith.Client.get_feature_value(flags, "secret_ ### When running in [Remote Evaluation mode](/clients#remote-evaluation) -- When requesting Flags for an Identity, all the Traits defined in the SDK will automatically be persisted against the - Identity within the Flagsmith API. -- Traits passed to the SDK will be added to all the other previously persisted Traits associated with that Identity. -- This full set of Traits are then used to evaluate the Flag values for the Identity. -- This all happens in a single request/response. +- When requesting Flags for an Identity, all the Traits defined in the SDK will automatically be persisted against the + Identity within the Flagsmith API. +- Traits passed to the SDK will be added to all the other previously persisted Traits associated with that Identity. +- This full set of Traits are then used to evaluate the Flag values for the Identity. +- This all happens in a single request/response. ### When running in [Local Evaluation mode](/clients#local-evaluation) -- _Only_ the Traits provided to the SDK at runtime will be used. Local Evaluation mode, by design, does not make any - network requests to the Flagsmith API when evaluating Flags for an Identity. - - When running in Local Evaluation Mode, the SDK requests the - [Environment Document](/clients#the-environment-document) from the Flagsmith API. This contains all the - information required to make Flag Evaluations, but it does _not_ contain any Trait data. +- _Only_ the Traits provided to the SDK at runtime will be used. Local Evaluation mode, by design, does not make any + network requests to the Flagsmith API when evaluating Flags for an Identity. + - When running in Local Evaluation Mode, the SDK requests the + [Environment Document](/clients#the-environment-document) from the Flagsmith API. This contains all the + information required to make Flag Evaluations, but it does _not_ contain any Trait data. ## Managing Default Flags @@ -662,7 +666,7 @@ static Flag defaultFlagHandler(string featureName) ``` - + ```javascript const flagsmith = new Flagsmith({ @@ -853,18 +857,32 @@ public class MyCustomOfflineHandler: BaseOfflineHandler ``` - + -```javascript -// Using the built-in local file handler -const localFileHandler = new LocalFileHandler('path_to_environment_file/environment_file.json'); -const flagsmith = new Flagsmith({ offlineMode: true, offlineHandler: localFileHandler }); +Use LocalFileHandler +to read an environment file generated by the [Flagsmith CLI](/clients/CLI): -// Defining a custom offline handler -class CustomOfflineHandler extends BaseOfflineHandler { - getEnvironment(): EnvironmentModel { - return someMethodToGetTheEnvironment(); - } +```typescript +import { Flagsmith, LocalFileHandler } from 'flagsmith-nodejs'; + +const flagsmith = new Flagsmith({ + offlineMode: true, + offlineHandler: new LocalFileHandler('./flagsmith.json'), +}); +``` + +To create your own offline handler, implement the `BaseOfflineHandler` interface. It must return an +{/* prettier-ignore */}EnvironmentModel +object: + +```typescript +import type { BaseOfflineHandler, EnvironmentModel } from 'flagsmith-nodejs'; + +class CustomOfflineHandler implements BaseOfflineHandler { + getEnvironment(): EnvironmentModel { + // ... + } } ``` @@ -983,10 +1001,10 @@ The Server Side SDKS share the same network behaviour across the different langu ### Remote Evaluation Mode Network Behaviour -- A blocking network request is made every time you make a call to get an Environment Flags. In Python, for example, - `flagsmith.get_environment_flags()` will trigger this request. -- A blocking network request is made every time you make a call to get an Identities Flags. In Python, for example, - `flagsmith.get_identity_flags(identifier=identifier, traits=traits)` will trigger this request. +- A blocking network request is made every time you make a call to get an Environment Flags. In Python, for example, + `flagsmith.get_environment_flags()` will trigger this request. +- A blocking network request is made every time you make a call to get an Identities Flags. In Python, for example, + `flagsmith.get_identity_flags(identifier=identifier, traits=traits)` will trigger this request. ### Local Evaluation Mode Network Behaviour @@ -998,9 +1016,9 @@ To use Local Evaluation mode, you must use a Server Side key. ::: -- When the SDK is initialised, it will make an asynchronous network request to retrieve details about the Environment. -- Every 60 seconds (by default), it will repeat this aysnchronous request to ensure that the Environment information - it has is up to date. +- When the SDK is initialised, it will make an asynchronous network request to retrieve details about the Environment. +- Every 60 seconds (by default), it will repeat this aysnchronous request to ensure that the Environment information it + has is up to date. To achieve Local Evaluation, in most languages, the SDK spawns a separate thread (or equivalent) to poll the API for changes to the Environment. In certain languages, you may be required to terminate this thread before cleaning up the @@ -1015,10 +1033,9 @@ flagsmith.close(); ``` - + ```javascript -// available from v2.2.1 flagsmith.close(); ``` @@ -1033,15 +1050,6 @@ Evaluation mode. Please see [caching](#caching) below. ### Offline Mode -:::info - -Offline mode is still in active development for some SDKs. We are building it for all our SDKs; those that are -production ready are listed below. - -Progress on the remaining SDKs can be seen [here](https://github.com/Flagsmith/flagsmith/issues/2024). - -::: - To run the SDK in a fully offline mode, you can set the client to offline mode. This will prevent the SDK from making any calls to the Flagsmith API. To use offline mode, you must also provide an [offline handler](server-side#using-an-offline-handler). See [Configuring the SDK](server-side#configuring-the-sdk) for @@ -1413,10 +1421,11 @@ $flagsmith = Flagsmith::Client.new( ``` - + ```typescript -import { bool, number } from 'prop-types'; +import { Flagsmith } from 'flagsmith-nodejs'; +import type { EnvironmentModel } from 'flagsmith-nodejs'; const flagsmith = new Flagsmith({ /* @@ -1440,9 +1449,8 @@ const flagsmith = new Flagsmith({ See https://docs.flagsmith.com/clients/server-side#caching */ cache: { - has: (key: string) => bool, - get: (key: string) => string | number | null, - set: (k: string, v: Flags) => (cache[k] = v), + get: (key: string) => Promise.resolve(), + set: (k: string, v: Flags) => Promise.resolve(), }, /* @@ -1780,7 +1788,8 @@ client_configuration = Flagsmith.Client.new( ## Caching -The following SDKs have code and functionality related to caching flags. +Some SDKs support caching flags retrieved from the Flagsmith API, or calculated from your environment definition if +using Local Evaluation. @@ -1793,9 +1802,9 @@ Flagsmith uses [Caffeine](https://github.com/ben-manes/caffeine), a high perform If you enable caching on the Flagsmith client without setting any values (as shown below), the following default values will be set for you: -- `maxSize(10)` -- `expireAfterWrite(5, TimeUnit.MINUTES)` -- project level caching will be disabled by default (i.e. only enabled if you configure a caching key) +- `maxSize(10)` +- `expireAfterWrite(5, TimeUnit.MINUTES)` +- project level caching will be disabled by default (i.e. only enabled if you configure a caching key) ```java // use in-memory caching with Flagsmith defaults as described above @@ -1874,68 +1883,37 @@ final FlagsAndTraits flags = cache.getIfPresent(projectLevelCacheKey); ``` - - -You can initialise the SDK with something like this: + -```javascript -flagsmith.init({ - cache: { - has:(key)=> return Promise.resolve(!!cache[key]) , // true | false - get: (k)=> cache[k] // return flags or flags for user - set: (k,v)=> cache[k] = v // gets called if has returns false with response from API for Identify or getFlags - } -}) -``` +The `cache` option in the `Flagsmith` constructor accepts a cache implementation. This cache must implement the +{/* prettier-ignore */}FlagsmithCache +interface. -The core concept is that if `has` returns false, the SDK will make the required API calls under the hood. The keys are -either `flags` or `flags_traits-${identity}`. +For example, this cache implementation uses Redis as a backing store: -An example of a concrete implemention is below. - -```javascript -const flagsmith = require('flagsmith-nodejs'); -const redis = require('redis'); +```typescript +import { Flagsmith } from 'flagsmith-nodejs'; +import type { BaseOfflineHandler, EnvironmentModel, Flags, FlagsmithCache } from 'flagsmith-nodejs'; +import * as redis from 'redis'; const redisClient = redis.createClient({ - host: 'localhost', - port: 6379, + url: 'localhost:6379', }); -flagsmith.init({ - environmentID: 'FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY', - cache: { - has: (key) => - new Promise((resolve, reject) => { - redisClient.exists(key, (err, reply) => { - console.log('check ' + key + ' from cache', err, reply); - resolve(reply === 1); - }); - }), - get: (key) => - new Promise((resolve) => { - redisClient.get(key, (err, cacheValue) => { - console.log('get ' + key + ' from cache'); - resolve(cacheValue && JSON.parse(cacheValue)); - }); - }), - set: (key, value) => - new Promise((resolve) => { - // Expire the key after 60 seconds - redisClient.set(key, JSON.stringify(value), 'EX', 60, (err, reply) => { - console.log('set ' + key + ' to cache', err); - resolve(); - }); - }), +const redisFlagsmithCache = { + async get(key: string): Promise { + const cachedValue = await redisClient.get(key); + return Promise.resolve(cachedValue && JSON.parse(cachedValue)); }, -}); + async set(key: string, value: Flags): Promise { + await redisClient.set(key, JSON.stringify(value), { EX: 60 }); + return Promise.resolve(); + }, +} satisfies FlagsmithCache; -router.get('/', function (req, res, next) { - flagsmith.getValue('background_colour').then((value) => { - res.render('index', { - title: value, - }); - }); +const flagsmith = new Flagsmith({ + environmentKey: 'ser...', + cache: redisFlagsmithCache, }); ``` @@ -1978,11 +1956,11 @@ $flagsmith->updateEnvironment(); Note: -- For the environment cache, please use the server key generated from the Flagsmith Settings menu. The key's prefix is - `ser.`. -- The cache is important for concurrent requests. Without the cache, each request in PHP is a different process with - its own memory objects. The cache (filesystem or other) would enforce that the network call is reduced to a file - system one. +- For the environment cache, please use the server key generated from the Flagsmith Settings menu. The key's prefix is + `ser.`. +- The cache is important for concurrent requests. Without the cache, each request in PHP is a different process with its + own memory objects. The cache (filesystem or other) would enforce that the network call is reduced to a file system + one. @@ -2037,7 +2015,7 @@ https://github.com/Flagsmith/flagsmith-java-client https://github.com/Flagsmith/flagsmith-dotnet-client - + https://github.com/Flagsmith/flagsmith-nodejs-client diff --git a/docs/docs/deployment/hosting/kubernetes.md b/docs/docs/deployment/hosting/kubernetes.md index 34fe81119617..6a07f0396a52 100644 --- a/docs/docs/deployment/hosting/kubernetes.md +++ b/docs/docs/deployment/hosting/kubernetes.md @@ -93,7 +93,7 @@ ingress: - /api/ - /health/ - /admin/ - - /static/ + - /static/admin/ frontend: extraEnv: diff --git a/docs/docs/deployment/hosting/locally-edge-proxy.md b/docs/docs/deployment/hosting/locally-edge-proxy.md index 45849e62d841..b0d5bf69d905 100644 --- a/docs/docs/deployment/hosting/locally-edge-proxy.md +++ b/docs/docs/deployment/hosting/locally-edge-proxy.md @@ -4,102 +4,141 @@ title: Edge Proxy sidebar_position: 25 --- -## Configuration +## Running -The Edge Proxy can be configured using a json configuration file (named `config.json` here). +The Edge Proxy runs as a [Docker container](https://hub.docker.com/repository/docker/flagsmith/edge-proxy) with no +external dependencies. It connects to the Flagsmith API to download environment documents, and your Flagsmith client +applications connect to it using [remote flag evaluation](/clients/#remote-evaluation). -You can set the following configuration in `config.json` to control the behaviour of the Edge Proxy: +The examples below assume you have a configuration file located at `./config.json`. Your Flagsmith client applications +can then consume the Edge Proxy by setting their API URL to `http://localhost:8000/api/v1/`. -### Basic Settings +
+Docker CLI +``` +docker run \ + -v ./config.json:/app/config.json \ + -p 8000:8000 \ + flagsmith/edge-proxy:latest +``` +
-#### `environment_key_pairs` +
+Docker Compose +```yaml +services: + edge_proxy: + image: flagsmith/edge-proxy:latest + volumes: + - type: bind + source: ./config.json + target: /app/config.json + ports: + - '8000:8000' +``` +
-An array of environment key pair objects: +## Configuration -```json -"environment_key_pairs": [{ - "server_side_key": "your_server_side_key", - "client_side_key": "your_client_side_environment_key" -}] -``` +The Edge Proxy can be configured with any combination of: -#### `api_poll_frequency` +- Environment variables. +- A JSON configuration file, by default located at `/app/config.json` in the Edge Proxy container. + +Environment variables take priority over their corresponding options defined in the configuration file. -:::note +Environment variables are case-insensitive, and are processed using +[Pydantic](https://docs.pydantic.dev/2.7/concepts/pydantic_settings/#environment-variable-names). -This setting is optional. +### Example configuration -::: +
-Control how often the Edge Proxy is going to ping the server for changes, in seconds: +Configuration file ```json -"api_poll_frequency": 30 +{ + "environment_key_pairs": [ + { + "server_side_key": "ser.your_server_side_key_1", + "client_side_key": "your_client_side_key_1" + } + ], + "api_poll_frequency": 5, + "logging": { + "log_level": "DEBUG", + "log_format": "json" + } +} ``` -Defaults to `10`. +
-#### `api_poll_timeout` +
-:::note +Environment variables -This setting is optional. +```ruby +ENVIRONMENT_KEY_PAIRS='[{"server_side_key":"ser.your_server_side_key_1","client_side_key":"your_client_side_key_1"}]' +API_POLL_FREQUENCY=5 +LOGGING='{"log_level":"DEBUG","log_format":"json"}' +``` + +
-::: +### Basic Settings -Specify the request timeout when trying to retrieve new changes, in seconds: +#### `environment_key_pairs` + +Specifies which environments to poll environment documents for. Each environment requires a server-side key and its +corresponding client-side key. ```json -"api_poll_timeout": 1 +"environment_key_pairs": [{ + "server_side_key": "ser.your_server_side_key", + "client_side_key": "your_client_side_environment_key" +}] ``` -Defaults to `5`. - #### `api_url` -:::note +If you are self-hosting Flagsmith, set this to your API URL: -This setting is optional. +```json +"api_url": "https://flagsmith.example.com/api/v1" +``` -::: +#### `api_poll_frequency` -Set if you are running a self hosted version of Flagsmith: +How often to poll the Flagsmith API for changes, in seconds. Defaults to 10. ```json -"api_url": "https://my-flagsmith.domain.com/api/v1" +"api_poll_frequency": 30 ``` -If not set, defaults to Flagsmith's Edge API. - -#### `allow_origins` +#### `api_poll_timeout` -:::note +The request timeout when trying to retrieve new changes, in seconds. Defaults to 5. -This setting is optional. +```json +"api_poll_timeout": 1 +``` -::: +#### `allow_origins` -Set a value for the `Access-Control-Allow-Origin` header. +Set a value for the `Access-Control-Allow-Origin` header. Defaults to `*`. ```json "allow_origins": "https://my-flagsmith.domain.com" ``` -If not set, defaults to `*`. - ### Endpoint Caches #### `endpoint_caches` -:::note - -This setting is optional. - -::: +Enables a LRU cache per endpoint. -Enable a LRU cache per endpoint. - -Optionally, specify the LRU cache size with `cache_max_size` (defaults to 128): +Optionally, specify the LRU cache size with `cache_max_size`. Defaults to 128. ```json "endpoint_caches": { @@ -117,12 +156,6 @@ Optionally, specify the LRU cache size with `cache_max_size` (defaults to 128): #### `logging.log_level` -:::note - -This setting is optional. - -::: - Choose a logging level from `"CRITICAL"`, `"ERROR"`, `"WARNING"`, `"INFO"`, `"DEBUG"`. Defaults to `"INFO"`. ```json @@ -131,13 +164,7 @@ Choose a logging level from `"CRITICAL"`, `"ERROR"`, `"WARNING"`, `"INFO"`, `"DE #### `logging.log_format` -:::note - -This setting is optional. - -::: - -Choose a logging forman between `"generic"` and `"json"`. Defaults to `"generic"`. +Choose a logging format between `"generic"` and `"json"`. Defaults to `"generic"`. ```json "logging": {"log_format": "json"} @@ -145,12 +172,6 @@ Choose a logging forman between `"generic"` and `"json"`. Defaults to `"generic" #### `logging.log_event_field_name` -:::note - -This setting is optional. - -::: - Set a name used for human-readable log entry field when logging events in JSON. Defaults to `"message"`. ```json @@ -159,23 +180,13 @@ Set a name used for human-readable log entry field when logging events in JSON. #### `logging.colour` -:::note - -- Added in [2.13.0](https://github.com/Flagsmith/edge-proxy/releases/tag/v2.13.0). -- This setting is optional. - -::: +Added in [2.13.0](https://github.com/Flagsmith/edge-proxy/releases/tag/v2.13.0). Set to `false` to disable coloured output. Useful when outputting the log to a file. #### `logging.override` -:::note - -- Added in [2.13.0](https://github.com/Flagsmith/edge-proxy/releases/tag/v2.13.0). -- This setting is optional. - -::: +Added in [2.13.0](https://github.com/Flagsmith/edge-proxy/releases/tag/v2.13.0). Accepts [Python-compatible logging settings](https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema) @@ -204,7 +215,7 @@ everything a file, one can set up own file handler and assign it to the root log } ``` -Or, log access logs to file in generic format while logging everything else to stdout in json: +Or, log access logs to file in generic format while logging everything else to stdout in JSON: ```json "logging": { @@ -264,23 +275,6 @@ if last_updated_all_environments_at < datetime.now() - timedelta(seconds=total_g return 200 ``` -### Example - -Here's an example of a minimal working Edge Proxy configuration: - -```json -{ - "environment_key_pairs": [ - { - "server_side_key": "your_server_side_environment_key", - "client_side_key": "your_client_side_environment_key" - } - ], - "api_poll_frequency": 10, - "api_url": "https://api.flagsmith.com/api/v1" -} -``` - ### Environment Variables You can configure the Edge Proxy with the following Environment Variables: @@ -288,71 +282,6 @@ You can configure the Edge Proxy with the following Environment Variables: - `WEB_CONCURRENCY` The number of [Uvicorn](https://www.uvicorn.org/) workers. Defaults to `1`. Set to the number of available CPU cores. -## Running the Edge Proxy - -The Edge Proxy runs as a docker container. It is currently available at the -[Docker Hub](https://hub.docker.com/repository/docker/flagsmith/edge-proxy). - -### With docker run - -```bash -# Download the Docker Image -docker pull flagsmith/edge-proxy - -# Run it -docker run \ - -v //config.json:/app/config.json \ - -p 8000:8000 \ - flagsmith/edge-proxy:latest -``` - -### With docker compose - -```yml -services: - edge_proxy: - image: flagsmith/edge-proxy:latest - volumes: - - type: bind - source: ./config.json - target: /app/config.json - ports: - - '8000:8000' -``` - -The Proxy is now running and available on port 8000. - -## Consuming the Edge Proxy - -The Edge Proxy provides an identical set of API methods as our Core API. You need to point your SDK to the Edge Proxy -domain name and you're good to go. For example, lets say you had your proxy running locally as per the instructions -above: - -```bash -curl "http://localhost:8000/api/v1/flags/" -H "x-environment-key: 95DybY5oJoRNhxPZYLrxk4" | jq - -[ - { - "enabled": true, - "feature_state_value": 5454, - "feature": { - "name": "feature_1", - "id": 2, - "type": "MULTIVARIATE" - } - }, - { - "enabled": true, - "feature_state_value": "some_value", - "feature": { - "name": "feature_2", - "id": 9, - "type": "STANDARD" - } - }, -] -``` - ## Monitoring There are 2 health check endpoints for the Edge Proxy. @@ -363,11 +292,6 @@ When making a request to `/proxy/health` the proxy will respond with a HTTP `200 your orchestration health checks to this endpoint. This endpoint checks that the [Environment Document](/clients#the-environment-document) is not stale, and that the proxy is serving SDK requests. -### Realtime Flags/Server Sent Events Health Check - -If you are using the Proxy to power Server Sent Events for realtime flag updates. When making a request to `/sse/health` -the proxy will respond with a HTTP `200` and `{"status": "ok"}`. - ## Architecture The standard Flagsmith architecture: diff --git a/docs/docs/pricing/index.mdx b/docs/docs/pricing/index.mdx index f9aa2e8f8573..e4f2c4cbec90 100644 --- a/docs/docs/pricing/index.mdx +++ b/docs/docs/pricing/index.mdx @@ -35,17 +35,17 @@ API request. The billable API requests your application makes depends mainly on which [flag evaluation mode](/clients) it uses: -- Remote Evaluation is the default for all applications. Applications using Remote Evaluation call the Flagsmith API - when they need to fetch the flags for the current environment or user. -- Local Evaluation is optional, and only for server-side applications. Each individual application or - [Edge Proxy](/advanced-use/edge-proxy) instance polls the Flagsmith API at a configurable interval (60 seconds by - default) to fetch the flags for the current environment and all users in one API request. +- Remote Evaluation is the default for all applications. Applications using Remote Evaluation call the Flagsmith API + when they need to fetch the flags for the current environment or user. +- Local Evaluation is optional, and only for server-side applications. Each individual application or + [Edge Proxy](/advanced-use/edge-proxy) instance polls the Flagsmith API at a configurable interval (60 seconds by + default) to fetch the flags for the current environment and all users in one API request. The following requests are not billable: -- [Admin API](/clients/rest#private-admin-api-endpoints) requests. -- Requests made by Flagsmith SDKs to track [Flag Analytics](/advanced-use/flag-analytics). -- Connecting to a [real-time flag updates](/advanced-use/real-time-flags) stream. +- [Admin API](/clients/rest#private-admin-api-endpoints) requests. +- Requests made by Flagsmith SDKs to track [Flag Analytics](/advanced-use/flag-analytics). +- Connecting to a [real-time flag updates](/advanced-use/real-time-flags) stream. ### Example: client-side application @@ -85,9 +85,9 @@ its lifecycle could look like this: "Service" in this context refers to any of the following: -- An instance of your application, running in a container or server, using the Flagsmith SDK with Local Evaluation - enabled. -- An [Edge Proxy](/advanced-use/edge-proxy) container instance. +- An instance of your application, running in a container or server, using the Flagsmith SDK with Local Evaluation + enabled. +- An [Edge Proxy](/advanced-use/edge-proxy) container instance. Following the example above, if you used the default polling frequency of 60 seconds, you could estimate your monthly API usage as: diff --git a/docs/package-lock.json b/docs/package-lock.json index ab3658e63ce4..d0359ca11466 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8196,9 +8196,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -22553,9 +22553,9 @@ } }, "http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "requires": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", diff --git a/docs/plugins/flagsmith-versions/index.js b/docs/plugins/flagsmith-versions/index.js index b68835d55499..2c43dcb16d07 100644 --- a/docs/plugins/flagsmith-versions/index.js +++ b/docs/plugins/flagsmith-versions/index.js @@ -69,9 +69,10 @@ export default async function fetchFlagsmithVersions(context, options) { return { name: 'flagsmith-versions', async loadContent() { - const [js, java, android, swiftpm, cocoapods, dotnet, rust, elixir] = await Promise.all( + const [js, nodejs, java, android, swiftpm, cocoapods, dotnet, rust, elixir] = await Promise.all( [ fetchNpmVersions('flagsmith'), + fetchNpmVersions('flagsmith-nodejs'), fetchJavaVersions(), fetchAndroidVersions(), fetchSwiftPMVersions(), @@ -83,6 +84,7 @@ export default async function fetchFlagsmithVersions(context, options) { ); return { js, + nodejs, java, android, swiftpm, diff --git a/docs/src/components/SdkVersions.js b/docs/src/components/SdkVersions.js index 47c96bf871d7..e0426a1d0695 100644 --- a/docs/src/components/SdkVersions.js +++ b/docs/src/components/SdkVersions.js @@ -27,3 +27,4 @@ export const DotnetVersion = ({ spec = '~5' }) => Version({ sdk: 'dotnet', spec export const ElixirVersion = ({ spec = '~2' }) => Version({ sdk: 'elixir', spec }); export const RustVersion = ({ spec = '~2' }) => Version({ sdk: 'rust', spec }); export const JsVersion = ({ spec = '~7' }) => Version({ sdk: 'js', spec }); +export const NodejsVersion = ({ spec } = { spec: '~5' }) => Version({ sdk: 'nodejs', spec }); diff --git a/frontend/common/dispatcher/app-actions.js b/frontend/common/dispatcher/app-actions.js index f284e64ec028..0eab0e1d1245 100644 --- a/frontend/common/dispatcher/app-actions.js +++ b/frontend/common/dispatcher/app-actions.js @@ -143,7 +143,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { segmentOverrides, changeRequest, commit, - mode, ) { Dispatcher.handleViewAction({ actionType: Actions.EDIT_ENVIRONMENT_FLAG_CHANGE_REQUEST, @@ -152,7 +151,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { environmentFlag, environmentId, flag, - mode, projectFlag, projectId, segmentOverrides, diff --git a/frontend/common/providers/FeatureListProvider.js b/frontend/common/providers/FeatureListProvider.js index 9d0edcce9782..3b8738bcf141 100644 --- a/frontend/common/providers/FeatureListProvider.js +++ b/frontend/common/providers/FeatureListProvider.js @@ -192,7 +192,6 @@ const FeatureListProvider = class extends React.Component { segmentOverrides, changeRequest, commit, - mode, ) => { AppActions.editFeatureMv( projectId, @@ -224,7 +223,6 @@ const FeatureListProvider = class extends React.Component { segmentOverrides, changeRequest, commit, - mode, ) }, ) diff --git a/frontend/common/services/useFeatureVersion.ts b/frontend/common/services/useFeatureVersion.ts index 9e81a84bf60b..8321873bf90c 100644 --- a/frontend/common/services/useFeatureVersion.ts +++ b/frontend/common/services/useFeatureVersion.ts @@ -43,35 +43,43 @@ export const getFeatureStateCrud = ( if (!oldFeatureStates) { return featureStates } - if (segments?.length) { - // filter out feature states that have no changes - const segmentDiffs = getSegmentDiff( - featureStates, - oldFeatureStates, - segments, - ) - return featureStates.filter((v) => { - const diff = segmentDiffs?.diffs?.find( - (diff) => v.feature_segment?.segment === diff.segment.id, + const segmentDiffs = segments?.length + ? getSegmentDiff( + featureStates.filter((v) => !!v.feature_segment), + oldFeatureStates.filter((v) => !!v.feature_segment), + segments, ) - return !!diff?.totalChanges - }) - } else { - // return nothing if feature state isn't different - const valueDiff = getFeatureStateDiff( - featureStates[0], - oldFeatureStates[0], + : null + const featureStateDiffs = featureStates.filter((v) => { + if (!v.feature_segment) return + const diff = segmentDiffs?.diffs?.find( + (diff) => v.feature_segment?.segment === diff.segment.id, ) - if (!valueDiff.totalChanges) { - const variationDiff = getVariationDiff( - featureStates[0], - oldFeatureStates[0], - ) - if (!variationDiff.totalChanges) return [] + return !!diff?.totalChanges + }) + const newValueFeatureState = featureStates.find((v) => !v.feature_segment)! + const oldValueFeatureState = oldFeatureStates.find( + (v) => !v.feature_segment, + )! + // return nothing if feature state isn't different + const valueDiff = getFeatureStateDiff( + oldValueFeatureState, + newValueFeatureState, + ) + if (!valueDiff.totalChanges) { + const variationDiff = getVariationDiff( + oldValueFeatureState, + newValueFeatureState, + ) + if (variationDiff.totalChanges) { + featureStateDiffs.push(newValueFeatureState) } - return featureStates + } else { + featureStateDiffs.push(newValueFeatureState) } + return featureStateDiffs } + const featureStatesToCreate = featureStates.filter( (v) => !v.id && !v.toRemove, ) diff --git a/frontend/common/stores/feature-list-store.ts b/frontend/common/stores/feature-list-store.ts index 2843884ef7e8..ebc4fa9ccc92 100644 --- a/frontend/common/stores/feature-list-store.ts +++ b/frontend/common/stores/feature-list-store.ts @@ -12,12 +12,12 @@ import { } from 'common/services/useProjectFlag' import OrganisationStore from './organisation-store' import { - ChangeRequest, - Environment, - FeatureState, - PagedResponse, - ProjectFlag, -} from 'common/types/responses' + ChangeRequest, + Environment, + FeatureState, + PagedResponse, + ProjectFlag, TypedFeatureState, +} from 'common/types/responses'; import Utils from 'common/utils/utils' import Actions from 'common/dispatcher/action-constants' import Project from 'common/project' @@ -466,14 +466,13 @@ const controller = { segmentOverrides: any, changeRequest: Req['createChangeRequest'], commit: boolean, - mode: 'VALUE' | 'SEGMENT', ) => { store.saving() try { API.trackEvent(Constants.events.EDIT_FEATURE) const env: Environment = ProjectStore.getEnvironment(environmentId) as any // Detect differences between change request and existing feature states - const res: { data: PagedResponse } = await getFeatureStates( + const res: { data: PagedResponse } = await getFeatureStates( getStore(), { environment: environmentFlag.environment, @@ -483,19 +482,15 @@ const controller = { forceRefetch: true, }, ) - let segments = null - if (mode === 'SEGMENT') { - const res = await getSegments(getStore(), { - include_feature_specific: true, - page_size: 1000, - projectId, - }) - segments = res.data.results - } - const oldFeatureStates = res.data.results.filter((v) => { - return mode === 'VALUE' ? !v.feature_segment : !!v.feature_segment + const segmentResult = await getSegments(getStore(), { + include_feature_specific: true, + page_size: 1000, + projectId, }) + const segments = segmentResult.data.results + const oldFeatureStates = res.data.results + let feature_states_to_create: | Req['createFeatureVersion']['feature_states_to_create'] | undefined = undefined, @@ -506,23 +501,20 @@ const controller = { | Req['createFeatureVersion']['segment_ids_to_delete_overrides'] | undefined = undefined if (env.use_v2_feature_versioning) { - let featureStates - if (mode === 'SEGMENT') { - featureStates = segmentOverrides?.map((override: any, i: number) => + const featureStates = (segmentOverrides || []) + .map((override: any, i: number) => convertSegmentOverrideToFeatureState(override, i, changeRequest), ) - } else { - featureStates = [ + .concat([ Object.assign({}, environmentFlag, { enabled: flag.default_enabled, feature_state_value: flag.initial_value, live_from: flag.live_from, }), - ] - } + ]) const version = getFeatureStateCrud( - featureStates.map((v) => ({ + featureStates.map((v: FeatureState) => ({ ...v, // endpoint returns object for feature_state_value rather than the value feature_state_value: Utils.valueToFeatureState( @@ -1082,7 +1074,6 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => { action.segmentOverrides, action.changeRequest, action.commit, - action.mode, ) break case Actions.EDIT_FEATURE: diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml deleted file mode 100644 index 5eee759e9994..000000000000 --- a/frontend/docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3.7' - -services: - bullettrain: - build: - context: . - target: development - dockerfile: Dockerfile - command: npm run dev - environment: - - FLAGSMITH_API_URL=http://localhost:8000/api/v1/ - ports: - - 8080:8080 - volumes: - - .:/srv/bt - - frontend_node_modules:/srv/bt/node_modules - -volumes: - frontend_node_modules: diff --git a/frontend/web/components/EnvironmentReadyChecker.tsx b/frontend/web/components/EnvironmentReadyChecker.tsx index 0967f5366ff5..cb82fa5abf49 100644 --- a/frontend/web/components/EnvironmentReadyChecker.tsx +++ b/frontend/web/components/EnvironmentReadyChecker.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react' +import { FC, useEffect, useState } from 'react' import { useGetEnvironmentQuery } from 'common/services/useEnvironment' type EnvironmentReadyCheckerType = { @@ -13,12 +13,22 @@ const EnvironmentReadyChecker: FC = ({ children, match, }) => { + const [environmentCreated, setEnvironmentCreated] = useState(false) + const { data, isLoading } = useGetEnvironmentQuery( { id: match.params.environmentId, }, - { pollingInterval: 1000, skip: !match.params.environmentId }, + { + pollingInterval: 1000, + skip: !match.params.environmentId || environmentCreated, + }, ) + useEffect(() => { + if (!!data && !data?.is_creating) { + setEnvironmentCreated(true) + } + }, [data]) if (!match?.params?.environmentId) { return children } diff --git a/frontend/web/components/base/forms/Button.tsx b/frontend/web/components/base/forms/Button.tsx index 306070be37be..8ca31c338d5a 100644 --- a/frontend/web/components/base/forms/Button.tsx +++ b/frontend/web/components/base/forms/Button.tsx @@ -56,21 +56,25 @@ export const Button: FC = ({ const hasPlan = feature ? Utils.getPlansPermission(feature) : true return href || !hasPlan ? ( - {!!iconLeft && ( - - )} - {children} +
+ {!!iconLeft && !!hasPlan && ( + + )} + {children} + {!hasPlan && } +
{!!iconRight && ( { const saveFeatureValue = saveFeatureWithValidation((schedule) => { - this.setState({ valueChanged: false }) if ((is4Eyes || schedule) && !identity) { + this.setState({ segmentsChanged: false, valueChanged: false }) + openModal2( schedule ? 'New Scheduled Flag Update' @@ -940,7 +941,6 @@ const CreateFlag = class extends Component { title, }, !is4Eyes, - 'VALUE', ) }, ) @@ -948,6 +948,7 @@ const CreateFlag = class extends Component { />, ) } else { + this.setState({ valueChanged: false }) this.save(editFeatureValue, isSaving) } }) @@ -957,80 +958,17 @@ const CreateFlag = class extends Component { this.save(editFeatureSettings, isSaving) } - const saveFeatureSegments = saveFeatureWithValidation(() => { - this.setState({ segmentsChanged: false }) + const saveFeatureSegments = saveFeatureWithValidation( + (schedule) => { + this.setState({ segmentsChanged: false }) - if (is4Eyes && isVersioned && !identity) { - openModal2( - this.props.changeRequest - ? 'Update Change Request' - : 'New Change Request', - { - closeModal2() - this.save( - ( - projectId, - environmentId, - flag, - projectFlag, - environmentFlag, - segmentOverrides, - ) => { - createChangeRequest( - projectId, - environmentId, - flag, - projectFlag, - environmentFlag, - segmentOverrides, - { - approvals, - description, - featureStateId: - this.props.changeRequest && - this.props.changeRequest.feature_states[0].id, - id: - this.props.changeRequest && - this.props.changeRequest.id, - live_from, - multivariate_options: this.props - .multivariate_options - ? this.props.multivariate_options.map((v) => { - const matching = - this.state.multivariate_options.find( - (m) => - m.id === - v.multivariate_feature_option, - ) - return { - ...v, - percentage_allocation: - matching.default_percentage_allocation, - } - }) - : this.state.multivariate_options, - title, - }, - !is4Eyes, - 'SEGMENT', - ) - }, - ) - }} - />, - ) - } else { - this.save(editFeatureSegments, isSaving) - } - }) + if ((is4Eyes || schedule) && isVersioned && !identity) { + return saveFeatureValue() + } else { + this.save(editFeatureSegments, isSaving) + } + }, + ) const onCreateFeature = saveFeatureWithValidation(() => { this.save(createFlag, isSaving) @@ -1122,7 +1060,11 @@ const CreateFlag = class extends Component {
{is4Eyes - ? 'This will create a change request for the environment' + ? `This will create a change request ${ + isVersioned + ? 'with any value and segment override changes ' + : '' + }for the environment` : 'This will update the feature value for the environment'}{' '} { @@ -1405,7 +1347,11 @@ const CreateFlag = class extends Component {

{is4Eyes && isVersioned - ? 'This will create a change request for the environment' + ? `This will create a change request ${ + isVersioned + ? 'with any value and segment override changes ' + : '' + }for the environment` : 'This will update the segment overrides for the environment'}{' '} { @@ -1485,24 +1431,58 @@ const CreateFlag = class extends Component { Constants.environmentPermissions( 'Manage segment overrides', ), - , + <> + {!is4Eyes && + isVersioned && ( + <> + + + )} + + , ) }} diff --git a/version.txt b/version.txt index ebe9185df092..9170c650ade7 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.154.0 +2.155.0