From 88fb5d8c11d7a5367d82f510fc592be68aab2aee Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Thu, 18 Jun 2026 03:26:09 +0530 Subject: [PATCH 1/4] fix: add refresh_regions utility and auto-refresh regions.json at build time - Add `contentstack_utils/region_refresh.py` exposing a `refresh_regions()` function that downloads the latest regions manifest from the Contentstack CDN and overwrites the bundled `assets/regions.json`. - Export `refresh_regions` at the package level so CI pipelines and tooling can call it programmatically (`from contentstack_utils import refresh_regions`). - Hook `setup.py` with a custom `BuildPyWithRegions` command so `regions.json` is refreshed automatically on every `pip install`; network failures warn but never block the build. - Include `assets/regions.json` in `package_data` so the bundled file is correctly shipped in the sdist/wheel. --- CHANGELOG.md | 8 ++- contentstack_utils/__init__.py | 2 + contentstack_utils/region_refresh.py | 82 ++++++++++++++++++++++++++++ setup.py | 24 +++++++- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 contentstack_utils/region_refresh.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cc498f9..e0b9526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## v1.6.0 (2026-06-05) +## v1.6.0 (2026-06-0) ### New feature: Multi-region endpoint resolution @@ -9,8 +9,12 @@ - Added `getContentstackEndpoint` camelCase alias on both `Endpoint` and `Utils` for cross-SDK parity. - Bundled `contentstack_utils/assets/regions.json` — the authoritative registry of 7 regions (AWS NA/EU/AU, Azure NA/EU, GCP NA/EU) and 18 service endpoint keys. - Added runtime fallback in `Endpoint._load_regions()` — downloads `regions.json` from `artifacts.contentstack.com` on first use when the file is absent. -- Added `scripts/refresh_regions.py` to manually pull the latest regions from Contentstack . +- Added `scripts/refresh_regions.py` to manually pull the latest regions from Contentstack. - Exported `Endpoint` at package level in `__all__`. +- Added `refresh_regions()` utility to programmatically download the latest regions manifest from the Contentstack CDN and overwrite the bundled `assets/regions.json`. +- Exposed `refresh_regions` at the package level (`from contentstack_utils import refresh_regions`) for use in CI pipelines and tooling. +- `setup.py` now auto-refreshes `regions.json` at build time via a custom `BuildPyWithRegions` command; network failures warn but never block the build. +- Added `assets/regions.json` to `package_data` so the bundled file is correctly shipped in the sdist/wheel. ## v1.5.0 diff --git a/contentstack_utils/__init__.py b/contentstack_utils/__init__.py index 1954233..da70a53 100644 --- a/contentstack_utils/__init__.py +++ b/contentstack_utils/__init__.py @@ -18,6 +18,7 @@ from contentstack_utils.gql import GQL from contentstack_utils.automate import Automate from contentstack_utils.entry_editable import addEditableTags, addTags, getTag +from contentstack_utils.region_refresh import refresh_regions __all__ = ( "Endpoint", @@ -32,6 +33,7 @@ "addEditableTags", "addTags", "getTag", +"refresh_regions", ) __title__ = 'contentstack_utils' diff --git a/contentstack_utils/region_refresh.py b/contentstack_utils/region_refresh.py new file mode 100644 index 0000000..48a03f5 --- /dev/null +++ b/contentstack_utils/region_refresh.py @@ -0,0 +1,82 @@ +""" +Utility to pull the latest regions.json from the Contentstack CDN and +overwrite the bundled copy at contentstack_utils/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_utils 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_utils/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() + print("Next steps:") + 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/setup.py b/setup.py index 525f153..a3afc96 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,23 @@ import os +import sys from setuptools import setup, find_packages -from distutils.core import setup +from setuptools.command.build_py import build_py + + +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_utils.region_refresh import refresh_regions + refresh_regions() + except Exception as exc: + # Never block a build over a network failure — warn and continue. + print(f"WARNING: Could not refresh regions.json: {exc}", file=sys.stderr) + super().run() + with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: long_description = readme.read() @@ -9,6 +25,9 @@ setup( name='contentstack_utils', packages=find_packages(), + package_data={ + "contentstack_utils": ["assets/regions.json"], + }, description="contentstack_utils is a Utility package for Contentstack headless CMS with an API-first approach.", author='contentstack', long_description=long_description, @@ -17,11 +36,12 @@ license='MIT', version='1.6.0', install_requires=[ - + ], setup_requires=['pytest-runner'], tests_require=['pytest==4.4.1'], test_suite='tests', + cmdclass={"build_py": BuildPyWithRegions}, classifiers=[ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", From 10d9e4029f7b07abf71f5f1c99a823e9681b1e19 Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Thu, 18 Jun 2026 08:51:12 +0530 Subject: [PATCH 2/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b9526..c1299d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## v1.6.0 (2026-06-0) +## v1.6.0 (2026-06-22) ### New feature: Multi-region endpoint resolution From 43b7655ba714aeec80ec66fcf1d682651b26b990 Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Thu, 18 Jun 2026 10:26:09 +0530 Subject: [PATCH 3/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1299d8..b93db75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Added `getContentstackEndpoint` camelCase alias on both `Endpoint` and `Utils` for cross-SDK parity. - Bundled `contentstack_utils/assets/regions.json` — the authoritative registry of 7 regions (AWS NA/EU/AU, Azure NA/EU, GCP NA/EU) and 18 service endpoint keys. - Added runtime fallback in `Endpoint._load_regions()` — downloads `regions.json` from `artifacts.contentstack.com` on first use when the file is absent. -- Added `scripts/refresh_regions.py` to manually pull the latest regions from Contentstack. +- Added `scripts/refresh_regions.py` (source-tree only) and `python3 -m contentstack_utils.region_refresh` (installed package CLI) to manually pull the latest regions from Contentstack. - Exported `Endpoint` at package level in `__all__`. - Added `refresh_regions()` utility to programmatically download the latest regions manifest from the Contentstack CDN and overwrite the bundled `assets/regions.json`. - Exposed `refresh_regions` at the package level (`from contentstack_utils import refresh_regions`) for use in CI pipelines and tooling. From c0d21466a24e0560096124ca8e917eeafa2fdca6 Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Fri, 19 Jun 2026 09:27:19 +0530 Subject: [PATCH 4/4] Update test_endpoint.py --- tests/test_endpoint.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index a386f1f..413fc7d 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -250,14 +250,12 @@ def test_proxy_error_propagates(self): # --------------------------------------------------------------------------- class TestCache: - def test_second_call_uses_cache(self, mocker): - # Prime the cache with the first call, then spy on open() to confirm - # the second call does NOT read the file again. + def test_second_call_uses_cache(self, reset_cache): + from unittest.mock import patch, MagicMock Endpoint.get_contentstack_endpoint("na", "contentDelivery") - spy = mocker.patch("builtins.open", wraps=open) - Endpoint.get_contentstack_endpoint("eu", "contentDelivery") - # The cached path must not trigger any file reads. - spy.assert_not_called() + with patch("builtins.open", wraps=open) as spy: + Endpoint.get_contentstack_endpoint("eu", "contentDelivery") + spy.assert_not_called() def test_reset_cache_clears_data(self): Endpoint.get_contentstack_endpoint("na") # primes cache