Skip to content

Conversation

@sighphyre
Copy link
Member

Adds in events for when features are fetched and when the SDK becomes ready.

This brings us more inline with the other SDKs which have these events at minimum.

Closes #356

@property
def features(self) -> dict:
if not hasattr(self, "_parsed_payload"):
self._parsed_payload = loads(self.raw_features)["features"]
Copy link
Member Author

Choose a reason for hiding this comment

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

Annoyingly this now becomes part of the public API.

Kinda struggled a little with choosing between only returning the features vs returning the whole response. Leaning ever so slightly in the direction of only returning the features since its less confusing for end users.

That being said, every time we've done that in the past (fetch features, bootstrap, load from backup), it's been a mistake that needs to be patched later. Open to being told this is similar but I can't see how right now

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 still a dict, I think it'd be different if this is an array because it's not easy to extend an array with other properties, but being a dict I think this gives enough flexibility. And I agree, returning less is better IMO

"""

if not event_callback:
return None
Copy link
Member Author

Choose a reason for hiding this comment

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

Poor mans optional.map()

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it makes sense to support this optional parameter. IMO, if you're going to call this function, you better have a callback. If you supply something that's not a callback then you should expect an error. But having this ability to call all functions with optional and checking if it's set seems a bit off.

On the other hand, if this function can be called from multiple places it centralizes the check in one single place in the code, so just picking your brain about it.

raw_features: str

@property
def features(self) -> dict:
Copy link
Member Author

Choose a reason for hiding this comment

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

Parsing is JSON deserialize, which is expensive. This gives us lazy, cached parse here which the cost is only incurred on demand

@coveralls
Copy link

coveralls commented Jul 2, 2025

Coverage Status

coverage: 95.021% (-0.5%) from 95.475%
when pulling af050b8 on feat/events
into 35ae106 on main.

@sighphyre sighphyre marked this pull request as ready for review July 2, 2025 11:01
Copy link
Contributor

@gastonfournier gastonfournier left a comment

Choose a reason for hiding this comment

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

This looks nice, just left you a couple of stylish comments

"""

if not event_callback:
return None
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it makes sense to support this optional parameter. IMO, if you're going to call this function, you better have a callback. If you supply something that's not a callback then you should expect an error. But having this ability to call all functions with optional and checking if it's set seems a bit off.

On the other hand, if this function can be called from multiple places it centralizes the check in one single place in the code, so just picking your brain about it.

scheduler_executor: Optional[str] = None,
multiple_instance_mode: InstanceAllowType = InstanceAllowType.WARN,
event_callback: Optional[Callable[[UnleashEvent], None]] = None,
event_callback: Optional[Callable[[BaseEvent], None]] = None,
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
event_callback: Optional[Callable[[BaseEvent], None]] = None,
event_callback: Callable[[BaseEvent], None] = lambda event: None,

Following my other comment, the alternative would be something like this.

@property
def features(self) -> dict:
if not hasattr(self, "_parsed_payload"):
self._parsed_payload = loads(self.raw_features)["features"]
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 still a dict, I think it'd be different if this is an array because it's not easy to extend an array with other properties, but being a dict I think this gives enough flexibility. And I agree, returning less is better IMO

Comment on lines 42 to 50
if event_callback:
event = UnleashFetchedEvent(
event_type=UnleashEventType.FETCHED,
event_id=uuid.uuid4(),
raw_features=state,
)
event_callback(event)
if ready_callback:
ready_callback()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I kind of like the idea of removing the if statement and having functions doing nothing.

@github-project-automation github-project-automation bot moved this from New to Approved PRs in Issues and PRs Jul 3, 2025
@sighphyre sighphyre merged commit b8e0b54 into main Jul 4, 2025
58 of 62 checks passed
@sighphyre sighphyre deleted the feat/events branch July 4, 2025 07:13
@github-project-automation github-project-automation bot moved this from Approved PRs to Done in Issues and PRs Jul 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Feature request: implement a subscriber API to align with other SDK offerings

3 participants