Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## _v2.2.0_

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

- Variants Support Added.

## _v2.1.1_

### **Date: 07-July-2025**
Expand Down
2 changes: 1 addition & 1 deletion contentstack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
__title__ = 'contentstack-delivery-python'
__author__ = 'contentstack'
__status__ = 'debug'
__version__ = 'v2.1.1'
__version__ = 'v2.2.0'
__endpoint__ = 'cdn.contentstack.io'
__email__ = 'support@contentstack.com'
__developer_email__ = 'mobile@contentstack.com'
Expand Down
16 changes: 16 additions & 0 deletions contentstack/contenttype.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from contentstack.entry import Entry
from contentstack.query import Query
from contentstack.variants import Variants

class ContentType:
"""
Expand Down Expand Up @@ -118,3 +119,18 @@ def find(self, params=None):
url = f'{endpoint}/content_types?{encoded_params}'
result = self.http_instance.get(url)
return result

def variants(self, variant_uid: str | list[str], params: dict = None):
"""
Fetches the variants of the content type
:param variant_uid: {str} -- variant_uid
:return: Entry, so you can chain this call.
"""
return Variants(
http_instance=self.http_instance,
content_type_uid=self.__content_type_uid,
entry_uid=None,
variant_uid=variant_uid,
params=params,
logger=None
)
17 changes: 17 additions & 0 deletions contentstack/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from contentstack.deep_merge_lp import DeepMergeMixin
from contentstack.entryqueryable import EntryQueryable
from contentstack.variants import Variants

class Entry(EntryQueryable):
"""
Expand Down Expand Up @@ -222,6 +223,22 @@ def _merged_response(self):
merged_response = DeepMergeMixin(entry_response, lp_entry).to_dict() # Convert to dictionary
return merged_response # Now correctly returns a dictionary
raise ValueError("Missing required keys in live_preview data")

def variants(self, variant_uid: str | list[str], params: dict = None):
"""
Fetches the variants of the entry
:param variant_uid: {str} -- variant_uid
:return: Entry, so you can chain this call.
"""
return Variants(
http_instance=self.http_instance,
content_type_uid=self.content_type_id,
entry_uid=self.entry_uid,
variant_uid=variant_uid,
params=params,
logger=None
)




Expand Down
94 changes: 94 additions & 0 deletions contentstack/variants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import logging
from urllib import parse

from contentstack.deep_merge_lp import DeepMergeMixin
from contentstack.entryqueryable import EntryQueryable

class Variants(EntryQueryable, ):
"""
An entry is the actual piece of content that you want to publish.
Entries can be created for one of the available content types.

Entry works with
version={version_number}
environment={environment_name}
locale={locale_code}
"""

def __init__(self,
http_instance=None,
content_type_uid=None,
entry_uid=None,
variant_uid=None,
params=None,
logger=None):

super().__init__()
EntryQueryable.__init__(self)
self.entry_param = {}
self.http_instance = http_instance
self.content_type_id = content_type_uid
self.entry_uid = entry_uid
self.variant_uid = variant_uid
self.logger = logger or logging.getLogger(__name__)
self.entry_param = params or {}

def find(self, params=None):
"""
find the variants of the entry of a particular content type
:param self.variant_uid: {str} -- self.variant_uid
:return: Entry, so you can chain this call.
"""
headers = self.http_instance.headers.copy() # Create a local copy of headers
if isinstance(self.variant_uid, str):
headers['x-cs-variant-uid'] = self.variant_uid
elif isinstance(self.variant_uid, list):
headers['x-cs-variant-uid'] = ','.join(self.variant_uid)

if params is not None:
self.entry_param.update(params)
encoded_params = parse.urlencode(self.entry_param)
endpoint = self.http_instance.endpoint
url = f'{endpoint}/content_types/{self.content_type_id}/entries?{encoded_params}'
self.http_instance.headers.update(headers)
result = self.http_instance.get(url)
self.http_instance.headers.pop('x-cs-variant-uid', None)
return result

def fetch(self, params=None):
"""
This method is useful to fetch variant entries of a paticular content type and entries of the of the stack.
:return:dict -- contentType response
------------------------------
Example:

>>> import contentstack
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
>>> content_type = stack.content_type('content_type_uid')
>>> some_dict = {'abc':'something'}
>>> response = content_type.fetch(some_dict)
------------------------------
"""
"""
Fetches the variants of the entry
:param self.variant_uid: {str} -- self.variant_uid
:return: Entry, so you can chain this call.
"""
if self.entry_uid is None:
raise ValueError("entry_uid is required")
else:
headers = self.http_instance.headers.copy() # Create a local copy of headers
if isinstance(self.variant_uid, str):
headers['x-cs-variant-uid'] = self.variant_uid
elif isinstance(self.variant_uid, list):
headers['x-cs-variant-uid'] = ','.join(self.variant_uid)

if params is not None:
self.entry_param.update(params)
encoded_params = parse.urlencode(self.entry_param)
endpoint = self.http_instance.endpoint
url = f'{endpoint}/content_types/{self.content_type_id}/entries/{self.entry_uid}?{encoded_params}'
self.http_instance.headers.update(headers)
result = self.http_instance.get(url)
self.http_instance.headers.pop('x-cs-variant-uid', None)
return result
24 changes: 23 additions & 1 deletion tests/test_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
ENVIRONMENT = config.ENVIRONMENT
HOST = config.HOST
FAQ_UID = config.FAQ_UID # Add this in your config.py

VARIANT_UID = config.VARIANT_UID

class TestEntry(unittest.TestCase):

Expand Down Expand Up @@ -134,6 +134,28 @@ def test_22_entry_include_metadata(self):
content_type = self.stack.content_type('faq')
entry = content_type.entry("878783238783").include_metadata()
self.assertEqual({'include_metadata': 'true'}, entry.entry_queryable_param)

def test_23_content_type_variants(self):
content_type = self.stack.content_type('faq')
entry = content_type.variants(VARIANT_UID).find()
self.assertIn('variants', entry['entries'][0]['publish_details'])

def test_24_entry_variants(self):
content_type = self.stack.content_type('faq')
entry = content_type.entry(FAQ_UID).variants(VARIANT_UID).fetch()
self.assertIn('variants', entry['entry']['publish_details'])

def test_25_content_type_variants_with_has_hash_variant(self):
content_type = self.stack.content_type('faq')
entry = content_type.variants([VARIANT_UID]).find()
self.assertIn('variants', entry['entries'][0]['publish_details'])

def test_25_content_type_entry_variants_with_has_hash_variant(self):
content_type = self.stack.content_type('faq').entry(FAQ_UID)
entry = content_type.variants([VARIANT_UID]).fetch()
self.assertIn('variants', entry['entry']['publish_details'])




if __name__ == '__main__':
Expand Down
Loading