From 8b18b1afc69d62447161b9b49618fda1ebed314a Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 11 Dec 2025 11:43:28 +0000 Subject: [PATCH 01/13] feat: Add DynamoDB document schemas --- .gitignore | 3 +- pyproject.toml | 4 +- src/flagsmith_models/__init__.py | 310 ++++++++++++++++++ src/flagsmith_models/py.typed | 0 .../integration/flagsmith_models/__init__.py | 0 .../flagsmith_models/data/environment.json | 294 +++++++++++++++++ .../flagsmith_models/test_flagsmith_models.py | 287 ++++++++++++++++ uv.lock | 135 ++++++++ 8 files changed, 1030 insertions(+), 3 deletions(-) create mode 100644 src/flagsmith_models/__init__.py create mode 100644 src/flagsmith_models/py.typed create mode 100644 tests/integration/flagsmith_models/__init__.py create mode 100644 tests/integration/flagsmith_models/data/environment.json create mode 100644 tests/integration/flagsmith_models/test_flagsmith_models.py diff --git a/.gitignore b/.gitignore index 0d6c567..4366751 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ *.pyc .coverage common.sqlite3 -dist/ \ No newline at end of file +dist/ +coverage.xml \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 203ba84..f789959 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ optional-dependencies = { test-tools = [ "psycopg2-binary (>=2.9,<3)", "requests", "simplejson (>=3,<4)", - ], task-processor = [ "backoff (>=2.2.1,<3.0.0)", "django (>4,<5)", @@ -69,6 +68,7 @@ dev = [ "djangorestframework-stubs (>=3.15.3, <4.0.0)", "mypy (>=1.15.0, <2.0.0)", "pre-commit", + "pydantic>=2.12.5", "pyfakefs (>=5.7.4, <6.0.0)", "pytest (>=8.3.4, <9.0.0)", "pytest-asyncio (>=0.25.3, <1.0.0)", @@ -78,8 +78,8 @@ dev = [ "pytest-httpserver (>=1.1.3, <2.0.0)", "pytest-mock (>=3.14.0, <4.0.0)", "setuptools (>=78.1.1, <79.0.0)", - "types-simplejson (>=3.20.0.20250326, <4.0.0)", "types-python-dateutil (>=2.9.0.20250516, <3.0.0)", + "types-simplejson (>=3.20.0.20250326, <4.0.0)", ] [build-system] diff --git a/src/flagsmith_models/__init__.py b/src/flagsmith_models/__init__.py new file mode 100644 index 0000000..71ce015 --- /dev/null +++ b/src/flagsmith_models/__init__.py @@ -0,0 +1,310 @@ +""" +Types describing Flagsmith Edge API's data model. +The schemas are written to DynamoDB documents by Core, and read by Edge. +""" + +from datetime import datetime +from typing import Literal, NotRequired, TypeAlias, TypedDict +from uuid import UUID + +FeatureType = Literal["STANDARD", "MULTIVARIATE"] +"""Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values.""" + +FeatureValue: TypeAlias = object +"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string. + +The default (SaaS) maximum length for strings is 20000 characters. +""" + +ContextValue: TypeAlias = int | float | bool | str +"""Represents a scalar value in the Flagsmith context, e.g., of an identity trait. +Here's how we store different types: +- Numeric string values (int, float) are stored as numbers. +- Boolean values are stored as booleans. +- All other values are stored as strings. +- Maximum length for strings is 2000 characters. + +This type does not include complex structures like lists or dictionaries. +""" + +ConditionOperator = Literal[ + "EQUAL", + "GREATER_THAN", + "LESS_THAN", + "LESS_THAN_INCLUSIVE", + "CONTAINS", + "GREATER_THAN_INCLUSIVE", + "NOT_CONTAINS", + "NOT_EQUAL", + "REGEX", + "PERCENTAGE_SPLIT", + "MODULO", + "IS_SET", + "IS_NOT_SET", + "IN", +] +"""Represents segment condition operators used by Flagsmith engine.""" + +RuleType = Literal[ + "ALL", + "ANY", + "NONE", +] +"""Represents segment rule types used by Flagsmith engine.""" + + +class Feature(TypedDict): + """Represents a Flagsmith feature defined at project level.""" + + id: int + """Unique identifier for the feature in Core.""" + name: str + """Name of the feature. Must be unique within a project.""" + type: FeatureType + + +class MultivariateFeatureOption(TypedDict): + """A container for a feature state value of a multivariate feature state.""" + + id: NotRequired[int | None] + """Unique identifier for the multivariate feature option in Core. **DEPRECATED**: MultivariateFeatureValue.id should be used instead.""" + value: FeatureValue + """The feature state value that should be served when this option's parent multivariate feature state is selected by the engine.""" + + +class MultivariateFeatureStateValue(TypedDict): + """Represents a multivariate feature state value assigned to an identity or environment.""" + + id: NotRequired[int | None] + """Unique identifier for the multivariate feature state value in Core. TODO: document why and when this can be `None`.""" + mv_fs_value_uuid: NotRequired[UUID] + """The UUID for this multivariate feature state value. Should be used if `id` is `None`.""" + percentage_allocation: float + """The percentage allocation for this multivariate feature state value. Should be between or equal to 0 and 100.""" + multivariate_feature_option: MultivariateFeatureOption + """The multivariate feature option that this value corresponds to.""" + + +class FeatureSegment(TypedDict): + """Represents data specific to a segment feature override.""" + + priority: NotRequired[int | None] + """The priority of this segment feature override. Lower numbers indicate stronger priority. If `None` or not set, the weakest priority is assumed.""" + + +class FeatureState(TypedDict): + """Represents a Flagsmith feature state. Used to define the state of a feature for an environment, segment overrides, and identity overrides.""" + + feature: Feature + """The feature that this feature state is for.""" + enabled: bool + """Whether the feature is enabled or disabled.""" + feature_state_value: FeatureValue + """The value for this feature state.""" + django_id: NotRequired[int | None] + """Unique identifier for the feature state in Core. TODO: document why and when this can be `None`.""" + featurestate_uuid: NotRequired[UUID] + """The UUID for this feature state. Should be used if `django_id` is `None`. If not set, should be generated.""" + feature_segment: NotRequired[FeatureSegment | None] + """Segment override data, if this feature state is for a segment override.""" + multivariate_feature_state_values: NotRequired[list[MultivariateFeatureStateValue]] + """List of multivariate feature state values, if this feature state is for a multivariate feature. + + Total `percentage_allocation` sum must be less or equal to 100. + """ + + +class Trait(TypedDict): + """Represents a key-value pair associated with an identity.""" + + trait_key: str + """Key of the trait.""" + trait_value: ContextValue + """Value of the trait.""" + + +class SegmentCondition(TypedDict): + """Represents a condition within a segment rule used by Flagsmith engine.""" + + operator: ConditionOperator + """Operator to be applied for this condition.""" + value: NotRequired[str | None] + """Value to be compared against in this condition. May be `None` for `IS_SET` and `IS_NOT_SET` operators.""" + property_: NotRequired[str | None] + """The property (context key) this condition applies to. May be `None` for the `PERCENTAGE_SPLIT` operator. + + Named `property_` to avoid conflict with Python's `property` built-in. + """ + + +class SegmentRule(TypedDict): + """Represents a rule within a segment used by Flagsmith engine.""" + + type: RuleType + """Type of the rule, defining how conditions are evaluated.""" + rules: "list[SegmentRule]" + """Nested rules within this rule.""" + conditions: list[SegmentCondition] + """Conditions that must be met for this rule, evaluated based on the rule type.""" + + +class Segment(TypedDict): + """Represents a Flagsmith segment. Carries rules, feature overrides, and segment rules.""" + + id: int + """Unique identifier for the segment in Core.""" + name: str + """Name of the segment.""" + rules: list[SegmentRule] + """List of rules within the segment.""" + feature_states: list[FeatureState] + """List of segment overrides.""" + + +class Organisation(TypedDict): + """Represents data about a Flagsmith organisation. Carries settings necessary for an SDK API operation.""" + + id: int + """Unique identifier for the organisation in Core.""" + name: str + """Organisation name as set via Core.""" + feature_analytics: NotRequired[bool] + """Whether the SDK API should log feature analytics events for this organisation. Defaults to `False`.""" + stop_serving_flags: NotRequired[bool] + """Whether flag serving is disabled for this organisation. Defaults to `False`.""" + persist_trait_data: NotRequired[bool] + """If set to `False`, trait data will never be persisted for this organisation. Defaults to `True`.""" + + +class Project(TypedDict): + """Represents data about a Flagsmith project. Carries settings necessary for an SDK API operation.""" + + id: int + """Unique identifier for the project in Core.""" + name: str + """Project name as set via Core.""" + organisation: Organisation + """The organisation that this project belongs to.""" + segments: list[Segment] + """List of segments.""" + server_key_only_feature_ids: NotRequired[list[int]] + """List of feature IDs that are skipped when the SDK API serves flags for a public client-side key.""" + enable_realtime_updates: NotRequired[bool] + """Whether the SDK API should use real-time updates. Defaults to `False`. Not currently used neither by SDK APIs nor by SDKs themselves.""" + hide_disabled_flags: NotRequired[bool] + """Whether the SDK API should hide disabled flags for this project. Defaults to `False`.""" + + +class Integration(TypedDict): + """Represents evaluation integration data.""" + + api_key: NotRequired[str | None] + """API key for the integration.""" + base_url: NotRequired[str | None] + """Base URL for the integration.""" + + +class DynatraceIntegration(Integration): + """Represents Dynatrace evaluation integration data.""" + + entity_selector: str + """A Dynatrace entity selector string.""" + + +class Webhook(TypedDict): + """Represents a webhook configuration.""" + + url: str + """Webhook target URL.""" + secret: str + """Secret used to sign webhook payloads.""" + + +### Root document schemas below. Indexed fields are marked as **INDEXED** in the docstrings. ### + + +class EnvironmentAPIKey(TypedDict): + """Represents a server-side API key for a Flagsmith environment.""" + + id: int + """Unique identifier for the environment API key in Core. **INDEXED**.""" + key: str + """The server-side API key string, e.g. `"ser.xxxxxxxxxxxxx"`. **INDEXED**.""" + created_at: datetime + """Creation timestamp.""" + name: str + """Name of the API key.""" + client_api_key: str + """The corresponding public client-side API key.""" + expires_at: NotRequired[datetime | None] + """Expiration timestamp. If `None`, the key does not expire.""" + active: bool + """Whether the key is active. Defaults to `True`.""" + + +class Identity(TypedDict): + """Represents a Flagsmith identity within an environment. Carries traits and feature overrides.""" + + identifier: str + """Unique identifier for the identity. **INDEXED**.""" + environment_api_key: str + """API key of the environment this identity belongs to. Used to scope the identity within a specific environment. **INDEXED**.""" + identity_uuid: UUID + """The UUID for this identity. **INDEXED**.""" + composite_key: str + """A composite key combining the environment and identifier. **INDEXED**. + + Generated as: `{environment_api_key}_{identifier}`. + """ + created_date: datetime + """Creation timestamp.""" + identity_features: NotRequired[list[FeatureState]] + """List of identity overrides for this identity.""" + identity_traits: list[Trait] + """List of traits associated with this identity.""" + django_id: NotRequired[int | None] + """Unique identifier for the identity in Core. TODO: document why and when this can be `None`.""" + + +class Environment(TypedDict): + """Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.""" + + id: int + """Unique identifier for the environment in Core. **INDEXED**.""" + api_key: str + """Public client-side API key for the environment. **INDEXED**.""" + name: NotRequired[str] + """Environment name.""" + updated_at: NotRequired[datetime | None] + """Last updated timestamp. If not set, current timestamp should be assumed.""" + + project: Project + """Project-specific data for this environment.""" + feature_states: list[FeatureState] + """List of feature states representing the environment defaults.""" + + allow_client_traits: NotRequired[bool] + """Whether the SDK API should allow clients to set traits for this environment. Identical to project-level's `persist_trait_data` setting. Defaults to `True`.""" + hide_sensitive_data: NotRequired[bool] + """Whether the SDK API should hide sensitive data for this environment. Defaults to `False`.""" + hide_disabled_flags: NotRequired[bool] + """Whether the SDK API should hide disabled flags for this environment. If `None`, the SDK API should fall back to project-level setting.""" + use_identity_composite_key_for_hashing: NotRequired[bool] + """Whether the SDK API should set `$.identity.key` in engine evaluation context to identity's composite key. Defaults to `False`.""" + use_identity_overrides_in_local_eval: NotRequired[bool] + """Whether the SDK API should return identity overrides as part of the environment document. Defaults to `False`.""" + + amplitude_config: NotRequired[Integration] + """Amplitude integration configuration.""" + dynatrace_config: NotRequired[DynatraceIntegration] + """Dynatrace integration configuration.""" + heap_config: NotRequired[Integration] + """Heap integration configuration.""" + mixpanel_config: NotRequired[Integration] + """Mixpanel integration configuration.""" + rudderstack_config: NotRequired[Integration] + """RudderStack integration configuration.""" + segment_config: NotRequired[Integration] + """Segment integration configuration.""" + webhook_config: NotRequired[Webhook] + """Webhook configuration.""" diff --git a/src/flagsmith_models/py.typed b/src/flagsmith_models/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/flagsmith_models/__init__.py b/tests/integration/flagsmith_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/flagsmith_models/data/environment.json b/tests/integration/flagsmith_models/data/environment.json new file mode 100644 index 0000000..999fb06 --- /dev/null +++ b/tests/integration/flagsmith_models/data/environment.json @@ -0,0 +1,294 @@ +{ + "project": { + "hide_disabled_flags": false, + "segments": [ + { + "name": "regular_segment", + "feature_states": [ + { + "feature_state_value": "segment_override", + "multivariate_feature_state_values": [], + "django_id": 81027, + "feature": { + "id": 15058, + "type": "STANDARD", + "name": "string_feature" + }, + "enabled": false + } + ], + "id": 4267, + "rules": [ + { + "type": "ALL", + "conditions": [], + "rules": [ + { + "type": "ANY", + "conditions": [ + { + "value": "40", + "property_": "age", + "operator": "LESS_THAN" + } + ], + "rules": [] + }, + { + "type": "ANY", + "conditions": [ + { + "value": "21", + "property_": "age", + "operator": "GREATER_THAN_INCLUSIVE" + } + ], + "rules": [] + }, + { + "type": "ANY", + "conditions": [ + { + "value": "green", + "property_": "favourite_colour", + "operator": "EQUAL" + }, + { + "value": "blue", + "property_": "favourite_colour", + "operator": "EQUAL" + } + ], + "rules": [] + } + ] + } + ] + }, + { + "name": "10_percent", + "feature_states": [ + { + "feature_state_value": "", + "multivariate_feature_state_values": [], + "django_id": 81026, + "feature": { + "id": 15060, + "type": "STANDARD", + "name": "basic_flag" + }, + "enabled": true + } + ], + "id": 4268, + "rules": [ + { + "type": "ALL", + "conditions": [], + "rules": [ + { + "type": "ANY", + "conditions": [ + { + "value": "0.1", + "property_": "", + "operator": "PERCENTAGE_SPLIT" + } + ], + "rules": [] + } + ] + } + ] + }, + { + "feature_states": [ + { + "feature_state_value": "segment_two_override_priority_0", + "multivariate_feature_state_values": [], + "django_id": 78978, + "feature": { + "id": 15058, + "type": "STANDARD", + "name": "string_feature" + }, + "enabled": true, + "feature_segment": { + "priority": 0 + }, + "featurestate_uuid": "1545809c-e97f-4a1f-9e67-8b4f2b396aa6" + } + ], + "id": 16, + "name": "segment_two", + "rules": [ + { + "conditions": [], + "rules": [ + { + "conditions": [ + { + "operator": "EQUAL", + "property_": "two", + "value": "2" + }, + { + "operator": "IS_SET", + "property_": "two", + "value": null + } + ], + "rules": [], + "type": "ANY" + } + ], + "type": "ALL" + } + ] + }, + { + "feature_states": [ + { + "feature_state_value": "segment_three_override_priority_1", + "multivariate_feature_state_values": [], + "django_id": 78977, + "feature": { + "id": 15058, + "type": "STANDARD", + "name": "string_feature" + }, + "enabled": true, + "feature_segment": { + "priority": 1 + }, + "featurestate_uuid": "1545809c-e97f-4a1f-9e67-8b4f2b396aa7" + } + ], + "id": 17, + "name": "segment_three", + "rules": [ + { + "conditions": [], + "rules": [ + { + "conditions": [ + { + "operator": "EQUAL", + "property_": "three", + "value": "3" + }, + { + "operator": "IS_NOT_SET", + "property_": "something_that_is_not_set", + "value": null + } + ], + "rules": [], + "type": "ALL" + } + ], + "type": "ALL" + } + ] + } + ], + "name": "Edge API Test Project", + "id": 5359, + "organisation": { + "persist_trait_data": true, + "name": "Flagsmith", + "feature_analytics": false, + "stop_serving_flags": false, + "id": 13 + } + }, + "api_key": "n9fbf9h3v4fFgH3U3ngWhb", + "feature_states": [ + { + "feature_state_value": "foo", + "multivariate_feature_state_values": [], + "feature_segment": null, + "django_id": 78978, + "feature": { + "id": 15058, + "type": "STANDARD", + "name": "string_feature" + }, + "enabled": true + }, + { + "feature_state_value": 1234, + "multivariate_feature_state_values": [], + "django_id": 78980, + "feature": { + "id": 15059, + "type": "STANDARD", + "name": "integer_feature" + }, + "enabled": true + }, + { + "feature_state_value": null, + "multivariate_feature_state_values": [], + "django_id": 78982, + "feature": { + "id": 15060, + "type": "STANDARD", + "name": "basic_flag" + }, + "enabled": false + }, + { + "feature_state_value": "12.34", + "multivariate_feature_state_values": [], + "django_id": 78984, + "feature": { + "id": 15061, + "type": "STANDARD", + "name": "float_feature" + }, + "enabled": true + }, + { + "feature_state_value": "foo", + "multivariate_feature_state_values": [ + { + "id": 3404, + "multivariate_feature_option": { + "value": "baz" + }, + "percentage_allocation": 30 + }, + { + "id": 3402, + "multivariate_feature_option": { + "value": "bar" + }, + "percentage_allocation": 30 + }, + { + "id": 3405, + "multivariate_feature_option": { + "value": 1 + }, + "percentage_allocation": 0 + }, + { + "id": 3406, + "multivariate_feature_option": { + "value": true + }, + "percentage_allocation": 0 + } + ], + "django_id": 78986, + "feature": { + "id": 15062, + "type": "MULTIVARIATE", + "name": "mv_feature" + }, + "enabled": true + } + ], + "id": 12561 +} \ No newline at end of file diff --git a/tests/integration/flagsmith_models/test_flagsmith_models.py b/tests/integration/flagsmith_models/test_flagsmith_models.py new file mode 100644 index 0000000..3638894 --- /dev/null +++ b/tests/integration/flagsmith_models/test_flagsmith_models.py @@ -0,0 +1,287 @@ +from uuid import UUID + +from pydantic.type_adapter import TypeAdapter +from pytest import FixtureRequest + +from common.test_tools import SnapshotFixture +from flagsmith_models import Environment + + +def test_environment__validate_json__expected_result( + request: FixtureRequest, + snapshot: SnapshotFixture, +) -> None: + # Given + type_adapter = TypeAdapter(Environment) + json_data = request.path.parent.joinpath("data/environment.json").read_text() + + # When + environment = type_adapter.validate_json(json_data) + + # Then + assert environment == { + "id": 12561, + "api_key": "n9fbf9h3v4fFgH3U3ngWhb", + "project": { + "id": 5359, + "name": "Edge API Test Project", + "organisation": { + "id": 13, + "name": "Flagsmith", + "feature_analytics": False, + "stop_serving_flags": False, + "persist_trait_data": True, + }, + "segments": [ + { + "id": 4267, + "name": "regular_segment", + "rules": [ + { + "type": "ALL", + "rules": [ + { + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "LESS_THAN", + "value": "40", + "property_": "age", + } + ], + }, + { + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "GREATER_THAN_INCLUSIVE", + "value": "21", + "property_": "age", + } + ], + }, + { + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "EQUAL", + "value": "green", + "property_": "favourite_colour", + }, + { + "operator": "EQUAL", + "value": "blue", + "property_": "favourite_colour", + }, + ], + }, + ], + "conditions": [], + } + ], + "feature_states": [ + { + "feature": { + "id": 15058, + "name": "string_feature", + "type": "STANDARD", + }, + "enabled": False, + "feature_state_value": "segment_override", + "django_id": 81027, + "multivariate_feature_state_values": [], + } + ], + }, + { + "id": 4268, + "name": "10_percent", + "rules": [ + { + "type": "ALL", + "rules": [ + { + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "PERCENTAGE_SPLIT", + "value": "0.1", + "property_": "", + } + ], + } + ], + "conditions": [], + } + ], + "feature_states": [ + { + "feature": { + "id": 15060, + "name": "basic_flag", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "", + "django_id": 81026, + "multivariate_feature_state_values": [], + } + ], + }, + { + "id": 16, + "name": "segment_two", + "rules": [ + { + "type": "ALL", + "rules": [ + { + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "EQUAL", + "value": "2", + "property_": "two", + }, + { + "operator": "IS_SET", + "value": None, + "property_": "two", + }, + ], + } + ], + "conditions": [], + } + ], + "feature_states": [ + { + "feature": { + "id": 15058, + "name": "string_feature", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "segment_two_override_priority_0", + "django_id": 78978, + "featurestate_uuid": UUID( + "1545809c-e97f-4a1f-9e67-8b4f2b396aa6" + ), + "feature_segment": {"priority": 0}, + "multivariate_feature_state_values": [], + } + ], + }, + { + "id": 17, + "name": "segment_three", + "rules": [ + { + "type": "ALL", + "rules": [ + { + "type": "ALL", + "rules": [], + "conditions": [ + { + "operator": "EQUAL", + "value": "3", + "property_": "three", + }, + { + "operator": "IS_NOT_SET", + "value": None, + "property_": "something_that_is_not_set", + }, + ], + } + ], + "conditions": [], + } + ], + "feature_states": [ + { + "feature": { + "id": 15058, + "name": "string_feature", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "segment_three_override_priority_1", + "django_id": 78977, + "featurestate_uuid": UUID( + "1545809c-e97f-4a1f-9e67-8b4f2b396aa7" + ), + "feature_segment": {"priority": 1}, + "multivariate_feature_state_values": [], + } + ], + }, + ], + "hide_disabled_flags": False, + }, + "feature_states": [ + { + "feature": {"id": 15058, "name": "string_feature", "type": "STANDARD"}, + "enabled": True, + "feature_state_value": "foo", + "django_id": 78978, + "feature_segment": None, + "multivariate_feature_state_values": [], + }, + { + "feature": {"id": 15059, "name": "integer_feature", "type": "STANDARD"}, + "enabled": True, + "feature_state_value": 1234, + "django_id": 78980, + "multivariate_feature_state_values": [], + }, + { + "feature": {"id": 15060, "name": "basic_flag", "type": "STANDARD"}, + "enabled": False, + "feature_state_value": None, + "django_id": 78982, + "multivariate_feature_state_values": [], + }, + { + "feature": {"id": 15061, "name": "float_feature", "type": "STANDARD"}, + "enabled": True, + "feature_state_value": "12.34", + "django_id": 78984, + "multivariate_feature_state_values": [], + }, + { + "feature": {"id": 15062, "name": "mv_feature", "type": "MULTIVARIATE"}, + "enabled": True, + "feature_state_value": "foo", + "django_id": 78986, + "multivariate_feature_state_values": [ + { + "id": 3404, + "percentage_allocation": 30.0, + "multivariate_feature_option": {"value": "baz"}, + }, + { + "id": 3402, + "percentage_allocation": 30.0, + "multivariate_feature_option": {"value": "bar"}, + }, + { + "id": 3405, + "percentage_allocation": 0.0, + "multivariate_feature_option": {"value": 1}, + }, + { + "id": 3406, + "percentage_allocation": 0.0, + "multivariate_feature_option": {"value": True}, + }, + ], + }, + ], + } diff --git a/uv.lock b/uv.lock index b03416e..ce2e73f 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.11, <4.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "asgiref" version = "3.11.0" @@ -415,6 +424,7 @@ dev = [ { name = "djangorestframework-stubs" }, { name = "mypy" }, { name = "pre-commit" }, + { name = "pydantic" }, { name = "pyfakefs" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -458,6 +468,7 @@ dev = [ { name = "djangorestframework-stubs", specifier = ">=3.15.3,<4.0.0" }, { name = "mypy", specifier = ">=1.15.0,<2.0.0" }, { name = "pre-commit" }, + { name = "pydantic", specifier = ">=2.12.5" }, { name = "pyfakefs", specifier = ">=5.7.4,<6.0.0" }, { name = "pytest", specifier = ">=8.3.4,<9.0.0" }, { name = "pytest-asyncio", specifier = ">=0.25.3,<1.0.0" }, @@ -847,6 +858,118 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pyfakefs" version = "5.10.2" @@ -1228,6 +1351,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "tzdata" version = "2025.2" From 815f0d3d6cc43e60444535782bd79943562389f0 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 11 Dec 2025 11:56:37 +0000 Subject: [PATCH 02/13] add a question --- src/flagsmith_models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flagsmith_models/__init__.py b/src/flagsmith_models/__init__.py index 71ce015..ed37fed 100644 --- a/src/flagsmith_models/__init__.py +++ b/src/flagsmith_models/__init__.py @@ -274,7 +274,7 @@ class Environment(TypedDict): api_key: str """Public client-side API key for the environment. **INDEXED**.""" name: NotRequired[str] - """Environment name.""" + """Environment name. TODO: Can we drop NotRequired and adjust test data?""" updated_at: NotRequired[datetime | None] """Last updated timestamp. If not set, current timestamp should be assumed.""" From 7c45ffb73d5f00fdfcb654763e5e59108d067a7e Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 11 Dec 2025 12:10:37 +0000 Subject: [PATCH 03/13] fix 3.11 support --- pyproject.toml | 2 ++ src/flagsmith_models/__init__.py | 4 +++- uv.lock | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f789959..1b8323b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ optional-dependencies = { test-tools = [ "django (>4,<5)", "django-health-check", "prometheus-client (>=0.0.16)", +], flagsmith-models = [ + "typing_extensions", ] } authors = [ { name = "Matthew Elwell" }, diff --git a/src/flagsmith_models/__init__.py b/src/flagsmith_models/__init__.py index ed37fed..2cd8411 100644 --- a/src/flagsmith_models/__init__.py +++ b/src/flagsmith_models/__init__.py @@ -4,9 +4,11 @@ """ from datetime import datetime -from typing import Literal, NotRequired, TypeAlias, TypedDict +from typing import Literal, TypeAlias from uuid import UUID +from typing_extensions import NotRequired, TypedDict + FeatureType = Literal["STANDARD", "MULTIVARIATE"] """Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values.""" diff --git a/uv.lock b/uv.lock index ce2e73f..970b2ef 100644 --- a/uv.lock +++ b/uv.lock @@ -406,6 +406,9 @@ common-core = [ { name = "requests" }, { name = "simplejson" }, ] +flagsmith-models = [ + { name = "typing-extensions" }, +] task-processor = [ { name = "backoff" }, { name = "django" }, @@ -458,8 +461,9 @@ requires-dist = [ { name = "pytest-django", marker = "extra == 'test-tools'", specifier = ">=4,<5" }, { name = "requests", marker = "extra == 'common-core'" }, { name = "simplejson", marker = "extra == 'common-core'", specifier = ">=3,<4" }, + { name = "typing-extensions", marker = "extra == 'flagsmith-models'" }, ] -provides-extras = ["test-tools", "common-core", "task-processor"] +provides-extras = ["test-tools", "common-core", "task-processor", "flagsmith-models"] [package.metadata.requires-dev] dev = [ From 9d21d932af799aaf7d57d2ca56dbc85dcb69796a Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Dec 2025 16:30:22 +0000 Subject: [PATCH 04/13] add all root documents + tests --- src/flagsmith_models/__init__.py | 141 ++-- src/flagsmith_models/types.py | 7 + .../data/flagsmith_environment_api_key.json | 9 + ...nment.json => flagsmith_environments.json} | 0 .../data/flagsmith_environments_v2:_META.json | 108 +++ .../data/flagsmith_identities.json | 29 + .../flagsmith_models/test_flagsmith_models.py | 670 ++++++++++++------ 7 files changed, 684 insertions(+), 280 deletions(-) create mode 100644 src/flagsmith_models/types.py create mode 100644 tests/integration/flagsmith_models/data/flagsmith_environment_api_key.json rename tests/integration/flagsmith_models/data/{environment.json => flagsmith_environments.json} (100%) create mode 100644 tests/integration/flagsmith_models/data/flagsmith_environments_v2:_META.json create mode 100644 tests/integration/flagsmith_models/data/flagsmith_identities.json diff --git a/src/flagsmith_models/__init__.py b/src/flagsmith_models/__init__.py index 2cd8411..54ad178 100644 --- a/src/flagsmith_models/__init__.py +++ b/src/flagsmith_models/__init__.py @@ -3,12 +3,12 @@ The schemas are written to DynamoDB documents by Core, and read by Edge. """ -from datetime import datetime from typing import Literal, TypeAlias -from uuid import UUID from typing_extensions import NotRequired, TypedDict +from flagsmith_models.types import DateTimeStr, UUIDStr + FeatureType = Literal["STANDARD", "MULTIVARIATE"] """Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values.""" @@ -79,7 +79,7 @@ class MultivariateFeatureStateValue(TypedDict): id: NotRequired[int | None] """Unique identifier for the multivariate feature state value in Core. TODO: document why and when this can be `None`.""" - mv_fs_value_uuid: NotRequired[UUID] + mv_fs_value_uuid: NotRequired[UUIDStr] """The UUID for this multivariate feature state value. Should be used if `id` is `None`.""" percentage_allocation: float """The percentage allocation for this multivariate feature state value. Should be between or equal to 0 and 100.""" @@ -105,7 +105,7 @@ class FeatureState(TypedDict): """The value for this feature state.""" django_id: NotRequired[int | None] """Unique identifier for the feature state in Core. TODO: document why and when this can be `None`.""" - featurestate_uuid: NotRequired[UUID] + featurestate_uuid: NotRequired[UUIDStr] """The UUID for this feature state. Should be used if `django_id` is `None`. If not set, should be generated.""" feature_segment: NotRequired[FeatureSegment | None] """Segment override data, if this feature state is for a segment override.""" @@ -193,7 +193,7 @@ class Project(TypedDict): """List of feature IDs that are skipped when the SDK API serves flags for a public client-side key.""" enable_realtime_updates: NotRequired[bool] """Whether the SDK API should use real-time updates. Defaults to `False`. Not currently used neither by SDK APIs nor by SDKs themselves.""" - hide_disabled_flags: NotRequired[bool] + hide_disabled_flags: NotRequired[bool | None] """Whether the SDK API should hide disabled flags for this project. Defaults to `False`.""" @@ -222,43 +222,89 @@ class Webhook(TypedDict): """Secret used to sign webhook payloads.""" +class _EnvironmentFields(TypedDict): + """Common fields for Environment documents.""" + + name: NotRequired[str] + """Environment name. TODO: Can we drop NotRequired and adjust test data?""" + updated_at: NotRequired[DateTimeStr | None] + """Last updated timestamp. If not set, current timestamp should be assumed.""" + + project: Project + """Project-specific data for this environment.""" + feature_states: list[FeatureState] + """List of feature states representing the environment defaults.""" + + allow_client_traits: NotRequired[bool] + """Whether the SDK API should allow clients to set traits for this environment. Identical to project-level's `persist_trait_data` setting. Defaults to `True`.""" + hide_sensitive_data: NotRequired[bool] + """Whether the SDK API should hide sensitive data for this environment. Defaults to `False`.""" + hide_disabled_flags: NotRequired[bool | None] + """Whether the SDK API should hide disabled flags for this environment. If `None`, the SDK API should fall back to project-level setting.""" + use_identity_composite_key_for_hashing: NotRequired[bool] + """Whether the SDK API should set `$.identity.key` in engine evaluation context to identity's composite key. Defaults to `False`.""" + use_identity_overrides_in_local_eval: NotRequired[bool] + """Whether the SDK API should return identity overrides as part of the environment document. Defaults to `False`.""" + + amplitude_config: NotRequired[Integration | None] + """Amplitude integration configuration.""" + dynatrace_config: NotRequired[DynatraceIntegration | None] + """Dynatrace integration configuration.""" + heap_config: NotRequired[Integration | None] + """Heap integration configuration.""" + mixpanel_config: NotRequired[Integration | None] + """Mixpanel integration configuration.""" + rudderstack_config: NotRequired[Integration | None] + """RudderStack integration configuration.""" + segment_config: NotRequired[Integration | None] + """Segment integration configuration.""" + webhook_config: NotRequired[Webhook | None] + """Webhook configuration.""" + + ### Root document schemas below. Indexed fields are marked as **INDEXED** in the docstrings. ### class EnvironmentAPIKey(TypedDict): - """Represents a server-side API key for a Flagsmith environment.""" + """Represents a server-side API key for a Flagsmith environment. + + **DynamoDB table**: `flagsmith_environment_api_key` + """ id: int """Unique identifier for the environment API key in Core. **INDEXED**.""" key: str """The server-side API key string, e.g. `"ser.xxxxxxxxxxxxx"`. **INDEXED**.""" - created_at: datetime + created_at: DateTimeStr """Creation timestamp.""" name: str """Name of the API key.""" client_api_key: str """The corresponding public client-side API key.""" - expires_at: NotRequired[datetime | None] + expires_at: NotRequired[DateTimeStr | None] """Expiration timestamp. If `None`, the key does not expire.""" active: bool """Whether the key is active. Defaults to `True`.""" class Identity(TypedDict): - """Represents a Flagsmith identity within an environment. Carries traits and feature overrides.""" + """Represents a Flagsmith identity within an environment. Carries traits and feature overrides. + + **DynamoDB table**: `flagsmith_identities` + """ identifier: str """Unique identifier for the identity. **INDEXED**.""" environment_api_key: str """API key of the environment this identity belongs to. Used to scope the identity within a specific environment. **INDEXED**.""" - identity_uuid: UUID + identity_uuid: UUIDStr """The UUID for this identity. **INDEXED**.""" composite_key: str """A composite key combining the environment and identifier. **INDEXED**. Generated as: `{environment_api_key}_{identifier}`. """ - created_date: datetime + created_date: DateTimeStr """Creation timestamp.""" identity_features: NotRequired[list[FeatureState]] """List of identity overrides for this identity.""" @@ -268,45 +314,50 @@ class Identity(TypedDict): """Unique identifier for the identity in Core. TODO: document why and when this can be `None`.""" -class Environment(TypedDict): - """Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.""" +class Environment(_EnvironmentFields): + """Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment. + + **DynamoDB table**: `flagsmith_environments` + """ id: int """Unique identifier for the environment in Core. **INDEXED**.""" api_key: str """Public client-side API key for the environment. **INDEXED**.""" - name: NotRequired[str] - """Environment name. TODO: Can we drop NotRequired and adjust test data?""" - updated_at: NotRequired[datetime | None] - """Last updated timestamp. If not set, current timestamp should be assumed.""" - project: Project - """Project-specific data for this environment.""" - feature_states: list[FeatureState] - """List of feature states representing the environment defaults.""" - allow_client_traits: NotRequired[bool] - """Whether the SDK API should allow clients to set traits for this environment. Identical to project-level's `persist_trait_data` setting. Defaults to `True`.""" - hide_sensitive_data: NotRequired[bool] - """Whether the SDK API should hide sensitive data for this environment. Defaults to `False`.""" - hide_disabled_flags: NotRequired[bool] - """Whether the SDK API should hide disabled flags for this environment. If `None`, the SDK API should fall back to project-level setting.""" - use_identity_composite_key_for_hashing: NotRequired[bool] - """Whether the SDK API should set `$.identity.key` in engine evaluation context to identity's composite key. Defaults to `False`.""" - use_identity_overrides_in_local_eval: NotRequired[bool] - """Whether the SDK API should return identity overrides as part of the environment document. Defaults to `False`.""" +class EnvironmentV2Meta(_EnvironmentFields): + """Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment. - amplitude_config: NotRequired[Integration] - """Amplitude integration configuration.""" - dynatrace_config: NotRequired[DynatraceIntegration] - """Dynatrace integration configuration.""" - heap_config: NotRequired[Integration] - """Heap integration configuration.""" - mixpanel_config: NotRequired[Integration] - """Mixpanel integration configuration.""" - rudderstack_config: NotRequired[Integration] - """RudderStack integration configuration.""" - segment_config: NotRequired[Integration] - """Segment integration configuration.""" - webhook_config: NotRequired[Webhook] - """Webhook configuration.""" + **DynamoDB table**: `flagsmith_environments_v2` + """ + + environment_id: str + """Unique identifier for the environment in Core. **INDEXED**.""" + environment_api_key: str + """Public client-side API key for the environment. **INDEXED**.""" + document_key: Literal["_META"] + """The fixed document key for the environment v2 document. Always `"_META"`. **INDEXED**.""" + + id: int + """Unique identifier for the environment in Core. Exists for compatibility with the API environment document schema.""" + + +class EnvironmentV2IdentityOverride(TypedDict): + """Represents an identity override. + + **DynamoDB table**: `flagsmith_environments_v2` + """ + + environment_id: str + """Unique identifier for the environment in Core. **INDEXED**.""" + document_key: str + """The document key for this identity override, formatted as `identity_override:{feature Core ID}:{identity UUID}`. **INDEXED**.""" + environment_api_key: str + """Public client-side API key for the environment. **INDEXED**.""" + identifier: str + """Unique identifier for the identity. **INDEXED**.""" + identity_uuid: str + """The UUID for this identity. **INDEXED**.""" + feature_state: FeatureState + """The feature state override for this identity.""" diff --git a/src/flagsmith_models/types.py b/src/flagsmith_models/types.py new file mode 100644 index 0000000..fce362b --- /dev/null +++ b/src/flagsmith_models/types.py @@ -0,0 +1,7 @@ +from typing import TypeAlias + +UUIDStr: TypeAlias = str +"""A string representing a UUID.""" + +DateTimeStr: TypeAlias = str +"""A string representing a date and time in ISO 8601 format.""" diff --git a/tests/integration/flagsmith_models/data/flagsmith_environment_api_key.json b/tests/integration/flagsmith_models/data/flagsmith_environment_api_key.json new file mode 100644 index 0000000..1206e3d --- /dev/null +++ b/tests/integration/flagsmith_models/data/flagsmith_environment_api_key.json @@ -0,0 +1,9 @@ +{ + "key": "ser.ZSwVCQrCGpXXKdvVsVxoie", + "active": true, + "client_api_key": "pQuzvsMLQoOVAwITrTWDQJ", + "created_at": "2023-04-21T13:11:13.913178+00:00", + "expires_at": null, + "id": 907, + "name": "TestKey" +} \ No newline at end of file diff --git a/tests/integration/flagsmith_models/data/environment.json b/tests/integration/flagsmith_models/data/flagsmith_environments.json similarity index 100% rename from tests/integration/flagsmith_models/data/environment.json rename to tests/integration/flagsmith_models/data/flagsmith_environments.json diff --git a/tests/integration/flagsmith_models/data/flagsmith_environments_v2:_META.json b/tests/integration/flagsmith_models/data/flagsmith_environments_v2:_META.json new file mode 100644 index 0000000..acecd2a --- /dev/null +++ b/tests/integration/flagsmith_models/data/flagsmith_environments_v2:_META.json @@ -0,0 +1,108 @@ +{ + "environment_id": "49268", + "document_key": "_META", + "allow_client_traits": true, + "amplitude_config": null, + "dynatrace_config": null, + "environment_api_key": "AQ9T6LixPqYMJkuqGJy3t2", + "feature_states": [ + { + "django_id": 577621, + "enabled": true, + "feature": { + "id": 100298, + "name": "test_feature", + "type": "MULTIVARIATE" + }, + "featurestate_uuid": "42d7805e-a9ac-401c-a7b7-d6583ac5a365", + "feature_segment": null, + "feature_state_value": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "multivariate_feature_state_values": [ + { + "id": 185130, + "multivariate_feature_option": { + "id": 20919, + "value": "second" + }, + "mv_fs_value_uuid": "0b02ce41-9965-4c61-8b96-c8d76e3d4a27", + "percentage_allocation": 10 + }, + { + "id": 48717, + "multivariate_feature_option": { + "id": 14004, + "value": true + }, + "mv_fs_value_uuid": "cb05f49c-de1f-44f1-87eb-c3b55d473063", + "percentage_allocation": 30 + } + ] + }, + { + "django_id": 1041292, + "enabled": false, + "feature": { + "id": 172422, + "name": "feature", + "type": "STANDARD" + }, + "featurestate_uuid": "58b7b954-1b75-493a-82df-5be0efeedd2a", + "feature_segment": null, + "feature_state_value": 3, + "multivariate_feature_state_values": [] + } + ], + "heap_config": null, + "hide_disabled_flags": null, + "hide_sensitive_data": false, + "id": 49268, + "identity_overrides": [], + "mixpanel_config": null, + "name": "Development", + "project": { + "enable_realtime_updates": false, + "hide_disabled_flags": false, + "id": 19368, + "name": "Example Project", + "organisation": { + "feature_analytics": false, + "id": 13, + "name": "Flagsmith", + "persist_trait_data": true, + "stop_serving_flags": false + }, + "segments": [ + { + "feature_states": [], + "id": 44126, + "name": "test", + "rules": [ + { + "conditions": [], + "rules": [ + { + "conditions": [ + { + "operator": "EQUAL", + "property_": "test", + "value": "test" + } + ], + "rules": [], + "type": "ANY" + } + ], + "type": "ALL" + } + ] + } + ], + "server_key_only_feature_ids": [] + }, + "rudderstack_config": null, + "segment_config": null, + "updated_at": "2025-11-16T13:28:31.244331+00:00", + "use_identity_composite_key_for_hashing": true, + "use_identity_overrides_in_local_eval": false, + "webhook_config": null +} \ No newline at end of file diff --git a/tests/integration/flagsmith_models/data/flagsmith_identities.json b/tests/integration/flagsmith_models/data/flagsmith_identities.json new file mode 100644 index 0000000..68ead05 --- /dev/null +++ b/tests/integration/flagsmith_models/data/flagsmith_identities.json @@ -0,0 +1,29 @@ +{ + "composite_key": "pQuzvsMLQoOVAwITrTWDQJ_57c6edf1bbf145a1a23ea287ce44877f", + "created_date": "2024-03-19T09:41:22.974595+00:00", + "django_id": null, + "environment_api_key": "pQuzvsMLQoOVAwITrTWDQJ", + "identifier": "57c6edf1bbf145a1a23ea287ce44877f", + "identity_features": [ + { + "django_id": null, + "enabled": true, + "feature": { + "id": 67, + "name": "test_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "20988957-d345-424e-9abc-1dc5a814da48", + "feature_segment": null, + "feature_state_value": null, + "multivariate_feature_state_values": [] + } + ], + "identity_traits": [ + { + "trait_key": "test_trait", + "trait_value": 42 + } + ], + "identity_uuid": "118ecfc9-5234-4218-8af8-dd994dbfedc0" +} \ No newline at end of file diff --git a/tests/integration/flagsmith_models/test_flagsmith_models.py b/tests/integration/flagsmith_models/test_flagsmith_models.py index 3638894..02f3e00 100644 --- a/tests/integration/flagsmith_models/test_flagsmith_models.py +++ b/tests/integration/flagsmith_models/test_flagsmith_models.py @@ -1,287 +1,487 @@ -from uuid import UUID +from typing import TypeVar -from pydantic.type_adapter import TypeAdapter -from pytest import FixtureRequest +import pytest +from pydantic import TypeAdapter -from common.test_tools import SnapshotFixture -from flagsmith_models import Environment +from flagsmith_models import Environment, EnvironmentAPIKey, EnvironmentV2Meta, Identity +from flagsmith_models.types import DateTimeStr, UUIDStr +T = TypeVar("T") -def test_environment__validate_json__expected_result( - request: FixtureRequest, - snapshot: SnapshotFixture, -) -> None: - # Given - type_adapter = TypeAdapter(Environment) - json_data = request.path.parent.joinpath("data/environment.json").read_text() - - # When - environment = type_adapter.validate_json(json_data) - # Then - assert environment == { - "id": 12561, - "api_key": "n9fbf9h3v4fFgH3U3ngWhb", - "project": { - "id": 5359, - "name": "Edge API Test Project", - "organisation": { - "id": 13, - "name": "Flagsmith", - "feature_analytics": False, - "stop_serving_flags": False, - "persist_trait_data": True, - }, - "segments": [ - { - "id": 4267, - "name": "regular_segment", - "rules": [ +@pytest.mark.parametrize( + ("document_type", "json_data_filename", "expected_result"), + [ + pytest.param( + Environment, + "flagsmith_environments.json", + { + "id": 12561, + "api_key": "n9fbf9h3v4fFgH3U3ngWhb", + "project": { + "id": 5359, + "name": "Edge API Test Project", + "organisation": { + "id": 13, + "name": "Flagsmith", + "feature_analytics": False, + "stop_serving_flags": False, + "persist_trait_data": True, + }, + "segments": [ { - "type": "ALL", + "id": 4267, + "name": "regular_segment", "rules": [ { - "type": "ANY", - "rules": [], - "conditions": [ - { - "operator": "LESS_THAN", - "value": "40", - "property_": "age", - } - ], - }, - { - "type": "ANY", - "rules": [], - "conditions": [ + "type": "ALL", + "rules": [ { - "operator": "GREATER_THAN_INCLUSIVE", - "value": "21", - "property_": "age", - } - ], - }, - { - "type": "ANY", - "rules": [], - "conditions": [ + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "LESS_THAN", + "value": "40", + "property_": "age", + } + ], + }, { - "operator": "EQUAL", - "value": "green", - "property_": "favourite_colour", + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "GREATER_THAN_INCLUSIVE", + "value": "21", + "property_": "age", + } + ], }, { - "operator": "EQUAL", - "value": "blue", - "property_": "favourite_colour", + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "EQUAL", + "value": "green", + "property_": "favourite_colour", + }, + { + "operator": "EQUAL", + "value": "blue", + "property_": "favourite_colour", + }, + ], }, ], - }, + "conditions": [], + } ], - "conditions": [], - } - ], - "feature_states": [ - { - "feature": { - "id": 15058, - "name": "string_feature", - "type": "STANDARD", - }, - "enabled": False, - "feature_state_value": "segment_override", - "django_id": 81027, - "multivariate_feature_state_values": [], - } - ], - }, - { - "id": 4268, - "name": "10_percent", - "rules": [ + "feature_states": [ + { + "feature": { + "id": 15058, + "name": "string_feature", + "type": "STANDARD", + }, + "enabled": False, + "feature_state_value": "segment_override", + "django_id": 81027, + "multivariate_feature_state_values": [], + } + ], + }, { - "type": "ALL", + "id": 4268, + "name": "10_percent", "rules": [ { - "type": "ANY", - "rules": [], - "conditions": [ + "type": "ALL", + "rules": [ { - "operator": "PERCENTAGE_SPLIT", - "value": "0.1", - "property_": "", + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "PERCENTAGE_SPLIT", + "value": "0.1", + "property_": "", + } + ], } ], + "conditions": [], } ], - "conditions": [], - } - ], - "feature_states": [ - { - "feature": { - "id": 15060, - "name": "basic_flag", - "type": "STANDARD", - }, - "enabled": True, - "feature_state_value": "", - "django_id": 81026, - "multivariate_feature_state_values": [], - } - ], - }, - { - "id": 16, - "name": "segment_two", - "rules": [ + "feature_states": [ + { + "feature": { + "id": 15060, + "name": "basic_flag", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "", + "django_id": 81026, + "multivariate_feature_state_values": [], + } + ], + }, { - "type": "ALL", + "id": 16, + "name": "segment_two", "rules": [ { - "type": "ANY", - "rules": [], - "conditions": [ - { - "operator": "EQUAL", - "value": "2", - "property_": "two", - }, + "type": "ALL", + "rules": [ { - "operator": "IS_SET", - "value": None, - "property_": "two", - }, + "type": "ANY", + "rules": [], + "conditions": [ + { + "operator": "EQUAL", + "value": "2", + "property_": "two", + }, + { + "operator": "IS_SET", + "value": None, + "property_": "two", + }, + ], + } ], + "conditions": [], } ], - "conditions": [], - } - ], - "feature_states": [ - { - "feature": { - "id": 15058, - "name": "string_feature", - "type": "STANDARD", - }, - "enabled": True, - "feature_state_value": "segment_two_override_priority_0", - "django_id": 78978, - "featurestate_uuid": UUID( - "1545809c-e97f-4a1f-9e67-8b4f2b396aa6" - ), - "feature_segment": {"priority": 0}, - "multivariate_feature_state_values": [], - } - ], - }, - { - "id": 17, - "name": "segment_three", - "rules": [ + "feature_states": [ + { + "feature": { + "id": 15058, + "name": "string_feature", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "segment_two_override_priority_0", + "django_id": 78978, + "featurestate_uuid": UUIDStr( + "1545809c-e97f-4a1f-9e67-8b4f2b396aa6" + ), + "feature_segment": {"priority": 0}, + "multivariate_feature_state_values": [], + } + ], + }, { - "type": "ALL", + "id": 17, + "name": "segment_three", "rules": [ { "type": "ALL", - "rules": [], - "conditions": [ - { - "operator": "EQUAL", - "value": "3", - "property_": "three", - }, + "rules": [ { - "operator": "IS_NOT_SET", - "value": None, - "property_": "something_that_is_not_set", - }, + "type": "ALL", + "rules": [], + "conditions": [ + { + "operator": "EQUAL", + "value": "3", + "property_": "three", + }, + { + "operator": "IS_NOT_SET", + "value": None, + "property_": "something_that_is_not_set", + }, + ], + } ], + "conditions": [], } ], - "conditions": [], - } - ], - "feature_states": [ - { - "feature": { - "id": 15058, - "name": "string_feature", - "type": "STANDARD", - }, - "enabled": True, - "feature_state_value": "segment_three_override_priority_1", - "django_id": 78977, - "featurestate_uuid": UUID( - "1545809c-e97f-4a1f-9e67-8b4f2b396aa7" - ), - "feature_segment": {"priority": 1}, - "multivariate_feature_state_values": [], - } + "feature_states": [ + { + "feature": { + "id": 15058, + "name": "string_feature", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "segment_three_override_priority_1", + "django_id": 78977, + "featurestate_uuid": UUIDStr( + "1545809c-e97f-4a1f-9e67-8b4f2b396aa7" + ), + "feature_segment": {"priority": 1}, + "multivariate_feature_state_values": [], + } + ], + }, ], + "hide_disabled_flags": False, }, - ], - "hide_disabled_flags": False, - }, - "feature_states": [ - { - "feature": {"id": 15058, "name": "string_feature", "type": "STANDARD"}, - "enabled": True, - "feature_state_value": "foo", - "django_id": 78978, - "feature_segment": None, - "multivariate_feature_state_values": [], - }, - { - "feature": {"id": 15059, "name": "integer_feature", "type": "STANDARD"}, - "enabled": True, - "feature_state_value": 1234, - "django_id": 78980, - "multivariate_feature_state_values": [], + "feature_states": [ + { + "feature": { + "id": 15058, + "name": "string_feature", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "foo", + "django_id": 78978, + "feature_segment": None, + "multivariate_feature_state_values": [], + }, + { + "feature": { + "id": 15059, + "name": "integer_feature", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": 1234, + "django_id": 78980, + "multivariate_feature_state_values": [], + }, + { + "feature": { + "id": 15060, + "name": "basic_flag", + "type": "STANDARD", + }, + "enabled": False, + "feature_state_value": None, + "django_id": 78982, + "multivariate_feature_state_values": [], + }, + { + "feature": { + "id": 15061, + "name": "float_feature", + "type": "STANDARD", + }, + "enabled": True, + "feature_state_value": "12.34", + "django_id": 78984, + "multivariate_feature_state_values": [], + }, + { + "feature": { + "id": 15062, + "name": "mv_feature", + "type": "MULTIVARIATE", + }, + "enabled": True, + "feature_state_value": "foo", + "django_id": 78986, + "multivariate_feature_state_values": [ + { + "id": 3404, + "percentage_allocation": 30.0, + "multivariate_feature_option": {"value": "baz"}, + }, + { + "id": 3402, + "percentage_allocation": 30.0, + "multivariate_feature_option": {"value": "bar"}, + }, + { + "id": 3405, + "percentage_allocation": 0.0, + "multivariate_feature_option": {"value": 1}, + }, + { + "id": 3406, + "percentage_allocation": 0.0, + "multivariate_feature_option": {"value": True}, + }, + ], + }, + ], }, + id="flagsmith_environments", + ), + pytest.param( + EnvironmentAPIKey, + "flagsmith_environment_api_key.json", { - "feature": {"id": 15060, "name": "basic_flag", "type": "STANDARD"}, - "enabled": False, - "feature_state_value": None, - "django_id": 78982, - "multivariate_feature_state_values": [], + "key": "ser.ZSwVCQrCGpXXKdvVsVxoie", + "active": True, + "client_api_key": "pQuzvsMLQoOVAwITrTWDQJ", + "created_at": DateTimeStr("2023-04-21T13:11:13.913178+00:00"), + "expires_at": None, + "id": 907, + "name": "TestKey", }, + id="flagsmith_environment_api_key", + ), + pytest.param( + Identity, + "flagsmith_identities.json", { - "feature": {"id": 15061, "name": "float_feature", "type": "STANDARD"}, - "enabled": True, - "feature_state_value": "12.34", - "django_id": 78984, - "multivariate_feature_state_values": [], + "composite_key": "pQuzvsMLQoOVAwITrTWDQJ_57c6edf1bbf145a1a23ea287ce44877f", + "created_date": DateTimeStr("2024-03-19T09:41:22.974595+00:00"), + "django_id": None, + "environment_api_key": "pQuzvsMLQoOVAwITrTWDQJ", + "identifier": "57c6edf1bbf145a1a23ea287ce44877f", + "identity_features": [ + { + "django_id": None, + "enabled": True, + "feature": { + "id": 67, + "name": "test_feature", + "type": "STANDARD", + }, + "featurestate_uuid": UUIDStr( + "20988957-d345-424e-9abc-1dc5a814da48" + ), + "feature_segment": None, + "feature_state_value": None, + "multivariate_feature_state_values": [], + } + ], + "identity_traits": [{"trait_key": "test_trait", "trait_value": 42}], + "identity_uuid": UUIDStr("118ecfc9-5234-4218-8af8-dd994dbfedc0"), }, + id="flagsmith_identities", + ), + pytest.param( + EnvironmentV2Meta, + "flagsmith_environments_v2:_META.json", { - "feature": {"id": 15062, "name": "mv_feature", "type": "MULTIVARIATE"}, - "enabled": True, - "feature_state_value": "foo", - "django_id": 78986, - "multivariate_feature_state_values": [ - { - "id": 3404, - "percentage_allocation": 30.0, - "multivariate_feature_option": {"value": "baz"}, - }, + "environment_id": "49268", + "document_key": "_META", + "allow_client_traits": True, + "amplitude_config": None, + "dynatrace_config": None, + "environment_api_key": "AQ9T6LixPqYMJkuqGJy3t2", + "feature_states": [ { - "id": 3402, - "percentage_allocation": 30.0, - "multivariate_feature_option": {"value": "bar"}, - }, - { - "id": 3405, - "percentage_allocation": 0.0, - "multivariate_feature_option": {"value": 1}, + "django_id": 577621, + "enabled": True, + "feature": { + "id": 100298, + "name": "test_feature", + "type": "MULTIVARIATE", + }, + "featurestate_uuid": UUIDStr( + "42d7805e-a9ac-401c-a7b7-d6583ac5a365" + ), + "feature_segment": None, + "feature_state_value": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "multivariate_feature_state_values": [ + { + "id": 185130, + "multivariate_feature_option": { + "id": 20919, + "value": "second", + }, + "mv_fs_value_uuid": UUIDStr( + "0b02ce41-9965-4c61-8b96-c8d76e3d4a27" + ), + "percentage_allocation": 10.0, + }, + { + "id": 48717, + "multivariate_feature_option": { + "id": 14004, + "value": True, + }, + "mv_fs_value_uuid": UUIDStr( + "cb05f49c-de1f-44f1-87eb-c3b55d473063" + ), + "percentage_allocation": 30.0, + }, + ], }, { - "id": 3406, - "percentage_allocation": 0.0, - "multivariate_feature_option": {"value": True}, + "django_id": 1041292, + "enabled": False, + "feature": { + "id": 172422, + "name": "feature", + "type": "STANDARD", + }, + "featurestate_uuid": UUIDStr( + "58b7b954-1b75-493a-82df-5be0efeedd2a" + ), + "feature_segment": None, + "feature_state_value": 3, + "multivariate_feature_state_values": [], }, ], + "heap_config": None, + "hide_disabled_flags": None, + "hide_sensitive_data": False, + "id": 49268, + "mixpanel_config": None, + "name": "Development", + "project": { + "enable_realtime_updates": False, + "hide_disabled_flags": False, + "id": 19368, + "name": "Example Project", + "organisation": { + "feature_analytics": False, + "id": 13, + "name": "Flagsmith", + "persist_trait_data": True, + "stop_serving_flags": False, + }, + "segments": [ + { + "feature_states": [], + "id": 44126, + "name": "test", + "rules": [ + { + "conditions": [], + "rules": [ + { + "conditions": [ + { + "operator": "EQUAL", + "property_": "test", + "value": "test", + } + ], + "rules": [], + "type": "ANY", + } + ], + "type": "ALL", + } + ], + } + ], + "server_key_only_feature_ids": [], + }, + "rudderstack_config": None, + "segment_config": None, + "updated_at": DateTimeStr("2025-11-16T13:28:31.244331+00:00"), + "use_identity_composite_key_for_hashing": True, + "use_identity_overrides_in_local_eval": False, + "webhook_config": None, }, - ], - } + id="flagsmith_environments_v2:_META", + ), + ], +) +def test_document__validate_json__expected_result( + request: pytest.FixtureRequest, + document_type: type[T], + json_data_filename: str, + expected_result: T, +) -> None: + # Given + type_adapter = TypeAdapter(document_type) + json_data = request.path.parent.joinpath(f"data/{json_data_filename}").read_text() + + # When + document = type_adapter.validate_json(json_data) + + # Then + assert document == expected_result From a29f1998297b8f4700121dc0fe4b8878fdbc2412 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Dec 2025 19:07:58 +0000 Subject: [PATCH 05/13] add identity override test --- ...ith_environments_v2:identity_override.json | 20 +++++++++++++ .../flagsmith_models/test_flagsmith_models.py | 30 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/integration/flagsmith_models/data/flagsmith_environments_v2:identity_override.json diff --git a/tests/integration/flagsmith_models/data/flagsmith_environments_v2:identity_override.json b/tests/integration/flagsmith_models/data/flagsmith_environments_v2:identity_override.json new file mode 100644 index 0000000..5a87ec6 --- /dev/null +++ b/tests/integration/flagsmith_models/data/flagsmith_environments_v2:identity_override.json @@ -0,0 +1,20 @@ +{ + "environment_id": "65061", + "document_key": "identity_override:136660:3018f59c-77a1-43df-a9a8-38723e99e441", + "environment_api_key": "pQuzvsMLQoOVAwITrTWDQJ", + "feature_state": { + "django_id": null, + "enabled": true, + "feature": { + "id": 136660, + "name": "test1", + "type": "STANDARD" + }, + "featurestate_uuid": "652d8931-37d9-438e-9825-f525b9e83077", + "feature_segment": null, + "feature_state_value": "test_override_value", + "multivariate_feature_state_values": [] + }, + "identifier": "Development_user_123456", + "identity_uuid": "3018f59c-77a1-43df-a9a8-38723e99e441" +} \ No newline at end of file diff --git a/tests/integration/flagsmith_models/test_flagsmith_models.py b/tests/integration/flagsmith_models/test_flagsmith_models.py index 02f3e00..2504d6b 100644 --- a/tests/integration/flagsmith_models/test_flagsmith_models.py +++ b/tests/integration/flagsmith_models/test_flagsmith_models.py @@ -3,7 +3,13 @@ import pytest from pydantic import TypeAdapter -from flagsmith_models import Environment, EnvironmentAPIKey, EnvironmentV2Meta, Identity +from flagsmith_models import ( + Environment, + EnvironmentAPIKey, + EnvironmentV2IdentityOverride, + EnvironmentV2Meta, + Identity, +) from flagsmith_models.types import DateTimeStr, UUIDStr T = TypeVar("T") @@ -468,6 +474,28 @@ }, id="flagsmith_environments_v2:_META", ), + pytest.param( + EnvironmentV2IdentityOverride, + "flagsmith_environments_v2:identity_override.json", + { + "environment_id": "65061", + "document_key": "identity_override:136660:3018f59c-77a1-43df-a9a8-38723e99e441", + "environment_api_key": "pQuzvsMLQoOVAwITrTWDQJ", + "feature_state": { + "django_id": None, + "enabled": True, + "feature": {"id": 136660, "name": "test1", "type": "STANDARD"}, + "featurestate_uuid": UUIDStr( + "652d8931-37d9-438e-9825-f525b9e83077" + ), + "feature_segment": None, + "feature_state_value": "test_override_value", + "multivariate_feature_state_values": [], + }, + "identifier": "Development_user_123456", + "identity_uuid": UUIDStr("3018f59c-77a1-43df-a9a8-38723e99e441"), + }, + ), ], ) def test_document__validate_json__expected_result( From 4d8c83a698f6bf51fb6f677fa87c5ab0330f4b82 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Dec 2025 19:09:36 +0000 Subject: [PATCH 06/13] document environment name default --- src/flagsmith_models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flagsmith_models/__init__.py b/src/flagsmith_models/__init__.py index 54ad178..33e1b96 100644 --- a/src/flagsmith_models/__init__.py +++ b/src/flagsmith_models/__init__.py @@ -226,7 +226,7 @@ class _EnvironmentFields(TypedDict): """Common fields for Environment documents.""" name: NotRequired[str] - """Environment name. TODO: Can we drop NotRequired and adjust test data?""" + """Environment name. Defaults to an empty string if not set.""" updated_at: NotRequired[DateTimeStr | None] """Last updated timestamp. If not set, current timestamp should be assumed.""" From ac9e8df4aeedd1e98257ed41dbcc4257a7d3b2eb Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Dec 2025 11:55:27 +0000 Subject: [PATCH 07/13] rename to flagsmith_schemas.dynamodb --- .../flagsmith_models => src/flagsmith_schemas}/__init__.py | 0 .../__init__.py => flagsmith_schemas/dynamodb.py} | 2 +- src/{flagsmith_models => flagsmith_schemas}/py.typed | 0 src/{flagsmith_models => flagsmith_schemas}/types.py | 0 tests/integration/flagsmith_schemas/__init__.py | 0 .../data/flagsmith_environment_api_key.json | 0 .../data/flagsmith_environments.json | 0 .../data/flagsmith_environments_v2:_META.json | 0 .../data/flagsmith_environments_v2:identity_override.json | 0 .../data/flagsmith_identities.json | 0 .../test_dynamodb.py} | 4 ++-- 11 files changed, 3 insertions(+), 3 deletions(-) rename {tests/integration/flagsmith_models => src/flagsmith_schemas}/__init__.py (100%) rename src/{flagsmith_models/__init__.py => flagsmith_schemas/dynamodb.py} (99%) rename src/{flagsmith_models => flagsmith_schemas}/py.typed (100%) rename src/{flagsmith_models => flagsmith_schemas}/types.py (100%) create mode 100644 tests/integration/flagsmith_schemas/__init__.py rename tests/integration/{flagsmith_models => flagsmith_schemas}/data/flagsmith_environment_api_key.json (100%) rename tests/integration/{flagsmith_models => flagsmith_schemas}/data/flagsmith_environments.json (100%) rename tests/integration/{flagsmith_models => flagsmith_schemas}/data/flagsmith_environments_v2:_META.json (100%) rename tests/integration/{flagsmith_models => flagsmith_schemas}/data/flagsmith_environments_v2:identity_override.json (100%) rename tests/integration/{flagsmith_models => flagsmith_schemas}/data/flagsmith_identities.json (100%) rename tests/integration/{flagsmith_models/test_flagsmith_models.py => flagsmith_schemas/test_dynamodb.py} (99%) diff --git a/tests/integration/flagsmith_models/__init__.py b/src/flagsmith_schemas/__init__.py similarity index 100% rename from tests/integration/flagsmith_models/__init__.py rename to src/flagsmith_schemas/__init__.py diff --git a/src/flagsmith_models/__init__.py b/src/flagsmith_schemas/dynamodb.py similarity index 99% rename from src/flagsmith_models/__init__.py rename to src/flagsmith_schemas/dynamodb.py index 33e1b96..fcc221f 100644 --- a/src/flagsmith_models/__init__.py +++ b/src/flagsmith_schemas/dynamodb.py @@ -7,7 +7,7 @@ from typing_extensions import NotRequired, TypedDict -from flagsmith_models.types import DateTimeStr, UUIDStr +from flagsmith_schemas.types import DateTimeStr, UUIDStr FeatureType = Literal["STANDARD", "MULTIVARIATE"] """Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values.""" diff --git a/src/flagsmith_models/py.typed b/src/flagsmith_schemas/py.typed similarity index 100% rename from src/flagsmith_models/py.typed rename to src/flagsmith_schemas/py.typed diff --git a/src/flagsmith_models/types.py b/src/flagsmith_schemas/types.py similarity index 100% rename from src/flagsmith_models/types.py rename to src/flagsmith_schemas/types.py diff --git a/tests/integration/flagsmith_schemas/__init__.py b/tests/integration/flagsmith_schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/flagsmith_models/data/flagsmith_environment_api_key.json b/tests/integration/flagsmith_schemas/data/flagsmith_environment_api_key.json similarity index 100% rename from tests/integration/flagsmith_models/data/flagsmith_environment_api_key.json rename to tests/integration/flagsmith_schemas/data/flagsmith_environment_api_key.json diff --git a/tests/integration/flagsmith_models/data/flagsmith_environments.json b/tests/integration/flagsmith_schemas/data/flagsmith_environments.json similarity index 100% rename from tests/integration/flagsmith_models/data/flagsmith_environments.json rename to tests/integration/flagsmith_schemas/data/flagsmith_environments.json diff --git a/tests/integration/flagsmith_models/data/flagsmith_environments_v2:_META.json b/tests/integration/flagsmith_schemas/data/flagsmith_environments_v2:_META.json similarity index 100% rename from tests/integration/flagsmith_models/data/flagsmith_environments_v2:_META.json rename to tests/integration/flagsmith_schemas/data/flagsmith_environments_v2:_META.json diff --git a/tests/integration/flagsmith_models/data/flagsmith_environments_v2:identity_override.json b/tests/integration/flagsmith_schemas/data/flagsmith_environments_v2:identity_override.json similarity index 100% rename from tests/integration/flagsmith_models/data/flagsmith_environments_v2:identity_override.json rename to tests/integration/flagsmith_schemas/data/flagsmith_environments_v2:identity_override.json diff --git a/tests/integration/flagsmith_models/data/flagsmith_identities.json b/tests/integration/flagsmith_schemas/data/flagsmith_identities.json similarity index 100% rename from tests/integration/flagsmith_models/data/flagsmith_identities.json rename to tests/integration/flagsmith_schemas/data/flagsmith_identities.json diff --git a/tests/integration/flagsmith_models/test_flagsmith_models.py b/tests/integration/flagsmith_schemas/test_dynamodb.py similarity index 99% rename from tests/integration/flagsmith_models/test_flagsmith_models.py rename to tests/integration/flagsmith_schemas/test_dynamodb.py index 2504d6b..074b611 100644 --- a/tests/integration/flagsmith_models/test_flagsmith_models.py +++ b/tests/integration/flagsmith_schemas/test_dynamodb.py @@ -3,14 +3,14 @@ import pytest from pydantic import TypeAdapter -from flagsmith_models import ( +from flagsmith_schemas.dynamodb import ( Environment, EnvironmentAPIKey, EnvironmentV2IdentityOverride, EnvironmentV2Meta, Identity, ) -from flagsmith_models.types import DateTimeStr, UUIDStr +from flagsmith_schemas.types import DateTimeStr, UUIDStr T = TypeVar("T") From 0179e003e0b39a0676be6faeaa32d5a7ec5ed5b9 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Dec 2025 11:57:43 +0000 Subject: [PATCH 08/13] Improve docstring --- src/flagsmith_schemas/dynamodb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flagsmith_schemas/dynamodb.py b/src/flagsmith_schemas/dynamodb.py index fcc221f..0333c6c 100644 --- a/src/flagsmith_schemas/dynamodb.py +++ b/src/flagsmith_schemas/dynamodb.py @@ -1,6 +1,6 @@ """ -Types describing Flagsmith Edge API's data model. -The schemas are written to DynamoDB documents by Core, and read by Edge. +The types in this module describe the Edge API's data model. +They are used to type DynamoDB documents representing Flagsmith entities. """ from typing import Literal, TypeAlias From c74378574be7ec403586cdc3b58184809b16358a Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Dec 2025 12:01:40 +0000 Subject: [PATCH 09/13] consolidate scalar types in the types module --- src/flagsmith_schemas/dynamodb.py | 57 ++++++------------------------- src/flagsmith_schemas/types.py | 47 ++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/src/flagsmith_schemas/dynamodb.py b/src/flagsmith_schemas/dynamodb.py index 0333c6c..0fd3ee3 100644 --- a/src/flagsmith_schemas/dynamodb.py +++ b/src/flagsmith_schemas/dynamodb.py @@ -3,56 +3,19 @@ They are used to type DynamoDB documents representing Flagsmith entities. """ -from typing import Literal, TypeAlias +from typing import Literal from typing_extensions import NotRequired, TypedDict -from flagsmith_schemas.types import DateTimeStr, UUIDStr - -FeatureType = Literal["STANDARD", "MULTIVARIATE"] -"""Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values.""" - -FeatureValue: TypeAlias = object -"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string. - -The default (SaaS) maximum length for strings is 20000 characters. -""" - -ContextValue: TypeAlias = int | float | bool | str -"""Represents a scalar value in the Flagsmith context, e.g., of an identity trait. -Here's how we store different types: -- Numeric string values (int, float) are stored as numbers. -- Boolean values are stored as booleans. -- All other values are stored as strings. -- Maximum length for strings is 2000 characters. - -This type does not include complex structures like lists or dictionaries. -""" - -ConditionOperator = Literal[ - "EQUAL", - "GREATER_THAN", - "LESS_THAN", - "LESS_THAN_INCLUSIVE", - "CONTAINS", - "GREATER_THAN_INCLUSIVE", - "NOT_CONTAINS", - "NOT_EQUAL", - "REGEX", - "PERCENTAGE_SPLIT", - "MODULO", - "IS_SET", - "IS_NOT_SET", - "IN", -] -"""Represents segment condition operators used by Flagsmith engine.""" - -RuleType = Literal[ - "ALL", - "ANY", - "NONE", -] -"""Represents segment rule types used by Flagsmith engine.""" +from flagsmith_schemas.types import ( + ConditionOperator, + ContextValue, + DateTimeStr, + FeatureType, + FeatureValue, + RuleType, + UUIDStr, +) class Feature(TypedDict): diff --git a/src/flagsmith_schemas/types.py b/src/flagsmith_schemas/types.py index fce362b..b727fa2 100644 --- a/src/flagsmith_schemas/types.py +++ b/src/flagsmith_schemas/types.py @@ -1,7 +1,52 @@ -from typing import TypeAlias +from typing import Literal, TypeAlias UUIDStr: TypeAlias = str """A string representing a UUID.""" DateTimeStr: TypeAlias = str """A string representing a date and time in ISO 8601 format.""" + +FeatureType = Literal["STANDARD", "MULTIVARIATE"] +"""Represents the type of a Flagsmith feature. Multivariate features include multiple weighted values.""" + +FeatureValue: TypeAlias = object +"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string. + +The default (SaaS) maximum length for strings is 20000 characters. +""" + +ContextValue: TypeAlias = int | float | bool | str +"""Represents a scalar value in the Flagsmith context, e.g., of an identity trait. +Here's how we store different types: +- Numeric string values (int, float) are stored as numbers. +- Boolean values are stored as booleans. +- All other values are stored as strings. +- Maximum length for strings is 2000 characters. + +This type does not include complex structures like lists or dictionaries. +""" + +ConditionOperator = Literal[ + "EQUAL", + "GREATER_THAN", + "LESS_THAN", + "LESS_THAN_INCLUSIVE", + "CONTAINS", + "GREATER_THAN_INCLUSIVE", + "NOT_CONTAINS", + "NOT_EQUAL", + "REGEX", + "PERCENTAGE_SPLIT", + "MODULO", + "IS_SET", + "IS_NOT_SET", + "IN", +] +"""Represents segment condition operators used by Flagsmith engine.""" + +RuleType = Literal[ + "ALL", + "ANY", + "NONE", +] +"""Represents segment rule types used by Flagsmith engine.""" From d014c4a00ea1d1b9648cbc7d60e00d0c80b82edc Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 26 Dec 2025 15:07:53 +0000 Subject: [PATCH 10/13] Improve feature state docstrings --- src/flagsmith_schemas/dynamodb.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/flagsmith_schemas/dynamodb.py b/src/flagsmith_schemas/dynamodb.py index 0fd3ee3..c1928f1 100644 --- a/src/flagsmith_schemas/dynamodb.py +++ b/src/flagsmith_schemas/dynamodb.py @@ -38,10 +38,13 @@ class MultivariateFeatureOption(TypedDict): class MultivariateFeatureStateValue(TypedDict): - """Represents a multivariate feature state value assigned to an identity or environment.""" + """Represents a multivariate feature state value. + + **NOTE**: identity overrides are meant to hold only one of these, solely to inform the UI which option is selected for the given identity. + """ id: NotRequired[int | None] - """Unique identifier for the multivariate feature state value in Core. TODO: document why and when this can be `None`.""" + """Unique identifier for the multivariate feature state value in Core. If feature state created via Core's `edge-identities` API, this can be missing or `None`.""" mv_fs_value_uuid: NotRequired[UUIDStr] """The UUID for this multivariate feature state value. Should be used if `id` is `None`.""" percentage_allocation: float @@ -67,7 +70,7 @@ class FeatureState(TypedDict): feature_state_value: FeatureValue """The value for this feature state.""" django_id: NotRequired[int | None] - """Unique identifier for the feature state in Core. TODO: document why and when this can be `None`.""" + """Unique identifier for the feature state in Core. If feature state created via Core's `edge-identities` API, this can be missing or `None`.""" featurestate_uuid: NotRequired[UUIDStr] """The UUID for this feature state. Should be used if `django_id` is `None`. If not set, should be generated.""" feature_segment: NotRequired[FeatureSegment | None] @@ -274,7 +277,7 @@ class Identity(TypedDict): identity_traits: list[Trait] """List of traits associated with this identity.""" django_id: NotRequired[int | None] - """Unique identifier for the identity in Core. TODO: document why and when this can be `None`.""" + """Unique identifier for the identity in Core. If identity created via Core's `edge-identities` API, this can be missing or `None`.""" class Environment(_EnvironmentFields): From 37a6acd105c1adac055d8f5d7bfc82e74677df38 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 26 Dec 2025 15:36:20 +0000 Subject: [PATCH 11/13] further docstring improvements --- src/flagsmith_schemas/dynamodb.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/flagsmith_schemas/dynamodb.py b/src/flagsmith_schemas/dynamodb.py index c1928f1..645d168 100644 --- a/src/flagsmith_schemas/dynamodb.py +++ b/src/flagsmith_schemas/dynamodb.py @@ -19,7 +19,7 @@ class Feature(TypedDict): - """Represents a Flagsmith feature defined at project level.""" + """Represents a Flagsmith feature, defined at project level.""" id: int """Unique identifier for the feature in Core.""" @@ -29,7 +29,7 @@ class Feature(TypedDict): class MultivariateFeatureOption(TypedDict): - """A container for a feature state value of a multivariate feature state.""" + """Represents a single multivariate feature option in the Flagsmith UI.""" id: NotRequired[int | None] """Unique identifier for the multivariate feature option in Core. **DEPRECATED**: MultivariateFeatureValue.id should be used instead.""" @@ -40,11 +40,11 @@ class MultivariateFeatureOption(TypedDict): class MultivariateFeatureStateValue(TypedDict): """Represents a multivariate feature state value. - **NOTE**: identity overrides are meant to hold only one of these, solely to inform the UI which option is selected for the given identity. + Identity overrides are meant to hold only one of these, solely to inform the UI which option is selected for the given identity. """ id: NotRequired[int | None] - """Unique identifier for the multivariate feature state value in Core. If feature state created via Core's `edge-identities` API, this can be missing or `None`.""" + """Unique identifier for the multivariate feature state value in Core. If feature state created via `edge-identities` APIs in Core, this can be missing or `None`.""" mv_fs_value_uuid: NotRequired[UUIDStr] """The UUID for this multivariate feature state value. Should be used if `id` is `None`.""" percentage_allocation: float @@ -61,7 +61,7 @@ class FeatureSegment(TypedDict): class FeatureState(TypedDict): - """Represents a Flagsmith feature state. Used to define the state of a feature for an environment, segment overrides, and identity overrides.""" + """Used to define the state of a feature for an environment, segment overrides, and identity overrides.""" feature: Feature """The feature that this feature state is for.""" @@ -70,7 +70,7 @@ class FeatureState(TypedDict): feature_state_value: FeatureValue """The value for this feature state.""" django_id: NotRequired[int | None] - """Unique identifier for the feature state in Core. If feature state created via Core's `edge-identities` API, this can be missing or `None`.""" + """Unique identifier for the feature state in Core. If feature state created via Core's `edge-identities` APIs in Core, this can be missing or `None`.""" featurestate_uuid: NotRequired[UUIDStr] """The UUID for this feature state. Should be used if `django_id` is `None`. If not set, should be generated.""" feature_segment: NotRequired[FeatureSegment | None] @@ -324,6 +324,6 @@ class EnvironmentV2IdentityOverride(TypedDict): identifier: str """Unique identifier for the identity. **INDEXED**.""" identity_uuid: str - """The UUID for this identity. **INDEXED**.""" + """The UUID for this identity, used by `edge-identities` APIs in Core. **INDEXED**.""" feature_state: FeatureState """The feature state override for this identity.""" From 581de51505d1daa4cd8af841ee617affa0090800 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 26 Dec 2025 15:40:06 +0000 Subject: [PATCH 12/13] use Decimal for numeric types --- src/flagsmith_schemas/dynamodb.py | 30 ++++++++++++++++-------------- src/flagsmith_schemas/types.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/flagsmith_schemas/dynamodb.py b/src/flagsmith_schemas/dynamodb.py index 645d168..187d506 100644 --- a/src/flagsmith_schemas/dynamodb.py +++ b/src/flagsmith_schemas/dynamodb.py @@ -11,6 +11,8 @@ ConditionOperator, ContextValue, DateTimeStr, + DynamoFloat, + DynamoInt, FeatureType, FeatureValue, RuleType, @@ -21,7 +23,7 @@ class Feature(TypedDict): """Represents a Flagsmith feature, defined at project level.""" - id: int + id: DynamoInt """Unique identifier for the feature in Core.""" name: str """Name of the feature. Must be unique within a project.""" @@ -31,7 +33,7 @@ class Feature(TypedDict): class MultivariateFeatureOption(TypedDict): """Represents a single multivariate feature option in the Flagsmith UI.""" - id: NotRequired[int | None] + id: NotRequired[DynamoInt | None] """Unique identifier for the multivariate feature option in Core. **DEPRECATED**: MultivariateFeatureValue.id should be used instead.""" value: FeatureValue """The feature state value that should be served when this option's parent multivariate feature state is selected by the engine.""" @@ -43,11 +45,11 @@ class MultivariateFeatureStateValue(TypedDict): Identity overrides are meant to hold only one of these, solely to inform the UI which option is selected for the given identity. """ - id: NotRequired[int | None] + id: NotRequired[DynamoInt | None] """Unique identifier for the multivariate feature state value in Core. If feature state created via `edge-identities` APIs in Core, this can be missing or `None`.""" mv_fs_value_uuid: NotRequired[UUIDStr] """The UUID for this multivariate feature state value. Should be used if `id` is `None`.""" - percentage_allocation: float + percentage_allocation: DynamoFloat """The percentage allocation for this multivariate feature state value. Should be between or equal to 0 and 100.""" multivariate_feature_option: MultivariateFeatureOption """The multivariate feature option that this value corresponds to.""" @@ -56,7 +58,7 @@ class MultivariateFeatureStateValue(TypedDict): class FeatureSegment(TypedDict): """Represents data specific to a segment feature override.""" - priority: NotRequired[int | None] + priority: NotRequired[DynamoInt | None] """The priority of this segment feature override. Lower numbers indicate stronger priority. If `None` or not set, the weakest priority is assumed.""" @@ -69,7 +71,7 @@ class FeatureState(TypedDict): """Whether the feature is enabled or disabled.""" feature_state_value: FeatureValue """The value for this feature state.""" - django_id: NotRequired[int | None] + django_id: NotRequired[DynamoInt | None] """Unique identifier for the feature state in Core. If feature state created via Core's `edge-identities` APIs in Core, this can be missing or `None`.""" featurestate_uuid: NotRequired[UUIDStr] """The UUID for this feature state. Should be used if `django_id` is `None`. If not set, should be generated.""" @@ -119,7 +121,7 @@ class SegmentRule(TypedDict): class Segment(TypedDict): """Represents a Flagsmith segment. Carries rules, feature overrides, and segment rules.""" - id: int + id: DynamoInt """Unique identifier for the segment in Core.""" name: str """Name of the segment.""" @@ -132,7 +134,7 @@ class Segment(TypedDict): class Organisation(TypedDict): """Represents data about a Flagsmith organisation. Carries settings necessary for an SDK API operation.""" - id: int + id: DynamoInt """Unique identifier for the organisation in Core.""" name: str """Organisation name as set via Core.""" @@ -147,7 +149,7 @@ class Organisation(TypedDict): class Project(TypedDict): """Represents data about a Flagsmith project. Carries settings necessary for an SDK API operation.""" - id: int + id: DynamoInt """Unique identifier for the project in Core.""" name: str """Project name as set via Core.""" @@ -155,7 +157,7 @@ class Project(TypedDict): """The organisation that this project belongs to.""" segments: list[Segment] """List of segments.""" - server_key_only_feature_ids: NotRequired[list[int]] + server_key_only_feature_ids: NotRequired[list[DynamoInt]] """List of feature IDs that are skipped when the SDK API serves flags for a public client-side key.""" enable_realtime_updates: NotRequired[bool] """Whether the SDK API should use real-time updates. Defaults to `False`. Not currently used neither by SDK APIs nor by SDKs themselves.""" @@ -237,7 +239,7 @@ class EnvironmentAPIKey(TypedDict): **DynamoDB table**: `flagsmith_environment_api_key` """ - id: int + id: DynamoInt """Unique identifier for the environment API key in Core. **INDEXED**.""" key: str """The server-side API key string, e.g. `"ser.xxxxxxxxxxxxx"`. **INDEXED**.""" @@ -276,7 +278,7 @@ class Identity(TypedDict): """List of identity overrides for this identity.""" identity_traits: list[Trait] """List of traits associated with this identity.""" - django_id: NotRequired[int | None] + django_id: NotRequired[DynamoInt | None] """Unique identifier for the identity in Core. If identity created via Core's `edge-identities` API, this can be missing or `None`.""" @@ -286,7 +288,7 @@ class Environment(_EnvironmentFields): **DynamoDB table**: `flagsmith_environments` """ - id: int + id: DynamoInt """Unique identifier for the environment in Core. **INDEXED**.""" api_key: str """Public client-side API key for the environment. **INDEXED**.""" @@ -305,7 +307,7 @@ class EnvironmentV2Meta(_EnvironmentFields): document_key: Literal["_META"] """The fixed document key for the environment v2 document. Always `"_META"`. **INDEXED**.""" - id: int + id: DynamoInt """Unique identifier for the environment in Core. Exists for compatibility with the API environment document schema.""" diff --git a/src/flagsmith_schemas/types.py b/src/flagsmith_schemas/types.py index b727fa2..ee5e891 100644 --- a/src/flagsmith_schemas/types.py +++ b/src/flagsmith_schemas/types.py @@ -1,5 +1,20 @@ +from decimal import Decimal from typing import Literal, TypeAlias +DynamoInt: TypeAlias = Decimal +"""An integer value stored in DynamoDB. + +DynamoDB represents all numbers as `Decimal`. +`DynamoInt` indicates that the value should be treated as an integer. +""" + +DynamoFloat: TypeAlias = Decimal +"""A float value stored in DynamoDB. + +DynamoDB represents all numbers as `Decimal`. +`DynamoFloat` indicates that the value should be treated as a float. +""" + UUIDStr: TypeAlias = str """A string representing a UUID.""" From 0b438e584c70b23910bae834105ef111877f7eb2 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 26 Dec 2025 16:35:34 +0000 Subject: [PATCH 13/13] fix ContextValue --- src/flagsmith_schemas/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flagsmith_schemas/types.py b/src/flagsmith_schemas/types.py index ee5e891..b6d628b 100644 --- a/src/flagsmith_schemas/types.py +++ b/src/flagsmith_schemas/types.py @@ -30,7 +30,7 @@ The default (SaaS) maximum length for strings is 20000 characters. """ -ContextValue: TypeAlias = int | float | bool | str +ContextValue: TypeAlias = DynamoInt | DynamoFloat | bool | str """Represents a scalar value in the Flagsmith context, e.g., of an identity trait. Here's how we store different types: - Numeric string values (int, float) are stored as numbers.