Skip to content
Open
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
8 changes: 8 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
**CHANGELOG**
================

*v1.4.0*
============

NEW FEATURE: Variants utility (CDA entry variant aliases).

- 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.
- Added ``Utils.get_variant_metadata_tags`` to build a ``data-csvariants`` HTML data-attribute value (JSON string of the multi-entry alias results).

*v1.3.3*
============

Expand Down
2 changes: 1 addition & 1 deletion contentstack_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
__title__ = 'contentstack_utils'
__author__ = 'contentstack'
__status__ = 'debug'
__version__ = '1.3.2'
__version__ = '1.4.0'
__endpoint__ = 'cdn.contentstack.io'
__contact__ = 'support@contentstack.com'
92 changes: 92 additions & 0 deletions contentstack_utils/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# pylint: disable=missing-function-docstring

import json
from typing import Any, Dict, List, Union

from lxml import etree

from contentstack_utils.automate import Automate
Expand All @@ -10,6 +13,95 @@

class Utils(Automate):

@staticmethod
def _variants_map_from_entry(entry: dict) -> dict:
publish_details = entry.get("publish_details")
if not isinstance(publish_details, dict):
return {}
raw = publish_details.get("variants")
return raw if isinstance(raw, dict) else {}

@staticmethod
def _aliases_from_variants_map(variants_map: dict) -> List[str]:
aliases: List[str] = []
for _variant_uid, value in variants_map.items():
if not isinstance(value, dict):
continue
alias = value.get("alias")
if alias is None:
continue
alias_str = str(alias).strip()
if alias_str:
aliases.append(alias_str)
return aliases

@staticmethod
def _variant_aliases_for_entry(entry: dict, content_type_uid: str = "") -> Dict[str, Any]:
if entry is None:
raise ValueError("entry cannot be None")
if not isinstance(entry, dict):
raise TypeError("entry must be a dict")
uid = entry.get("uid")
if uid is None or (isinstance(uid, str) and uid.strip() == ""):
raise ValueError("entry must contain a non-empty uid")
entry_uid = str(uid)
ct = entry.get("_content_type_uid")
if ct is None or ct == "":
ct = content_type_uid or ""
contenttype_uid = "" if ct is None else str(ct)
variants_map = Utils._variants_map_from_entry(entry)
aliases = Utils._aliases_from_variants_map(variants_map)
return {
"entry_uid": entry_uid,
"contenttype_uid": contenttype_uid,
"variants": aliases,
}

@staticmethod
def get_variant_aliases(
entry_or_entries: Union[dict, List[dict]],
content_type_uid: str = "",
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
"""
Extract variant aliases from a CDA entry (or list of entries).

The entry must have been fetched with ``x-cs-variant-uid`` set to variant
aliases (not UIDs) for ``publish_details.variants`` to be present.

:param entry_or_entries: A single entry dict, or a list of entry dicts.
:param content_type_uid: Used when ``entry._content_type_uid`` is absent;
ignored when ``entry_or_entries`` is a list (each entry supplies its own).
:raises ValueError: if ``entry_or_entries`` is None, an entry is None, or an
entry has no non-empty ``uid``.
:raises TypeError: if a single entry is not a dict, or a list is expected but
another type was passed for the multi-entry overload.
"""
if entry_or_entries is None:
raise ValueError("entry is required and cannot be None")
if isinstance(entry_or_entries, list):
return [Utils._variant_aliases_for_entry(e, "") for e in entry_or_entries]
if isinstance(entry_or_entries, dict):
return Utils._variant_aliases_for_entry(entry_or_entries, content_type_uid or "")
raise TypeError("entry must be a dict or a list of dicts")

@staticmethod
def get_variant_metadata_tags(entries: List[dict]) -> Dict[str, str]:
"""
Build a ``data-csvariants`` HTML data-attribute payload from entry objects.

:param entries: List of CDA entry dicts (same shape as for multi-entry
:meth:`get_variant_aliases`).
:raises ValueError: if ``entries`` is None.
:raises TypeError: if ``entries`` is not a list.
"""
if entries is None:
raise ValueError("entries is required and cannot be None")
if not isinstance(entries, list):
raise TypeError("entries must be a list")
results = Utils.get_variant_aliases(entries)
payload = json.dumps(results, separators=(",", ":"))
return {"data-csvariants": payload}

@staticmethod
def render(entry_obj, key_path: list, option: Options):
valid = Automate.is_json(entry_obj)
Expand Down
164 changes: 164 additions & 0 deletions tests/test_variant_aliases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import json
import unittest

from contentstack_utils.utils import Utils


class TestVariantAliases(unittest.TestCase):

def _sample_entry(self):
return {
"uid": "blt3e91e3812a44ba90",
"_content_type_uid": "landing_page",
"publish_details": {
"variants": {
"cs669f1759b774fe1d": {
"alias": "cs_personalize_0_2",
"environment": "bltb5963e2163c24eb6",
"locale": "en",
},
"csbf165536748bdee2": {
"alias": "cs_personalize_0_1",
"environment": "bltb5963e2163c24eb6",
"locale": "en",
},
}
},
}

def test_single_entry_extracts_aliases(self):
result = Utils.get_variant_aliases(self._sample_entry())
self.assertEqual(result["entry_uid"], "blt3e91e3812a44ba90")
self.assertEqual(result["contenttype_uid"], "landing_page")
self.assertEqual(
result["variants"],
["cs_personalize_0_2", "cs_personalize_0_1"],
)

def test_content_type_from_parameter_when_missing_on_entry(self):
entry = {
"uid": "blt1",
"publish_details": {"variants": {}},
}
result = Utils.get_variant_aliases(entry, "landing_page")
self.assertEqual(result["contenttype_uid"], "landing_page")

def test_empty_contenttype_when_missing(self):
entry = {"uid": "blt1", "publish_details": {"variants": {}}}
result = Utils.get_variant_aliases(entry)
self.assertEqual(result["contenttype_uid"], "")

def test_missing_publish_details(self):
entry = {"uid": "blt1", "_content_type_uid": "page"}
result = Utils.get_variant_aliases(entry)
self.assertEqual(result["variants"], [])

def test_missing_variants_key(self):
entry = {"uid": "blt1", "publish_details": {}}
result = Utils.get_variant_aliases(entry)
self.assertEqual(result["variants"], [])

def test_empty_variants_object(self):
entry = {"uid": "blt1", "publish_details": {"variants": {}}}
result = Utils.get_variant_aliases(entry)
self.assertEqual(result["variants"], [])

def test_skips_variant_without_alias(self):
entry = {
"uid": "blt1",
"publish_details": {
"variants": {
"a": {"alias": "ok"},
"b": {},
"c": {"alias": ""},
"d": {"alias": " "},
}
},
}
result = Utils.get_variant_aliases(entry)
self.assertEqual(result["variants"], ["ok"])

def test_non_dict_variant_value_skipped(self):
entry = {
"uid": "blt1",
"publish_details": {"variants": {"x": "not-a-dict"}},
}
result = Utils.get_variant_aliases(entry)
self.assertEqual(result["variants"], [])

def test_none_entry_raises(self):
with self.assertRaises(ValueError):
Utils.get_variant_aliases(None)

def test_missing_uid_raises(self):
with self.assertRaises(ValueError):
Utils.get_variant_aliases({"publish_details": {}})

def test_empty_uid_raises(self):
with self.assertRaises(ValueError):
Utils.get_variant_aliases({"uid": ""})

def test_whitespace_uid_raises(self):
with self.assertRaises(ValueError):
Utils.get_variant_aliases({"uid": " "})

def test_non_dict_entry_raises(self):
with self.assertRaises(TypeError):
Utils.get_variant_aliases("not-a-dict")

def test_multiple_entries(self):
results = Utils.get_variant_aliases(
[
{
"uid": "blt123",
"_content_type_uid": "page",
"publish_details": {
"variants": {
"v1": {"alias": "cs_personalize_3_1"},
"v2": {"alias": "cs_personalize_4_0"},
}
},
},
{"uid": "blt456", "_content_type_uid": "page"},
]
)
self.assertEqual(len(results), 2)
self.assertEqual(results[0]["entry_uid"], "blt123")
self.assertEqual(
results[0]["variants"],
["cs_personalize_3_1", "cs_personalize_4_0"],
)
self.assertEqual(results[1]["variants"], [])

def test_list_entry_none_raises(self):
with self.assertRaises(ValueError):
Utils.get_variant_aliases([None])

def test_get_variant_metadata_tags(self):
entries = [
{
"uid": "blt123",
"_content_type_uid": "page",
"publish_details": {
"variants": {"v1": {"alias": "cs_personalize_3_1"}}
},
}
]
tag = Utils.get_variant_metadata_tags(entries)
self.assertIn("data-csvariants", tag)
parsed = json.loads(tag["data-csvariants"])
self.assertEqual(len(parsed), 1)
self.assertEqual(parsed[0]["entry_uid"], "blt123")
self.assertEqual(parsed[0]["variants"], ["cs_personalize_3_1"])

def test_get_variant_metadata_tags_none_raises(self):
with self.assertRaises(ValueError):
Utils.get_variant_metadata_tags(None)

def test_get_variant_metadata_tags_not_list_raises(self):
with self.assertRaises(TypeError):
Utils.get_variant_metadata_tags({})


if __name__ == "__main__":
unittest.main()
Loading