Skip to content

Commit 3726c1e

Browse files
Merge pull request #135 from contentstack/staging
DX | 09-06-2025 | Release
2 parents 0246321 + a2f0706 commit 3726c1e

File tree

9 files changed

+464
-2
lines changed

9 files changed

+464
-2
lines changed

CHANGELOG.md

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

3+
## _v2.1.0_
4+
5+
### **Date: 02-June-2025**
6+
7+
- Global fields support added.
8+
39
## _v2.0.1_
410

511
### **Date: 12-MAY-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.0.1'
25+
__version__ = 'v2.1.0'
2626
__endpoint__ = 'cdn.contentstack.io'
2727
__email__ = '[email protected]'
2828
__developer_email__ = '[email protected]'

contentstack/globalfields.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

contentstack/stack.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from contentstack.asset import Asset
77
from contentstack.assetquery import AssetQuery
88
from contentstack.contenttype import ContentType
9+
from contentstack.globalfields import GlobalField
910
from contentstack.https_connection import HTTPSConnection
1011
from contentstack.image_transform import ImageTransform
1112

@@ -202,6 +203,15 @@ def content_type(self, content_type_uid=None):
202203
:return: ContentType
203204
"""
204205
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)
205215

206216
def asset(self, uid):
207217
"""

tests/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from .test_entry import TestEntry
1313
from .test_query import TestQuery
1414
from .test_stack import TestStack
15+
from .test_global_fields import TestGlobalFieldInit
16+
from .test_early_fetch import TestGlobalFieldFetch
17+
from .test_early_find import TestGlobalFieldFind
1518
from .test_live_preview import TestLivePreviewConfig
1619

1720

@@ -21,10 +24,16 @@ def all_tests():
2124
test_module_entry = TestLoader().loadTestsFromTestCase(TestEntry)
2225
test_module_query = TestLoader().loadTestsFromTestCase(TestQuery)
2326
test_module_live_preview = TestLoader().loadTestsFromTestCase(TestLivePreviewConfig)
27+
test_module_globalFields = TestLoader().loadTestsFromName(TestGlobalFieldInit)
28+
test_module_globalFields_fetch = TestLoader().loadTestsFromName(TestGlobalFieldFetch)
29+
test_module_globalFields_find = TestLoader().loadTestsFromName(TestGlobalFieldFind)
2430
TestSuite([
2531
test_module_stack,
2632
test_module_asset,
2733
test_module_entry,
2834
test_module_query,
29-
test_module_live_preview
35+
test_module_live_preview,
36+
test_module_globalFields,
37+
test_module_globalFields_fetch,
38+
test_module_globalFields_find
3039
])

tests/pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# pytest.ini
2+
[pytest]
3+
filterwarnings =
4+
ignore::DeprecationWarning:.*ast.*:

tests/test_early_fetch.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)