Skip to content

Commit 9ed229a

Browse files
authored
Merge pull request #149 from contentstack/staging
DX | 21-07-2025 | Release
2 parents c7864e4 + ea77e12 commit 9ed229a

File tree

7 files changed

+184
-3
lines changed

7 files changed

+184
-3
lines changed

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.3.0_
4+
5+
### **Date: 21-July-2025**
6+
7+
- Taxonomy Support Added.
8+
39
## _v2.2.0_
410

511
### **Date: 14-July-2025**

contentstack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
__title__ = 'contentstack-delivery-python'
2323
__author__ = 'contentstack'
2424
__status__ = 'debug'
25-
__version__ = 'v2.2.0'
25+
__version__ = 'v2.3.0'
2626
__endpoint__ = 'cdn.contentstack.io'
2727
__email__ = '[email protected]'
2828
__developer_email__ = '[email protected]'

contentstack/stack.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from contentstack.asset import Asset
77
from contentstack.assetquery import AssetQuery
88
from contentstack.contenttype import ContentType
9+
from contentstack.taxonomy import Taxonomy
910
from contentstack.globalfields import GlobalField
1011
from contentstack.https_connection import HTTPSConnection
1112
from contentstack.image_transform import ImageTransform
@@ -204,6 +205,14 @@ def content_type(self, content_type_uid=None):
204205
"""
205206
return ContentType(self.http_instance, content_type_uid)
206207

208+
def taxonomy(self):
209+
"""
210+
taxonomy defines the structure or schema of a page or a section
211+
of your web or mobile property.
212+
:return: taxonomy
213+
"""
214+
return Taxonomy(self.http_instance)
215+
207216
def global_field(self, global_field_uid=None):
208217
"""
209218
Global field defines the structure or schema of a page or a section

contentstack/taxonomy.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import json
2+
from urllib import parse
3+
from urllib.parse import quote
4+
5+
6+
7+
class Taxonomy:
8+
def __init__(self, http_instance):
9+
self.http_instance = http_instance
10+
self._filters: dict = {}
11+
12+
def _add(self, field: str, condition: dict) -> "TaxonomyQuery":
13+
self._filters[field] = condition
14+
return self
15+
16+
def in_(self, field: str, terms: list) -> "TaxonomyQuery":
17+
return self._add(field, {"$in": terms})
18+
19+
def or_(self, *conds: dict) -> "TaxonomyQuery":
20+
return self._add("$or", list(conds))
21+
22+
def and_(self, *conds: dict) -> "TaxonomyQuery":
23+
return self._add("$and", list(conds))
24+
25+
def exists(self, field: str) -> "TaxonomyQuery":
26+
return self._add(field, {"$exists": True})
27+
28+
def equal_and_below(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
29+
cond = {"$eq_below": term_uid, "levels": levels}
30+
return self._add(field, cond)
31+
32+
def below(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
33+
cond = {"$below": term_uid, "levels": levels}
34+
return self._add(field, cond)
35+
36+
def equal_and_above(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
37+
cond = {"$eq_above": term_uid, "levels": levels}
38+
return self._add(field, cond)
39+
40+
def above(self, field: str, term_uid: str, levels: int = 10) -> "TaxonomyQuery":
41+
cond = {"$above": term_uid, "levels": levels}
42+
return self._add(field, cond)
43+
44+
def find(self, params=None):
45+
"""
46+
This method fetches entries filtered by taxonomy from the stack.
47+
"""
48+
self.local_param = {}
49+
self.local_param['environment'] = self.http_instance.headers['environment']
50+
51+
# Ensure query param is always present
52+
query_string = json.dumps(self._filters or {})
53+
query_encoded = quote(query_string, safe='{}":,[]') # preserves JSON characters
54+
55+
# Build the base URL
56+
endpoint = self.http_instance.endpoint
57+
url = f'{endpoint}/taxonomies/entries?environment={self.local_param["environment"]}&query={query_encoded}'
58+
59+
# Append any additional params manually
60+
if params:
61+
other_params = '&'.join(f'{k}={v}' for k, v in params.items())
62+
url += f'&{other_params}'
63+
return self.http_instance.get(url)
64+

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ tox==4.5.1
88
virtualenv==20.26.6
99
Sphinx==7.3.7
1010
sphinxcontrib-websupport==1.2.7
11-
pip==23.3.1
11+
pip==25.1.1
1212
build==0.10.0
1313
wheel==0.45.1
1414
lxml==5.3.1

tests/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .test_early_fetch import TestGlobalFieldFetch
1717
from .test_early_find import TestGlobalFieldFind
1818
from .test_live_preview import TestLivePreviewConfig
19+
from .test_taxonomies import TestTaxonomyAPI
1920

2021

2122
def all_tests():
@@ -27,6 +28,7 @@ def all_tests():
2728
test_module_globalFields = TestLoader().loadTestsFromName(TestGlobalFieldInit)
2829
test_module_globalFields_fetch = TestLoader().loadTestsFromName(TestGlobalFieldFetch)
2930
test_module_globalFields_find = TestLoader().loadTestsFromName(TestGlobalFieldFind)
31+
test_module_taxonomies = TestLoader().loadTestsFromTestCase(TestTaxonomyAPI)
3032
TestSuite([
3133
test_module_stack,
3234
test_module_asset,
@@ -35,5 +37,6 @@ def all_tests():
3537
test_module_live_preview,
3638
test_module_globalFields,
3739
test_module_globalFields_fetch,
38-
test_module_globalFields_find
40+
test_module_globalFields_find,
41+
test_module_taxonomies
3942
])

tests/test_taxonomies.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import logging
2+
import unittest
3+
import config
4+
import contentstack
5+
import pytest
6+
7+
API_KEY = config.APIKEY
8+
DELIVERY_TOKEN = config.DELIVERYTOKEN
9+
ENVIRONMENT = config.ENVIRONMENT
10+
HOST = config.HOST
11+
12+
class TestTaxonomyAPI(unittest.TestCase):
13+
def setUp(self):
14+
self.stack = contentstack.Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, host=HOST)
15+
16+
def test_01_taxonomy_complex_query(self):
17+
"""Test complex taxonomy query combining multiple filters"""
18+
taxonomy = self.stack.taxonomy()
19+
result = taxonomy.and_(
20+
{"taxonomies.category": {"$in": ["test"]}},
21+
{"taxonomies.test1": {"$exists": True}}
22+
).or_(
23+
{"taxonomies.status": {"$in": ["active"]}},
24+
{"taxonomies.priority": {"$in": ["high"]}}
25+
).find({'limit': 10})
26+
if result is not None:
27+
self.assertIn('entries', result)
28+
29+
def test_02_taxonomy_in_query(self):
30+
"""Test taxonomy query with $in filter"""
31+
taxonomy = self.stack.taxonomy()
32+
result = taxonomy.in_("taxonomies.category", ["category1", "category2"]).find()
33+
if result is not None:
34+
self.assertIn('entries', result)
35+
36+
def test_03_taxonomy_exists_query(self):
37+
"""Test taxonomy query with $exists filter"""
38+
taxonomy = self.stack.taxonomy()
39+
result = taxonomy.exists("taxonomies.test1").find()
40+
if result is not None:
41+
self.assertIn('entries', result)
42+
43+
def test_04_taxonomy_or_query(self):
44+
"""Test taxonomy query with $or filter"""
45+
taxonomy = self.stack.taxonomy()
46+
result = taxonomy.or_(
47+
{"taxonomies.category": {"$in": ["category1"]}},
48+
{"taxonomies.test1": {"$exists": True}}
49+
).find()
50+
if result is not None:
51+
self.assertIn('entries', result)
52+
53+
def test_05_taxonomy_and_query(self):
54+
"""Test taxonomy query with $and filter"""
55+
taxonomy = self.stack.taxonomy()
56+
result = taxonomy.and_(
57+
{"taxonomies.category": {"$in": ["category1"]}},
58+
{"taxonomies.test1": {"$exists": True}}
59+
).find()
60+
if result is not None:
61+
self.assertIn('entries', result)
62+
63+
def test_06_taxonomy_equal_and_below(self):
64+
"""Test taxonomy query with $eq_below filter"""
65+
taxonomy = self.stack.taxonomy()
66+
result = taxonomy.equal_and_below("taxonomies.color", "blue", levels=1).find()
67+
if result is not None:
68+
self.assertIn('entries', result)
69+
70+
def test_07_taxonomy_below(self):
71+
"""Test taxonomy query with $below filter"""
72+
taxonomy = self.stack.taxonomy()
73+
result = taxonomy.below("taxonomies.hierarchy", "parent_uid", levels=2).find()
74+
if result is not None:
75+
self.assertIn('entries', result)
76+
77+
def test_08_taxonomy_equal_and_above(self):
78+
"""Test taxonomy query with $eq_above filter"""
79+
taxonomy = self.stack.taxonomy()
80+
result = taxonomy.equal_and_above("taxonomies.hierarchy", "child_uid", levels=3).find()
81+
if result is not None:
82+
self.assertIn('entries', result)
83+
84+
def test_09_taxonomy_above(self):
85+
"""Test taxonomy query with $above filter"""
86+
taxonomy = self.stack.taxonomy()
87+
result = taxonomy.above("taxonomies.hierarchy", "child_uid", levels=2).find()
88+
if result is not None:
89+
self.assertIn('entries', result)
90+
91+
def test_10_taxonomy_find_with_params(self):
92+
"""Test taxonomy find with additional parameters"""
93+
taxonomy = self.stack.taxonomy()
94+
result = taxonomy.in_("taxonomies.category", ["test"]).find({'limit': 5})
95+
if result is not None:
96+
self.assertIn('entries', result)
97+
98+
if __name__ == '__main__':
99+
unittest.main()

0 commit comments

Comments
 (0)