Skip to content
Merged
46 changes: 46 additions & 0 deletions tests/test_api_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pytest

from tests.utils.fixtures.mock_api_key import MockApiKey
from tests.utils.syncify import syncify
from workos.api_key import ApiKey, AsyncApiKey
from workos.exceptions import AuthenticationException


@pytest.mark.sync_and_async(ApiKey, AsyncApiKey)
class TestApiKey:
@pytest.fixture
def mock_api_key_details(self):
api_key_details = MockApiKey()
return api_key_details.model_dump()

def test_validate_api_key_with_valid_key(
self,
module_instance,
mock_api_key_details,
capture_and_mock_http_client_request,
):
request_kwargs = capture_and_mock_http_client_request(
module_instance._http_client, mock_api_key_details, 200
)

api_key_details = syncify(module_instance.validate_api_key())

assert request_kwargs["url"].endswith("/api_keys/validate")
assert request_kwargs["method"] == "post"
assert api_key_details.id == mock_api_key_details["id"]
assert api_key_details.name == mock_api_key_details["name"]
assert api_key_details.object == "api_key"

def test_validate_api_key_with_invalid_key(
self,
module_instance,
mock_http_client_with_response,
):
mock_http_client_with_response(
module_instance._http_client,
{"message": "Invalid API key", "error": "invalid_api_key"},
401,
)

with pytest.raises(AuthenticationException):
syncify(module_instance.validate_api_key())
6 changes: 6 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self):
os.environ.pop("WORKOS_API_KEY")
os.environ.pop("WORKOS_CLIENT_ID")

def test_initialize_api_keys(self, default_client):
assert bool(default_client.api_keys)

def test_initialize_sso(self, default_client):
assert bool(default_client.sso)

Expand Down Expand Up @@ -112,6 +115,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self):
os.environ.pop("WORKOS_API_KEY")
os.environ.pop("WORKOS_CLIENT_ID")

def test_initialize_api_keys(self, default_client):
assert bool(default_client.api_keys)

def test_initialize_directory_sync(self, default_client):
assert bool(default_client.directory_sync)

Expand Down
16 changes: 16 additions & 0 deletions tests/utils/fixtures/mock_api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import datetime

from workos.types.api_keys import ApiKey


class MockApiKey(ApiKey):
def __init__(self, id="api_key_01234567890"):
now = datetime.datetime.now().isoformat()
super().__init__(
object="api_key",
id=id,
name="Development API Key",
last_used_at=now,
created_at=now,
updated_at=now,
)
5 changes: 5 additions & 0 deletions workos/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from workos.fga import FGAModule
from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT
from workos.utils.http_client import HTTPClient
from workos.api_key import ApiKeyModule
from workos.audit_logs import AuditLogsModule
from workos.directory_sync import DirectorySyncModule
from workos.events import EventsModule
Expand Down Expand Up @@ -65,6 +66,10 @@ def __init__(
else int(os.getenv("WORKOS_REQUEST_TIMEOUT", DEFAULT_REQUEST_TIMEOUT))
)

@property
@abstractmethod
def api_keys(self) -> ApiKeyModule: ...

@property
@abstractmethod
def audit_logs(self) -> AuditLogsModule: ...
Expand Down
54 changes: 54 additions & 0 deletions workos/api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Protocol

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


class ApiKeyModule(Protocol):
def validate_api_key(self) -> SyncOrAsync[ApiKey]:
"""Validates the configured API key.

Returns:
ApiKey: The validated API key details containing
information about the key's name and usage

Raises:
AuthenticationException: If the API key is invalid or
unauthorized (401)
NotFoundException: If the API key is not found (404)
ServerException: If the API server encounters an error
(5xx)
"""
...


class ApiKey(ApiKeyModule):
_http_client: SyncHTTPClient

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

def validate_api_key(self) -> ApiKey:
response = self._http_client.request(
"api_keys/validate",
method=REQUEST_METHOD_POST,
)

return ApiKey.model_validate(response)


class AsyncApiKey(ApiKeyModule):
_http_client: AsyncHTTPClient

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

async def validate_api_key(self) -> ApiKey:
response = await self._http_client.request(
"api_keys/validate",
method=REQUEST_METHOD_POST,
)

return ApiKey.model_validate(response)
7 changes: 7 additions & 0 deletions workos/async_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional
from workos.__about__ import __version__
from workos._base_client import BaseClient
from workos.api_key import AsyncApiKey
from workos.audit_logs import AuditLogsModule
from workos.directory_sync import AsyncDirectorySync
from workos.events import AsyncEvents
Expand Down Expand Up @@ -45,6 +46,12 @@ def __init__(
timeout=self.request_timeout,
)

@property
def api_keys(self) -> AsyncApiKey:
if not getattr(self, "_api_keys", None):
self._api_keys = AsyncApiKey(self._http_client)
return self._api_keys

@property
def sso(self) -> AsyncSSO:
if not getattr(self, "_sso", None):
Expand Down
7 changes: 7 additions & 0 deletions workos/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional
from workos.__about__ import __version__
from workos._base_client import BaseClient
from workos.api_key import ApiKey
from workos.audit_logs import AuditLogs
from workos.directory_sync import DirectorySync
from workos.fga import FGA
Expand Down Expand Up @@ -45,6 +46,12 @@ def __init__(
timeout=self.request_timeout,
)

@property
def api_keys(self) -> ApiKey:
if not getattr(self, "_api_keys", None):
self._api_keys = ApiKey(self._http_client)
return self._api_keys

@property
def sso(self) -> SSO:
if not getattr(self, "_sso", None):
Expand Down
1 change: 1 addition & 0 deletions workos/types/api_key/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .api_key import ApiKey as ApiKey # noqa: F401
12 changes: 12 additions & 0 deletions workos/types/api_key/api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Literal

from workos.types.workos_model import WorkOSModel


class ApiKey(WorkOSModel):
object: Literal["api_key"]
id: str
name: str
last_used_at: str | None = None
created_at: str
updated_at: str