contentstack-utils 1.4.0__tar.gz → 1.5.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 (38) hide show
  1. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/PKG-INFO +1 -1
  2. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/__init__.py +5 -1
  3. contentstack_utils-1.5.0/contentstack_utils/entry_editable.py +248 -0
  4. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/utils.py +53 -1
  5. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/PKG-INFO +1 -1
  6. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/SOURCES.txt +2 -0
  7. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/setup.py +1 -1
  8. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/convert_style.py +1 -1
  9. contentstack_utils-1.5.0/tests/test_editable_tags.py +56 -0
  10. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/LICENSE +0 -0
  11. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/README.md +0 -0
  12. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/automate.py +0 -0
  13. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/embedded/__init__.py +0 -0
  14. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/embedded/item_type.py +0 -0
  15. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/embedded/styletype.py +0 -0
  16. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/gql.py +0 -0
  17. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/helper/__init__.py +0 -0
  18. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/helper/converter.py +0 -0
  19. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/helper/metadata.py +0 -0
  20. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/helper/node_to_html.py +0 -0
  21. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/render/__init__.py +0 -0
  22. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils/render/options.py +0 -0
  23. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/dependency_links.txt +0 -0
  24. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/top_level.txt +0 -0
  25. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/setup.cfg +0 -0
  26. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/__init__.py +0 -0
  27. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_default_opt_others.py +0 -0
  28. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_gql_to_html_func.py +0 -0
  29. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_helper_node_to_html.py +0 -0
  30. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_item_types.py +0 -0
  31. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_metadata.py +0 -0
  32. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_option_render_mark.py +0 -0
  33. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_render_default_options.py +0 -0
  34. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_render_options.py +0 -0
  35. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_style_type.py +0 -0
  36. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_util_srte.py +0 -0
  37. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_utils.py +0 -0
  38. {contentstack_utils-1.4.0 → contentstack_utils-1.5.0}/tests/test_variant_aliases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack_utils
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -16,6 +16,7 @@ from contentstack_utils.render.options import Options
16
16
  from contentstack_utils.utils import Utils
17
17
  from contentstack_utils.gql import GQL
18
18
  from contentstack_utils.automate import Automate
19
+ from contentstack_utils.entry_editable import addEditableTags, addTags, getTag
19
20
 
20
21
  __all__ = (
21
22
  "Utils",
@@ -25,7 +26,10 @@ __all__ = (
25
26
  "Automate",
26
27
  "StyleType",
27
28
  "ItemType",
28
- "NodeToHtml"
29
+ "NodeToHtml",
30
+ "addEditableTags",
31
+ "addTags",
32
+ "getTag",
29
33
  )
30
34
 
31
35
  __title__ = 'contentstack_utils'
@@ -0,0 +1,248 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional, Union, cast
4
+
5
+
6
+ AppliedVariants = Optional[Dict[str, Any]]
7
+ TagValue = Union[str, Dict[str, str]]
8
+
9
+
10
+ def _get_parent_variantised_path(applied_variants: Dict[str, Any], meta_key: str) -> str:
11
+ """
12
+ Port of JS getParentVariantisedPath().
13
+ Finds the longest variantised field path that is a prefix of meta_key.
14
+ """
15
+ try:
16
+ if not meta_key:
17
+ return ""
18
+ variantised_field_paths = sorted(applied_variants.keys(), key=len, reverse=True)
19
+ child_fragments = meta_key.split(".")
20
+ if not child_fragments or not variantised_field_paths:
21
+ return ""
22
+ for path in variantised_field_paths:
23
+ parent_fragments = str(path).split(".")
24
+ if len(parent_fragments) > len(child_fragments):
25
+ continue
26
+ if all(child_fragments[i] == parent_fragments[i] for i in range(len(parent_fragments))):
27
+ return str(path)
28
+ return ""
29
+ except Exception:
30
+ return ""
31
+
32
+
33
+ def _apply_variant_to_data_value(data_value: str, applied_variants: AppliedVariants, meta_key: str, should_apply_variant: bool) -> str:
34
+ """
35
+ Port of JS applyVariantToDataValue().
36
+
37
+ If the current field (or its parent field path) is variantised, prefixes with
38
+ 'v2:' and appends `_{variant}` to the entry uid segment of the dot-path.
39
+ """
40
+ if not should_apply_variant or not applied_variants or not meta_key or not isinstance(applied_variants, dict):
41
+ return data_value
42
+
43
+ variant: Optional[str] = None
44
+ if meta_key in applied_variants:
45
+ variant = str(applied_variants[meta_key])
46
+ else:
47
+ parent_path = _get_parent_variantised_path(applied_variants, meta_key)
48
+ if parent_path:
49
+ variant = str(applied_variants.get(parent_path))
50
+
51
+ if not variant:
52
+ return data_value
53
+
54
+ parts = ("v2:" + data_value).split(".")
55
+ if len(parts) >= 2:
56
+ parts[1] = parts[1] + "_" + variant
57
+ return ".".join(parts)
58
+
59
+
60
+ def _tags_value(data_value: str, tags_as_object: bool, applied_variants: AppliedVariants, meta_key: str, should_apply_variant: bool) -> TagValue:
61
+ resolved = _apply_variant_to_data_value(data_value, applied_variants, meta_key, should_apply_variant)
62
+ if tags_as_object:
63
+ return {"data-cslp": resolved}
64
+ return f"data-cslp={resolved}"
65
+
66
+
67
+ def _parent_tags_value(data_value: str, tags_as_object: bool) -> TagValue:
68
+ if tags_as_object:
69
+ return {"data-cslp-parent-field": data_value}
70
+ return f"data-cslp-parent-field={data_value}"
71
+
72
+
73
+ def getTag( # pylint: disable=invalid-name
74
+ content: Any,
75
+ prefix: str,
76
+ tags_as_object: bool,
77
+ locale: str,
78
+ applied_variants: AppliedVariants,
79
+ should_apply_variant: bool,
80
+ meta_key: str = "",
81
+ ) -> Dict[str, Any]:
82
+ """
83
+ Port of JS getTag() from `src/entry-editable.ts`.
84
+
85
+ Returns a dict mapping field keys to CSLP tag values, and mutates nested objects/refs
86
+ by attaching their own `$` tag maps.
87
+ """
88
+ if content is None or not isinstance(content, dict):
89
+ return {}
90
+
91
+ tags: Dict[str, Any] = {}
92
+ for key, value in content.items():
93
+ if key == "$":
94
+ continue
95
+
96
+ meta_uid = ""
97
+ if isinstance(value, dict):
98
+ meta = value.get("_metadata")
99
+ if isinstance(meta, dict) and meta.get("uid"):
100
+ meta_uid = str(meta.get("uid"))
101
+
102
+ meta_key_prefix = (meta_key + ".") if meta_key else ""
103
+ updated_meta_key = f"{meta_key_prefix}{key}" if should_apply_variant else ""
104
+ if meta_uid and updated_meta_key:
105
+ updated_meta_key = updated_meta_key + "." + meta_uid
106
+
107
+ if isinstance(value, list):
108
+ for index, obj in enumerate(value):
109
+ if obj is None:
110
+ continue
111
+
112
+ child_key = f"{key}__{index}"
113
+ parent_key = f"{key}__parent"
114
+
115
+ obj_meta_uid = ""
116
+ if isinstance(obj, dict):
117
+ meta = obj.get("_metadata")
118
+ if isinstance(meta, dict) and meta.get("uid"):
119
+ obj_meta_uid = str(meta.get("uid"))
120
+
121
+ array_meta_key = f"{meta_key_prefix}{key}" if should_apply_variant else ""
122
+ if obj_meta_uid and array_meta_key:
123
+ array_meta_key = array_meta_key + "." + obj_meta_uid
124
+
125
+ tags[child_key] = _tags_value(
126
+ f"{prefix}.{key}.{index}",
127
+ tags_as_object,
128
+ applied_variants,
129
+ array_meta_key,
130
+ should_apply_variant,
131
+ )
132
+ tags[parent_key] = _parent_tags_value(f"{prefix}.{key}", tags_as_object)
133
+
134
+ # Reference entries in array
135
+ if isinstance(obj, dict) and obj.get("_content_type_uid") is not None and obj.get("uid") is not None:
136
+ new_applied_variants = obj.get("_applied_variants")
137
+ if new_applied_variants is None and isinstance(obj.get("system"), dict):
138
+ new_applied_variants = cast(dict, obj["system"]).get("applied_variants")
139
+ new_should_apply_variant = bool(new_applied_variants)
140
+
141
+ obj_locale = obj.get("locale") or locale
142
+ obj["$"] = getTag(
143
+ obj,
144
+ f"{obj.get('_content_type_uid')}.{obj.get('uid')}.{obj_locale}",
145
+ tags_as_object,
146
+ locale,
147
+ cast(AppliedVariants, new_applied_variants),
148
+ new_should_apply_variant,
149
+ meta_key="",
150
+ )
151
+ continue
152
+
153
+ if isinstance(obj, dict):
154
+ obj["$"] = getTag(
155
+ obj,
156
+ f"{prefix}.{key}.{index}",
157
+ tags_as_object,
158
+ locale,
159
+ applied_variants,
160
+ should_apply_variant,
161
+ meta_key=array_meta_key,
162
+ )
163
+
164
+ tags[key] = _tags_value(
165
+ f"{prefix}.{key}",
166
+ tags_as_object,
167
+ applied_variants,
168
+ updated_meta_key,
169
+ should_apply_variant,
170
+ )
171
+ continue
172
+
173
+ if isinstance(value, dict):
174
+ value["$"] = getTag(
175
+ value,
176
+ f"{prefix}.{key}",
177
+ tags_as_object,
178
+ locale,
179
+ applied_variants,
180
+ should_apply_variant,
181
+ meta_key=updated_meta_key,
182
+ )
183
+ tags[key] = _tags_value(
184
+ f"{prefix}.{key}",
185
+ tags_as_object,
186
+ applied_variants,
187
+ updated_meta_key,
188
+ should_apply_variant,
189
+ )
190
+ continue
191
+
192
+ tags[key] = _tags_value(
193
+ f"{prefix}.{key}",
194
+ tags_as_object,
195
+ applied_variants,
196
+ updated_meta_key,
197
+ should_apply_variant,
198
+ )
199
+
200
+ return tags
201
+
202
+
203
+ def addTags( # pylint: disable=invalid-name
204
+ entry: Optional[dict],
205
+ contentTypeUid: str,
206
+ tagsAsObject: bool,
207
+ locale: str = "en-us",
208
+ options: Optional[dict] = None,
209
+ ) -> None:
210
+ """
211
+ Port of JS addTags() from `src/entry-editable.ts`.
212
+ Mutates `entry` by attaching a `$` dict of CSLP tags.
213
+ """
214
+ if not entry:
215
+ return
216
+
217
+ use_lower_case_locale = True
218
+ if isinstance(options, dict) and "useLowerCaseLocale" in options:
219
+ use_lower_case_locale = bool(options.get("useLowerCaseLocale"))
220
+
221
+ content_type_uid = (contentTypeUid or "").lower()
222
+ resolved_locale = (locale or "en-us")
223
+ if use_lower_case_locale:
224
+ resolved_locale = resolved_locale.lower()
225
+
226
+ applied_variants = entry.get("_applied_variants")
227
+ if applied_variants is None and isinstance(entry.get("system"), dict):
228
+ applied_variants = cast(dict, entry["system"]).get("applied_variants")
229
+ should_apply_variant = bool(applied_variants)
230
+
231
+ entry["$"] = getTag(
232
+ entry,
233
+ f"{content_type_uid}.{entry.get('uid')}.{resolved_locale}",
234
+ tagsAsObject,
235
+ resolved_locale,
236
+ cast(AppliedVariants, applied_variants),
237
+ should_apply_variant,
238
+ meta_key="",
239
+ )
240
+
241
+
242
+ # JS parity export name
243
+ addEditableTags = addTags # pylint: disable=invalid-name
244
+
245
+ # Pythonic aliases
246
+ add_tags = addTags
247
+ get_tags = getTag
248
+
@@ -1,17 +1,69 @@
1
1
  # pylint: disable=missing-function-docstring
2
2
 
3
3
  import json
4
- from typing import Any, Dict, List, Union
4
+ from typing import Any, Dict, List, Optional, Union
5
5
 
6
6
  from lxml import etree
7
7
 
8
8
  from contentstack_utils.automate import Automate
9
+ from contentstack_utils.entry_editable import addEditableTags as _addEditableTags
10
+ from contentstack_utils.entry_editable import addTags as _addTags
11
+ from contentstack_utils.entry_editable import getTag as _getTag
9
12
  from contentstack_utils.helper.converter import convert_style
10
13
  from contentstack_utils.helper.metadata import Metadata
11
14
  from contentstack_utils.render.options import Options
12
15
 
13
16
 
14
17
  class Utils(Automate):
18
+ # JS parity helpers (moved to `contentstack_utils/entry_editable.py`)
19
+ @staticmethod
20
+ def addTags( # pylint: disable=invalid-name
21
+ entry: dict,
22
+ contentTypeUid: str,
23
+ tagsAsObject: Optional[bool] = None,
24
+ locale: str = "en-us",
25
+ options: Optional[dict] = None,
26
+ **kwargs,
27
+ ) -> None:
28
+ # Support pythonic kwarg name too (backward compatibility with earlier port).
29
+ if tagsAsObject is None and "tags_as_object" in kwargs:
30
+ tagsAsObject = bool(kwargs["tags_as_object"])
31
+ if tagsAsObject is None:
32
+ tagsAsObject = False
33
+ return _addTags(entry, contentTypeUid, tagsAsObject, locale, options)
34
+
35
+ @staticmethod
36
+ def addEditableTags( # pylint: disable=invalid-name
37
+ entry: dict,
38
+ contentTypeUid: str,
39
+ tagsAsObject: Optional[bool] = None,
40
+ locale: str = "en-us",
41
+ options: Optional[dict] = None,
42
+ **kwargs,
43
+ ) -> None:
44
+ if tagsAsObject is None and "tags_as_object" in kwargs:
45
+ tagsAsObject = bool(kwargs["tags_as_object"])
46
+ if tagsAsObject is None:
47
+ tagsAsObject = False
48
+ return _addEditableTags(entry, contentTypeUid, tagsAsObject, locale, options)
49
+
50
+ @staticmethod
51
+ def getTag( # pylint: disable=invalid-name
52
+ content: Any,
53
+ prefix: str,
54
+ tagsAsObject: bool,
55
+ locale: str,
56
+ appliedVariants: Optional[dict],
57
+ shouldApplyVariant: bool,
58
+ metaKey: str = "",
59
+ ) -> Dict[str, Any]:
60
+ # Keep JS argument names for parity.
61
+ return _getTag(content, prefix, tagsAsObject, locale, appliedVariants, shouldApplyVariant, metaKey)
62
+
63
+ # Pythonic aliases
64
+ add_tags = addTags
65
+ get_tags = getTag
66
+ get_tag = getTag
15
67
 
16
68
  @staticmethod
17
69
  def _variants_map_from_entry(entry: dict) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack_utils
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -3,6 +3,7 @@ README.md
3
3
  setup.py
4
4
  contentstack_utils/__init__.py
5
5
  contentstack_utils/automate.py
6
+ contentstack_utils/entry_editable.py
6
7
  contentstack_utils/gql.py
7
8
  contentstack_utils/utils.py
8
9
  contentstack_utils.egg-info/PKG-INFO
@@ -21,6 +22,7 @@ contentstack_utils/render/options.py
21
22
  tests/__init__.py
22
23
  tests/convert_style.py
23
24
  tests/test_default_opt_others.py
25
+ tests/test_editable_tags.py
24
26
  tests/test_gql_to_html_func.py
25
27
  tests/test_helper_node_to_html.py
26
28
  tests/test_item_types.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.4.0',
18
+ version='1.5.0',
19
19
  install_requires=[
20
20
 
21
21
  ],
@@ -10,7 +10,7 @@ class TestConvertStyle(unittest.TestCase):
10
10
 
11
11
  def test_converter_style_block(self):
12
12
  _returns = converter.convert_style('block')
13
- self.assertEquals(StyleType.BLOCK, _returns)
13
+ self.assertEqual(StyleType.BLOCK, _returns)
14
14
 
15
15
  def test_converter_style_inline(self):
16
16
  _returns = converter.convert_style('inline')
@@ -0,0 +1,56 @@
1
+ import unittest
2
+
3
+ from contentstack_utils.utils import Utils
4
+
5
+
6
+ class TestEditableTags(unittest.TestCase):
7
+ def test_add_tags_mutates_entry_with_dollar_map(self):
8
+ entry = {"uid": "e1", "title": "Hello", "count": 1}
9
+ Utils.addTags(entry, "Blog_Post", tags_as_object=True, locale="EN-us")
10
+ self.assertIn("$", entry)
11
+ self.assertEqual(entry["$"]["title"], {"data-cslp": "blog_post.e1.en-us.title"})
12
+ self.assertEqual(entry["$"]["count"], {"data-cslp": "blog_post.e1.en-us.count"})
13
+
14
+ def test_add_tags_string_mode(self):
15
+ entry = {"uid": "e1", "title": "Hello"}
16
+ Utils.addTags(entry, "blog_post", tags_as_object=False, locale="en-us")
17
+ self.assertEqual(entry["$"]["title"], "data-cslp=blog_post.e1.en-us.title")
18
+
19
+ def test_array_tags_add_index_and_parent_keys(self):
20
+ entry = {"uid": "e1", "array": ["hello", "world"]}
21
+ Utils.addTags(entry, "blog", tags_as_object=True, locale="en-us")
22
+ self.assertEqual(entry["$"]["array"], {"data-cslp": "blog.e1.en-us.array"})
23
+ self.assertEqual(entry["$"]["array__0"], {"data-cslp": "blog.e1.en-us.array.0"})
24
+ self.assertEqual(entry["$"]["array__1"], {"data-cslp": "blog.e1.en-us.array.1"})
25
+ self.assertEqual(entry["$"]["array__parent"], {"data-cslp-parent-field": "blog.e1.en-us.array"})
26
+
27
+ def test_reference_entry_inside_array_gets_own_dollar(self):
28
+ entry = {
29
+ "uid": "e1",
30
+ "refs": [
31
+ {
32
+ "uid": "r1",
33
+ "_content_type_uid": "ref_ct",
34
+ "title": "Ref Title",
35
+ }
36
+ ],
37
+ }
38
+ Utils.addTags(entry, "blog", tags_as_object=True, locale="en-us")
39
+ ref = entry["refs"][0]
40
+ self.assertIn("$", ref)
41
+ self.assertEqual(ref["$"]["title"], {"data-cslp": "ref_ct.r1.en-us.title"})
42
+
43
+ def test_variantised_field_applies_v2_prefix_and_uid_suffix(self):
44
+ entry = {
45
+ "uid": "e1",
46
+ "_applied_variants": {"title": "v123"},
47
+ "title": {"value": "Hello"},
48
+ }
49
+ Utils.addTags(entry, "blog", tags_as_object=True, locale="en-us")
50
+ # title is an object; ensure we tag the object itself (like JS) and apply variant.
51
+ self.assertEqual(entry["$"]["title"], {"data-cslp": "v2:blog.e1_v123.en-us.title"})
52
+
53
+
54
+ if __name__ == "__main__":
55
+ unittest.main()
56
+