Skip to content

Commit 51f5fd9

Browse files
Merge pull request #141 from contentstack/enh/dx-3059
Added variants support in Python CDA SDK
2 parents bbf9bef + c0e6400 commit 51f5fd9

File tree

6 files changed

+156
-2
lines changed

6 files changed

+156
-2
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.2.0_
4+
5+
### **Date: 14-July-2025**
6+
7+
- Variants Support Added.
8+
39
## _v2.1.1_
410

511
### **Date: 07-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.1.1'
25+
__version__ = 'v2.2.0'
2626
__endpoint__ = 'cdn.contentstack.io'
2727
__email__ = '[email protected]'
2828
__developer_email__ = '[email protected]'

contentstack/contenttype.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from contentstack.entry import Entry
1515
from contentstack.query import Query
16+
from contentstack.variants import Variants
1617

1718
class ContentType:
1819
"""
@@ -118,3 +119,18 @@ def find(self, params=None):
118119
url = f'{endpoint}/content_types?{encoded_params}'
119120
result = self.http_instance.get(url)
120121
return result
122+
123+
def variants(self, variant_uid: str | list[str], params: dict = None):
124+
"""
125+
Fetches the variants of the content type
126+
:param variant_uid: {str} -- variant_uid
127+
:return: Entry, so you can chain this call.
128+
"""
129+
return Variants(
130+
http_instance=self.http_instance,
131+
content_type_uid=self.__content_type_uid,
132+
entry_uid=None,
133+
variant_uid=variant_uid,
134+
params=params,
135+
logger=None
136+
)

contentstack/entry.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from contentstack.deep_merge_lp import DeepMergeMixin
1010
from contentstack.entryqueryable import EntryQueryable
11+
from contentstack.variants import Variants
1112

1213
class Entry(EntryQueryable):
1314
"""
@@ -222,6 +223,22 @@ def _merged_response(self):
222223
merged_response = DeepMergeMixin(entry_response, lp_entry).to_dict() # Convert to dictionary
223224
return merged_response # Now correctly returns a dictionary
224225
raise ValueError("Missing required keys in live_preview data")
226+
227+
def variants(self, variant_uid: str | list[str], params: dict = None):
228+
"""
229+
Fetches the variants of the entry
230+
:param variant_uid: {str} -- variant_uid
231+
:return: Entry, so you can chain this call.
232+
"""
233+
return Variants(
234+
http_instance=self.http_instance,
235+
content_type_uid=self.content_type_id,
236+
entry_uid=self.entry_uid,
237+
variant_uid=variant_uid,
238+
params=params,
239+
logger=self.logger
240+
)
241+
225242

226243

227244

contentstack/variants.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import logging
2+
from urllib import parse
3+
4+
from contentstack.entryqueryable import EntryQueryable
5+
6+
class Variants(EntryQueryable):
7+
"""
8+
An entry is the actual piece of content that you want to publish.
9+
Entries can be created for one of the available content types.
10+
11+
Entry works with
12+
version={version_number}
13+
environment={environment_name}
14+
locale={locale_code}
15+
"""
16+
17+
def __init__(self,
18+
http_instance=None,
19+
content_type_uid=None,
20+
entry_uid=None,
21+
variant_uid=None,
22+
params=None,
23+
logger=None):
24+
25+
super().__init__()
26+
EntryQueryable.__init__(self)
27+
self.entry_param = {}
28+
self.http_instance = http_instance
29+
self.content_type_id = content_type_uid
30+
self.entry_uid = entry_uid
31+
self.variant_uid = variant_uid
32+
self.logger = logger or logging.getLogger(__name__)
33+
self.entry_param = params or {}
34+
35+
def find(self, params=None):
36+
"""
37+
find the variants of the entry of a particular content type
38+
:param self.variant_uid: {str} -- self.variant_uid
39+
:return: Entry, so you can chain this call.
40+
"""
41+
headers = self.http_instance.headers.copy() # Create a local copy of headers
42+
if isinstance(self.variant_uid, str):
43+
headers['x-cs-variant-uid'] = self.variant_uid
44+
elif isinstance(self.variant_uid, list):
45+
headers['x-cs-variant-uid'] = ','.join(self.variant_uid)
46+
47+
if params is not None:
48+
self.entry_param.update(params)
49+
encoded_params = parse.urlencode(self.entry_param)
50+
endpoint = self.http_instance.endpoint
51+
url = f'{endpoint}/content_types/{self.content_type_id}/entries?{encoded_params}'
52+
self.http_instance.headers.update(headers)
53+
result = self.http_instance.get(url)
54+
self.http_instance.headers.pop('x-cs-variant-uid', None)
55+
return result
56+
57+
def fetch(self, params=None):
58+
"""
59+
This method is useful to fetch variant entries of a particular content type and entries of the of the stack.
60+
:return:dict -- contentType response
61+
------------------------------
62+
Example:
63+
64+
>>> import contentstack
65+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
66+
>>> content_type = stack.content_type('content_type_uid')
67+
>>> some_dict = {'abc':'something'}
68+
>>> response = content_type.fetch(some_dict)
69+
------------------------------
70+
"""
71+
"""
72+
Fetches the variants of the entry
73+
:param self.variant_uid: {str} -- self.variant_uid
74+
:return: Entry, so you can chain this call.
75+
"""
76+
if self.entry_uid is None:
77+
raise ValueError("entry_uid is required")
78+
else:
79+
headers = self.http_instance.headers.copy() # Create a local copy of headers
80+
if isinstance(self.variant_uid, str):
81+
headers['x-cs-variant-uid'] = self.variant_uid
82+
elif isinstance(self.variant_uid, list):
83+
headers['x-cs-variant-uid'] = ','.join(self.variant_uid)
84+
85+
if params is not None:
86+
self.entry_param.update(params)
87+
encoded_params = parse.urlencode(self.entry_param)
88+
endpoint = self.http_instance.endpoint
89+
url = f'{endpoint}/content_types/{self.content_type_id}/entries/{self.entry_uid}?{encoded_params}'
90+
self.http_instance.headers.update(headers)
91+
result = self.http_instance.get(url)
92+
self.http_instance.headers.pop('x-cs-variant-uid', None)
93+
return result

tests/test_entry.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
ENVIRONMENT = config.ENVIRONMENT
99
HOST = config.HOST
1010
FAQ_UID = config.FAQ_UID # Add this in your config.py
11-
11+
VARIANT_UID = config.VARIANT_UID
1212

1313
class TestEntry(unittest.TestCase):
1414

@@ -134,6 +134,28 @@ def test_22_entry_include_metadata(self):
134134
content_type = self.stack.content_type('faq')
135135
entry = content_type.entry("878783238783").include_metadata()
136136
self.assertEqual({'include_metadata': 'true'}, entry.entry_queryable_param)
137+
138+
def test_23_content_type_variants(self):
139+
content_type = self.stack.content_type('faq')
140+
entry = content_type.variants(VARIANT_UID).find()
141+
self.assertIn('variants', entry['entries'][0]['publish_details'])
142+
143+
def test_24_entry_variants(self):
144+
content_type = self.stack.content_type('faq')
145+
entry = content_type.entry(FAQ_UID).variants(VARIANT_UID).fetch()
146+
self.assertIn('variants', entry['entry']['publish_details'])
147+
148+
def test_25_content_type_variants_with_has_hash_variant(self):
149+
content_type = self.stack.content_type('faq')
150+
entry = content_type.variants([VARIANT_UID]).find()
151+
self.assertIn('variants', entry['entries'][0]['publish_details'])
152+
153+
def test_25_content_type_entry_variants_with_has_hash_variant(self):
154+
content_type = self.stack.content_type('faq').entry(FAQ_UID)
155+
entry = content_type.variants([VARIANT_UID]).fetch()
156+
self.assertIn('variants', entry['entry']['publish_details'])
157+
158+
137159

138160

139161
if __name__ == '__main__':

0 commit comments

Comments
 (0)