Skip to content

Commit c7864e4

Browse files
Merge pull request #143 from contentstack/staging
DX | 14-07-2025 | Release
2 parents 3726c1e + 2ee6a16 commit c7864e4

File tree

9 files changed

+194
-51
lines changed

9 files changed

+194
-51
lines changed

CHANGELOG.md

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

3+
## _v2.2.0_
4+
5+
### **Date: 14-July-2025**
6+
7+
- Variants Support Added.
8+
9+
## _v2.1.1_
10+
11+
### **Date: 07-July-2025**
12+
13+
- Fixed sanity testcases and removed hardcoded secrets.
14+
315
## _v2.1.0_
416

517
### **Date: 02-June-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.0'
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

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ twython==3.9.1
22
setuptools==80.3.1
33
contentstack-utils==1.3.0
44
python-dateutil==2.8.2
5-
requests==2.32.3
5+
requests==2.32.4
66
coverage==7.6.0
77
tox==4.5.1
88
virtualenv==20.26.6
@@ -58,6 +58,6 @@ zipp==3.20.1
5858
distlib~=0.3.8
5959
cachetools~=5.4.0
6060
tomlkit~=0.13.2
61-
urllib3==2.2.3
61+
urllib3==2.5.0
6262
exceptiongroup~=1.2.2
6363
iniconfig~=2.0.0

tests/test_entry.py

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import config
44
import contentstack
55

6-
_UID = 'blt53ca1231625bdde4'
76
API_KEY = config.APIKEY
87
DELIVERY_TOKEN = config.DELIVERYTOKEN
98
ENVIRONMENT = config.ENVIRONMENT
109
HOST = config.HOST
11-
10+
FAQ_UID = config.FAQ_UID # Add this in your config.py
11+
VARIANT_UID = config.VARIANT_UID
1212

1313
class TestEntry(unittest.TestCase):
1414

@@ -19,69 +19,54 @@ def test_run_initial_query(self):
1919
query = self.stack.content_type('faq').query()
2020
result = query.find()
2121
if result is not None:
22-
self._UID = result['entries'][0]['uid']
23-
print(f'the uid is: {_UID}')
22+
self.faq_uid = result['entries'][0]['uid']
23+
print(f'the uid is: {self.faq_uid}')
2424

2525
def test_entry_by_UID(self):
26-
global _UID
27-
entry = self.stack.content_type('faq').entry(_UID)
26+
entry = self.stack.content_type('faq').entry(FAQ_UID)
2827
result = entry.fetch()
2928
if result is not None:
30-
_UID = result['entry']['uid']
31-
self.assertEqual(_UID, result['entry']['uid'])
29+
self.assertEqual(FAQ_UID, result['entry']['uid'])
3230

3331
def test_03_entry_environment(self):
34-
global _UID
35-
entry = self.stack.content_type('faq').entry(
36-
_UID).environment('test')
32+
entry = self.stack.content_type('faq').entry(FAQ_UID).environment('test')
3733
self.assertEqual("test", entry.http_instance.headers['environment'])
3834

3935
def test_04_entry_locale(self):
40-
global _UID
41-
entry = self.stack.content_type('faq').entry(_UID).locale('en-ei')
36+
entry = self.stack.content_type('faq').entry(FAQ_UID).locale('en-ei')
4237
entry.fetch()
4338
self.assertEqual('en-ei', entry.entry_param['locale'])
4439

4540
def test_05_entry_version(self):
46-
global _UID
47-
entry = self.stack.content_type('faq').entry(_UID).version(3)
41+
entry = self.stack.content_type('faq').entry(FAQ_UID).version(3)
4842
entry.fetch()
4943
self.assertEqual(3, entry.entry_param['version'])
5044

5145
def test_06_entry_params(self):
52-
global _UID
53-
entry = self.stack.content_type('faq').entry(
54-
_UID).param('param_key', 'param_value')
46+
entry = self.stack.content_type('faq').entry(FAQ_UID).param('param_key', 'param_value')
5547
entry.fetch()
5648
self.assertEqual('param_value', entry.entry_param['param_key'])
5749

5850
def test_07_entry_base_only(self):
59-
global _UID
60-
entry = self.stack.content_type(
61-
'faq').entry(_UID).only('field_UID')
51+
entry = self.stack.content_type('faq').entry(FAQ_UID).only('field_UID')
6252
entry.fetch()
6353
self.assertEqual({'environment': 'development',
6454
'only[BASE][]': 'field_UID'}, entry.entry_param)
6555

6656
def test_08_entry_base_excepts(self):
67-
global _UID
68-
entry = self.stack.content_type('faq').entry(
69-
_UID).excepts('field_UID')
57+
entry = self.stack.content_type('faq').entry(FAQ_UID).excepts('field_UID')
7058
entry.fetch()
7159
self.assertEqual({'environment': 'development',
7260
'except[BASE][]': 'field_UID'}, entry.entry_param)
7361

7462
def test_10_entry_base_include_reference_only(self):
75-
global _UID
76-
entry = self.stack.content_type('faq').entry(_UID).only('field1')
63+
entry = self.stack.content_type('faq').entry(FAQ_UID).only('field1')
7764
entry.fetch()
7865
self.assertEqual({'environment': 'development', 'only[BASE][]': 'field1'},
7966
entry.entry_param)
8067

8168
def test_11_entry_base_include_reference_excepts(self):
82-
global _UID
83-
entry = self.stack.content_type(
84-
'faq').entry(_UID).excepts('field1')
69+
entry = self.stack.content_type('faq').entry(FAQ_UID).excepts('field1')
8570
entry.fetch()
8671
self.assertEqual({'environment': 'development', 'except[BASE][]': 'field1'},
8772
entry.entry_param)
@@ -95,15 +80,13 @@ def test_12_entry_include_reference_github_issue(self):
9580
response = _entry.fetch()
9681

9782
def test_13_entry_support_include_fallback_unit_test(self):
98-
global _UID
99-
entry = self.stack.content_type('faq').entry(
100-
_UID).include_fallback()
83+
entry = self.stack.content_type('faq').entry(FAQ_UID).include_fallback()
10184
self.assertEqual(
10285
True, entry.entry_param.__contains__('include_fallback'))
10386

10487
def test_14_entry_queryable_only(self):
10588
try:
106-
entry = self.stack.content_type('faq').entry(_UID).only(4)
89+
entry = self.stack.content_type('faq').entry(FAQ_UID).only(4)
10790
result = entry.fetch()
10891
self.assertEqual(None, result['uid'])
10992
except KeyError as e:
@@ -112,28 +95,25 @@ def test_14_entry_queryable_only(self):
11295

11396
def test_entry_queryable_excepts(self):
11497
try:
115-
entry = self.stack.content_type('faq').entry(_UID).excepts(4)
98+
entry = self.stack.content_type('faq').entry(FAQ_UID).excepts(4)
11699
result = entry.fetch()
117100
self.assertEqual(None, result['uid'])
118101
except KeyError as e:
119102
if hasattr(e, 'message'):
120103
self.assertEqual("Invalid field_UID provided", e.args[0])
121104

122105
def test_16_entry_queryable_include_content_type(self):
123-
entry = self.stack.content_type('faq').entry(
124-
_UID).include_content_type()
106+
entry = self.stack.content_type('faq').entry(FAQ_UID).include_content_type()
125107
self.assertEqual({'include_content_type': 'true', 'include_global_field_schema': 'true'},
126108
entry.entry_queryable_param)
127109

128110
def test_reference_content_type_uid(self):
129-
entry = self.stack.content_type('faq').entry(
130-
_UID).include_reference_content_type_uid()
111+
entry = self.stack.content_type('faq').entry(FAQ_UID).include_reference_content_type_uid()
131112
self.assertEqual({'include_reference_content_type_uid': 'true'},
132113
entry.entry_queryable_param)
133114

134115
def test_19_entry_queryable_add_param(self):
135-
entry = self.stack.content_type('faq').entry(
136-
_UID).add_param('cms', 'contentstack')
116+
entry = self.stack.content_type('faq').entry(FAQ_UID).add_param('cms', 'contentstack')
137117
self.assertEqual({'cms': 'contentstack'}, entry.entry_queryable_param)
138118

139119
def test_20_entry_include_fallback(self):
@@ -154,6 +134,28 @@ def test_22_entry_include_metadata(self):
154134
content_type = self.stack.content_type('faq')
155135
entry = content_type.entry("878783238783").include_metadata()
156136
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+
157159

158160

159161
if __name__ == '__main__':

tests/test_live_preview.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import contentstack
55
from contentstack.deep_merge_lp import DeepMergeMixin
66

7-
management_token = 'cs8743874323343u9'
8-
entry_uid = 'blt8743874323343u9'
9-
preview_token = 'abcdefgh1234567890'
7+
management_token = config.MANAGEMENT_TOKEN
8+
entry_uid = config.LIVE_PREVIEW_ENTRY_UID
9+
preview_token = config.PREVIEW_TOKEN
1010

1111
_lp_query = {
1212
'live_preview': '#0#0#0#0#0#0#0#0#0#',

0 commit comments

Comments
 (0)