Contentstack 2.5.0__tar.gz → 2.6.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 (43) hide show
  1. {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/PKG-INFO +1 -1
  2. {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/SOURCES.txt +3 -0
  3. {contentstack-2.5.0 → contentstack-2.6.0}/PKG-INFO +1 -1
  4. contentstack-2.6.0/contentstack/__init__.py +49 -0
  5. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/contenttype.py +1 -0
  6. contentstack-2.6.0/contentstack/endpoint.py +180 -0
  7. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/entry.py +1 -0
  8. contentstack-2.6.0/contentstack/region_refresh.py +81 -0
  9. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/stack.py +18 -16
  10. {contentstack-2.5.0 → contentstack-2.6.0}/setup.py +16 -0
  11. contentstack-2.6.0/tests/test_endpoint.py +287 -0
  12. contentstack-2.5.0/contentstack/__init__.py +0 -29
  13. {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/dependency_links.txt +0 -0
  14. {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/not-zip-safe +0 -0
  15. {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/requires.txt +0 -0
  16. {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/top_level.txt +0 -0
  17. {contentstack-2.5.0 → contentstack-2.6.0}/LICENSE +0 -0
  18. {contentstack-2.5.0 → contentstack-2.6.0}/README.md +0 -0
  19. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/asset.py +0 -0
  20. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/assetquery.py +0 -0
  21. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/basequery.py +0 -0
  22. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/controller.py +0 -0
  23. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/deep_merge_lp.py +0 -0
  24. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/entryqueryable.py +0 -0
  25. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/error_messages.py +0 -0
  26. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/globalfields.py +0 -0
  27. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/https_connection.py +0 -0
  28. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/image_transform.py +0 -0
  29. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/query.py +0 -0
  30. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/taxonomy.py +0 -0
  31. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/utility.py +0 -0
  32. {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/variants.py +0 -0
  33. {contentstack-2.5.0 → contentstack-2.6.0}/setup.cfg +0 -0
  34. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_assets.py +0 -0
  35. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_basequery.py +0 -0
  36. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_early_fetch.py +0 -0
  37. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_early_find.py +0 -0
  38. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_entry.py +0 -0
  39. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_global_fields.py +0 -0
  40. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_live_preview.py +0 -0
  41. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_query.py +0 -0
  42. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_stack.py +0 -0
  43. {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_taxonomies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Contentstack
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Contentstack is a headless CMS with an API-first approach.
5
5
  Home-page: https://github.com/contentstack/contentstack-python
6
6
  Author: Contentstack
@@ -14,6 +14,7 @@ contentstack/basequery.py
14
14
  contentstack/contenttype.py
15
15
  contentstack/controller.py
16
16
  contentstack/deep_merge_lp.py
17
+ contentstack/endpoint.py
17
18
  contentstack/entry.py
18
19
  contentstack/entryqueryable.py
19
20
  contentstack/error_messages.py
@@ -21,6 +22,7 @@ contentstack/globalfields.py
21
22
  contentstack/https_connection.py
22
23
  contentstack/image_transform.py
23
24
  contentstack/query.py
25
+ contentstack/region_refresh.py
24
26
  contentstack/stack.py
25
27
  contentstack/taxonomy.py
26
28
  contentstack/utility.py
@@ -29,6 +31,7 @@ tests/test_assets.py
29
31
  tests/test_basequery.py
30
32
  tests/test_early_fetch.py
31
33
  tests/test_early_find.py
34
+ tests/test_endpoint.py
32
35
  tests/test_entry.py
33
36
  tests/test_global_fields.py
34
37
  tests/test_live_preview.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Contentstack
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Contentstack is a headless CMS with an API-first approach.
5
5
  Home-page: https://github.com/contentstack/contentstack-python
6
6
  Author: Contentstack
@@ -0,0 +1,49 @@
1
+ """
2
+ The __init__.py files are required to make Python treat the directories as containing
3
+ packages; this is done to prevent directories with a common name, such as string,
4
+ from unintentionally hiding valid modules that occur later on the module search path
5
+ """
6
+ from .entry import Entry
7
+ from .asset import Asset
8
+ from .contenttype import ContentType
9
+ from .endpoint import Endpoint
10
+ from .https_connection import HTTPSConnection
11
+ from contentstack.stack import Stack
12
+ from .utility import Utils
13
+ from .region_refresh import refresh_regions
14
+
15
+ __all__ = (
16
+ "Entry",
17
+ "Asset",
18
+ "ContentType",
19
+ "Endpoint",
20
+ "HTTPSConnection",
21
+ "Stack",
22
+ "Utils",
23
+ "refresh_regions",
24
+ )
25
+
26
+
27
+ def get_contentstack_endpoint(region='us', service='', omit_https=False):
28
+ """
29
+ Resolve a Contentstack service endpoint URL for a given region.
30
+
31
+ Proxy to :class:`Endpoint.get_contentstack_endpoint` for convenience —
32
+ mirrors ``Contentstack::getContentstackEndpoint()`` in the PHP SDK.
33
+
34
+ :param region: Region ID or alias ('us', 'eu', 'azure-na', 'gcp-eu', ...).
35
+ :param service: Service key ('contentDelivery', 'contentManagement', ...).
36
+ When empty, returns a dict of all endpoints for the region.
37
+ :param omit_https: When True, strips 'https://' from the returned URL(s).
38
+ :returns: str when service is provided, dict[str,str] otherwise.
39
+ """
40
+ return Endpoint.get_contentstack_endpoint(region, service, omit_https)
41
+
42
+ __title__ = 'contentstack-delivery-python'
43
+ __author__ = 'contentstack'
44
+ __status__ = 'debug'
45
+ __version__ = 'v2.6.0'
46
+ __endpoint__ = 'cdn.contentstack.io'
47
+ __email__ = 'support@contentstack.com'
48
+ __developer_email__ = 'mobile@contentstack.com'
49
+ __license__ = "MIT"
@@ -7,6 +7,7 @@ content type.
7
7
 
8
8
  # ************* Module ContentType **************
9
9
  # Your code has been rated at 10.00/10 by pylint
10
+ from __future__ import annotations
10
11
  import json
11
12
  import logging
12
13
  from urllib import parse
@@ -0,0 +1,180 @@
1
+ """
2
+ Endpoint — Contentstack region-to-URL resolver.
3
+
4
+ Resolves Contentstack service endpoint URLs for any supported region.
5
+ Region data is loaded from contentstack/assets/regions.json (bundled) and
6
+ cached in-memory for the lifetime of the process. When the bundled file is
7
+ absent the class attempts a live download from the Contentstack CDN so the
8
+ SDK continues to work even when the file was not created during installation.
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import re
14
+
15
+ REGIONS_URL = 'https://artifacts.contentstack.com/regions.json'
16
+
17
+
18
+ class Endpoint:
19
+ """
20
+ Resolves Contentstack service endpoint URLs for any supported region.
21
+
22
+ Usage::
23
+
24
+ from contentstack.endpoint import Endpoint
25
+
26
+ # Single service URL
27
+ url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery')
28
+ # 'https://eu-cdn.contentstack.com'
29
+
30
+ # All services for a region
31
+ endpoints = Endpoint.get_contentstack_endpoint('azure-na')
32
+ # {'contentDelivery': 'https://...', 'contentManagement': 'https://...', ...}
33
+
34
+ # Strip scheme (useful when setting host directly)
35
+ host = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentDelivery', omit_https=True)
36
+ # 'gcp-eu-cdn.contentstack.com'
37
+ """
38
+
39
+ _regions_data = None # in-memory cache — shared across all instances
40
+
41
+ @staticmethod
42
+ def get_contentstack_endpoint(region='us', service='', omit_https=False):
43
+ """
44
+ Resolve a Contentstack service endpoint URL for a given region.
45
+
46
+ :param region: Region ID or alias ('us', 'eu', 'azure-na', 'gcp-eu', etc.).
47
+ Defaults to 'us' (AWS North America).
48
+ :param service: Service key ('contentDelivery', 'contentManagement', ...).
49
+ When empty, returns a dict of all endpoints for the region.
50
+ :param omit_https: When True, strips 'https://' prefix from returned URL(s).
51
+ :returns: str when service is provided, dict[str,str] otherwise.
52
+ :raises ValueError: When region is empty, unknown, or service is not found.
53
+ :raises RuntimeError: When regions.json cannot be read or parsed.
54
+ """
55
+ if not region:
56
+ raise ValueError('Empty region provided. Please put valid region.')
57
+
58
+ data = Endpoint._load_regions()
59
+ normalized = region.strip().lower()
60
+ region_row = Endpoint._find_region(data['regions'], normalized)
61
+
62
+ if region_row is None:
63
+ raise ValueError(f'Invalid region: {region}')
64
+
65
+ if service:
66
+ if service not in region_row['endpoints']:
67
+ raise ValueError(
68
+ f'Service "{service}" not found for region "{region_row["id"]}"'
69
+ )
70
+ url = region_row['endpoints'][service]
71
+ return Endpoint._strip_https(url) if omit_https else url
72
+
73
+ endpoints = region_row['endpoints']
74
+ if omit_https:
75
+ return {k: Endpoint._strip_https(v) for k, v in endpoints.items()}
76
+ return dict(endpoints)
77
+
78
+ @staticmethod
79
+ def _load_regions():
80
+ """
81
+ Load and cache regions.json.
82
+
83
+ Resolution order:
84
+ 1. In-memory static cache (zero I/O after first call)
85
+ 2. contentstack/assets/regions.json on disk (written by download script)
86
+ 3. Live download from artifacts.contentstack.com (fallback)
87
+ """
88
+ if Endpoint._regions_data is not None:
89
+ return Endpoint._regions_data
90
+
91
+ assets_dir = os.path.join(os.path.dirname(__file__), 'assets')
92
+ path = os.path.join(assets_dir, 'regions.json')
93
+
94
+ if not os.path.exists(path):
95
+ Endpoint._download_and_save(path)
96
+
97
+ if not os.path.exists(path):
98
+ raise RuntimeError(
99
+ 'contentstack: regions.json not found and could not be downloaded. '
100
+ 'Run "python scripts/download_regions.py" and ensure network access.'
101
+ )
102
+
103
+ try:
104
+ with open(path, 'r', encoding='utf-8') as f:
105
+ decoded = json.load(f)
106
+ except (OSError, json.JSONDecodeError) as exc:
107
+ raise RuntimeError(
108
+ f'contentstack: Could not read or parse regions.json: {exc}. '
109
+ 'Run "python scripts/download_regions.py" to re-download it.'
110
+ ) from exc
111
+
112
+ if not isinstance(decoded, dict) or 'regions' not in decoded:
113
+ raise RuntimeError(
114
+ 'contentstack: regions.json is corrupt. '
115
+ 'Run "python scripts/download_regions.py" to re-download it.'
116
+ )
117
+
118
+ Endpoint._regions_data = decoded
119
+ return Endpoint._regions_data
120
+
121
+ @staticmethod
122
+ def _download_and_save(dest):
123
+ """
124
+ Download regions.json from the Contentstack CDN and save to disk.
125
+ Uses the requests library (already an SDK dependency).
126
+ Silent on failure — the caller decides whether a missing file is fatal.
127
+
128
+ :param dest: Absolute path to write the file to.
129
+ """
130
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
131
+
132
+ try:
133
+ import requests
134
+ response = requests.get(REGIONS_URL, timeout=30)
135
+ response.raise_for_status()
136
+ data = response.text
137
+ except Exception: # noqa: BLE001
138
+ return
139
+
140
+ try:
141
+ decoded = json.loads(data)
142
+ except json.JSONDecodeError:
143
+ return
144
+
145
+ if isinstance(decoded, dict) and 'regions' in decoded:
146
+ try:
147
+ with open(dest, 'w', encoding='utf-8') as f:
148
+ f.write(data)
149
+ except OSError:
150
+ pass
151
+
152
+ @staticmethod
153
+ def _find_region(regions, input_str):
154
+ """
155
+ Find a region entry by its id or any alias (case-insensitive).
156
+
157
+ Two-pass: exact id match first, then alias[] scan — mirrors PHP implementation.
158
+
159
+ :param regions: list of region dicts from regions.json
160
+ :param input_str: already-lowercased input
161
+ :returns: region dict or None
162
+ """
163
+ for row in regions:
164
+ if row['id'] == input_str:
165
+ return row
166
+ for row in regions:
167
+ for alias in row.get('alias', []):
168
+ if alias.lower() == input_str:
169
+ return row
170
+ return None
171
+
172
+ @staticmethod
173
+ def _strip_https(url):
174
+ """Strip the https:// (or http://) scheme from a URL string."""
175
+ return re.sub(r'^https?://', '', url)
176
+
177
+ @staticmethod
178
+ def reset_cache():
179
+ """Reset the internal region cache. Intended for testing only."""
180
+ Endpoint._regions_data = None
@@ -3,6 +3,7 @@ The Get a single entry request fetches a particular entry of a content type.
3
3
  API Reference: https://www.contentstack.com/docs/developers/apis/content-delivery-api/#single-entry
4
4
  """
5
5
  #min-similarity-lines=10
6
+ from __future__ import annotations
6
7
  import logging
7
8
  from urllib import parse
8
9
  from contentstack.error_messages import ErrorMessages
@@ -0,0 +1,81 @@
1
+ """
2
+ Utility to pull the latest regions.json from the Contentstack CDN and
3
+ overwrite the bundled copy at contentstack/assets/regions.json.
4
+
5
+ Exposed as a package-level function so tooling and CI pipelines can call it
6
+ programmatically instead of invoking the script directly:
7
+
8
+ from contentstack import refresh_regions
9
+ refresh_regions()
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+ import urllib.request
16
+
17
+ _REGIONS_URL = "https://artifacts.contentstack.com/regions.json"
18
+ _ASSET_PATH = os.path.join(os.path.dirname(__file__), "assets", "regions.json")
19
+
20
+
21
+ def refresh_regions(
22
+ url: str = _REGIONS_URL,
23
+ dest: str = _ASSET_PATH,
24
+ *,
25
+ timeout: int = 30,
26
+ silent: bool = False,
27
+ ) -> dict:
28
+ """
29
+ Download the latest regions manifest from the Contentstack CDN and write
30
+ it to the bundled assets file so all consumers get the update.
31
+
32
+ @param url - URL to fetch regions.json from (defaults to Contentstack CDN)
33
+ @param dest - Destination file path (defaults to contentstack/assets/regions.json)
34
+ @param timeout - HTTP request timeout in seconds
35
+ @param silent - Suppress progress output when True
36
+ @returns The parsed regions dict on success
37
+ @raises RuntimeError on download failure, invalid JSON, or unexpected schema
38
+ """
39
+ dest = os.path.normpath(dest)
40
+
41
+ if not silent:
42
+ print(f"Fetching {url} ...")
43
+
44
+ try:
45
+ with urllib.request.urlopen(url, timeout=timeout) as resp:
46
+ data = resp.read().decode("utf-8")
47
+ except Exception as exc:
48
+ raise RuntimeError(f"Could not download regions.json: {exc}") from exc
49
+
50
+ try:
51
+ decoded = json.loads(data)
52
+ except json.JSONDecodeError as exc:
53
+ raise RuntimeError(f"Downloaded content is not valid JSON: {exc}") from exc
54
+
55
+ if not isinstance(decoded, dict) or "regions" not in decoded:
56
+ raise RuntimeError("Downloaded JSON does not contain a 'regions' key.")
57
+
58
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
59
+ with open(dest, "w", encoding="utf-8") as fh:
60
+ json.dump(decoded, fh, indent=2, ensure_ascii=False)
61
+ fh.write("\n")
62
+
63
+ region_count = len(decoded["regions"])
64
+ if not silent:
65
+ print(f"OK: Wrote {region_count} regions to {dest}")
66
+
67
+ return decoded
68
+
69
+
70
+ def _cli_main() -> int:
71
+ """Entry point kept for backward compatibility with the scripts/ invocation."""
72
+ try:
73
+ refresh_regions()
74
+ return 0
75
+ except RuntimeError as exc:
76
+ print(f"ERROR: {exc}", file=sys.stderr)
77
+ return 1
78
+
79
+
80
+ if __name__ == "__main__":
81
+ sys.exit(_cli_main())
@@ -7,6 +7,7 @@ from contentstack.error_messages import ErrorMessages
7
7
  from contentstack.asset import Asset
8
8
  from contentstack.assetquery import AssetQuery
9
9
  from contentstack.contenttype import ContentType
10
+ from contentstack.endpoint import Endpoint
10
11
  from contentstack.taxonomy import Taxonomy
11
12
  from contentstack.globalfields import GlobalField
12
13
  from contentstack.https_connection import HTTPSConnection
@@ -119,20 +120,15 @@ class Stack:
119
120
  if self.environment is None or self.environment == "":
120
121
  raise PermissionError(ErrorMessages.INVALID_ENVIRONMENT_TOKEN)
121
122
 
122
- if self.region.value == 'eu' and self.host == DEFAULT_HOST:
123
- self.host = 'eu-cdn.contentstack.com'
124
- elif self.region.value == 'au' and self.host == DEFAULT_HOST:
125
- self.host = 'au-cdn.contentstack.com'
126
- elif self.region.value == 'azure-na' and self.host == DEFAULT_HOST:
127
- self.host = 'azure-na-cdn.contentstack.com'
128
- elif self.region.value == 'azure-eu' and self.host == DEFAULT_HOST:
129
- self.host = 'azure-eu-cdn.contentstack.com'
130
- elif self.region.value == 'gcp-na' and self.host == DEFAULT_HOST:
131
- self.host = 'gcp-na-cdn.contentstack.com'
132
- elif self.region.value == 'gcp-eu' and self.host == DEFAULT_HOST:
133
- self.host = 'gcp-eu-cdn.contentstack.com'
134
- elif self.region.value != 'us':
135
- self.host = f'{self.region.value}-{DEFAULT_HOST}'
123
+ if self.host == DEFAULT_HOST:
124
+ try:
125
+ self.host = Endpoint.get_contentstack_endpoint(
126
+ self.region.value, 'contentDelivery', omit_https=True)
127
+ except (ValueError, RuntimeError):
128
+ # Unknown/custom region — fall back to legacy pattern so
129
+ # code written before this feature was added continues to work.
130
+ if self.region.value != 'us':
131
+ self.host = f'{self.region.value}-{DEFAULT_HOST}'
136
132
  self.endpoint = f'https://{self.host}/{self.version}'
137
133
 
138
134
  def _setup_headers(self):
@@ -365,8 +361,14 @@ class Stack:
365
361
 
366
362
  def _setup_live_preview(self):
367
363
  if self.live_preview and self.live_preview.get("enable"):
368
- region_prefix = "" if self.region.value == "us" else f"{self.region.value}-"
369
- self.live_preview["host"] = f"{region_prefix}rest-preview.contentstack.com"
364
+ if not self.live_preview.get("host"):
365
+ try:
366
+ mgmt_host = Endpoint.get_contentstack_endpoint(
367
+ self.region.value, 'contentManagement', omit_https=True)
368
+ except (ValueError, RuntimeError):
369
+ region_prefix = "" if self.region.value == "us" else f"{self.region.value}-"
370
+ mgmt_host = f"{region_prefix}api.contentstack.io"
371
+ self.live_preview["host"] = mgmt_host
370
372
 
371
373
  if self.live_preview.get("preview_token"):
372
374
  self.headers["preview_token"] = self.live_preview["preview_token"]
@@ -9,11 +9,25 @@ import sys
9
9
 
10
10
  try:
11
11
  from setuptools import setup, find_packages
12
+ from setuptools.command.build_py import build_py
12
13
  except ImportError:
13
14
  from distutils.core import setup
14
15
 
15
16
  package = "contentstack"
16
17
 
18
+
19
+ class BuildPyWithRegions(build_py):
20
+ """Fetch latest regions.json from Contentstack CDN before packaging."""
21
+
22
+ def run(self):
23
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
+ try:
25
+ from contentstack.region_refresh import refresh_regions
26
+ refresh_regions()
27
+ except Exception as exc:
28
+ print(f"WARNING: Could not refresh regions.json: {exc}", file=sys.stderr)
29
+ super().run()
30
+
17
31
  def get_version(package):
18
32
  """
19
33
  Return package version as listed in `__version__` in `init.py`.
@@ -32,6 +46,7 @@ requirements = [
32
46
  ]
33
47
 
34
48
  setup(
49
+ cmdclass={"build_py": BuildPyWithRegions},
35
50
  title="contentstack-python",
36
51
  name="Contentstack",
37
52
  status="Active",
@@ -46,6 +61,7 @@ setup(
46
61
  long_description_content_type="text/markdown",
47
62
  url="https://github.com/contentstack/contentstack-python",
48
63
  packages=['contentstack'],
64
+ package_data={'contentstack': ['assets/regions.json']},
49
65
  license='MIT',
50
66
  test_suite='tests',
51
67
  install_requires=requirements,
@@ -0,0 +1,287 @@
1
+ import unittest
2
+
3
+ import contentstack
4
+ from contentstack.endpoint import Endpoint
5
+ from contentstack.stack import ContentstackRegion, Stack
6
+
7
+ API_KEY = 'test_api_key'
8
+ DELIVERY_TOKEN = 'test_delivery_token'
9
+ ENVIRONMENT = 'test_environment'
10
+
11
+
12
+ class TestEndpoint(unittest.TestCase):
13
+
14
+ def setUp(self):
15
+ Endpoint.reset_cache()
16
+
17
+ # -------------------------------------------------------------------------
18
+ # Default region (us / na)
19
+ # -------------------------------------------------------------------------
20
+
21
+ def test_default_region_returns_all_endpoints(self):
22
+ endpoints = Endpoint.get_contentstack_endpoint()
23
+ self.assertIsInstance(endpoints, dict)
24
+ self.assertIn('contentDelivery', endpoints)
25
+ self.assertIn('contentManagement', endpoints)
26
+
27
+ def test_default_region_content_delivery(self):
28
+ url = Endpoint.get_contentstack_endpoint('us', 'contentDelivery')
29
+ self.assertEqual('https://cdn.contentstack.io', url)
30
+
31
+ def test_default_region_content_management(self):
32
+ url = Endpoint.get_contentstack_endpoint('us', 'contentManagement')
33
+ self.assertEqual('https://api.contentstack.io', url)
34
+
35
+ # -------------------------------------------------------------------------
36
+ # All 7 regions — contentDelivery spot-checks
37
+ # -------------------------------------------------------------------------
38
+
39
+ def test_content_delivery_na(self):
40
+ url = Endpoint.get_contentstack_endpoint('na', 'contentDelivery')
41
+ self.assertEqual('https://cdn.contentstack.io', url)
42
+
43
+ def test_content_delivery_eu(self):
44
+ url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery')
45
+ self.assertEqual('https://eu-cdn.contentstack.com', url)
46
+
47
+ def test_content_delivery_au(self):
48
+ url = Endpoint.get_contentstack_endpoint('au', 'contentDelivery')
49
+ self.assertEqual('https://au-cdn.contentstack.com', url)
50
+
51
+ def test_content_delivery_azure_na(self):
52
+ url = Endpoint.get_contentstack_endpoint('azure-na', 'contentDelivery')
53
+ self.assertEqual('https://azure-na-cdn.contentstack.com', url)
54
+
55
+ def test_content_delivery_azure_eu(self):
56
+ url = Endpoint.get_contentstack_endpoint('azure-eu', 'contentDelivery')
57
+ self.assertEqual('https://azure-eu-cdn.contentstack.com', url)
58
+
59
+ def test_content_delivery_gcp_na(self):
60
+ url = Endpoint.get_contentstack_endpoint('gcp-na', 'contentDelivery')
61
+ self.assertEqual('https://gcp-na-cdn.contentstack.com', url)
62
+
63
+ def test_content_delivery_gcp_eu(self):
64
+ url = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentDelivery')
65
+ self.assertEqual('https://gcp-eu-cdn.contentstack.com', url)
66
+
67
+ # -------------------------------------------------------------------------
68
+ # All 7 regions — contentManagement spot-checks
69
+ # -------------------------------------------------------------------------
70
+
71
+ def test_content_management_na(self):
72
+ url = Endpoint.get_contentstack_endpoint('na', 'contentManagement')
73
+ self.assertEqual('https://api.contentstack.io', url)
74
+
75
+ def test_content_management_eu(self):
76
+ url = Endpoint.get_contentstack_endpoint('eu', 'contentManagement')
77
+ self.assertEqual('https://eu-api.contentstack.com', url)
78
+
79
+ def test_content_management_au(self):
80
+ url = Endpoint.get_contentstack_endpoint('au', 'contentManagement')
81
+ self.assertEqual('https://au-api.contentstack.com', url)
82
+
83
+ def test_content_management_azure_na(self):
84
+ url = Endpoint.get_contentstack_endpoint('azure-na', 'contentManagement')
85
+ self.assertEqual('https://azure-na-api.contentstack.com', url)
86
+
87
+ def test_content_management_azure_eu(self):
88
+ url = Endpoint.get_contentstack_endpoint('azure-eu', 'contentManagement')
89
+ self.assertEqual('https://azure-eu-api.contentstack.com', url)
90
+
91
+ def test_content_management_gcp_na(self):
92
+ url = Endpoint.get_contentstack_endpoint('gcp-na', 'contentManagement')
93
+ self.assertEqual('https://gcp-na-api.contentstack.com', url)
94
+
95
+ def test_content_management_gcp_eu(self):
96
+ url = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentManagement')
97
+ self.assertEqual('https://gcp-eu-api.contentstack.com', url)
98
+
99
+ # -------------------------------------------------------------------------
100
+ # NA aliases all resolve to the same endpoint
101
+ # -------------------------------------------------------------------------
102
+
103
+ def test_alias_na(self):
104
+ url = Endpoint.get_contentstack_endpoint('na', 'contentDelivery')
105
+ self.assertEqual('https://cdn.contentstack.io', url)
106
+
107
+ def test_alias_us(self):
108
+ url = Endpoint.get_contentstack_endpoint('us', 'contentDelivery')
109
+ self.assertEqual('https://cdn.contentstack.io', url)
110
+
111
+ def test_alias_aws_na_hyphen(self):
112
+ url = Endpoint.get_contentstack_endpoint('aws-na', 'contentDelivery')
113
+ self.assertEqual('https://cdn.contentstack.io', url)
114
+
115
+ def test_alias_aws_na_underscore(self):
116
+ url = Endpoint.get_contentstack_endpoint('aws_na', 'contentDelivery')
117
+ self.assertEqual('https://cdn.contentstack.io', url)
118
+
119
+ def test_alias_na_uppercase(self):
120
+ url = Endpoint.get_contentstack_endpoint('NA', 'contentDelivery')
121
+ self.assertEqual('https://cdn.contentstack.io', url)
122
+
123
+ def test_alias_us_uppercase(self):
124
+ url = Endpoint.get_contentstack_endpoint('US', 'contentDelivery')
125
+ self.assertEqual('https://cdn.contentstack.io', url)
126
+
127
+ # -------------------------------------------------------------------------
128
+ # Case-insensitive alias matching for other regions
129
+ # -------------------------------------------------------------------------
130
+
131
+ def test_alias_aws_na_uppercase(self):
132
+ url = Endpoint.get_contentstack_endpoint('AWS-NA', 'contentDelivery')
133
+ self.assertEqual('https://cdn.contentstack.io', url)
134
+
135
+ def test_alias_azure_na_underscore(self):
136
+ url = Endpoint.get_contentstack_endpoint('azure_na', 'contentDelivery')
137
+ self.assertEqual('https://azure-na-cdn.contentstack.com', url)
138
+
139
+ def test_alias_gcp_eu_underscore(self):
140
+ url = Endpoint.get_contentstack_endpoint('gcp_eu', 'contentManagement')
141
+ self.assertEqual('https://gcp-eu-api.contentstack.com', url)
142
+
143
+ # -------------------------------------------------------------------------
144
+ # ContentstackRegion enum constants resolve correctly
145
+ # -------------------------------------------------------------------------
146
+
147
+ def test_region_constant_us(self):
148
+ url = Endpoint.get_contentstack_endpoint(ContentstackRegion.US.value, 'contentDelivery')
149
+ self.assertEqual('https://cdn.contentstack.io', url)
150
+
151
+ def test_region_constant_eu(self):
152
+ url = Endpoint.get_contentstack_endpoint(ContentstackRegion.EU.value, 'contentDelivery')
153
+ self.assertEqual('https://eu-cdn.contentstack.com', url)
154
+
155
+ def test_region_constant_au(self):
156
+ url = Endpoint.get_contentstack_endpoint(ContentstackRegion.AU.value, 'contentDelivery')
157
+ self.assertEqual('https://au-cdn.contentstack.com', url)
158
+
159
+ def test_region_constant_azure_na(self):
160
+ url = Endpoint.get_contentstack_endpoint(ContentstackRegion.AZURE_NA.value, 'contentDelivery')
161
+ self.assertEqual('https://azure-na-cdn.contentstack.com', url)
162
+
163
+ def test_region_constant_azure_eu(self):
164
+ url = Endpoint.get_contentstack_endpoint(ContentstackRegion.AZURE_EU.value, 'contentDelivery')
165
+ self.assertEqual('https://azure-eu-cdn.contentstack.com', url)
166
+
167
+ def test_region_constant_gcp_na(self):
168
+ url = Endpoint.get_contentstack_endpoint(ContentstackRegion.GCP_NA.value, 'contentDelivery')
169
+ self.assertEqual('https://gcp-na-cdn.contentstack.com', url)
170
+
171
+ def test_region_constant_gcp_eu(self):
172
+ url = Endpoint.get_contentstack_endpoint(ContentstackRegion.GCP_EU.value, 'contentDelivery')
173
+ self.assertEqual('https://gcp-eu-cdn.contentstack.com', url)
174
+
175
+ # -------------------------------------------------------------------------
176
+ # omit_https flag
177
+ # -------------------------------------------------------------------------
178
+
179
+ def test_omit_https_strips_scheme_single_service(self):
180
+ url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery', omit_https=True)
181
+ self.assertEqual('eu-cdn.contentstack.com', url)
182
+
183
+ def test_omit_https_strips_scheme_all_services(self):
184
+ endpoints = Endpoint.get_contentstack_endpoint('na', omit_https=True)
185
+ self.assertIsInstance(endpoints, dict)
186
+ for key, url in endpoints.items():
187
+ self.assertNotIn('https://', url, f'Service {key} still has https://')
188
+ self.assertNotIn('http://', url, f'Service {key} still has http://')
189
+
190
+ def test_omit_https_false_retains_scheme(self):
191
+ url = Endpoint.get_contentstack_endpoint('na', 'contentManagement', omit_https=False)
192
+ self.assertTrue(url.startswith('https://'))
193
+
194
+ # -------------------------------------------------------------------------
195
+ # No service — returns full dict
196
+ # -------------------------------------------------------------------------
197
+
198
+ def test_no_service_returns_dict(self):
199
+ result = Endpoint.get_contentstack_endpoint('au')
200
+ self.assertIsInstance(result, dict)
201
+ self.assertGreater(len(result), 1)
202
+
203
+ def test_no_service_contains_correct_urls(self):
204
+ endpoints = Endpoint.get_contentstack_endpoint('au')
205
+ self.assertEqual('https://au-cdn.contentstack.com', endpoints['contentDelivery'])
206
+ self.assertEqual('https://au-api.contentstack.com', endpoints['contentManagement'])
207
+
208
+ # -------------------------------------------------------------------------
209
+ # Error cases
210
+ # -------------------------------------------------------------------------
211
+
212
+ def test_empty_region_raises_value_error(self):
213
+ with self.assertRaises(ValueError) as ctx:
214
+ Endpoint.get_contentstack_endpoint('')
215
+ self.assertIn('Empty region', str(ctx.exception))
216
+
217
+ def test_unknown_region_raises_value_error(self):
218
+ with self.assertRaises(ValueError) as ctx:
219
+ Endpoint.get_contentstack_endpoint('invalid-region')
220
+ self.assertIn('Invalid region', str(ctx.exception))
221
+
222
+ def test_unknown_service_raises_value_error(self):
223
+ with self.assertRaises(ValueError) as ctx:
224
+ Endpoint.get_contentstack_endpoint('na', 'unknownService')
225
+ self.assertIn('unknownService', str(ctx.exception))
226
+
227
+ # -------------------------------------------------------------------------
228
+ # contentstack.get_contentstack_endpoint() module-level proxy
229
+ # -------------------------------------------------------------------------
230
+
231
+ def test_module_proxy_returns_same_result(self):
232
+ via_class = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery')
233
+ via_module = contentstack.get_contentstack_endpoint('eu', 'contentDelivery')
234
+ self.assertEqual(via_class, via_module)
235
+
236
+ def test_module_proxy_default_region(self):
237
+ url = contentstack.get_contentstack_endpoint('us', 'contentManagement')
238
+ self.assertEqual('https://api.contentstack.io', url)
239
+
240
+ def test_module_proxy_omit_https(self):
241
+ url = contentstack.get_contentstack_endpoint('gcp-na', 'contentDelivery', omit_https=True)
242
+ self.assertEqual('gcp-na-cdn.contentstack.com', url)
243
+
244
+ def test_module_proxy_all_endpoints(self):
245
+ endpoints = contentstack.get_contentstack_endpoint('azure-eu')
246
+ self.assertIsInstance(endpoints, dict)
247
+ self.assertIn('contentDelivery', endpoints)
248
+
249
+ # -------------------------------------------------------------------------
250
+ # Stack host resolution via Endpoint
251
+ # -------------------------------------------------------------------------
252
+
253
+ def test_stack_us_host_resolves_to_default_cdn(self):
254
+ stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.US)
255
+ self.assertEqual('cdn.contentstack.io', stack.host)
256
+
257
+ def test_stack_eu_host_resolves_via_endpoint(self):
258
+ stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.EU)
259
+ self.assertEqual('eu-cdn.contentstack.com', stack.host)
260
+
261
+ def test_stack_au_host_resolves_via_endpoint(self):
262
+ stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.AU)
263
+ self.assertEqual('au-cdn.contentstack.com', stack.host)
264
+
265
+ def test_stack_azure_na_host_resolves_via_endpoint(self):
266
+ stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.AZURE_NA)
267
+ self.assertEqual('azure-na-cdn.contentstack.com', stack.host)
268
+
269
+ def test_stack_gcp_eu_host_resolves_via_endpoint(self):
270
+ stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.GCP_EU)
271
+ self.assertEqual('gcp-eu-cdn.contentstack.com', stack.host)
272
+
273
+ def test_stack_explicit_host_overrides_region(self):
274
+ stack = Stack(
275
+ API_KEY, DELIVERY_TOKEN, ENVIRONMENT,
276
+ host='custom.cdn.example.com',
277
+ region=ContentstackRegion.EU
278
+ )
279
+ self.assertEqual('custom.cdn.example.com', stack.host)
280
+
281
+ def test_stack_endpoint_built_from_resolved_host(self):
282
+ stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.EU)
283
+ self.assertEqual('https://eu-cdn.contentstack.com/v3', stack.endpoint)
284
+
285
+
286
+ if __name__ == '__main__':
287
+ unittest.main()
@@ -1,29 +0,0 @@
1
- """
2
- The __init__.py files are required to make Python treat the directories as containing
3
- packages; this is done to prevent directories with a common name, such as string,
4
- from unintentionally hiding valid modules that occur later on the module search path
5
- """
6
- from .entry import Entry
7
- from .asset import Asset
8
- from .contenttype import ContentType
9
- from .https_connection import HTTPSConnection
10
- from contentstack.stack import Stack
11
- from .utility import Utils
12
-
13
- __all__ = (
14
- "Entry",
15
- "Asset",
16
- "ContentType",
17
- "HTTPSConnection",
18
- "Stack",
19
- "Utils"
20
- )
21
-
22
- __title__ = 'contentstack-delivery-python'
23
- __author__ = 'contentstack'
24
- __status__ = 'debug'
25
- __version__ = 'v2.5.0'
26
- __endpoint__ = 'cdn.contentstack.io'
27
- __email__ = 'support@contentstack.com'
28
- __developer_email__ = 'mobile@contentstack.com'
29
- __license__ = "MIT"
File without changes
File without changes
File without changes