contentstack-management 1.6.0__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/PKG-INFO +1 -1
  2. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/__init__.py +6 -2
  3. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/_api_client.py +13 -3
  4. contentstack_management-1.7.0/contentstack_management/contentstack.py +181 -0
  5. contentstack_management-1.7.0/contentstack_management/oauth/__init__.py +5 -0
  6. contentstack_management-1.7.0/contentstack_management/oauth/oauth_handler.py +409 -0
  7. contentstack_management-1.7.0/contentstack_management/oauth/oauth_interceptor.py +187 -0
  8. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/user_session/user_session.py +1 -1
  9. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management.egg-info/PKG-INFO +1 -1
  10. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management.egg-info/SOURCES.txt +3 -0
  11. contentstack_management-1.6.0/contentstack_management/contentstack.py +0 -98
  12. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/LICENSE +0 -0
  13. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/README.md +0 -0
  14. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/_constant.py +0 -0
  15. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/_errors.py +0 -0
  16. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/aliases/__init__.py +0 -0
  17. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/aliases/aliases.py +0 -0
  18. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/assets/__init__.py +0 -0
  19. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/assets/assets.py +0 -0
  20. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/auditlogs/__init__.py +0 -0
  21. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/auditlogs/auditlog.py +0 -0
  22. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/branches/__init__.py +0 -0
  23. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/branches/branches.py +0 -0
  24. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/bulk_operations/__init__.py +0 -0
  25. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/bulk_operations/bulk_operation.py +0 -0
  26. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/common.py +0 -0
  27. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/content_types/__init__.py +0 -0
  28. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/content_types/content_type.py +0 -0
  29. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/delivery_token/__init__.py +0 -0
  30. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/delivery_token/delivery_token.py +0 -0
  31. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/entries/__init__.py +0 -0
  32. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/entries/entry.py +0 -0
  33. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/entry_variants/__init__.py +0 -0
  34. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/entry_variants/entry_variants.py +0 -0
  35. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/environments/__init__.py +0 -0
  36. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/environments/environment.py +0 -0
  37. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/extensions/__init__.py +0 -0
  38. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/extensions/extension.py +0 -0
  39. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/global_fields/__init__.py +0 -0
  40. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/global_fields/global_fields.py +0 -0
  41. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/labels/__init__.py +0 -0
  42. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/labels/label.py +0 -0
  43. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/locale/__init__.py +0 -0
  44. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/locale/locale.py +0 -0
  45. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/management_token/__init__.py +0 -0
  46. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/management_token/management_token.py +0 -0
  47. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/metadata/__init__.py +0 -0
  48. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/metadata/metadata.py +0 -0
  49. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/organizations/__init__.py +0 -0
  50. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/organizations/organization.py +0 -0
  51. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/publish_queue/__init__.py +0 -0
  52. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/publish_queue/publish_queue.py +0 -0
  53. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/release_items/__init__.py +0 -0
  54. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/release_items/release_item.py +0 -0
  55. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/releases/__init__.py +0 -0
  56. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/releases/release.py +0 -0
  57. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/roles/__init__.py +0 -0
  58. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/roles/roles.py +0 -0
  59. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/stack/__init__.py +0 -0
  60. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/stack/stack.py +0 -0
  61. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/taxonomies/__init__.py +0 -0
  62. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/taxonomies/taxonomy.py +0 -0
  63. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/terms/__init__.py +0 -0
  64. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/terms/terms.py +0 -0
  65. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/user_session/__init__.py +0 -0
  66. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/users/__init__.py +0 -0
  67. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/users/user.py +0 -0
  68. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/variant_group/__init__.py +0 -0
  69. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/variant_group/variant_group.py +0 -0
  70. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/variants/__init__.py +0 -0
  71. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/variants/variants.py +0 -0
  72. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/webhooks/__init__.py +0 -0
  73. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/webhooks/webhook.py +0 -0
  74. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/workflows/__init__.py +0 -0
  75. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management/workflows/workflows.py +0 -0
  76. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management.egg-info/dependency_links.txt +0 -0
  77. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management.egg-info/requires.txt +0 -0
  78. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/contentstack_management.egg-info/top_level.txt +0 -0
  79. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/setup.cfg +0 -0
  80. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/setup.py +0 -0
  81. {contentstack_management-1.6.0 → contentstack_management-1.7.0}/tests/test_contentstack.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack-management
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Contentstack API Client Library for Python
5
5
  Home-page: https://github.com/contentstack/contentstack-management-python
6
6
  Author: dev-ex
@@ -34,6 +34,8 @@ from .publish_queue.publish_queue import PublishQueue
34
34
  from .extensions.extension import Extension
35
35
  from .variant_group.variant_group import VariantGroup
36
36
  from .variants.variants import Variants
37
+ from .oauth.oauth_handler import OAuthHandler
38
+ from .oauth.oauth_interceptor import OAuthInterceptor
37
39
 
38
40
 
39
41
  __all__ = (
@@ -71,14 +73,16 @@ __all__ = (
71
73
  "PublishQueue",
72
74
  "Extension",
73
75
  "VariantGroup",
74
- "Variants"
76
+ "Variants",
77
+ "OAuthHandler",
78
+ "OAuthInterceptor"
75
79
  )
76
80
 
77
81
  __title__ = 'contentstack-management-python'
78
82
  __author__ = 'dev-ex'
79
83
  __status__ = 'debug'
80
84
  __region__ = 'na'
81
- __version__ = '1.6.0'
85
+ __version__ = '1.7.0'
82
86
  __host__ = 'api.contentstack.io'
83
87
  __protocol__ = 'https://'
84
88
  __api_version__ = 'v3'
@@ -2,7 +2,7 @@ import requests
2
2
 
3
3
 
4
4
  class _APIClient:
5
- def __init__(self, endpoint, headers, timeout=30, max_retries: int = 5):
5
+ def __init__(self, endpoint, headers, timeout=30, max_retries: int = 5, oauth_interceptor=None):
6
6
  """
7
7
  The function is a constructor that initializes the endpoint, headers, timeout, and max_retries
8
8
  attributes of an object.
@@ -25,6 +25,8 @@ class _APIClient:
25
25
  self.headers = headers
26
26
  self.timeout = timeout
27
27
  self.max_retries = max_retries
28
+ self.oauth_interceptor = oauth_interceptor
29
+ self.oauth = {} # OAuth token storage
28
30
  pass
29
31
 
30
32
  def _call_request(self, method, url, headers: dict = None, params=None, data=None, json_data=None, files=None):
@@ -52,9 +54,17 @@ class _APIClient:
52
54
  :return: the JSON response from the HTTP request.
53
55
  """
54
56
 
55
- # headers.update(self.headers)
57
+ if self.oauth_interceptor and self.oauth_interceptor.is_oauth_configured():
58
+ return self.oauth_interceptor.execute_request(
59
+ method, url, headers=headers, params=params, data=data,
60
+ json=json_data, files=files, timeout=self.timeout
61
+ )
62
+
63
+ if headers is None:
64
+ headers = {}
65
+ headers.update(self.headers) # Merge client headers (including authtoken) with request headers
56
66
  response = requests.request(
57
- method, url, headers=headers, params=params, data=data, json=json_data, files=files)
67
+ method, url, headers=headers, params=params, data=data, json=json_data, files=files, timeout=self.timeout)
58
68
  # response.raise_for_status()
59
69
  return response
60
70
 
@@ -0,0 +1,181 @@
1
+ from enum import Enum
2
+ import os
3
+ import pyotp
4
+ from ._api_client import _APIClient
5
+ from contentstack_management.organizations import organization
6
+ from contentstack_management.stack import stack
7
+ from contentstack_management.user_session import user_session
8
+ from contentstack_management.users import user
9
+ from contentstack_management.oauth.oauth_handler import OAuthHandler
10
+
11
+ version = '0.0.1'
12
+
13
+
14
+ class Region(Enum):
15
+ US = "us"
16
+ EU = "eu"
17
+ AU = "au"
18
+ AZURE_EU = "azure-eu"
19
+ AZURE_NA = "azure-na"
20
+ GCP_NA = "gcp-na"
21
+ GCP_EU = "gcp-eu"
22
+
23
+
24
+ def user_agents(headers=None):
25
+ if headers is None:
26
+ headers = {}
27
+ local_headers = {'X-User-Agent': f'contentstack-management-python/v{version}',
28
+ "Content-Type": "application/json"}
29
+ headers.update(local_headers)
30
+ return headers
31
+
32
+
33
+ class Client:
34
+
35
+ # TODO: DefaultCSCredential(), needs to be implemented
36
+ def __init__(self, host: str = 'api.contentstack.io', scheme: str = 'https://',
37
+ authtoken: str = None , management_token=None, headers: dict = None,
38
+ region: Region = Region.US.value, version='v3', timeout=2, max_retries: int = 18, early_access: list = None,
39
+ oauth_config: dict = None, **kwargs):
40
+ self.endpoint = 'https://api.contentstack.io/v3/'
41
+
42
+ if region is not None and region is not Region.US.value:
43
+ if host is not None and host != 'api.contentstack.io':
44
+ self.endpoint = f'{scheme}{region}-api.{host}/{version}/'
45
+ else:
46
+ host = 'api.contentstack.com'
47
+ self.endpoint = f'{scheme}{region}-{host}/{version}/'
48
+ elif host is not None and host != 'api.contentstack.io':
49
+ self.endpoint = f'{scheme}{host}/{version}/'
50
+ if headers is None:
51
+ headers = {}
52
+ if early_access is not None:
53
+ early_access_str = ', '.join(early_access)
54
+ headers['x-header-ea'] = early_access_str
55
+
56
+ if authtoken is not None:
57
+ headers['authtoken'] = authtoken
58
+
59
+ if management_token is not None:
60
+ headers['authorization'] = management_token
61
+ headers = user_agents(headers)
62
+ self.client = _APIClient(endpoint=self.endpoint, headers=headers, timeout=timeout, max_retries=max_retries)
63
+
64
+ # Initialize OAuth if configuration is provided
65
+ self.oauth_handler = None
66
+ if oauth_config:
67
+ self.oauth_handler = OAuthHandler(
68
+ app_id=oauth_config.get('app_id'),
69
+ client_id=oauth_config.get('client_id'),
70
+ redirect_uri=oauth_config.get('redirect_uri'),
71
+ response_type=oauth_config.get('response_type', 'code'),
72
+ client_secret=oauth_config.get('client_secret'),
73
+ scope=oauth_config.get('scope'),
74
+ api_client=self.client
75
+ )
76
+
77
+ """
78
+ :param host: Optional hostname for the API endpoint.
79
+ :param authtoken: Optional authentication token for API requests
80
+ :param headers: Optional headers to be included with API requests
81
+ :param authorization: Optional headers to be included in the api request
82
+ :param region: optional region to be included in API requests,
83
+ We have region support options for na, eu, azure-eu, azure-na
84
+ :param scheme: optional scheme to be included in API requests
85
+ :param version: optional version to be included in API request path url,
86
+ :param timeout: Optional timeout value for API requests
87
+ :param max_requests:Optional maximum number of requests to be made
88
+ :param retry_on_error: Optional boolean value indicating whether to retry API requests on error.
89
+ :return: A client object for performing API operations.
90
+ -------------------------------
91
+ [Example:]
92
+
93
+ >>> import contentstack_management
94
+ >>> contentstack_client = contentstack_management.Client()
95
+ -------------------------------
96
+ """
97
+
98
+ def login(self, email: str, password: str, tfa_token: str = None, mfa_secret: str = None):
99
+ """
100
+ Login to Contentstack with optional TOTP support.
101
+
102
+ :param email: User's email address
103
+ :param password: User's password
104
+ :param tfa_token: Optional two-factor authentication token
105
+ :param mfa_secret: Optional MFA secret for automatic TOTP generation.
106
+ If not provided, will check MFA_SECRET environment variable
107
+ :return: Response object from the login request
108
+ """
109
+ final_tfa_token = tfa_token
110
+
111
+ if not mfa_secret:
112
+ mfa_secret = os.getenv('MFA_SECRET')
113
+
114
+ if mfa_secret and not tfa_token:
115
+ final_tfa_token = self._generate_totp(mfa_secret)
116
+
117
+ return user_session.UserSession(self.client).login(email, password, final_tfa_token)
118
+
119
+ def _generate_totp(self, secret: str) -> str:
120
+ """
121
+ Generate a Time-Based One-Time Password (TOTP) from the provided secret.
122
+
123
+ :param secret: The MFA secret key for TOTP generation
124
+ :return: The current TOTP code as a string
125
+ """
126
+ totp = pyotp.TOTP(secret)
127
+ return totp.now()
128
+
129
+ def logout(self):
130
+ return user_session.UserSession(client=self.client).logout()
131
+
132
+ @property
133
+ def authtoken(self):
134
+ return self.client.headers['authtoken']
135
+
136
+ def user(self):
137
+ return user.User(self.client)
138
+
139
+ def organizations(self, organization_uid: str = None):
140
+ return organization.Organization(self.client, organization_uid)
141
+
142
+ def stack(self, api_key: str = None):
143
+ return stack.Stack(self.client, api_key)
144
+
145
+ def oauth(self, app_id: str, client_id: str, redirect_uri: str,
146
+ response_type: str = "code", client_secret: str = None,
147
+ scope: list = None):
148
+ """
149
+ Create an OAuth handler for OAuth 2.0 authentication.
150
+
151
+ Args:
152
+ app_id: Your registered App ID
153
+ client_id: Your OAuth Client ID
154
+ redirect_uri: The URL where the user is redirected after login and consent
155
+ response_type: OAuth response type (default: "code")
156
+ client_secret: Client secret for standard OAuth flows (optional for PKCE)
157
+ scope: Permissions requested (optional)
158
+
159
+ Returns:
160
+ OAuthHandler instance
161
+
162
+ Example:
163
+ >>> import contentstack_management
164
+ >>> client = contentstack_management.Client()
165
+ >>> oauth_handler = client.oauth(
166
+ ... app_id='your-app-id',
167
+ ... client_id='your-client-id',
168
+ ... redirect_uri='http://localhost:3000/callback'
169
+ ... )
170
+ >>> auth_url = oauth_handler.authorize()
171
+ >>> print(f"Visit this URL to authorize: {auth_url}")
172
+ """
173
+ return OAuthHandler(
174
+ app_id=app_id,
175
+ client_id=client_id,
176
+ redirect_uri=redirect_uri,
177
+ response_type=response_type,
178
+ client_secret=client_secret,
179
+ scope=scope,
180
+ api_client=self.client
181
+ )
@@ -0,0 +1,5 @@
1
+ """OAuth 2.0 authentication module for Contentstack Management SDK."""
2
+
3
+ from .oauth_handler import OAuthHandler
4
+
5
+ __all__ = ["OAuthHandler"]
@@ -0,0 +1,409 @@
1
+ """
2
+ OAuth 2.0 Handler for Contentstack Management SDK.
3
+
4
+ This module provides OAuth 2.0 authentication support including:
5
+ - Authorization code flow
6
+ - PKCE (Proof Key for Code Exchange)
7
+ - Token management and refresh
8
+ - Secure token storage
9
+ """
10
+
11
+ import hashlib
12
+ import secrets
13
+ import time
14
+ import urllib.parse
15
+ from typing import Dict, List, Optional, Union
16
+ from urllib.parse import urlparse, parse_qs
17
+
18
+ import requests
19
+
20
+
21
+ class OAuthHandler:
22
+ """
23
+ OAuth 2.0 Handler for Contentstack Management SDK.
24
+
25
+ This class manages OAuth 2.0 authentication flow including authorization,
26
+ token exchange, refresh, and secure storage.
27
+ """
28
+
29
+ # Error messages
30
+ ACCESS_TOKEN_EXPIRED_MSG = "🔄 Access token expired, refreshing..."
31
+ NO_ACCESS_TOKEN_MSG = "No access token available. Please authenticate first."
32
+ NO_OAUTH_TOKENS_MSG = "No OAuth tokens available"
33
+ NO_REFRESH_TOKEN_MSG = "No refresh token available"
34
+ OAUTH_NOT_CONFIGURED_MSG = "OAuth is not configured. Please set up OAuth first."
35
+ INVALID_AUTHORIZATION_CODE_MSG = "Authorization code not found in redirect URL"
36
+ TOKEN_EXCHANGE_FAILED_MSG = "Token exchange failed"
37
+ TOKEN_REFRESH_FAILED_MSG = "Token refresh failed"
38
+ OAUTH_BASE_URL_NOT_SET_MSG = "OAuthBaseURL is not set"
39
+ OAUTH_BASE_URL = 'https://app.contentstack.com'
40
+ DEVELOPER_HUB_BASE_URL = 'https://developerhub-api.contentstack.com'
41
+
42
+ def __init__(
43
+ self,
44
+ app_id: str,
45
+ client_id: str,
46
+ redirect_uri: str,
47
+ response_type: str = "code",
48
+ client_secret: Optional[str] = None,
49
+ scope: Optional[List[str]] = None,
50
+ api_client=None
51
+ ):
52
+ self.app_id = app_id
53
+ self.client_id = client_id
54
+ self.redirect_uri = redirect_uri
55
+ self.response_type = response_type
56
+ self.scope = ' '.join(scope) if scope else ''
57
+ self.client_secret = client_secret # Optional, if provided, PKCE will be skipped
58
+ self.api_client = api_client
59
+
60
+ self._oauth_base_url = self._construct_oauth_base_url()
61
+ self._developer_hub_base_url = self._construct_developer_hub_base_url()
62
+
63
+ if self.api_client:
64
+ if not hasattr(self.api_client, 'oauth'):
65
+ self.api_client.oauth = {}
66
+ self.api_client.oauth.update({
67
+ 'redirect_uri': redirect_uri,
68
+ 'client_id': client_id,
69
+ 'app_id': app_id
70
+ })
71
+
72
+ # PKCE setup
73
+ self.use_pkce = client_secret is None
74
+ if self.use_pkce:
75
+ self.code_verifier = self._generate_code_verifier()
76
+ self.code_challenge = self._generate_code_challenge(self.code_verifier)
77
+ else:
78
+ self.code_verifier = None
79
+ self.code_challenge = None
80
+
81
+ def _construct_oauth_base_url(self) -> str:
82
+ """
83
+ Construct OAuth base URL based on api_client endpoint using dynamic text replacement.
84
+ Returns:
85
+ OAuth base URL string
86
+ """
87
+ if not self.api_client or not hasattr(self.api_client, 'endpoint'):
88
+ return self.OAUTH_BASE_URL
89
+
90
+ endpoint = self.api_client.endpoint
91
+
92
+ from urllib.parse import urlparse
93
+ parsed = urlparse(endpoint)
94
+ domain = parsed.netloc
95
+ if 'api.contentstack.io' in domain:
96
+ oauth_domain = domain.replace('api.contentstack.io', 'app.contentstack.com')
97
+ else:
98
+ oauth_domain = domain.replace('-api.', '-app.').replace('api.', 'app.')
99
+ oauth_url = f'https://{oauth_domain}'
100
+ return oauth_url
101
+
102
+ def _construct_developer_hub_base_url(self) -> str:
103
+ """
104
+ Construct Developer Hub base URL based on api_client endpoint using dynamic text replacement.
105
+ Returns:
106
+ Developer Hub base URL string
107
+ """
108
+ if not self.api_client or not hasattr(self.api_client, 'endpoint'):
109
+ return self.DEVELOPER_HUB_BASE_URL
110
+
111
+ endpoint = self.api_client.endpoint
112
+ from urllib.parse import urlparse
113
+ parsed = urlparse(endpoint)
114
+ domain = parsed.netloc
115
+
116
+ if 'api.contentstack.io' in domain:
117
+ dev_hub_domain = domain.replace('api.contentstack.io', 'developerhub-api.contentstack.com')
118
+ else:
119
+ dev_hub_domain = domain.replace('-api.', '-developerhub-api.')
120
+ dev_hub_url = f'https://{dev_hub_domain}'
121
+ return dev_hub_url
122
+
123
+ def _generate_code_verifier(self, length: int = 128) -> str:
124
+ """
125
+ Generate a random code verifier for PKCE.
126
+ Returns:
127
+ Base64 URL-encoded code verifier
128
+ """
129
+ code_verifier = secrets.token_urlsafe(length)
130
+ return code_verifier
131
+
132
+ def _generate_code_challenge(self, code_verifier: str) -> str:
133
+ """
134
+ Generate code challenge from code verifier using SHA256.
135
+ Returns:
136
+ Base64 URL-encoded SHA256 hash of the code verifier
137
+ """
138
+ sha256_hash = hashlib.sha256(code_verifier.encode('utf-8')).digest()
139
+ import base64
140
+ code_challenge = base64.urlsafe_b64encode(sha256_hash).decode('utf-8').rstrip('=')
141
+ return code_challenge
142
+
143
+ def _get_headers(self) -> Dict[str, str]:
144
+ """Get common headers for OAuth requests."""
145
+ headers = {
146
+ "Content-Type": "application/x-www-form-urlencoded",
147
+ "Accept": "application/json"
148
+ }
149
+
150
+ if (self.api_client and hasattr(self.api_client, 'oauth') and
151
+ self.api_client.oauth.get('accessToken')):
152
+ headers["authorization"] = f"Bearer {self.api_client.oauth['accessToken']}"
153
+ return headers
154
+
155
+ def authorize(self) -> str:
156
+ """
157
+ Start the OAuth authorization flow.
158
+ Returns:
159
+ Authorization URL for user to visit
160
+ """
161
+ try:
162
+ print(f"OAuth Handler - authorize() called with app_id: {self.app_id}, client_id: {self.client_id}") # Debug
163
+ if not self._oauth_base_url:
164
+ raise ValueError(self.OAUTH_BASE_URL_NOT_SET_MSG)
165
+ oauth_base = self._oauth_base_url.rstrip('/')
166
+ base_url = f"{oauth_base}/#!/apps/{self.app_id}/authorize"
167
+ params = {
168
+ 'response_type': 'code', # Always use 'code'
169
+ 'client_id': self.client_id
170
+ }
171
+
172
+ if self.redirect_uri and self.redirect_uri.strip() and self.redirect_uri != 'None':
173
+ params['redirect_uri'] = self.redirect_uri
174
+
175
+ # Add PKCE parameters if using PKCE
176
+ if self.use_pkce:
177
+ if not self.code_challenge:
178
+ self.code_challenge = self._generate_code_challenge(self.code_verifier)
179
+ params['code_challenge'] = self.code_challenge
180
+ params['code_challenge_method'] = 'S256'
181
+
182
+ query_string = urllib.parse.urlencode(params)
183
+ final_url = f"{base_url}?{query_string}"
184
+ print(f"OAuth Handler - final authorization URL: {final_url}") # Debug
185
+ return final_url
186
+
187
+ except Exception as e:
188
+ raise ValueError(f"Error generating authorization URL: {e}")
189
+
190
+ def handle_redirect(self, redirect_url: str) -> Dict:
191
+ """
192
+ Handle the redirect from OAuth authorization server.
193
+ Returns:
194
+ Dictionary containing token information
195
+ """
196
+ parsed_url = urlparse(redirect_url)
197
+ query_params = parse_qs(parsed_url.query)
198
+ if "code" not in query_params:
199
+ raise ValueError(self.INVALID_AUTHORIZATION_CODE_MSG)
200
+ authorization_code = query_params["code"][0]
201
+ return self.exchange_code_for_token(authorization_code)
202
+
203
+ def exchange_code_for_token(self, authorization_code: str) -> Dict:
204
+ """
205
+ Exchange authorization code for access token.
206
+ Returns:
207
+ Dictionary containing token information
208
+ """
209
+ if not authorization_code or not authorization_code.strip():
210
+ raise ValueError("Authorization code cannot be empty")
211
+ data = {
212
+ "grant_type": "authorization_code",
213
+ "code": authorization_code.strip(),
214
+ "redirect_uri": self.redirect_uri,
215
+ "client_id": self.client_id,
216
+ "app_id": self.app_id
217
+ }
218
+
219
+ if self.client_secret:
220
+ data["client_secret"] = self.client_secret
221
+ else:
222
+ data["code_verifier"] = self.code_verifier
223
+
224
+ headers = self._get_headers()
225
+
226
+ try:
227
+ token_endpoint = f"{self._developer_hub_base_url}/token"
228
+ response = requests.post(
229
+ token_endpoint,
230
+ data=data,
231
+ headers=headers,
232
+ timeout=30
233
+ )
234
+ response.raise_for_status()
235
+ token_data = response.json()
236
+ self._save_tokens(token_data)
237
+ return token_data
238
+
239
+ except requests.RequestException as e:
240
+ raise requests.RequestException(f"{self.TOKEN_EXCHANGE_FAILED_MSG}: {str(e)}")
241
+
242
+ def _save_tokens(self, token_data: Dict):
243
+ """
244
+ Save tokens and related information.
245
+ """
246
+ # Store tokens in api_client.oauth
247
+ if self.api_client:
248
+ self.api_client.oauth["accessToken"] = token_data.get("access_token")
249
+ self.api_client.oauth["refreshToken"] = token_data.get("refresh_token") or self.api_client.oauth.get("refreshToken")
250
+ self.api_client.oauth["organizationUID"] = token_data.get("organization_uid")
251
+ self.api_client.oauth["userUID"] = token_data.get("user_uid")
252
+ expires_in = token_data.get("expires_in", 3600)
253
+ self.api_client.oauth["tokenExpiryTime"] = int(time.time() * 1000) + (expires_in - 60) * 1000 # Store expiry time in milliseconds
254
+
255
+ self._access_token = token_data.get("access_token")
256
+ self._refresh_token = token_data.get("refresh_token")
257
+ expires_in = token_data.get("expires_in", 3600) # Default 1 hour
258
+ self._token_expiry_time = time.time() + expires_in
259
+ self._organization_uid = token_data.get("organization_uid")
260
+ self._user_uid = token_data.get("user_uid")
261
+ if self.api_client and self._access_token:
262
+ self.api_client.headers["Authorization"] = f"Bearer {self._access_token}"
263
+
264
+ def get_valid_access_token(self) -> str:
265
+ """
266
+ Get valid access token, refreshing if necessary.
267
+ Returns:
268
+ Valid access token
269
+ """
270
+ if self.is_token_expired():
271
+ print(self.ACCESS_TOKEN_EXPIRED_MSG)
272
+ self.refresh_access_token()
273
+ access_token = self.get_access_token()
274
+ if not access_token:
275
+ raise ValueError(self.NO_ACCESS_TOKEN_MSG)
276
+ return access_token
277
+
278
+ def is_token_expired(self) -> bool:
279
+ """
280
+ Check if access token is expired.
281
+ Returns:
282
+ True if token is expired, False otherwise
283
+ """
284
+ if not self.api_client or not hasattr(self.api_client, 'oauth'):
285
+ return True
286
+
287
+ token_expiry_time = self.api_client.oauth.get('tokenExpiryTime')
288
+ if not token_expiry_time:
289
+ return True
290
+ if token_expiry_time > 1e10:
291
+ expiry_time = token_expiry_time / 1000
292
+ else:
293
+ expiry_time = token_expiry_time
294
+ return time.time() >= expiry_time
295
+
296
+ def refresh_access_token(self) -> str:
297
+ """
298
+ Refresh the access token using refresh token.
299
+ Returns:
300
+ New access token
301
+ """
302
+ if not self.api_client or not hasattr(self.api_client, 'oauth') or not self.api_client.oauth:
303
+ raise ValueError(self.NO_OAUTH_TOKENS_MSG)
304
+ refresh_token = self.api_client.oauth.get('refreshToken')
305
+ if not refresh_token:
306
+ raise ValueError(self.NO_REFRESH_TOKEN_MSG)
307
+ data = {
308
+ "grant_type": "refresh_token",
309
+ "refresh_token": refresh_token,
310
+ "client_id": self.client_id,
311
+ "app_id": self.app_id
312
+ }
313
+ if self.client_secret:
314
+ data["client_secret"] = self.client_secret
315
+ headers = self._get_headers()
316
+ try:
317
+ response = requests.post(
318
+ f"{self._developer_hub_base_url}/token",
319
+ data=data,
320
+ headers=headers,
321
+ timeout=30
322
+ )
323
+ response.raise_for_status()
324
+
325
+ token_data = response.json()
326
+ self._save_tokens(token_data)
327
+
328
+ return self._access_token
329
+ except requests.RequestException as e:
330
+ raise requests.RequestException(f"{self.TOKEN_REFRESH_FAILED_MSG}: {str(e)}")
331
+
332
+ def logout(self, revoke_authorization: bool = True) -> bool:
333
+ """
334
+ Logout and clear OAuth tokens.
335
+ Returns:
336
+ True if logout successful, False otherwise
337
+ """
338
+ try:
339
+ self._clear_tokens()
340
+ return True
341
+
342
+ except Exception:
343
+ self._clear_tokens()
344
+ return False
345
+
346
+ def _clear_tokens(self):
347
+ """Clear all stored tokens and related information."""
348
+ self._access_token = None
349
+ self._refresh_token = None
350
+ self._token_expiry_time = None
351
+ self._organization_uid = None
352
+ self._user_uid = None
353
+
354
+ if self.api_client and "Authorization" in self.api_client.headers:
355
+ del self.api_client.headers["Authorization"]
356
+
357
+ def get_access_token(self) -> Optional[str]:
358
+ """Get the current access token."""
359
+ return self._access_token
360
+
361
+ def set_access_token(self, token: str):
362
+ """Set the access token."""
363
+ self._access_token = token
364
+ if self.api_client:
365
+ self.api_client.headers["Authorization"] = f"Bearer {token}"
366
+ if not hasattr(self.api_client, 'oauth'):
367
+ self.api_client.oauth = {}
368
+ self.api_client.oauth['accessToken'] = token
369
+
370
+ def get_refresh_token(self) -> Optional[str]:
371
+ """Get the current refresh token."""
372
+ return self._refresh_token
373
+
374
+ def set_refresh_token(self, token: str):
375
+ """Set the refresh token."""
376
+ self._refresh_token = token
377
+ if self.api_client:
378
+ if not hasattr(self.api_client, 'oauth'):
379
+ self.api_client.oauth = {}
380
+ self.api_client.oauth['refreshToken'] = token
381
+
382
+ def get_organization_uid(self) -> Optional[str]:
383
+ """Get the organization UID."""
384
+ return self._organization_uid
385
+
386
+ def set_organization_uid(self, uid: str):
387
+ """Set the organization UID."""
388
+ self._organization_uid = uid
389
+
390
+ def get_user_uid(self) -> Optional[str]:
391
+ """Get the user UID."""
392
+ return self._user_uid
393
+
394
+ def set_user_uid(self, uid: str):
395
+ """Set the user UID."""
396
+ self._user_uid = uid
397
+
398
+ def get_token_expiry_time(self) -> Optional[float]:
399
+ """Get the token expiry time."""
400
+ return self._token_expiry_time
401
+
402
+ def set_token_expiry_time(self, expiry_time: float):
403
+ """Set the token expiry time."""
404
+ self._token_expiry_time = expiry_time
405
+ if self.api_client:
406
+ if not hasattr(self.api_client, 'oauth'):
407
+ self.api_client.oauth = {}
408
+ self.api_client.oauth['tokenExpiryTime'] = expiry_time
409
+
@@ -0,0 +1,187 @@
1
+ """
2
+ OAuth Interceptor for automatic token management and request handling.
3
+ """
4
+
5
+ import time
6
+ import threading
7
+ from typing import Dict, Any, Optional
8
+ import requests
9
+ from requests.adapters import HTTPAdapter
10
+ from urllib3.util.retry import Retry
11
+
12
+ class OAuthInterceptor:
13
+ """
14
+ OAuth interceptor that automatically handles token refresh and request retries.
15
+ """
16
+ MAX_RETRIES = 3
17
+ REFRESH_TIMEOUT = 30
18
+ TOKEN_ENDPOINT_PATH = "/token"
19
+ TOKEN_REFRESH_FAILED_MSG = "Token refresh failed"
20
+ NO_VALID_TOKENS_MSG = "OAuth: No valid tokens available"
21
+ TOKEN_REFRESH_FAILED_AFTER_401_MSG = "OAuth: Token refresh failed after 401"
22
+ # User agent strings
23
+ USER_AGENT = "contentstack-python-management-sdk"
24
+ X_USER_AGENT = "contentstack-python-management-sdk"
25
+
26
+ def __init__(self, oauth_handler):
27
+ """
28
+ Initialize the OAuth interceptor.
29
+ Args:
30
+ oauth_handler: The OAuthHandler instance
31
+ """
32
+ self.oauth_handler = oauth_handler
33
+ self.early_access = None
34
+ self.refresh_lock = threading.Lock()
35
+
36
+ def set_early_access(self, early_access: list):
37
+ """Set early access headers."""
38
+ self.early_access = early_access
39
+
40
+ def is_oauth_configured(self) -> bool:
41
+ """Check if OAuth is properly configured."""
42
+ return (self.oauth_handler is not None and
43
+ hasattr(self.oauth_handler, 'app_id') and
44
+ self.oauth_handler.app_id is not None)
45
+
46
+ def has_valid_tokens(self) -> bool:
47
+ """Check if we have valid (non-expired) tokens."""
48
+ if not self.oauth_handler or not hasattr(self.oauth_handler, 'api_client'):
49
+ return False
50
+
51
+ api_client = self.oauth_handler.api_client
52
+ if not hasattr(api_client, 'oauth') or not api_client.oauth:
53
+ return False
54
+ return not self.oauth_handler.is_token_expired()
55
+
56
+ def _get_default_headers(self, request_url: str) -> Dict[str, str]:
57
+ """
58
+ Get default headers for requests.
59
+ Returns:
60
+ Dictionary of headers
61
+ """
62
+ headers = {
63
+ "X-User-Agent": self.X_USER_AGENT,
64
+ "User-Agent": self.USER_AGENT,
65
+ "x-header-ea": ",".join(self.early_access) if self.early_access else "true"
66
+ }
67
+
68
+ if self.TOKEN_ENDPOINT_PATH in request_url:
69
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
70
+ else:
71
+ headers["Content-Type"] = "application/json"
72
+
73
+ return headers
74
+
75
+ def _add_auth_header(self, headers: Dict[str, str], request_url: str) -> Dict[str, str]:
76
+ """
77
+ Add authorization header if appropriate.
78
+ Returns:
79
+ Updated headers with authorization if needed
80
+ """
81
+ if self.TOKEN_ENDPOINT_PATH in request_url:
82
+ return headers
83
+ if (self.oauth_handler and
84
+ hasattr(self.oauth_handler, 'api_client') and
85
+ self.oauth_handler.api_client and
86
+ hasattr(self.oauth_handler.api_client, 'oauth') and
87
+ self.oauth_handler.api_client.oauth and
88
+ self.oauth_handler.api_client.oauth.get('accessToken')):
89
+
90
+ headers["Authorization"] = f"Bearer {self.oauth_handler.api_client.oauth['accessToken']}"
91
+ return headers
92
+
93
+ def _ensure_valid_token(self) -> bool:
94
+ """
95
+ Ensure we have a valid token, refreshing if necessary.
96
+ Returns:
97
+ True if we have a valid token, False otherwise
98
+ """
99
+ if not self.oauth_handler or not hasattr(self.oauth_handler, 'api_client'):
100
+ return False
101
+
102
+ api_client = self.oauth_handler.api_client
103
+ if not hasattr(api_client, 'oauth') or not api_client.oauth:
104
+ return False
105
+
106
+ # Check if token is expired and refresh if needed
107
+ if self.oauth_handler.is_token_expired():
108
+ with self.refresh_lock:
109
+ try:
110
+ if self.oauth_handler.is_token_expired():
111
+ self.oauth_handler.refresh_access_token()
112
+ return True
113
+ except Exception as e:
114
+ print(f"{self.TOKEN_REFRESH_FAILED_MSG}: {e}")
115
+ return False
116
+
117
+ return True
118
+
119
+ def execute_request(self, method: str, url: str, **kwargs) -> requests.Response:
120
+ """
121
+ Execute a request with OAuth handling and retry logic.
122
+ Returns:
123
+ Response object
124
+ """
125
+ if self.TOKEN_ENDPOINT_PATH in url:
126
+ return self._make_request(method, url, **kwargs)
127
+ if not self._ensure_valid_token():
128
+ raise requests.RequestException(self.NO_VALID_TOKENS_MSG)
129
+ return self._execute_with_retry(method, url, 0, **kwargs)
130
+
131
+ def _execute_with_retry(self, method: str, url: str, retry_count: int, **kwargs) -> requests.Response:
132
+ """
133
+ Execute request with retry logic.
134
+ Returns:
135
+ Response object
136
+ """
137
+ headers = self._get_default_headers(url)
138
+ headers = self._add_auth_header(headers, url)
139
+ if 'headers' in kwargs:
140
+ headers.update(kwargs['headers'])
141
+ kwargs['headers'] = headers
142
+
143
+ response = self._make_request(method, url, **kwargs)
144
+ if not response.ok and retry_count < self.MAX_RETRIES:
145
+ status_code = response.status_code
146
+
147
+ if (status_code == 401 and
148
+ self.oauth_handler and
149
+ hasattr(self.oauth_handler, 'api_client') and
150
+ self.oauth_handler.api_client and
151
+ hasattr(self.oauth_handler.api_client, 'oauth') and
152
+ self.oauth_handler.api_client.oauth and
153
+ self.oauth_handler.api_client.oauth.get('refreshToken')):
154
+
155
+ with self.refresh_lock:
156
+ try:
157
+ self.oauth_handler.refresh_access_token()
158
+ headers["Authorization"] = f"Bearer {self.oauth_handler.api_client.oauth['accessToken']}"
159
+ kwargs['headers'] = headers
160
+ return self._execute_with_retry(method, url, retry_count + 1, **kwargs)
161
+ except Exception as e:
162
+ raise requests.RequestException(f"{self.TOKEN_REFRESH_FAILED_AFTER_401_MSG}: {e}")
163
+
164
+ if status_code == 429 or (status_code >= 500 and status_code != 501):
165
+ # Calculate delay with exponential backoff
166
+ delay = min(1000 * (2 ** retry_count), 30000) / 1000 # Convert to seconds
167
+ time.sleep(delay)
168
+ return self._execute_with_retry(method, url, retry_count + 1, **kwargs)
169
+ return response
170
+
171
+ def _make_request(self, method: str, url: str, **kwargs) -> requests.Response:
172
+ """
173
+ Make the actual HTTP request.
174
+ Returns:
175
+ Response object
176
+ """
177
+ return requests.request(method, url, **kwargs)
178
+
179
+ def get_valid_access_token(self) -> Optional[str]:
180
+ """
181
+ Get a valid access token, refreshing if necessary.
182
+ Returns:
183
+ Valid access token or None if unavailable
184
+ """
185
+ if self._ensure_valid_token():
186
+ return self.oauth_handler.api_client.oauth.get('accessToken')
187
+ return None
@@ -63,7 +63,7 @@ class UserSession:
63
63
  }
64
64
 
65
65
  if tfa_token is not None:
66
- data["user"]["tf_token"] = tfa_token
66
+ data["user"]["tfa_token"] = tfa_token
67
67
 
68
68
  data = json.dumps(data)
69
69
  response = self.client.post(_path, headers=self.client.headers, data=data, json_data=None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack-management
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Contentstack API Client Library for Python
5
5
  Home-page: https://github.com/contentstack/contentstack-management-python
6
6
  Author: dev-ex
@@ -44,6 +44,9 @@ contentstack_management/management_token/__init__.py
44
44
  contentstack_management/management_token/management_token.py
45
45
  contentstack_management/metadata/__init__.py
46
46
  contentstack_management/metadata/metadata.py
47
+ contentstack_management/oauth/__init__.py
48
+ contentstack_management/oauth/oauth_handler.py
49
+ contentstack_management/oauth/oauth_interceptor.py
47
50
  contentstack_management/organizations/__init__.py
48
51
  contentstack_management/organizations/organization.py
49
52
  contentstack_management/publish_queue/__init__.py
@@ -1,98 +0,0 @@
1
- from enum import Enum
2
- from ._api_client import _APIClient
3
- from contentstack_management.organizations import organization
4
- from contentstack_management.stack import stack
5
- from contentstack_management.user_session import user_session
6
- from contentstack_management.users import user
7
-
8
- version = '0.0.1'
9
-
10
-
11
- class Region(Enum):
12
- US = "us"
13
- EU = "eu"
14
- AU = "au"
15
- AZURE_EU = "azure-eu"
16
- AZURE_NA = "azure-na"
17
- GCP_NA = "gcp-na"
18
- GCP_EU = "gcp-eu"
19
-
20
-
21
- def user_agents(headers=None):
22
- if headers is None:
23
- headers = {}
24
- local_headers = {'X-User-Agent': f'contentstack-management-python/v{version}',
25
- "Content-Type": "application/json"}
26
- headers.update(local_headers)
27
- return headers
28
-
29
-
30
- class Client:
31
-
32
- # TODO: DefaultCSCredential(), needs to be implemented
33
- def __init__(self, host: str = 'api.contentstack.io', scheme: str = 'https://',
34
- authtoken: str = None , management_token=None, headers: dict = None,
35
- region: Region = Region.US.value, version='v3', timeout=2, max_retries: int = 18, early_access: list = None,
36
- **kwargs):
37
- self.endpoint = 'https://api.contentstack.io/v3/'
38
- if region is not None and host is not None and region is not Region.US.value:
39
- self.endpoint = f'{scheme}{region}-{host}/{version}/'
40
- if region is not None and host is None and region is not Region.US.value:
41
- host = 'api.contentstack.com'
42
- self.endpoint = f'{scheme}{region}-{host}/{version}/'
43
- if host is not None and region is None:
44
- self.endpoint = f'{scheme}{host}/{version}/'
45
- if headers is None:
46
- headers = {}
47
- if early_access is not None:
48
- early_access_str = ', '.join(early_access)
49
- headers['x-header-ea'] = early_access_str
50
-
51
- if authtoken is not None:
52
- headers['authtoken'] = authtoken
53
-
54
- if management_token is not None:
55
- headers['authorization'] = management_token
56
- headers = user_agents(headers)
57
- self.client = _APIClient(endpoint=self.endpoint, headers=headers, timeout=timeout, max_retries=max_retries)
58
-
59
- """
60
- :param host: Optional hostname for the API endpoint.
61
- :param authtoken: Optional authentication token for API requests
62
- :param headers: Optional headers to be included with API requests
63
- :param authorization: Optional headers to be included in the api request
64
- :param region: optional region to be included in API requests,
65
- We have region support options for na, eu, azure-eu, azure-na
66
- :param scheme: optional scheme to be included in API requests
67
- :param version: optional version to be included in API request path url,
68
- :param timeout: Optional timeout value for API requests
69
- :param max_requests:Optional maximum number of requests to be made
70
- :param retry_on_error: Optional boolean value indicating whether to retry API requests on error.
71
- :return: A client object for performing API operations.
72
- -------------------------------
73
- [Example:]
74
-
75
- >>> import contentstack_management
76
- >>> contentstack_client = contentstack_management.Client()
77
- -------------------------------
78
- """
79
-
80
- def login(self, email: str, password: str, tfa_token: str = None):
81
- return user_session.UserSession(self.client).login(email, password, tfa_token)
82
- pass
83
-
84
- def logout(self):
85
- return user_session.UserSession(client=self.client).logout()
86
-
87
- @property
88
- def authtoken(self):
89
- return self.client.headers['authtoken']
90
-
91
- def user(self):
92
- return user.User(self.client)
93
-
94
- def organizations(self, organization_uid: str = None):
95
- return organization.Organization(self.client, organization_uid)
96
-
97
- def stack(self, api_key: str = None):
98
- return stack.Stack(self.client, api_key)