diff --git a/.gitignore b/.gitignore index 3030607..f31c87b 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,5 @@ tests/resources/.DS_Store tests/.DS_Store tests/resources/.DS_Store .DS_Store +*/data/regions.json .talismanrc \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4adea91..c56438a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ # CHANGELOG ## Content Management SDK For Python +--- +## v1.10.0 + +#### Date: 08 June 2026 + +- Dynamic region endpoint resolution via the Contentstack Regions Registry (`regions.json`). +- Added `Endpoint` class with 3-tier resolution: in-memory cache → bundled `data/regions.json` → live CDN download. +- Exposed `contentstack_management.get_contentstack_endpoint(region, service, omit_https)` module-level proxy. +- `Client` now resolves the `contentManagement` endpoint from the registry instead of a hardcoded host pattern. +- Added `scripts/download_regions.py` to refresh the bundled registry file. +- New regions and services require no SDK code changes — registry update is sufficient. + --- ## v1.9.0 diff --git a/contentstack_management/__init__.py b/contentstack_management/__init__.py index efadd82..ba7b064 100644 --- a/contentstack_management/__init__.py +++ b/contentstack_management/__init__.py @@ -18,6 +18,7 @@ from .entries.entry import Entry from .entry_variants.entry_variants import EntryVariants from .contentstack import Client, Region +from .endpoint import Endpoint from ._api_client import _APIClient from .common import Parameter from ._errors import ArgumentException @@ -41,6 +42,7 @@ __all__ = ( "Client", "Region", +"Endpoint", "_APIClient", "Parameter", "ArgumentException", @@ -78,11 +80,27 @@ "OAuthInterceptor" ) +def get_contentstack_endpoint(region='us', service='', omit_https=False): + """ + Resolve a Contentstack service endpoint URL for a given region. + + Proxy to :class:`Endpoint.get_contentstack_endpoint` for convenience — + mirrors ``Contentstack::getContentstackEndpoint()`` in the PHP SDK. + + :param region: Region ID or alias ('us', 'eu', 'azure-na', 'gcp-eu', ...). + :param service: Service key ('contentDelivery', 'contentManagement', ...). + When empty, returns a dict of all endpoints for the region. + :param omit_https: When True, strips 'https://' from the returned URL(s). + :returns: str when service is provided, dict[str,str] otherwise. + """ + return Endpoint.get_contentstack_endpoint(region, service, omit_https) + + __title__ = 'contentstack-management-python' __author__ = 'dev-ex' __status__ = 'debug' __region__ = 'na' -__version__ = '1.9.0' +__version__ = '1.10.0' __host__ = 'api.contentstack.io' __protocol__ = 'https://' __api_version__ = 'v3' diff --git a/contentstack_management/contentstack.py b/contentstack_management/contentstack.py index 09192cf..2ad6198 100644 --- a/contentstack_management/contentstack.py +++ b/contentstack_management/contentstack.py @@ -2,6 +2,7 @@ import os import pyotp from ._api_client import _APIClient +from .endpoint import Endpoint from contentstack_management.organizations import organization from contentstack_management.stack import stack from contentstack_management.user_session import user_session @@ -37,16 +38,25 @@ 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, oauth_config: dict = None, **kwargs): - self.endpoint = 'https://api.contentstack.io/v3/' - - if region is not None and region is not Region.US.value: - if host is not None and host != 'api.contentstack.io': + _DEFAULT_HOST = 'api.contentstack.io' + self.endpoint = f'{scheme}{_DEFAULT_HOST}/{version}/' + + if host is None or host == _DEFAULT_HOST: + # No custom host — resolve via Endpoint (regions.json-driven) + try: + base = Endpoint.get_contentstack_endpoint( + region or 'us', 'contentManagement', omit_https=True) + self.endpoint = f'{scheme}{base}/{version}/' + except (ValueError, RuntimeError): + # Unknown/custom region string — fall back to legacy pattern + if region and region != Region.US.value: + self.endpoint = f'{scheme}{region}-api.contentstack.com/{version}/' + else: + # Explicit custom host always wins; apply region prefix when non-US + if region and region != Region.US.value: self.endpoint = f'{scheme}{region}-api.{host}/{version}/' else: - host = 'api.contentstack.com' - self.endpoint = f'{scheme}{region}-{host}/{version}/' - elif host is not None and host != 'api.contentstack.io': - self.endpoint = f'{scheme}{host}/{version}/' + self.endpoint = f'{scheme}{host}/{version}/' if headers is None: headers = {} if early_access is not None: diff --git a/contentstack_management/endpoint.py b/contentstack_management/endpoint.py new file mode 100644 index 0000000..cf2801f --- /dev/null +++ b/contentstack_management/endpoint.py @@ -0,0 +1,180 @@ +""" +Endpoint — Contentstack region-to-URL resolver for the Management SDK. + +Resolves Contentstack service endpoint URLs for any supported region. +Region data is loaded from contentstack_management/data/regions.json (bundled) +and cached in-memory for the lifetime of the process. When the bundled file is +absent the class attempts a live download from the Contentstack CDN so the +SDK continues to work even when the file was not created during installation. +""" + +import json +import os +import re + +REGIONS_URL = 'https://artifacts.contentstack.com/regions.json' + + +class Endpoint: + """ + Resolves Contentstack service endpoint URLs for any supported region. + + Usage:: + + from contentstack_management.endpoint import Endpoint + + # Single service URL + url = Endpoint.get_contentstack_endpoint('eu', 'contentManagement') + # 'https://eu-api.contentstack.com' + + # All services for a region + endpoints = Endpoint.get_contentstack_endpoint('azure-na') + # {'contentDelivery': '...', 'contentManagement': '...', ...} + + # Strip scheme (useful when building endpoint strings manually) + host = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentManagement', omit_https=True) + # 'gcp-eu-api.contentstack.com' + """ + + _regions_data = None # in-memory cache — shared across all instances + + @staticmethod + def get_contentstack_endpoint(region='us', service='', omit_https=False): + """ + Resolve a Contentstack service endpoint URL for a given region. + + :param region: Region ID or alias ('us', 'eu', 'azure-na', 'gcp-eu', etc.). + Defaults to 'us' (AWS North America). + :param service: Service key ('contentDelivery', 'contentManagement', ...). + When empty, returns a dict of all endpoints for the region. + :param omit_https: When True, strips 'https://' prefix from returned URL(s). + :returns: str when service is provided, dict[str,str] otherwise. + :raises ValueError: When region is empty, unknown, or service is not found. + :raises RuntimeError: When regions.json cannot be read or parsed. + """ + if not region: + raise ValueError('Empty region provided. Please put valid region.') + + data = Endpoint._load_regions() + normalized = region.strip().lower() + region_row = Endpoint._find_region(data['regions'], normalized) + + if region_row is None: + raise ValueError(f'Invalid region: {region}') + + if service: + if service not in region_row['endpoints']: + raise ValueError( + f'Service "{service}" not found for region "{region_row["id"]}"' + ) + url = region_row['endpoints'][service] + return Endpoint._strip_https(url) if omit_https else url + + endpoints = region_row['endpoints'] + if omit_https: + return {k: Endpoint._strip_https(v) for k, v in endpoints.items()} + return dict(endpoints) + + @staticmethod + def _load_regions(): + """ + Load and cache regions.json. + + Resolution order: + 1. In-memory static cache (zero I/O after first call) + 2. contentstack_management/data/regions.json on disk (written by download script) + 3. Live download from artifacts.contentstack.com (fallback) + """ + if Endpoint._regions_data is not None: + return Endpoint._regions_data + + data_dir = os.path.join(os.path.dirname(__file__), 'data') + path = os.path.join(data_dir, 'regions.json') + + if not os.path.exists(path): + Endpoint._download_and_save(path) + + if not os.path.exists(path): + raise RuntimeError( + 'contentstack-management: regions.json not found and could not be downloaded. ' + 'Run "python scripts/download_regions.py" and ensure network access.' + ) + + try: + with open(path, 'r', encoding='utf-8') as f: + decoded = json.load(f) + except (OSError, json.JSONDecodeError) as exc: + raise RuntimeError( + f'contentstack-management: Could not read or parse regions.json: {exc}. ' + 'Run "python scripts/download_regions.py" to re-download it.' + ) from exc + + if not isinstance(decoded, dict) or 'regions' not in decoded: + raise RuntimeError( + 'contentstack-management: regions.json is corrupt. ' + 'Run "python scripts/download_regions.py" to re-download it.' + ) + + Endpoint._regions_data = decoded + return Endpoint._regions_data + + @staticmethod + def _download_and_save(dest): + """ + Download regions.json from the Contentstack CDN and save to disk. + Uses the requests library (already an SDK dependency). + Silent on failure — the caller decides whether a missing file is fatal. + + :param dest: Absolute path to write the file to. + """ + os.makedirs(os.path.dirname(dest), exist_ok=True) + + try: + import requests + response = requests.get(REGIONS_URL, timeout=30) + response.raise_for_status() + data = response.text + except Exception: # noqa: BLE001 + return + + try: + decoded = json.loads(data) + except json.JSONDecodeError: + return + + if isinstance(decoded, dict) and 'regions' in decoded: + try: + with open(dest, 'w', encoding='utf-8') as f: + f.write(data) + except OSError: + pass + + @staticmethod + def _find_region(regions, input_str): + """ + Find a region entry by its id or any alias (case-insensitive). + + Two-pass: exact id match first, then alias[] scan — mirrors PHP implementation. + + :param regions: list of region dicts from regions.json + :param input_str: already-lowercased input + :returns: region dict or None + """ + for row in regions: + if row['id'] == input_str: + return row + for row in regions: + for alias in row.get('alias', []): + if alias.lower() == input_str: + return row + return None + + @staticmethod + def _strip_https(url): + """Strip the https:// (or http://) scheme from a URL string.""" + return re.sub(r'^https?://', '', url) + + @staticmethod + def reset_cache(): + """Reset the internal region cache. Intended for testing only.""" + Endpoint._regions_data = None diff --git a/scripts/download_regions.py b/scripts/download_regions.py new file mode 100644 index 0000000..3a78301 --- /dev/null +++ b/scripts/download_regions.py @@ -0,0 +1,68 @@ +""" +Downloads the Contentstack regions registry from the official source and +saves it to contentstack_management/data/regions.json. + +Run manually: + python3 scripts/download_regions.py +""" + +import json +import os +import sys +import requests + +REGIONS_URL = 'https://artifacts.contentstack.com/regions.json' + +DEST = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'contentstack_management', 'data', 'regions.json' +) + + +def download(): + dest_dir = os.path.dirname(DEST) + os.makedirs(dest_dir, exist_ok=True) + + print(f'contentstack-management: Downloading regions.json from {REGIONS_URL} ...') + + try: + response = requests.get(REGIONS_URL, timeout=30) + response.raise_for_status() + data = response.text + except Exception as exc: + sys.stderr.write( + f'contentstack-management: Warning — could not download regions.json: {exc}. ' + 'The SDK will attempt to download it at runtime on first use.\n' + ) + sys.exit(0) + + try: + decoded = json.loads(data) + except json.JSONDecodeError: + sys.stderr.write( + 'contentstack-management: Warning — downloaded data is not valid JSON.\n' + ) + sys.exit(0) + + if not isinstance(decoded, dict) or 'regions' not in decoded or \ + not isinstance(decoded['regions'], list): + sys.stderr.write( + 'contentstack-management: Warning — downloaded data is not a valid regions.json.\n' + ) + sys.exit(0) + + try: + with open(DEST, 'w', encoding='utf-8') as f: + f.write(data) + except OSError as exc: + sys.stderr.write( + f'contentstack-management: Warning — could not write regions.json to {DEST}: {exc}\n' + ) + sys.exit(0) + + region_count = len(decoded['regions']) + print(f'contentstack-management: regions.json downloaded ({region_count} regions) → {DEST}') + + +if __name__ == '__main__': + download() diff --git a/setup.py b/setup.py index e7ac797..684be4c 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ def get_author_email(package): name="contentstack-management", version=get_version(package), packages=find_packages(exclude=['tests']), + package_data={'contentstack_management': ['data/regions.json']}, py_modules=['_api_client', 'contentstack','common','_errors','_constant'], description="Contentstack API Client Library for Python", long_description=long_description, diff --git a/tests/unit/contentstack/test_endpoint.py b/tests/unit/contentstack/test_endpoint.py new file mode 100644 index 0000000..2851d3e --- /dev/null +++ b/tests/unit/contentstack/test_endpoint.py @@ -0,0 +1,298 @@ +import unittest + +import contentstack_management +from contentstack_management.endpoint import Endpoint +from contentstack_management.contentstack import Region, Client + +AUTHTOKEN = 'test_authtoken' + + +class TestEndpoint(unittest.TestCase): + + def setUp(self): + Endpoint.reset_cache() + + # ------------------------------------------------------------------------- + # Default region (us / na) + # ------------------------------------------------------------------------- + + def test_default_region_returns_all_endpoints(self): + endpoints = Endpoint.get_contentstack_endpoint() + self.assertIsInstance(endpoints, dict) + self.assertIn('contentDelivery', endpoints) + self.assertIn('contentManagement', endpoints) + + def test_default_region_content_management(self): + url = Endpoint.get_contentstack_endpoint('us', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_default_region_content_delivery(self): + url = Endpoint.get_contentstack_endpoint('us', 'contentDelivery') + self.assertEqual('https://cdn.contentstack.io', url) + + # ------------------------------------------------------------------------- + # All 7 regions — contentManagement spot-checks (primary service for Mgmt SDK) + # ------------------------------------------------------------------------- + + def test_content_management_na(self): + url = Endpoint.get_contentstack_endpoint('na', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_content_management_eu(self): + url = Endpoint.get_contentstack_endpoint('eu', 'contentManagement') + self.assertEqual('https://eu-api.contentstack.com', url) + + def test_content_management_au(self): + url = Endpoint.get_contentstack_endpoint('au', 'contentManagement') + self.assertEqual('https://au-api.contentstack.com', url) + + def test_content_management_azure_na(self): + url = Endpoint.get_contentstack_endpoint('azure-na', 'contentManagement') + self.assertEqual('https://azure-na-api.contentstack.com', url) + + def test_content_management_azure_eu(self): + url = Endpoint.get_contentstack_endpoint('azure-eu', 'contentManagement') + self.assertEqual('https://azure-eu-api.contentstack.com', url) + + def test_content_management_gcp_na(self): + url = Endpoint.get_contentstack_endpoint('gcp-na', 'contentManagement') + self.assertEqual('https://gcp-na-api.contentstack.com', url) + + def test_content_management_gcp_eu(self): + url = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentManagement') + self.assertEqual('https://gcp-eu-api.contentstack.com', url) + + # ------------------------------------------------------------------------- + # All 7 regions — contentDelivery spot-checks + # ------------------------------------------------------------------------- + + def test_content_delivery_na(self): + url = Endpoint.get_contentstack_endpoint('na', 'contentDelivery') + self.assertEqual('https://cdn.contentstack.io', url) + + def test_content_delivery_eu(self): + url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery') + self.assertEqual('https://eu-cdn.contentstack.com', url) + + def test_content_delivery_au(self): + url = Endpoint.get_contentstack_endpoint('au', 'contentDelivery') + self.assertEqual('https://au-cdn.contentstack.com', url) + + def test_content_delivery_azure_na(self): + url = Endpoint.get_contentstack_endpoint('azure-na', 'contentDelivery') + self.assertEqual('https://azure-na-cdn.contentstack.com', url) + + def test_content_delivery_azure_eu(self): + url = Endpoint.get_contentstack_endpoint('azure-eu', 'contentDelivery') + self.assertEqual('https://azure-eu-cdn.contentstack.com', url) + + def test_content_delivery_gcp_na(self): + url = Endpoint.get_contentstack_endpoint('gcp-na', 'contentDelivery') + self.assertEqual('https://gcp-na-cdn.contentstack.com', url) + + def test_content_delivery_gcp_eu(self): + url = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentDelivery') + self.assertEqual('https://gcp-eu-cdn.contentstack.com', url) + + # ------------------------------------------------------------------------- + # NA aliases all resolve to the same endpoint + # ------------------------------------------------------------------------- + + def test_alias_na(self): + url = Endpoint.get_contentstack_endpoint('na', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_alias_us(self): + url = Endpoint.get_contentstack_endpoint('us', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_alias_aws_na_hyphen(self): + url = Endpoint.get_contentstack_endpoint('aws-na', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_alias_aws_na_underscore(self): + url = Endpoint.get_contentstack_endpoint('aws_na', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_alias_na_uppercase(self): + url = Endpoint.get_contentstack_endpoint('NA', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_alias_us_uppercase(self): + url = Endpoint.get_contentstack_endpoint('US', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + # ------------------------------------------------------------------------- + # Case-insensitive alias matching for other regions + # ------------------------------------------------------------------------- + + def test_alias_aws_na_uppercase(self): + url = Endpoint.get_contentstack_endpoint('AWS-NA', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_alias_azure_na_underscore(self): + url = Endpoint.get_contentstack_endpoint('azure_na', 'contentManagement') + self.assertEqual('https://azure-na-api.contentstack.com', url) + + def test_alias_gcp_eu_underscore(self): + url = Endpoint.get_contentstack_endpoint('gcp_eu', 'contentManagement') + self.assertEqual('https://gcp-eu-api.contentstack.com', url) + + # ------------------------------------------------------------------------- + # Region enum constants resolve correctly + # ------------------------------------------------------------------------- + + def test_region_constant_us(self): + url = Endpoint.get_contentstack_endpoint(Region.US.value, 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_region_constant_eu(self): + url = Endpoint.get_contentstack_endpoint(Region.EU.value, 'contentManagement') + self.assertEqual('https://eu-api.contentstack.com', url) + + def test_region_constant_au(self): + url = Endpoint.get_contentstack_endpoint(Region.AU.value, 'contentManagement') + self.assertEqual('https://au-api.contentstack.com', url) + + def test_region_constant_azure_na(self): + url = Endpoint.get_contentstack_endpoint(Region.AZURE_NA.value, 'contentManagement') + self.assertEqual('https://azure-na-api.contentstack.com', url) + + def test_region_constant_azure_eu(self): + url = Endpoint.get_contentstack_endpoint(Region.AZURE_EU.value, 'contentManagement') + self.assertEqual('https://azure-eu-api.contentstack.com', url) + + def test_region_constant_gcp_na(self): + url = Endpoint.get_contentstack_endpoint(Region.GCP_NA.value, 'contentManagement') + self.assertEqual('https://gcp-na-api.contentstack.com', url) + + def test_region_constant_gcp_eu(self): + url = Endpoint.get_contentstack_endpoint(Region.GCP_EU.value, 'contentManagement') + self.assertEqual('https://gcp-eu-api.contentstack.com', url) + + # ------------------------------------------------------------------------- + # omit_https flag + # ------------------------------------------------------------------------- + + def test_omit_https_strips_scheme_single_service(self): + host = Endpoint.get_contentstack_endpoint('eu', 'contentManagement', omit_https=True) + self.assertEqual('eu-api.contentstack.com', host) + + def test_omit_https_strips_scheme_all_services(self): + endpoints = Endpoint.get_contentstack_endpoint('na', omit_https=True) + self.assertIsInstance(endpoints, dict) + for key, url in endpoints.items(): + self.assertNotIn('https://', url, f'Service {key} still has https://') + self.assertNotIn('http://', url, f'Service {key} still has http://') + + def test_omit_https_false_retains_scheme(self): + url = Endpoint.get_contentstack_endpoint('na', 'contentManagement', omit_https=False) + self.assertTrue(url.startswith('https://')) + + # ------------------------------------------------------------------------- + # No service — returns full dict + # ------------------------------------------------------------------------- + + def test_no_service_returns_dict(self): + result = Endpoint.get_contentstack_endpoint('au') + self.assertIsInstance(result, dict) + self.assertGreater(len(result), 1) + + def test_no_service_contains_correct_urls(self): + endpoints = Endpoint.get_contentstack_endpoint('au') + self.assertEqual('https://au-cdn.contentstack.com', endpoints['contentDelivery']) + self.assertEqual('https://au-api.contentstack.com', endpoints['contentManagement']) + + # ------------------------------------------------------------------------- + # Error cases + # ------------------------------------------------------------------------- + + def test_empty_region_raises_value_error(self): + with self.assertRaises(ValueError) as ctx: + Endpoint.get_contentstack_endpoint('') + self.assertIn('Empty region', str(ctx.exception)) + + def test_unknown_region_raises_value_error(self): + with self.assertRaises(ValueError) as ctx: + Endpoint.get_contentstack_endpoint('invalid-region') + self.assertIn('Invalid region', str(ctx.exception)) + + def test_unknown_service_raises_value_error(self): + with self.assertRaises(ValueError) as ctx: + Endpoint.get_contentstack_endpoint('na', 'unknownService') + self.assertIn('unknownService', str(ctx.exception)) + + # ------------------------------------------------------------------------- + # contentstack_management.get_contentstack_endpoint() module-level proxy + # ------------------------------------------------------------------------- + + def test_module_proxy_returns_same_result(self): + via_class = Endpoint.get_contentstack_endpoint('eu', 'contentManagement') + via_module = contentstack_management.get_contentstack_endpoint('eu', 'contentManagement') + self.assertEqual(via_class, via_module) + + def test_module_proxy_default_region(self): + url = contentstack_management.get_contentstack_endpoint('us', 'contentManagement') + self.assertEqual('https://api.contentstack.io', url) + + def test_module_proxy_omit_https(self): + host = contentstack_management.get_contentstack_endpoint( + 'gcp-na', 'contentManagement', omit_https=True) + self.assertEqual('gcp-na-api.contentstack.com', host) + + def test_module_proxy_all_endpoints(self): + endpoints = contentstack_management.get_contentstack_endpoint('azure-eu') + self.assertIsInstance(endpoints, dict) + self.assertIn('contentManagement', endpoints) + + # ------------------------------------------------------------------------- + # Client endpoint resolution via Endpoint + # ------------------------------------------------------------------------- + + def test_client_us_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region=Region.US.value) + self.assertEqual('https://api.contentstack.io/v3/', client.endpoint) + + def test_client_eu_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region='eu') + self.assertEqual('https://eu-api.contentstack.com/v3/', client.endpoint) + + def test_client_au_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region='au') + self.assertEqual('https://au-api.contentstack.com/v3/', client.endpoint) + + def test_client_azure_na_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region='azure-na') + self.assertEqual('https://azure-na-api.contentstack.com/v3/', client.endpoint) + + def test_client_azure_eu_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region='azure-eu') + self.assertEqual('https://azure-eu-api.contentstack.com/v3/', client.endpoint) + + def test_client_gcp_na_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region='gcp-na') + self.assertEqual('https://gcp-na-api.contentstack.com/v3/', client.endpoint) + + def test_client_gcp_eu_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region='gcp-eu') + self.assertEqual('https://gcp-eu-api.contentstack.com/v3/', client.endpoint) + + def test_client_region_enum_value_au(self): + client = Client(authtoken=AUTHTOKEN, region=Region.AU.value) + self.assertEqual('https://au-api.contentstack.com/v3/', client.endpoint) + + def test_client_custom_host_overrides_endpoint(self): + client = Client(authtoken=AUTHTOKEN, region='au', host='example.com') + self.assertEqual('https://au-api.example.com/v3/', client.endpoint) + + def test_client_custom_host_us_region(self): + client = Client(authtoken=AUTHTOKEN, host='custom.contentstack.io') + self.assertEqual('https://custom.contentstack.io/v3/', client.endpoint) + + def test_client_default_us_no_region(self): + client = Client(authtoken=AUTHTOKEN) + self.assertEqual('https://api.contentstack.io/v3/', client.endpoint) + + +if __name__ == '__main__': + unittest.main()