From 5c2456dc383a9a3bffd970d278b86fe99e0609b0 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:53:33 +0530 Subject: [PATCH 1/4] Added variants support in Python CDA SDK --- CHANGELOG.md | 6 ++++++ contentstack/__init__.py | 2 +- contentstack/contenttype.py | 20 ++++++++++++++++++++ contentstack/entry.py | 20 ++++++++++++++++++++ tests/test_entry.py | 24 +++++++++++++++++++++++- 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be03fd..22b14f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## _v2.2.0_ + +### **Date: 14-July-2025** + +- Variants Support Added. + ## _v2.1.1_ ### **Date: 07-July-2025** diff --git a/contentstack/__init__.py b/contentstack/__init__.py index c1603e4..d8f02b0 100644 --- a/contentstack/__init__.py +++ b/contentstack/__init__.py @@ -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' diff --git a/contentstack/contenttype.py b/contentstack/contenttype.py index 363a206..88aca09 100644 --- a/contentstack/contenttype.py +++ b/contentstack/contenttype.py @@ -118,3 +118,23 @@ 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. + """ + if isinstance(variant_uid, str): + self.http_instance.headers['x-cs-variant-uid'] = variant_uid + elif isinstance(variant_uid, list): + self.http_instance.headers['x-cs-variant-uid'] = ','.join(variant_uid) + + if params is not None: + self.local_param.update(params) + + encoded_params = parse.urlencode(self.local_param) + endpoint = self.http_instance.endpoint + url = f'{endpoint}/content_types/{self.__content_type_uid}/entries?{encoded_params}' + result = self.http_instance.get(url) + return result diff --git a/contentstack/entry.py b/contentstack/entry.py index 7cfcb13..ecb93e0 100644 --- a/contentstack/entry.py +++ b/contentstack/entry.py @@ -222,6 +222,26 @@ 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. + """ + if isinstance(variant_uid, str): + self.http_instance.headers['x-cs-variant-uid'] = variant_uid + elif isinstance(variant_uid, list): + self.http_instance.headers['x-cs-variant-uid'] = ','.join(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}' + result = self.http_instance.get(url) + return result + diff --git a/tests/test_entry.py b/tests/test_entry.py index fe30466..d00a1bc 100644 --- a/tests/test_entry.py +++ b/tests/test_entry.py @@ -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): @@ -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) + 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) + 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]) + 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]) + self.assertIn('variants', entry['entry']['publish_details']) + + if __name__ == '__main__': From 4843091c20ee2c6b7a27af5625f393be159ec6a1 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:03:19 +0530 Subject: [PATCH 2/4] Pass the modified headers explicitly --- contentstack/contenttype.py | 7 ++++--- contentstack/entry.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/contentstack/contenttype.py b/contentstack/contenttype.py index 88aca09..ab5b72d 100644 --- a/contentstack/contenttype.py +++ b/contentstack/contenttype.py @@ -125,10 +125,11 @@ def variants(self, variant_uid: str | list[str], params: dict = None): :param variant_uid: {str} -- variant_uid :return: Entry, so you can chain this call. """ + headers = self.http_instance.headers.copy() # Create a local copy of headers if isinstance(variant_uid, str): - self.http_instance.headers['x-cs-variant-uid'] = variant_uid + headers['x-cs-variant-uid'] = variant_uid elif isinstance(variant_uid, list): - self.http_instance.headers['x-cs-variant-uid'] = ','.join(variant_uid) + headers['x-cs-variant-uid'] = ','.join(variant_uid) if params is not None: self.local_param.update(params) @@ -136,5 +137,5 @@ def variants(self, variant_uid: str | list[str], params: dict = None): encoded_params = parse.urlencode(self.local_param) endpoint = self.http_instance.endpoint url = f'{endpoint}/content_types/{self.__content_type_uid}/entries?{encoded_params}' - result = self.http_instance.get(url) + result = self.http_instance.get(url, headers=headers) return result diff --git a/contentstack/entry.py b/contentstack/entry.py index ecb93e0..be7dd65 100644 --- a/contentstack/entry.py +++ b/contentstack/entry.py @@ -229,17 +229,18 @@ def variants(self, variant_uid: str | list[str], params: dict = None): :param variant_uid: {str} -- variant_uid :return: Entry, so you can chain this call. """ + headers = self.http_instance.headers.copy() # Create a local copy of headers if isinstance(variant_uid, str): - self.http_instance.headers['x-cs-variant-uid'] = variant_uid + headers['x-cs-variant-uid'] = variant_uid elif isinstance(variant_uid, list): - self.http_instance.headers['x-cs-variant-uid'] = ','.join(variant_uid) + headers['x-cs-variant-uid'] = ','.join(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}' - result = self.http_instance.get(url) + result = self.http_instance.get(url, headers=headers) return result From 5734a1f1e67ba3b50d6bdb5ae46aa65e328c2dfa Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:17:42 +0530 Subject: [PATCH 3/4] Added find and fetch methods for variants --- contentstack/contenttype.py | 23 ++++----- contentstack/entry.py | 22 ++++----- contentstack/variants.py | 94 +++++++++++++++++++++++++++++++++++++ tests/test_entry.py | 8 ++-- 4 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 contentstack/variants.py diff --git a/contentstack/contenttype.py b/contentstack/contenttype.py index ab5b72d..2795e29 100644 --- a/contentstack/contenttype.py +++ b/contentstack/contenttype.py @@ -13,6 +13,7 @@ from contentstack.entry import Entry from contentstack.query import Query +from contentstack.variants import Variants class ContentType: """ @@ -125,17 +126,11 @@ def variants(self, variant_uid: str | list[str], params: dict = None): :param variant_uid: {str} -- variant_uid :return: Entry, so you can chain this call. """ - headers = self.http_instance.headers.copy() # Create a local copy of headers - if isinstance(variant_uid, str): - headers['x-cs-variant-uid'] = variant_uid - elif isinstance(variant_uid, list): - headers['x-cs-variant-uid'] = ','.join(variant_uid) - - if params is not None: - self.local_param.update(params) - - encoded_params = parse.urlencode(self.local_param) - endpoint = self.http_instance.endpoint - url = f'{endpoint}/content_types/{self.__content_type_uid}/entries?{encoded_params}' - result = self.http_instance.get(url, headers=headers) - return result + 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 + ) diff --git a/contentstack/entry.py b/contentstack/entry.py index be7dd65..0c44190 100644 --- a/contentstack/entry.py +++ b/contentstack/entry.py @@ -8,6 +8,7 @@ from contentstack.deep_merge_lp import DeepMergeMixin from contentstack.entryqueryable import EntryQueryable +from contentstack.variants import Variants class Entry(EntryQueryable): """ @@ -229,19 +230,14 @@ def variants(self, variant_uid: str | list[str], params: dict = None): :param variant_uid: {str} -- variant_uid :return: Entry, so you can chain this call. """ - headers = self.http_instance.headers.copy() # Create a local copy of headers - if isinstance(variant_uid, str): - headers['x-cs-variant-uid'] = variant_uid - elif isinstance(variant_uid, list): - headers['x-cs-variant-uid'] = ','.join(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}' - result = self.http_instance.get(url, headers=headers) - return result + 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 + ) diff --git a/contentstack/variants.py b/contentstack/variants.py new file mode 100644 index 0000000..2d16c8b --- /dev/null +++ b/contentstack/variants.py @@ -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 \ No newline at end of file diff --git a/tests/test_entry.py b/tests/test_entry.py index d00a1bc..cdfeb4d 100644 --- a/tests/test_entry.py +++ b/tests/test_entry.py @@ -137,22 +137,22 @@ def test_22_entry_include_metadata(self): def test_23_content_type_variants(self): content_type = self.stack.content_type('faq') - entry = content_type.variants(VARIANT_UID) + 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) + 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]) + 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]) + entry = content_type.variants([VARIANT_UID]).fetch() self.assertIn('variants', entry['entry']['publish_details']) From c0e6400092113afbfecd4251fb703352a3b18279 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:22:53 +0530 Subject: [PATCH 4/4] Fixed PR comments by copilot --- contentstack/entry.py | 2 +- contentstack/variants.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contentstack/entry.py b/contentstack/entry.py index 0c44190..f2836f8 100644 --- a/contentstack/entry.py +++ b/contentstack/entry.py @@ -236,7 +236,7 @@ def variants(self, variant_uid: str | list[str], params: dict = None): entry_uid=self.entry_uid, variant_uid=variant_uid, params=params, - logger=None + logger=self.logger ) diff --git a/contentstack/variants.py b/contentstack/variants.py index 2d16c8b..133630d 100644 --- a/contentstack/variants.py +++ b/contentstack/variants.py @@ -1,10 +1,9 @@ import logging from urllib import parse -from contentstack.deep_merge_lp import DeepMergeMixin from contentstack.entryqueryable import EntryQueryable -class Variants(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. @@ -57,7 +56,7 @@ def find(self, params=None): 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. + 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: