Contentstack 2.4.1__tar.gz → 2.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-2.4.1 → contentstack-2.6.0}/Contentstack.egg-info/PKG-INFO +2 -2
- {contentstack-2.4.1 → contentstack-2.6.0}/Contentstack.egg-info/SOURCES.txt +4 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/LICENSE +1 -1
- {contentstack-2.4.1 → contentstack-2.6.0}/PKG-INFO +2 -2
- {contentstack-2.4.1 → contentstack-2.6.0}/README.md +1 -1
- contentstack-2.6.0/contentstack/__init__.py +49 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/asset.py +30 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/assetquery.py +30 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/contenttype.py +1 -0
- contentstack-2.6.0/contentstack/endpoint.py +180 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/entry.py +43 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/entryqueryable.py +39 -0
- contentstack-2.6.0/contentstack/region_refresh.py +81 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/stack.py +18 -16
- {contentstack-2.4.1 → contentstack-2.6.0}/setup.py +16 -0
- contentstack-2.6.0/tests/test_assets.py +557 -0
- contentstack-2.6.0/tests/test_basequery.py +410 -0
- contentstack-2.6.0/tests/test_endpoint.py +287 -0
- contentstack-2.6.0/tests/test_entry.py +408 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/tests/test_global_fields.py +54 -0
- contentstack-2.6.0/tests/test_query.py +452 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/tests/test_stack.py +200 -1
- contentstack-2.6.0/tests/test_taxonomies.py +246 -0
- contentstack-2.4.1/contentstack/__init__.py +0 -29
- contentstack-2.4.1/tests/test_assets.py +0 -215
- contentstack-2.4.1/tests/test_entry.py +0 -162
- contentstack-2.4.1/tests/test_query.py +0 -145
- contentstack-2.4.1/tests/test_taxonomies.py +0 -99
- {contentstack-2.4.1 → contentstack-2.6.0}/Contentstack.egg-info/dependency_links.txt +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/Contentstack.egg-info/not-zip-safe +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/Contentstack.egg-info/requires.txt +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/Contentstack.egg-info/top_level.txt +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/basequery.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/controller.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/deep_merge_lp.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/error_messages.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/globalfields.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/https_connection.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/image_transform.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/query.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/taxonomy.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/utility.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/contentstack/variants.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/setup.cfg +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/tests/test_early_fetch.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/tests/test_early_find.py +0 -0
- {contentstack-2.4.1 → contentstack-2.6.0}/tests/test_live_preview.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Contentstack
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Contentstack is a headless CMS with an API-first approach.
|
|
5
5
|
Home-page: https://github.com/contentstack/contentstack-python
|
|
6
6
|
Author: Contentstack
|
|
@@ -188,7 +188,7 @@ Read through to understand how to use the Sync API with Contentstack Python SDK.
|
|
|
188
188
|
|
|
189
189
|
### The MIT License (MIT)
|
|
190
190
|
|
|
191
|
-
Copyright © 2012-
|
|
191
|
+
Copyright © 2012-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved
|
|
192
192
|
|
|
193
193
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
194
194
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
@@ -14,6 +14,7 @@ contentstack/basequery.py
|
|
|
14
14
|
contentstack/contenttype.py
|
|
15
15
|
contentstack/controller.py
|
|
16
16
|
contentstack/deep_merge_lp.py
|
|
17
|
+
contentstack/endpoint.py
|
|
17
18
|
contentstack/entry.py
|
|
18
19
|
contentstack/entryqueryable.py
|
|
19
20
|
contentstack/error_messages.py
|
|
@@ -21,13 +22,16 @@ contentstack/globalfields.py
|
|
|
21
22
|
contentstack/https_connection.py
|
|
22
23
|
contentstack/image_transform.py
|
|
23
24
|
contentstack/query.py
|
|
25
|
+
contentstack/region_refresh.py
|
|
24
26
|
contentstack/stack.py
|
|
25
27
|
contentstack/taxonomy.py
|
|
26
28
|
contentstack/utility.py
|
|
27
29
|
contentstack/variants.py
|
|
28
30
|
tests/test_assets.py
|
|
31
|
+
tests/test_basequery.py
|
|
29
32
|
tests/test_early_fetch.py
|
|
30
33
|
tests/test_early_find.py
|
|
34
|
+
tests/test_endpoint.py
|
|
31
35
|
tests/test_entry.py
|
|
32
36
|
tests/test_global_fields.py
|
|
33
37
|
tests/test_live_preview.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2012 -
|
|
3
|
+
Copyright (c) 2012 - 2026 Contentstack. All rights reserved.
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Contentstack
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Contentstack is a headless CMS with an API-first approach.
|
|
5
5
|
Home-page: https://github.com/contentstack/contentstack-python
|
|
6
6
|
Author: Contentstack
|
|
@@ -188,7 +188,7 @@ Read through to understand how to use the Sync API with Contentstack Python SDK.
|
|
|
188
188
|
|
|
189
189
|
### The MIT License (MIT)
|
|
190
190
|
|
|
191
|
-
Copyright © 2012-
|
|
191
|
+
Copyright © 2012-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved
|
|
192
192
|
|
|
193
193
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
194
194
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
@@ -152,7 +152,7 @@ Read through to understand how to use the Sync API with Contentstack Python SDK.
|
|
|
152
152
|
|
|
153
153
|
### The MIT License (MIT)
|
|
154
154
|
|
|
155
|
-
Copyright © 2012-
|
|
155
|
+
Copyright © 2012-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved
|
|
156
156
|
|
|
157
157
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
158
158
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The __init__.py files are required to make Python treat the directories as containing
|
|
3
|
+
packages; this is done to prevent directories with a common name, such as string,
|
|
4
|
+
from unintentionally hiding valid modules that occur later on the module search path
|
|
5
|
+
"""
|
|
6
|
+
from .entry import Entry
|
|
7
|
+
from .asset import Asset
|
|
8
|
+
from .contenttype import ContentType
|
|
9
|
+
from .endpoint import Endpoint
|
|
10
|
+
from .https_connection import HTTPSConnection
|
|
11
|
+
from contentstack.stack import Stack
|
|
12
|
+
from .utility import Utils
|
|
13
|
+
from .region_refresh import refresh_regions
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
"Entry",
|
|
17
|
+
"Asset",
|
|
18
|
+
"ContentType",
|
|
19
|
+
"Endpoint",
|
|
20
|
+
"HTTPSConnection",
|
|
21
|
+
"Stack",
|
|
22
|
+
"Utils",
|
|
23
|
+
"refresh_regions",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_contentstack_endpoint(region='us', service='', omit_https=False):
|
|
28
|
+
"""
|
|
29
|
+
Resolve a Contentstack service endpoint URL for a given region.
|
|
30
|
+
|
|
31
|
+
Proxy to :class:`Endpoint.get_contentstack_endpoint` for convenience —
|
|
32
|
+
mirrors ``Contentstack::getContentstackEndpoint()`` in the PHP SDK.
|
|
33
|
+
|
|
34
|
+
:param region: Region ID or alias ('us', 'eu', 'azure-na', 'gcp-eu', ...).
|
|
35
|
+
:param service: Service key ('contentDelivery', 'contentManagement', ...).
|
|
36
|
+
When empty, returns a dict of all endpoints for the region.
|
|
37
|
+
:param omit_https: When True, strips 'https://' from the returned URL(s).
|
|
38
|
+
:returns: str when service is provided, dict[str,str] otherwise.
|
|
39
|
+
"""
|
|
40
|
+
return Endpoint.get_contentstack_endpoint(region, service, omit_https)
|
|
41
|
+
|
|
42
|
+
__title__ = 'contentstack-delivery-python'
|
|
43
|
+
__author__ = 'contentstack'
|
|
44
|
+
__status__ = 'debug'
|
|
45
|
+
__version__ = 'v2.6.0'
|
|
46
|
+
__endpoint__ = 'cdn.contentstack.io'
|
|
47
|
+
__email__ = 'support@contentstack.com'
|
|
48
|
+
__developer_email__ = 'mobile@contentstack.com'
|
|
49
|
+
__license__ = "MIT"
|
|
@@ -17,6 +17,7 @@ class Asset:
|
|
|
17
17
|
self.__uid = uid
|
|
18
18
|
if self.__uid is None or self.__uid.strip() == 0:
|
|
19
19
|
raise KeyError(ErrorMessages.INVALID_UID)
|
|
20
|
+
self.uid = uid
|
|
20
21
|
self.base_url = f'{self.http_instance.endpoint}/assets/{self.__uid}'
|
|
21
22
|
if 'environment' in self.http_instance.headers:
|
|
22
23
|
self.asset_params['environment'] = self.http_instance.headers['environment']
|
|
@@ -117,6 +118,35 @@ class Asset:
|
|
|
117
118
|
self.asset_params['include_fallback'] = "true"
|
|
118
119
|
return self
|
|
119
120
|
|
|
121
|
+
def asset_fields(self, *field_names):
|
|
122
|
+
r"""Include specific asset fields in the response.
|
|
123
|
+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
|
|
124
|
+
Pass one or more field names. Can be called multiple times to add more fields.
|
|
125
|
+
|
|
126
|
+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
|
|
127
|
+
:return: `Asset`, so we can chain the call
|
|
128
|
+
----------------------------
|
|
129
|
+
Example::
|
|
130
|
+
>>> import contentstack
|
|
131
|
+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
|
|
132
|
+
>>> asset = stack.asset(uid='asset_uid')
|
|
133
|
+
>>> result = asset.asset_fields('user_defined_fields', 'visual_markups').fetch()
|
|
134
|
+
----------------------------
|
|
135
|
+
"""
|
|
136
|
+
if field_names:
|
|
137
|
+
values = []
|
|
138
|
+
for name in field_names:
|
|
139
|
+
if isinstance(name, (list, tuple)):
|
|
140
|
+
values.extend(str(v) for v in name)
|
|
141
|
+
else:
|
|
142
|
+
values.append(str(name))
|
|
143
|
+
if values:
|
|
144
|
+
existing = self.asset_params.get('asset_fields[]', [])
|
|
145
|
+
if not isinstance(existing, list):
|
|
146
|
+
existing = [existing]
|
|
147
|
+
self.asset_params['asset_fields[]'] = existing + values
|
|
148
|
+
return self
|
|
149
|
+
|
|
120
150
|
def fetch(self):
|
|
121
151
|
r"""This call fetches the latest version of a specific asset of a particular stack.
|
|
122
152
|
:return: json response of asset
|
|
@@ -157,6 +157,36 @@ class AssetQuery(BaseQuery):
|
|
|
157
157
|
self.asset_query_params['locale'] = locale
|
|
158
158
|
return self
|
|
159
159
|
|
|
160
|
+
def asset_fields(self, *field_names):
|
|
161
|
+
r"""Include specific asset fields in the response.
|
|
162
|
+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
|
|
163
|
+
Pass one or more field names. Can be called multiple times to add more fields.
|
|
164
|
+
|
|
165
|
+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
|
|
166
|
+
:return: AssetQuery: so we can chain the call
|
|
167
|
+
|
|
168
|
+
-----------------------------
|
|
169
|
+
[Example]:
|
|
170
|
+
|
|
171
|
+
>>> import contentstack
|
|
172
|
+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
|
|
173
|
+
>>> result = stack.asset_query().asset_fields('user_defined_fields', 'visual_markups').find()
|
|
174
|
+
------------------------------
|
|
175
|
+
"""
|
|
176
|
+
if field_names:
|
|
177
|
+
values = []
|
|
178
|
+
for name in field_names:
|
|
179
|
+
if isinstance(name, (list, tuple)):
|
|
180
|
+
values.extend(str(v) for v in name)
|
|
181
|
+
else:
|
|
182
|
+
values.append(str(name))
|
|
183
|
+
if values:
|
|
184
|
+
existing = self.asset_query_params.get('asset_fields[]', [])
|
|
185
|
+
if not isinstance(existing, list):
|
|
186
|
+
existing = [existing]
|
|
187
|
+
self.asset_query_params['asset_fields[]'] = existing + values
|
|
188
|
+
return self
|
|
189
|
+
|
|
160
190
|
def find(self):
|
|
161
191
|
r"""This call fetches the list of all the assets of a particular stack.
|
|
162
192
|
It also returns the content of each asset in JSON format.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Endpoint — Contentstack region-to-URL resolver.
|
|
3
|
+
|
|
4
|
+
Resolves Contentstack service endpoint URLs for any supported region.
|
|
5
|
+
Region data is loaded from contentstack/assets/regions.json (bundled) and
|
|
6
|
+
cached in-memory for the lifetime of the process. When the bundled file is
|
|
7
|
+
absent the class attempts a live download from the Contentstack CDN so the
|
|
8
|
+
SDK continues to work even when the file was not created during installation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
REGIONS_URL = 'https://artifacts.contentstack.com/regions.json'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Endpoint:
|
|
19
|
+
"""
|
|
20
|
+
Resolves Contentstack service endpoint URLs for any supported region.
|
|
21
|
+
|
|
22
|
+
Usage::
|
|
23
|
+
|
|
24
|
+
from contentstack.endpoint import Endpoint
|
|
25
|
+
|
|
26
|
+
# Single service URL
|
|
27
|
+
url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery')
|
|
28
|
+
# 'https://eu-cdn.contentstack.com'
|
|
29
|
+
|
|
30
|
+
# All services for a region
|
|
31
|
+
endpoints = Endpoint.get_contentstack_endpoint('azure-na')
|
|
32
|
+
# {'contentDelivery': 'https://...', 'contentManagement': 'https://...', ...}
|
|
33
|
+
|
|
34
|
+
# Strip scheme (useful when setting host directly)
|
|
35
|
+
host = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentDelivery', omit_https=True)
|
|
36
|
+
# 'gcp-eu-cdn.contentstack.com'
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_regions_data = None # in-memory cache — shared across all instances
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def get_contentstack_endpoint(region='us', service='', omit_https=False):
|
|
43
|
+
"""
|
|
44
|
+
Resolve a Contentstack service endpoint URL for a given region.
|
|
45
|
+
|
|
46
|
+
:param region: Region ID or alias ('us', 'eu', 'azure-na', 'gcp-eu', etc.).
|
|
47
|
+
Defaults to 'us' (AWS North America).
|
|
48
|
+
:param service: Service key ('contentDelivery', 'contentManagement', ...).
|
|
49
|
+
When empty, returns a dict of all endpoints for the region.
|
|
50
|
+
:param omit_https: When True, strips 'https://' prefix from returned URL(s).
|
|
51
|
+
:returns: str when service is provided, dict[str,str] otherwise.
|
|
52
|
+
:raises ValueError: When region is empty, unknown, or service is not found.
|
|
53
|
+
:raises RuntimeError: When regions.json cannot be read or parsed.
|
|
54
|
+
"""
|
|
55
|
+
if not region:
|
|
56
|
+
raise ValueError('Empty region provided. Please put valid region.')
|
|
57
|
+
|
|
58
|
+
data = Endpoint._load_regions()
|
|
59
|
+
normalized = region.strip().lower()
|
|
60
|
+
region_row = Endpoint._find_region(data['regions'], normalized)
|
|
61
|
+
|
|
62
|
+
if region_row is None:
|
|
63
|
+
raise ValueError(f'Invalid region: {region}')
|
|
64
|
+
|
|
65
|
+
if service:
|
|
66
|
+
if service not in region_row['endpoints']:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f'Service "{service}" not found for region "{region_row["id"]}"'
|
|
69
|
+
)
|
|
70
|
+
url = region_row['endpoints'][service]
|
|
71
|
+
return Endpoint._strip_https(url) if omit_https else url
|
|
72
|
+
|
|
73
|
+
endpoints = region_row['endpoints']
|
|
74
|
+
if omit_https:
|
|
75
|
+
return {k: Endpoint._strip_https(v) for k, v in endpoints.items()}
|
|
76
|
+
return dict(endpoints)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _load_regions():
|
|
80
|
+
"""
|
|
81
|
+
Load and cache regions.json.
|
|
82
|
+
|
|
83
|
+
Resolution order:
|
|
84
|
+
1. In-memory static cache (zero I/O after first call)
|
|
85
|
+
2. contentstack/assets/regions.json on disk (written by download script)
|
|
86
|
+
3. Live download from artifacts.contentstack.com (fallback)
|
|
87
|
+
"""
|
|
88
|
+
if Endpoint._regions_data is not None:
|
|
89
|
+
return Endpoint._regions_data
|
|
90
|
+
|
|
91
|
+
assets_dir = os.path.join(os.path.dirname(__file__), 'assets')
|
|
92
|
+
path = os.path.join(assets_dir, 'regions.json')
|
|
93
|
+
|
|
94
|
+
if not os.path.exists(path):
|
|
95
|
+
Endpoint._download_and_save(path)
|
|
96
|
+
|
|
97
|
+
if not os.path.exists(path):
|
|
98
|
+
raise RuntimeError(
|
|
99
|
+
'contentstack: regions.json not found and could not be downloaded. '
|
|
100
|
+
'Run "python scripts/download_regions.py" and ensure network access.'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
105
|
+
decoded = json.load(f)
|
|
106
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
107
|
+
raise RuntimeError(
|
|
108
|
+
f'contentstack: Could not read or parse regions.json: {exc}. '
|
|
109
|
+
'Run "python scripts/download_regions.py" to re-download it.'
|
|
110
|
+
) from exc
|
|
111
|
+
|
|
112
|
+
if not isinstance(decoded, dict) or 'regions' not in decoded:
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
'contentstack: regions.json is corrupt. '
|
|
115
|
+
'Run "python scripts/download_regions.py" to re-download it.'
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
Endpoint._regions_data = decoded
|
|
119
|
+
return Endpoint._regions_data
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _download_and_save(dest):
|
|
123
|
+
"""
|
|
124
|
+
Download regions.json from the Contentstack CDN and save to disk.
|
|
125
|
+
Uses the requests library (already an SDK dependency).
|
|
126
|
+
Silent on failure — the caller decides whether a missing file is fatal.
|
|
127
|
+
|
|
128
|
+
:param dest: Absolute path to write the file to.
|
|
129
|
+
"""
|
|
130
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
import requests
|
|
134
|
+
response = requests.get(REGIONS_URL, timeout=30)
|
|
135
|
+
response.raise_for_status()
|
|
136
|
+
data = response.text
|
|
137
|
+
except Exception: # noqa: BLE001
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
decoded = json.loads(data)
|
|
142
|
+
except json.JSONDecodeError:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
if isinstance(decoded, dict) and 'regions' in decoded:
|
|
146
|
+
try:
|
|
147
|
+
with open(dest, 'w', encoding='utf-8') as f:
|
|
148
|
+
f.write(data)
|
|
149
|
+
except OSError:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _find_region(regions, input_str):
|
|
154
|
+
"""
|
|
155
|
+
Find a region entry by its id or any alias (case-insensitive).
|
|
156
|
+
|
|
157
|
+
Two-pass: exact id match first, then alias[] scan — mirrors PHP implementation.
|
|
158
|
+
|
|
159
|
+
:param regions: list of region dicts from regions.json
|
|
160
|
+
:param input_str: already-lowercased input
|
|
161
|
+
:returns: region dict or None
|
|
162
|
+
"""
|
|
163
|
+
for row in regions:
|
|
164
|
+
if row['id'] == input_str:
|
|
165
|
+
return row
|
|
166
|
+
for row in regions:
|
|
167
|
+
for alias in row.get('alias', []):
|
|
168
|
+
if alias.lower() == input_str:
|
|
169
|
+
return row
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _strip_https(url):
|
|
174
|
+
"""Strip the https:// (or http://) scheme from a URL string."""
|
|
175
|
+
return re.sub(r'^https?://', '', url)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def reset_cache():
|
|
179
|
+
"""Reset the internal region cache. Intended for testing only."""
|
|
180
|
+
Endpoint._regions_data = None
|
|
@@ -3,6 +3,7 @@ The Get a single entry request fetches a particular entry of a content type.
|
|
|
3
3
|
API Reference: https://www.contentstack.com/docs/developers/apis/content-delivery-api/#single-entry
|
|
4
4
|
"""
|
|
5
5
|
#min-similarity-lines=10
|
|
6
|
+
from __future__ import annotations
|
|
6
7
|
import logging
|
|
7
8
|
from urllib import parse
|
|
8
9
|
from contentstack.error_messages import ErrorMessages
|
|
@@ -54,6 +55,25 @@ class Entry(EntryQueryable):
|
|
|
54
55
|
self.http_instance.headers['environment'] = environment
|
|
55
56
|
return self
|
|
56
57
|
|
|
58
|
+
def remove_environment(self):
|
|
59
|
+
"""Removes environment from the request headers
|
|
60
|
+
:return: Entry, so we can chain the call
|
|
61
|
+
-------------------------------
|
|
62
|
+
Example::
|
|
63
|
+
|
|
64
|
+
>>> import contentstack
|
|
65
|
+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
|
|
66
|
+
>>> content_type = stack.content_type('content_type_uid')
|
|
67
|
+
>>> entry = content_type.entry(uid='entry_uid')
|
|
68
|
+
>>> entry = entry.environment('test')
|
|
69
|
+
>>> entry = entry.remove_environment()
|
|
70
|
+
>>> result = entry.fetch()
|
|
71
|
+
-------------------------------
|
|
72
|
+
"""
|
|
73
|
+
if 'environment' in self.http_instance.headers:
|
|
74
|
+
self.http_instance.headers.pop('environment')
|
|
75
|
+
return self
|
|
76
|
+
|
|
57
77
|
def version(self, version):
|
|
58
78
|
"""When no version is specified, it returns the latest version
|
|
59
79
|
To retrieve a specific version, specify the version number under this parameter.
|
|
@@ -96,6 +116,9 @@ class Entry(EntryQueryable):
|
|
|
96
116
|
"""
|
|
97
117
|
if None in (key, value) and not isinstance(key, str):
|
|
98
118
|
raise ValueError(ErrorMessages.INVALID_KEY_VALUE_ARGS)
|
|
119
|
+
# Convert non-string values to strings
|
|
120
|
+
if not isinstance(value, str):
|
|
121
|
+
value = str(value)
|
|
99
122
|
self.entry_param[key] = value
|
|
100
123
|
return self
|
|
101
124
|
|
|
@@ -154,6 +177,26 @@ class Entry(EntryQueryable):
|
|
|
154
177
|
self.entry_param['include_embedded_items[]'] = "BASE"
|
|
155
178
|
return self
|
|
156
179
|
|
|
180
|
+
def asset_fields(self, *field_names):
|
|
181
|
+
"""Include specific asset fields in the response.
|
|
182
|
+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
|
|
183
|
+
Pass one or more field names. Can be called multiple times to add more fields.
|
|
184
|
+
|
|
185
|
+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
|
|
186
|
+
:return: Entry, so we can chain the call
|
|
187
|
+
----------------------------
|
|
188
|
+
Example::
|
|
189
|
+
|
|
190
|
+
>>> import contentstack
|
|
191
|
+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
|
|
192
|
+
>>> content_type = stack.content_type('content_type_uid')
|
|
193
|
+
>>> entry = content_type.entry(uid='entry_uid')
|
|
194
|
+
>>> entry = entry.asset_fields('user_defined_fields', 'visual_markups')
|
|
195
|
+
>>> result = entry.fetch()
|
|
196
|
+
----------------------------
|
|
197
|
+
"""
|
|
198
|
+
return super().asset_fields(*field_names)
|
|
199
|
+
|
|
157
200
|
def __get_base_url(self, endpoint=''):
|
|
158
201
|
if endpoint is not None and endpoint.strip(): # .strip() removes leading/trailing whitespace
|
|
159
202
|
self.http_instance.endpoint = endpoint
|
|
@@ -167,6 +167,45 @@ class EntryQueryable:
|
|
|
167
167
|
self.entry_queryable_param['include_metadata'] = 'true'
|
|
168
168
|
return self
|
|
169
169
|
|
|
170
|
+
def asset_fields(self, *field_names):
|
|
171
|
+
"""
|
|
172
|
+
Include specific asset fields in the response.
|
|
173
|
+
Supported values: user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups.
|
|
174
|
+
Pass one or more field names. Can be called multiple times to add more fields.
|
|
175
|
+
|
|
176
|
+
:param field_names: One or more asset field names (user_defined_fields, embedded_metadata, ai_generated_metadata, visual_markups)
|
|
177
|
+
:return: self: so you can chain this call.
|
|
178
|
+
|
|
179
|
+
Example (Query):
|
|
180
|
+
>>> import contentstack
|
|
181
|
+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
|
|
182
|
+
>>> content_type = stack.content_type('content_type_uid')
|
|
183
|
+
>>> query = content_type.query()
|
|
184
|
+
>>> query = query.asset_fields('user_defined_fields', 'visual_markups')
|
|
185
|
+
>>> result = query.find()
|
|
186
|
+
|
|
187
|
+
Example (Entry):
|
|
188
|
+
>>> import contentstack
|
|
189
|
+
>>> stack = contentstack.Stack('api_key', 'delivery_token', 'environment')
|
|
190
|
+
>>> content_type = stack.content_type('content_type_uid')
|
|
191
|
+
>>> entry = content_type.entry('entry_uid')
|
|
192
|
+
>>> entry = entry.asset_fields('user_defined_fields', 'visual_markups')
|
|
193
|
+
>>> result = entry.fetch()
|
|
194
|
+
"""
|
|
195
|
+
if field_names:
|
|
196
|
+
values = []
|
|
197
|
+
for name in field_names:
|
|
198
|
+
if isinstance(name, (list, tuple)):
|
|
199
|
+
values.extend(str(v) for v in name)
|
|
200
|
+
else:
|
|
201
|
+
values.append(str(name))
|
|
202
|
+
if values:
|
|
203
|
+
existing = self.entry_queryable_param.get('asset_fields[]', [])
|
|
204
|
+
if not isinstance(existing, list):
|
|
205
|
+
existing = [existing]
|
|
206
|
+
self.entry_queryable_param['asset_fields[]'] = existing + values
|
|
207
|
+
return self
|
|
208
|
+
|
|
170
209
|
def add_param(self, key: str, value: str):
|
|
171
210
|
"""
|
|
172
211
|
This method adds key and value to an Entry.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility to pull the latest regions.json from the Contentstack CDN and
|
|
3
|
+
overwrite the bundled copy at contentstack/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 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/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
|
+
return 0
|
|
75
|
+
except RuntimeError as exc:
|
|
76
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
sys.exit(_cli_main())
|
|
@@ -7,6 +7,7 @@ from contentstack.error_messages import ErrorMessages
|
|
|
7
7
|
from contentstack.asset import Asset
|
|
8
8
|
from contentstack.assetquery import AssetQuery
|
|
9
9
|
from contentstack.contenttype import ContentType
|
|
10
|
+
from contentstack.endpoint import Endpoint
|
|
10
11
|
from contentstack.taxonomy import Taxonomy
|
|
11
12
|
from contentstack.globalfields import GlobalField
|
|
12
13
|
from contentstack.https_connection import HTTPSConnection
|
|
@@ -119,20 +120,15 @@ class Stack:
|
|
|
119
120
|
if self.environment is None or self.environment == "":
|
|
120
121
|
raise PermissionError(ErrorMessages.INVALID_ENVIRONMENT_TOKEN)
|
|
121
122
|
|
|
122
|
-
if self.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
self.host = 'gcp-na-cdn.contentstack.com'
|
|
132
|
-
elif self.region.value == 'gcp-eu' and self.host == DEFAULT_HOST:
|
|
133
|
-
self.host = 'gcp-eu-cdn.contentstack.com'
|
|
134
|
-
elif self.region.value != 'us':
|
|
135
|
-
self.host = f'{self.region.value}-{DEFAULT_HOST}'
|
|
123
|
+
if self.host == DEFAULT_HOST:
|
|
124
|
+
try:
|
|
125
|
+
self.host = Endpoint.get_contentstack_endpoint(
|
|
126
|
+
self.region.value, 'contentDelivery', omit_https=True)
|
|
127
|
+
except (ValueError, RuntimeError):
|
|
128
|
+
# Unknown/custom region — fall back to legacy pattern so
|
|
129
|
+
# code written before this feature was added continues to work.
|
|
130
|
+
if self.region.value != 'us':
|
|
131
|
+
self.host = f'{self.region.value}-{DEFAULT_HOST}'
|
|
136
132
|
self.endpoint = f'https://{self.host}/{self.version}'
|
|
137
133
|
|
|
138
134
|
def _setup_headers(self):
|
|
@@ -365,8 +361,14 @@ class Stack:
|
|
|
365
361
|
|
|
366
362
|
def _setup_live_preview(self):
|
|
367
363
|
if self.live_preview and self.live_preview.get("enable"):
|
|
368
|
-
|
|
369
|
-
|
|
364
|
+
if not self.live_preview.get("host"):
|
|
365
|
+
try:
|
|
366
|
+
mgmt_host = Endpoint.get_contentstack_endpoint(
|
|
367
|
+
self.region.value, 'contentManagement', omit_https=True)
|
|
368
|
+
except (ValueError, RuntimeError):
|
|
369
|
+
region_prefix = "" if self.region.value == "us" else f"{self.region.value}-"
|
|
370
|
+
mgmt_host = f"{region_prefix}api.contentstack.io"
|
|
371
|
+
self.live_preview["host"] = mgmt_host
|
|
370
372
|
|
|
371
373
|
if self.live_preview.get("preview_token"):
|
|
372
374
|
self.headers["preview_token"] = self.live_preview["preview_token"]
|