From ae75376d3aeb653d97b78398116c4c4a0bf80546 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:08:24 -0700 Subject: [PATCH 01/12] Add undefined type --- unkey/undefined.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 unkey/undefined.py diff --git a/unkey/undefined.py b/unkey/undefined.py new file mode 100644 index 0000000..234d323 --- /dev/null +++ b/unkey/undefined.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import typing as t + + +__all__ = ("UndefinedNoneOr", "UndefinedOr", "UNDEFINED") + + +class Undefined: + """Represents an undefined value - without being None.""" + + __slots__ = () + + def __bool__(self) -> t.Literal[False]: + return False + + def __str__(self) -> str: + return "UNDEFINED" + + +UNDEFINED = Undefined() +"""A value that does not exist.""" + +T = t.TypeVar("T", covariant=True) + +UndefinedOr = t.Union[T, Undefined] +"""A value that is undefined or T""" + +UndefinedNoneOr = t.Union[UndefinedOr[T], None] +"""A value that is undefined, none, or T""" + + +def all_undefined(*values: t.Any) -> bool: + """Whether or not all values are undefined. + + Returns: + bool: `True` if all values were undefined. + """ + return all(v is UNDEFINED for v in values) + + +def any_undefined(*values: t.Any) -> bool: + """Whether or not any values are undefined. + + Returns: + bool: `True` if any values were undefined. + """ + return any(v is UNDEFINED for v in values) From d744b34c0647cb98476353a099d297377ddce904 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:08:48 -0700 Subject: [PATCH 02/12] Add missing required argument error --- unkey/errors.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/unkey/errors.py b/unkey/errors.py index f76d9d4..08055ce 100644 --- a/unkey/errors.py +++ b/unkey/errors.py @@ -1,6 +1,6 @@ from __future__ import annotations -__all__ = ("UnwrapError", "BaseError") +__all__ = ("BaseError", "MissingRequiredArgument", "UnwrapError") class BaseError(Exception): @@ -10,12 +10,18 @@ class BaseError(Exception): class UnwrapError(BaseError): - """Raised when calling unwrap or unwrap_err incorrectly. - - message: The error message. - """ + """Raised when calling unwrap or unwrap_err incorrectly.""" __slots__ = () def __init__(self, message: str) -> None: super().__init__(f"Unwrap failed: {message}") + + +class MissingRequiredArgument(BaseError): + """Raised when a required argument is missing.""" + + __slots__ = () + + def __init__(self, message: str) -> None: + super().__init__(f"Missing required argument: {message}") From b9c3b9581b61adb32c896c067c313e4dba42e445 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:10:33 -0700 Subject: [PATCH 03/12] Add update key route --- unkey/routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unkey/routes.py b/unkey/routes.py index 854bb31..3c9c4fd 100644 --- a/unkey/routes.py +++ b/unkey/routes.py @@ -83,6 +83,7 @@ def compile(self, *args: t.Union[str, int]) -> CompiledRoute: 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/{}") # Apis GET_API: t.Final[Route] = Route(c.GET, "/apis/{}") From a5c2d150907ae834678c4a187f0ada8bf03b426f Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:13:53 -0700 Subject: [PATCH 04/12] Use undefined in the base service --- unkey/services/base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/unkey/services/base.py b/unkey/services/base.py index 7b434f6..59e6702 100644 --- a/unkey/services/base.py +++ b/unkey/services/base.py @@ -5,6 +5,8 @@ from datetime import datetime from datetime import timedelta +from unkey import undefined + if t.TYPE_CHECKING: from unkey import serializer @@ -30,13 +32,13 @@ def __init__(self, http_service: HttpService, serializer: serializer.Serializer) self._serializer = serializer def _generate_map(self, **kwargs: t.Any) -> t.Dict[str, t.Any]: - return {k: v for k, v in kwargs.items() if v is not None} + return {k: v for k, v in kwargs.items() if v is not undefined.UNDEFINED} def _expires_in( self, *, milliseconds: int = 0, seconds: int = 0, minutes: int = 0, days: int = 0 - ) -> int | None: + ) -> undefined.UndefinedOr[int]: if not any({milliseconds, seconds, minutes, days}): - return None + return undefined.UNDEFINED delta = timedelta(days=days, minutes=minutes, seconds=seconds, milliseconds=milliseconds) return int((datetime.now() + delta).timestamp()) * 1000 From 0e5b015bd6625b777a3f259560366c22868229f2 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:14:22 -0700 Subject: [PATCH 05/12] Use undefined in the api service --- unkey/services/apis.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/unkey/services/apis.py b/unkey/services/apis.py index d87797d..b490960 100644 --- a/unkey/services/apis.py +++ b/unkey/services/apis.py @@ -5,6 +5,7 @@ from unkey import models from unkey import result from unkey import routes +from unkey import undefined from . import BaseService @@ -46,7 +47,12 @@ async def get_api(self, api_id: str) -> ResultT[models.Api]: return result.Ok(self._serializer.to_api(data)) async def list_keys( - self, api_id: str, *, owner_id: t.Optional[str] = None, limit: int = 100, offset: int = 0 + self, + api_id: str, + *, + owner_id: undefined.UndefinedOr[str] = undefined.UNDEFINED, + limit: int = 100, + offset: int = 0, ) -> ResultT[models.ApiKeyList]: """Gets a paginated list of keys for the given api. From e2b3bfb10f96fcbc2e13d84a5e5ba20d62584956 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:16:13 -0700 Subject: [PATCH 06/12] Add update key method and use undefined type --- unkey/services/keys.py | 75 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/unkey/services/keys.py b/unkey/services/keys.py index bf4c9c8..3f05ad5 100644 --- a/unkey/services/keys.py +++ b/unkey/services/keys.py @@ -2,9 +2,11 @@ import typing as t +from unkey import errors from unkey import models from unkey import result from unkey import routes +from unkey import undefined from . import BaseService @@ -25,15 +27,18 @@ async def create_key( owner_id: str, prefix: str, *, - byte_length: t.Optional[int] = None, - meta: t.Optional[t.Dict[str, t.Any]] = None, - expires: t.Optional[int] = None, - remaining: t.Optional[int] = None, - ratelimit: t.Optional[models.Ratelimit] = None, + name: undefined.UndefinedOr[str] = undefined.UNDEFINED, + byte_length: undefined.UndefinedOr[int] = undefined.UNDEFINED, + meta: undefined.UndefinedOr[t.Dict[str, t.Any]] = undefined.UNDEFINED, + expires: undefined.UndefinedOr[int] = undefined.UNDEFINED, + remaining: undefined.UndefinedOr[int] = undefined.UNDEFINED, + ratelimit: undefined.UndefinedOr[models.Ratelimit] = undefined.UNDEFINED, ) -> ResultT[models.ApiKey]: """Creates a new api key. Args: + name: The name to use for this key. + api_id: The id of the api this key is for. owner_id: The owner id to use for this key. Represents the @@ -63,6 +68,7 @@ async def create_key( route = routes.CREATE_KEY.compile() payload = self._generate_map( meta=meta, + name=name, apiId=api_id, prefix=prefix, ownerId=owner_id, @@ -129,3 +135,62 @@ async def revoke_key(self, key_id: str) -> ResultT[models.HttpResponse]: ) return result.Ok(models.HttpResponse(200, "OK")) + + async def update_key( + self, + key_id: str, + *, + name: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED, + owner_id: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED, + meta: undefined.UndefinedNoneOr[t.Dict[str, t.Any]] = undefined.UNDEFINED, + expires: undefined.UndefinedNoneOr[int] = undefined.UNDEFINED, + remaining: undefined.UndefinedNoneOr[int] = undefined.UNDEFINED, + ratelimit: undefined.UndefinedNoneOr[models.Ratelimit] = undefined.UNDEFINED, + ) -> ResultT[models.HttpResponse]: + """Updates an existing api key. To delete a key set its value + to `None`. + + Args: + key_id: The id of the key to update. + + Keyword Args: + name: The new name to use for this key. + + owner_id: The new owner id to use for this key. + + meta: The new dynamic mapping of information used + to provide context around this keys user. + + expires: The new number of milliseconds into the future + when this key should expire. + + remaining: The new max number of times this key can be + used. + + ratelimit: The new Ratelimit to set on this key. + + Returns: + A result containing the OK response or an error. + """ + if undefined.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) + payload = self._generate_map( + name=name, + meta=meta, + keyId=key_id, + ownerId=owner_id, + remaining=remaining, + ratelimit=ratelimit, + expires=self._expires_in(milliseconds=expires or 0) + if expires is not None + else expires, + ) + + data = await self._http.fetch(route, payload=payload) + + if isinstance(data, models.HttpResponse): + return result.Err(data) + + return result.Ok(models.HttpResponse(200, "OK")) From 67eea094abb7fccce1f4efbe404651b63390de38 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:16:33 -0700 Subject: [PATCH 07/12] export to top level and bump init project version --- unkey/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/unkey/__init__.py b/unkey/__init__.py index 06787c2..c0033e4 100644 --- a/unkey/__init__.py +++ b/unkey/__init__.py @@ -3,7 +3,7 @@ from typing import Final __packagename__: Final[str] = "unkey.py" -__version__: Final[str] = "0.3.0" +__version__: Final[str] = "0.4.0" __author__: Final[str] = "Jonxslays" __copyright__: Final[str] = "2023-present Jonxslays" __description__: Final[str] = "An asynchronous Python SDK for unkey.dev." @@ -21,6 +21,7 @@ from . import routes from . import serializer from . import services +from . import undefined from .client import * from .errors import * from .models import * @@ -28,6 +29,7 @@ from .routes import * from .serializer import * from .services import * +from .undefined import * __all__ = ( "client", @@ -38,6 +40,7 @@ "routes", "serializer", "services", + "undefined", "Api", "ApiKey", "ApiKeyList", @@ -55,11 +58,15 @@ "HttpResponse", "HttpService", "KeyService", + "MissingRequiredArgument", "Ok", "Ratelimit", "RatelimitType", "Result", "Route", "Serializer", + "UndefinedNoneOr", + "UndefinedOr", "UnwrapError", + "UNDEFINED", ) From 70973a01c4c16f32f5ac7848a99eef96fed2ef49 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:17:05 -0700 Subject: [PATCH 08/12] Bump project version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 80ba4fe..75eae25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "unkey.py" -version = "0.3.0" +version = "0.4.0" description = "An asynchronous Python SDK for unkey.dev." authors = ["Jonxslays"] license = "GPL-3.0-only" From 6c6016a9996f98976c492c6df0bf522e607cb796 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 13:52:52 -0700 Subject: [PATCH 09/12] Finish serializer tests --- tests/test_serializer.py | 100 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index e9bbe5c..1c6a4ec 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -228,3 +228,103 @@ def test_to_ratelimit( result = serializer.to_ratelimit(raw_ratelimit) assert result == full_ratelimit + + +################# +# to_api_key_meta +################# + + +def _raw_api_key_meta() -> DictT: + return { + "id": "fxc_DDD", + "meta": {"test": 1}, + "start": "fxc", + "apiId": "api_FFF", + "expires": 123, + "remaining": 12, + "ownerId": "jonxslays", + "createdAt": 456, + "workspaceId": "ws_GGG", + "ratelimit": { + "type": "fast", + "limit": 1, + "refillRate": 2, + "refillInterval": 3, + }, + } + + +@pytest.fixture() +def raw_api_key_meta() -> DictT: + return _raw_api_key_meta() + + +def _full_api_key_meta() -> models.ApiKeyMeta: + model = models.ApiKeyMeta() + model.id = "fxc_DDD" + model.meta = {"test": 1} + model.start = "fxc" + model.api_id = "api_FFF" + model.expires = 123 + model.remaining = 12 + model.owner_id = "jonxslays" + model.created_at = 456 + model.workspace_id = "ws_GGG" + model.ratelimit = models.Ratelimit( + models.RatelimitType.Fast, + limit=1, + refill_rate=2, + refill_interval=3, + ) + + return model + + +@pytest.fixture() +def full_api_key_meta() -> models.ApiKeyMeta: + return _full_api_key_meta() + + +def test_to_api_key_meta( + raw_api_key_meta: DictT, + full_api_key_meta: models.ApiKeyMeta, +) -> None: + result = serializer.to_api_key_meta(raw_api_key_meta) + + assert result == full_api_key_meta + + +################# +# to_api_key_list +################# + + +def _raw_api_key_list() -> DictT: + return {"total": 1, "keys": [_raw_api_key_meta()]} + + +@pytest.fixture() +def raw_api_key_list() -> DictT: + return _raw_api_key_list() + + +def _full_api_key_list() -> models.ApiKeyList: + model = models.ApiKeyList() + model.total = 1 + model.keys = [_full_api_key_meta()] + return model + + +@pytest.fixture() +def full_api_key_list() -> models.ApiKeyList: + return _full_api_key_list() + + +def test_to_api_key_list( + raw_api_key_list: DictT, + full_api_key_list: models.ApiKeyList, +) -> None: + result = serializer.to_api_key_list(raw_api_key_list) + + assert result == full_api_key_list From 3266fdf3782d4a151af40f4e09ef7278a52052d6 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 14:43:59 -0700 Subject: [PATCH 10/12] Add base service tests --- tests/services/__init__.py | 0 tests/services/test_base_service.py | 31 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/services/__init__.py create mode 100644 tests/services/test_base_service.py diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/services/test_base_service.py b/tests/services/test_base_service.py new file mode 100644 index 0000000..82dd603 --- /dev/null +++ b/tests/services/test_base_service.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from unittest import mock + +import pytest + +from unkey import BaseService +from unkey import UNDEFINED + + +@pytest.fixture() +def service() -> BaseService: + return BaseService(mock.Mock(), mock.Mock()) + + +def test_generate_map(service: BaseService) -> None: + result = service._generate_map(one=1, two=2) # type: ignore + + assert result == {"one": 1, "two": 2} + + +def test_generate_map_with_undefined(service: BaseService) -> None: + result = service._generate_map(one=1, two=UNDEFINED) # type: ignore + + assert result == {"one": 1} + + +def test_generate_map_with_none(service: BaseService) -> None: + result = service._generate_map(one=1, two=None) # type: ignore + + assert result == {"one": 1, "two": None} From fe4690cbf837e45ea797fba1a817dfe6826eee38 Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 14:44:17 -0700 Subject: [PATCH 11/12] Update changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f370fab..5c9f8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v0.4.0 (Jul 2023) + +### Additions + +- Add `UNDEFINED`, `UndefinedOr`, and `UndefinedNoneOr` types. +- Add `update_key` method to key service. +- Add `name` parameter to the `create_key` method. + +### Changes + +- Refactor existing methods to use the new `UNDEFINED` type. + +--- + ## v0.3.0 (Jul 2023) ### Bugfixes From 7a8180c8c136a798818a9a25f079af53eb353d2a Mon Sep 17 00:00:00 2001 From: Jonxslays <51417989+Jonxslays@users.noreply.github.com> Date: Sun, 23 Jul 2023 14:48:05 -0700 Subject: [PATCH 12/12] Format imports --- tests/services/test_base_service.py | 2 +- unkey/undefined.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/services/test_base_service.py b/tests/services/test_base_service.py index 82dd603..7cf2814 100644 --- a/tests/services/test_base_service.py +++ b/tests/services/test_base_service.py @@ -4,8 +4,8 @@ import pytest -from unkey import BaseService from unkey import UNDEFINED +from unkey import BaseService @pytest.fixture() diff --git a/unkey/undefined.py b/unkey/undefined.py index 234d323..f5b814c 100644 --- a/unkey/undefined.py +++ b/unkey/undefined.py @@ -2,7 +2,6 @@ import typing as t - __all__ = ("UndefinedNoneOr", "UndefinedOr", "UNDEFINED")