Skip to content

Commit e09ceb8

Browse files
authored
Merge pull request #169 from contentstack/development
Development
2 parents 5011fb4 + b7797f9 commit e09ceb8

File tree

14 files changed

+1639
-9
lines changed

14 files changed

+1639
-9
lines changed

.github/workflows/sca-scan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
steps:
99
- uses: actions/checkout@master
1010
- name: Run Snyk to check for vulnerabilities
11-
uses: snyk/actions/python@master
11+
uses: snyk/actions/python-3.12@master
1212
env:
1313
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
1414
with:

.snyk

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2+
version: v1.25.0
3+
language-settings:
4+
python:
5+
version: "3.13"

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2012 - 2025 Contentstack. All rights reserved.
3+
Copyright (c) 2012 - 2026 Contentstack. All rights reserved.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ Read through to understand how to use the Sync API with Contentstack Python SDK.
152152

153153
### The MIT License (MIT)
154154

155-
Copyright © 2012-2025 [Contentstack](https://www.contentstack.com/). All Rights Reserved
155+
Copyright © 2012-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved
156156

157157
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
158158
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the

contentstack/asset.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __init__(self, http_instance, uid=None, logger=None):
1717
self.__uid = uid
1818
if self.__uid is None or self.__uid.strip() == 0:
1919
raise KeyError(ErrorMessages.INVALID_UID)
20+
self.uid = uid
2021
self.base_url = f'{self.http_instance.endpoint}/assets/{self.__uid}'
2122
if 'environment' in self.http_instance.headers:
2223
self.asset_params['environment'] = self.http_instance.headers['environment']

contentstack/entry.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ def environment(self, environment):
5454
self.http_instance.headers['environment'] = environment
5555
return self
5656

57+
def remove_environment(self):
58+
"""Removes environment from the request headers
59+
:return: Entry, so we can chain the call
60+
-------------------------------
61+
Example::
62+
63+
>>> import contentstack
64+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
65+
>>> content_type = stack.content_type('content_type_uid')
66+
>>> entry = content_type.entry(uid='entry_uid')
67+
>>> entry = entry.environment('test')
68+
>>> entry = entry.remove_environment()
69+
>>> result = entry.fetch()
70+
-------------------------------
71+
"""
72+
if 'environment' in self.http_instance.headers:
73+
self.http_instance.headers.pop('environment')
74+
return self
75+
5776
def version(self, version):
5877
"""When no version is specified, it returns the latest version
5978
To retrieve a specific version, specify the version number under this parameter.
@@ -96,6 +115,9 @@ def param(self, key, value):
96115
"""
97116
if None in (key, value) and not isinstance(key, str):
98117
raise ValueError(ErrorMessages.INVALID_KEY_VALUE_ARGS)
118+
# Convert non-string values to strings
119+
if not isinstance(value, str):
120+
value = str(value)
99121
self.entry_param[key] = value
100122
return self
101123

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ argparse~=1.4.0
2323
toml~=0.10.2
2424
Jinja2~=3.1.4
2525
env~=0.1.0
26-
filelock~=3.13.4
26+
filelock~=3.20.1
2727
pluggy~=1.5.0
2828
six~=1.16.0
2929
packaging~=23.1
@@ -33,7 +33,7 @@ pytz==2024.1
3333
Babel==2.14.0
3434
pep517==0.13.1
3535
tomli~=2.0.1
36-
Werkzeug==3.0.6
36+
Werkzeug==3.1.4
3737
Flask~=2.3.2
3838
click~=8.1.7
3939
MarkupSafe==2.1.5
@@ -58,7 +58,7 @@ zipp==3.20.1
5858
distlib~=0.3.8
5959
cachetools~=5.4.0
6060
tomlkit~=0.13.2
61-
urllib3==2.5.0
61+
urllib3==2.6.0
6262
exceptiongroup~=1.2.2
6363
iniconfig~=2.0.0
6464
pytest-cov>=4.0.0

tests/test_assets.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,5 +211,284 @@ def test_25_include_metadata(self):
211211
self.assertTrue(
212212
self.asset_query.asset_query_params.__contains__('include_metadata'))
213213

214+
def test_26_where_with_include_count_and_pagination(self):
215+
"""Test combination of where, include_count, skip, and limit for assets"""
216+
query = (self.asset_query
217+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
218+
.include_count()
219+
.skip(2)
220+
.limit(5))
221+
self.assertEqual({"title": IMAGE}, query.parameters)
222+
self.assertEqual("true", query.query_params["include_count"])
223+
self.assertEqual("2", query.query_params["skip"])
224+
self.assertEqual("5", query.query_params["limit"])
225+
226+
def test_27_where_with_order_by_and_pagination(self):
227+
"""Test combination of where, order_by, skip, and limit for assets"""
228+
query = (self.asset_query
229+
.where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000)
230+
.order_by_ascending("file_size")
231+
.skip(0)
232+
.limit(10))
233+
self.assertEqual({"file_size": {"$gt": 1000}}, query.parameters)
234+
self.assertEqual("file_size", query.query_params["asc"])
235+
self.assertEqual("0", query.query_params["skip"])
236+
self.assertEqual("10", query.query_params["limit"])
237+
238+
def test_28_multiple_where_conditions_with_all_base_methods(self):
239+
"""Test multiple where conditions combined with all BaseQuery methods for assets"""
240+
query = (self.asset_query
241+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
242+
.where("file_size", QueryOperation.IS_LESS_THAN, fields=10000)
243+
.where("content_type", QueryOperation.INCLUDES, fields=["image/jpeg", "image/png"])
244+
.include_count()
245+
.skip(5)
246+
.limit(20)
247+
.order_by_descending("created_at")
248+
.param("locale", "en-us"))
249+
250+
# Verify parameters
251+
self.assertEqual(3, len(query.parameters))
252+
self.assertEqual(IMAGE, query.parameters["title"])
253+
self.assertEqual({"$lt": 10000}, query.parameters["file_size"])
254+
self.assertEqual({"$in": ["image/jpeg", "image/png"]}, query.parameters["content_type"])
255+
256+
# Verify query_params
257+
self.assertEqual("true", query.query_params["include_count"])
258+
self.assertEqual("5", query.query_params["skip"])
259+
self.assertEqual("20", query.query_params["limit"])
260+
self.assertEqual("created_at", query.query_params["desc"])
261+
self.assertEqual("en-us", query.query_params["locale"])
262+
263+
def test_29_where_with_all_query_operations_combined(self):
264+
"""Test where with all QueryOperation types combined for assets"""
265+
query = (self.asset_query
266+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
267+
.where("file_size", QueryOperation.NOT_EQUALS, fields=0)
268+
.where("tags", QueryOperation.INCLUDES, fields=["tag1"])
269+
.where("excluded", QueryOperation.EXCLUDES, fields=["tag2"])
270+
.where("min_size", QueryOperation.IS_GREATER_THAN, fields=100)
271+
.where("max_size", QueryOperation.IS_LESS_THAN, fields=1000000)
272+
.where("width", QueryOperation.IS_GREATER_THAN_OR_EQUAL, fields=100)
273+
.where("height", QueryOperation.IS_LESS_THAN_OR_EQUAL, fields=2000)
274+
.where("has_metadata", QueryOperation.EXISTS, fields=True)
275+
.where("filename", QueryOperation.MATCHES, fields=".*\\.jpg$"))
276+
277+
self.assertEqual(10, len(query.parameters))
278+
self.assertEqual(IMAGE, query.parameters["title"])
279+
self.assertEqual({"$ne": 0}, query.parameters["file_size"])
280+
self.assertEqual({"$in": ["tag1"]}, query.parameters["tags"])
281+
self.assertEqual({"$nin": ["tag2"]}, query.parameters["excluded"])
282+
self.assertEqual({"$gt": 100}, query.parameters["min_size"])
283+
self.assertEqual({"$lt": 1000000}, query.parameters["max_size"])
284+
self.assertEqual({"$gte": 100}, query.parameters["width"])
285+
self.assertEqual({"$lte": 2000}, query.parameters["height"])
286+
self.assertEqual({"$exists": True}, query.parameters["has_metadata"])
287+
self.assertEqual({"$regex": ".*\\.jpg$"}, query.parameters["filename"])
288+
289+
def test_30_asset_specific_methods_with_base_query_methods(self):
290+
"""Test AssetQuery specific methods combined with BaseQuery methods"""
291+
query = (self.asset_query
292+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
293+
.environment("dev")
294+
.version("1")
295+
.include_dimension()
296+
.relative_url()
297+
.include_count()
298+
.skip(0)
299+
.limit(10)
300+
.order_by_ascending("title"))
301+
302+
self.assertEqual({"title": IMAGE}, query.parameters)
303+
self.assertEqual("dev", query.http_instance.headers["environment"])
304+
self.assertEqual("1", query.asset_query_params["version"])
305+
self.assertEqual("true", query.asset_query_params["include_dimension"])
306+
self.assertEqual("true", query.asset_query_params["relative_urls"])
307+
self.assertEqual("true", query.query_params["include_count"])
308+
self.assertEqual("0", query.query_params["skip"])
309+
self.assertEqual("10", query.query_params["limit"])
310+
self.assertEqual("title", query.query_params["asc"])
311+
312+
def test_31_include_fallback_with_where_and_base_methods(self):
313+
"""Test include_fallback combined with where and BaseQuery methods"""
314+
query = (self.asset_query
315+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
316+
.include_fallback()
317+
.include_count()
318+
.skip(5)
319+
.limit(15)
320+
.order_by_ascending("title"))
321+
322+
self.assertEqual({"title": IMAGE}, query.parameters)
323+
self.assertEqual("true", query.asset_query_params["include_fallback"])
324+
self.assertEqual("true", query.query_params["include_count"])
325+
self.assertEqual("5", query.query_params["skip"])
326+
self.assertEqual("15", query.query_params["limit"])
327+
self.assertEqual("title", query.query_params["asc"])
328+
329+
def test_32_include_metadata_with_where_and_base_methods(self):
330+
"""Test include_metadata combined with where and BaseQuery methods"""
331+
query = (self.asset_query
332+
.where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000)
333+
.include_metadata()
334+
.include_count()
335+
.skip(10)
336+
.limit(20)
337+
.order_by_descending("file_size"))
338+
339+
self.assertEqual({"file_size": {"$gt": 1000}}, query.parameters)
340+
self.assertEqual("true", query.asset_query_params["include_metadata"])
341+
self.assertEqual("true", query.query_params["include_count"])
342+
self.assertEqual("10", query.query_params["skip"])
343+
self.assertEqual("20", query.query_params["limit"])
344+
self.assertEqual("file_size", query.query_params["desc"])
345+
346+
def test_33_locale_with_where_and_pagination(self):
347+
"""Test locale combined with where and pagination for assets"""
348+
query = (self.asset_query
349+
.locale('en-us')
350+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
351+
.include_count()
352+
.skip(0)
353+
.limit(10))
354+
355+
self.assertEqual("en-us", query.asset_query_params["locale"])
356+
self.assertEqual({"title": IMAGE}, query.parameters)
357+
self.assertEqual("true", query.query_params["include_count"])
358+
self.assertEqual("0", query.query_params["skip"])
359+
self.assertEqual("10", query.query_params["limit"])
360+
361+
def test_34_include_branch_with_where_and_base_methods(self):
362+
"""Test include_branch combined with where and BaseQuery methods"""
363+
query = (self.asset_query
364+
.where("title", QueryOperation.INCLUDES, fields=[IMAGE, "other.jpg"])
365+
.include_branch()
366+
.include_count()
367+
.skip(0)
368+
.limit(10))
369+
370+
self.assertEqual({"title": {"$in": [IMAGE, "other.jpg"]}}, query.parameters)
371+
self.assertEqual("true", query.asset_query_params["include_branch"])
372+
self.assertEqual("true", query.query_params["include_count"])
373+
self.assertEqual("0", query.query_params["skip"])
374+
self.assertEqual("10", query.query_params["limit"])
375+
376+
def test_35_complex_combination_all_asset_and_base_methods(self):
377+
"""Test complex combination of all AssetQuery and BaseQuery methods"""
378+
query = (self.asset_query
379+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
380+
.where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000)
381+
.where("content_type", QueryOperation.INCLUDES, fields=["image/jpeg", "image/png"])
382+
.environment("production")
383+
.version("2")
384+
.include_dimension()
385+
.relative_url()
386+
.include_fallback()
387+
.include_metadata()
388+
.include_branch()
389+
.locale("en-us")
390+
.include_count()
391+
.skip(10)
392+
.limit(50)
393+
.order_by_descending("created_at")
394+
.param("custom_param", "custom_value"))
395+
396+
# Verify parameters
397+
self.assertEqual(3, len(query.parameters))
398+
self.assertEqual(IMAGE, query.parameters["title"])
399+
self.assertEqual({"$gt": 1000}, query.parameters["file_size"])
400+
self.assertEqual({"$in": ["image/jpeg", "image/png"]}, query.parameters["content_type"])
401+
402+
# Verify asset_query_params
403+
self.assertEqual("production", query.http_instance.headers["environment"])
404+
self.assertEqual("2", query.asset_query_params["version"])
405+
self.assertEqual("true", query.asset_query_params["include_dimension"])
406+
self.assertEqual("true", query.asset_query_params["relative_urls"])
407+
self.assertEqual("true", query.asset_query_params["include_fallback"])
408+
self.assertEqual("true", query.asset_query_params["include_metadata"])
409+
self.assertEqual("true", query.asset_query_params["include_branch"])
410+
self.assertEqual("en-us", query.asset_query_params["locale"])
411+
412+
# Verify query_params
413+
self.assertEqual("true", query.query_params["include_count"])
414+
self.assertEqual("10", query.query_params["skip"])
415+
self.assertEqual("50", query.query_params["limit"])
416+
self.assertEqual("created_at", query.query_params["desc"])
417+
self.assertEqual("custom_value", query.query_params["custom_param"])
418+
419+
def test_36_add_params_with_where_and_other_methods(self):
420+
"""Test add_params combined with where and other methods for assets"""
421+
query = (self.asset_query
422+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
423+
.add_params({"locale": "en-us", "include_count": "true"})
424+
.skip(5)
425+
.limit(10))
426+
427+
self.assertEqual({"title": IMAGE}, query.parameters)
428+
self.assertEqual("en-us", query.query_params["locale"])
429+
self.assertEqual("true", query.query_params["include_count"])
430+
self.assertEqual("5", query.query_params["skip"])
431+
self.assertEqual("10", query.query_params["limit"])
432+
433+
def test_37_remove_param_after_combination(self):
434+
"""Test remove_param after building a complex asset query"""
435+
query = (self.asset_query
436+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
437+
.include_count()
438+
.skip(10)
439+
.limit(20)
440+
.param("key1", "value1")
441+
.param("key2", "value2")
442+
.remove_param("key1"))
443+
444+
self.assertEqual({"title": IMAGE}, query.parameters)
445+
self.assertNotIn("key1", query.query_params)
446+
self.assertEqual("value2", query.query_params["key2"])
447+
self.assertEqual("true", query.query_params["include_count"])
448+
self.assertEqual("10", query.query_params["skip"])
449+
self.assertEqual("20", query.query_params["limit"])
450+
451+
def test_38_order_by_ascending_then_descending_coexist(self):
452+
"""Test that order_by_ascending and order_by_descending can coexist for assets"""
453+
query = (self.asset_query
454+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
455+
.order_by_ascending("title")
456+
.order_by_descending("file_size"))
457+
458+
self.assertEqual({"title": IMAGE}, query.parameters)
459+
# Both asc and desc can coexist (they use different keys)
460+
self.assertEqual("title", query.query_params["asc"])
461+
self.assertEqual("file_size", query.query_params["desc"])
462+
463+
def test_39_multiple_where_conditions_with_complex_operations(self):
464+
"""Test multiple where conditions with complex operations and all BaseQuery methods for assets"""
465+
query = (self.asset_query
466+
.where("title", QueryOperation.EQUALS, fields=IMAGE)
467+
.where("file_size", QueryOperation.IS_GREATER_THAN, fields=1000)
468+
.where("file_size", QueryOperation.IS_LESS_THAN, fields=100000)
469+
.where("tags", QueryOperation.INCLUDES, fields=["image", "photo"])
470+
.where("excluded_tags", QueryOperation.EXCLUDES, fields=["archive"])
471+
.include_count()
472+
.skip(0)
473+
.limit(100)
474+
.order_by_descending("file_size")
475+
.param("locale", "en-us")
476+
.include_fallback())
477+
478+
# Verify all where conditions are present
479+
self.assertEqual(IMAGE, query.parameters["title"])
480+
# Note: file_size is overwritten by the second where call - last call wins
481+
self.assertEqual({"$lt": 100000}, query.parameters["file_size"])
482+
self.assertEqual({"$in": ["image", "photo"]}, query.parameters["tags"])
483+
self.assertEqual({"$nin": ["archive"]}, query.parameters["excluded_tags"])
484+
485+
# Verify all query_params
486+
self.assertEqual("true", query.query_params["include_count"])
487+
self.assertEqual("0", query.query_params["skip"])
488+
self.assertEqual("100", query.query_params["limit"])
489+
self.assertEqual("file_size", query.query_params["desc"])
490+
self.assertEqual("en-us", query.query_params["locale"])
491+
self.assertEqual("true", query.asset_query_params["include_fallback"])
492+
214493
# if __name__ == '__main__':
215494
# unittest.main()

0 commit comments

Comments
 (0)