Contentstack 2.0.1__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {contentstack-2.0.1 → contentstack-2.2.0}/Contentstack.egg-info/PKG-INFO +1 -1
  2. {contentstack-2.0.1 → contentstack-2.2.0}/Contentstack.egg-info/SOURCES.txt +5 -0
  3. {contentstack-2.0.1 → contentstack-2.2.0}/PKG-INFO +1 -1
  4. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/__init__.py +1 -1
  5. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/contenttype.py +16 -0
  6. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/entry.py +17 -0
  7. contentstack-2.2.0/contentstack/globalfields.py +73 -0
  8. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/stack.py +10 -0
  9. contentstack-2.2.0/contentstack/variants.py +93 -0
  10. contentstack-2.2.0/tests/test_early_fetch.py +124 -0
  11. contentstack-2.2.0/tests/test_early_find.py +138 -0
  12. {contentstack-2.0.1 → contentstack-2.2.0}/tests/test_entry.py +42 -40
  13. contentstack-2.2.0/tests/test_global_fields.py +98 -0
  14. {contentstack-2.0.1 → contentstack-2.2.0}/tests/test_live_preview.py +3 -3
  15. {contentstack-2.0.1 → contentstack-2.2.0}/tests/test_stack.py +8 -5
  16. {contentstack-2.0.1 → contentstack-2.2.0}/Contentstack.egg-info/dependency_links.txt +0 -0
  17. {contentstack-2.0.1 → contentstack-2.2.0}/Contentstack.egg-info/not-zip-safe +0 -0
  18. {contentstack-2.0.1 → contentstack-2.2.0}/Contentstack.egg-info/requires.txt +0 -0
  19. {contentstack-2.0.1 → contentstack-2.2.0}/Contentstack.egg-info/top_level.txt +0 -0
  20. {contentstack-2.0.1 → contentstack-2.2.0}/LICENSE +0 -0
  21. {contentstack-2.0.1 → contentstack-2.2.0}/README.md +0 -0
  22. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/asset.py +0 -0
  23. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/assetquery.py +0 -0
  24. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/basequery.py +0 -0
  25. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/controller.py +0 -0
  26. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/deep_merge_lp.py +0 -0
  27. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/entryqueryable.py +0 -0
  28. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/https_connection.py +0 -0
  29. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/image_transform.py +0 -0
  30. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/query.py +0 -0
  31. {contentstack-2.0.1 → contentstack-2.2.0}/contentstack/utility.py +0 -0
  32. {contentstack-2.0.1 → contentstack-2.2.0}/setup.cfg +0 -0
  33. {contentstack-2.0.1 → contentstack-2.2.0}/setup.py +0 -0
  34. {contentstack-2.0.1 → contentstack-2.2.0}/tests/test_assets.py +0 -0
  35. {contentstack-2.0.1 → contentstack-2.2.0}/tests/test_query.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Contentstack
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: Contentstack is a headless CMS with an API-first approach.
5
5
  Home-page: https://github.com/contentstack/contentstack-python
6
6
  Author: Contentstack
@@ -16,13 +16,18 @@ contentstack/controller.py
16
16
  contentstack/deep_merge_lp.py
17
17
  contentstack/entry.py
18
18
  contentstack/entryqueryable.py
19
+ contentstack/globalfields.py
19
20
  contentstack/https_connection.py
20
21
  contentstack/image_transform.py
21
22
  contentstack/query.py
22
23
  contentstack/stack.py
23
24
  contentstack/utility.py
25
+ contentstack/variants.py
24
26
  tests/test_assets.py
27
+ tests/test_early_fetch.py
28
+ tests/test_early_find.py
25
29
  tests/test_entry.py
30
+ tests/test_global_fields.py
26
31
  tests/test_live_preview.py
27
32
  tests/test_query.py
28
33
  tests/test_stack.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Contentstack
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: Contentstack is a headless CMS with an API-first approach.
5
5
  Home-page: https://github.com/contentstack/contentstack-python
6
6
  Author: Contentstack
@@ -22,7 +22,7 @@ __all__ = (
22
22
  __title__ = 'contentstack-delivery-python'
23
23
  __author__ = 'contentstack'
24
24
  __status__ = 'debug'
25
- __version__ = 'v2.0.1'
25
+ __version__ = 'v2.2.0'
26
26
  __endpoint__ = 'cdn.contentstack.io'
27
27
  __email__ = 'support@contentstack.com'
28
28
  __developer_email__ = 'mobile@contentstack.com'
@@ -13,6 +13,7 @@ from urllib import parse
13
13
 
14
14
  from contentstack.entry import Entry
15
15
  from contentstack.query import Query
16
+ from contentstack.variants import Variants
16
17
 
17
18
  class ContentType:
18
19
  """
@@ -118,3 +119,18 @@ class ContentType:
118
119
  url = f'{endpoint}/content_types?{encoded_params}'
119
120
  result = self.http_instance.get(url)
120
121
  return result
122
+
123
+ def variants(self, variant_uid: str | list[str], params: dict = None):
124
+ """
125
+ Fetches the variants of the content type
126
+ :param variant_uid: {str} -- variant_uid
127
+ :return: Entry, so you can chain this call.
128
+ """
129
+ return Variants(
130
+ http_instance=self.http_instance,
131
+ content_type_uid=self.__content_type_uid,
132
+ entry_uid=None,
133
+ variant_uid=variant_uid,
134
+ params=params,
135
+ logger=None
136
+ )
@@ -8,6 +8,7 @@ from urllib import parse
8
8
 
9
9
  from contentstack.deep_merge_lp import DeepMergeMixin
10
10
  from contentstack.entryqueryable import EntryQueryable
11
+ from contentstack.variants import Variants
11
12
 
12
13
  class Entry(EntryQueryable):
13
14
  """
@@ -222,6 +223,22 @@ class Entry(EntryQueryable):
222
223
  merged_response = DeepMergeMixin(entry_response, lp_entry).to_dict() # Convert to dictionary
223
224
  return merged_response # Now correctly returns a dictionary
224
225
  raise ValueError("Missing required keys in live_preview data")
226
+
227
+ def variants(self, variant_uid: str | list[str], params: dict = None):
228
+ """
229
+ Fetches the variants of the entry
230
+ :param variant_uid: {str} -- variant_uid
231
+ :return: Entry, so you can chain this call.
232
+ """
233
+ return Variants(
234
+ http_instance=self.http_instance,
235
+ content_type_uid=self.content_type_id,
236
+ entry_uid=self.entry_uid,
237
+ variant_uid=variant_uid,
238
+ params=params,
239
+ logger=self.logger
240
+ )
241
+
225
242
 
226
243
 
227
244
 
@@ -0,0 +1,73 @@
1
+ """
2
+ Global field defines the structure or schema of a page or a section of your web
3
+ or mobile property. To create content for your application, you are required
4
+ to first create a Global field, and then create entries using the
5
+ Global field.
6
+ """
7
+
8
+ import logging
9
+ from urllib import parse
10
+
11
+ class GlobalField:
12
+ """
13
+ Global field defines the structure or schema of a page or a
14
+ section of your web or mobile property. To create
15
+ content for your application, you are required to
16
+ first create a Global field, and then create entries using the
17
+ Global field.
18
+ """
19
+
20
+ def __init__(self, http_instance, global_field_uid, logger=None):
21
+ self.http_instance = http_instance
22
+ self.__global_field_uid = global_field_uid
23
+ self.local_param = {}
24
+ self.logger = logger or logging.getLogger(__name__)
25
+
26
+
27
+ def fetch(self):
28
+ """
29
+ This method is useful to fetch GlobalField of the of the stack.
30
+ :return:dict -- GlobalField response
31
+ ------------------------------
32
+ Example:
33
+
34
+ >>> import contentstack
35
+ >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
36
+ >>> global_field = stack.global_field('global_field_uid')
37
+ >>> some_dict = {'abc':'something'}
38
+ >>> response = global_field.fetch(some_dict)
39
+ ------------------------------
40
+ """
41
+ if self.__global_field_uid is None:
42
+ raise KeyError(
43
+ 'global_field_uid can not be None to fetch GlobalField')
44
+ self.local_param['environment'] = self.http_instance.headers['environment']
45
+ uri = f'{self.http_instance.endpoint}/global_fields/{self.__global_field_uid}'
46
+ encoded_params = parse.urlencode(self.local_param)
47
+ url = f'{uri}?{encoded_params}'
48
+ result = self.http_instance.get(url)
49
+ return result
50
+
51
+ def find(self, params=None):
52
+ """
53
+ This method is useful to fetch GlobalField of the of the stack.
54
+ :param params: dictionary of params
55
+ :return:dict -- GlobalField response
56
+ ------------------------------
57
+ Example:
58
+
59
+ >>> import contentstack
60
+ >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
61
+ >>> global_field = stack.global_field()
62
+ >>> some_dict = {'abc':'something'}
63
+ >>> response = global_field.find(param=some_dict)
64
+ ------------------------------
65
+ """
66
+ self.local_param['environment'] = self.http_instance.headers['environment']
67
+ if params is not None:
68
+ self.local_param.update(params)
69
+ encoded_params = parse.urlencode(self.local_param)
70
+ endpoint = self.http_instance.endpoint
71
+ url = f'{endpoint}/global_fields?{encoded_params}'
72
+ result = self.http_instance.get(url)
73
+ return result
@@ -6,6 +6,7 @@ from urllib3.util import Retry
6
6
  from contentstack.asset import Asset
7
7
  from contentstack.assetquery import AssetQuery
8
8
  from contentstack.contenttype import ContentType
9
+ from contentstack.globalfields import GlobalField
9
10
  from contentstack.https_connection import HTTPSConnection
10
11
  from contentstack.image_transform import ImageTransform
11
12
 
@@ -202,6 +203,15 @@ class Stack:
202
203
  :return: ContentType
203
204
  """
204
205
  return ContentType(self.http_instance, content_type_uid)
206
+
207
+ def global_field(self, global_field_uid=None):
208
+ """
209
+ Global field defines the structure or schema of a page or a section
210
+ of your web or mobile property.
211
+ param global_field_uid:
212
+ :return: GlobalField
213
+ """
214
+ return GlobalField(self.http_instance, global_field_uid)
205
215
 
206
216
  def asset(self, uid):
207
217
  """
@@ -0,0 +1,93 @@
1
+ import logging
2
+ from urllib import parse
3
+
4
+ from contentstack.entryqueryable import EntryQueryable
5
+
6
+ class Variants(EntryQueryable):
7
+ """
8
+ An entry is the actual piece of content that you want to publish.
9
+ Entries can be created for one of the available content types.
10
+
11
+ Entry works with
12
+ version={version_number}
13
+ environment={environment_name}
14
+ locale={locale_code}
15
+ """
16
+
17
+ def __init__(self,
18
+ http_instance=None,
19
+ content_type_uid=None,
20
+ entry_uid=None,
21
+ variant_uid=None,
22
+ params=None,
23
+ logger=None):
24
+
25
+ super().__init__()
26
+ EntryQueryable.__init__(self)
27
+ self.entry_param = {}
28
+ self.http_instance = http_instance
29
+ self.content_type_id = content_type_uid
30
+ self.entry_uid = entry_uid
31
+ self.variant_uid = variant_uid
32
+ self.logger = logger or logging.getLogger(__name__)
33
+ self.entry_param = params or {}
34
+
35
+ def find(self, params=None):
36
+ """
37
+ find the variants of the entry of a particular content type
38
+ :param self.variant_uid: {str} -- self.variant_uid
39
+ :return: Entry, so you can chain this call.
40
+ """
41
+ headers = self.http_instance.headers.copy() # Create a local copy of headers
42
+ if isinstance(self.variant_uid, str):
43
+ headers['x-cs-variant-uid'] = self.variant_uid
44
+ elif isinstance(self.variant_uid, list):
45
+ headers['x-cs-variant-uid'] = ','.join(self.variant_uid)
46
+
47
+ if params is not None:
48
+ self.entry_param.update(params)
49
+ encoded_params = parse.urlencode(self.entry_param)
50
+ endpoint = self.http_instance.endpoint
51
+ url = f'{endpoint}/content_types/{self.content_type_id}/entries?{encoded_params}'
52
+ self.http_instance.headers.update(headers)
53
+ result = self.http_instance.get(url)
54
+ self.http_instance.headers.pop('x-cs-variant-uid', None)
55
+ return result
56
+
57
+ def fetch(self, params=None):
58
+ """
59
+ This method is useful to fetch variant entries of a particular content type and entries of the of the stack.
60
+ :return:dict -- contentType response
61
+ ------------------------------
62
+ Example:
63
+
64
+ >>> import contentstack
65
+ >>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
66
+ >>> content_type = stack.content_type('content_type_uid')
67
+ >>> some_dict = {'abc':'something'}
68
+ >>> response = content_type.fetch(some_dict)
69
+ ------------------------------
70
+ """
71
+ """
72
+ Fetches the variants of the entry
73
+ :param self.variant_uid: {str} -- self.variant_uid
74
+ :return: Entry, so you can chain this call.
75
+ """
76
+ if self.entry_uid is None:
77
+ raise ValueError("entry_uid is required")
78
+ else:
79
+ headers = self.http_instance.headers.copy() # Create a local copy of headers
80
+ if isinstance(self.variant_uid, str):
81
+ headers['x-cs-variant-uid'] = self.variant_uid
82
+ elif isinstance(self.variant_uid, list):
83
+ headers['x-cs-variant-uid'] = ','.join(self.variant_uid)
84
+
85
+ if params is not None:
86
+ self.entry_param.update(params)
87
+ encoded_params = parse.urlencode(self.entry_param)
88
+ endpoint = self.http_instance.endpoint
89
+ url = f'{endpoint}/content_types/{self.content_type_id}/entries/{self.entry_uid}?{encoded_params}'
90
+ self.http_instance.headers.update(headers)
91
+ result = self.http_instance.get(url)
92
+ self.http_instance.headers.pop('x-cs-variant-uid', None)
93
+ return result
@@ -0,0 +1,124 @@
1
+ """
2
+ Unit tests for GlobalField.fetch method in contentstack.globalfields
3
+ """
4
+
5
+ import pytest
6
+ from unittest.mock import MagicMock
7
+ from contentstack.globalfields import GlobalField
8
+ from urllib.parse import urlencode
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_http_instance():
13
+ """
14
+ Fixture to provide a mock http_instance with required attributes.
15
+ """
16
+ mock = MagicMock()
17
+ mock.endpoint = "https://api.contentstack.io/v3"
18
+ mock.headers = {"environment": "test_env"}
19
+ mock.get = MagicMock(return_value={"global_field": "data"})
20
+ return mock
21
+
22
+
23
+ @pytest.fixture
24
+ def global_field_uid():
25
+ """
26
+ Fixture to provide a sample global_field_uid.
27
+ """
28
+ return "sample_uid"
29
+
30
+
31
+ @pytest.fixture
32
+ def global_field(mock_http_instance, global_field_uid):
33
+ """
34
+ Fixture to provide a GlobalField instance with a mock http_instance and uid.
35
+ """
36
+ return GlobalField(mock_http_instance, global_field_uid)
37
+
38
+
39
+ class TestGlobalFieldFetch:
40
+ # ------------------- Happy Path Tests -------------------
41
+
42
+ def test_fetch_returns_expected_result(self, global_field):
43
+ """
44
+ Test that fetch returns the result from http_instance.get with correct URL and params.
45
+ """
46
+ result = global_field.fetch()
47
+ assert result == {"global_field": "data"}
48
+ assert global_field.local_param["environment"] == "test_env"
49
+ expected_params = urlencode({"environment": "test_env"})
50
+ expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
51
+ global_field.http_instance.get.assert_called_once_with(expected_url)
52
+
53
+ def test_fetch_with_different_environment(self, mock_http_instance, global_field_uid):
54
+ """
55
+ Test fetch with a different environment value in headers.
56
+ """
57
+ mock_http_instance.headers["environment"] = "prod_env"
58
+ gf = GlobalField(mock_http_instance, global_field_uid)
59
+ result = gf.fetch()
60
+ assert result == {"global_field": "data"}
61
+ expected_params = urlencode({"environment": "prod_env"})
62
+ expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
63
+ mock_http_instance.get.assert_called_once_with(expected_url)
64
+
65
+ def test_fetch_preserves_existing_local_param(self, global_field):
66
+ """
67
+ Test that fetch overwrites only the 'environment' key in local_param, preserving others.
68
+ """
69
+ global_field.local_param = {"foo": "bar"}
70
+ result = global_field.fetch()
71
+ assert result == {"global_field": "data"}
72
+ assert global_field.local_param["foo"] == "bar"
73
+ assert global_field.local_param["environment"] == "test_env"
74
+ expected_params = urlencode({"foo": "bar", "environment": "test_env"})
75
+ expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
76
+ global_field.http_instance.get.assert_called_once_with(expected_url)
77
+
78
+ # ------------------- Edge Case Tests -------------------
79
+
80
+ def test_fetch_raises_keyerror_when_uid_is_none(self, mock_http_instance):
81
+ """
82
+ Test that fetch raises KeyError if global_field_uid is None.
83
+ """
84
+ gf = GlobalField(mock_http_instance, None)
85
+ with pytest.raises(KeyError, match="global_field_uid can not be None"):
86
+ gf.fetch()
87
+
88
+ def test_fetch_raises_keyerror_when_uid_is_explicitly_set_to_none(self, mock_http_instance):
89
+ """
90
+ Test that fetch raises KeyError if global_field_uid is explicitly set to None after init.
91
+ """
92
+ gf = GlobalField(mock_http_instance, "not_none")
93
+ gf._GlobalField__global_field_uid = None # forcibly set to None
94
+ with pytest.raises(KeyError, match="global_field_uid can not be None"):
95
+ gf.fetch()
96
+
97
+ def test_fetch_handles_special_characters_in_params(self, global_field):
98
+ """
99
+ Test that fetch correctly encodes special characters in local_param.
100
+ """
101
+ global_field.local_param = {"foo": "bar baz", "qux": "a&b"}
102
+ result = global_field.fetch()
103
+ assert result == {"global_field": "data"}
104
+ expected_params = urlencode({"foo": "bar baz", "qux": "a&b", "environment": "test_env"})
105
+ expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
106
+ global_field.http_instance.get.assert_called_once_with(expected_url)
107
+
108
+ def test_fetch_raises_keyerror_if_environment_header_missing(self, mock_http_instance, global_field_uid):
109
+ """
110
+ Test that fetch raises KeyError if 'environment' is missing from http_instance.headers.
111
+ """
112
+ del mock_http_instance.headers["environment"]
113
+ gf = GlobalField(mock_http_instance, global_field_uid)
114
+ with pytest.raises(KeyError):
115
+ gf.fetch()
116
+
117
+ def test_fetch_propagates_http_instance_get_exception(self, global_field):
118
+ """
119
+ Test that fetch propagates exceptions raised by http_instance.get.
120
+ """
121
+ global_field.http_instance.get.side_effect = RuntimeError("Network error")
122
+ with pytest.raises(RuntimeError, match="Network error"):
123
+ global_field.fetch()
124
+
@@ -0,0 +1,138 @@
1
+ # test_globalfields_find.py
2
+
3
+ import pytest
4
+ from unittest.mock import MagicMock, patch
5
+ from contentstack.globalfields import GlobalField
6
+
7
+ @pytest.fixture
8
+ def mock_http_instance():
9
+ """
10
+ Fixture to provide a mock http_instance with headers and endpoint.
11
+ """
12
+ mock = MagicMock()
13
+ mock.headers = {"environment": "test_env"}
14
+ mock.endpoint = "https://api.contentstack.io/v3"
15
+ mock.get = MagicMock(return_value={"global_fields": "data"})
16
+ return mock
17
+
18
+ @pytest.fixture
19
+ def global_field_uid():
20
+ """
21
+ Fixture to provide a sample global_field_uid.
22
+ """
23
+ return "sample_uid"
24
+
25
+ class TestGlobalFieldFind:
26
+ """
27
+ Unit tests for GlobalField.find method, covering happy paths and edge cases.
28
+ """
29
+
30
+ # -------------------- Happy Path Tests --------------------
31
+
32
+ def test_find_with_no_params(self, mock_http_instance, global_field_uid):
33
+ """
34
+ Test that find() with no params returns expected result and constructs correct URL.
35
+ """
36
+ gf = GlobalField(mock_http_instance, global_field_uid)
37
+ result = gf.find()
38
+ assert result == {"global_fields": "data"}
39
+ expected_url = (
40
+ "https://api.contentstack.io/v3/global_fields?environment=test_env"
41
+ )
42
+ mock_http_instance.get.assert_called_once_with(expected_url)
43
+
44
+ def test_find_with_params(self, mock_http_instance, global_field_uid):
45
+ """
46
+ Test that find() with additional params merges them and encodes URL correctly.
47
+ """
48
+ gf = GlobalField(mock_http_instance, global_field_uid)
49
+ params = {"limit": 10, "skip": 5}
50
+ result = gf.find(params=params)
51
+ # The order of query params in the URL is not guaranteed, so check both possibilities
52
+ called_url = mock_http_instance.get.call_args[0][0]
53
+ assert result == {"global_fields": "data"}
54
+ assert called_url.startswith("https://api.contentstack.io/v3/global_fields?")
55
+ # All params must be present in the URL
56
+ for k, v in {"environment": "test_env", "limit": "10", "skip": "5"}.items():
57
+ assert f"{k}={v}" in called_url
58
+
59
+ def test_find_with_empty_params_dict(self, mock_http_instance, global_field_uid):
60
+ """
61
+ Test that find() with an empty params dict behaves like no params.
62
+ """
63
+ gf = GlobalField(mock_http_instance, global_field_uid)
64
+ result = gf.find(params={})
65
+ assert result == {"global_fields": "data"}
66
+ expected_url = (
67
+ "https://api.contentstack.io/v3/global_fields?environment=test_env"
68
+ )
69
+ mock_http_instance.get.assert_called_once_with(expected_url)
70
+
71
+
72
+
73
+ def test_find_with_special_characters_in_params(self, mock_http_instance, global_field_uid):
74
+ """
75
+ Test that find() correctly URL-encodes special characters in params.
76
+ """
77
+ gf = GlobalField(mock_http_instance, global_field_uid)
78
+ params = {"q": "name:foo/bar&baz", "limit": 1}
79
+ result = gf.find(params=params)
80
+ called_url = mock_http_instance.get.call_args[0][0]
81
+ # Check that special characters are URL-encoded
82
+ assert "q=name%3Afoo%2Fbar%26baz" in called_url
83
+ assert "limit=1" in called_url
84
+ assert result == {"global_fields": "data"}
85
+
86
+ def test_find_with_none_environment_in_headers(self, mock_http_instance, global_field_uid):
87
+ """
88
+ Test that find() handles the case where 'environment' in headers is None.
89
+ """
90
+ mock_http_instance.headers["environment"] = None
91
+ gf = GlobalField(mock_http_instance, global_field_uid)
92
+ result = gf.find()
93
+ called_url = mock_http_instance.get.call_args[0][0]
94
+ # Should include 'environment=None' in the query string
95
+ assert "environment=None" in called_url
96
+ assert result == {"global_fields": "data"}
97
+
98
+ def test_find_with_non_string_param_values(self, mock_http_instance, global_field_uid):
99
+ """
100
+ Test that find() handles non-string param values (e.g., int, bool, None).
101
+ """
102
+ gf = GlobalField(mock_http_instance, global_field_uid)
103
+ params = {"int_val": 42, "bool_val": True, "none_val": None}
104
+ result = gf.find(params=params)
105
+ called_url = mock_http_instance.get.call_args[0][0]
106
+ # int and bool should be stringified, None should be 'None'
107
+ assert "int_val=42" in called_url
108
+ assert "bool_val=True" in called_url
109
+ assert "none_val=None" in called_url
110
+ assert result == {"global_fields": "data"}
111
+
112
+ def test_find_with_empty_headers(self, global_field_uid):
113
+ """
114
+ Test that find() raises KeyError if 'environment' is missing from headers.
115
+ """
116
+ mock_http_instance = MagicMock()
117
+ mock_http_instance.headers = {}
118
+ mock_http_instance.endpoint = "https://api.contentstack.io/v3"
119
+ mock_http_instance.get = MagicMock(return_value={"global_fields": "data"})
120
+ gf = GlobalField(mock_http_instance, global_field_uid)
121
+ with pytest.raises(KeyError):
122
+ gf.find()
123
+
124
+
125
+ def test_find_with_mutable_local_param(self, mock_http_instance, global_field_uid):
126
+ """
127
+ Test that local_param is updated and persists between calls.
128
+ """
129
+ gf = GlobalField(mock_http_instance, global_field_uid)
130
+ # First call with a param
131
+ gf.find(params={"foo": "bar"})
132
+ # Second call with a different param
133
+ gf.find(params={"baz": "qux"})
134
+ # local_param should have been updated with the last call's params
135
+ assert gf.local_param["baz"] == "qux"
136
+ assert gf.local_param["environment"] == "test_env"
137
+ # The previous param 'foo' should still be present (since update is cumulative)
138
+ assert gf.local_param["foo"] == "bar"
@@ -3,12 +3,12 @@ import unittest
3
3
  import config
4
4
  import contentstack
5
5
 
6
- _UID = 'blt53ca1231625bdde4'
7
6
  API_KEY = config.APIKEY
8
7
  DELIVERY_TOKEN = config.DELIVERYTOKEN
9
8
  ENVIRONMENT = config.ENVIRONMENT
10
9
  HOST = config.HOST
11
-
10
+ FAQ_UID = config.FAQ_UID # Add this in your config.py
11
+ VARIANT_UID = config.VARIANT_UID
12
12
 
13
13
  class TestEntry(unittest.TestCase):
14
14
 
@@ -19,69 +19,54 @@ class TestEntry(unittest.TestCase):
19
19
  query = self.stack.content_type('faq').query()
20
20
  result = query.find()
21
21
  if result is not None:
22
- self._UID = result['entries'][0]['uid']
23
- print(f'the uid is: {_UID}')
22
+ self.faq_uid = result['entries'][0]['uid']
23
+ print(f'the uid is: {self.faq_uid}')
24
24
 
25
25
  def test_entry_by_UID(self):
26
- global _UID
27
- entry = self.stack.content_type('faq').entry(_UID)
26
+ entry = self.stack.content_type('faq').entry(FAQ_UID)
28
27
  result = entry.fetch()
29
28
  if result is not None:
30
- _UID = result['entry']['uid']
31
- self.assertEqual(_UID, result['entry']['uid'])
29
+ self.assertEqual(FAQ_UID, result['entry']['uid'])
32
30
 
33
31
  def test_03_entry_environment(self):
34
- global _UID
35
- entry = self.stack.content_type('faq').entry(
36
- _UID).environment('test')
32
+ entry = self.stack.content_type('faq').entry(FAQ_UID).environment('test')
37
33
  self.assertEqual("test", entry.http_instance.headers['environment'])
38
34
 
39
35
  def test_04_entry_locale(self):
40
- global _UID
41
- entry = self.stack.content_type('faq').entry(_UID).locale('en-ei')
36
+ entry = self.stack.content_type('faq').entry(FAQ_UID).locale('en-ei')
42
37
  entry.fetch()
43
38
  self.assertEqual('en-ei', entry.entry_param['locale'])
44
39
 
45
40
  def test_05_entry_version(self):
46
- global _UID
47
- entry = self.stack.content_type('faq').entry(_UID).version(3)
41
+ entry = self.stack.content_type('faq').entry(FAQ_UID).version(3)
48
42
  entry.fetch()
49
43
  self.assertEqual(3, entry.entry_param['version'])
50
44
 
51
45
  def test_06_entry_params(self):
52
- global _UID
53
- entry = self.stack.content_type('faq').entry(
54
- _UID).param('param_key', 'param_value')
46
+ entry = self.stack.content_type('faq').entry(FAQ_UID).param('param_key', 'param_value')
55
47
  entry.fetch()
56
48
  self.assertEqual('param_value', entry.entry_param['param_key'])
57
49
 
58
50
  def test_07_entry_base_only(self):
59
- global _UID
60
- entry = self.stack.content_type(
61
- 'faq').entry(_UID).only('field_UID')
51
+ entry = self.stack.content_type('faq').entry(FAQ_UID).only('field_UID')
62
52
  entry.fetch()
63
53
  self.assertEqual({'environment': 'development',
64
54
  'only[BASE][]': 'field_UID'}, entry.entry_param)
65
55
 
66
56
  def test_08_entry_base_excepts(self):
67
- global _UID
68
- entry = self.stack.content_type('faq').entry(
69
- _UID).excepts('field_UID')
57
+ entry = self.stack.content_type('faq').entry(FAQ_UID).excepts('field_UID')
70
58
  entry.fetch()
71
59
  self.assertEqual({'environment': 'development',
72
60
  'except[BASE][]': 'field_UID'}, entry.entry_param)
73
61
 
74
62
  def test_10_entry_base_include_reference_only(self):
75
- global _UID
76
- entry = self.stack.content_type('faq').entry(_UID).only('field1')
63
+ entry = self.stack.content_type('faq').entry(FAQ_UID).only('field1')
77
64
  entry.fetch()
78
65
  self.assertEqual({'environment': 'development', 'only[BASE][]': 'field1'},
79
66
  entry.entry_param)
80
67
 
81
68
  def test_11_entry_base_include_reference_excepts(self):
82
- global _UID
83
- entry = self.stack.content_type(
84
- 'faq').entry(_UID).excepts('field1')
69
+ entry = self.stack.content_type('faq').entry(FAQ_UID).excepts('field1')
85
70
  entry.fetch()
86
71
  self.assertEqual({'environment': 'development', 'except[BASE][]': 'field1'},
87
72
  entry.entry_param)
@@ -95,15 +80,13 @@ class TestEntry(unittest.TestCase):
95
80
  response = _entry.fetch()
96
81
 
97
82
  def test_13_entry_support_include_fallback_unit_test(self):
98
- global _UID
99
- entry = self.stack.content_type('faq').entry(
100
- _UID).include_fallback()
83
+ entry = self.stack.content_type('faq').entry(FAQ_UID).include_fallback()
101
84
  self.assertEqual(
102
85
  True, entry.entry_param.__contains__('include_fallback'))
103
86
 
104
87
  def test_14_entry_queryable_only(self):
105
88
  try:
106
- entry = self.stack.content_type('faq').entry(_UID).only(4)
89
+ entry = self.stack.content_type('faq').entry(FAQ_UID).only(4)
107
90
  result = entry.fetch()
108
91
  self.assertEqual(None, result['uid'])
109
92
  except KeyError as e:
@@ -112,7 +95,7 @@ class TestEntry(unittest.TestCase):
112
95
 
113
96
  def test_entry_queryable_excepts(self):
114
97
  try:
115
- entry = self.stack.content_type('faq').entry(_UID).excepts(4)
98
+ entry = self.stack.content_type('faq').entry(FAQ_UID).excepts(4)
116
99
  result = entry.fetch()
117
100
  self.assertEqual(None, result['uid'])
118
101
  except KeyError as e:
@@ -120,20 +103,17 @@ class TestEntry(unittest.TestCase):
120
103
  self.assertEqual("Invalid field_UID provided", e.args[0])
121
104
 
122
105
  def test_16_entry_queryable_include_content_type(self):
123
- entry = self.stack.content_type('faq').entry(
124
- _UID).include_content_type()
106
+ entry = self.stack.content_type('faq').entry(FAQ_UID).include_content_type()
125
107
  self.assertEqual({'include_content_type': 'true', 'include_global_field_schema': 'true'},
126
108
  entry.entry_queryable_param)
127
109
 
128
110
  def test_reference_content_type_uid(self):
129
- entry = self.stack.content_type('faq').entry(
130
- _UID).include_reference_content_type_uid()
111
+ entry = self.stack.content_type('faq').entry(FAQ_UID).include_reference_content_type_uid()
131
112
  self.assertEqual({'include_reference_content_type_uid': 'true'},
132
113
  entry.entry_queryable_param)
133
114
 
134
115
  def test_19_entry_queryable_add_param(self):
135
- entry = self.stack.content_type('faq').entry(
136
- _UID).add_param('cms', 'contentstack')
116
+ entry = self.stack.content_type('faq').entry(FAQ_UID).add_param('cms', 'contentstack')
137
117
  self.assertEqual({'cms': 'contentstack'}, entry.entry_queryable_param)
138
118
 
139
119
  def test_20_entry_include_fallback(self):
@@ -154,6 +134,28 @@ class TestEntry(unittest.TestCase):
154
134
  content_type = self.stack.content_type('faq')
155
135
  entry = content_type.entry("878783238783").include_metadata()
156
136
  self.assertEqual({'include_metadata': 'true'}, entry.entry_queryable_param)
137
+
138
+ def test_23_content_type_variants(self):
139
+ content_type = self.stack.content_type('faq')
140
+ entry = content_type.variants(VARIANT_UID).find()
141
+ self.assertIn('variants', entry['entries'][0]['publish_details'])
142
+
143
+ def test_24_entry_variants(self):
144
+ content_type = self.stack.content_type('faq')
145
+ entry = content_type.entry(FAQ_UID).variants(VARIANT_UID).fetch()
146
+ self.assertIn('variants', entry['entry']['publish_details'])
147
+
148
+ def test_25_content_type_variants_with_has_hash_variant(self):
149
+ content_type = self.stack.content_type('faq')
150
+ entry = content_type.variants([VARIANT_UID]).find()
151
+ self.assertIn('variants', entry['entries'][0]['publish_details'])
152
+
153
+ def test_25_content_type_entry_variants_with_has_hash_variant(self):
154
+ content_type = self.stack.content_type('faq').entry(FAQ_UID)
155
+ entry = content_type.variants([VARIANT_UID]).fetch()
156
+ self.assertIn('variants', entry['entry']['publish_details'])
157
+
158
+
157
159
 
158
160
 
159
161
  if __name__ == '__main__':
@@ -0,0 +1,98 @@
1
+ # test_globalfields_init.py
2
+
3
+ import pytest
4
+ import logging
5
+ from contentstack.globalfields import GlobalField
6
+
7
+ class DummyHttpInstance:
8
+ """A dummy HTTP instance for testing purposes."""
9
+ pass
10
+
11
+ @pytest.fixture
12
+ def dummy_http():
13
+ """Fixture to provide a dummy http_instance."""
14
+ return DummyHttpInstance()
15
+
16
+ @pytest.fixture
17
+ def dummy_logger():
18
+ """Fixture to provide a dummy logger."""
19
+ return logging.getLogger("dummy_logger")
20
+
21
+ @pytest.mark.usefixtures("dummy_http")
22
+ class TestGlobalFieldInit:
23
+ """
24
+ Unit tests for GlobalField.__init__ method.
25
+ """
26
+
27
+ # -------------------- Happy Path Tests --------------------
28
+
29
+ def test_init_with_all_arguments(self, dummy_http, dummy_logger):
30
+ """
31
+ Test that __init__ correctly assigns all arguments when all are provided.
32
+ """
33
+ uid = "global_field_123"
34
+ gf = GlobalField(dummy_http, uid, logger=dummy_logger)
35
+ assert gf.http_instance is dummy_http
36
+ # Accessing the private variable via name mangling
37
+ assert gf._GlobalField__global_field_uid == uid
38
+ assert gf.local_param == {}
39
+ assert gf.logger is dummy_logger
40
+
41
+ def test_init_without_logger_uses_default(self, dummy_http):
42
+ """
43
+ Test that __init__ assigns a default logger if none is provided.
44
+ """
45
+ uid = "gf_uid"
46
+ gf = GlobalField(dummy_http, uid)
47
+ assert gf.http_instance is dummy_http
48
+ assert gf._GlobalField__global_field_uid == uid
49
+ assert gf.local_param == {}
50
+ # Should be a logger instance, and not None
51
+ assert isinstance(gf.logger, logging.Logger)
52
+ # Should be the logger for the module
53
+ assert gf.logger.name == "contentstack.globalfields"
54
+
55
+ # -------------------- Edge Case Tests --------------------
56
+
57
+ def test_init_with_none_uid(self, dummy_http):
58
+ """
59
+ Test that __init__ accepts None as global_field_uid.
60
+ """
61
+ gf = GlobalField(dummy_http, None)
62
+ assert gf._GlobalField__global_field_uid is None
63
+
64
+ def test_init_with_empty_string_uid(self, dummy_http):
65
+ """
66
+ Test that __init__ accepts empty string as global_field_uid.
67
+ """
68
+ gf = GlobalField(dummy_http, "")
69
+ assert gf._GlobalField__global_field_uid == ""
70
+
71
+ def test_init_with_non_string_uid(self, dummy_http):
72
+ """
73
+ Test that __init__ accepts non-string types for global_field_uid.
74
+ """
75
+ for val in [123, 45.6, {"a": 1}, [1, 2, 3], (4, 5), True, object()]:
76
+ gf = GlobalField(dummy_http, val)
77
+ assert gf._GlobalField__global_field_uid == val
78
+
79
+ def test_init_with_none_http_instance(self):
80
+ """
81
+ Test that __init__ accepts None as http_instance.
82
+ """
83
+ uid = "gf_uid"
84
+ gf = GlobalField(None, uid)
85
+ assert gf.http_instance is None
86
+ assert gf._GlobalField__global_field_uid == uid
87
+
88
+ def test_init_with_custom_logger_object(self, dummy_http):
89
+ """
90
+ Test that __init__ accepts any object as logger.
91
+ """
92
+ class DummyLogger:
93
+ def info(self, msg): pass
94
+ dummy = DummyLogger()
95
+ gf = GlobalField(dummy_http, "uid", logger=dummy)
96
+ assert gf.logger is dummy
97
+
98
+
@@ -4,9 +4,9 @@ import config
4
4
  import contentstack
5
5
  from contentstack.deep_merge_lp import DeepMergeMixin
6
6
 
7
- management_token = 'cs8743874323343u9'
8
- entry_uid = 'blt8743874323343u9'
9
- preview_token = 'abcdefgh1234567890'
7
+ management_token = config.MANAGEMENT_TOKEN
8
+ entry_uid = config.LIVE_PREVIEW_ENTRY_UID
9
+ preview_token = config.PREVIEW_TOKEN
10
10
 
11
11
  _lp_query = {
12
12
  'live_preview': '#0#0#0#0#0#0#0#0#0#',
@@ -131,11 +131,14 @@ class TestStack(unittest.TestCase):
131
131
  self.assertEqual(
132
132
  'is not valid.', result['errors']['pagination_token'][0])
133
133
 
134
- @unittest.skip('Work in progress')
135
- def test_16_initialise_sync(self):
136
- result = self.stack.sync_init()
137
- if result is not None:
138
- self.assertEqual(16, result['total_count'])
134
+ # Deprecated: This test was skipped due to deprecation of the sync_init feature or its API.
135
+ # If sync_init is permanently removed or unsupported, this test should remain commented or be deleted.
136
+ # If migration or replacement is planned, update this test accordingly.
137
+ # @unittest.skip('Work in progress')
138
+ # def test_16_initialise_sync(self):
139
+ # result = self.stack.sync_init()
140
+ # if result is not None:
141
+ # self.assertEqual(16, result['total_count'])
139
142
 
140
143
  def test_17_entry_with_sync_token(self):
141
144
  result = self.stack.sync_token('sync_token')
File without changes
File without changes
File without changes
File without changes