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
31 changes: 30 additions & 1 deletion examples/general/api_tokens.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import os
from datetime import datetime
from datetime import timedelta
from datetime import timezone

import mailtrap as mt
from mailtrap.models.api_tokens import ApiToken
Expand All @@ -12,6 +15,12 @@
api_tokens_api = client.general_api.api_tokens


def one_year_from_now() -> str:
return (datetime.now(timezone.utc) + timedelta(days=365)).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)


def list_api_tokens(account_id: int) -> list[ApiToken]:
return api_tokens_api.get_list(account_id=account_id)

Expand All @@ -22,10 +31,14 @@ def get_api_token(account_id: int, api_token_id: int) -> ApiToken:

def create_api_token(account_id: int) -> ApiTokenWithToken:
# The full token value is only returned once on the response — store it securely.
# Omit expires_at for the server default expiration, pass an ISO 8601
# date-time for an explicit expiry, or pass expires_at=None for a token
# that never expires.
return api_tokens_api.create(
account_id=account_id,
token_params=mt.CreateApiTokenParams(
name="My API Token",
expires_at=one_year_from_now(),
resources=[
mt.ApiTokenResource(
resource_type="account",
Expand All @@ -39,9 +52,22 @@ def create_api_token(account_id: int) -> ApiTokenWithToken:

def reset_api_token(account_id: int, api_token_id: int) -> ApiTokenWithToken:
# The reset response includes the new full token value once — store it securely.
# Omit token_params for the server default expiration of the new token.
return api_tokens_api.reset(account_id=account_id, api_token_id=api_token_id)


def reset_api_token_with_expiration(
account_id: int, api_token_id: int
) -> ApiTokenWithToken:
# Pass an ISO 8601 date-time for an explicit expiry of the new token,
# or expires_at=None for a token that never expires.
return api_tokens_api.reset(
account_id=account_id,
api_token_id=api_token_id,
token_params=mt.ResetApiTokenParams(expires_at=one_year_from_now()),
)


def delete_api_token(account_id: int, api_token_id: int) -> DeletedObject:
return api_tokens_api.delete(account_id=account_id, api_token_id=api_token_id)

Expand All @@ -59,5 +85,8 @@ def delete_api_token(account_id: int, api_token_id: int) -> DeletedObject:
reset = reset_api_token(ACCOUNT_ID, created.id)
print(reset)

deleted = delete_api_token(ACCOUNT_ID, reset.id)
reset_with_expiration = reset_api_token_with_expiration(ACCOUNT_ID, reset.id)
print(reset_with_expiration)

deleted = delete_api_token(ACCOUNT_ID, reset_with_expiration.id)
print(deleted)
2 changes: 2 additions & 0 deletions mailtrap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from .models.accounts import AccountAccessFilterParams
from .models.api_tokens import ApiTokenResource
from .models.api_tokens import CreateApiTokenParams
from .models.api_tokens import ResetApiTokenParams
from .models.common import UNSET
from .models.company_info import CreateCompanyInfoParams
from .models.company_info import UpdateCompanyInfoParams
from .models.contacts import ContactEventParams
Expand Down
26 changes: 23 additions & 3 deletions mailtrap/api/resources/api_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from mailtrap.models.api_tokens import ApiToken
from mailtrap.models.api_tokens import ApiTokenWithToken
from mailtrap.models.api_tokens import CreateApiTokenParams
from mailtrap.models.api_tokens import ResetApiTokenParams
from mailtrap.models.common import DeletedObject


Expand Down Expand Up @@ -31,6 +32,11 @@ def create(
"""
Create a new API token. The full token value is only returned once
in the response — store it securely.

expires_at is an optional token expiration as an ISO 8601 date-time.
Omit it for the server default (a 1-year default is being rolled out).
Pass an explicit None for a token that never expires. Past or
more-than-5-years-ahead values are rejected with a 422 error.
"""
response = self._client.post(
self._api_path(account_id), json=token_params.api_data
Expand All @@ -44,13 +50,27 @@ def delete(self, account_id: int, api_token_id: int) -> DeletedObject:
self._client.delete(self._api_path(account_id, api_token_id))
return DeletedObject(id=api_token_id)

def reset(self, account_id: int, api_token_id: int) -> ApiTokenWithToken:
def reset(
self,
account_id: int,
api_token_id: int,
token_params: Optional[ResetApiTokenParams] = None,
) -> ApiTokenWithToken:
"""
Expire the requested token and create a new token with the same
permissions. The full new token value is returned once — store it
securely. Only tokens that have not already been reset can be reset.
securely. Tokens that have already expired cannot be reset.

expires_at is an optional expiration of the new token as an ISO 8601
date-time. Omit token_params or expires_at for the server default
(a 1-year default is being rolled out). Pass an explicit None for a
token that never expires. Past or more-than-5-years-ahead values are
rejected with a 422 error.
Comment thread
Copilot marked this conversation as resolved.
"""
response = self._client.post(f"{self._api_path(account_id, api_token_id)}/reset")
response = self._client.post(
f"{self._api_path(account_id, api_token_id)}/reset",
json=token_params.api_data if token_params is not None else None,
)
return ApiTokenWithToken(**response)

@staticmethod
Expand Down
27 changes: 27 additions & 0 deletions mailtrap/models/api_tokens.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from typing import Any
from typing import Optional
from typing import Union

from pydantic import Field
from pydantic.dataclasses import dataclass

from mailtrap.models.common import UNSET
from mailtrap.models.common import RequestParams
from mailtrap.models.common import UnsetType


@dataclass
Expand Down Expand Up @@ -33,3 +36,27 @@ class ApiTokenWithToken(ApiToken):
class CreateApiTokenParams(RequestParams):
name: str
resources: list[ApiTokenResource] = Field(default_factory=list)
expires_at: Union[str, None, UnsetType] = UNSET

@property
def api_data(self) -> dict[str, Any]:
data = super().api_data
# exclude_none strips an explicit None, but here it must be sent
# as "expires_at": null (a token that never expires).
if self.expires_at is None:
data["expires_at"] = None
return data


@dataclass
class ResetApiTokenParams(RequestParams):
expires_at: Union[str, None, UnsetType] = UNSET

@property
def api_data(self) -> dict[str, Any]:
data = super().api_data
# exclude_none strips an explicit None, but here it must be sent
# as "expires_at": null (a token that never expires).
if self.expires_at is None:
data["expires_at"] = None
return data
54 changes: 53 additions & 1 deletion mailtrap/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,72 @@
from typing import Union
from typing import cast

from pydantic import GetCoreSchemaHandler
from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass
from pydantic_core import core_schema

T = TypeVar("T", bound="RequestParams")


class UnsetType:
"""
Sentinel type for request fields that should be omitted from the payload.

api_data drops fields whose value is UNSET at any depth, keeping an
omitted field distinct from an explicit None value.
"""

_instance: Optional["UnsetType"] = None

def __new__(cls) -> "UnsetType":
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __repr__(self) -> str:
return "UNSET"

@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.is_instance_schema(
cls,
serialization=core_schema.plain_serializer_function_ser_schema(
cls._serialize
),
)

@staticmethod
def _serialize(value: "UnsetType") -> "UnsetType":
return value


UNSET = UnsetType()


def _drop_unset(value: Any) -> Any:
if isinstance(value, dict):
return {
key: _drop_unset(item)
for key, item in value.items()
if not isinstance(item, UnsetType)
}
if isinstance(value, list):
return [_drop_unset(item) for item in value if not isinstance(item, UnsetType)]
return value


@dataclass
class RequestParams:
@property
def api_data(self: T) -> dict[str, Any]:
return cast(
data = cast(
dict[str, Any],
TypeAdapter(type(self)).dump_python(self, by_alias=True, exclude_none=True),
)
return cast(dict[str, Any], _drop_unset(data))

@property
def api_query_params(self: T) -> dict[str, Any]:
Expand Down
Loading
Loading