Skip to content

Commit ba58d4f

Browse files
authored
Merge pull request #217 from contentstack/enhc/DX-9682
feat: add Taxonomy CDA support (list/fetch taxonomies, terms, hierarchy)
2 parents 39cdb3f + fa9b62e commit ba58d4f

8 files changed

Lines changed: 1222 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## _v2.8.0_
4+
5+
### **Date: 28-July-2026**
6+
7+
- Added Taxonomy CDA support: `stack.taxonomy(uid)` now fetches a single published taxonomy (`.fetch()`), lists its terms (`.term().find()`), and fetches/traverses a single term (`.term(uid).fetch()/.locales()/.ancestors()/.descendants()`), with `.locale()`, `.include_fallback()`, `.include_branch()`, `.depth()`, `.skip()`, `.limit()`, `.include_count()`, and `.param()` chainable across the new classes. `stack.taxonomy()` (no uid) now also lists all published taxonomies via `.find()` when no filter is chained — existing entry-filter usage (`.in_()`, `.above()`, `.below()`, `.exists()`, etc. followed by `.find()`) is fully backward compatible and unchanged.
8+
39
## _v2.7.1_
410

511
### **Date: 22-July-2026**

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,36 @@ image = stack.image_transform(url, {'width': 100, 'height': 100}).get_url()
126126
image = stack.image_transform(url, {'auto': 'webp'}).get_url()
127127
```
128128

129+
### Working with Taxonomies
130+
131+
Taxonomies published to an environment/locale can be fetched directly via the Content Delivery API.
132+
133+
```python
134+
# List all published taxonomies
135+
result = stack.taxonomy().limit(10).include_count().find()
136+
taxonomies = result['taxonomies']
137+
138+
# Fetch a single taxonomy, with locale fallback
139+
taxonomy = stack.taxonomy('regions').locale('fr-fr').include_fallback().fetch()
140+
141+
# List all terms in a taxonomy
142+
terms_result = stack.taxonomy('regions').term().locale('en-us').depth(3).find()
143+
144+
# Fetch a single term
145+
term = stack.taxonomy('regions').term('california').fetch()
146+
147+
# Term hierarchy traversal
148+
ancestors = stack.taxonomy('regions').term('san-francisco').depth(5).ancestors()
149+
descendants = stack.taxonomy('electronics').term('laptops').depth(2).descendants()
150+
locales = stack.taxonomy('regions').term('california').locales()
151+
```
152+
153+
`stack.taxonomy()` (no uid) with a filter chained still filters entries by taxonomy, unchanged:
154+
155+
```python
156+
result = stack.taxonomy().in_('taxonomies.category', ['category1', 'category2']).find()
157+
```
158+
129159
### Using the Sync API with Python SDK
130160

131161
The Sync API takes care of syncing your Contentstack data with your application and ensures that the data is always

contentstack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def get_contentstack_endpoint(region='us', service='', omit_https=False):
4242
__title__ = 'contentstack-delivery-python'
4343
__author__ = 'contentstack'
4444
__status__ = 'debug'
45-
__version__ = 'v2.7.1'
45+
__version__ = 'v2.8.0'
4646
__endpoint__ = 'cdn.contentstack.io'
4747
__email__ = 'support@contentstack.com'
4848
__developer_email__ = 'mobile@contentstack.com'

contentstack/stack.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from contentstack.assetquery import AssetQuery
99
from contentstack.contenttype import ContentType
1010
from contentstack.endpoint import Endpoint
11-
from contentstack.taxonomy import Taxonomy
11+
from contentstack.taxonomy import Taxonomy, TaxonomyQuery
1212
from contentstack.globalfields import GlobalField
1313
from contentstack.https_connection import HTTPSConnection
1414
from contentstack.image_transform import ImageTransform
@@ -202,13 +202,35 @@ def content_type(self, content_type_uid=None):
202202
"""
203203
return ContentType(self.http_instance, content_type_uid)
204204

205-
def taxonomy(self):
205+
def taxonomy(self, taxonomy_uid: str = None):
206206
"""
207-
taxonomy defines the structure or schema of a page or a section
208-
of your web or mobile property.
209-
:return: taxonomy
207+
Without taxonomy_uid: returns a TaxonomyQuery. Chain a filter
208+
(in_, or_, and_, exists, above, below, equal_and_above, equal_and_below)
209+
then find() to fetch entries filtered by taxonomy (legacy behavior,
210+
unchanged). Or call find() directly with nothing chained to list all
211+
published taxonomies from the Content Delivery API.
212+
213+
With taxonomy_uid: returns a Taxonomy for CDA access to that specific
214+
published taxonomy (fetch(), term()).
215+
216+
:param taxonomy_uid: {str} -- (optional) unique identifier of the taxonomy.
217+
:return: TaxonomyQuery or Taxonomy
218+
-----------------------------
219+
Example::
220+
221+
>>> import contentstack
222+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
223+
>>> # Legacy: filter entries by taxonomy
224+
>>> result = stack.taxonomy().in_('taxonomies.color', ['red']).find()
225+
>>> # New: list all published taxonomies
226+
>>> result = stack.taxonomy().limit(10).find()
227+
>>> # New: fetch a single published taxonomy
228+
>>> result = stack.taxonomy('regions').fetch()
229+
-----------------------------
210230
"""
211-
return Taxonomy(self.http_instance)
231+
if taxonomy_uid:
232+
return Taxonomy(self.http_instance, taxonomy_uid)
233+
return TaxonomyQuery(self.http_instance)
212234

213235
def global_field(self, global_field_uid=None):
214236
"""

contentstack/taxonomy.py

Lines changed: 180 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,51 @@
11
import json
2-
from urllib import parse
32
from urllib.parse import quote
43

4+
from contentstack.error_messages import ErrorMessages
5+
from contentstack.utility import Utils
56

67

7-
class Taxonomy:
8+
class TaxonomyFilter:
9+
"""
10+
Taxonomy entry-filter queries ($above, $below, $in, $exists, ...).
11+
Use stack.taxonomy() to instantiate.
12+
13+
API Reference: https://www.contentstack.com/docs/developers/apis/content-delivery-api/#taxonomies
14+
"""
15+
816
def __init__(self, http_instance):
917
self.http_instance = http_instance
1018
self._filters: dict = {}
1119

12-
def _add(self, field: str, condition: dict) -> "TaxonomyQuery":
20+
def _add(self, field: str, condition: dict) -> "TaxonomyFilter":
1321
self._filters[field] = condition
1422
return self
1523

16-
def in_(self, field: str, terms: list) -> "TaxonomyQuery":
24+
def in_(self, field: str, terms: list) -> "TaxonomyFilter":
1725
return self._add(field, {"$in": terms})
1826

19-
def or_(self, *conds: dict) -> "TaxonomyQuery":
27+
def or_(self, *conds: dict) -> "TaxonomyFilter":
2028
return self._add("$or", list(conds))
2129

22-
def and_(self, *conds: dict) -> "TaxonomyQuery":
30+
def and_(self, *conds: dict) -> "TaxonomyFilter":
2331
return self._add("$and", list(conds))
2432

25-
def exists(self, field: str) -> "TaxonomyQuery":
33+
def exists(self, field: str) -> "TaxonomyFilter":
2634
return self._add(field, {"$exists": True})
2735

28-
def equal_and_below(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
36+
def equal_and_below(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyFilter":
2937
cond = {"$eq_below": term_uid, "levels": levels}
3038
return self._add(field, cond)
3139

32-
def below(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
40+
def below(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyFilter":
3341
cond = {"$below": term_uid, "levels": levels}
3442
return self._add(field, cond)
3543

36-
def equal_and_above(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
44+
def equal_and_above(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyFilter":
3745
cond = {"$eq_above": term_uid, "levels": levels}
3846
return self._add(field, cond)
3947

40-
def above(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
48+
def above(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyFilter":
4149
cond = {"$above": term_uid, "levels": levels}
4250
return self._add(field, cond)
4351

@@ -62,3 +70,164 @@ def find(self, params=None):
6270
url += f'&{other_params}'
6371
return self.http_instance.get(url)
6472

73+
74+
class TaxonomyQuery(TaxonomyFilter):
75+
"""
76+
stack.taxonomy() — no uid.
77+
78+
Chain a filter (in_, or_, and_, exists, above, below, equal_and_above,
79+
equal_and_below) then call find() to fetch entries filtered by taxonomy
80+
(unchanged legacy behavior, inherited from TaxonomyFilter).
81+
82+
Call find() directly with nothing chained to list all published
83+
taxonomies from the Content Delivery API instead.
84+
85+
Example::
86+
87+
>>> import contentstack
88+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
89+
>>> # Legacy: filter entries by taxonomy
90+
>>> result = stack.taxonomy().in_('taxonomies.color', ['red']).find()
91+
>>> # New: list all published taxonomies
92+
>>> result = stack.taxonomy().limit(10).include_count().find()
93+
"""
94+
95+
def __init__(self, http_instance):
96+
super().__init__(http_instance)
97+
self._query_params = {}
98+
99+
def skip(self, skip: int) -> "TaxonomyQuery":
100+
"""Sets pagination offset for listing taxonomies.
101+
:param skip: {int} -- number of taxonomies to skip
102+
:return: TaxonomyQuery, so you can chain this call.
103+
"""
104+
self._query_params['skip'] = skip
105+
return self
106+
107+
def limit(self, limit: int) -> "TaxonomyQuery":
108+
"""Sets the maximum number of taxonomies to return.
109+
:param limit: {int} -- max number of taxonomies to return
110+
:return: TaxonomyQuery, so you can chain this call.
111+
"""
112+
self._query_params['limit'] = limit
113+
return self
114+
115+
def include_count(self) -> "TaxonomyQuery":
116+
"""Includes the total count of taxonomies in the response.
117+
:return: TaxonomyQuery, so you can chain this call.
118+
"""
119+
self._query_params['include_count'] = 'true'
120+
return self
121+
122+
def param(self, key: str, value) -> "TaxonomyQuery":
123+
"""Adds an arbitrary query parameter to the list-taxonomies request
124+
(e.g. 'locale').
125+
:param key: {str} -- query parameter key
126+
:param value: value for the query parameter
127+
:return: TaxonomyQuery, so you can chain this call.
128+
"""
129+
if None in (key, value):
130+
raise KeyError(ErrorMessages.INVALID_KEY_VALUE_ARGS)
131+
self._query_params[key] = value
132+
return self
133+
134+
def find(self, params=None):
135+
"""
136+
If a filter was chained (in_/or_/and_/exists/above/below/...), fetches
137+
entries filtered by taxonomy — unchanged legacy behavior.
138+
GET /taxonomies/entries?environment=...&query=...
139+
140+
Otherwise, fetches all published taxonomies from the CDA.
141+
GET /taxonomies
142+
143+
:return: dict -- filtered entries, or {'taxonomies': [...], 'count'?: n}
144+
"""
145+
if self._filters:
146+
return super().find(params)
147+
if params:
148+
self._query_params.update(params)
149+
url = f'{self.http_instance.endpoint}/taxonomies'
150+
query_str = Utils.do_url_encode(self._query_params)
151+
return self.http_instance.get(f'{url}?{query_str}' if query_str else url)
152+
153+
154+
class Taxonomy:
155+
"""
156+
Represents a single published taxonomy from the Content Delivery API.
157+
Use stack.taxonomy(uid) to instantiate.
158+
159+
Example::
160+
161+
>>> import contentstack
162+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
163+
>>> taxonomy = stack.taxonomy('regions').locale('fr-fr').include_fallback()
164+
>>> result = taxonomy.fetch()
165+
"""
166+
167+
def __init__(self, http_instance, taxonomy_uid: str):
168+
if not taxonomy_uid:
169+
raise KeyError(ErrorMessages.INVALID_KEY_OR_VALUE)
170+
self.http_instance = http_instance
171+
self._taxonomy_uid = taxonomy_uid
172+
self._url = f'{http_instance.endpoint}/taxonomies/{taxonomy_uid}'
173+
self._query_params = {}
174+
175+
def term(self, term_uid: str = None):
176+
"""
177+
Without term_uid: returns TermQuery for listing all terms in this taxonomy.
178+
With term_uid: returns Term for a specific term.
179+
180+
:param term_uid: {str} -- (optional) unique identifier of the term.
181+
:return: Term or TermQuery
182+
"""
183+
from contentstack.term import Term, TermQuery
184+
if term_uid:
185+
return Term(self.http_instance, self._taxonomy_uid, term_uid)
186+
return TermQuery(self.http_instance, self._taxonomy_uid)
187+
188+
def locale(self, locale: str) -> "Taxonomy":
189+
"""Sets the locale for this taxonomy fetch (e.g. 'en-us', 'fr-fr').
190+
:param locale: {str} -- locale code
191+
:return: Taxonomy, so you can chain this call.
192+
"""
193+
self._query_params['locale'] = locale
194+
return self
195+
196+
def include_fallback(self) -> "Taxonomy":
197+
"""Enables locale fallback through the branch hierarchy.
198+
If the taxonomy is not published in the requested locale, falls back
199+
to the parent locale in the branch hierarchy.
200+
:return: Taxonomy, so you can chain this call.
201+
"""
202+
self._query_params['include_fallback'] = 'true'
203+
return self
204+
205+
def include_branch(self) -> "Taxonomy":
206+
"""Includes the _branch field in the response.
207+
:return: Taxonomy, so you can chain this call.
208+
"""
209+
self._query_params['include_branch'] = 'true'
210+
return self
211+
212+
def param(self, key: str, value) -> "Taxonomy":
213+
"""Adds an arbitrary query parameter.
214+
:param key: {str} -- query parameter key
215+
:param value: value for the query parameter
216+
:return: Taxonomy, so you can chain this call.
217+
"""
218+
if None in (key, value):
219+
raise KeyError(ErrorMessages.INVALID_KEY_VALUE_ARGS)
220+
self._query_params[key] = value
221+
return self
222+
223+
def fetch(self) -> dict:
224+
"""
225+
Fetches this taxonomy from the CDA.
226+
GET /taxonomies/{taxonomy_uid}
227+
228+
:return: dict -- the taxonomy object.
229+
"""
230+
query_str = Utils.do_url_encode(self._query_params)
231+
url = f'{self._url}?{query_str}' if query_str else self._url
232+
response = self.http_instance.get(url)
233+
return response.get('taxonomy', response)

0 commit comments

Comments
 (0)