Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Added OAuth 2.0 support
  • Loading branch information
sunil-lakshman committed Sep 10, 2025
commit 01377a4bfaacde44de343c187fe603ce7316c2bd
6 changes: 6 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,10 @@ fileignoreconfig:
checksum: 344aa9e4b3ec399c581a507eceff63ff6faf56ad938475e5f4865f6cb590df68
- filename: tests/unit/contentstack/test_contentstack.py
checksum: 98503cbd96cb546a19aed037a6ca28ef54fcea312efcd9bac1171e43760f6e86
- filename: contentstack_management/contentstack.py
checksum: 520f6fa236569a05579011fa67cb29381f187616d96526ecdfad5ec8255231a5
- filename: tests/unit/test_oauth_handler.py
checksum: 8b6853ba64c3de4f9097ca506719c5e33c7468ae5985b8adcda3eb6461d76be5
- filename: contentstack_management/oauth/oauth_handler.py
checksum: e33cfd32d90c0553c4959c0d266fef1247cd0e0fe7bbe85cae98bb205e62c70e
version: "1.0"
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# CHANGELOG

## Content Management SDK For Python

---
## v1.7.0

#### Date: 15 September 2025

- OAuth 2.0 support.
---
## v1.6.0

Expand Down
8 changes: 6 additions & 2 deletions contentstack_management/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
from .extensions.extension import Extension
from .variant_group.variant_group import VariantGroup
from .variants.variants import Variants
from .oauth.oauth_handler import OAuthHandler
from .oauth.oauth_interceptor import OAuthInterceptor


__all__ = (
Expand Down Expand Up @@ -71,14 +73,16 @@
"PublishQueue",
"Extension",
"VariantGroup",
"Variants"
"Variants",
"OAuthHandler",
"OAuthInterceptor"
)

__title__ = 'contentstack-management-python'
__author__ = 'dev-ex'
__status__ = 'debug'
__region__ = 'na'
__version__ = '1.6.0'
__version__ = '1.7.0'
__host__ = 'api.contentstack.io'
__protocol__ = 'https://'
__api_version__ = 'v3'
Expand Down
18 changes: 15 additions & 3 deletions contentstack_management/_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


class _APIClient:
def __init__(self, endpoint, headers, timeout=30, max_retries: int = 5):
def __init__(self, endpoint, headers, timeout=30, max_retries: int = 5, oauth_interceptor=None):
"""
The function is a constructor that initializes the endpoint, headers, timeout, and max_retries
attributes of an object.
Expand All @@ -25,6 +25,8 @@ def __init__(self, endpoint, headers, timeout=30, max_retries: int = 5):
self.headers = headers
self.timeout = timeout
self.max_retries = max_retries
self.oauth_interceptor = oauth_interceptor
self.oauth = {} # OAuth token storage
pass

def _call_request(self, method, url, headers: dict = None, params=None, data=None, json_data=None, files=None):
Expand Down Expand Up @@ -52,9 +54,19 @@ def _call_request(self, method, url, headers: dict = None, params=None, data=Non
:return: the JSON response from the HTTP request.
"""

# headers.update(self.headers)
# Use OAuth interceptor if available (matching Java implementation)
if self.oauth_interceptor and self.oauth_interceptor.is_oauth_configured():
return self.oauth_interceptor.execute_request(
method, url, headers=headers, params=params, data=data,
json=json_data, files=files, timeout=self.timeout
)

# Fallback to standard requests (matching Java implementation)
if headers is None:
headers = {}
headers.update(self.headers) # Merge client headers (including authtoken) with request headers
response = requests.request(
method, url, headers=headers, params=params, data=data, json=json_data, files=files)
method, url, headers=headers, params=params, data=data, json=json_data, files=files, timeout=self.timeout)
# response.raise_for_status()
return response

Expand Down
54 changes: 53 additions & 1 deletion contentstack_management/contentstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from contentstack_management.stack import stack
from contentstack_management.user_session import user_session
from contentstack_management.users import user
from contentstack_management.oauth.oauth_handler import OAuthHandler

version = '0.0.1'

Expand Down Expand Up @@ -33,7 +34,7 @@ class Client:
def __init__(self, host: str = 'api.contentstack.io', scheme: str = 'https://',
authtoken: str = None , management_token=None, headers: dict = None,
region: Region = Region.US.value, version='v3', timeout=2, max_retries: int = 18, early_access: list = None,
**kwargs):
oauth_config: dict = None, **kwargs):
self.endpoint = 'https://api.contentstack.io/v3/'
if region is not None and host is not None and region is not Region.US.value:
self.endpoint = f'{scheme}{region}-{host}/{version}/'
Expand All @@ -55,6 +56,19 @@ def __init__(self, host: str = 'api.contentstack.io', scheme: str = 'https://',
headers['authorization'] = management_token
headers = user_agents(headers)
self.client = _APIClient(endpoint=self.endpoint, headers=headers, timeout=timeout, max_retries=max_retries)

# Initialize OAuth if configuration is provided
self.oauth_handler = None
if oauth_config:
self.oauth_handler = OAuthHandler(
app_id=oauth_config.get('app_id'),
client_id=oauth_config.get('client_id'),
redirect_uri=oauth_config.get('redirect_uri'),
response_type=oauth_config.get('response_type', 'code'),
client_secret=oauth_config.get('client_secret'),
scope=oauth_config.get('scope'),
api_client=self.client
)

"""
:param host: Optional hostname for the API endpoint.
Expand Down Expand Up @@ -96,3 +110,41 @@ def organizations(self, organization_uid: str = None):

def stack(self, api_key: str = None):
return stack.Stack(self.client, api_key)

def oauth(self, app_id: str, client_id: str, redirect_uri: str,
response_type: str = "code", client_secret: str = None,
scope: list = None):
"""
Create an OAuth handler for OAuth 2.0 authentication.

Args:
app_id: Your registered App ID
client_id: Your OAuth Client ID
redirect_uri: The URL where the user is redirected after login and consent
response_type: OAuth response type (default: "code")
client_secret: Client secret for standard OAuth flows (optional for PKCE)
scope: Permissions requested (optional)

Returns:
OAuthHandler instance

Example:
>>> import contentstack_management
>>> client = contentstack_management.Client()
>>> oauth_handler = client.oauth(
... app_id='your-app-id',
... client_id='your-client-id',
... redirect_uri='http://localhost:3000/callback'
... )
>>> auth_url = oauth_handler.authorize()
>>> print(f"Visit this URL to authorize: {auth_url}")
"""
return OAuthHandler(
app_id=app_id,
client_id=client_id,
redirect_uri=redirect_uri,
response_type=response_type,
client_secret=client_secret,
scope=scope,
api_client=self.client
)
5 changes: 5 additions & 0 deletions contentstack_management/oauth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""OAuth 2.0 authentication module for Contentstack Management SDK."""

from .oauth_handler import OAuthHandler

__all__ = ["OAuthHandler"]
Loading