Skip to content

Commit 6b37aac

Browse files
Merge pull request #190 from contentstack/staging
DX | 02-03-2026 | Release
2 parents 8eca216 + 9185c19 commit 6b37aac

File tree

9 files changed

+238
-12
lines changed

9 files changed

+238
-12
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG
22

3+
## _v2.5.0_
4+
5+
### **Date: 02-March-2026**
6+
7+
- Assets fields(DAM 2.0) support added
38
## _v2.4.1_
49

510
### **Date: 10-November-2025**

contentstack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
__title__ = 'contentstack-delivery-python'
2323
__author__ = 'contentstack'
2424
__status__ = 'debug'
25-
__version__ = 'v2.4.1'
25+
__version__ = 'v2.5.0'
2626
__endpoint__ = 'cdn.contentstack.io'
2727
__email__ = 'support@contentstack.com'
2828
__developer_email__ = 'mobile@contentstack.com'

contentstack/asset.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,35 @@ def include_fallback(self):
118118
self.asset_params['include_fallback'] = "true"
119119
return self
120120

121+
def asset_fields(self, *field_names):
122+
r"""Include specific asset fields in the response.
123+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
124+
Pass one or more field names. Can be called multiple times to add more fields.
125+
126+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
127+
:return: `Asset`, so we can chain the call
128+
----------------------------
129+
Example::
130+
>>> import contentstack
131+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
132+
>>> asset = stack.asset(uid='asset_uid')
133+
>>> result = asset.asset_fields('user_defined_fields', 'visual_markups').fetch()
134+
----------------------------
135+
"""
136+
if field_names:
137+
values = []
138+
for name in field_names:
139+
if isinstance(name, (list, tuple)):
140+
values.extend(str(v) for v in name)
141+
else:
142+
values.append(str(name))
143+
if values:
144+
existing = self.asset_params.get('asset_fields[]', [])
145+
if not isinstance(existing, list):
146+
existing = [existing]
147+
self.asset_params['asset_fields[]'] = existing + values
148+
return self
149+
121150
def fetch(self):
122151
r"""This call fetches the latest version of a specific asset of a particular stack.
123152
:return: json response of asset

contentstack/assetquery.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,36 @@ def locale(self, locale: str):
157157
self.asset_query_params['locale'] = locale
158158
return self
159159

160+
def asset_fields(self, *field_names):
161+
r"""Include specific asset fields in the response.
162+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
163+
Pass one or more field names. Can be called multiple times to add more fields.
164+
165+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
166+
:return: AssetQuery: so we can chain the call
167+
168+
-----------------------------
169+
[Example]:
170+
171+
>>> import contentstack
172+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
173+
>>> result = stack.asset_query().asset_fields('user_defined_fields', 'visual_markups').find()
174+
------------------------------
175+
"""
176+
if field_names:
177+
values = []
178+
for name in field_names:
179+
if isinstance(name, (list, tuple)):
180+
values.extend(str(v) for v in name)
181+
else:
182+
values.append(str(name))
183+
if values:
184+
existing = self.asset_query_params.get('asset_fields[]', [])
185+
if not isinstance(existing, list):
186+
existing = [existing]
187+
self.asset_query_params['asset_fields[]'] = existing + values
188+
return self
189+
160190
def find(self):
161191
r"""This call fetches the list of all the assets of a particular stack.
162192
It also returns the content of each asset in JSON format.

contentstack/entry.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,26 @@ def include_embedded_items(self):
176176
self.entry_param['include_embedded_items[]'] = "BASE"
177177
return self
178178

179+
def asset_fields(self, *field_names):
180+
"""Include specific asset fields in the response.
181+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
182+
Pass one or more field names. Can be called multiple times to add more fields.
183+
184+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
185+
:return: Entry, so we can chain the call
186+
----------------------------
187+
Example::
188+
189+
>>> import contentstack
190+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
191+
>>> content_type = stack.content_type('content_type_uid')
192+
>>> entry = content_type.entry(uid='entry_uid')
193+
>>> entry = entry.asset_fields('user_defined_fields', 'visual_markups')
194+
>>> result = entry.fetch()
195+
----------------------------
196+
"""
197+
return super().asset_fields(*field_names)
198+
179199
def __get_base_url(self, endpoint=''):
180200
if endpoint is not None and endpoint.strip(): # .strip() removes leading/trailing whitespace
181201
self.http_instance.endpoint = endpoint

contentstack/entryqueryable.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,45 @@ def include_metadata(self):
167167
self.entry_queryable_param['include_metadata'] = 'true'
168168
return self
169169

170+
def asset_fields(self, *field_names):
171+
"""
172+
Include specific asset fields in the response.
173+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
174+
Pass one or more field names. Can be called multiple times to add more fields.
175+
176+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
177+
:return: self: so you can chain this call.
178+
179+
Example (Query):
180+
>>> import contentstack
181+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
182+
>>> content_type = stack.content_type('content_type_uid')
183+
>>> query = content_type.query()
184+
>>> query = query.asset_fields('user_defined_fields', 'visual_markups')
185+
>>> result = query.find()
186+
187+
Example (Entry):
188+
>>> import contentstack
189+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
190+
>>> content_type = stack.content_type('content_type_uid')
191+
>>> entry = content_type.entry('entry_uid')
192+
>>> entry = entry.asset_fields('user_defined_fields', 'visual_markups')
193+
>>> result = entry.fetch()
194+
"""
195+
if field_names:
196+
values = []
197+
for name in field_names:
198+
if isinstance(name, (list, tuple)):
199+
values.extend(str(v) for v in name)
200+
else:
201+
values.append(str(name))
202+
if values:
203+
existing = self.entry_queryable_param.get('asset_fields[]', [])
204+
if not isinstance(existing, list):
205+
existing = [existing]
206+
self.entry_queryable_param['asset_fields[]'] = existing + values
207+
return self
208+
170209
def add_param(self, key: str, value: str):
171210
"""
172211
This method adds key and value to an Entry.

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ Babel==2.14.0
3434
pep517==0.13.1
3535
tomli~=2.0.1
3636
werkzeug~=3.1.5
37-
Flask~=2.3.2
37+
Flask~=3.1.3
3838
click~=8.1.7
3939
MarkupSafe==2.1.5
40-
blinker~=1.8.2
40+
blinker~=1.9.0
4141
itsdangerous~=2.2.0
4242
isort==5.13.2
4343
pkginfo==1.11.1

tests/test_assets.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ def test_014_setting_retry_strategy_api(self):
5555

5656
def test_01_assets_query_initial_run(self):
5757
result = self.asset_query.find()
58-
if result is not None:
59-
assets = result['assets']
60-
for item in assets:
61-
if item['title'] == 'if_icon-72-lightning_316154_(1).png':
62-
global ASSET_UID
63-
ASSET_UID = item['uid']
64-
self.assertEqual(8, len(assets))
58+
self.assertIsNotNone(result)
59+
assets = result['assets']
60+
for item in assets:
61+
if item['title'] == 'if_icon-72-lightning_316154_(1).png':
62+
global ASSET_UID
63+
ASSET_UID = item['uid']
64+
self.assertGreaterEqual(len(assets), 8)
6565

6666
def test_02_asset_method(self):
6767
self.asset = self.stack.asset(uid=ASSET_UID)
@@ -117,14 +117,37 @@ def test_08_support_include_fallback(self):
117117
self.assertEqual({'environment': 'development',
118118
'include_fallback': 'true'}, asset_params)
119119

120+
def test_08a_asset_fields_single_asset(self):
121+
"""Test single asset asset_fields sets asset_params"""
122+
self.asset = self.stack.asset(uid=ASSET_UID or 'test_asset_uid')
123+
self.asset.asset_fields('user_defined_fields', 'visual_markups')
124+
self.assertEqual(['user_defined_fields', 'visual_markups'],
125+
self.asset.asset_params['asset_fields[]'])
126+
127+
def test_08b_asset_fields_single_asset_chained_calls(self):
128+
"""Test single asset asset_fields with chained calls"""
129+
self.asset = self.stack.asset(uid=ASSET_UID or 'test_asset_uid')
130+
self.asset.asset_fields('user_defined_fields').asset_fields('visual_markups')
131+
self.assertEqual(['user_defined_fields', 'visual_markups'],
132+
self.asset.asset_params['asset_fields[]'])
133+
134+
def test_08c_asset_fields_single_asset_all_supported_values(self):
135+
"""Test single asset asset_fields with all supported values"""
136+
self.asset = self.stack.asset(uid=ASSET_UID or 'test_asset_uid')
137+
self.asset.asset_fields('user_defined_fields', 'embedded_metadata',
138+
'ai_generated_metadata', 'visual_markups')
139+
self.assertEqual(
140+
['user_defined_fields', 'embedded_metadata', 'ai_generated_metadata', 'visual_markups'],
141+
self.asset.asset_params['asset_fields[]'])
142+
120143
############################################
121144
# ==== Asset Query ====
122145
############################################
123146

124147
def test_09_assets_query(self):
125148
result = self.asset_query.find()
126-
if result is not None:
127-
self.assertEqual(8, len(result['assets']))
149+
self.assertIsNotNone(result)
150+
self.assertGreaterEqual(len(result['assets']), 8)
128151

129152
def test_10_assets_base_query_where_exclude_title(self):
130153
query = self.asset_query.where(
@@ -211,6 +234,46 @@ def test_25_include_metadata(self):
211234
self.assertTrue(
212235
self.asset_query.asset_query_params.__contains__('include_metadata'))
213236

237+
def test_25a_asset_query_asset_fields_single_field(self):
238+
"""Test asset_query asset_fields with a single field"""
239+
query = self.asset_query.asset_fields('user_defined_fields')
240+
self.assertEqual(['user_defined_fields'],
241+
query.asset_query_params['asset_fields[]'])
242+
243+
def test_25b_asset_query_asset_fields_multiple_fields(self):
244+
"""Test asset_query asset_fields with multiple fields"""
245+
query = self.asset_query.asset_fields('user_defined_fields', 'visual_markups')
246+
self.assertEqual(['user_defined_fields', 'visual_markups'],
247+
query.asset_query_params['asset_fields[]'])
248+
249+
def test_25c_asset_query_asset_fields_chained_calls(self):
250+
"""Test asset_query asset_fields with chained calls"""
251+
query = (self.asset_query
252+
.asset_fields('user_defined_fields')
253+
.asset_fields('visual_markups'))
254+
self.assertEqual(['user_defined_fields', 'visual_markups'],
255+
query.asset_query_params['asset_fields[]'])
256+
257+
def test_25d_asset_query_asset_fields_all_supported_values(self):
258+
"""Test asset_query asset_fields with all supported values"""
259+
query = (self.asset_query
260+
.asset_fields('user_defined_fields', 'embedded_metadata',
261+
'ai_generated_metadata', 'visual_markups'))
262+
self.assertEqual(
263+
['user_defined_fields', 'embedded_metadata', 'ai_generated_metadata', 'visual_markups'],
264+
query.asset_query_params['asset_fields[]'])
265+
266+
def test_25e_asset_query_asset_fields_with_other_params(self):
267+
"""Test asset_query asset_fields combined with include_metadata and locale"""
268+
query = (self.asset_query
269+
.asset_fields('user_defined_fields', 'visual_markups')
270+
.include_metadata()
271+
.locale('en-us'))
272+
self.assertEqual(['user_defined_fields', 'visual_markups'],
273+
query.asset_query_params['asset_fields[]'])
274+
self.assertEqual('true', query.asset_query_params['include_metadata'])
275+
self.assertEqual('en-us', query.asset_query_params['locale'])
276+
214277
def test_26_where_with_include_count_and_pagination(self):
215278
"""Test combination of where, include_count, skip, and limit for assets"""
216279
query = (self.asset_query

tests/test_entry.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,46 @@ def test_46_entry_all_queryable_methods_combined(self):
363363
self.assertIn('include_reference_content_type_uid', entry.entry_queryable_param)
364364
self.assertEqual('value', entry.entry_queryable_param['custom'])
365365

366+
def test_47_entry_asset_fields_single_field(self):
367+
"""Test entry asset_fields with a single field"""
368+
entry = self.stack.content_type('faq').entry(FAQ_UID).asset_fields('user_defined_fields')
369+
self.assertEqual(['user_defined_fields'], entry.entry_queryable_param['asset_fields[]'])
370+
371+
def test_48_entry_asset_fields_multiple_fields(self):
372+
"""Test entry asset_fields with multiple fields in one call"""
373+
entry = (self.stack.content_type('faq')
374+
.entry(FAQ_UID)
375+
.asset_fields('user_defined_fields', 'visual_markups'))
376+
self.assertEqual(['user_defined_fields', 'visual_markups'],
377+
entry.entry_queryable_param['asset_fields[]'])
378+
379+
def test_49_entry_asset_fields_chained_calls(self):
380+
"""Test entry asset_fields with chained calls"""
381+
entry = (self.stack.content_type('faq')
382+
.entry(FAQ_UID)
383+
.asset_fields('user_defined_fields')
384+
.asset_fields('visual_markups'))
385+
self.assertEqual(['user_defined_fields', 'visual_markups'],
386+
entry.entry_queryable_param['asset_fields[]'])
387+
388+
def test_50_entry_asset_fields_all_supported_values(self):
389+
"""Test entry asset_fields with all supported values"""
390+
entry = (self.stack.content_type('faq')
391+
.entry(FAQ_UID)
392+
.asset_fields('user_defined_fields', 'embedded_metadata',
393+
'ai_generated_metadata', 'visual_markups'))
394+
self.assertEqual(
395+
['user_defined_fields', 'embedded_metadata', 'ai_generated_metadata', 'visual_markups'],
396+
entry.entry_queryable_param['asset_fields[]'])
397+
398+
def test_51_query_asset_fields(self):
399+
"""Test query asset_fields sets entry_queryable_param"""
400+
query = (self.stack.content_type('faq')
401+
.query()
402+
.asset_fields('user_defined_fields', 'visual_markups'))
403+
self.assertEqual(['user_defined_fields', 'visual_markups'],
404+
query.entry_queryable_param['asset_fields[]'])
405+
366406

367407
if __name__ == '__main__':
368408
unittest.main()

0 commit comments

Comments
 (0)