Skip to content

Commit 9d208b1

Browse files
committed
Merge branch 'development' into enh/dx-7265
2 parents 59b63cc + 8d1b46e commit 9d208b1

69 files changed

Lines changed: 5944 additions & 171 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Cursor (optional)
22

3-
**Cursor** users: start at the repo root **[`AGENTS.md`](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`** (universal for any editor or tool).
3+
**Cursor** users: start at **[AGENTS.md](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**.
44

5-
This folder only points contributors here so nothing editor-specific duplicates the canonical docs.
5+
This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs.

.github/workflows/issues-jira.yml

Lines changed: 0 additions & 31 deletions
This file was deleted.

.gitignore

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,17 @@ tests/config/default.yml
133133
.vscode/settings.json
134134
run.py
135135
tests/resources/.DS_Store
136-
.talismanrc
137136
tests/.DS_Store
138-
tests/resources/.DS_Store
139137
.DS_Store
140-
.talismanrc
138+
*/data/regions.json
139+
# Local backup of legacy tests (do not commit)
140+
141+
# --- CMA integration suite: do not track ---
142+
docs/
143+
tests_backup_legacy/
144+
tests/integration/report/
145+
tests/integration/.env
146+
tests/integration/.env.example
147+
# Timestamped reports written at repo root
148+
cma-python-report-*.html
149+
api-requests-*.txt

.talismanrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,7 @@ fileignoreconfig:
406406
- filename: tests/mock/assets/test_assets_mock.py
407407
checksum: 12c9091bb88c0c12712f046b29fb4a9dce3b95ecc45f4ea46bbc3fd4529742a0
408408
version: "1.0"
409+
fileignoreconfig:
410+
- filename: tests/integration/framework/capture.py
411+
checksum: c9680c4ee3e1def0765fd89767fa1383aee5d195222fa021a19618607373047c
412+
version: "1.0"

AGENTS.md

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
# Contentstack Management Python Agent guide
1+
# Contentstack Management Python Agent guide
22

3-
**Universal entry point** for anyone automating or assisting work in this repo—AI agents (Cursor, Copilot, CLI tools), reviewers, and contributors. Conventions and detailed guidance live in **`skills/*/SKILL.md`**, not in editor-specific config, so the same instructions apply whether or not you use Cursor.
3+
**Universal entry point** for contributors and AI agents. Detailed conventions live in **`skills/*/SKILL.md`**.
44

55
## What this repo is
66

7-
| | |
8-
|---|---|
9-
| **Name** | **`contentstack-management`** (PyPI) — **Contentstack Management Python SDK** |
10-
| **Purpose** | Python client for the **Content Management API (CMA)**: organizations, stacks, content types, entries, assets, webhooks, workflows, OAuth, and related resources. Uses **`requests`** via **`_APIClient`**. |
11-
| **Repository** | [contentstack/contentstack-management-python](https://github.com/contentstack/contentstack-management-python.git) |
7+
| Field | Detail |
8+
|--------|--------|
9+
| **Name:** | **`contentstack-management`** (PyPI) — [contentstack/contentstack-management-python](https://github.com/contentstack/contentstack-management-python) |
10+
| **Purpose:** | Python client for the **Content Management API (CMA)**: organizations, stacks, content types, entries, assets, webhooks, workflows, OAuth, and related resources. Uses **`requests`** via **`_APIClient`**. |
1211

13-
## Tech stack
12+
## Tech stack (at a glance)
1413

1514
| Area | Details |
1615
|------|---------|
17-
| **Language** | **Python** ≥ 3.9 (`setup.py` `python_requires`) |
18-
| **HTTP** | **`requests`**, **`requests-toolbelt`**, **`urllib3`** |
19-
| **Tests** | **pytest****`tests/unit`**, **`tests/api`**, **`tests/mock`** |
20-
| **Lint** | **pylint** (see `requirements.txt`) |
21-
| **Secrets / hooks** | **Talisman**, **Snyk** (see **README.md** development setup) |
16+
| Language | Python ≥ 3.9 (`setup.py` `python_requires`) |
17+
| Build | `setuptools` / `setup.py`; package `contentstack_management` |
18+
| HTTP | `requests`, `requests-toolbelt`, `urllib3` |
19+
| Tests | `pytest``tests/integration` (live e2e / sanity, dynamic stack), `tests/unit`, `tests/mock`, `tests/api` (legacy, superseded by `tests/integration`) |
20+
| Lint / coverage | `pylint`, `coverage` (see `requirements.txt`) |
21+
| Secrets / hooks | Talisman, Snyk (see `README.md` development setup) |
2222

2323
## Source layout
2424

@@ -29,30 +29,44 @@
2929
| `contentstack_management/stack/stack.py` | **Stack**-scoped CMA operations |
3030
| `contentstack_management/*/` | Domain modules (entries, assets, webhooks, taxonomies, …) |
3131
| `contentstack_management/__init__.py` | Public exports |
32-
| `tests/cred.py` | **`get_credentials()`****dotenv** + env vars for API/mock tests |
32+
| `tests/integration/` | **Live e2e / sanity suite** (pytest). Self-contained: creates a fresh stack per run, exercises every SDK method (positive/negative/edge), tears it down. Own `framework/` + `data/`; config in `tests/integration/.env`. |
33+
| `tests/cred.py` | **`get_credentials()`****dotenv** + env vars for the legacy `tests/api` / `tests/mock` suites |
3334

3435
## Commands (quick reference)
3536

36-
```bash
37-
pip install -e ".[dev]"
38-
# or: pip install -r requirements.txt && pip install pytest pytest-cov
37+
| Command Type | Command |
38+
|---|---|
39+
| Install | `pip install -e ".[dev]"` |
40+
| **Sanity / e2e (live)** | `pytest tests/integration` — dynamically creates a stack, runs the full suite, tears it down. Needs `tests/integration/.env` (`EMAIL`, `PASSWORD`, `HOST`, `ORGANIZATION`). Writes a timestamped HTML report + cURL log to the repo root. |
41+
| Sanity, keep stack | `DELETE_DYNAMIC_RESOURCES=false pytest tests/integration` (preserve the created stack for debugging) |
42+
| Sanity, one resource | `pytest tests/integration/api/test_12_content_type.py` |
43+
| Test (unit) | `pytest tests/unit/ -v` |
44+
| Test (mock) | `pytest tests/mock/ -v` |
45+
| Test (legacy API, live) | `pytest tests/api/ -v` (needs `.env` — see `tests/cred.py`) |
46+
| Coverage (CI) | `coverage run -m pytest tests/unit/` |
47+
| Lint | `pylint contentstack_management/` |
48+
49+
> **CI note:** `.github/workflows/unit-test.yml` runs **only `tests/unit/`** (no credentials). The `tests/integration` sanity suite is run manually (or via a credential-gated job) because it provisions real stacks.
3950
40-
pytest tests/unit/ -v
41-
pytest tests/api/ -v # live CMA — needs .env (see tests/cred.py)
42-
pytest tests/mock/ -v
43-
pytest tests/ -v
44-
coverage run -m pytest tests/unit/
45-
```
51+
## Environment variables
4652

47-
## Environment variables (API / integration tests)
53+
**Sanity / e2e suite** (`tests/integration`) — configured via **`tests/integration/.env`** (gitignored). No pre-existing stack/UIDs needed; the suite creates everything at runtime.
4854

49-
Loaded via **`tests/cred.py`** (`load_dotenv()`). Examples include **`HOST`**, **`APIKEY`**, **`AUTHTOKEN`**, **`MANAGEMENT_TOKEN`**, **`ORG_UID`**, and resource UIDs (**`CONTENT_TYPE_UID`**, **`ENTRY_UID`**, …). See that file for the full list.
55+
| Var | Required | Purpose |
56+
|-----|----------|---------|
57+
| `EMAIL`, `PASSWORD` || Login for the run (a **non-2FA** account) |
58+
| `HOST` || API host (e.g. `api.contentstack.io`) |
59+
| `ORGANIZATION` || Org the dynamic test stack is created in |
60+
| `MFA_SECRET` || TOTP secret (for the OAuth/2FA account, not the primary login) |
61+
| `DELETE_DYNAMIC_RESOURCES` || `false` keeps the created stack for debugging (default deletes) |
62+
| `CLIENT_ID`, `APP_ID`, `REDIRECT_URI` || OAuth tests |
63+
| `PERSONALIZE_HOST` || Personalize project for variant tests |
5064

51-
Do not commit secrets.
65+
**Legacy `tests/api` / `tests/mock`** — loaded via **`tests/cred.py`** (`load_dotenv()`): `HOST`, `APIKEY`, `AUTHTOKEN`, `MANAGEMENT_TOKEN`, `ORG_UID`, and resource UIDs. See that file for the full list.
5266

53-
## Where the real documentation lives: skills
67+
Do not commit secrets. `tests/integration/.env`, `docs/`, and the repo-root `cma-python-report-*.html` / `api-requests-*.txt` are gitignored.
5468

55-
Read these **`SKILL.md` files** for full conventions—**this is the source of truth** for implementation and review:
69+
## Where the documentation lives: skills
5670

5771
| Skill | Path | What it covers |
5872
|-------|------|----------------|
@@ -63,10 +77,10 @@ Read these **`SKILL.md` files** for full conventions—**this is the source of t
6377
| **Code review** | [`skills/code-review/SKILL.md`](skills/code-review/SKILL.md) | PR checklist—public API, HTTP/auth, tests, security |
6478
| **Framework / HTTP** | [`skills/framework/SKILL.md`](skills/framework/SKILL.md) | **`requests`**, retries, OAuth interceptor, where to change transport |
6579

66-
An index with short “when to use” hints is in [`skills/README.md`](skills/README.md).
80+
An index with “when to use” hints is in [`skills/README.md`](skills/README.md).
6781

68-
## Using Cursor
82+
## Using Cursor (optional)
6983

70-
If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—same source of truth as everyone else; no separate `.mdc` rule files.
84+
If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—same docs as everyone else.
7185

7286
Product docs: [Content Management API](https://www.contentstack.com/docs/developers/apis/content-management-api/).

CHANGELOG.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,51 @@
22

33
## Content Management SDK For Python
44
---
5-
## v1.9.0
5+
## v1.11.0
66

7-
#### Date: 18 May 2026
7+
#### Date: 20 July 2026
88

99
- Entry variants: added `publish` and `unpublish` for the entry publish/unpublish endpoints; documented payloads including `entry.variants` and optional `entry.variant_rules` on publish.
1010
- Entry variants: optional stack branch via the `branch` request header; `Entry.variants()` accepts no arguments, a branch UID only, or `(variant_uid, branch)` (use `variants(variant_uid, None)` when targeting a variant without a branch).
1111

12+
---
13+
## v1.10.2
14+
15+
#### Date: 13 July 2026
16+
17+
- Removed hardcoded password literals in unit tests to resolve Snyk CWE-798 (Use of Hardcoded Passwords) findings.
18+
19+
---
20+
## v1.10.1
21+
22+
#### Date: 26 June 2026
23+
24+
- Fixed `Asset.update()` to send the JSON body with `Content-Type: application/json` instead of an invalid bare `multipart/form-data`, which the API rejected with 422.
25+
- Fixed `Asset.replace()` to let the HTTP layer set `multipart/form-data` with a proper boundary (a bare `multipart/form-data` header without a boundary previously caused a 422). Both fixes also remove a side effect that leaked the wrong `Content-Type` onto subsequent requests.
26+
27+
---
28+
## v1.10.0
29+
30+
#### Date: 22 June 2026
31+
32+
- Dynamic region endpoint resolution via the Contentstack Regions Registry (`regions.json`).
33+
- Added `Endpoint` class with 3-tier resolution: in-memory cache → bundled `data/regions.json` → live CDN download.
34+
- Exposed `contentstack_management.get_contentstack_endpoint(region, service, omit_https)` module-level proxy.
35+
- `Client` now resolves the `contentManagement` endpoint from the registry instead of a hardcoded host pattern.
36+
- Bundled `contentstack_management/data/regions.json` included in `package_data` — always present after `pip install`.
37+
- `setup.py` auto-refreshes `regions.json` at build time via a custom `BuildPyWithRegions` command; network failures warn but never block the build.
38+
- Runtime fallback: if `regions.json` is absent, the SDK downloads it live on the first `Endpoint` call.
39+
- New regions and services require no SDK code changes — registry update is sufficient.
40+
- Added `refresh_regions()` utility to programmatically download the latest regions manifest from the Contentstack CDN and overwrite the bundled `data/regions.json` (`from contentstack_management import refresh_regions`).
41+
- Added `python3 -m contentstack_management.region_refresh` CLI command for refreshing the registry after `pip install` (source-tree script `scripts/download_regions.py` is for contributors only).
42+
43+
---
44+
## v1.9.0
45+
46+
#### Date: 01 June 2026
47+
48+
- Removed unused `bson` dependency so the package installs on Python 3.12+ and 3.14 (the standalone `bson` 0.5.x package fails to build on newer Python versions).
49+
1250
---
1351
## v1.8.1
1452

contentstack_management/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .entries.entry import Entry
1919
from .entry_variants.entry_variants import EntryVariants
2020
from .contentstack import Client, Region
21+
from .endpoint import Endpoint
2122
from ._api_client import _APIClient
2223
from .common import Parameter
2324
from ._errors import ArgumentException
@@ -36,11 +37,13 @@
3637
from .variants.variants import Variants
3738
from .oauth.oauth_handler import OAuthHandler
3839
from .oauth.oauth_interceptor import OAuthInterceptor
40+
from .region_refresh import refresh_regions
3941

4042

4143
__all__ = (
4244
"Client",
4345
"Region",
46+
"Endpoint",
4447
"_APIClient",
4548
"Parameter",
4649
"ArgumentException",
@@ -75,14 +78,31 @@
7578
"VariantGroup",
7679
"Variants",
7780
"OAuthHandler",
78-
"OAuthInterceptor"
81+
"OAuthInterceptor",
82+
"refresh_regions",
7983
)
8084

85+
def get_contentstack_endpoint(region='us', service='', omit_https=False):
86+
"""
87+
Resolve a Contentstack service endpoint URL for a given region.
88+
89+
Proxy to :class:`Endpoint.get_contentstack_endpoint` for convenience —
90+
mirrors ``Contentstack::getContentstackEndpoint()`` in the PHP SDK.
91+
92+
:param region: Region ID or alias ('us', 'eu', 'azure-na', 'gcp-eu', ...).
93+
:param service: Service key ('contentDelivery', 'contentManagement', ...).
94+
When empty, returns a dict of all endpoints for the region.
95+
:param omit_https: When True, strips 'https://' from the returned URL(s).
96+
:returns: str when service is provided, dict[str,str] otherwise.
97+
"""
98+
return Endpoint.get_contentstack_endpoint(region, service, omit_https)
99+
100+
81101
__title__ = 'contentstack-management-python'
82102
__author__ = 'dev-ex'
83103
__status__ = 'debug'
84104
__region__ = 'na'
85-
__version__ = '1.9.0'
105+
__version__ = '1.10.2'
86106
__host__ = 'api.contentstack.io'
87107
__protocol__ = 'https://'
88108
__api_version__ = 'v3'

contentstack_management/assets/assets.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,17 @@ def replace(self, file_path):
174174
"""
175175

176176
url = f"assets/{self.asset_uid}"
177-
Parameter.add_header(self, "Content-Type", "multipart/form-data")
178-
files = {"asset": open(f"{file_path}",'rb')}
179-
return self.client.put(url, headers = self.client.headers, params = self.params, files = files)
177+
# _api_client._call_request does headers.update(self.headers) so a per-request
178+
# copy is overwritten before the request is sent. The only way to suppress
179+
# Content-Type is to temporarily remove it from the shared dict so requests can
180+
# set multipart/form-data with the correct boundary automatically.
181+
saved_ct = self.client.headers.pop("Content-Type", None)
182+
try:
183+
with open(file_path, 'rb') as fh:
184+
return self.client.put(url, headers=self.client.headers, params=self.params, files={"asset": fh})
185+
finally:
186+
if saved_ct is not None:
187+
self.client.headers["Content-Type"] = saved_ct
180188

181189
def generate(self, data):
182190
"""
@@ -407,8 +415,11 @@ def update(self, data):
407415
if self.asset_uid is None or '':
408416
raise Exception(ASSET_UID_REQUIRED)
409417
url = f"assets/{self.asset_uid}"
410-
Parameter.add_header(self, "Content-Type", "multipart/form-data")
411-
return self.client.put(url, headers = self.client.headers, params = self.params, data = data)
418+
# Use a per-request header copy with Content-Type explicitly set to application/json
419+
# so this call is not affected by whatever a preceding upload/replace left in
420+
# the shared headers dict.
421+
request_headers = {**self.client.headers, "Content-Type": "application/json"}
422+
return self.client.put(url, headers=request_headers, params=self.params, data=data)
412423

413424
def publish(self, data):
414425
"""

contentstack_management/contentstack.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import pyotp
44
from ._api_client import _APIClient
5+
from .endpoint import Endpoint
56
from contentstack_management.organizations import organization
67
from contentstack_management.stack import stack
78
from contentstack_management.user_session import user_session
@@ -37,16 +38,25 @@ def __init__(self, host: str = 'api.contentstack.io', scheme: str = 'https://',
3738
authtoken: str = None , management_token=None, headers: dict = None,
3839
region: Region = Region.US.value, version='v3', timeout=2, max_retries: int = 18, early_access: list = None,
3940
oauth_config: dict = None, **kwargs):
40-
self.endpoint = 'https://api.contentstack.io/v3/'
41-
42-
if region is not None and region is not Region.US.value:
43-
if host is not None and host != 'api.contentstack.io':
41+
_DEFAULT_HOST = 'api.contentstack.io'
42+
self.endpoint = f'{scheme}{_DEFAULT_HOST}/{version}/'
43+
44+
if host is None or host == _DEFAULT_HOST:
45+
# No custom host — resolve via Endpoint (regions.json-driven)
46+
try:
47+
base = Endpoint.get_contentstack_endpoint(
48+
region or 'us', 'contentManagement', omit_https=True)
49+
self.endpoint = f'{scheme}{base}/{version}/'
50+
except (ValueError, RuntimeError):
51+
# Unknown/custom region string — fall back to legacy pattern
52+
if region and region != Region.US.value:
53+
self.endpoint = f'{scheme}{region}-api.contentstack.com/{version}/'
54+
else:
55+
# Explicit custom host always wins; apply region prefix when non-US
56+
if region and region != Region.US.value:
4457
self.endpoint = f'{scheme}{region}-api.{host}/{version}/'
4558
else:
46-
host = 'api.contentstack.com'
47-
self.endpoint = f'{scheme}{region}-{host}/{version}/'
48-
elif host is not None and host != 'api.contentstack.io':
49-
self.endpoint = f'{scheme}{host}/{version}/'
59+
self.endpoint = f'{scheme}{host}/{version}/'
5060
if headers is None:
5161
headers = {}
5262
if early_access is not None:

0 commit comments

Comments
 (0)