Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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=self.logger
)




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

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 particular 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