diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ef629..b83025e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,26 +2,18 @@ ## _v2.6.0_ -### **Date: 15-June-2026** +### **Date: 22-June-2026** - Dynamic endpoint resolution via new `Endpoint` class. - Region-to-URL mapping is now loaded from a bundled `regions.json` (sourced from `artifacts.contentstack.com`) instead of hardcoded `if/elif` chains. - Added `Endpoint.get_contentstack_endpoint(region, service, omit_https)` — resolves any supported region to its `contentDelivery`, `contentManagement`, or other service URL. - Added `contentstack.get_contentstack_endpoint()` module-level proxy. - `Stack` now auto-resolves `host` and `live_preview` management host via `Endpoint` on initialization. -- Added `scripts/download_regions.py` to pre-populate `regions.json` at install time. -- Runtime fallback: if `regions.json` is absent, the SDK downloads it live on first use. -## _v2.5.3_ - -### **Date: 08-June-2026** - -- Fixed security issues. - -## _v2.5.2_ - -### **Date: 18-May-2026** - -- Bumped urllib3 in development requirements to address reported vulnerabilities. +- Bundled `contentstack/assets/regions.json` included in `package_data` — always present after `pip install`. +- `setup.py` auto-refreshes `regions.json` at build time via a custom `BuildPyWithRegions` command; network failures warn but never block the build. +- Runtime fallback: if `regions.json` is absent, the SDK downloads it live on the first `Endpoint` call. +- Added `refresh_regions()` utility to programmatically download the latest regions manifest from the Contentstack CDN and overwrite the bundled `assets/regions.json` (`from contentstack import refresh_regions`). +- Added `python3 -m contentstack.region_refresh` CLI command for refreshing the registry after `pip install` (source-tree script `scripts/download_regions.py` is for contributors only). ## _v2.5.1_ diff --git a/contentstack/__init__.py b/contentstack/__init__.py index 8a17aed..97350db 100644 --- a/contentstack/__init__.py +++ b/contentstack/__init__.py @@ -10,6 +10,7 @@ from .https_connection import HTTPSConnection from contentstack.stack import Stack from .utility import Utils +from .region_refresh import refresh_regions __all__ = ( "Entry", @@ -18,7 +19,8 @@ "Endpoint", "HTTPSConnection", "Stack", -"Utils" +"Utils", +"refresh_regions", ) diff --git a/contentstack/region_refresh.py b/contentstack/region_refresh.py new file mode 100644 index 0000000..5579502 --- /dev/null +++ b/contentstack/region_refresh.py @@ -0,0 +1,81 @@ +""" +Utility to pull the latest regions.json from the Contentstack CDN and +overwrite the bundled copy at contentstack/assets/regions.json. + +Exposed as a package-level function so tooling and CI pipelines can call it +programmatically instead of invoking the script directly: + + from contentstack import refresh_regions + refresh_regions() +""" + +import json +import os +import sys +import urllib.request + +_REGIONS_URL = "https://artifacts.contentstack.com/regions.json" +_ASSET_PATH = os.path.join(os.path.dirname(__file__), "assets", "regions.json") + + +def refresh_regions( + url: str = _REGIONS_URL, + dest: str = _ASSET_PATH, + *, + timeout: int = 30, + silent: bool = False, +) -> dict: + """ + Download the latest regions manifest from the Contentstack CDN and write + it to the bundled assets file so all consumers get the update. + + @param url - URL to fetch regions.json from (defaults to Contentstack CDN) + @param dest - Destination file path (defaults to contentstack/assets/regions.json) + @param timeout - HTTP request timeout in seconds + @param silent - Suppress progress output when True + @returns The parsed regions dict on success + @raises RuntimeError on download failure, invalid JSON, or unexpected schema + """ + dest = os.path.normpath(dest) + + if not silent: + print(f"Fetching {url} ...") + + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + data = resp.read().decode("utf-8") + except Exception as exc: + raise RuntimeError(f"Could not download regions.json: {exc}") from exc + + try: + decoded = json.loads(data) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Downloaded content is not valid JSON: {exc}") from exc + + if not isinstance(decoded, dict) or "regions" not in decoded: + raise RuntimeError("Downloaded JSON does not contain a 'regions' key.") + + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w", encoding="utf-8") as fh: + json.dump(decoded, fh, indent=2, ensure_ascii=False) + fh.write("\n") + + region_count = len(decoded["regions"]) + if not silent: + print(f"OK: Wrote {region_count} regions to {dest}") + + return decoded + + +def _cli_main() -> int: + """Entry point kept for backward compatibility with the scripts/ invocation.""" + try: + refresh_regions() + return 0 + except RuntimeError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(_cli_main()) diff --git a/requirements.txt b/requirements.txt index b6f9601..892dccf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ filelock~=3.20.1 pluggy~=1.5.0 six~=1.16.0 packaging>=24.0 -pytest==7.3.1 +pytest>=8.1.0 dill~=0.3.8 pytz==2024.1 Babel==2.14.0 diff --git a/setup.py b/setup.py index e83c0b7..943b229 100644 --- a/setup.py +++ b/setup.py @@ -9,11 +9,25 @@ try: from setuptools import setup, find_packages + from setuptools.command.build_py import build_py except ImportError: from distutils.core import setup package = "contentstack" + +class BuildPyWithRegions(build_py): + """Fetch latest regions.json from Contentstack CDN before packaging.""" + + def run(self): + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + try: + from contentstack.region_refresh import refresh_regions + refresh_regions() + except Exception as exc: + print(f"WARNING: Could not refresh regions.json: {exc}", file=sys.stderr) + super().run() + def get_version(package): """ Return package version as listed in `__version__` in `init.py`. @@ -32,6 +46,7 @@ def get_version(package): ] setup( + cmdclass={"build_py": BuildPyWithRegions}, title="contentstack-python", name="Contentstack", status="Active",