Skip to content
Merged
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
27 changes: 24 additions & 3 deletions src/flagpole/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,22 @@ def load_json_schema() -> dict[str, Any]:
return data


@dataclasses.dataclass(frozen=True)
class OwnerInfo:
team: str
"The team that owns this feature."

email: str | None = None
"The email address of the owner."


@dataclasses.dataclass(frozen=True)
class Feature:
name: str
"The feature name."

owner: str
"The owner of this feature. Either an email address or team name, preferably."
owner: str | OwnerInfo
"The owner of this feature."

enabled: bool = dataclasses.field(default=True)
"Whether or not the feature is enabled."
Expand Down Expand Up @@ -133,9 +142,20 @@ def from_feature_dictionary(cls, name: str, config_dict: dict[str, Any]) -> Feat
raise InvalidFeatureFlagConfiguration("Feature has no segments defined")
try:
segments = [Segment.from_dict(segment) for segment in segment_data]

raw_owner = config_dict.get("owner", "")
owner: str | OwnerInfo
if isinstance(raw_owner, dict):
owner = OwnerInfo(
team=raw_owner["team"],
email=raw_owner.get("email"),
)
else:
owner = str(raw_owner)

feature = cls(
name=name,
owner=str(config_dict.get("owner", "")),
owner=owner,
enabled=bool(config_dict.get("enabled", True)),
created_at=str(config_dict.get("created_at")),
segments=segments,
Expand Down Expand Up @@ -198,6 +218,7 @@ def to_json_str(self) -> str:

__all__ = [
"Feature",
"OwnerInfo",
"InvalidFeatureFlagConfiguration",
"ContextBuilder",
"EvaluationContext",
Expand Down
26 changes: 23 additions & 3 deletions src/flagpole/flagpole-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,29 @@
},
"owner": {
"title": "Owner",
"description": "The owner of this feature. Either an email address or team name, preferably.",
"minLength": 1,
"type": "string"
"description": "The owner of this feature.",
"oneOf": [
{
"type": "string",
"minLength": 1
},
{
"type": "object",
"properties": {
"team": {
"type": "string",
"minLength": 1,
"description": "The team that owns this feature."
},
"email": {
"type": ["string", "null"],
"description": "The email address of the owner. Not required."
}
},
"required": ["team"],
"additionalProperties": false
}
]
},
"segments": {
"title": "Segments",
Expand Down
62 changes: 61 additions & 1 deletion tests/flagpole/test_flagpole_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest
import yaml

from flagpole import Feature
from flagpole import Feature, OwnerInfo
from flagpole.conditions import EqualsCondition, Segment
from flagpole.evaluation_context import EvaluationContext
from flagpole.flagpole_eval import evaluate_flag, get_arguments, read_feature
Expand Down Expand Up @@ -163,6 +163,66 @@ def test_read_feature_missing_flag(self):
Path(temp_file).unlink()


class TestOwnerInfo:
"""Test OwnerInfo dataclass and Feature with OwnerInfo owner."""

def test_feature_with_owner_info(self):
"""Test creating a Feature with OwnerInfo as owner."""
owner = OwnerInfo(team="test-team", email="owner@sentry.io")
feature = Feature(
name="test-feature",
owner=owner,
enabled=True,
segments=[Segment(name="test-segment", rollout=100, conditions=[])],
)

assert isinstance(feature.owner, OwnerInfo)
assert feature.owner.team == "test-team"
assert feature.owner.email == "owner@sentry.io"

def test_feature_with_owner_info_no_email(self):
"""Test creating a Feature with OwnerInfo without email."""
owner = OwnerInfo(team="test-team")
feature = Feature(
name="test-feature",
owner=owner,
enabled=True,
segments=[Segment(name="test-segment", rollout=100, conditions=[])],
)

assert isinstance(feature.owner, OwnerInfo)
assert feature.owner.team == "test-team"
assert feature.owner.email is None

def test_feature_from_dict_with_owner_object(self):
"""Test parsing a Feature from dict with owner as object."""
config_dict = {
"enabled": True,
"owner": {"team": "test-team", "email": "owner@sentry.io"},
"segments": [{"name": "test-segment", "rollout": 100, "conditions": []}],
}

feature = Feature.from_feature_dictionary("test-feature", config_dict)

assert isinstance(feature.owner, OwnerInfo)
assert feature.owner.team == "test-team"
assert feature.owner.email == "owner@sentry.io"

def test_feature_from_dict_with_owner_object_no_email(self):
"""Test parsing a Feature from dict with owner object without email."""
config_dict = {
"enabled": True,
"owner": {"team": "test-team"},
"segments": [{"name": "test-segment", "rollout": 100, "conditions": []}],
}

feature = Feature.from_feature_dictionary("test-feature", config_dict)

assert isinstance(feature.owner, OwnerInfo)
assert feature.owner.team == "test-team"
assert feature.owner.email is None


class TestEvaluateFlag:
"""Test evaluate_flag() function for different flag scenarios."""

Expand Down
Loading