Skip to content

Commit

Permalink
Migrate to new API endpoints (#7)
Browse files Browse the repository at this point in the history
* Add conflict error code

* Update changelog

* Update create key

* Update verify key

* Update update key

* Update user agent usage

* Update revoke key

* Update get api

* Start updating list keys, more complicated though

* Refactor verify key and update list keys

* Update tests

* Update changelog

* Add get key

* Update changelog

* Bump project version

* Formatting
  • Loading branch information
Jonxslays committed Dec 15, 2023
1 parent 8d67d3c commit ec8355c
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 31 deletions.
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))

0 comments on commit ec8355c

Please sign in to comment.