Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ optional-dependencies = { test-tools = [
"prometheus-client (>=0.0.16)",
], flagsmith-schemas = [
"typing_extensions",
"flagsmith-flag-engine>10",
] }
authors = [
{ name = "Matthew Elwell" },
Expand Down
9 changes: 8 additions & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
"release-type": "python"
}
},
"extra-files": [
{
"path": "uv.lock",
"type": "toml",
"jsonpath": "$.package[?(@.name.value=='flagsmith-common')].version"
}
],
"changelog-sections": [
{
"type": "chore",
Expand Down Expand Up @@ -59,4 +66,4 @@
"section": "Tests"
}
]
}
}
217 changes: 217 additions & 0 deletions src/flagsmith_schemas/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"""
The types in this module describe Flagsmith SDK API request and response schemas.
The docstrings here comprise user-facing documentation for these types.

The types are used by:
- SDK API OpenAPI schema generation.
- Flagsmith's API and SDK implementations written in Python.

These types can be used with for validation and serialization
with any library that supports TypedDict, such as Pydantic or typeguard.

When updating this module, ensure that the changes are backwards compatible.
"""

from flag_engine.engine import ContextValue
from flag_engine.segments.types import ConditionOperator, RuleType
from typing_extensions import NotRequired, TypedDict

from flagsmith_schemas.types import FeatureType, FeatureValue, UUIDStr


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
"""Feature type."""


class MultivariateFeatureOption(TypedDict):
"""Represents a single multivariate feature option in the Flagsmith UI."""

value: str
"""The feature state value that should be served when this option's parent multivariate feature state is selected by the engine."""
Comment on lines +33 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is out of scope, but is worth a note: I think this entity makes no sense in the API. I understand this represents the data model, but it also makes the API schema slightly harder to grasp as it adds hierarchy with no value.

Copy link
Member Author

@khvn26 khvn26 Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its sole purpose is to inform the UI of which option is selected by an override. I guess it's for cases when multiple options bear the same value.

Definitely no value in the SDK context, I agree.



class MultivariateFeatureStateValue(TypedDict):
"""Represents a multivariate feature state value."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Represents a multivariate feature state value."""
"""Represents a feature override that will apply to identities falling into the specified percentage of matched identities."""

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as my other reply.


id: int | None
"""Unique identifier for the multivariate feature state value in Core. Used for multivariate bucketing. If feature state created via `edge-identities` APIs in Core, this can be missing or `None`."""
mv_fs_value_uuid: UUIDStr | None
"""The UUID for this multivariate feature state value. Should be used for multivariate bucketing if `id` is null."""
percentage_allocation: float
"""The percentage allocation for this multivariate feature state value. Should be between or equal to 0 and 100; total percentage allocation of grouped `MultivariateFeatureStateValue` must not exceed 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: int | None
"""The priority of this segment feature override. Lower numbers indicate stronger priority. If null or not set, the weakest priority is assumed."""


class FeatureState(TypedDict):
"""Used to define the state of a feature for an environment, segment overrides, and identity overrides."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Used to define the state of a feature for an environment, segment overrides, and identity overrides."""
"""A feature override for an environment, a segment, or an identity."""

Copy link
Member Author

@khvn26 khvn26 Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we use the "feature override" term in relation to environment feature states anywhere else in the product, or the documentation.


feature: Feature
"""The feature that this feature state is for."""
enabled: bool
"""Whether the feature is enabled or disabled."""
feature_state_value: object
"""The value for this feature state."""
featurestate_uuid: UUIDStr
"""The UUID for this feature state."""
feature_segment: FeatureSegment | None
"""Segment override data, if this feature state is for a segment override."""
multivariate_feature_state_values: list[MultivariateFeatureStateValue]
"""List of multivariate feature state values, if this feature state is for a multivariate feature."""


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: str
"""Value to be compared against in this condition. May be `None` for `IS_SET` and `IS_NOT_SET` operators."""
property_: str
"""The property (context key) this condition applies to. May be `None` for the `PERCENTAGE_SPLIT` operator.

Named `property_` for legacy reasons.
"""
Comment on lines +93 to +97
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth introducing a new attribute context_key and have property_ aliasing to it for backwards compatibility?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposal does sound good to me, but it's out of the scope of this PR; we're documenting the status quo here.



class SegmentRule(TypedDict):
"""Represents a rule within a segment used by Flagsmith engine."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Represents a rule within a segment used by Flagsmith engine."""
"""Represents a group of logic conditions to match identities with; root segment rules may contain sub rules."""

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment.

I appreciate the addition concerning recursion, however; I believe it's good to include this.


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."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Represents a Flagsmith segment. Carries rules, feature overrides, and segment rules."""
"""Represents a dynamic group of identities matching the set of `rules`, which will be applied all overrides from `feature_states`."""

Copy link
Member Author

@khvn26 khvn26 Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to disagree with this edit. We're not explaining the product here, rather, documenting the SDK API. Using Flagsmith terminology clearly documented/explained elsewhere is okay.


id: int
"""Unique identifier for the segment in Core."""
name: str
"""Segment name."""
rules: list[SegmentRule]
"""List of rules within the segment."""
feature_states: NotRequired[list[FeatureState]]
"""List of segment overrides."""


class Project(TypedDict):
"""Represents a Flagsmith project. For SDKs, this is mainly used to convey segment data."""

segments: list[Segment]
"""List of segments."""


class IdentityOverride(TypedDict):
"""Represents an identity override, defining feature states specific to an identity."""

identifier: str
"""Unique identifier for the identity."""
identity_features: list[FeatureState]
"""List of identity overrides for this identity."""


class InputTrait(TypedDict):
"""Represents a key-value pair trait provided as input when creating or updating an identity."""

trait_key: str
"""Trait key."""
trait_value: ContextValue
"""Trait value. If `null`, the trait will be deleted."""
transient: NotRequired[bool | None]
"""Whether this trait is transient (not persisted). Defaults to `false`."""


class V1Flag(TypedDict):
"""Represents a single flag (feature state) returned by the Flagsmith SDK."""

feature: Feature
"""The feature that this flag represents."""
enabled: bool
"""Whether the feature is enabled or disabled."""
feature_state_value: FeatureValue
"""The value for this feature state."""


### Root request schemas below. ###


class V1IdentitiesRequest(TypedDict):
"""`/api/v1/identities/` request.

Used to retrieve flags for an identity and store its traits.
"""

identifier: str
"""Unique identifier for the identity."""
traits: NotRequired[list[InputTrait] | None]
"""List of traits to set for the identity. If `null` or not provided, no traits are set or updated."""
transient: NotRequired[bool | None]
"""Whether the identity is transient (not persisted). Defaults to `false`."""


### Root response schemas below. ###


class V1EnvironmentDocumentResponse(TypedDict):
"""`/api/v1/environments-document/` response.

Powers Flagsmith SDK's local evaluation mode.
"""

api_key: str
"""Public client-side API key for the environment, used to identify it."""
feature_states: list[FeatureState]
"""List of feature states representing the environment defaults."""
identity_overrides: list[IdentityOverride]
"""List of identity overrides defined for this environment."""
name: str
"""Environment name."""
project: Project
"""Project-specific data for this environment."""


V1FlagsResponse = list[V1Flag]
"""`/api/v1/flags/` response.

A list of flags for the specified environment."""


class V1IdentitiesResponse(TypedDict):
"""`/api/v1/identities/` response.

Represents the identity created or updated, along with its flags.
"""

identifier: str
"""Unique identifier for the identity."""
flags: list[V1Flag]
"""List of flags (feature states) for the identity."""
traits: list[Trait]
"""List of traits associated with the identity."""
9 changes: 4 additions & 5 deletions src/flagsmith_schemas/dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@

from typing import Annotated, Literal

from flag_engine.segments.types import ConditionOperator, RuleType
from typing_extensions import NotRequired, TypedDict

from flagsmith_schemas.constants import PYDANTIC_INSTALLED
from flagsmith_schemas.types import (
ConditionOperator,
DateTimeStr,
DynamoContextValue,
DynamoFeatureValue,
DynamoFloat,
DynamoInt,
FeatureType,
RuleType,
UUIDStr,
)

Expand Down Expand Up @@ -99,9 +98,9 @@ class Trait(TypedDict):
"""Represents a key-value pair associated with an identity."""

trait_key: str
"""Key of the trait."""
"""Trait key."""
trait_value: DynamoContextValue
"""Value of the trait."""
"""Trait value."""


class SegmentCondition(TypedDict):
Expand Down Expand Up @@ -138,7 +137,7 @@ class Segment(TypedDict):
"""Name of the segment."""
rules: list[SegmentRule]
"""List of rules within the segment."""
feature_states: list[FeatureState]
feature_states: NotRequired[list[FeatureState]]
"""List of segment overrides."""


Expand Down
39 changes: 13 additions & 26 deletions src/flagsmith_schemas/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from flagsmith_schemas.constants import PYDANTIC_INSTALLED

if PYDANTIC_INSTALLED:
from pydantic import WithJsonSchema

from flagsmith_schemas.pydantic_types import (
ValidateDecimalAsFloat,
ValidateDecimalAsInt,
Expand All @@ -15,6 +17,9 @@
# This code runs at runtime when Pydantic is not installed.
# We could use PEP 649 strings with `Annotated`, but Pydantic is inconsistent in how it parses them.
# Define dummy types instead.
def WithJsonSchema(_: object) -> object:
return ...

ValidateDecimalAsFloat = ...
ValidateDecimalAsInt = ...
ValidateDynamoFeatureStateValue = ...
Expand All @@ -36,7 +41,11 @@
`DynamoFloat` indicates that the value should be treated as a float.
"""

UUIDStr: TypeAlias = Annotated[str, ValidateStrAsUUID]
UUIDStr: TypeAlias = Annotated[
str,
ValidateStrAsUUID,
WithJsonSchema({"type": "string", "format": "uuid"}),
]
"""A string representing a UUID."""

DateTimeStr: TypeAlias = Annotated[str, ValidateStrAsISODateTime]
Expand All @@ -49,7 +58,7 @@
DynamoInt | bool | str | None,
ValidateDynamoFeatureStateValue,
]
"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string.
"""Represents the value of a Flagsmith feature stored in DynamoDB. Can be stored a boolean, an integer, or a string.

The default (SaaS) maximum length for strings is 20000 characters.
"""
Expand All @@ -65,27 +74,5 @@
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."""
FeatureValue: TypeAlias = int | bool | str | None
"""Represents the value of a Flagsmith feature. Can be stored a boolean, an integer, or a string."""
18 changes: 18 additions & 0 deletions tests/integration/flagsmith_schemas/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pydantic import TypeAdapter

from flagsmith_schemas.api import FeatureState


def test_feature_state__featurestate_uuid__expected_json_schema() -> None:
# Given
type_adapter: TypeAdapter[FeatureState] = TypeAdapter(FeatureState)

# When
schema = type_adapter.json_schema()["properties"]["featurestate_uuid"]

# Then
assert schema == {
"format": "uuid",
"title": "Featurestate Uuid",
"type": "string",
}
Loading