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..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: """ @@ -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 + ) diff --git a/contentstack/entry.py b/contentstack/entry.py index 7cfcb13..f2836f8 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): """ @@ -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 + ) + diff --git a/contentstack/variants.py b/contentstack/variants.py new file mode 100644 index 0000000..133630d --- /dev/null +++ b/contentstack/variants.py @@ -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 \ No newline at end of file diff --git a/tests/test_entry.py b/tests/test_entry.py index fe30466..cdfeb4d 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).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__':