-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Added support for endpoint integration #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,3 +118,4 @@ venv.bak/ | |
| .mypy_cache/ | ||
| .idea/ | ||
| .vscode/ | ||
| */assets/regions.json | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| """ | ||
| Endpoint — Contentstack region-to-URL resolver. | ||
|
|
||
| Resolves Contentstack service endpoint URLs for any supported region. | ||
| Region data is loaded from contentstack/assets/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.endpoint import Endpoint | ||
|
|
||
| # Single service URL | ||
| url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery') | ||
| # 'https://eu-cdn.contentstack.com' | ||
|
|
||
| # All services for a region | ||
| endpoints = Endpoint.get_contentstack_endpoint('azure-na') | ||
| # {'contentDelivery': 'https://...', 'contentManagement': 'https://...', ...} | ||
|
|
||
| # Strip scheme (useful when setting host directly) | ||
| host = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentDelivery', omit_https=True) | ||
| # 'gcp-eu-cdn.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/assets/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 | ||
|
|
||
| assets_dir = os.path.join(os.path.dirname(__file__), 'assets') | ||
| path = os.path.join(assets_dir, 'regions.json') | ||
|
|
||
| if not os.path.exists(path): | ||
| Endpoint._download_and_save(path) | ||
|
|
||
| if not os.path.exists(path): | ||
| raise RuntimeError( | ||
| 'contentstack: 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: 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: 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| """ | ||
| Downloads the Contentstack regions registry from the official source and | ||
| saves it to contentstack/assets/regions.json. | ||
|
|
||
| Run manually: | ||
| python scripts/download_regions.py | ||
|
|
||
| Can also be wired into setup.py post-install hooks or tox envsetup if needed. | ||
| """ | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
|
|
||
| REGIONS_URL = 'https://artifacts.contentstack.com/regions.json' | ||
|
|
||
| DEST = os.path.join( | ||
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | ||
| 'contentstack', 'assets', 'regions.json' | ||
| ) | ||
|
|
||
|
|
||
| def download(): | ||
| dest_dir = os.path.dirname(DEST) | ||
| os.makedirs(dest_dir, exist_ok=True) | ||
|
|
||
| try: | ||
| import requests | ||
| except ImportError: | ||
| sys.stderr.write( | ||
| 'contentstack: requests library not available. ' | ||
| 'Install dependencies first: pip install -r requirements.txt\n' | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
| print(f'contentstack: 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: Warning — could not download regions.json: {exc}. ' | ||
| 'The SDK will attempt to download it at runtime on first use.\n' | ||
| ) | ||
| sys.exit(0) # non-fatal | ||
|
|
||
| try: | ||
| decoded = json.loads(data) | ||
| except json.JSONDecodeError: | ||
| sys.stderr.write( | ||
| 'contentstack: 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: 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: Warning — could not write regions.json to {DEST}: {exc}\n' | ||
| ) | ||
| sys.exit(0) | ||
|
|
||
| region_count = len(decoded['regions']) | ||
| print(f'contentstack: regions.json downloaded ({region_count} regions) → {DEST}') | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| download() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.