Skip to content

Commit 5ecb078

Browse files
feat!: Engine V7 compatibility (#150)
deps: Bump flagsmith-engine from 6.1.0 to 7.0.0 --- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a55a921 commit 5ecb078

15 files changed

+603
-412
lines changed

.github/workflows/pull-request.yml

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,3 @@ jobs:
2525
refactor
2626
test
2727
chore
28-
- name: Auto-label PR with Conventional Commit title
29-
uses: kramen22/conventional-release-labels@v1
30-
with:
31-
type_labels: |
32-
{
33-
"feat": "feature",
34-
"fix": "fix",
35-
"ci": "ci-cd",
36-
"docs": "docs",
37-
"deps": "dependencies",
38-
"perf": "performance",
39-
"refactor": "refactor",
40-
"test": "testing",
41-
"chore": "chore"
42-
}
43-
ignored_types: '[]'

.github/workflows/pytest.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
max-parallel: 5
1515
matrix:
16-
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
16+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
1717

1818
steps:
1919
- name: Cloning repo
@@ -33,7 +33,6 @@ jobs:
3333
poetry install --with dev
3434
3535
- name: Check for new typing errors
36-
if: ${{ matrix.python-version != '3.8' }}
3736
run: poetry run mypy --strict .
3837

3938
- name: Run Tests

flagsmith/api/__init__.py

Whitespace-only changes.

flagsmith/flagsmith.py

Lines changed: 66 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
import logging
22
import sys
33
import typing
4-
from datetime import timezone
4+
from datetime import datetime
5+
from urllib.parse import urljoin
56

6-
import pydantic
77
import requests
88
from flag_engine import engine
9-
from flag_engine.context.mappers import map_environment_identity_to_context
10-
from flag_engine.environments.models import EnvironmentModel
11-
from flag_engine.identities.models import IdentityModel
12-
from flag_engine.identities.traits.models import TraitModel
13-
from flag_engine.identities.traits.types import TraitValue
149
from requests.adapters import HTTPAdapter
1510
from requests.utils import default_user_agent
1611
from urllib3 import Retry
1712

1813
from flagsmith.analytics import AnalyticsProcessor
1914
from flagsmith.exceptions import FlagsmithAPIError, FlagsmithClientError
15+
from flagsmith.mappers import (
16+
map_context_and_identity_data_to_context,
17+
map_environment_document_to_context,
18+
map_environment_document_to_environment_updated_at,
19+
)
2020
from flagsmith.models import DefaultFlag, Flags, Segment
21-
from flagsmith.offline_handlers import BaseOfflineHandler
21+
from flagsmith.offline_handlers import OfflineHandler
2222
from flagsmith.polling_manager import EnvironmentDataPollingManager
23-
from flagsmith.streaming_manager import EventStreamManager, StreamEvent
23+
from flagsmith.streaming_manager import EventStreamManager
2424
from flagsmith.types import (
2525
ApplicationMetadata,
2626
JsonType,
27-
TraitConfig,
27+
StreamEvent,
2828
TraitMapping,
2929
)
3030
from flagsmith.utils.identities import generate_identity_data
@@ -72,7 +72,7 @@ def __init__(
7272
] = None,
7373
proxies: typing.Optional[typing.Dict[str, str]] = None,
7474
offline_mode: bool = False,
75-
offline_handler: typing.Optional[BaseOfflineHandler] = None,
75+
offline_handler: typing.Optional[OfflineHandler] = None,
7676
enable_realtime_updates: bool = False,
7777
application_metadata: typing.Optional[ApplicationMetadata] = None,
7878
):
@@ -112,8 +112,8 @@ def __init__(
112112
self.default_flag_handler = default_flag_handler
113113
self.enable_realtime_updates = enable_realtime_updates
114114
self._analytics_processor: typing.Optional[AnalyticsProcessor] = None
115-
self._environment: typing.Optional[EnvironmentModel] = None
116-
self._identity_overrides_by_identifier: typing.Dict[str, IdentityModel] = {}
115+
self._evaluation_context: typing.Optional[engine.EvaluationContext] = None
116+
self._environment_updated_at: typing.Optional[datetime] = None
117117

118118
# argument validation
119119
if offline_mode and not offline_handler:
@@ -129,7 +129,7 @@ def __init__(
129129
)
130130

131131
if self.offline_handler:
132-
self._environment = self.offline_handler.get_environment()
132+
self._evaluation_context = self.offline_handler.get_evaluation_context()
133133

134134
if not self.offline_mode:
135135
if not environment_key:
@@ -159,9 +159,9 @@ def __init__(
159159
self.request_timeout_seconds = request_timeout_seconds
160160
self.session.mount(self.api_url, HTTPAdapter(max_retries=retries))
161161

162-
self.environment_flags_url = f"{self.api_url}flags/"
163-
self.identities_url = f"{self.api_url}identities/"
164-
self.environment_url = f"{self.api_url}environment-document/"
162+
self.environment_flags_url = urljoin(self.api_url, "flags/")
163+
self.identities_url = urljoin(self.api_url, "identities/")
164+
self.environment_url = urljoin(self.api_url, "environment-document/")
165165

166166
if self.enable_local_evaluation:
167167
if not environment_key.startswith("ser."):
@@ -182,10 +182,13 @@ def _initialise_local_evaluation(self) -> None:
182182
# method calls, update the environment manually.
183183
self.update_environment()
184184
if self.enable_realtime_updates:
185-
if not self._environment:
185+
if not self._evaluation_context:
186186
raise ValueError("Unable to get environment from API key")
187187

188-
stream_url = f"{self.realtime_api_url}sse/environments/{self._environment.api_key}/stream"
188+
stream_url = urljoin(
189+
self.realtime_api_url,
190+
f"sse/environments/{self._evaluation_context['environment']['key']}/stream",
191+
)
189192

190193
self.event_stream_thread = EventStreamManager(
191194
stream_url=stream_url,
@@ -207,15 +210,11 @@ def _initialise_local_evaluation(self) -> None:
207210
self.environment_data_polling_manager_thread.start()
208211

209212
def handle_stream_event(self, event: StreamEvent) -> None:
210-
if not self._environment:
213+
if not (environment_updated_at := self._environment_updated_at):
211214
raise ValueError(
212-
"Unable to access environment. Environment should not be null"
215+
"Cannot handle stream events before retrieving initial environment"
213216
)
214-
environment_updated_at = self._environment.updated_at
215-
if environment_updated_at.tzinfo is None:
216-
environment_updated_at = environment_updated_at.astimezone(timezone.utc)
217-
218-
if event.updated_at > environment_updated_at:
217+
if event["updated_at"] > environment_updated_at:
219218
self.update_environment()
220219

221220
def get_environment_flags(self) -> Flags:
@@ -224,7 +223,9 @@ def get_environment_flags(self) -> Flags:
224223
225224
:return: Flags object holding all the flags for the current environment.
226225
"""
227-
if (self.offline_mode or self.enable_local_evaluation) and self._environment:
226+
if (
227+
self.offline_mode or self.enable_local_evaluation
228+
) and self._evaluation_context:
228229
return self._get_environment_flags_from_document()
229230
return self._get_environment_flags_from_api()
230231

@@ -250,7 +251,9 @@ def get_identity_flags(
250251
:return: Flags object holding all the flags for the given identity.
251252
"""
252253
traits = traits or {}
253-
if (self.offline_mode or self.enable_local_evaluation) and self._environment:
254+
if (
255+
self.offline_mode or self.enable_local_evaluation
256+
) and self._evaluation_context:
254257
return self._get_identity_flags_from_document(identifier, traits)
255258
return self._get_identity_flags_from_api(
256259
identifier,
@@ -261,7 +264,7 @@ def get_identity_flags(
261264
def get_identity_segments(
262265
self,
263266
identifier: str,
264-
traits: typing.Optional[typing.Mapping[str, TraitValue]] = None,
267+
traits: typing.Optional[typing.Mapping[str, engine.ContextValue]] = None,
265268
) -> typing.List[Segment]:
266269
"""
267270
Get a list of segments that the given identity is in.
@@ -272,37 +275,44 @@ def get_identity_segments(
272275
Flagsmith, e.g. {"num_orders": 10}
273276
:return: list of Segment objects that the identity is part of.
274277
"""
275-
276-
if not self._environment:
278+
if not self._evaluation_context:
277279
raise FlagsmithClientError(
278280
"Local evaluation required to obtain identity segments."
279281
)
280282

281-
traits = traits or {}
282-
identity_model = self._get_identity_model(identifier, **traits)
283-
context = map_environment_identity_to_context(
284-
environment=self._environment,
285-
identity=identity_model,
286-
override_traits=None,
283+
context = map_context_and_identity_data_to_context(
284+
context=self._evaluation_context,
285+
identifier=identifier,
286+
traits=traits,
287287
)
288+
288289
evaluation_result = engine.get_evaluation_result(
289290
context=context,
290291
)
291292
return [
292-
Segment(id=int(sm["key"]), name=sm["name"])
293-
for sm in evaluation_result.get("segments", [])
293+
Segment(id=int(segment_result["key"]), name=segment_result["name"])
294+
for segment_result in evaluation_result["segments"]
294295
]
295296

296297
def update_environment(self) -> None:
297298
try:
298-
self._environment = self._get_environment_from_api()
299-
except (FlagsmithAPIError, pydantic.ValidationError):
300-
logger.exception("Error updating environment")
299+
environment_data = self._get_json_response(
300+
self.environment_url, method="GET"
301+
)
302+
except FlagsmithAPIError:
303+
logger.exception("Error retrieving environment document from API")
301304
else:
302-
if overrides := self._environment.identity_overrides:
303-
self._identity_overrides_by_identifier = {
304-
identity.identifier: identity for identity in overrides
305-
}
305+
try:
306+
self._evaluation_context = map_environment_document_to_context(
307+
environment_data,
308+
)
309+
self._environment_updated_at = (
310+
map_environment_document_to_environment_updated_at(
311+
environment_data,
312+
)
313+
)
314+
except (KeyError, TypeError, ValueError):
315+
logger.exception("Error parsing environment document")
306316

307317
def _get_headers(
308318
self,
@@ -322,22 +332,11 @@ def _get_headers(
322332
headers.update(custom_headers or {})
323333
return headers
324334

325-
def _get_environment_from_api(self) -> EnvironmentModel:
326-
environment_data = self._get_json_response(self.environment_url, method="GET")
327-
return EnvironmentModel.model_validate(environment_data)
328-
329335
def _get_environment_flags_from_document(self) -> Flags:
330-
if self._environment is None:
336+
if self._evaluation_context is None:
331337
raise TypeError("No environment present")
332-
identity = self._get_identity_model(identifier="", traits=None)
333-
334-
context = map_environment_identity_to_context(
335-
environment=self._environment,
336-
identity=identity,
337-
override_traits=None,
338-
)
339338

340-
evaluation_result = engine.get_evaluation_result(context=context)
339+
evaluation_result = engine.get_evaluation_result(self._evaluation_context)
341340

342341
return Flags.from_evaluation_result(
343342
evaluation_result=evaluation_result,
@@ -346,18 +345,18 @@ def _get_environment_flags_from_document(self) -> Flags:
346345
)
347346

348347
def _get_identity_flags_from_document(
349-
self, identifier: str, traits: TraitMapping
348+
self,
349+
identifier: str,
350+
traits: TraitMapping,
350351
) -> Flags:
351-
identity_model = self._get_identity_model(identifier, **traits)
352-
if self._environment is None:
352+
if self._evaluation_context is None:
353353
raise TypeError("No environment present")
354354

355-
context = map_environment_identity_to_context(
356-
environment=self._environment,
357-
identity=identity_model,
358-
override_traits=None,
355+
context = map_context_and_identity_data_to_context(
356+
context=self._evaluation_context,
357+
identifier=identifier,
358+
traits=traits,
359359
)
360-
361360
evaluation_result = engine.get_evaluation_result(
362361
context=context,
363362
)
@@ -435,34 +434,6 @@ def _get_json_response(
435434
"Unable to get valid response from Flagsmith API."
436435
) from e
437436

438-
def _get_identity_model(
439-
self,
440-
identifier: str,
441-
**traits: typing.Union[TraitValue, TraitConfig],
442-
) -> IdentityModel:
443-
if not self._environment:
444-
raise FlagsmithClientError(
445-
"Unable to build identity model when no local environment present."
446-
)
447-
448-
trait_models = [
449-
TraitModel(
450-
trait_key=key,
451-
trait_value=value["value"] if isinstance(value, dict) else value,
452-
)
453-
for key, value in traits.items()
454-
]
455-
456-
if identity := self._identity_overrides_by_identifier.get(identifier):
457-
identity.update_traits(trait_models)
458-
return identity
459-
460-
return IdentityModel(
461-
identifier=identifier,
462-
environment_api_key=self._environment.api_key,
463-
identity_traits=trait_models,
464-
)
465-
466437
def __del__(self) -> None:
467438
if hasattr(self, "environment_data_polling_manager_thread"):
468439
self.environment_data_polling_manager_thread.stop()

0 commit comments

Comments
 (0)