Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to new API endpoints #7

Merged
merged 16 commits into from
Dec 15, 2023
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## v0.5.0 (Dec 2023)

### Breaking Changes

- `verify_key` now requires an `api_id` parameter.
- `list_keys` no longer accepts the `offset` parameter.

### Additions

- Add `Conflict` variant to `ErrorCode`.
- Add `get_key` method to `KeyService`.
- Add `cursor` parameter to `list_keys`.

## Bugfixes

- Fix invalid default used when ratelimit was not passed in `create_key`.

### Changes

- Refactor internal routes to use new API endpoints.

---

## v0.4.3 (Sep 2023)

### Additions
Expand All @@ -10,6 +33,8 @@

- Rename `UsageExceeded` error code to `KeyUsageExceeded`.

---

## v0.4.2 (Aug 2023)

### Additions
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "unkey.py"
version = "0.4.3"
version = "0.5.0"
description = "An asynchronous Python SDK for unkey.dev."
authors = ["Jonxslays"]
license = "GPL-3.0-only"
Expand Down
4 changes: 4 additions & 0 deletions tests/services/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ def test_init() -> None:
assert http._api_version == "/v1" # type: ignore
assert http._base_url == constants.API_BASE_URL # type: ignore
assert http._headers == { # type: ignore
"Unkey-SDK": constants.USER_AGENT,
"User-Agent": constants.USER_AGENT,
"x-user-agent": constants.USER_AGENT,
"Authorization": "Bearer abc123",
}
Expand All @@ -32,6 +34,8 @@ def test_full_init() -> None:
assert http._api_version == "/v4" # type: ignore
assert http._base_url == "1234" # type: ignore
assert http._headers == { # type: ignore
"Unkey-SDK": constants.USER_AGENT,
"User-Agent": constants.USER_AGENT,
"x-user-agent": constants.USER_AGENT,
"Authorization": "Bearer abc123",
}
3 changes: 2 additions & 1 deletion tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ def test_to_api_key_meta(


def _raw_api_key_list() -> DictT:
return {"total": 1, "keys": [_raw_api_key_meta()]}
return {"cursor": None, "total": 1, "keys": [_raw_api_key_meta()]}


@pytest.fixture()
Expand All @@ -409,6 +409,7 @@ def raw_api_key_list() -> DictT:

def _full_api_key_list() -> models.ApiKeyList:
model = models.ApiKeyList()
model.cursor = None
model.total = 1
model.keys = [_full_api_key_meta()]
return model
Expand Down
2 changes: 1 addition & 1 deletion unkey/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Final

__packagename__: Final[str] = "unkey.py"
__version__: Final[str] = "0.4.3"
__version__: Final[str] = "0.5.0"
__author__: Final[str] = "Jonxslays"
__copyright__: Final[str] = "2023-present Jonxslays"
__description__: Final[str] = "An asynchronous Python SDK for unkey.dev."
Expand Down
2 changes: 1 addition & 1 deletion unkey/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
__all__ = ()

API_BASE_URL: Final[str] = "https://api.unkey.dev"
USER_AGENT: Final[str] = f"unkey.py v{unkey.__version__}"
USER_AGENT: Final[str] = f"unkey.py@v{unkey.__version__}"

GET: Final[str] = "GET"
PUT: Final[str] = "PUT"
Expand Down
3 changes: 3 additions & 0 deletions unkey/models/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class Api(BaseModel):
class ApiKeyList(BaseModel):
"""Data representing keys for an api."""

cursor: t.Optional[str]
"""The cursor indicating the last key that was returned."""

keys: t.List[ApiKeyMeta]
"""A list of keys associated with the api."""

Expand Down
1 change: 1 addition & 0 deletions unkey/models/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ErrorCode(BaseEnum):
InvalidKeyType = "INVALID_KEY_TYPE"
NotUnique = "NOT_UNIQUE"
Unknown = "UNKNOWN"
Conflict = "CONFLICT"


@attrs.define(weakref_slot=False)
Expand Down
13 changes: 7 additions & 6 deletions unkey/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ def compile(self, *args: t.Union[str, int]) -> CompiledRoute:


# Keys
CREATE_KEY: t.Final[Route] = Route(c.POST, "/keys")
VERIFY_KEY: t.Final[Route] = Route(c.POST, "/keys/verify")
REVOKE_KEY: t.Final[Route] = Route(c.DELETE, "/keys/{}")
UPDATE_KEY: t.Final[Route] = Route(c.PUT, "/keys/{}")
CREATE_KEY: t.Final[Route] = Route(c.POST, "/keys.createKey")
VERIFY_KEY: t.Final[Route] = Route(c.POST, "/keys.verifyKey")
REVOKE_KEY: t.Final[Route] = Route(c.POST, "/keys.deleteKey")
UPDATE_KEY: t.Final[Route] = Route(c.POST, "/keys.updateKey")
GET_KEY: t.Final[Route] = Route(c.GET, "/keys.getKey")

# Apis
GET_API: t.Final[Route] = Route(c.GET, "/apis/{}")
GET_KEYS: t.Final[Route] = Route(c.GET, "/apis/{}/keys")
GET_API: t.Final[Route] = Route(c.GET, "/apis.getApi")
GET_KEYS: t.Final[Route] = Route(c.GET, "/apis.listKeys")
1 change: 1 addition & 0 deletions unkey/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def to_api_key_meta(self, data: DictT) -> models.ApiKeyMeta:

def to_api_key_list(self, data: DictT) -> models.ApiKeyList:
model = models.ApiKeyList()
model.cursor = data.get("cursor")
model.total = data["total"]
model.keys = [self.to_api_key_meta(key) for key in data["keys"]]
return model
24 changes: 12 additions & 12 deletions unkey/services/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ async def get_api(self, api_id: str) -> ResultT[models.Api]:
Returns:
A result containing the requested information or an error.
"""
route = routes.GET_API.compile(api_id)
params = self._generate_map(apiId=api_id)
route = routes.GET_API.compile().with_params(params)
data = await self._http.fetch(route)

if isinstance(data, models.HttpResponse):
Expand All @@ -40,8 +41,8 @@ async def get_api(self, api_id: str) -> ResultT[models.Api]:
return result.Err(
models.HttpResponse(
404,
data["error"],
models.ErrorCode.from_str_maybe(data.get("code", "unknown")),
data["error"].get("message", "Unknown error"),
models.ErrorCode.from_str_maybe(data["error"].get("code", "UNKNOWN")),
)
)

Expand All @@ -52,8 +53,8 @@ async def list_keys(
api_id: str,
*,
owner_id: UndefinedOr[str] = UNDEFINED,
limit: int = 100,
offset: int = 0,
limit: UndefinedOr[int] = UNDEFINED,
cursor: UndefinedOr[str] = UNDEFINED,
) -> ResultT[models.ApiKeyList]:
"""Gets a paginated list of keys for the given api.

Expand All @@ -63,16 +64,15 @@ async def list_keys(
Keyword Args:
owner_id: The optional owner id to list the keys for.

limit: The max number of keys to include in this page.
Defaults to 100.
limit: The optional max number of keys to include in this page.

offset: How many keys to offset by, for pagination.
cursor: Optional key used to determine pagination offset.

Returns:
A result containing api key list or an error.
"""
params = self._generate_map(ownerId=owner_id, limit=limit, offset=offset)
route = routes.GET_KEYS.compile(api_id).with_params(params)
params = self._generate_map(apiId=api_id, ownerId=owner_id, limit=limit, cursor=cursor)
route = routes.GET_KEYS.compile().with_params(params)
data = await self._http.fetch(route)

if isinstance(data, models.HttpResponse):
Expand All @@ -82,8 +82,8 @@ async def list_keys(
return result.Err(
models.HttpResponse(
404,
data["error"],
models.ErrorCode.from_str_maybe(data.get("code", "unknown")),
data["error"].get("message", "Unknown error"),
models.ErrorCode.from_str_maybe(data["error"].get("code", "UNKNOWN")),
)
)

Expand Down
2 changes: 2 additions & 0 deletions unkey/services/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def __init__(
raise ValueError("Api key must be provided.")

self._headers = {
"Unkey-SDK": constants.USER_AGENT,
"User-Agent": constants.USER_AGENT,
"x-user-agent": constants.USER_AGENT,
"Authorization": f"Bearer {api_key}",
}
Expand Down
48 changes: 39 additions & 9 deletions unkey/services/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async def create_key(
remaining=remaining,
byteLength=byte_length,
expires=self._expires_in(milliseconds=expires or 0),
ratelimit=None
ratelimit=UNDEFINED
if not ratelimit
else self._generate_map(
limit=ratelimit.limit,
Expand All @@ -95,17 +95,19 @@ async def create_key(

return result.Ok(self._serializer.to_api_key(data))

async def verify_key(self, key: str) -> ResultT[models.ApiKeyVerification]:
async def verify_key(self, key: str, api_id: str) -> ResultT[models.ApiKeyVerification]:
"""Verifies a key is valid and within ratelimit.

Args:
key: The key to verify.

api_id: The id of the api to verify the key against.

Returns:
A result containing the api key verification or an error.
"""
route = routes.VERIFY_KEY.compile()
payload = self._generate_map(key=key)
payload = self._generate_map(key=key, apiId=api_id)
data = await self._http.fetch(route, payload=payload)

if isinstance(data, models.HttpResponse):
Expand All @@ -122,8 +124,9 @@ async def revoke_key(self, key_id: str) -> ResultT[models.HttpResponse]:
Returns:
A result containing the http response or an error.
"""
route = routes.REVOKE_KEY.compile(key_id)
data = await self._http.fetch(route)
route = routes.REVOKE_KEY.compile()
payload = self._generate_map(keyId=key_id)
data = await self._http.fetch(route, payload=payload)

if isinstance(data, models.HttpResponse):
return result.Err(data)
Expand All @@ -132,8 +135,8 @@ async def revoke_key(self, key_id: str) -> ResultT[models.HttpResponse]:
return result.Err(
models.HttpResponse(
404,
data["error"],
models.ErrorCode.from_str_maybe(data.get("code", "unknown")),
data["error"].get("message", "Unknown error"),
models.ErrorCode.from_str_maybe(data["error"].get("code", "UNKNOWN")),
)
)

Expand Down Expand Up @@ -178,7 +181,7 @@ async def update_key(
if all_undefined(name, owner_id, meta, expires, remaining, ratelimit):
raise errors.MissingRequiredArgument("At least one value is required to be updated.")

route = routes.UPDATE_KEY.compile(key_id)
route = routes.UPDATE_KEY.compile()
payload = self._generate_map(
name=name,
meta=meta,
Expand All @@ -187,7 +190,7 @@ async def update_key(
remaining=remaining,
ratelimit=ratelimit,
expires=self._expires_in(milliseconds=expires or 0)
if expires is not None
if expires is not UNDEFINED
else expires,
)

Expand All @@ -197,3 +200,30 @@ async def update_key(
return result.Err(data)

return result.Ok(models.HttpResponse(200, "OK"))

async def get_key(self, key_id: str) -> ResultT[models.ApiKeyMeta]:
"""Retrieves details for the given key.

Args:
key_id: The id of the key.

Returns:
A result containing the api key metadata or an error.
"""
params = self._generate_map(keyId=key_id)
route = routes.GET_KEY.compile().with_params(params)
data = await self._http.fetch(route)

if isinstance(data, models.HttpResponse):
return result.Err(data)

if "error" in data:
return result.Err(
models.HttpResponse(
404,
data["error"].get("message", "Unknown error"),
models.ErrorCode.from_str_maybe(data["error"].get("code", "UNKNOWN")),
)
)

return result.Ok(self._serializer.to_api_key_meta(data))