diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ba6dd..bc7cedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## _v2.5.0_ + +### **Date: 02-March-2026** + +- Assets fields(DAM 2.0) support added ## _v2.4.1_ ### **Date: 10-November-2025** diff --git a/contentstack/__init__.py b/contentstack/__init__.py index 70b2807..fc6f5e0 100644 --- a/contentstack/__init__.py +++ b/contentstack/__init__.py @@ -22,7 +22,7 @@ __title__ = 'contentstack-delivery-python' __author__ = 'contentstack' __status__ = 'debug' -__version__ = 'v2.4.1' +__version__ = 'v2.5.0' __endpoint__ = 'cdn.contentstack.io' __email__ = 'support@contentstack.com' __developer_email__ = 'mobile@contentstack.com' diff --git a/contentstack/asset.py b/contentstack/asset.py index 9b48f90..a6cb181 100644 --- a/contentstack/asset.py +++ b/contentstack/asset.py @@ -118,6 +118,35 @@ def include_fallback(self): self.asset_params['include_fallback'] = "true" return self + def asset_fields(self, *field_names): + r"""Include specific asset fields in the response. + Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups. + Pass one or more field names. Can be called multiple times to add more fields. + + :param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups) + :return: `Asset`, so we can chain the call + ---------------------------- + Example:: + >>> import contentstack + >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment') + >>> asset = stack.asset(uid='asset_uid') + >>> result = asset.asset_fields('user_defined_fields', 'visual_markups').fetch() + ---------------------------- + """ + if field_names: + values = [] + for name in field_names: + if isinstance(name, (list, tuple)): + values.extend(str(v) for v in name) + else: + values.append(str(name)) + if values: + existing = self.asset_params.get('asset_fields[]', []) + if not isinstance(existing, list): + existing = [existing] + self.asset_params['asset_fields[]'] = existing + values + return self + def fetch(self): r"""This call fetches the latest version of a specific asset of a particular stack. :return: json response of asset diff --git a/contentstack/assetquery.py b/contentstack/assetquery.py index ecdc34b..093e67e 100644 --- a/contentstack/assetquery.py +++ b/contentstack/assetquery.py @@ -157,6 +157,36 @@ def locale(self, locale: str): self.asset_query_params['locale'] = locale return self + def asset_fields(self, *field_names): + r"""Include specific asset fields in the response. + Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups. + Pass one or more field names. Can be called multiple times to add more fields. + + :param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups) + :return: AssetQuery: so we can chain the call + + ----------------------------- + [Example]: + + >>> import contentstack + >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment') + >>> result = stack.asset_query().asset_fields('user_defined_fields', 'visual_markups').find() + ------------------------------ + """ + if field_names: + values = [] + for name in field_names: + if isinstance(name, (list, tuple)): + values.extend(str(v) for v in name) + else: + values.append(str(name)) + if values: + existing = self.asset_query_params.get('asset_fields[]', []) + if not isinstance(existing, list): + existing = [existing] + self.asset_query_params['asset_fields[]'] = existing + values + return self + def find(self): r"""This call fetches the list of all the assets of a particular stack. It also returns the content of each asset in JSON format. diff --git a/contentstack/entry.py b/contentstack/entry.py index 3c20edf..b0bae6b 100644 --- a/contentstack/entry.py +++ b/contentstack/entry.py @@ -176,6 +176,26 @@ def include_embedded_items(self): self.entry_param['include_embedded_items[]'] = "BASE" return self + def asset_fields(self, *field_names): + """Include specific asset fields in the response. + Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups. + Pass one or more field names. Can be called multiple times to add more fields. + + :param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups) + :return: Entry, so we can chain the call + ---------------------------- + Example:: + + >>> import contentstack + >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment') + >>> content_type = stack.content_type('content_type_uid') + >>> entry = content_type.entry(uid='entry_uid') + >>> entry = entry.asset_fields('user_defined_fields', 'visual_markups') + >>> result = entry.fetch() + ---------------------------- + """ + return super().asset_fields(*field_names) + def __get_base_url(self, endpoint=''): if endpoint is not None and endpoint.strip(): # .strip() removes leading/trailing whitespace self.http_instance.endpoint = endpoint diff --git a/contentstack/entryqueryable.py b/contentstack/entryqueryable.py index 7426510..be10e81 100644 --- a/contentstack/entryqueryable.py +++ b/contentstack/entryqueryable.py @@ -167,6 +167,45 @@ def include_metadata(self): self.entry_queryable_param['include_metadata'] = 'true' return self + def asset_fields(self, *field_names): + """ + Include specific asset fields in the response. + Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups. + Pass one or more field names. Can be called multiple times to add more fields. + + :param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups) + :return: self: so you can chain this call. + + Example (Query): + >>> import contentstack + >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment') + >>> content_type = stack.content_type('content_type_uid') + >>> query = content_type.query() + >>> query = query.asset_fields('user_defined_fields', 'visual_markups') + >>> result = query.find() + + Example (Entry): + >>> import contentstack + >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment') + >>> content_type = stack.content_type('content_type_uid') + >>> entry = content_type.entry('entry_uid') + >>> entry = entry.asset_fields('user_defined_fields', 'visual_markups') + >>> result = entry.fetch() + """ + if field_names: + values = [] + for name in field_names: + if isinstance(name, (list, tuple)): + values.extend(str(v) for v in name) + else: + values.append(str(name)) + if values: + existing = self.entry_queryable_param.get('asset_fields[]', []) + if not isinstance(existing, list): + existing = [existing] + self.entry_queryable_param['asset_fields[]'] = existing + values + return self + def add_param(self, key: str, value: str): """ This method adds key and value to an Entry. diff --git a/requirements.txt b/requirements.txt index 0201897..5ac3538 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,10 +34,10 @@ Babel==2.14.0 pep517==0.13.1 tomli~=2.0.1 werkzeug~=3.1.5 -Flask~=2.3.2 +Flask~=3.1.3 click~=8.1.7 MarkupSafe==2.1.5 -blinker~=1.8.2 +blinker~=1.9.0 itsdangerous~=2.2.0 isort==5.13.2 pkginfo==1.11.1 diff --git a/tests/test_assets.py b/tests/test_assets.py index f2ae65c..b494f81 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -55,13 +55,13 @@ def test_014_setting_retry_strategy_api(self): def test_01_assets_query_initial_run(self): result = self.asset_query.find() - if result is not None: - assets = result['assets'] - for item in assets: - if item['title'] == 'if_icon-72-lightning_316154_(1).png': - global ASSET_UID - ASSET_UID = item['uid'] - self.assertEqual(8, len(assets)) + self.assertIsNotNone(result) + assets = result['assets'] + for item in assets: + if item['title'] == 'if_icon-72-lightning_316154_(1).png': + global ASSET_UID + ASSET_UID = item['uid'] + self.assertGreaterEqual(len(assets), 8) def test_02_asset_method(self): self.asset = self.stack.asset(uid=ASSET_UID) @@ -117,14 +117,37 @@ def test_08_support_include_fallback(self): self.assertEqual({'environment': 'development', 'include_fallback': 'true'}, asset_params) + def test_08a_asset_fields_single_asset(self): + """Test single asset asset_fields sets asset_params""" + self.asset = self.stack.asset(uid=ASSET_UID or 'test_asset_uid') + self.asset.asset_fields('user_defined_fields', 'visual_markups') + self.assertEqual(['user_defined_fields', 'visual_markups'], + self.asset.asset_params['asset_fields[]']) + + def test_08b_asset_fields_single_asset_chained_calls(self): + """Test single asset asset_fields with chained calls""" + self.asset = self.stack.asset(uid=ASSET_UID or 'test_asset_uid') + self.asset.asset_fields('user_defined_fields').asset_fields('visual_markups') + self.assertEqual(['user_defined_fields', 'visual_markups'], + self.asset.asset_params['asset_fields[]']) + + def test_08c_asset_fields_single_asset_all_supported_values(self): + """Test single asset asset_fields with all supported values""" + self.asset = self.stack.asset(uid=ASSET_UID or 'test_asset_uid') + self.asset.asset_fields('user_defined_fields', 'embedded_metadata', + 'ai_generated_metadata', 'visual_markups') + self.assertEqual( + ['user_defined_fields', 'embedded_metadata', 'ai_generated_metadata', 'visual_markups'], + self.asset.asset_params['asset_fields[]']) + ############################################ # ==== Asset Query ==== ############################################ def test_09_assets_query(self): result = self.asset_query.find() - if result is not None: - self.assertEqual(8, len(result['assets'])) + self.assertIsNotNone(result) + self.assertGreaterEqual(len(result['assets']), 8) def test_10_assets_base_query_where_exclude_title(self): query = self.asset_query.where( @@ -211,6 +234,46 @@ def test_25_include_metadata(self): self.assertTrue( self.asset_query.asset_query_params.__contains__('include_metadata')) + def test_25a_asset_query_asset_fields_single_field(self): + """Test asset_query asset_fields with a single field""" + query = self.asset_query.asset_fields('user_defined_fields') + self.assertEqual(['user_defined_fields'], + query.asset_query_params['asset_fields[]']) + + def test_25b_asset_query_asset_fields_multiple_fields(self): + """Test asset_query asset_fields with multiple fields""" + query = self.asset_query.asset_fields('user_defined_fields', 'visual_markups') + self.assertEqual(['user_defined_fields', 'visual_markups'], + query.asset_query_params['asset_fields[]']) + + def test_25c_asset_query_asset_fields_chained_calls(self): + """Test asset_query asset_fields with chained calls""" + query = (self.asset_query + .asset_fields('user_defined_fields') + .asset_fields('visual_markups')) + self.assertEqual(['user_defined_fields', 'visual_markups'], + query.asset_query_params['asset_fields[]']) + + def test_25d_asset_query_asset_fields_all_supported_values(self): + """Test asset_query asset_fields with all supported values""" + query = (self.asset_query + .asset_fields('user_defined_fields', 'embedded_metadata', + 'ai_generated_metadata', 'visual_markups')) + self.assertEqual( + ['user_defined_fields', 'embedded_metadata', 'ai_generated_metadata', 'visual_markups'], + query.asset_query_params['asset_fields[]']) + + def test_25e_asset_query_asset_fields_with_other_params(self): + """Test asset_query asset_fields combined with include_metadata and locale""" + query = (self.asset_query + .asset_fields('user_defined_fields', 'visual_markups') + .include_metadata() + .locale('en-us')) + self.assertEqual(['user_defined_fields', 'visual_markups'], + query.asset_query_params['asset_fields[]']) + self.assertEqual('true', query.asset_query_params['include_metadata']) + self.assertEqual('en-us', query.asset_query_params['locale']) + def test_26_where_with_include_count_and_pagination(self): """Test combination of where, include_count, skip, and limit for assets""" query = (self.asset_query diff --git a/tests/test_entry.py b/tests/test_entry.py index e2f2462..6e388fc 100644 --- a/tests/test_entry.py +++ b/tests/test_entry.py @@ -363,6 +363,46 @@ def test_46_entry_all_queryable_methods_combined(self): self.assertIn('include_reference_content_type_uid', entry.entry_queryable_param) self.assertEqual('value', entry.entry_queryable_param['custom']) + def test_47_entry_asset_fields_single_field(self): + """Test entry asset_fields with a single field""" + entry = self.stack.content_type('faq').entry(FAQ_UID).asset_fields('user_defined_fields') + self.assertEqual(['user_defined_fields'], entry.entry_queryable_param['asset_fields[]']) + + def test_48_entry_asset_fields_multiple_fields(self): + """Test entry asset_fields with multiple fields in one call""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .asset_fields('user_defined_fields', 'visual_markups')) + self.assertEqual(['user_defined_fields', 'visual_markups'], + entry.entry_queryable_param['asset_fields[]']) + + def test_49_entry_asset_fields_chained_calls(self): + """Test entry asset_fields with chained calls""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .asset_fields('user_defined_fields') + .asset_fields('visual_markups')) + self.assertEqual(['user_defined_fields', 'visual_markups'], + entry.entry_queryable_param['asset_fields[]']) + + def test_50_entry_asset_fields_all_supported_values(self): + """Test entry asset_fields with all supported values""" + entry = (self.stack.content_type('faq') + .entry(FAQ_UID) + .asset_fields('user_defined_fields', 'embedded_metadata', + 'ai_generated_metadata', 'visual_markups')) + self.assertEqual( + ['user_defined_fields', 'embedded_metadata', 'ai_generated_metadata', 'visual_markups'], + entry.entry_queryable_param['asset_fields[]']) + + def test_51_query_asset_fields(self): + """Test query asset_fields sets entry_queryable_param""" + query = (self.stack.content_type('faq') + .query() + .asset_fields('user_defined_fields', 'visual_markups')) + self.assertEqual(['user_defined_fields', 'visual_markups'], + query.entry_queryable_param['asset_fields[]']) + if __name__ == '__main__': unittest.main()