Skip to content
Merged
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
17 changes: 10 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ repos:
hooks:
- id: sync_with_poetry
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-yaml
- id: check-toml
- id: debug-statements
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/PyCQA/isort
rev: 6.1.0
rev: 7.0.0
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 25.11.0
rev: 25.12.0
hooks:
- id: black
language_version: python3
Expand All @@ -36,18 +36,21 @@ repos:
- repo: https://github.com/python-poetry/poetry
rev: 2.2.1
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
args: ["-f", "requirements.txt", "-o", "requirements.txt"]
- repo: https://github.com/pre-commit/pre-commit
rev: v4.5.0
rev: v4.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.7.0
rev: 1.7.1
hooks:
- id: tox-ini-fmt
args: ["-p", "type"]
Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,59 @@ descope_client = DescopeClient()
descope_client = DescopeClient(project_id="<Project ID>", management_key="<Management Key>")
```

### Verbose Mode for Debugging

When debugging failed API requests, you can enable verbose mode to capture HTTP response metadata like headers (`cf-ray`, `x-request-id`), status codes, and raw response bodies. This is especially useful when working with Descope support to troubleshoot issues.

```python
from descope import DescopeClient, AuthException
import logging

logger = logging.getLogger(__name__)

# Enable verbose mode during client initialization
client = DescopeClient(
project_id="<Project ID>",
management_key="<Management Key>",
verbose=True # Enable response metadata capture
)

try:
# Make any API call
client.mgmt.user.create(
login_id="test@example.com",
email="test@example.com"
)
except AuthException as e:
# Access the last response metadata for debugging
response = client.get_last_response()
if response:
logger.error(f"Request failed with status {response.status_code}")
logger.error(f"cf-ray: {response.headers.get('cf-ray')}")
logger.error(f"x-request-id: {response.headers.get('x-request-id')}")
logger.error(f"Response body: {response.text}")

# Provide cf-ray to Descope support for debugging
print(f"Please provide this cf-ray to support: {response.headers.get('cf-ray')}")
```

**Important Notes:**
- Verbose mode is **disabled by default** (no performance impact when not needed)
- When enabled, only the **most recent** HTTP response is stored
- `get_last_response()` returns `None` when verbose mode is disabled
- The response object provides dict-like access to JSON data while also exposing HTTP metadata

**Available metadata on response objects:**
- `response.headers` - HTTP response headers (dict-like object)
- `response.status_code` - HTTP status code (int)
- `response.text` - Raw response body as text (str)
- `response.url` - Request URL (str)
- `response.ok` - Whether status code is < 400 (bool)
- `response.json()` - Parsed JSON response (dict/list)
- `response["key"]` - Dict-like access to JSON data (for backward compatibility)

For a complete example, see [samples/verbose_mode_example.py](https://github.com/descope/python-sdk/blob/main/samples/verbose_mode_example.py).

### Manage Tenants

You can create, update, delete or load tenants:
Expand Down
1 change: 1 addition & 0 deletions descope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
AuthException,
RateLimitException,
)
from descope.http_client import DescopeResponse
from descope.management.common import (
AssociatedTenant,
SAMLIDPAttributeMappingInfo,
Expand Down
63 changes: 51 additions & 12 deletions descope/descope_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import os
from typing import Iterable, Optional
from typing import Iterable

import requests

Expand All @@ -27,15 +27,16 @@ class DescopeClient:
def __init__(
self,
project_id: str,
public_key: Optional[dict] = None,
public_key: dict | None = None,
skip_verify: bool = False,
management_key: Optional[str] = None,
management_key: str | None = None,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
jwt_validation_leeway: int = 5,
auth_management_key: Optional[str] = None,
fga_cache_url: Optional[str] = None,
auth_management_key: str | None = None,
fga_cache_url: str | None = None,
*,
base_url: Optional[str] = None,
base_url: str | None = None,
verbose: bool = False,
):
# validate project id
project_id = project_id or os.getenv("DESCOPE_PROJECT_ID", "")
Expand All @@ -57,6 +58,7 @@ def __init__(
secure=not skip_verify,
management_key=auth_management_key
or os.getenv("DESCOPE_AUTH_MANAGEMENT_KEY"),
verbose=verbose,
)
self._auth = Auth(
project_id,
Expand All @@ -81,13 +83,18 @@ def __init__(
timeout_seconds=auth_http_client.timeout_seconds,
secure=auth_http_client.secure,
management_key=management_key or os.getenv("DESCOPE_MANAGEMENT_KEY"),
verbose=verbose,
)
self._mgmt = MGMT(
http_client=mgmt_http_client,
auth=self._auth,
fga_cache_url=fga_cache_url,
)

# Store references to HTTP clients for verbose mode access
self._auth_http_client = auth_http_client
self._mgmt_http_client = mgmt_http_client

@property
def mgmt(self):
return self._mgmt
Expand Down Expand Up @@ -328,7 +335,7 @@ def get_matched_tenant_roles(
return matched

def validate_session(
self, session_token: str, audience: Optional[Iterable[str] | str] = None
self, session_token: str, audience: Iterable[str] | str | None = None
) -> dict:
"""
Validate a session token. Call this function for every incoming request to your
Expand All @@ -351,7 +358,7 @@ def validate_session(
return self._auth.validate_session(session_token, audience)

def refresh_session(
self, refresh_token: str, audience: Optional[Iterable[str] | str] = None
self, refresh_token: str, audience: Iterable[str] | str | None = None
) -> dict:
"""
Refresh a session. Call this function when a session expires and needs to be refreshed.
Expand All @@ -372,7 +379,7 @@ def validate_and_refresh_session(
self,
session_token: str,
refresh_token: str,
audience: Optional[Iterable[str] | str] = None,
audience: Iterable[str] | str | None = None,
) -> dict:
"""
Validate the session token and refresh it if it has expired, the session token will automatically be refreshed.
Expand Down Expand Up @@ -472,7 +479,7 @@ def my_tenants(
self,
refresh_token: str,
dct: bool = False,
ids: Optional[list[str]] = None,
ids: list[str] | None = None,
) -> dict:
"""
Retrieve tenant attributes that user belongs to, one of dct/ids must be populated .
Expand Down Expand Up @@ -553,8 +560,8 @@ def history(self, refresh_token: str) -> list[dict]:
def exchange_access_key(
self,
access_key: str,
audience: Optional[Iterable[str] | str] = None,
login_options: Optional[AccessKeyLoginOptions] = None,
audience: Iterable[str] | str | None = None,
login_options: AccessKeyLoginOptions | None = None,
) -> dict:
"""
Return a new session token for the given access key
Expand Down Expand Up @@ -595,3 +602,35 @@ def select_tenant(
AuthException: Exception is raised if session is not authorized or another error occurs
"""
return self._auth.select_tenant(tenant_id, refresh_token)

def get_last_response(self):
"""
Get the last HTTP response from either auth or management operations.

Only available when verbose mode is enabled during client initialization.
This provides access to HTTP metadata like headers (cf-ray), status codes,
and raw response data for debugging failed requests.

Returns:
DescopeResponse: The last response if verbose mode is enabled.
Returns the most recent response from either auth or mgmt operations.
None if verbose mode is disabled or no requests have been made.

Example:
client = DescopeClient(project_id, management_key, verbose=True)
try:
client.mgmt.user.create(login_id="test@example.com")
except AuthException:
resp = client.get_last_response()
if resp:
# Access metadata for debugging
cf_ray = resp.headers.get("cf-ray")
status = resp.status_code
"""
# Return the most recently used response
mgmt_resp = self._mgmt_http_client.get_last_response()
auth_resp = self._auth_http_client.get_last_response()

# Return whichever is not None, preferring mgmt if both exist
# (in practice, only one should be non-None at a time)
return mgmt_resp or auth_resp
Loading
Loading