contentstack-utils 1.3.2__tar.gz → 1.4.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 (36) hide show
  1. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/LICENSE +1 -1
  2. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/PKG-INFO +1 -1
  3. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/__init__.py +1 -1
  4. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/utils.py +92 -0
  5. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils.egg-info/PKG-INFO +1 -1
  6. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils.egg-info/SOURCES.txt +2 -1
  7. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/setup.py +1 -1
  8. contentstack_utils-1.4.0/tests/test_variant_aliases.py +164 -0
  9. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/README.md +0 -0
  10. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/automate.py +0 -0
  11. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/embedded/__init__.py +0 -0
  12. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/embedded/item_type.py +0 -0
  13. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/embedded/styletype.py +0 -0
  14. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/gql.py +0 -0
  15. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/helper/__init__.py +0 -0
  16. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/helper/converter.py +0 -0
  17. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/helper/metadata.py +0 -0
  18. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/helper/node_to_html.py +0 -0
  19. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/render/__init__.py +0 -0
  20. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils/render/options.py +0 -0
  21. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils.egg-info/dependency_links.txt +0 -0
  22. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/contentstack_utils.egg-info/top_level.txt +0 -0
  23. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/setup.cfg +0 -0
  24. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/__init__.py +0 -0
  25. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/convert_style.py +0 -0
  26. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_default_opt_others.py +0 -0
  27. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_gql_to_html_func.py +0 -0
  28. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_helper_node_to_html.py +0 -0
  29. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_item_types.py +0 -0
  30. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_metadata.py +0 -0
  31. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_option_render_mark.py +0 -0
  32. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_render_default_options.py +0 -0
  33. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_render_options.py +0 -0
  34. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_style_type.py +0 -0
  35. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_util_srte.py +0 -0
  36. {contentstack_utils-1.3.2 → contentstack_utils-1.4.0}/tests/test_utils.py +0 -0
@@ -1,4 +1,4 @@
1
- Copyright 2021-2025 Contentstack
1
+ Copyright 2021-2026 Contentstack
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack_utils
3
- Version: 1.3.2
3
+ Version: 1.4.0
4
4
  Summary: contentstack_utils is a Utility package for Contentstack headless CMS with an API-first approach.
5
5
  Home-page: https://github.com/contentstack/contentstack-utils-python
6
6
  Author: contentstack
@@ -31,6 +31,6 @@ __all__ = (
31
31
  __title__ = 'contentstack_utils'
32
32
  __author__ = 'contentstack'
33
33
  __status__ = 'debug'
34
- __version__ = '1.3.2'
34
+ __version__ = '1.4.0'
35
35
  __endpoint__ = 'cdn.contentstack.io'
36
36
  __contact__ = 'support@contentstack.com'
@@ -1,5 +1,8 @@
1
1
  # pylint: disable=missing-function-docstring
2
2
 
3
+ import json
4
+ from typing import Any, Dict, List, Union
5
+
3
6
  from lxml import etree
4
7
 
5
8
  from contentstack_utils.automate import Automate
@@ -10,6 +13,95 @@ from contentstack_utils.render.options import Options
10
13
 
11
14
  class Utils(Automate):
12
15
 
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
+
13
105
  @staticmethod
14
106
  def render(entry_obj, key_path: list, option: Options):
15
107
  valid = Automate.is_json(entry_obj)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack_utils
3
- Version: 1.3.2
3
+ Version: 1.4.0
4
4
  Summary: contentstack_utils is a Utility package for Contentstack headless CMS with an API-first approach.
5
5
  Home-page: https://github.com/contentstack/contentstack-utils-python
6
6
  Author: contentstack
@@ -30,4 +30,5 @@ tests/test_render_default_options.py
30
30
  tests/test_render_options.py
31
31
  tests/test_style_type.py
32
32
  tests/test_util_srte.py
33
- tests/test_utils.py
33
+ tests/test_utils.py
34
+ tests/test_variant_aliases.py
@@ -15,7 +15,7 @@ setup(
15
15
  long_description_content_type="text/markdown",
16
16
  url="https://github.com/contentstack/contentstack-utils-python",
17
17
  license='MIT',
18
- version='1.3.2',
18
+ version='1.4.0',
19
19
  install_requires=[
20
20
 
21
21
  ],
@@ -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()