Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## v1.6.0 (2026-06-05)
## v1.6.0 (2026-06-22)

### New feature: Multi-region endpoint resolution

Expand All @@ -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` (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.
- `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

Expand Down
2 changes: 2 additions & 0 deletions contentstack_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -32,6 +33,7 @@
"addEditableTags",
"addTags",
"getTag",
"refresh_regions",
)

__title__ = 'contentstack_utils'
Expand Down
82 changes: 82 additions & 0 deletions contentstack_utils/region_refresh.py
Original file line number Diff line number Diff line change
@@ -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())
24 changes: 22 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
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()

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,
Expand All @@ -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",
Expand Down
Loading