From 88d5b02c97a4b132ce877ed719cc6a78579c7218 Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Mon, 3 Aug 2026 23:37:06 +0530 Subject: [PATCH 1/2] feat: add taxonomy publish/unpublish/localize/unlocalize and terms localize/unlocalize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes parity gap with JS SDK and Java (feat/DX-9676-taxonomy-publish). Taxonomy (taxonomy.py): - publish(data) → POST /taxonomies/publish (collection-level) - unpublish(data) → POST /taxonomies/unpublish (collection-level) - localize(data, locale) → POST /taxonomies/{uid}?locale= (instance-level) - unlocalize(locale) → DELETE /taxonomies/{uid}?locale= (instance-level) Terms (terms.py): - localize(data, locale) → POST /taxonomies/{t}/terms/{uid}?locale= (instance-level) - unlocalize(locale) → DELETE /taxonomies/{t}/terms/{uid}?locale= (instance-level) 21/21 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 --- .../taxonomies/taxonomy.py | 65 ++++++++++++++++++- contentstack_management/terms/terms.py | 39 ++++++++++- tests/unit/taxonomies/test_taxonomy_unit.py | 25 +++++++ tests/unit/terms/test_terms_unit.py | 13 ++++ 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/contentstack_management/taxonomies/taxonomy.py b/contentstack_management/taxonomies/taxonomy.py index 1223a28..f5131ac 100644 --- a/contentstack_management/taxonomies/taxonomy.py +++ b/contentstack_management/taxonomies/taxonomy.py @@ -144,10 +144,73 @@ def delete(self, taxonomy_uid: str = None): return self.client.delete(url, headers = self.client.headers, params = self.params) + def publish(self, data: dict): + """ + Publish one or more taxonomies to the specified environments and locales. + Call on a collection-level instance (no taxonomy_uid set). + + :param data: ``{"locales": [...], "environments": [...], "items": [{"uid": "..."}]}`` + :return: requests.Response + + [Example:] + >>> data = {"locales": ["en-us"], "environments": ["production"], "items": [{"uid": "taxonomy_1"}]} + >>> client.stack('api_key').taxonomy().publish(data).json() + """ + url = f"{self.path}/publish" + return self.client.post(url, headers=self.client.headers, data=json.dumps(data), params=self.params) + + def unpublish(self, data: dict): + """ + Unpublish one or more taxonomies from the specified environments and locales. + Call on a collection-level instance (no taxonomy_uid set). + + :param data: ``{"locales": [...], "environments": [...], "items": [{"uid": "..."}]}`` + :return: requests.Response + + [Example:] + >>> data = {"locales": ["en-us"], "environments": ["production"], "items": [{"uid": "taxonomy_1"}]} + >>> client.stack('api_key').taxonomy().unpublish(data).json() + """ + url = f"{self.path}/unpublish" + return self.client.post(url, headers=self.client.headers, data=json.dumps(data), params=self.params) + + def localize(self, data: dict, locale: str): + """ + Localize a taxonomy into the specified locale. + Requires a taxonomy_uid (instance-level). + + :param data: ``{"taxonomy": {"name": "..."}}`` + :param locale: Target locale code, e.g. ``"hi-in"`` + :return: requests.Response + + [Example:] + >>> client.stack('api_key').taxonomy('uid').localize({"taxonomy": {"name": "Hindi"}}, 'hi-in').json() + """ + self.validate_taxonomy_uid() + self.add_param('locale', locale) + url = f"{self.path}/{self.taxonomy_uid}" + return self.client.post(url, headers=self.client.headers, data=json.dumps(data), params=self.params) + + def unlocalize(self, locale: str): + """ + Remove a locale variant of a taxonomy. + Requires a taxonomy_uid (instance-level). + + :param locale: Locale code to remove, e.g. ``"hi-in"`` + :return: requests.Response + + [Example:] + >>> client.stack('api_key').taxonomy('uid').unlocalize('hi-in').json() + """ + self.validate_taxonomy_uid() + self.add_param('locale', locale) + url = f"{self.path}/{self.taxonomy_uid}" + return self.client.delete(url, headers=self.client.headers, params=self.params) + def validate_taxonomy_uid(self): if self.taxonomy_uid is None or '': raise ArgumentException(TAXONOMY_UID_REQUIRED) - + def terms(self, terms_uid: str = None): self.validate_taxonomy_uid() return Terms(self.client, self.taxonomy_uid, terms_uid) diff --git a/contentstack_management/terms/terms.py b/contentstack_management/terms/terms.py index 18d5de1..f37031d 100644 --- a/contentstack_management/terms/terms.py +++ b/contentstack_management/terms/terms.py @@ -282,14 +282,49 @@ def descendants(self, terms_uid: str = None): url = f"{self.path}/{self.terms_uid}/descendants" return self.client.get(url, headers = self.client.headers, params = self.params) + def localize(self, data: dict, locale: str): + """ + Localize a term into the specified locale. + Requires both taxonomy_uid and terms_uid (instance-level). + + :param data: ``{"term": {"name": "..."}}`` + :param locale: Target locale code, e.g. ``"hi-in"`` + :return: requests.Response + + [Example:] + >>> client.stack('api_key').taxonomy('t_uid').terms('term_uid').localize({"term": {"name": "Hindi"}}, 'hi-in').json() + """ + self.validate_taxonomy_uid() + self.validate_terms_uid() + self.add_param('locale', locale) + url = f"{self.path}/{self.terms_uid}" + return self.client.post(url, headers=self.client.headers, data=json.dumps(data), params=self.params) + + def unlocalize(self, locale: str): + """ + Remove a locale variant of a term. + Requires both taxonomy_uid and terms_uid (instance-level). + + :param locale: Locale code to remove, e.g. ``"hi-in"`` + :return: requests.Response + + [Example:] + >>> client.stack('api_key').taxonomy('t_uid').terms('term_uid').unlocalize('hi-in').json() + """ + self.validate_taxonomy_uid() + self.validate_terms_uid() + self.add_param('locale', locale) + url = f"{self.path}/{self.terms_uid}" + return self.client.delete(url, headers=self.client.headers, params=self.params) + def validate_taxonomy_uid(self): if self.taxonomy_uid is None or '': raise ArgumentException(TAXONOMY_UID_REQUIRED) - + def validate_terms_uid(self): if self.terms_uid is None or '': raise ArgumentException(TERMS_UID_REQUIRED) - + def validate_term_string(self, term_string): if term_string is None or '': raise ArgumentException(TERM_STRING_REQUIRED) diff --git a/tests/unit/taxonomies/test_taxonomy_unit.py b/tests/unit/taxonomies/test_taxonomy_unit.py index 561ca88..fdf8810 100644 --- a/tests/unit/taxonomies/test_taxonomy_unit.py +++ b/tests/unit/taxonomies/test_taxonomy_unit.py @@ -71,3 +71,28 @@ def test_delete_taxonomy(self): response = self.client.stack(api_key).taxonomy(taxonomy_uid).delete() self.assertEqual(response.request.url, f"{self.client.endpoint}taxonomies/{taxonomy_uid}") self.assertEqual(response.request.method, "DELETE") + + def test_publish(self): + data = {"locales": ["en-us"], "environments": ["production"], "items": [{"uid": taxonomy_uid}]} + response = self.client.stack(api_key).taxonomy().publish(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}taxonomies/publish") + self.assertEqual(response.request.method, "POST") + + def test_unpublish(self): + data = {"locales": ["en-us"], "environments": ["production"], "items": [{"uid": taxonomy_uid}]} + response = self.client.stack(api_key).taxonomy().unpublish(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}taxonomies/unpublish") + self.assertEqual(response.request.method, "POST") + + def test_localize(self): + data = {"taxonomy": {"name": "Taxonomy Hindi"}} + response = self.client.stack(api_key).taxonomy(taxonomy_uid).localize(data, "hi-in") + self.assertIn(f"taxonomies/{taxonomy_uid}", response.request.url) + self.assertIn("locale=hi-in", response.request.url) + self.assertEqual(response.request.method, "POST") + + def test_unlocalize(self): + response = self.client.stack(api_key).taxonomy(taxonomy_uid).unlocalize("hi-in") + self.assertIn(f"taxonomies/{taxonomy_uid}", response.request.url) + self.assertIn("locale=hi-in", response.request.url) + self.assertEqual(response.request.method, "DELETE") diff --git a/tests/unit/terms/test_terms_unit.py b/tests/unit/terms/test_terms_unit.py index 95ff992..d3e233b 100644 --- a/tests/unit/terms/test_terms_unit.py +++ b/tests/unit/terms/test_terms_unit.py @@ -95,5 +95,18 @@ def test_descendants(self): self.assertEqual(response.request.method, "GET") self.assertEqual(response.request.headers["Content-Type"], "application/json") + def test_localize(self): + data = {"term": {"name": "Term Hindi"}} + response = self.client.stack(api_key).taxonomy(taxonomy_uid).terms(terms_uid).localize(data, "hi-in") + self.assertIn(f"taxonomies/{taxonomy_uid}/terms/{terms_uid}", response.request.url) + self.assertIn("locale=hi-in", response.request.url) + self.assertEqual(response.request.method, "POST") + + def test_unlocalize(self): + response = self.client.stack(api_key).taxonomy(taxonomy_uid).terms(terms_uid).unlocalize("hi-in") + self.assertIn(f"taxonomies/{taxonomy_uid}/terms/{terms_uid}", response.request.url) + self.assertIn("locale=hi-in", response.request.url) + self.assertEqual(response.request.method, "DELETE") + if __name__ == '__main__': unittest.main() From 3f90a1f5b879f8841899f3dd9a7094426be7051b Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Tue, 4 Aug 2026 00:06:08 +0530 Subject: [PATCH 2/2] test: add integration tests for taxonomy publish/unpublish/localize/unlocalize and terms localize/unlocalize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_07_taxonomy.py: - TestTaxonomyLocalize: localize → unlocalize against live stack - TestTaxonomyPublish: publish → unpublish (skips gracefully if taxonomy_publish feature not enabled) test_08_terms.py: - TestTermsLocalize: localize → unlocalize against live stack Co-Authored-By: Claude Sonnet 4.6 --- tests/integration/api/test_07_taxonomy.py | 48 +++++++++++++++++++++++ tests/integration/api/test_08_terms.py | 19 +++++++++ 2 files changed, 67 insertions(+) diff --git a/tests/integration/api/test_07_taxonomy.py b/tests/integration/api/test_07_taxonomy.py index 7314f11..7cfe33b 100644 --- a/tests/integration/api/test_07_taxonomy.py +++ b/tests/integration/api/test_07_taxonomy.py @@ -39,6 +39,54 @@ def test_fetch_nonexistent(self, stack): h.assert_status(resp, 404, 422) +class TestTaxonomyLocalize: + def test_localize(self, stack, store): + """Localize a taxonomy into a non-master locale.""" + uid = store["taxonomies"]["main"] + locale = store["locales"].get("custom", "fr-fr") + data = {"taxonomy": {"name": f"Taxonomy {locale}"}} + resp = stack.taxonomy(uid).localize(data, locale) + h.assert_status(resp, 200, 201) + h.wait(h.SHORT_DELAY) + + def test_unlocalize(self, stack, store): + """Remove the non-master locale variant.""" + uid = store["taxonomies"]["main"] + locale = store["locales"].get("custom", "fr-fr") + stack.client.headers.pop("Content-Type", None) + resp = stack.taxonomy(uid).unlocalize(locale) + h.assert_status(resp, 200, 204) + + +class TestTaxonomyPublish: + def test_publish(self, stack, store): + """Publish a taxonomy (requires taxonomy_publish feature flag on the stack).""" + uid = store["taxonomies"]["main"] + env = store["environments"].get("main") + locale = store["locales"].get("custom", "fr-fr") + if not env: + pytest.skip("No environment in store — skipping publish test") + data = {"locales": [locale], "environments": [env], "items": [{"uid": uid}]} + resp = stack.taxonomy().publish(data) + if resp.status_code == 403: + pytest.skip("taxonomy_publish feature not enabled on this stack") + h.assert_status(resp, 200, 201, 202) + h.wait(h.SHORT_DELAY) + + def test_unpublish(self, stack, store): + """Unpublish a taxonomy (requires taxonomy_publish feature flag on the stack).""" + uid = store["taxonomies"]["main"] + env = store["environments"].get("main") + locale = store["locales"].get("custom", "fr-fr") + if not env: + pytest.skip("No environment in store — skipping unpublish test") + data = {"locales": [locale], "environments": [env], "items": [{"uid": uid}]} + resp = stack.taxonomy().unpublish(data) + if resp.status_code == 403: + pytest.skip("taxonomy_publish feature not enabled on this stack") + h.assert_status(resp, 200, 201, 202) + + class TestTaxonomyDelete: def test_delete(self, stack): uid = h.generate_valid_uid("tax_del") diff --git a/tests/integration/api/test_08_terms.py b/tests/integration/api/test_08_terms.py index ca067ad..ce12e54 100644 --- a/tests/integration/api/test_08_terms.py +++ b/tests/integration/api/test_08_terms.py @@ -78,6 +78,25 @@ def test_fetch_nonexistent(self, stack, taxonomy_uid): h.assert_status(resp, 404, 422) +class TestTermsLocalize: + def test_localize(self, stack, store, taxonomy_uid): + """Localize a term into a non-master locale.""" + uid = store["terms"]["main"] + locale = store["locales"].get("custom", "fr-fr") + data = {"term": {"name": f"Term {locale}"}} + resp = stack.taxonomy(taxonomy_uid).terms(uid).localize(data, locale) + h.assert_status(resp, 200, 201) + h.wait(h.SHORT_DELAY) + + def test_unlocalize(self, stack, store, taxonomy_uid): + """Remove the non-master locale variant of a term.""" + uid = store["terms"]["main"] + locale = store["locales"].get("custom", "fr-fr") + stack.client.headers.pop("Content-Type", None) + resp = stack.taxonomy(taxonomy_uid).terms(uid).unlocalize(locale) + h.assert_status(resp, 200, 204) + + class TestTermsDelete: def test_delete(self, stack, taxonomy_uid): uid = h.generate_valid_uid("term_del")