Skip to content

Commit fdb130a

Browse files
Merge pull request #60 from contentstack/enh/dx-5411
Added variant utility functions
2 parents 479aa4a + ac6d4f6 commit fdb130a

File tree

4 files changed

+265
-1
lines changed

4 files changed

+265
-1
lines changed

changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
**CHANGELOG**
33
================
44

5+
*v1.4.0*
6+
============
7+
8+
NEW FEATURE: Variants utility (CDA entry variant aliases).
9+
10+
- Added ``Utils.get_variant_aliases`` to read variant alias strings from ``publish_details.variants`` on a CDA entry (single dict or list of entries). Supports optional ``content_type_uid`` when ``_content_type_uid`` is absent on the entry.
11+
- Added ``Utils.get_variant_metadata_tags`` to build a ``data-csvariants`` HTML data-attribute value (JSON string of the multi-entry alias results).
12+
513
*v1.3.3*
614
============
715

contentstack_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@
3131
__title__ = 'contentstack_utils'
3232
__author__ = 'contentstack'
3333
__status__ = 'debug'
34-
__version__ = '1.3.2'
34+
__version__ = '1.4.0'
3535
__endpoint__ = 'cdn.contentstack.io'
3636
__contact__ = 'support@contentstack.com'

contentstack_utils/utils.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# pylint: disable=missing-function-docstring
22

3+
import json
4+
from typing import Any, Dict, List, Union
5+
36
from lxml import etree
47

58
from contentstack_utils.automate import Automate
@@ -10,6 +13,95 @@
1013

1114
class Utils(Automate):
1215

16+
@staticmethod
17+
def _variants_map_from_entry(entry: dict) -> dict:
18+
publish_details = entry.get("publish_details")
19+
if not isinstance(publish_details, dict):
20+
return {}
21+
raw = publish_details.get("variants")
22+
return raw if isinstance(raw, dict) else {}
23+
24+
@staticmethod
25+
def _aliases_from_variants_map(variants_map: dict) -> List[str]:
26+
aliases: List[str] = []
27+
for _variant_uid, value in variants_map.items():
28+
if not isinstance(value, dict):
29+
continue
30+
alias = value.get("alias")
31+
if alias is None:
32+
continue
33+
alias_str = str(alias).strip()
34+
if alias_str:
35+
aliases.append(alias_str)
36+
return aliases
37+
38+
@staticmethod
39+
def _variant_aliases_for_entry(entry: dict, content_type_uid: str = "") -> Dict[str, Any]:
40+
if entry is None:
41+
raise ValueError("entry cannot be None")
42+
if not isinstance(entry, dict):
43+
raise TypeError("entry must be a dict")
44+
uid = entry.get("uid")
45+
if uid is None or (isinstance(uid, str) and uid.strip() == ""):
46+
raise ValueError("entry must contain a non-empty uid")
47+
entry_uid = str(uid)
48+
ct = entry.get("_content_type_uid")
49+
if ct is None or ct == "":
50+
ct = content_type_uid or ""
51+
contenttype_uid = "" if ct is None else str(ct)
52+
variants_map = Utils._variants_map_from_entry(entry)
53+
aliases = Utils._aliases_from_variants_map(variants_map)
54+
return {
55+
"entry_uid": entry_uid,
56+
"contenttype_uid": contenttype_uid,
57+
"variants": aliases,
58+
}
59+
60+
@staticmethod
61+
def get_variant_aliases(
62+
entry_or_entries: Union[dict, List[dict]],
63+
content_type_uid: str = "",
64+
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
65+
"""
66+
Extract variant aliases from a CDA entry (or list of entries).
67+
68+
The entry must have been fetched with ``x-cs-variant-uid`` set to variant
69+
aliases (not UIDs) for ``publish_details.variants`` to be present.
70+
71+
:param entry_or_entries: A single entry dict, or a list of entry dicts.
72+
:param content_type_uid: Used when ``entry._content_type_uid`` is absent;
73+
ignored when ``entry_or_entries`` is a list (each entry supplies its own).
74+
:raises ValueError: if ``entry_or_entries`` is None, an entry is None, or an
75+
entry has no non-empty ``uid``.
76+
:raises TypeError: if a single entry is not a dict, or a list is expected but
77+
another type was passed for the multi-entry overload.
78+
"""
79+
if entry_or_entries is None:
80+
raise ValueError("entry is required and cannot be None")
81+
if isinstance(entry_or_entries, list):
82+
return [Utils._variant_aliases_for_entry(e, "") for e in entry_or_entries]
83+
if isinstance(entry_or_entries, dict):
84+
return Utils._variant_aliases_for_entry(entry_or_entries, content_type_uid or "")
85+
raise TypeError("entry must be a dict or a list of dicts")
86+
87+
@staticmethod
88+
def get_variant_metadata_tags(entries: List[dict]) -> Dict[str, str]:
89+
"""
90+
Build a ``data-csvariants`` HTML data-attribute payload from entry objects.
91+
92+
:param entries: List of CDA entry dicts (same shape as for multi-entry
93+
:meth:`get_variant_aliases`).
94+
:raises ValueError: if ``entries`` is None.
95+
:raises TypeError: if ``entries`` is not a list.
96+
"""
97+
if entries is None:
98+
raise ValueError("entries is required and cannot be None")
99+
if not isinstance(entries, list):
100+
raise TypeError("entries must be a list")
101+
results = Utils.get_variant_aliases(entries)
102+
payload = json.dumps(results, separators=(",", ":"))
103+
return {"data-csvariants": payload}
104+
13105
@staticmethod
14106
def render(entry_obj, key_path: list, option: Options):
15107
valid = Automate.is_json(entry_obj)

tests/test_variant_aliases.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import json
2+
import unittest
3+
4+
from contentstack_utils.utils import Utils
5+
6+
7+
class TestVariantAliases(unittest.TestCase):
8+
9+
def _sample_entry(self):
10+
return {
11+
"uid": "blt3e91e3812a44ba90",
12+
"_content_type_uid": "landing_page",
13+
"publish_details": {
14+
"variants": {
15+
"cs669f1759b774fe1d": {
16+
"alias": "cs_personalize_0_2",
17+
"environment": "bltb5963e2163c24eb6",
18+
"locale": "en",
19+
},
20+
"csbf165536748bdee2": {
21+
"alias": "cs_personalize_0_1",
22+
"environment": "bltb5963e2163c24eb6",
23+
"locale": "en",
24+
},
25+
}
26+
},
27+
}
28+
29+
def test_single_entry_extracts_aliases(self):
30+
result = Utils.get_variant_aliases(self._sample_entry())
31+
self.assertEqual(result["entry_uid"], "blt3e91e3812a44ba90")
32+
self.assertEqual(result["contenttype_uid"], "landing_page")
33+
self.assertEqual(
34+
result["variants"],
35+
["cs_personalize_0_2", "cs_personalize_0_1"],
36+
)
37+
38+
def test_content_type_from_parameter_when_missing_on_entry(self):
39+
entry = {
40+
"uid": "blt1",
41+
"publish_details": {"variants": {}},
42+
}
43+
result = Utils.get_variant_aliases(entry, "landing_page")
44+
self.assertEqual(result["contenttype_uid"], "landing_page")
45+
46+
def test_empty_contenttype_when_missing(self):
47+
entry = {"uid": "blt1", "publish_details": {"variants": {}}}
48+
result = Utils.get_variant_aliases(entry)
49+
self.assertEqual(result["contenttype_uid"], "")
50+
51+
def test_missing_publish_details(self):
52+
entry = {"uid": "blt1", "_content_type_uid": "page"}
53+
result = Utils.get_variant_aliases(entry)
54+
self.assertEqual(result["variants"], [])
55+
56+
def test_missing_variants_key(self):
57+
entry = {"uid": "blt1", "publish_details": {}}
58+
result = Utils.get_variant_aliases(entry)
59+
self.assertEqual(result["variants"], [])
60+
61+
def test_empty_variants_object(self):
62+
entry = {"uid": "blt1", "publish_details": {"variants": {}}}
63+
result = Utils.get_variant_aliases(entry)
64+
self.assertEqual(result["variants"], [])
65+
66+
def test_skips_variant_without_alias(self):
67+
entry = {
68+
"uid": "blt1",
69+
"publish_details": {
70+
"variants": {
71+
"a": {"alias": "ok"},
72+
"b": {},
73+
"c": {"alias": ""},
74+
"d": {"alias": " "},
75+
}
76+
},
77+
}
78+
result = Utils.get_variant_aliases(entry)
79+
self.assertEqual(result["variants"], ["ok"])
80+
81+
def test_non_dict_variant_value_skipped(self):
82+
entry = {
83+
"uid": "blt1",
84+
"publish_details": {"variants": {"x": "not-a-dict"}},
85+
}
86+
result = Utils.get_variant_aliases(entry)
87+
self.assertEqual(result["variants"], [])
88+
89+
def test_none_entry_raises(self):
90+
with self.assertRaises(ValueError):
91+
Utils.get_variant_aliases(None)
92+
93+
def test_missing_uid_raises(self):
94+
with self.assertRaises(ValueError):
95+
Utils.get_variant_aliases({"publish_details": {}})
96+
97+
def test_empty_uid_raises(self):
98+
with self.assertRaises(ValueError):
99+
Utils.get_variant_aliases({"uid": ""})
100+
101+
def test_whitespace_uid_raises(self):
102+
with self.assertRaises(ValueError):
103+
Utils.get_variant_aliases({"uid": " "})
104+
105+
def test_non_dict_entry_raises(self):
106+
with self.assertRaises(TypeError):
107+
Utils.get_variant_aliases("not-a-dict")
108+
109+
def test_multiple_entries(self):
110+
results = Utils.get_variant_aliases(
111+
[
112+
{
113+
"uid": "blt123",
114+
"_content_type_uid": "page",
115+
"publish_details": {
116+
"variants": {
117+
"v1": {"alias": "cs_personalize_3_1"},
118+
"v2": {"alias": "cs_personalize_4_0"},
119+
}
120+
},
121+
},
122+
{"uid": "blt456", "_content_type_uid": "page"},
123+
]
124+
)
125+
self.assertEqual(len(results), 2)
126+
self.assertEqual(results[0]["entry_uid"], "blt123")
127+
self.assertEqual(
128+
results[0]["variants"],
129+
["cs_personalize_3_1", "cs_personalize_4_0"],
130+
)
131+
self.assertEqual(results[1]["variants"], [])
132+
133+
def test_list_entry_none_raises(self):
134+
with self.assertRaises(ValueError):
135+
Utils.get_variant_aliases([None])
136+
137+
def test_get_variant_metadata_tags(self):
138+
entries = [
139+
{
140+
"uid": "blt123",
141+
"_content_type_uid": "page",
142+
"publish_details": {
143+
"variants": {"v1": {"alias": "cs_personalize_3_1"}}
144+
},
145+
}
146+
]
147+
tag = Utils.get_variant_metadata_tags(entries)
148+
self.assertIn("data-csvariants", tag)
149+
parsed = json.loads(tag["data-csvariants"])
150+
self.assertEqual(len(parsed), 1)
151+
self.assertEqual(parsed[0]["entry_uid"], "blt123")
152+
self.assertEqual(parsed[0]["variants"], ["cs_personalize_3_1"])
153+
154+
def test_get_variant_metadata_tags_none_raises(self):
155+
with self.assertRaises(ValueError):
156+
Utils.get_variant_metadata_tags(None)
157+
158+
def test_get_variant_metadata_tags_not_list_raises(self):
159+
with self.assertRaises(TypeError):
160+
Utils.get_variant_metadata_tags({})
161+
162+
163+
if __name__ == "__main__":
164+
unittest.main()

0 commit comments

Comments
 (0)