Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.venv
.tox
.pytest_cache
.vscode
13 changes: 13 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Build docker images
run: docker-compose build
- name: Run tests
run: |
docker-compose run app poetry run pytest tests
48 changes: 0 additions & 48 deletions .github/workflows/test.yml

This file was deleted.

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/

.DS_Store
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.11.4-alpine3.18
ENV PYTHONUNBUFFERED 1
ENV PYTHONFAULTHANDLER 1

WORKDIR /app
RUN apk add musl-dev linux-headers gcc g++ zlib-dev libffi-dev

RUN pip install --no-cache-dir poetry

COPY ./pyproject.toml /app/
COPY ./poetry.lock /app/

ARG VERSION
ENV VERSION $VERSION


RUN poetry install

COPY . .
4 changes: 2 additions & 2 deletions clerk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .client import Client
from .client import Client, Service

__version__ = "0.1.0"
__all__ = ["Client", "Service"]
40 changes: 21 additions & 19 deletions clerk/client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import http
from collections.abc import Mapping
from contextlib import asynccontextmanager
from typing import Any, Mapping, Optional
from typing import Any

import aiohttp

from clerk.errors import ClerkAPIException
from .errors import ClerkAPIException

__all__ = ["Client", "Service"]

Expand All @@ -13,7 +14,10 @@ class Client:
"""An API client for the clerk.dev API"""

def __init__(
self, token: str, base_url: str = "https://api.clerk.dev/v1/", timeout_seconds: float = 30.0
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}"},
Expand All @@ -29,55 +33,53 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):

@property
def verification(self):
from clerk.verification import VerificationService
from .verification import VerificationService

return VerificationService(self)

@property
def session(self):
from clerk.sessions import SessionsService
from .sessions import SessionsService

return SessionsService(self)

@property
def clients(self):
from clerk.clients import ClientsService
from .clients import ClientsService

return ClientsService(self)

@property
def users(self):
from clerk.users import UsersService
from .users import UsersService

return UsersService(self)

@asynccontextmanager
async def get(
self, endpoint: str, params: Optional[Mapping[str, str]] = None
) -> aiohttp.ClientResponse:
async def get(self, endpoint: str, params: Mapping[str, str] | None = None):
async with self._session.get(self._make_url(endpoint), params=params) as r:
await self._check_response_err(r)
yield 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:
async def post(self, endpoint: str, data: Any = None, json: Any = None):
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 def delete(self, endpoint: str):
async with self._session.delete(self._make_url(endpoint)) as r:
await self._check_response_err(r)
yield r

@asynccontextmanager
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:
async def patch(self, endpoint: str, data: Any = None, json: Any = None):
async with self._session.patch(
self._make_url(endpoint), data=data, json=json
) as r:
await self._check_response_err(r)
yield r

Expand Down
14 changes: 6 additions & 8 deletions clerk/clients.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
from typing import List

from clerk import types
from clerk.client import Service
from . import types
from .client import Service


class ClientsService(Service):
endpoint = "clients"
verify_endpoint = endpoint + "/verify"

async def list(self) -> List[types.Client]:
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()]
return [types.Client.model_validate(s) for s in await r.json()]

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())
return types.Client.model_validate(await r.json())

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())
return types.Client.model_validate(await r.json())
8 changes: 5 additions & 3 deletions clerk/errors.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import aiohttp

from clerk import types
from . import types

__all__ = ["ClerkAPIException", "NoActiveSessionException"]


class ClerkAPIException(Exception):
def __init__(self, status: int, method: str, url: str, *api_errors: types.Error) -> None:
def __init__(
self, status: int, method: str, url: str, *api_errors: types.Error
) -> None:
self.status = status
self.method = method
self.url = url
Expand All @@ -21,7 +23,7 @@ async def from_response(cls, resp: aiohttp.ClientResponse) -> "ClerkAPIException
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)

Expand Down
16 changes: 7 additions & 9 deletions clerk/sessions.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
from typing import List

from clerk import types
from clerk.client import Service
from . import types
from .client import Service


class SessionsService(Service):
endpoint = "sessions"

async def list(self) -> List[types.Session]:
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()]
return [types.Session.model_validate(s) for s in await r.json()]

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())
return types.Session.model_validate(await r.json())

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())
return types.Session.model_validate(await r.json())

async def verify(self, session_id: str, token: str) -> types.Session:
"""Verify a session by its id and a given token"""
Expand All @@ -29,4 +27,4 @@ async def verify(self, session_id: str, token: str) -> types.Session:
async with self._client.post(
f"{self.endpoint}/{session_id}/verify", data=request.json()
) as r:
return types.Session.parse_obj(await r.json())
return types.Session.model_validate(await r.json())
49 changes: 25 additions & 24 deletions clerk/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, List, Optional
from typing import Any

from pydantic import BaseModel

Expand All @@ -17,9 +17,9 @@ class Session(BaseModel):
class Client(BaseModel):
object: str
id: str
last_active_session_id: Optional[str] = None
sign_in_attempt_id: Optional[str] = None
sign_up_attempt_id: Optional[str] = None
last_active_session_id: str | None = None
sign_in_attempt_id: str | None = None
sign_up_attempt_id: str | None = None
ended: bool = False


Expand All @@ -39,34 +39,35 @@ class PhoneNumber(BaseModel):
phone_number: str
reserved_for_second_factor: bool
verification: Verification
linked_to: List[IdentificationLink]
linked_to: list[IdentificationLink]


class EmailAddress(BaseModel):
object: str
id: str
email_address: str
verification: Verification
linked_to: List[IdentificationLink]
linked_to: list[IdentificationLink]


class User(BaseModel):
object: str
id: str
username: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
gender: Optional[str] = None
birthday: Optional[str] = None
username: str | None = None
first_name: str | None = None
last_name: str | None = None
gender: str | None = None
birthday: str | None = None
profile_image_url: str
primary_email_address_id: Optional[str] = None
primary_phone_number_id: Optional[str] = None
primary_email_address_id: str | None = None
primary_phone_number_id: str | None = None
password_enabled: bool
two_factor_enabled: bool
email_addresses: List[EmailAddress]
phone_numbers: List[PhoneNumber]
external_accounts: List[Any]
metadata: Any
email_addresses: list[EmailAddress]
phone_numbers: list[PhoneNumber]
external_accounts: list[Any]
public_metadata: Any
unsafe_metadata: Any
private_metadata: Any
created_at: int
updated_at: int
Expand All @@ -76,7 +77,7 @@ class Error(BaseModel):
message: str
long_message: str
code: str
meta: Optional[Any] = None
meta: Any | None = None


class VerifyRequest(BaseModel):
Expand All @@ -90,9 +91,9 @@ class DeleteUserResponse(BaseModel):


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
first_name: str | None = None
last_name: str | None = None
primary_email_address_id: str | None = None
primary_phone_number_id: str | None = None
profile_image: str | None = None
password: str | None = None
Loading