Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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**
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.4.1'
__version__ = 'v2.5.0'
__endpoint__ = 'cdn.contentstack.io'
__email__ = 'support@contentstack.com'
__developer_email__ = 'mobile@contentstack.com'
Expand Down
29 changes: 29 additions & 0 deletions contentstack/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions contentstack/assetquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions contentstack/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions contentstack/entryqueryable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 72 additions & 9 deletions tests/test_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions tests/test_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading