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
19 changes: 7 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repos:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 25.1.0
rev: 24.10.0
hooks:
- id: black
language_version: python3
Expand All @@ -29,19 +29,22 @@ repos:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/pycqa/flake8
rev: 7.2.0
rev: 7.1.2
hooks:
- id: flake8
additional_dependencies: [Flake8-pyproject]
- repo: https://github.com/python-poetry/poetry
rev: 2.1.3
hooks:
- id: poetry-export
files: pyproject.toml
- id: poetry-lock
files: pyproject.toml
- id: poetry-check
files: pyproject.toml
- repo: https://github.com/python-poetry/poetry-plugin-export
rev: 1.9.0
hooks:
- id: poetry-export
files: pyproject.toml
- repo: https://github.com/pre-commit/pre-commit
rev: v4.2.0
hooks:
Expand All @@ -62,11 +65,3 @@ repos:
types: [python]
entry: "print"
language: pygrep
- id: liccheck
name: Run Python License Checker
description: Check license compliance of python requirements
entry: poetry
args: [run, liccheck, --level, paranoid]
language: system
files: ^(.*requirements.*\.txt|setup\.cfg|setup\.py|pyproject\.toml|liccheck\.ini)$
pass_filenames: false
82 changes: 78 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
The Descope SDK for python provides convenient access to the Descope user management and authentication API
for a backend written in python. You can read more on the [Descope Website](https://descope.com).

## ✨ New: Async Support

The SDK now supports async/await patterns alongside the existing synchronous API. Both APIs provide identical functionality with the same method signatures and error handling.

```python
# Synchronous usage (existing)
from descope import DescopeClient, DeliveryMethod

client = DescopeClient(project_id="P123")
masked_email = client.otp.sign_up(DeliveryMethod.EMAIL, "[email protected]")

# Asynchronous usage (new) - preferred with context manager
from descope import AsyncDescopeClient, DeliveryMethod

async def main():
async with AsyncDescopeClient(project_id="P123") as client:
masked_email = await client.otp.sign_up_async(DeliveryMethod.EMAIL, "[email protected]")

# Or manual resource management
async def main_manual():
client = AsyncDescopeClient(project_id="P123")
try:
masked_email = await client.otp.sign_up_async(DeliveryMethod.EMAIL, "[email protected]")
finally:
await client.close()
```

## Requirements

The SDK supports Python 3.8.1 and above.
Expand All @@ -15,7 +42,7 @@ Install the package with:
pip install descope
```

#### If you would like to use the Flask decorators, make sure to install the Flask extras:
### If you would like to use the Flask decorators, make sure to install the Flask extras:

```bash
pip install descope[Flask]
Expand Down Expand Up @@ -86,8 +113,12 @@ Send a user a one-time password (OTP) using your preferred delivery method (_ema

The user can either `sign up`, `sign in` or `sign up or in`

#### Synchronous usage

```python
from descope import DeliveryMethod
from descope import DescopeClient, DeliveryMethod

descope_client = DescopeClient(project_id="<Project ID>")

# Every user must have a login ID. All other user information is optional
email = "[email protected]"
Expand All @@ -105,6 +136,26 @@ session_token = jwt_response[SESSION_TOKEN_NAME].get("jwt")
refresh_token = jwt_response[REFRESH_SESSION_TOKEN_NAME].get("jwt")
```

#### Asynchronous usage

```python
from descope import AsyncDescopeClient, DeliveryMethod

async def otp_example():
async with AsyncDescopeClient(project_id="<Project ID>") as client:
# Every user must have a login ID. All other user information is optional
email = "[email protected]"
user = {"name": "Desmond Copeland", "phone": "212-555-1234", "email": email}
masked_address = await client.otp.sign_up_async(method=DeliveryMethod.EMAIL, login_id=email, user=user)

# Verify the code
jwt_response = await client.otp.verify_code_async(
method=DeliveryMethod.EMAIL, login_id=email, code=value
)
session_token = jwt_response[SESSION_TOKEN_NAME].get("jwt")
refresh_token = jwt_response[REFRESH_SESSION_TOKEN_NAME].get("jwt")
```

The session and refresh JWTs should be returned to the caller, and passed with every request in the session. Read more on [session validation](#session-validation)

### Magic Link
Expand All @@ -115,8 +166,12 @@ This redirection can be configured in code, or generally in the [Descope Console

The user can either `sign up`, `sign in` or `sign up or in`

#### Synchronous usage

```python
from descope import DeliveryMethod
from descope import DescopeClient, DeliveryMethod

descope_client = DescopeClient(project_id="<Project ID>")

masked_address = descope_client.magiclink.sign_up_or_in(
method=DeliveryMethod.EMAIL,
Expand All @@ -133,6 +188,25 @@ session_token = jwt_response[SESSION_TOKEN_NAME].get("jwt")
refresh_token = jwt_response[REFRESH_SESSION_TOKEN_NAME].get("jwt")
```

#### Asynchronous usage

```python
from descope import AsyncDescopeClient, DeliveryMethod

async def magiclink_example():
async with AsyncDescopeClient(project_id="<Project ID>") as client:
masked_address = await client.magiclink.sign_up_or_in_async(
method=DeliveryMethod.EMAIL,
login_id="[email protected]",
uri="http://myapp.com/verify-magic-link", # Set redirect URI here or via console
)

# Verify the magic link token
jwt_response = await client.magiclink.verify_async(token=token)
session_token = jwt_response[SESSION_TOKEN_NAME].get("jwt")
refresh_token = jwt_response[REFRESH_SESSION_TOKEN_NAME].get("jwt")
```

The session and refresh JWTs should be returned to the caller, and passed with every request in the session. Read more on [session validation](#session-validation)

### Enchanted Link
Expand Down Expand Up @@ -1178,7 +1252,7 @@ type doc
permission can_create: owner | parent.owner
permission can_edit: editor | can_create
permission can_view: viewer | can_edit
```
```

Descope SDK allows you to fully manage the schema and relations as well as perform simple (and not so simple) checks regarding the existence of relations.

Expand Down
33 changes: 33 additions & 0 deletions descope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SignUpOptions,
)
from descope.descope_client import DescopeClient
from descope.descope_client_async import AsyncDescopeClient
from descope.exceptions import (
API_RATE_LIMIT_RETRY_AFTER_HEADER,
ERROR_TYPE_API_RATE_LIMIT,
Expand Down Expand Up @@ -39,3 +40,35 @@
UserPasswordFirebase,
UserPasswordPbkdf2,
)

__all__ = [
"COOKIE_DATA_NAME",
"REFRESH_SESSION_COOKIE_NAME",
"REFRESH_SESSION_TOKEN_NAME",
"SESSION_COOKIE_NAME",
"SESSION_TOKEN_NAME",
"AccessKeyLoginOptions",
"DeliveryMethod",
"LoginOptions",
"SignUpOptions",
"DescopeClient",
"AsyncDescopeClient",
"AuthException",
"RateLimitException",
"AssociatedTenant",
"SAMLIDPAttributeMappingInfo",
"SAMLIDPGroupsMappingInfo",
"SAMLIDPRoleGroupMappingInfo",
"AttributeMapping",
"OIDCAttributeMapping",
"RoleMapping",
"SSOOIDCSettings",
"SSOSAMLSettings",
"SSOSAMLSettingsByMetadata",
"UserObj",
"UserPassword",
"UserPasswordBcrypt",
"UserPasswordDjango",
"UserPasswordFirebase",
"UserPasswordPbkdf2",
]
4 changes: 2 additions & 2 deletions descope/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
try:
from importlib.metadata import version
except ImportError:
from pkg_resources import get_distribution
from pkg_resources import get_distribution # type: ignore

import requests
from email_validator import EmailNotValidError, validate_email
Expand Down Expand Up @@ -120,7 +120,7 @@ def _raise_rate_limit_exception(self, response):
)
except RateLimitException:
raise
except Exception as e:
except Exception:
raise RateLimitException(
status_code=HTTPStatus.TOO_MANY_REQUESTS,
error_type=ERROR_TYPE_API_RATE_LIMIT,
Expand Down
9 changes: 7 additions & 2 deletions descope/authmethod/enchantedlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,13 @@ def update_user_email(
Auth.validate_email(email)

body = EnchantedLink._compose_update_user_email_body(
login_id, email, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
email,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
uri = EndpointsV1.update_user_email_enchantedlink_path
response = self._auth.do_post(uri, body, None, refresh_token)
Expand Down
18 changes: 14 additions & 4 deletions descope/authmethod/magiclink.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,13 @@ def update_user_email(
Auth.validate_email(email)

body = MagicLink._compose_update_user_email_body(
login_id, email, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
email,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
uri = EndpointsV1.update_user_email_magiclink_path
response = self._auth.do_post(uri, body, None, refresh_token)
Expand All @@ -144,8 +149,13 @@ def update_user_phone(
Auth.validate_phone(method, phone)

body = MagicLink._compose_update_user_phone_body(
login_id, phone, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
phone,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
uri = EndpointsV1.update_user_phone_magiclink_path
response = self._auth.do_post(uri, body, None, refresh_token)
Expand Down
18 changes: 14 additions & 4 deletions descope/authmethod/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,13 @@ def update_user_email(

uri = EndpointsV1.update_user_email_otp_path
body = OTP._compose_update_user_email_body(
login_id, email, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
email,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
response = self._auth.do_post(uri, body, None, refresh_token)
return Auth.extract_masked_address(response.json(), DeliveryMethod.EMAIL)
Expand Down Expand Up @@ -241,8 +246,13 @@ def update_user_phone(

uri = OTP._compose_update_phone_url(method)
body = OTP._compose_update_user_phone_body(
login_id, phone, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
phone,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
response = self._auth.do_post(uri, body, None, refresh_token)
return Auth.extract_masked_address(response.json(), method)
Expand Down
Loading