Skip to content

DX | 09-06-2025 | Release #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## _v2.1.0_

### **Date: 02-June-2025**

- Global fields support added.

## _v2.0.1_

### **Date: 12-MAY-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.0.1'
__version__ = 'v2.1.0'
__endpoint__ = 'cdn.contentstack.io'
__email__ = '[email protected]'
__developer_email__ = '[email protected]'
Expand Down
73 changes: 73 additions & 0 deletions contentstack/globalfields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Global field defines the structure or schema of a page or a section of your web
or mobile property. To create content for your application, you are required
to first create a Global field, and then create entries using the
Global field.
"""

import logging
from urllib import parse

class GlobalField:
"""
Global field defines the structure or schema of a page or a
section of your web or mobile property. To create
content for your application, you are required to
first create a Global field, and then create entries using the
Global field.
"""

def __init__(self, http_instance, global_field_uid, logger=None):
self.http_instance = http_instance
self.__global_field_uid = global_field_uid
self.local_param = {}
self.logger = logger or logging.getLogger(__name__)


def fetch(self):
"""
This method is useful to fetch GlobalField of the of the stack.
:return:dict -- GlobalField response
------------------------------
Example:

>>> import contentstack
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
>>> global_field = stack.global_field('global_field_uid')
>>> some_dict = {'abc':'something'}
>>> response = global_field.fetch(some_dict)
------------------------------
"""
if self.__global_field_uid is None:
raise KeyError(
'global_field_uid can not be None to fetch GlobalField')
self.local_param['environment'] = self.http_instance.headers['environment']
uri = f'{self.http_instance.endpoint}/global_fields/{self.__global_field_uid}'
encoded_params = parse.urlencode(self.local_param)
url = f'{uri}?{encoded_params}'
result = self.http_instance.get(url)
return result

def find(self, params=None):
"""
This method is useful to fetch GlobalField of the of the stack.
:param params: dictionary of params
:return:dict -- GlobalField response
------------------------------
Example:

>>> import contentstack
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
>>> global_field = stack.global_field()
>>> some_dict = {'abc':'something'}
>>> response = global_field.find(param=some_dict)
------------------------------
"""
self.local_param['environment'] = self.http_instance.headers['environment']
if params is not None:
self.local_param.update(params)
encoded_params = parse.urlencode(self.local_param)
endpoint = self.http_instance.endpoint
url = f'{endpoint}/global_fields?{encoded_params}'
result = self.http_instance.get(url)
return result
10 changes: 10 additions & 0 deletions contentstack/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from contentstack.asset import Asset
from contentstack.assetquery import AssetQuery
from contentstack.contenttype import ContentType
from contentstack.globalfields import GlobalField
from contentstack.https_connection import HTTPSConnection
from contentstack.image_transform import ImageTransform

Expand Down Expand Up @@ -202,6 +203,15 @@ def content_type(self, content_type_uid=None):
:return: ContentType
"""
return ContentType(self.http_instance, content_type_uid)

def global_field(self, global_field_uid=None):
"""
Global field defines the structure or schema of a page or a section
of your web or mobile property.
param global_field_uid:
:return: GlobalField
"""
return GlobalField(self.http_instance, global_field_uid)

def asset(self, uid):
"""
Expand Down
11 changes: 10 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from .test_entry import TestEntry
from .test_query import TestQuery
from .test_stack import TestStack
from .test_global_fields import TestGlobalFieldInit
from .test_early_fetch import TestGlobalFieldFetch
from .test_early_find import TestGlobalFieldFind
from .test_live_preview import TestLivePreviewConfig


Expand All @@ -21,10 +24,16 @@ def all_tests():
test_module_entry = TestLoader().loadTestsFromTestCase(TestEntry)
test_module_query = TestLoader().loadTestsFromTestCase(TestQuery)
test_module_live_preview = TestLoader().loadTestsFromTestCase(TestLivePreviewConfig)
test_module_globalFields = TestLoader().loadTestsFromName(TestGlobalFieldInit)
test_module_globalFields_fetch = TestLoader().loadTestsFromName(TestGlobalFieldFetch)
test_module_globalFields_find = TestLoader().loadTestsFromName(TestGlobalFieldFind)
TestSuite([
test_module_stack,
test_module_asset,
test_module_entry,
test_module_query,
test_module_live_preview
test_module_live_preview,
test_module_globalFields,
test_module_globalFields_fetch,
test_module_globalFields_find
])
4 changes: 4 additions & 0 deletions tests/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pytest.ini
[pytest]
filterwarnings =
ignore::DeprecationWarning:.*ast.*:
124 changes: 124 additions & 0 deletions tests/test_early_fetch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
Unit tests for GlobalField.fetch method in contentstack.globalfields
"""

import pytest
from unittest.mock import MagicMock
from contentstack.globalfields import GlobalField
from urllib.parse import urlencode


@pytest.fixture
def mock_http_instance():
"""
Fixture to provide a mock http_instance with required attributes.
"""
mock = MagicMock()
mock.endpoint = "https://api.contentstack.io/v3"
mock.headers = {"environment": "test_env"}
mock.get = MagicMock(return_value={"global_field": "data"})
return mock


@pytest.fixture
def global_field_uid():
"""
Fixture to provide a sample global_field_uid.
"""
return "sample_uid"


@pytest.fixture
def global_field(mock_http_instance, global_field_uid):
"""
Fixture to provide a GlobalField instance with a mock http_instance and uid.
"""
return GlobalField(mock_http_instance, global_field_uid)


class TestGlobalFieldFetch:
# ------------------- Happy Path Tests -------------------

def test_fetch_returns_expected_result(self, global_field):
"""
Test that fetch returns the result from http_instance.get with correct URL and params.
"""
result = global_field.fetch()
assert result == {"global_field": "data"}
assert global_field.local_param["environment"] == "test_env"
expected_params = urlencode({"environment": "test_env"})
expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
global_field.http_instance.get.assert_called_once_with(expected_url)

def test_fetch_with_different_environment(self, mock_http_instance, global_field_uid):
"""
Test fetch with a different environment value in headers.
"""
mock_http_instance.headers["environment"] = "prod_env"
gf = GlobalField(mock_http_instance, global_field_uid)
result = gf.fetch()
assert result == {"global_field": "data"}
expected_params = urlencode({"environment": "prod_env"})
expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
mock_http_instance.get.assert_called_once_with(expected_url)

def test_fetch_preserves_existing_local_param(self, global_field):
"""
Test that fetch overwrites only the 'environment' key in local_param, preserving others.
"""
global_field.local_param = {"foo": "bar"}
result = global_field.fetch()
assert result == {"global_field": "data"}
assert global_field.local_param["foo"] == "bar"
assert global_field.local_param["environment"] == "test_env"
expected_params = urlencode({"foo": "bar", "environment": "test_env"})
expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
global_field.http_instance.get.assert_called_once_with(expected_url)

# ------------------- Edge Case Tests -------------------

def test_fetch_raises_keyerror_when_uid_is_none(self, mock_http_instance):
"""
Test that fetch raises KeyError if global_field_uid is None.
"""
gf = GlobalField(mock_http_instance, None)
with pytest.raises(KeyError, match="global_field_uid can not be None"):
gf.fetch()

def test_fetch_raises_keyerror_when_uid_is_explicitly_set_to_none(self, mock_http_instance):
"""
Test that fetch raises KeyError if global_field_uid is explicitly set to None after init.
"""
gf = GlobalField(mock_http_instance, "not_none")
gf._GlobalField__global_field_uid = None # forcibly set to None
with pytest.raises(KeyError, match="global_field_uid can not be None"):
gf.fetch()

def test_fetch_handles_special_characters_in_params(self, global_field):
"""
Test that fetch correctly encodes special characters in local_param.
"""
global_field.local_param = {"foo": "bar baz", "qux": "a&b"}
result = global_field.fetch()
assert result == {"global_field": "data"}
expected_params = urlencode({"foo": "bar baz", "qux": "a&b", "environment": "test_env"})
expected_url = f"https://api.contentstack.io/v3/global_fields/sample_uid?{expected_params}"
global_field.http_instance.get.assert_called_once_with(expected_url)

def test_fetch_raises_keyerror_if_environment_header_missing(self, mock_http_instance, global_field_uid):
"""
Test that fetch raises KeyError if 'environment' is missing from http_instance.headers.
"""
del mock_http_instance.headers["environment"]
gf = GlobalField(mock_http_instance, global_field_uid)
with pytest.raises(KeyError):
gf.fetch()

def test_fetch_propagates_http_instance_get_exception(self, global_field):
"""
Test that fetch propagates exceptions raised by http_instance.get.
"""
global_field.http_instance.get.side_effect = RuntimeError("Network error")
with pytest.raises(RuntimeError, match="Network error"):
global_field.fetch()

Loading
Loading