contentstack-utils 1.3.3__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 (39) hide show
  1. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/PKG-INFO +1 -1
  2. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/__init__.py +6 -2
  3. contentstack_utils-1.5.0/contentstack_utils/entry_editable.py +248 -0
  4. contentstack_utils-1.5.0/contentstack_utils/utils.py +238 -0
  5. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/PKG-INFO +1 -1
  6. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/SOURCES.txt +4 -1
  7. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/setup.py +1 -1
  8. {contentstack_utils-1.3.3 → 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.5.0/tests/test_variant_aliases.py +164 -0
  11. contentstack_utils-1.3.3/contentstack_utils/utils.py +0 -94
  12. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/LICENSE +0 -0
  13. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/README.md +0 -0
  14. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/automate.py +0 -0
  15. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/embedded/__init__.py +0 -0
  16. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/embedded/item_type.py +0 -0
  17. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/embedded/styletype.py +0 -0
  18. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/gql.py +0 -0
  19. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/helper/__init__.py +0 -0
  20. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/helper/converter.py +0 -0
  21. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/helper/metadata.py +0 -0
  22. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/helper/node_to_html.py +0 -0
  23. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/render/__init__.py +0 -0
  24. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils/render/options.py +0 -0
  25. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/dependency_links.txt +0 -0
  26. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/contentstack_utils.egg-info/top_level.txt +0 -0
  27. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/setup.cfg +0 -0
  28. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/__init__.py +0 -0
  29. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_default_opt_others.py +0 -0
  30. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_gql_to_html_func.py +0 -0
  31. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_helper_node_to_html.py +0 -0
  32. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_item_types.py +0 -0
  33. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_metadata.py +0 -0
  34. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_option_render_mark.py +0 -0
  35. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_render_default_options.py +0 -0
  36. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_render_options.py +0 -0
  37. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_style_type.py +0 -0
  38. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_util_srte.py +0 -0
  39. {contentstack_utils-1.3.3 → contentstack_utils-1.5.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack_utils
3
- Version: 1.3.3
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,12 +26,15 @@ __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'
32
36
  __author__ = 'contentstack'
33
37
  __status__ = 'debug'
34
- __version__ = '1.3.2'
38
+ __version__ = '1.4.0'
35
39
  __endpoint__ = 'cdn.contentstack.io'
36
40
  __contact__ = 'support@contentstack.com'
@@ -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
+
@@ -0,0 +1,238 @@
1
+ # pylint: disable=missing-function-docstring
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ from lxml import etree
7
+
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
12
+ from contentstack_utils.helper.converter import convert_style
13
+ from contentstack_utils.helper.metadata import Metadata
14
+ from contentstack_utils.render.options import Options
15
+
16
+
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
67
+
68
+ @staticmethod
69
+ def _variants_map_from_entry(entry: dict) -> dict:
70
+ publish_details = entry.get("publish_details")
71
+ if not isinstance(publish_details, dict):
72
+ return {}
73
+ raw = publish_details.get("variants")
74
+ return raw if isinstance(raw, dict) else {}
75
+
76
+ @staticmethod
77
+ def _aliases_from_variants_map(variants_map: dict) -> List[str]:
78
+ aliases: List[str] = []
79
+ for _variant_uid, value in variants_map.items():
80
+ if not isinstance(value, dict):
81
+ continue
82
+ alias = value.get("alias")
83
+ if alias is None:
84
+ continue
85
+ alias_str = str(alias).strip()
86
+ if alias_str:
87
+ aliases.append(alias_str)
88
+ return aliases
89
+
90
+ @staticmethod
91
+ def _variant_aliases_for_entry(entry: dict, content_type_uid: str = "") -> Dict[str, Any]:
92
+ if entry is None:
93
+ raise ValueError("entry cannot be None")
94
+ if not isinstance(entry, dict):
95
+ raise TypeError("entry must be a dict")
96
+ uid = entry.get("uid")
97
+ if uid is None or (isinstance(uid, str) and uid.strip() == ""):
98
+ raise ValueError("entry must contain a non-empty uid")
99
+ entry_uid = str(uid)
100
+ ct = entry.get("_content_type_uid")
101
+ if ct is None or ct == "":
102
+ ct = content_type_uid or ""
103
+ contenttype_uid = "" if ct is None else str(ct)
104
+ variants_map = Utils._variants_map_from_entry(entry)
105
+ aliases = Utils._aliases_from_variants_map(variants_map)
106
+ return {
107
+ "entry_uid": entry_uid,
108
+ "contenttype_uid": contenttype_uid,
109
+ "variants": aliases,
110
+ }
111
+
112
+ @staticmethod
113
+ def get_variant_aliases(
114
+ entry_or_entries: Union[dict, List[dict]],
115
+ content_type_uid: str = "",
116
+ ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
117
+ """
118
+ Extract variant aliases from a CDA entry (or list of entries).
119
+
120
+ The entry must have been fetched with ``x-cs-variant-uid`` set to variant
121
+ aliases (not UIDs) for ``publish_details.variants`` to be present.
122
+
123
+ :param entry_or_entries: A single entry dict, or a list of entry dicts.
124
+ :param content_type_uid: Used when ``entry._content_type_uid`` is absent;
125
+ ignored when ``entry_or_entries`` is a list (each entry supplies its own).
126
+ :raises ValueError: if ``entry_or_entries`` is None, an entry is None, or an
127
+ entry has no non-empty ``uid``.
128
+ :raises TypeError: if a single entry is not a dict, or a list is expected but
129
+ another type was passed for the multi-entry overload.
130
+ """
131
+ if entry_or_entries is None:
132
+ raise ValueError("entry is required and cannot be None")
133
+ if isinstance(entry_or_entries, list):
134
+ return [Utils._variant_aliases_for_entry(e, "") for e in entry_or_entries]
135
+ if isinstance(entry_or_entries, dict):
136
+ return Utils._variant_aliases_for_entry(entry_or_entries, content_type_uid or "")
137
+ raise TypeError("entry must be a dict or a list of dicts")
138
+
139
+ @staticmethod
140
+ def get_variant_metadata_tags(entries: List[dict]) -> Dict[str, str]:
141
+ """
142
+ Build a ``data-csvariants`` HTML data-attribute payload from entry objects.
143
+
144
+ :param entries: List of CDA entry dicts (same shape as for multi-entry
145
+ :meth:`get_variant_aliases`).
146
+ :raises ValueError: if ``entries`` is None.
147
+ :raises TypeError: if ``entries`` is not a list.
148
+ """
149
+ if entries is None:
150
+ raise ValueError("entries is required and cannot be None")
151
+ if not isinstance(entries, list):
152
+ raise TypeError("entries must be a list")
153
+ results = Utils.get_variant_aliases(entries)
154
+ payload = json.dumps(results, separators=(",", ":"))
155
+ return {"data-csvariants": payload}
156
+
157
+ @staticmethod
158
+ def render(entry_obj, key_path: list, option: Options):
159
+ valid = Automate.is_json(entry_obj)
160
+ if not valid:
161
+ raise FileNotFoundError('Invalid file found')
162
+
163
+ if isinstance(entry_obj, list):
164
+ for entry in entry_obj:
165
+ Utils.render(entry, key_path, option)
166
+
167
+ if isinstance(entry_obj, dict):
168
+ Automate._get_embedded_keys(entry_obj, key_path, option, render_callback=Utils.render_content)
169
+
170
+ @staticmethod
171
+ def render_content(rte_content, embed_obj: dict, option: Options) -> object:
172
+ if isinstance(rte_content, str):
173
+ return Utils.__get_embedded_objects(rte_content, embed_obj, option)
174
+ elif isinstance(rte_content, list):
175
+ render_callback = []
176
+ for rte in rte_content:
177
+ render_callback.append(Utils.render_content(rte, embed_obj, option))
178
+ return render_callback
179
+ return rte_content
180
+
181
+ @staticmethod
182
+ def __get_embedded_objects(html_doc, entry, option):
183
+ import re
184
+ document = f"<items>{html_doc}</items>"
185
+ tag = etree.fromstring(document)
186
+ html_doc = etree.tostring(tag).decode('utf-8')
187
+ html_doc = re.sub('(?ms)<%s[^>]*>(.*)</%s>' % (tag.tag, tag.tag), '\\1', html_doc)
188
+ elements = tag.xpath("//*[contains(@class, 'embedded-asset') or contains(@class, 'embedded-entry')]")
189
+ metadata = Utils.__get_metadata(elements)
190
+ string_content = Utils._str_from_embed_items(metadata=metadata, entry=entry, option=option)
191
+ html_doc = html_doc.replace(metadata.outer_html, string_content)
192
+ return html_doc
193
+
194
+ @staticmethod
195
+ def _str_from_embed_items(metadata, entry, option):
196
+ if '_embedded_items' in entry:
197
+ items = entry['_embedded_items'].keys()
198
+ for item in items:
199
+ items_array = entry['_embedded_items'][item]
200
+ content = Automate._find_embedded_entry(items_array, metadata)
201
+ if content is not None:
202
+ return option.render_options(content, metadata)
203
+ return ''
204
+
205
+ @staticmethod
206
+ def __get_metadata(elements):
207
+ for element in elements:
208
+ content_type = None
209
+ typeof = element.attrib['type']
210
+ if typeof == 'asset':
211
+ uid = element.attrib['data-sys-asset-uid']
212
+ else:
213
+ uid = element.attrib['data-sys-entry-uid']
214
+ content_type = element.attrib['data-sys-content-type-uid']
215
+ style = element.attrib['sys-style-type']
216
+ outer_html = etree.tostring(element).decode('utf-8')
217
+ attributes = element.attrib
218
+ style = convert_style(style)
219
+ metadata = Metadata(element.text, typeof, uid, content_type, style, outer_html, attributes)
220
+ return metadata
221
+
222
+ ####################################################
223
+ # SUPERCHARGED #
224
+ ####################################################
225
+
226
+ @staticmethod
227
+ def json_to_html(entry_obj, key_path: list, option: Options):
228
+ if not Automate.is_json(entry_obj):
229
+ raise FileNotFoundError('Could not process invalid content')
230
+ if isinstance(entry_obj, list):
231
+ for entry in entry_obj:
232
+ return Utils.json_to_html(entry, key_path, option)
233
+ if isinstance(entry_obj, dict):
234
+ if key_path is not None:
235
+ for path in key_path:
236
+ render_callback = Automate._enumerate_content(entry_obj, path, option)
237
+ # Automate._find_embed_keys(entry_obj, path, option, render_callback) This method used in GQL class.
238
+ return render_callback
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentstack_utils
3
- Version: 1.3.3
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
@@ -30,4 +32,5 @@ tests/test_render_default_options.py
30
32
  tests/test_render_options.py
31
33
  tests/test_style_type.py
32
34
  tests/test_util_srte.py
33
- tests/test_utils.py
35
+ tests/test_utils.py
36
+ 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.3',
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
+
@@ -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()
@@ -1,94 +0,0 @@
1
- # pylint: disable=missing-function-docstring
2
-
3
- from lxml import etree
4
-
5
- from contentstack_utils.automate import Automate
6
- from contentstack_utils.helper.converter import convert_style
7
- from contentstack_utils.helper.metadata import Metadata
8
- from contentstack_utils.render.options import Options
9
-
10
-
11
- class Utils(Automate):
12
-
13
- @staticmethod
14
- def render(entry_obj, key_path: list, option: Options):
15
- valid = Automate.is_json(entry_obj)
16
- if not valid:
17
- raise FileNotFoundError('Invalid file found')
18
-
19
- if isinstance(entry_obj, list):
20
- for entry in entry_obj:
21
- Utils.render(entry, key_path, option)
22
-
23
- if isinstance(entry_obj, dict):
24
- Automate._get_embedded_keys(entry_obj, key_path, option, render_callback=Utils.render_content)
25
-
26
- @staticmethod
27
- def render_content(rte_content, embed_obj: dict, option: Options) -> object:
28
- if isinstance(rte_content, str):
29
- return Utils.__get_embedded_objects(rte_content, embed_obj, option)
30
- elif isinstance(rte_content, list):
31
- render_callback = []
32
- for rte in rte_content:
33
- render_callback.append(Utils.render_content(rte, embed_obj, option))
34
- return render_callback
35
- return rte_content
36
-
37
- @staticmethod
38
- def __get_embedded_objects(html_doc, entry, option):
39
- import re
40
- document = f"<items>{html_doc}</items>"
41
- tag = etree.fromstring(document)
42
- html_doc = etree.tostring(tag).decode('utf-8')
43
- html_doc = re.sub('(?ms)<%s[^>]*>(.*)</%s>' % (tag.tag, tag.tag), '\\1', html_doc)
44
- elements = tag.xpath("//*[contains(@class, 'embedded-asset') or contains(@class, 'embedded-entry')]")
45
- metadata = Utils.__get_metadata(elements)
46
- string_content = Utils._str_from_embed_items(metadata=metadata, entry=entry, option=option)
47
- html_doc = html_doc.replace(metadata.outer_html, string_content)
48
- return html_doc
49
-
50
- @staticmethod
51
- def _str_from_embed_items(metadata, entry, option):
52
- if '_embedded_items' in entry:
53
- items = entry['_embedded_items'].keys()
54
- for item in items:
55
- items_array = entry['_embedded_items'][item]
56
- content = Automate._find_embedded_entry(items_array, metadata)
57
- if content is not None:
58
- return option.render_options(content, metadata)
59
- return ''
60
-
61
- @staticmethod
62
- def __get_metadata(elements):
63
- for element in elements:
64
- content_type = None
65
- typeof = element.attrib['type']
66
- if typeof == 'asset':
67
- uid = element.attrib['data-sys-asset-uid']
68
- else:
69
- uid = element.attrib['data-sys-entry-uid']
70
- content_type = element.attrib['data-sys-content-type-uid']
71
- style = element.attrib['sys-style-type']
72
- outer_html = etree.tostring(element).decode('utf-8')
73
- attributes = element.attrib
74
- style = convert_style(style)
75
- metadata = Metadata(element.text, typeof, uid, content_type, style, outer_html, attributes)
76
- return metadata
77
-
78
- ####################################################
79
- # SUPERCHARGED #
80
- ####################################################
81
-
82
- @staticmethod
83
- def json_to_html(entry_obj, key_path: list, option: Options):
84
- if not Automate.is_json(entry_obj):
85
- raise FileNotFoundError('Could not process invalid content')
86
- if isinstance(entry_obj, list):
87
- for entry in entry_obj:
88
- return Utils.json_to_html(entry, key_path, option)
89
- if isinstance(entry_obj, dict):
90
- if key_path is not None:
91
- for path in key_path:
92
- render_callback = Automate._enumerate_content(entry_obj, path, option)
93
- # Automate._find_embed_keys(entry_obj, path, option, render_callback) This method used in GQL class.
94
- return render_callback