Skip to content

Update pytest* to latest, pydantic to v2 #4

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,4 @@ dmypy.json
# Cython debug symbols
cython_debug/

*.idea
28 changes: 28 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
default_language_version:
python: python3.12
repos:

- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
args: ['--line-length=120']

- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies: [flake8-print]
args: ['--enable=T', '--max-line-length=120']
exclude: init.py

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: debug-statements

- repo: https://github.com/PyCQA/bandit
rev: '1.7.5'
hooks:
- id: bandit
exclude: ^uep_backend/tests/
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ from clerk import Client


async def main():
async with Client("my-token") as client:
users = await client.users.list()
for user in users:
print(f"Got user {user.id} -> {user.first_name} {user.last_name}")
client = Client("my-token"):
users = await client.users.list()
for user in users:
print(f"Got user {user.id} -> {user.first_name} {user.last_name}")


asyncio.run(main())
Expand Down
85 changes: 42 additions & 43 deletions clerk/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Any, Mapping
import http
from contextlib import asynccontextmanager
from typing import Any, Mapping, Optional

import aiohttp
from pydantic import BaseModel
import httpx

from clerk.errors import ClerkAPIException

Expand All @@ -15,18 +15,12 @@ class Client:
def __init__(
self, token: str, base_url: str = "https://api.clerk.dev/v1/", timeout_seconds: float = 30.0
) -> None:
self._session = aiohttp.ClientSession(
headers={"Authorization": f"Bearer {token}"},
timeout=aiohttp.ClientTimeout(total=timeout_seconds),
self._session = httpx.AsyncClient(
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
timeout=httpx.Timeout(timeout_seconds),
)
self._base_url = base_url

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
await self._session.close()

@property
def verification(self):
from clerk.verification import VerificationService
Expand All @@ -51,40 +45,45 @@ def users(self):

return UsersService(self)

@asynccontextmanager
async def get(
self, endpoint: str, params: Optional[Mapping[str, str]] = None
) -> aiohttp.ClientResponse:
async with self._session.get(self._make_url(endpoint), params=params) as r:
await self._check_response_err(r)
yield r
@property
def organizations(self):
from clerk.organizations import OrganizationsService

return OrganizationsService(self)

async def get(self, endpoint: str, params: Mapping[str, str] | None = None) -> httpx.Response:
r = await self._session.get(self._make_url(endpoint), params=params)
await self._check_response_err(r)
return r

@asynccontextmanager
async def post(
self, endpoint: str, data: Any = None, json: Any = None
) -> aiohttp.ClientResponse:
async with self._session.post(self._make_url(endpoint), data=data, json=json) as r:
await self._check_response_err(r)
yield r

@asynccontextmanager
async def delete(self, endpoint: str) -> aiohttp.ClientResponse:
async with self._session.delete(self._make_url(endpoint)) as r:
await self._check_response_err(r)
yield r

@asynccontextmanager
self, endpoint: str, request: BaseModel | None = None, json: Any = None
) -> httpx.Response:
r = await self._session.post(
self._make_url(endpoint),
data=request.model_dump_json() if request else None,
json=json,
)
await self._check_response_err(r)
return r

async def delete(self, endpoint: str) -> httpx.Response:
r = await self._session.delete(self._make_url(endpoint))
await self._check_response_err(r)
return r

async def patch(
self, endpoint: str, data: Any = None, json: Any = None
) -> aiohttp.ClientResponse:
async with self._session.patch(self._make_url(endpoint), data=data, json=json) as r:
await self._check_response_err(r)
yield r

async def _check_response_err(self, r: aiohttp.ClientResponse):
if http.HTTPStatus.OK <= r.status < http.HTTPStatus.BAD_REQUEST:
return # no error
raise await ClerkAPIException.from_response(r)
self, endpoint: str, request: BaseModel | None = None, json: Any = None
) -> httpx.Response:
r = await self._session.patch(
self._make_url(endpoint), data=request and request.model_dump_json(), json=json
)
await self._check_response_err(r)
return r

async def _check_response_err(self, r: httpx.Response):
if not http.HTTPStatus.OK <= r.status_code < http.HTTPStatus.BAD_REQUEST:
raise await ClerkAPIException.from_response(r)

def _make_url(self, endpoint: str) -> str:
return f"{self._base_url.rstrip('/')}/{endpoint.strip('/')}/"
Expand Down
12 changes: 6 additions & 6 deletions clerk/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ class ClientsService(Service):

async def list(self) -> List[types.Client]:
"""Retrieve a list of all clients"""
async with self._client.get(self.endpoint) as r:
return [types.Client.parse_obj(s) for s in await r.json()]
r = await self._client.get(self.endpoint)
return [types.Client.model_validate(s) for s in r.json()["data"]]

async def get(self, client_id: str) -> types.Client:
"""Retrieve a client by its id"""
async with self._client.get(f"{self.endpoint}/{client_id}") as r:
return types.Client.parse_obj(await r.json())
r = await self._client.get(f"{self.endpoint}/{client_id}")
return types.Client.model_validate_json(r.content)

async def verify(self, token: str) -> types.Client:
"""Verify a token and return its associated client, if valid"""
request = types.VerifyRequest(token=token)

async with self._client.post(self.verify_endpoint, data=request.json()) as r:
return types.Client.parse_obj(await r.json())
r = await self._client.post(self.verify_endpoint, request=request)
return types.Client.model_validate_json(r.content)
18 changes: 9 additions & 9 deletions clerk/errors.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import aiohttp
import httpx

from clerk import types

__all__ = ["ClerkAPIException", "NoActiveSessionException"]


class ClerkAPIException(Exception):
def __init__(self, status: int, method: str, url: str, *api_errors: types.Error) -> None:
self.status = status
self.method = method
def __init__(self, status: int, data: str, url: str, *api_errors: types.Error) -> None:
self.status_code = status
self.data = data
self.url = url
self.api_errors = api_errors
super().__init__(f"{self.method} {self.url}: {self.status} {self.api_errors!r}")
super().__init__(f"{self.url}: {self.status_code} {self.api_errors!r}")

@classmethod
async def from_response(cls, resp: aiohttp.ClientResponse) -> "ClerkAPIException":
async def from_response(cls, resp: httpx.Response) -> "ClerkAPIException":
try:
data = await resp.json()
data = resp.json()
except: # noqa
api_errors = []
else:
errors = data.get("errors", [])
api_errors = [types.Error.parse_obj(e) for e in errors]
api_errors = [types.Error.model_validate(e) for e in errors]

return ClerkAPIException(resp.status, resp.method, str(resp.url), *api_errors)
return ClerkAPIException(resp.status_code, resp.content, str(resp.url), *api_errors)


class NoActiveSessionException(Exception):
Expand Down
33 changes: 33 additions & 0 deletions clerk/organizations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import List

from clerk import types
from clerk.client import Service


class OrganizationsService(Service):
endpoint = "organizations"

async def list(self) -> List[types.Organization]:
"""Retrieve a list of all organizations"""
r = await self._client.get(self.endpoint)
return [types.Organization.model_validate(s) for s in r.json()["data"]]

async def get(self, organization_id: str) -> types.Organization:
"""Retrieve an organization by their id"""
r = await self._client.get(f"{self.endpoint}/{organization_id}")
return types.Organization.model_validate_json(r.content)

async def delete(self, organization_id: str) -> types.DeleteOrganizationResponse:
"""Delete an organization by their id"""
r = await self._client.delete(f"{self.endpoint}/{organization_id}")
return types.DeleteOrganizationResponse.model_validate_json(r.content)

async def update(
self, organization_id: str, request: types.UpdateOrganizationRequest
) -> types.Organization:
"""Update an organization by their id"""
r = await self._client.patch(
f"{self.endpoint}/{organization_id}",
json=request.model_dump_json(exclude_unset=True),
)
return types.Organization.model_validate_json(r.content)
21 changes: 6 additions & 15 deletions clerk/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,15 @@ class SessionsService(Service):

async def list(self) -> List[types.Session]:
"""Retrieve a list of all sessions"""
async with self._client.get(self.endpoint) as r:
return [types.Session.parse_obj(s) for s in await r.json()]
r = await self._client.get(self.endpoint)
return [types.Session.model_validate(s) for s in r.json()["data"]]

async def get(self, session_id: str) -> types.Session:
"""Retrieve a session by its id"""
async with self._client.get(f"{self.endpoint}/{session_id}") as r:
return types.Session.parse_obj(await r.json())
r = await self._client.get(f"{self.endpoint}/{session_id}")
return types.Session.model_validate_json(r.content)

async def revoke(self, session_id: str) -> types.Session:
"""Revoke a session by its id"""
async with self._client.post(f"{self.endpoint}/{session_id}/revoke") as r:
return types.Session.parse_obj(await r.json())

async def verify(self, session_id: str, token: str) -> types.Session:
"""Verify a session by its id and a given token"""
request = types.VerifyRequest(token=token)

async with self._client.post(
f"{self.endpoint}/{session_id}/verify", data=request.json()
) as r:
return types.Session.parse_obj(await r.json())
r = await self._client.post(f"{self.endpoint}/{session_id}/revoke")
return types.Session.model_validate_json(r.content)
48 changes: 46 additions & 2 deletions clerk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Session(BaseModel):
user_id: str
status: str
last_active_at: int
last_active_organization_id: str
expire_at: int
abandon_at: int

Expand Down Expand Up @@ -66,10 +67,12 @@ class User(BaseModel):
email_addresses: List[EmailAddress]
phone_numbers: List[PhoneNumber]
external_accounts: List[Any]
metadata: Any
public_metadata: Any
private_metadata: Any
created_at: int
updated_at: int
last_sign_in_at: int
last_active_at: int


class Error(BaseModel):
Expand All @@ -83,16 +86,57 @@ class VerifyRequest(BaseModel):
token: str


class DeleteUserResponse(BaseModel):
class DeleteResponse(BaseModel):
object: str
id: str
deleted: bool


class DeleteUserResponse(DeleteResponse):
pass


class UpdateUserRequest(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
primary_email_address_id: Optional[str] = None
primary_phone_number_id: Optional[str] = None
profile_image: Optional[str] = None
password: Optional[str] = None


class Organization(BaseModel):
object: str
id: str
name: str
slug: str
max_allowed_memberships: int
admin_delete_enabled: bool | None = None
public_metadata: dict
private_metadata: dict
created_by: str | None = None
image_url: str | None = None


class DeleteOrganizationResponse(DeleteResponse):
pass


class UpdateOrganizationRequest(BaseModel):
name: str | None = None
slug: str | None = None
max_allowed_memberships: int | None = None
admin_delete_enabled: bool | None = None


class OrganizationMembership(BaseModel):
id: str
object: str
role: str
permissions: List[str]
public_metadata: dict
private_metadata: dict
organization: Organization
public_user_data: Any
created_at: int
updated_at: int
Loading