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
7 changes: 7 additions & 0 deletions src/workos/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from workos.connect import AsyncConnect
from workos.directory_sync import AsyncDirectorySync
from workos.events import AsyncEvents
from workos.feature_flags import AsyncFeatureFlags
from workos.fga import FGAModule
from workos.mfa import MFAModule
from workos.organizations import AsyncOrganizations
Expand Down Expand Up @@ -95,6 +96,12 @@ def events(self) -> AsyncEvents:
self._events = AsyncEvents(self._http_client)
return self._events

@property
def feature_flags(self) -> AsyncFeatureFlags:
if not getattr(self, "_feature_flags", None):
self._feature_flags = AsyncFeatureFlags(self._http_client)
return self._feature_flags

@property
def fga(self) -> FGAModule:
raise NotImplementedError("FGA APIs are not yet supported in the async client.")
Expand Down
7 changes: 7 additions & 0 deletions src/workos/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from workos.authorization import Authorization
from workos.connect import Connect
from workos.directory_sync import DirectorySync
from workos.feature_flags import FeatureFlags
from workos.fga import FGA
from workos.organizations import Organizations
from workos.organization_domains import OrganizationDomains
Expand Down Expand Up @@ -93,6 +94,12 @@ def events(self) -> Events:
self._events = Events(self._http_client)
return self._events

@property
def feature_flags(self) -> FeatureFlags:
if not getattr(self, "_feature_flags", None):
self._feature_flags = FeatureFlags(self._http_client)
return self._feature_flags

@property
def fga(self) -> FGA:
if not getattr(self, "_fga", None):
Expand Down
251 changes: 251 additions & 0 deletions src/workos/feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
from typing import Optional, Protocol

from workos.types.feature_flags import FeatureFlag
from workos.types.feature_flags.list_filters import FeatureFlagListFilters
from workos.types.list_resource import ListMetadata, ListPage, WorkOSListResource
from workos.typing.sync_or_async import SyncOrAsync
from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient
from workos.utils.pagination_order import PaginationOrder
from workos.utils.request_helper import (
DEFAULT_LIST_RESPONSE_LIMIT,
REQUEST_METHOD_DELETE,
REQUEST_METHOD_GET,
REQUEST_METHOD_POST,
REQUEST_METHOD_PUT,
)

FEATURE_FLAGS_PATH = "feature-flags"

FeatureFlagsListResource = WorkOSListResource[
FeatureFlag, FeatureFlagListFilters, ListMetadata
]


class FeatureFlagsModule(Protocol):
"""Offers methods through the WorkOS Feature Flags service."""

def list_feature_flags(
self,
*,
limit: int = DEFAULT_LIST_RESPONSE_LIMIT,
before: Optional[str] = None,
after: Optional[str] = None,
order: PaginationOrder = "desc",
) -> SyncOrAsync[FeatureFlagsListResource]:
"""Retrieve a list of feature flags.

Kwargs:
limit (int): Maximum number of records to return. (Optional)
before (str): Pagination cursor to receive records before a provided ID. (Optional)
after (str): Pagination cursor to receive records after a provided ID. (Optional)
order (Literal["asc","desc"]): Sort records in either ascending or descending order. (Optional)

Returns:
FeatureFlagsListResource: Feature flags list response from WorkOS.
"""
...

def get_feature_flag(self, slug: str) -> SyncOrAsync[FeatureFlag]:
"""Gets details for a single feature flag.

Args:
slug (str): The unique slug identifier of the feature flag.

Returns:
FeatureFlag: Feature flag response from WorkOS.
"""
...

def enable_feature_flag(self, slug: str) -> SyncOrAsync[FeatureFlag]:
"""Enable a feature flag.

Args:
slug (str): The unique slug identifier of the feature flag.

Returns:
FeatureFlag: Updated feature flag response from WorkOS.
"""
...

def disable_feature_flag(self, slug: str) -> SyncOrAsync[FeatureFlag]:
"""Disable a feature flag.

Args:
slug (str): The unique slug identifier of the feature flag.

Returns:
FeatureFlag: Updated feature flag response from WorkOS.
"""
...

def add_feature_flag_target(self, slug: str, resource_id: str) -> SyncOrAsync[None]:
"""Add a target to a feature flag.

Args:
slug (str): The unique slug identifier of the feature flag.
resource_id (str): Resource ID in format user_<id> or org_<id>.

Returns:
None
"""
...

def remove_feature_flag_target(
self, slug: str, resource_id: str
) -> SyncOrAsync[None]:
"""Remove a target from a feature flag.

Args:
slug (str): The unique slug identifier of the feature flag.
resource_id (str): Resource ID in format user_<id> or org_<id>.

Returns:
None
"""
...


class FeatureFlags(FeatureFlagsModule):
_http_client: SyncHTTPClient

def __init__(self, http_client: SyncHTTPClient):
self._http_client = http_client

def list_feature_flags(
self,
*,
limit: int = DEFAULT_LIST_RESPONSE_LIMIT,
before: Optional[str] = None,
after: Optional[str] = None,
order: PaginationOrder = "desc",
) -> FeatureFlagsListResource:
list_params: FeatureFlagListFilters = {
"limit": limit,
"before": before,
"after": after,
"order": order,
}

response = self._http_client.request(
FEATURE_FLAGS_PATH,
method=REQUEST_METHOD_GET,
params=list_params,
)

return WorkOSListResource[FeatureFlag, FeatureFlagListFilters, ListMetadata](
list_method=self.list_feature_flags,
list_args=list_params,
**ListPage[FeatureFlag](**response).model_dump(),
)

def get_feature_flag(self, slug: str) -> FeatureFlag:
response = self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}",
method=REQUEST_METHOD_GET,
)

return FeatureFlag.model_validate(response)

def enable_feature_flag(self, slug: str) -> FeatureFlag:
response = self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/enable",
method=REQUEST_METHOD_PUT,
json={},
)

return FeatureFlag.model_validate(response)

def disable_feature_flag(self, slug: str) -> FeatureFlag:
response = self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/disable",
method=REQUEST_METHOD_PUT,
json={},
)

return FeatureFlag.model_validate(response)

def add_feature_flag_target(self, slug: str, resource_id: str) -> None:
self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/targets/{resource_id}",
method=REQUEST_METHOD_POST,
json={},
)
Comment on lines +167 to +172
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 resource_id interpolated directly into URL path

resource_id is supplied by the caller and is embedded verbatim into the URL path. The docstring documents the expected format as user_<id> or org_<id>, but there is no validation or URL-encoding applied. A value containing /, ?, or # would silently alter the request URL.

The same pattern applies to slug throughout the module, and this is consistent with how other modules in the SDK build paths. Since WorkOS-generated IDs won't contain these characters in practice, the risk is low — but a lightweight guard (e.g., urllib.parse.quote) on resource_id (and similarly for slug) would make the surface more robust against malformed input:

from urllib.parse import quote

f"{FEATURE_FLAGS_PATH}/{quote(slug)}/targets/{quote(resource_id)}"

This also applies to the same pattern in the AsyncFeatureFlags counterpart (line 241–244).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is low-risk and consistent with how some other modules handle it but if you'd like me to do this, I am happy to!


def remove_feature_flag_target(self, slug: str, resource_id: str) -> None:
self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/targets/{resource_id}",
method=REQUEST_METHOD_DELETE,
)


class AsyncFeatureFlags(FeatureFlagsModule):
_http_client: AsyncHTTPClient

def __init__(self, http_client: AsyncHTTPClient):
self._http_client = http_client

async def list_feature_flags(
self,
*,
limit: int = DEFAULT_LIST_RESPONSE_LIMIT,
before: Optional[str] = None,
after: Optional[str] = None,
order: PaginationOrder = "desc",
) -> FeatureFlagsListResource:
list_params: FeatureFlagListFilters = {
"limit": limit,
"before": before,
"after": after,
"order": order,
}

response = await self._http_client.request(
FEATURE_FLAGS_PATH,
method=REQUEST_METHOD_GET,
params=list_params,
)

return WorkOSListResource[FeatureFlag, FeatureFlagListFilters, ListMetadata](
list_method=self.list_feature_flags,
list_args=list_params,
**ListPage[FeatureFlag](**response).model_dump(),
)

async def get_feature_flag(self, slug: str) -> FeatureFlag:
response = await self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}",
method=REQUEST_METHOD_GET,
)

return FeatureFlag.model_validate(response)

async def enable_feature_flag(self, slug: str) -> FeatureFlag:
response = await self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/enable",
method=REQUEST_METHOD_PUT,
json={},
)

return FeatureFlag.model_validate(response)

async def disable_feature_flag(self, slug: str) -> FeatureFlag:
response = await self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/disable",
method=REQUEST_METHOD_PUT,
json={},
)

return FeatureFlag.model_validate(response)

async def add_feature_flag_target(self, slug: str, resource_id: str) -> None:
await self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/targets/{resource_id}",
method=REQUEST_METHOD_POST,
json={},
)

async def remove_feature_flag_target(self, slug: str, resource_id: str) -> None:
await self._http_client.request(
f"{FEATURE_FLAGS_PATH}/{slug}/targets/{resource_id}",
method=REQUEST_METHOD_DELETE,
)
5 changes: 1 addition & 4 deletions src/workos/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from workos.types.api_keys import ApiKey, ApiKeyWithValue
from workos.types.api_keys.list_filters import ApiKeyListFilters
from workos.feature_flags import FeatureFlagsListResource
from workos.types.feature_flags import FeatureFlag
from workos.types.feature_flags.list_filters import FeatureFlagListFilters
from workos.types.metadata import Metadata
Expand All @@ -29,10 +30,6 @@
Organization, OrganizationListFilters, ListMetadata
]

FeatureFlagsListResource = WorkOSListResource[
FeatureFlag, FeatureFlagListFilters, ListMetadata
]

ApiKeysListResource = WorkOSListResource[ApiKey, ApiKeyListFilters, ListMetadata]


Expand Down
4 changes: 2 additions & 2 deletions src/workos/types/feature_flags/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from workos.types.feature_flags.feature_flag import FeatureFlag
from workos.types.feature_flags.feature_flag import FeatureFlag, FeatureFlagOwner

__all__ = ["FeatureFlag"]
__all__ = ["FeatureFlag", "FeatureFlagOwner"]
12 changes: 11 additions & 1 deletion src/workos/types/feature_flags/feature_flag.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
from typing import Literal, Optional
from typing import Literal, Optional, Sequence
from workos.types.workos_model import WorkOSModel


class FeatureFlagOwner(WorkOSModel):
email: str
first_name: Optional[str]
last_name: Optional[str]


class FeatureFlag(WorkOSModel):
id: str
object: Literal["feature_flag"]
slug: str
name: str
description: Optional[str]
tags: Sequence[str]
owner: Optional[FeatureFlagOwner]
enabled: bool
default_value: bool
created_at: str
updated_at: str
5 changes: 1 addition & 4 deletions src/workos/user_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from workos._client_configuration import ClientConfiguration
from workos.session import AsyncSession, Session
from workos.feature_flags import FeatureFlagsListResource
from workos.types.feature_flags import FeatureFlag
from workos.types.feature_flags.list_filters import FeatureFlagListFilters
from workos.types.list_resource import (
Expand Down Expand Up @@ -119,10 +120,6 @@
Invitation, InvitationsListFilters, ListMetadata
]

FeatureFlagsListResource = WorkOSListResource[
FeatureFlag, FeatureFlagListFilters, ListMetadata
]

SessionsListResource = WorkOSListResource[
UserManagementSession, SessionsListFilters, ListMetadata
]
Expand Down
Loading
Loading