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
26 changes: 25 additions & 1 deletion src/workos/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from workos.types.api_keys import ApiKey
from workos.typing.sync_or_async import SyncOrAsync
from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient
from workos.utils.request_helper import REQUEST_METHOD_POST
from workos.utils.request_helper import REQUEST_METHOD_DELETE, REQUEST_METHOD_POST

API_KEYS_PATH = "api_keys"
API_KEY_VALIDATION_PATH = "api_keys/validations"
RESOURCE_OBJECT_ATTRIBUTE_NAME = "api_key"

Expand All @@ -22,6 +23,17 @@ def validate_api_key(self, *, value: str) -> SyncOrAsync[Optional[ApiKey]]:
"""
...

def delete_api_key(self, api_key_id: str) -> SyncOrAsync[None]:
"""Delete an API key.

Args:
api_key_id (str): The ID of the API key to delete

Returns:
None
"""
...


class ApiKeys(ApiKeysModule):
_http_client: SyncHTTPClient
Expand All @@ -37,6 +49,12 @@ def validate_api_key(self, *, value: str) -> Optional[ApiKey]:
return None
return ApiKey.model_validate(response[RESOURCE_OBJECT_ATTRIBUTE_NAME])

def delete_api_key(self, api_key_id: str) -> None:
self._http_client.request(
f"{API_KEYS_PATH}/{api_key_id}",
method=REQUEST_METHOD_DELETE,
)


class AsyncApiKeys(ApiKeysModule):
_http_client: AsyncHTTPClient
Expand All @@ -51,3 +69,9 @@ async def validate_api_key(self, *, value: str) -> Optional[ApiKey]:
if response.get(RESOURCE_OBJECT_ATTRIBUTE_NAME) is None:
return None
return ApiKey.model_validate(response[RESOURCE_OBJECT_ATTRIBUTE_NAME])

async def delete_api_key(self, api_key_id: str) -> None:
await self._http_client.request(
f"{API_KEYS_PATH}/{api_key_id}",
method=REQUEST_METHOD_DELETE,
)
150 changes: 150 additions & 0 deletions src/workos/organizations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional, Protocol, Sequence

from workos.types.api_keys import ApiKey, ApiKeyListFilters, ApiKeyWithValue
from workos.types.feature_flags import FeatureFlag
from workos.types.feature_flags.list_filters import FeatureFlagListFilters
from workos.types.metadata import Metadata
Expand Down Expand Up @@ -30,6 +31,8 @@
FeatureFlag, FeatureFlagListFilters, ListMetadata
]

ApiKeysListResource = WorkOSListResource[ApiKey, ApiKeyListFilters, ListMetadata]


class OrganizationsModule(Protocol):
"""Offers methods through the WorkOS Organizations service."""
Expand Down Expand Up @@ -157,6 +160,55 @@ def list_feature_flags(
"""
...

def create_api_key(
self,
organization_id: str,
*,
name: str,
permissions: Optional[Sequence[str]] = None,
) -> SyncOrAsync[ApiKeyWithValue]:
"""Create an API key for an organization.

The response includes the full API key value which is only returned once
at creation time. Make sure to store this value securely.

Args:
organization_id (str): Organization's unique identifier

Kwargs:
name (str): A descriptive name for the API key
permissions (Sequence[str]): List of permissions to assign to the key (Optional)

Returns:
ApiKeyWithValue: API key with the full value field
"""
...

def list_api_keys(
self,
organization_id: str,
*,
limit: int = DEFAULT_LIST_RESPONSE_LIMIT,
before: Optional[str] = None,
after: Optional[str] = None,
order: PaginationOrder = "desc",
) -> SyncOrAsync[ApiKeysListResource]:
"""Retrieve a list of API keys for an organization

Args:
organization_id (str): Organization's unique identifier

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

Returns:
ApiKeysListResource: API keys list response from WorkOS.
"""
...


class Organizations(OrganizationsModule):
_http_client: SyncHTTPClient
Expand Down Expand Up @@ -304,6 +356,55 @@ def list_feature_flags(
**ListPage[FeatureFlag](**response).model_dump(),
)

def create_api_key(
self,
organization_id: str,
*,
name: str,
permissions: Optional[Sequence[str]] = None,
) -> ApiKeyWithValue:
json = {
"name": name,
"permissions": permissions,
}

response = self._http_client.request(
f"organizations/{organization_id}/api_keys",
method=REQUEST_METHOD_POST,
json=json,
)

return ApiKeyWithValue.model_validate(response)

def list_api_keys(
self,
organization_id: str,
*,
limit: int = DEFAULT_LIST_RESPONSE_LIMIT,
before: Optional[str] = None,
after: Optional[str] = None,
order: PaginationOrder = "desc",
) -> ApiKeysListResource:
list_params: ApiKeyListFilters = {
"organization_id": organization_id,
"limit": limit,
"before": before,
"after": after,
"order": order,
}

response = self._http_client.request(
f"organizations/{organization_id}/api_keys",
method=REQUEST_METHOD_GET,
params=list_params,
)

return WorkOSListResource[ApiKey, ApiKeyListFilters, ListMetadata](
list_method=self.list_api_keys,
list_args=list_params,
**ListPage[ApiKey](**response).model_dump(),
)


class AsyncOrganizations(OrganizationsModule):
_http_client: AsyncHTTPClient
Expand Down Expand Up @@ -450,3 +551,52 @@ async def list_feature_flags(
list_args=list_params,
**ListPage[FeatureFlag](**response).model_dump(),
)

async def create_api_key(
self,
organization_id: str,
*,
name: str,
permissions: Optional[Sequence[str]] = None,
) -> ApiKeyWithValue:
json = {
"name": name,
"permissions": permissions,
}

response = await self._http_client.request(
f"organizations/{organization_id}/api_keys",
method=REQUEST_METHOD_POST,
json=json,
)

return ApiKeyWithValue.model_validate(response)

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

response = await self._http_client.request(
f"organizations/{organization_id}/api_keys",
method=REQUEST_METHOD_GET,
params=list_params,
)

return WorkOSListResource[ApiKey, ApiKeyListFilters, ListMetadata](
list_method=self.list_api_keys,
list_args=list_params,
**ListPage[ApiKey](**response).model_dump(),
)
2 changes: 2 additions & 0 deletions src/workos/types/api_keys/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .api_keys import ApiKey as ApiKey # noqa: F401
from .api_keys import ApiKeyWithValue as ApiKeyWithValue # noqa: F401
from .list_filters import ApiKeyListFilters as ApiKeyListFilters # noqa: F401
6 changes: 6 additions & 0 deletions src/workos/types/api_keys/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ class ApiKey(WorkOSModel):
permissions: Sequence[str]
created_at: str
updated_at: str


class ApiKeyWithValue(ApiKey):
"""API key with the full value field, returned only on creation."""

value: str
6 changes: 6 additions & 0 deletions src/workos/types/api_keys/list_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import Optional
from workos.types.list_resource import ListArgs


class ApiKeyListFilters(ListArgs, total=False):
organization_id: Optional[str]
2 changes: 2 additions & 0 deletions src/workos/types/list_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
cast,
)
from typing_extensions import Required, TypedDict
from workos.types.api_keys import ApiKey
from workos.types.directory_sync import (
Directory,
DirectoryGroup,
Expand All @@ -42,6 +43,7 @@
ListableResource = TypeVar(
# add all possible generics of List Resource
"ListableResource",
ApiKey,
AuthenticationFactor,
ConnectionWithDomains,
Directory,
Expand Down
22 changes: 21 additions & 1 deletion tests/test_api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@

from tests.utils.fixtures.mock_api_key import MockApiKey
from tests.utils.syncify import syncify
from workos.api_keys import API_KEY_VALIDATION_PATH, ApiKeys, AsyncApiKeys
from workos.api_keys import (
API_KEYS_PATH,
API_KEY_VALIDATION_PATH,
ApiKeys,
AsyncApiKeys,
)


@pytest.mark.sync_and_async(ApiKeys, AsyncApiKeys)
Expand Down Expand Up @@ -48,3 +53,18 @@ def test_validate_api_key_with_invalid_key(
)

assert syncify(module_instance.validate_api_key(value="invalid-key")) is None

def test_delete_api_key(
self,
module_instance,
capture_and_mock_http_client_request,
):
api_key_id = "api_key_01234567890"
request_kwargs = capture_and_mock_http_client_request(
module_instance._http_client, {}, 204
)

syncify(module_instance.delete_api_key(api_key_id))

assert request_kwargs["url"].endswith(f"{API_KEYS_PATH}/{api_key_id}")
assert request_kwargs["method"] == "delete"
Loading