contentstack-utils 1.5.0__tar.gz → 1.6.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.
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/PKG-INFO +1 -1
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/__init__.py +5 -1
- contentstack_utils-1.6.0/contentstack_utils/endpoint.py +242 -0
- contentstack_utils-1.6.0/contentstack_utils/region_refresh.py +82 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/utils.py +29 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils.egg-info/PKG-INFO +1 -1
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils.egg-info/SOURCES.txt +3 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/setup.py +23 -3
- contentstack_utils-1.6.0/tests/test_endpoint.py +264 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/LICENSE +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/README.md +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/automate.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/embedded/__init__.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/embedded/item_type.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/embedded/styletype.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/entry_editable.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/gql.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/helper/__init__.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/helper/converter.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/helper/metadata.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/helper/node_to_html.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/render/__init__.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/render/options.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils.egg-info/dependency_links.txt +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils.egg-info/top_level.txt +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/setup.cfg +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/__init__.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/convert_style.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_default_opt_others.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_editable_tags.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_gql_to_html_func.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_helper_node_to_html.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_item_types.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_metadata.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_option_render_mark.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_render_default_options.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_render_options.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_style_type.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_util_srte.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/tests/test_utils.py +0 -0
- {contentstack_utils-1.5.0 → contentstack_utils-1.6.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.
|
|
3
|
+
Version: 1.6.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
|
|
@@ -10,6 +10,7 @@ __author__, __status__, __version__, __endpoint__ and __email__
|
|
|
10
10
|
|
|
11
11
|
from contentstack_utils.embedded.item_type import ItemType
|
|
12
12
|
from contentstack_utils.embedded.styletype import StyleType
|
|
13
|
+
from contentstack_utils.endpoint import Endpoint
|
|
13
14
|
from contentstack_utils.helper.metadata import Metadata
|
|
14
15
|
from contentstack_utils.helper.node_to_html import NodeToHtml
|
|
15
16
|
from contentstack_utils.render.options import Options
|
|
@@ -17,8 +18,10 @@ from contentstack_utils.utils import Utils
|
|
|
17
18
|
from contentstack_utils.gql import GQL
|
|
18
19
|
from contentstack_utils.automate import Automate
|
|
19
20
|
from contentstack_utils.entry_editable import addEditableTags, addTags, getTag
|
|
21
|
+
from contentstack_utils.region_refresh import refresh_regions
|
|
20
22
|
|
|
21
23
|
__all__ = (
|
|
24
|
+
"Endpoint",
|
|
22
25
|
"Utils",
|
|
23
26
|
"Options",
|
|
24
27
|
"Metadata",
|
|
@@ -30,11 +33,12 @@ __all__ = (
|
|
|
30
33
|
"addEditableTags",
|
|
31
34
|
"addTags",
|
|
32
35
|
"getTag",
|
|
36
|
+
"refresh_regions",
|
|
33
37
|
)
|
|
34
38
|
|
|
35
39
|
__title__ = 'contentstack_utils'
|
|
36
40
|
__author__ = 'contentstack'
|
|
37
41
|
__status__ = 'debug'
|
|
38
|
-
__version__ = '1.
|
|
42
|
+
__version__ = '1.6.0'
|
|
39
43
|
__endpoint__ = 'cdn.contentstack.io'
|
|
40
44
|
__contact__ = 'support@contentstack.com'
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Endpoint resolution for Contentstack services across all regions.
|
|
3
|
+
|
|
4
|
+
Reads a bundled regions.json (src/assets/regions.json) and resolves
|
|
5
|
+
the correct base URL for any region + service combination. No runtime
|
|
6
|
+
HTTP calls — the file is shipped with the package and updated via
|
|
7
|
+
``python scripts/refresh_regions.py``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Dict, Optional, Union
|
|
15
|
+
|
|
16
|
+
_REGIONS_URL = "https://artifacts.contentstack.com/regions.json"
|
|
17
|
+
_ASSETS_DIR = os.path.join(os.path.dirname(__file__), "assets")
|
|
18
|
+
_REGIONS_FILE = os.path.join(_ASSETS_DIR, "regions.json")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Endpoint:
|
|
22
|
+
"""
|
|
23
|
+
Resolve Contentstack service URLs for any region.
|
|
24
|
+
|
|
25
|
+
All public methods are static — no instantiation required.
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
from contentstack_utils import Endpoint
|
|
30
|
+
|
|
31
|
+
# Full URL
|
|
32
|
+
url = Endpoint.get_contentstack_endpoint("na", "contentDelivery")
|
|
33
|
+
# → "https://cdn.contentstack.io"
|
|
34
|
+
|
|
35
|
+
# Host only (strip https://) — useful for SDK setHost() calls
|
|
36
|
+
host = Endpoint.get_contentstack_endpoint("eu", "contentDelivery", omit_https=True)
|
|
37
|
+
# → "eu-cdn.contentstack.com"
|
|
38
|
+
|
|
39
|
+
# All endpoints for a region
|
|
40
|
+
all_endpoints = Endpoint.get_contentstack_endpoint("azure-na")
|
|
41
|
+
# → {"contentDelivery": "...", "contentManagement": "...", ...}
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Module-level cache — loaded once per Python process, shared across all calls.
|
|
45
|
+
_regions_data: Optional[Dict] = None
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def get_contentstack_endpoint(
|
|
49
|
+
region: str = "us",
|
|
50
|
+
service: str = "",
|
|
51
|
+
omit_https: bool = False,
|
|
52
|
+
) -> Union[str, Dict[str, str]]:
|
|
53
|
+
"""
|
|
54
|
+
Resolve a Contentstack service endpoint URL for the given region.
|
|
55
|
+
|
|
56
|
+
:param region:
|
|
57
|
+
Region ID or any accepted alias (case-insensitive, ``-`` and ``_``
|
|
58
|
+
are interchangeable). Examples: ``'na'``, ``'us'``, ``'eu'``,
|
|
59
|
+
``'AWS-NA'``, ``'azure_eu'``, ``'gcp-na'``.
|
|
60
|
+
Defaults to ``'us'`` (AWS North America).
|
|
61
|
+
:param service:
|
|
62
|
+
Optional service key. When provided, a single URL string is
|
|
63
|
+
returned. When omitted, a dict of **all** service URLs is returned.
|
|
64
|
+
Valid keys include: ``'contentDelivery'``, ``'contentManagement'``,
|
|
65
|
+
``'auth'``, ``'graphqlDelivery'``, ``'preview'``, ``'images'``,
|
|
66
|
+
``'assets'``, ``'automate'``, ``'launch'``, ``'developerHub'``,
|
|
67
|
+
``'brandKit'``, ``'genAI'``, ``'personalizeManagement'``,
|
|
68
|
+
``'personalizeEdge'``, ``'composableStudio'``, ``'assetManagement'``.
|
|
69
|
+
:param omit_https:
|
|
70
|
+
When ``True``, strips the ``https://`` (or ``http://``) scheme from
|
|
71
|
+
every returned URL. Useful when passing the host to an SDK that
|
|
72
|
+
constructs its own URLs (e.g. ``stack.set_host(host)``).
|
|
73
|
+
:returns:
|
|
74
|
+
- A ``str`` URL when *service* is specified.
|
|
75
|
+
- A ``dict[str, str]`` mapping service keys → URLs when *service*
|
|
76
|
+
is omitted.
|
|
77
|
+
:raises ValueError:
|
|
78
|
+
If *region* is an empty string.
|
|
79
|
+
:raises LookupError:
|
|
80
|
+
If *region* does not match any known region ID or alias, or if
|
|
81
|
+
*service* is not present in the resolved region's endpoint map.
|
|
82
|
+
:raises RuntimeError:
|
|
83
|
+
If the bundled ``regions.json`` cannot be read or is malformed.
|
|
84
|
+
|
|
85
|
+
Examples::
|
|
86
|
+
|
|
87
|
+
Endpoint.get_contentstack_endpoint("na", "contentDelivery")
|
|
88
|
+
# → "https://cdn.contentstack.io"
|
|
89
|
+
|
|
90
|
+
Endpoint.get_contentstack_endpoint("eu", "contentDelivery", omit_https=True)
|
|
91
|
+
# → "eu-cdn.contentstack.com"
|
|
92
|
+
|
|
93
|
+
Endpoint.get_contentstack_endpoint("azure-na")
|
|
94
|
+
# → {"contentDelivery": "https://...", ...}
|
|
95
|
+
"""
|
|
96
|
+
if not region:
|
|
97
|
+
raise ValueError("Empty region provided. Please put valid region.")
|
|
98
|
+
|
|
99
|
+
data = Endpoint._load_regions()
|
|
100
|
+
normalized = region.strip().lower()
|
|
101
|
+
|
|
102
|
+
if not normalized:
|
|
103
|
+
raise ValueError("Empty region provided. Please put valid region.")
|
|
104
|
+
region_row = Endpoint._find_region_by_id_or_alias(data["regions"], normalized)
|
|
105
|
+
|
|
106
|
+
if region_row is None:
|
|
107
|
+
raise LookupError(f"Invalid region: {region}")
|
|
108
|
+
|
|
109
|
+
if service:
|
|
110
|
+
endpoints = region_row["endpoints"]
|
|
111
|
+
if service not in endpoints:
|
|
112
|
+
raise LookupError(
|
|
113
|
+
f'Service "{service}" not found for region "{region_row["id"]}"'
|
|
114
|
+
)
|
|
115
|
+
url = endpoints[service]
|
|
116
|
+
return Endpoint._strip_https(url) if omit_https else url
|
|
117
|
+
|
|
118
|
+
endpoints = dict(region_row["endpoints"])
|
|
119
|
+
return Endpoint._strip_https_from_map(endpoints) if omit_https else endpoints
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
getContentstackEndpoint = get_contentstack_endpoint
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def reset_cache() -> None:
|
|
126
|
+
"""
|
|
127
|
+
Clear the in-memory region cache.
|
|
128
|
+
|
|
129
|
+
Intended for testing only — forces the next call to re-read
|
|
130
|
+
``regions.json`` from disk.
|
|
131
|
+
"""
|
|
132
|
+
Endpoint._regions_data = None
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
# Internal helpers
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _load_regions() -> Dict:
|
|
140
|
+
"""
|
|
141
|
+
Load and cache regions data.
|
|
142
|
+
|
|
143
|
+
Resolution order:
|
|
144
|
+
1. In-memory cache (zero I/O after the first call in a process)
|
|
145
|
+
2. Bundled ``contentstack_utils/assets/regions.json`` on disk
|
|
146
|
+
3. Live download from ``artifacts.contentstack.com`` (fallback when
|
|
147
|
+
the file is absent — e.g. an editable install without assets)
|
|
148
|
+
"""
|
|
149
|
+
if Endpoint._regions_data is not None:
|
|
150
|
+
return Endpoint._regions_data
|
|
151
|
+
|
|
152
|
+
if not os.path.exists(_REGIONS_FILE):
|
|
153
|
+
Endpoint._download_and_save(_REGIONS_FILE)
|
|
154
|
+
|
|
155
|
+
if not os.path.exists(_REGIONS_FILE):
|
|
156
|
+
raise RuntimeError(
|
|
157
|
+
"contentstack_utils: regions.json not found and could not be downloaded. "
|
|
158
|
+
"Run 'python scripts/refresh_regions.py' and ensure network access."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
with open(_REGIONS_FILE, "r", encoding="utf-8") as fh:
|
|
163
|
+
raw = fh.read()
|
|
164
|
+
except OSError as exc:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"contentstack_utils: Could not read regions.json: {exc}"
|
|
167
|
+
) from exc
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
decoded = json.loads(raw)
|
|
171
|
+
except json.JSONDecodeError as exc:
|
|
172
|
+
raise RuntimeError(
|
|
173
|
+
"contentstack_utils: regions.json is corrupt. "
|
|
174
|
+
"Run 'python scripts/refresh_regions.py' to re-download it."
|
|
175
|
+
) from exc
|
|
176
|
+
|
|
177
|
+
if not isinstance(decoded, dict) or "regions" not in decoded:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"contentstack_utils: regions.json is corrupt. "
|
|
180
|
+
"Run 'python scripts/refresh_regions.py' to re-download it."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
Endpoint._regions_data = decoded
|
|
184
|
+
return Endpoint._regions_data
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _download_and_save(dest: str) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Fetch regions.json from the Contentstack CDN and write it to *dest*.
|
|
190
|
+
|
|
191
|
+
Silent on failure — the caller decides whether a missing file is fatal.
|
|
192
|
+
"""
|
|
193
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
194
|
+
try:
|
|
195
|
+
with urllib.request.urlopen(_REGIONS_URL, timeout=30) as resp:
|
|
196
|
+
data = resp.read().decode("utf-8")
|
|
197
|
+
except Exception:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
decoded = json.loads(data)
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if isinstance(decoded, dict) and "regions" in decoded:
|
|
206
|
+
with open(dest, "w", encoding="utf-8") as fh:
|
|
207
|
+
fh.write(data)
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _find_region_by_id_or_alias(
|
|
211
|
+
regions: list, normalized_input: str
|
|
212
|
+
) -> Optional[Dict]:
|
|
213
|
+
"""
|
|
214
|
+
Find a region by its ``id`` field first, then by any alias.
|
|
215
|
+
|
|
216
|
+
Both passes are case-insensitive (caller must pass a lowercased string).
|
|
217
|
+
Two-pass approach mirrors the PHP implementation: ID match wins over alias
|
|
218
|
+
match, which avoids surprising behaviour when a future alias happens to
|
|
219
|
+
collide with another region's canonical ID.
|
|
220
|
+
"""
|
|
221
|
+
# Pass 1 — exact id match
|
|
222
|
+
for row in regions:
|
|
223
|
+
if row["id"] == normalized_input:
|
|
224
|
+
return row
|
|
225
|
+
|
|
226
|
+
# Pass 2 — alias match
|
|
227
|
+
for row in regions:
|
|
228
|
+
for alias in row.get("alias", []):
|
|
229
|
+
if alias.lower() == normalized_input:
|
|
230
|
+
return row
|
|
231
|
+
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _strip_https(url: str) -> str:
|
|
236
|
+
"""Strip ``https://`` or ``http://`` from the start of a URL."""
|
|
237
|
+
return re.sub(r"^https?://", "", url)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _strip_https_from_map(endpoints: Dict[str, str]) -> Dict[str, str]:
|
|
241
|
+
"""Return a new dict with the scheme stripped from every URL value."""
|
|
242
|
+
return {key: Endpoint._strip_https(url) for key, url in endpoints.items()}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility to pull the latest regions.json from the Contentstack CDN and
|
|
3
|
+
overwrite the bundled copy at contentstack_utils/assets/regions.json.
|
|
4
|
+
|
|
5
|
+
Exposed as a package-level function so tooling and CI pipelines can call it
|
|
6
|
+
programmatically instead of invoking the script directly:
|
|
7
|
+
|
|
8
|
+
from contentstack_utils import refresh_regions
|
|
9
|
+
refresh_regions()
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.request
|
|
16
|
+
|
|
17
|
+
_REGIONS_URL = "https://artifacts.contentstack.com/regions.json"
|
|
18
|
+
_ASSET_PATH = os.path.join(os.path.dirname(__file__), "assets", "regions.json")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def refresh_regions(
|
|
22
|
+
url: str = _REGIONS_URL,
|
|
23
|
+
dest: str = _ASSET_PATH,
|
|
24
|
+
*,
|
|
25
|
+
timeout: int = 30,
|
|
26
|
+
silent: bool = False,
|
|
27
|
+
) -> dict:
|
|
28
|
+
"""
|
|
29
|
+
Download the latest regions manifest from the Contentstack CDN and write
|
|
30
|
+
it to the bundled assets file so all consumers get the update.
|
|
31
|
+
|
|
32
|
+
@param url - URL to fetch regions.json from (defaults to Contentstack CDN)
|
|
33
|
+
@param dest - Destination file path (defaults to contentstack_utils/assets/regions.json)
|
|
34
|
+
@param timeout - HTTP request timeout in seconds
|
|
35
|
+
@param silent - Suppress progress output when True
|
|
36
|
+
@returns The parsed regions dict on success
|
|
37
|
+
@raises RuntimeError on download failure, invalid JSON, or unexpected schema
|
|
38
|
+
"""
|
|
39
|
+
dest = os.path.normpath(dest)
|
|
40
|
+
|
|
41
|
+
if not silent:
|
|
42
|
+
print(f"Fetching {url} ...")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
46
|
+
data = resp.read().decode("utf-8")
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
raise RuntimeError(f"Could not download regions.json: {exc}") from exc
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
decoded = json.loads(data)
|
|
52
|
+
except json.JSONDecodeError as exc:
|
|
53
|
+
raise RuntimeError(f"Downloaded content is not valid JSON: {exc}") from exc
|
|
54
|
+
|
|
55
|
+
if not isinstance(decoded, dict) or "regions" not in decoded:
|
|
56
|
+
raise RuntimeError("Downloaded JSON does not contain a 'regions' key.")
|
|
57
|
+
|
|
58
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
59
|
+
with open(dest, "w", encoding="utf-8") as fh:
|
|
60
|
+
json.dump(decoded, fh, indent=2, ensure_ascii=False)
|
|
61
|
+
fh.write("\n")
|
|
62
|
+
|
|
63
|
+
region_count = len(decoded["regions"])
|
|
64
|
+
if not silent:
|
|
65
|
+
print(f"OK: Wrote {region_count} regions to {dest}")
|
|
66
|
+
|
|
67
|
+
return decoded
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _cli_main() -> int:
|
|
71
|
+
"""Entry point kept for backward compatibility with the scripts/ invocation."""
|
|
72
|
+
try:
|
|
73
|
+
refresh_regions()
|
|
74
|
+
print("Next steps:")
|
|
75
|
+
return 0
|
|
76
|
+
except RuntimeError as exc:
|
|
77
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
sys.exit(_cli_main())
|
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
6
6
|
from lxml import etree
|
|
7
7
|
|
|
8
8
|
from contentstack_utils.automate import Automate
|
|
9
|
+
from contentstack_utils.endpoint import Endpoint
|
|
9
10
|
from contentstack_utils.entry_editable import addEditableTags as _addEditableTags
|
|
10
11
|
from contentstack_utils.entry_editable import addTags as _addTags
|
|
11
12
|
from contentstack_utils.entry_editable import getTag as _getTag
|
|
@@ -219,6 +220,34 @@ class Utils(Automate):
|
|
|
219
220
|
metadata = Metadata(element.text, typeof, uid, content_type, style, outer_html, attributes)
|
|
220
221
|
return metadata
|
|
221
222
|
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Endpoint resolution — thin proxy to Endpoint class
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def get_contentstack_endpoint(
|
|
229
|
+
region: str = "us",
|
|
230
|
+
service: str = "",
|
|
231
|
+
omit_https: bool = False,
|
|
232
|
+
):
|
|
233
|
+
"""
|
|
234
|
+
Resolve a Contentstack service URL for the given region.
|
|
235
|
+
|
|
236
|
+
Delegates entirely to :class:`~contentstack_utils.endpoint.Endpoint`.
|
|
237
|
+
Both ``Utils.get_contentstack_endpoint(...)`` and
|
|
238
|
+
``Endpoint.get_contentstack_endpoint(...)`` produce identical results —
|
|
239
|
+
choose whichever import path suits your codebase.
|
|
240
|
+
|
|
241
|
+
:param region: Region ID or alias (e.g. ``'na'``, ``'eu'``, ``'azure-na'``).
|
|
242
|
+
:param service: Optional service key (e.g. ``'contentDelivery'``).
|
|
243
|
+
:param omit_https: Strip ``https://`` from the returned URL(s).
|
|
244
|
+
:returns: URL string when *service* is given, dict of all URLs otherwise.
|
|
245
|
+
"""
|
|
246
|
+
return Endpoint.get_contentstack_endpoint(region, service, omit_https)
|
|
247
|
+
|
|
248
|
+
# camelCase alias for cross-SDK parity
|
|
249
|
+
getContentstackEndpoint = get_contentstack_endpoint
|
|
250
|
+
|
|
222
251
|
####################################################
|
|
223
252
|
# SUPERCHARGED #
|
|
224
253
|
####################################################
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contentstack_utils
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
{contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils.egg-info/SOURCES.txt
RENAMED
|
@@ -3,8 +3,10 @@ README.md
|
|
|
3
3
|
setup.py
|
|
4
4
|
contentstack_utils/__init__.py
|
|
5
5
|
contentstack_utils/automate.py
|
|
6
|
+
contentstack_utils/endpoint.py
|
|
6
7
|
contentstack_utils/entry_editable.py
|
|
7
8
|
contentstack_utils/gql.py
|
|
9
|
+
contentstack_utils/region_refresh.py
|
|
8
10
|
contentstack_utils/utils.py
|
|
9
11
|
contentstack_utils.egg-info/PKG-INFO
|
|
10
12
|
contentstack_utils.egg-info/SOURCES.txt
|
|
@@ -23,6 +25,7 @@ tests/__init__.py
|
|
|
23
25
|
tests/convert_style.py
|
|
24
26
|
tests/test_default_opt_others.py
|
|
25
27
|
tests/test_editable_tags.py
|
|
28
|
+
tests/test_endpoint.py
|
|
26
29
|
tests/test_gql_to_html_func.py
|
|
27
30
|
tests/test_helper_node_to_html.py
|
|
28
31
|
tests/test_item_types.py
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
|
|
3
4
|
from setuptools import setup, find_packages
|
|
4
|
-
from
|
|
5
|
+
from setuptools.command.build_py import build_py
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BuildPyWithRegions(build_py):
|
|
9
|
+
"""Fetch latest regions.json from Contentstack CDN before packaging."""
|
|
10
|
+
|
|
11
|
+
def run(self):
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
try:
|
|
14
|
+
from contentstack_utils.region_refresh import refresh_regions
|
|
15
|
+
refresh_regions()
|
|
16
|
+
except Exception as exc:
|
|
17
|
+
# Never block a build over a network failure — warn and continue.
|
|
18
|
+
print(f"WARNING: Could not refresh regions.json: {exc}", file=sys.stderr)
|
|
19
|
+
super().run()
|
|
20
|
+
|
|
5
21
|
|
|
6
22
|
with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme:
|
|
7
23
|
long_description = readme.read()
|
|
@@ -9,19 +25,23 @@ with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme:
|
|
|
9
25
|
setup(
|
|
10
26
|
name='contentstack_utils',
|
|
11
27
|
packages=find_packages(),
|
|
28
|
+
package_data={
|
|
29
|
+
"contentstack_utils": ["assets/regions.json"],
|
|
30
|
+
},
|
|
12
31
|
description="contentstack_utils is a Utility package for Contentstack headless CMS with an API-first approach.",
|
|
13
32
|
author='contentstack',
|
|
14
33
|
long_description=long_description,
|
|
15
34
|
long_description_content_type="text/markdown",
|
|
16
35
|
url="https://github.com/contentstack/contentstack-utils-python",
|
|
17
36
|
license='MIT',
|
|
18
|
-
version='1.
|
|
37
|
+
version='1.6.0',
|
|
19
38
|
install_requires=[
|
|
20
|
-
|
|
39
|
+
|
|
21
40
|
],
|
|
22
41
|
setup_requires=['pytest-runner'],
|
|
23
42
|
tests_require=['pytest==4.4.1'],
|
|
24
43
|
test_suite='tests',
|
|
44
|
+
cmdclass={"build_py": BuildPyWithRegions},
|
|
25
45
|
classifiers=[
|
|
26
46
|
"License :: OSI Approved :: MIT License",
|
|
27
47
|
"Operating System :: OS Independent",
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for contentstack_utils.endpoint.Endpoint and the Utils proxy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from contentstack_utils.endpoint import Endpoint
|
|
8
|
+
from contentstack_utils.utils import Utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def reset_cache():
|
|
13
|
+
"""Isolate each test — forces a fresh regions.json read."""
|
|
14
|
+
Endpoint.reset_cache()
|
|
15
|
+
yield
|
|
16
|
+
Endpoint.reset_cache()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Default region (us / na)
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class TestDefaultRegion:
|
|
24
|
+
def test_returns_all_endpoints_when_no_service(self):
|
|
25
|
+
endpoints = Endpoint.get_contentstack_endpoint()
|
|
26
|
+
assert isinstance(endpoints, dict)
|
|
27
|
+
assert "contentDelivery" in endpoints
|
|
28
|
+
assert "contentManagement" in endpoints
|
|
29
|
+
|
|
30
|
+
def test_content_delivery_url(self):
|
|
31
|
+
url = Endpoint.get_contentstack_endpoint("us", "contentDelivery")
|
|
32
|
+
assert url == "https://cdn.contentstack.io"
|
|
33
|
+
|
|
34
|
+
def test_content_management_url(self):
|
|
35
|
+
url = Endpoint.get_contentstack_endpoint("us", "contentManagement")
|
|
36
|
+
assert url == "https://api.contentstack.io"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# NA region alias resolution
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
NA_ALIASES = ["na", "us", "aws-na", "aws_na", "NA", "US", "AWS-NA", "AWS_NA"]
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("alias", NA_ALIASES)
|
|
46
|
+
def test_na_aliases_resolve_to_same_cdn(alias):
|
|
47
|
+
url = Endpoint.get_contentstack_endpoint(alias, "contentDelivery")
|
|
48
|
+
assert url == "https://cdn.contentstack.io"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# All 7 regions — contentDelivery spot-checks
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("region,expected", [
|
|
56
|
+
("na", "https://cdn.contentstack.io"),
|
|
57
|
+
("eu", "https://eu-cdn.contentstack.com"),
|
|
58
|
+
("au", "https://au-cdn.contentstack.com"),
|
|
59
|
+
("azure-na", "https://azure-na-cdn.contentstack.com"),
|
|
60
|
+
("azure-eu", "https://azure-eu-cdn.contentstack.com"),
|
|
61
|
+
("gcp-na", "https://gcp-na-cdn.contentstack.com"),
|
|
62
|
+
("gcp-eu", "https://gcp-eu-cdn.contentstack.com"),
|
|
63
|
+
])
|
|
64
|
+
def test_content_delivery_by_region(region, expected):
|
|
65
|
+
assert Endpoint.get_contentstack_endpoint(region, "contentDelivery") == expected
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# All 7 regions — contentManagement spot-checks
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
@pytest.mark.parametrize("region,expected", [
|
|
73
|
+
("na", "https://api.contentstack.io"),
|
|
74
|
+
("eu", "https://eu-api.contentstack.com"),
|
|
75
|
+
("au", "https://au-api.contentstack.com"),
|
|
76
|
+
("azure-na", "https://azure-na-api.contentstack.com"),
|
|
77
|
+
("azure-eu", "https://azure-eu-api.contentstack.com"),
|
|
78
|
+
("gcp-na", "https://gcp-na-api.contentstack.com"),
|
|
79
|
+
("gcp-eu", "https://gcp-eu-api.contentstack.com"),
|
|
80
|
+
])
|
|
81
|
+
def test_content_management_by_region(region, expected):
|
|
82
|
+
assert Endpoint.get_contentstack_endpoint(region, "contentManagement") == expected
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# All expected service keys present
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
EXPECTED_SERVICE_KEYS = [
|
|
90
|
+
"application", "contentDelivery", "contentManagement", "auth",
|
|
91
|
+
"graphqlDelivery", "preview", "graphqlPreview", "images", "assets",
|
|
92
|
+
"automate", "launch", "developerHub", "brandKit", "genAI",
|
|
93
|
+
"personalizeManagement", "personalizeEdge", "composableStudio",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
def test_all_service_keys_present_for_eu():
|
|
97
|
+
endpoints = Endpoint.get_contentstack_endpoint("eu")
|
|
98
|
+
for key in EXPECTED_SERVICE_KEYS:
|
|
99
|
+
assert key in endpoints, f"Missing service key: {key}"
|
|
100
|
+
|
|
101
|
+
def test_na_has_asset_management_key():
|
|
102
|
+
# NA is the only region that currently includes assetManagement.
|
|
103
|
+
endpoints = Endpoint.get_contentstack_endpoint("na")
|
|
104
|
+
assert "assetManagement" in endpoints
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# omit_https flag
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
class TestOmitHttps:
|
|
112
|
+
def test_strips_scheme_from_single_service(self):
|
|
113
|
+
host = Endpoint.get_contentstack_endpoint("eu", "contentDelivery", omit_https=True)
|
|
114
|
+
assert host == "eu-cdn.contentstack.com"
|
|
115
|
+
|
|
116
|
+
def test_strips_scheme_from_all_services(self):
|
|
117
|
+
endpoints = Endpoint.get_contentstack_endpoint("na", omit_https=True)
|
|
118
|
+
assert isinstance(endpoints, dict)
|
|
119
|
+
for key, url in endpoints.items():
|
|
120
|
+
assert "https://" not in url, f"Service {key} still has https://"
|
|
121
|
+
assert "http://" not in url, f"Service {key} still has http://"
|
|
122
|
+
|
|
123
|
+
def test_false_retains_scheme(self):
|
|
124
|
+
url = Endpoint.get_contentstack_endpoint("na", "contentManagement", omit_https=False)
|
|
125
|
+
assert url.startswith("https://")
|
|
126
|
+
|
|
127
|
+
def test_omit_https_positional_argument(self):
|
|
128
|
+
# Confirm the third positional arg is honoured (mirrors PHP signature).
|
|
129
|
+
host = Endpoint.get_contentstack_endpoint("gcp-na", "contentDelivery", True)
|
|
130
|
+
assert host == "gcp-na-cdn.contentstack.com"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Case-insensitive and underscore alias matching
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
class TestAliasMatching:
|
|
138
|
+
def test_uppercase_alias(self):
|
|
139
|
+
url = Endpoint.get_contentstack_endpoint("AWS-NA", "contentDelivery")
|
|
140
|
+
assert url == "https://cdn.contentstack.io"
|
|
141
|
+
|
|
142
|
+
def test_underscore_azure_alias(self):
|
|
143
|
+
url = Endpoint.get_contentstack_endpoint("azure_na", "contentDelivery")
|
|
144
|
+
assert url == "https://azure-na-cdn.contentstack.com"
|
|
145
|
+
|
|
146
|
+
def test_underscore_gcp_alias(self):
|
|
147
|
+
url = Endpoint.get_contentstack_endpoint("gcp_eu", "contentManagement")
|
|
148
|
+
assert url == "https://gcp-eu-api.contentstack.com"
|
|
149
|
+
|
|
150
|
+
def test_mixed_case_eu(self):
|
|
151
|
+
url = Endpoint.get_contentstack_endpoint("EU", "contentDelivery")
|
|
152
|
+
assert url == "https://eu-cdn.contentstack.com"
|
|
153
|
+
|
|
154
|
+
def test_mixed_case_au(self):
|
|
155
|
+
url = Endpoint.get_contentstack_endpoint("AU", "contentDelivery")
|
|
156
|
+
assert url == "https://au-cdn.contentstack.com"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Return-all-endpoints (no service argument)
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
class TestNoService:
|
|
164
|
+
def test_returns_dict(self):
|
|
165
|
+
result = Endpoint.get_contentstack_endpoint("au")
|
|
166
|
+
assert isinstance(result, dict)
|
|
167
|
+
assert len(result) > 1
|
|
168
|
+
|
|
169
|
+
def test_dict_contains_correct_urls(self):
|
|
170
|
+
endpoints = Endpoint.get_contentstack_endpoint("au")
|
|
171
|
+
assert endpoints["contentDelivery"] == "https://au-cdn.contentstack.com"
|
|
172
|
+
assert endpoints["contentManagement"] == "https://au-api.contentstack.com"
|
|
173
|
+
|
|
174
|
+
def test_default_call_returns_na(self):
|
|
175
|
+
endpoints = Endpoint.get_contentstack_endpoint()
|
|
176
|
+
assert endpoints["contentDelivery"] == "https://cdn.contentstack.io"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Error cases
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
class TestErrorCases:
|
|
184
|
+
def test_empty_region_raises_value_error(self):
|
|
185
|
+
with pytest.raises(ValueError, match="Empty region provided"):
|
|
186
|
+
Endpoint.get_contentstack_endpoint("")
|
|
187
|
+
|
|
188
|
+
def test_unknown_region_raises_lookup_error(self):
|
|
189
|
+
with pytest.raises(LookupError, match="Invalid region: invalid-region"):
|
|
190
|
+
Endpoint.get_contentstack_endpoint("invalid-region")
|
|
191
|
+
|
|
192
|
+
def test_unknown_service_raises_lookup_error(self):
|
|
193
|
+
with pytest.raises(LookupError, match='Service "unknownService" not found'):
|
|
194
|
+
Endpoint.get_contentstack_endpoint("na", "unknownService")
|
|
195
|
+
|
|
196
|
+
def test_whitespace_region_raises_value_error(self):
|
|
197
|
+
with pytest.raises(ValueError, match="Empty region provided"):
|
|
198
|
+
Endpoint.get_contentstack_endpoint(" ")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# camelCase alias (cross-SDK parity)
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
class TestCamelCaseAlias:
|
|
206
|
+
def test_get_contentstack_endpoint_camel_case(self):
|
|
207
|
+
url = Endpoint.getContentstackEndpoint("na", "contentDelivery")
|
|
208
|
+
assert url == "https://cdn.contentstack.io"
|
|
209
|
+
|
|
210
|
+
def test_camel_case_and_snake_case_return_same(self):
|
|
211
|
+
snake = Endpoint.get_contentstack_endpoint("eu", "contentDelivery")
|
|
212
|
+
camel = Endpoint.getContentstackEndpoint("eu", "contentDelivery")
|
|
213
|
+
assert snake == camel
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Utils proxy
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
class TestUtilsProxy:
|
|
221
|
+
def test_proxy_returns_same_as_endpoint_class(self):
|
|
222
|
+
via_endpoint = Endpoint.get_contentstack_endpoint("eu", "contentDelivery")
|
|
223
|
+
via_utils = Utils.get_contentstack_endpoint("eu", "contentDelivery")
|
|
224
|
+
assert via_endpoint == via_utils
|
|
225
|
+
|
|
226
|
+
def test_proxy_default_region(self):
|
|
227
|
+
url = Utils.get_contentstack_endpoint("us", "contentManagement")
|
|
228
|
+
assert url == "https://api.contentstack.io"
|
|
229
|
+
|
|
230
|
+
def test_proxy_omit_https(self):
|
|
231
|
+
host = Utils.get_contentstack_endpoint("gcp-na", "contentDelivery", omit_https=True)
|
|
232
|
+
assert host == "gcp-na-cdn.contentstack.com"
|
|
233
|
+
|
|
234
|
+
def test_proxy_all_endpoints(self):
|
|
235
|
+
endpoints = Utils.get_contentstack_endpoint("azure-eu")
|
|
236
|
+
assert isinstance(endpoints, dict)
|
|
237
|
+
assert "contentDelivery" in endpoints
|
|
238
|
+
|
|
239
|
+
def test_proxy_camel_case_alias(self):
|
|
240
|
+
url = Utils.getContentstackEndpoint("na", "contentDelivery")
|
|
241
|
+
assert url == "https://cdn.contentstack.io"
|
|
242
|
+
|
|
243
|
+
def test_proxy_error_propagates(self):
|
|
244
|
+
with pytest.raises(LookupError):
|
|
245
|
+
Utils.get_contentstack_endpoint("not-a-region", "contentDelivery")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# Cache behaviour
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
class TestCache:
|
|
253
|
+
def test_second_call_uses_cache(self, reset_cache):
|
|
254
|
+
from unittest.mock import patch, MagicMock
|
|
255
|
+
Endpoint.get_contentstack_endpoint("na", "contentDelivery")
|
|
256
|
+
with patch("builtins.open", wraps=open) as spy:
|
|
257
|
+
Endpoint.get_contentstack_endpoint("eu", "contentDelivery")
|
|
258
|
+
spy.assert_not_called()
|
|
259
|
+
|
|
260
|
+
def test_reset_cache_clears_data(self):
|
|
261
|
+
Endpoint.get_contentstack_endpoint("na") # primes cache
|
|
262
|
+
assert Endpoint._regions_data is not None
|
|
263
|
+
Endpoint.reset_cache()
|
|
264
|
+
assert Endpoint._regions_data is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/embedded/__init__.py
RENAMED
|
File without changes
|
{contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/embedded/item_type.py
RENAMED
|
File without changes
|
{contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/embedded/styletype.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/helper/converter.py
RENAMED
|
File without changes
|
|
File without changes
|
{contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils/helper/node_to_html.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentstack_utils-1.5.0 → contentstack_utils-1.6.0}/contentstack_utils.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|