Contentstack 2.5.0__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.5.0 → contentstack-2.6.0}/Contentstack.egg-info/PKG-INFO +1 -1
- {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/SOURCES.txt +3 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/PKG-INFO +1 -1
- contentstack-2.6.0/contentstack/__init__.py +49 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/contenttype.py +1 -0
- contentstack-2.6.0/contentstack/endpoint.py +180 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/entry.py +1 -0
- contentstack-2.6.0/contentstack/region_refresh.py +81 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/stack.py +18 -16
- {contentstack-2.5.0 → contentstack-2.6.0}/setup.py +16 -0
- contentstack-2.6.0/tests/test_endpoint.py +287 -0
- contentstack-2.5.0/contentstack/__init__.py +0 -29
- {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/dependency_links.txt +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/not-zip-safe +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/requires.txt +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/Contentstack.egg-info/top_level.txt +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/LICENSE +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/README.md +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/asset.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/assetquery.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/basequery.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/controller.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/deep_merge_lp.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/entryqueryable.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/error_messages.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/globalfields.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/https_connection.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/image_transform.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/query.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/taxonomy.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/utility.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/contentstack/variants.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/setup.cfg +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_assets.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_basequery.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_early_fetch.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_early_find.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_entry.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_global_fields.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_live_preview.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_query.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_stack.py +0 -0
- {contentstack-2.5.0 → contentstack-2.6.0}/tests/test_taxonomies.py +0 -0
|
@@ -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,6 +22,7 @@ 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
|
|
@@ -29,6 +31,7 @@ tests/test_assets.py
|
|
|
29
31
|
tests/test_basequery.py
|
|
30
32
|
tests/test_early_fetch.py
|
|
31
33
|
tests/test_early_find.py
|
|
34
|
+
tests/test_endpoint.py
|
|
32
35
|
tests/test_entry.py
|
|
33
36
|
tests/test_global_fields.py
|
|
34
37
|
tests/test_live_preview.py
|
|
@@ -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"
|
|
@@ -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
|
|
@@ -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"]
|
|
@@ -9,11 +9,25 @@ import sys
|
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from setuptools import setup, find_packages
|
|
12
|
+
from setuptools.command.build_py import build_py
|
|
12
13
|
except ImportError:
|
|
13
14
|
from distutils.core import setup
|
|
14
15
|
|
|
15
16
|
package = "contentstack"
|
|
16
17
|
|
|
18
|
+
|
|
19
|
+
class BuildPyWithRegions(build_py):
|
|
20
|
+
"""Fetch latest regions.json from Contentstack CDN before packaging."""
|
|
21
|
+
|
|
22
|
+
def run(self):
|
|
23
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
24
|
+
try:
|
|
25
|
+
from contentstack.region_refresh import refresh_regions
|
|
26
|
+
refresh_regions()
|
|
27
|
+
except Exception as exc:
|
|
28
|
+
print(f"WARNING: Could not refresh regions.json: {exc}", file=sys.stderr)
|
|
29
|
+
super().run()
|
|
30
|
+
|
|
17
31
|
def get_version(package):
|
|
18
32
|
"""
|
|
19
33
|
Return package version as listed in `__version__` in `init.py`.
|
|
@@ -32,6 +46,7 @@ requirements = [
|
|
|
32
46
|
]
|
|
33
47
|
|
|
34
48
|
setup(
|
|
49
|
+
cmdclass={"build_py": BuildPyWithRegions},
|
|
35
50
|
title="contentstack-python",
|
|
36
51
|
name="Contentstack",
|
|
37
52
|
status="Active",
|
|
@@ -46,6 +61,7 @@ setup(
|
|
|
46
61
|
long_description_content_type="text/markdown",
|
|
47
62
|
url="https://github.com/contentstack/contentstack-python",
|
|
48
63
|
packages=['contentstack'],
|
|
64
|
+
package_data={'contentstack': ['assets/regions.json']},
|
|
49
65
|
license='MIT',
|
|
50
66
|
test_suite='tests',
|
|
51
67
|
install_requires=requirements,
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import contentstack
|
|
4
|
+
from contentstack.endpoint import Endpoint
|
|
5
|
+
from contentstack.stack import ContentstackRegion, Stack
|
|
6
|
+
|
|
7
|
+
API_KEY = 'test_api_key'
|
|
8
|
+
DELIVERY_TOKEN = 'test_delivery_token'
|
|
9
|
+
ENVIRONMENT = 'test_environment'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestEndpoint(unittest.TestCase):
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
Endpoint.reset_cache()
|
|
16
|
+
|
|
17
|
+
# -------------------------------------------------------------------------
|
|
18
|
+
# Default region (us / na)
|
|
19
|
+
# -------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def test_default_region_returns_all_endpoints(self):
|
|
22
|
+
endpoints = Endpoint.get_contentstack_endpoint()
|
|
23
|
+
self.assertIsInstance(endpoints, dict)
|
|
24
|
+
self.assertIn('contentDelivery', endpoints)
|
|
25
|
+
self.assertIn('contentManagement', endpoints)
|
|
26
|
+
|
|
27
|
+
def test_default_region_content_delivery(self):
|
|
28
|
+
url = Endpoint.get_contentstack_endpoint('us', 'contentDelivery')
|
|
29
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
30
|
+
|
|
31
|
+
def test_default_region_content_management(self):
|
|
32
|
+
url = Endpoint.get_contentstack_endpoint('us', 'contentManagement')
|
|
33
|
+
self.assertEqual('https://api.contentstack.io', url)
|
|
34
|
+
|
|
35
|
+
# -------------------------------------------------------------------------
|
|
36
|
+
# All 7 regions — contentDelivery spot-checks
|
|
37
|
+
# -------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def test_content_delivery_na(self):
|
|
40
|
+
url = Endpoint.get_contentstack_endpoint('na', 'contentDelivery')
|
|
41
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
42
|
+
|
|
43
|
+
def test_content_delivery_eu(self):
|
|
44
|
+
url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery')
|
|
45
|
+
self.assertEqual('https://eu-cdn.contentstack.com', url)
|
|
46
|
+
|
|
47
|
+
def test_content_delivery_au(self):
|
|
48
|
+
url = Endpoint.get_contentstack_endpoint('au', 'contentDelivery')
|
|
49
|
+
self.assertEqual('https://au-cdn.contentstack.com', url)
|
|
50
|
+
|
|
51
|
+
def test_content_delivery_azure_na(self):
|
|
52
|
+
url = Endpoint.get_contentstack_endpoint('azure-na', 'contentDelivery')
|
|
53
|
+
self.assertEqual('https://azure-na-cdn.contentstack.com', url)
|
|
54
|
+
|
|
55
|
+
def test_content_delivery_azure_eu(self):
|
|
56
|
+
url = Endpoint.get_contentstack_endpoint('azure-eu', 'contentDelivery')
|
|
57
|
+
self.assertEqual('https://azure-eu-cdn.contentstack.com', url)
|
|
58
|
+
|
|
59
|
+
def test_content_delivery_gcp_na(self):
|
|
60
|
+
url = Endpoint.get_contentstack_endpoint('gcp-na', 'contentDelivery')
|
|
61
|
+
self.assertEqual('https://gcp-na-cdn.contentstack.com', url)
|
|
62
|
+
|
|
63
|
+
def test_content_delivery_gcp_eu(self):
|
|
64
|
+
url = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentDelivery')
|
|
65
|
+
self.assertEqual('https://gcp-eu-cdn.contentstack.com', url)
|
|
66
|
+
|
|
67
|
+
# -------------------------------------------------------------------------
|
|
68
|
+
# All 7 regions — contentManagement spot-checks
|
|
69
|
+
# -------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def test_content_management_na(self):
|
|
72
|
+
url = Endpoint.get_contentstack_endpoint('na', 'contentManagement')
|
|
73
|
+
self.assertEqual('https://api.contentstack.io', url)
|
|
74
|
+
|
|
75
|
+
def test_content_management_eu(self):
|
|
76
|
+
url = Endpoint.get_contentstack_endpoint('eu', 'contentManagement')
|
|
77
|
+
self.assertEqual('https://eu-api.contentstack.com', url)
|
|
78
|
+
|
|
79
|
+
def test_content_management_au(self):
|
|
80
|
+
url = Endpoint.get_contentstack_endpoint('au', 'contentManagement')
|
|
81
|
+
self.assertEqual('https://au-api.contentstack.com', url)
|
|
82
|
+
|
|
83
|
+
def test_content_management_azure_na(self):
|
|
84
|
+
url = Endpoint.get_contentstack_endpoint('azure-na', 'contentManagement')
|
|
85
|
+
self.assertEqual('https://azure-na-api.contentstack.com', url)
|
|
86
|
+
|
|
87
|
+
def test_content_management_azure_eu(self):
|
|
88
|
+
url = Endpoint.get_contentstack_endpoint('azure-eu', 'contentManagement')
|
|
89
|
+
self.assertEqual('https://azure-eu-api.contentstack.com', url)
|
|
90
|
+
|
|
91
|
+
def test_content_management_gcp_na(self):
|
|
92
|
+
url = Endpoint.get_contentstack_endpoint('gcp-na', 'contentManagement')
|
|
93
|
+
self.assertEqual('https://gcp-na-api.contentstack.com', url)
|
|
94
|
+
|
|
95
|
+
def test_content_management_gcp_eu(self):
|
|
96
|
+
url = Endpoint.get_contentstack_endpoint('gcp-eu', 'contentManagement')
|
|
97
|
+
self.assertEqual('https://gcp-eu-api.contentstack.com', url)
|
|
98
|
+
|
|
99
|
+
# -------------------------------------------------------------------------
|
|
100
|
+
# NA aliases all resolve to the same endpoint
|
|
101
|
+
# -------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def test_alias_na(self):
|
|
104
|
+
url = Endpoint.get_contentstack_endpoint('na', 'contentDelivery')
|
|
105
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
106
|
+
|
|
107
|
+
def test_alias_us(self):
|
|
108
|
+
url = Endpoint.get_contentstack_endpoint('us', 'contentDelivery')
|
|
109
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
110
|
+
|
|
111
|
+
def test_alias_aws_na_hyphen(self):
|
|
112
|
+
url = Endpoint.get_contentstack_endpoint('aws-na', 'contentDelivery')
|
|
113
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
114
|
+
|
|
115
|
+
def test_alias_aws_na_underscore(self):
|
|
116
|
+
url = Endpoint.get_contentstack_endpoint('aws_na', 'contentDelivery')
|
|
117
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
118
|
+
|
|
119
|
+
def test_alias_na_uppercase(self):
|
|
120
|
+
url = Endpoint.get_contentstack_endpoint('NA', 'contentDelivery')
|
|
121
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
122
|
+
|
|
123
|
+
def test_alias_us_uppercase(self):
|
|
124
|
+
url = Endpoint.get_contentstack_endpoint('US', 'contentDelivery')
|
|
125
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
126
|
+
|
|
127
|
+
# -------------------------------------------------------------------------
|
|
128
|
+
# Case-insensitive alias matching for other regions
|
|
129
|
+
# -------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def test_alias_aws_na_uppercase(self):
|
|
132
|
+
url = Endpoint.get_contentstack_endpoint('AWS-NA', 'contentDelivery')
|
|
133
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
134
|
+
|
|
135
|
+
def test_alias_azure_na_underscore(self):
|
|
136
|
+
url = Endpoint.get_contentstack_endpoint('azure_na', 'contentDelivery')
|
|
137
|
+
self.assertEqual('https://azure-na-cdn.contentstack.com', url)
|
|
138
|
+
|
|
139
|
+
def test_alias_gcp_eu_underscore(self):
|
|
140
|
+
url = Endpoint.get_contentstack_endpoint('gcp_eu', 'contentManagement')
|
|
141
|
+
self.assertEqual('https://gcp-eu-api.contentstack.com', url)
|
|
142
|
+
|
|
143
|
+
# -------------------------------------------------------------------------
|
|
144
|
+
# ContentstackRegion enum constants resolve correctly
|
|
145
|
+
# -------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def test_region_constant_us(self):
|
|
148
|
+
url = Endpoint.get_contentstack_endpoint(ContentstackRegion.US.value, 'contentDelivery')
|
|
149
|
+
self.assertEqual('https://cdn.contentstack.io', url)
|
|
150
|
+
|
|
151
|
+
def test_region_constant_eu(self):
|
|
152
|
+
url = Endpoint.get_contentstack_endpoint(ContentstackRegion.EU.value, 'contentDelivery')
|
|
153
|
+
self.assertEqual('https://eu-cdn.contentstack.com', url)
|
|
154
|
+
|
|
155
|
+
def test_region_constant_au(self):
|
|
156
|
+
url = Endpoint.get_contentstack_endpoint(ContentstackRegion.AU.value, 'contentDelivery')
|
|
157
|
+
self.assertEqual('https://au-cdn.contentstack.com', url)
|
|
158
|
+
|
|
159
|
+
def test_region_constant_azure_na(self):
|
|
160
|
+
url = Endpoint.get_contentstack_endpoint(ContentstackRegion.AZURE_NA.value, 'contentDelivery')
|
|
161
|
+
self.assertEqual('https://azure-na-cdn.contentstack.com', url)
|
|
162
|
+
|
|
163
|
+
def test_region_constant_azure_eu(self):
|
|
164
|
+
url = Endpoint.get_contentstack_endpoint(ContentstackRegion.AZURE_EU.value, 'contentDelivery')
|
|
165
|
+
self.assertEqual('https://azure-eu-cdn.contentstack.com', url)
|
|
166
|
+
|
|
167
|
+
def test_region_constant_gcp_na(self):
|
|
168
|
+
url = Endpoint.get_contentstack_endpoint(ContentstackRegion.GCP_NA.value, 'contentDelivery')
|
|
169
|
+
self.assertEqual('https://gcp-na-cdn.contentstack.com', url)
|
|
170
|
+
|
|
171
|
+
def test_region_constant_gcp_eu(self):
|
|
172
|
+
url = Endpoint.get_contentstack_endpoint(ContentstackRegion.GCP_EU.value, 'contentDelivery')
|
|
173
|
+
self.assertEqual('https://gcp-eu-cdn.contentstack.com', url)
|
|
174
|
+
|
|
175
|
+
# -------------------------------------------------------------------------
|
|
176
|
+
# omit_https flag
|
|
177
|
+
# -------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def test_omit_https_strips_scheme_single_service(self):
|
|
180
|
+
url = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery', omit_https=True)
|
|
181
|
+
self.assertEqual('eu-cdn.contentstack.com', url)
|
|
182
|
+
|
|
183
|
+
def test_omit_https_strips_scheme_all_services(self):
|
|
184
|
+
endpoints = Endpoint.get_contentstack_endpoint('na', omit_https=True)
|
|
185
|
+
self.assertIsInstance(endpoints, dict)
|
|
186
|
+
for key, url in endpoints.items():
|
|
187
|
+
self.assertNotIn('https://', url, f'Service {key} still has https://')
|
|
188
|
+
self.assertNotIn('http://', url, f'Service {key} still has http://')
|
|
189
|
+
|
|
190
|
+
def test_omit_https_false_retains_scheme(self):
|
|
191
|
+
url = Endpoint.get_contentstack_endpoint('na', 'contentManagement', omit_https=False)
|
|
192
|
+
self.assertTrue(url.startswith('https://'))
|
|
193
|
+
|
|
194
|
+
# -------------------------------------------------------------------------
|
|
195
|
+
# No service — returns full dict
|
|
196
|
+
# -------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def test_no_service_returns_dict(self):
|
|
199
|
+
result = Endpoint.get_contentstack_endpoint('au')
|
|
200
|
+
self.assertIsInstance(result, dict)
|
|
201
|
+
self.assertGreater(len(result), 1)
|
|
202
|
+
|
|
203
|
+
def test_no_service_contains_correct_urls(self):
|
|
204
|
+
endpoints = Endpoint.get_contentstack_endpoint('au')
|
|
205
|
+
self.assertEqual('https://au-cdn.contentstack.com', endpoints['contentDelivery'])
|
|
206
|
+
self.assertEqual('https://au-api.contentstack.com', endpoints['contentManagement'])
|
|
207
|
+
|
|
208
|
+
# -------------------------------------------------------------------------
|
|
209
|
+
# Error cases
|
|
210
|
+
# -------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def test_empty_region_raises_value_error(self):
|
|
213
|
+
with self.assertRaises(ValueError) as ctx:
|
|
214
|
+
Endpoint.get_contentstack_endpoint('')
|
|
215
|
+
self.assertIn('Empty region', str(ctx.exception))
|
|
216
|
+
|
|
217
|
+
def test_unknown_region_raises_value_error(self):
|
|
218
|
+
with self.assertRaises(ValueError) as ctx:
|
|
219
|
+
Endpoint.get_contentstack_endpoint('invalid-region')
|
|
220
|
+
self.assertIn('Invalid region', str(ctx.exception))
|
|
221
|
+
|
|
222
|
+
def test_unknown_service_raises_value_error(self):
|
|
223
|
+
with self.assertRaises(ValueError) as ctx:
|
|
224
|
+
Endpoint.get_contentstack_endpoint('na', 'unknownService')
|
|
225
|
+
self.assertIn('unknownService', str(ctx.exception))
|
|
226
|
+
|
|
227
|
+
# -------------------------------------------------------------------------
|
|
228
|
+
# contentstack.get_contentstack_endpoint() module-level proxy
|
|
229
|
+
# -------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def test_module_proxy_returns_same_result(self):
|
|
232
|
+
via_class = Endpoint.get_contentstack_endpoint('eu', 'contentDelivery')
|
|
233
|
+
via_module = contentstack.get_contentstack_endpoint('eu', 'contentDelivery')
|
|
234
|
+
self.assertEqual(via_class, via_module)
|
|
235
|
+
|
|
236
|
+
def test_module_proxy_default_region(self):
|
|
237
|
+
url = contentstack.get_contentstack_endpoint('us', 'contentManagement')
|
|
238
|
+
self.assertEqual('https://api.contentstack.io', url)
|
|
239
|
+
|
|
240
|
+
def test_module_proxy_omit_https(self):
|
|
241
|
+
url = contentstack.get_contentstack_endpoint('gcp-na', 'contentDelivery', omit_https=True)
|
|
242
|
+
self.assertEqual('gcp-na-cdn.contentstack.com', url)
|
|
243
|
+
|
|
244
|
+
def test_module_proxy_all_endpoints(self):
|
|
245
|
+
endpoints = contentstack.get_contentstack_endpoint('azure-eu')
|
|
246
|
+
self.assertIsInstance(endpoints, dict)
|
|
247
|
+
self.assertIn('contentDelivery', endpoints)
|
|
248
|
+
|
|
249
|
+
# -------------------------------------------------------------------------
|
|
250
|
+
# Stack host resolution via Endpoint
|
|
251
|
+
# -------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def test_stack_us_host_resolves_to_default_cdn(self):
|
|
254
|
+
stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.US)
|
|
255
|
+
self.assertEqual('cdn.contentstack.io', stack.host)
|
|
256
|
+
|
|
257
|
+
def test_stack_eu_host_resolves_via_endpoint(self):
|
|
258
|
+
stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.EU)
|
|
259
|
+
self.assertEqual('eu-cdn.contentstack.com', stack.host)
|
|
260
|
+
|
|
261
|
+
def test_stack_au_host_resolves_via_endpoint(self):
|
|
262
|
+
stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.AU)
|
|
263
|
+
self.assertEqual('au-cdn.contentstack.com', stack.host)
|
|
264
|
+
|
|
265
|
+
def test_stack_azure_na_host_resolves_via_endpoint(self):
|
|
266
|
+
stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.AZURE_NA)
|
|
267
|
+
self.assertEqual('azure-na-cdn.contentstack.com', stack.host)
|
|
268
|
+
|
|
269
|
+
def test_stack_gcp_eu_host_resolves_via_endpoint(self):
|
|
270
|
+
stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.GCP_EU)
|
|
271
|
+
self.assertEqual('gcp-eu-cdn.contentstack.com', stack.host)
|
|
272
|
+
|
|
273
|
+
def test_stack_explicit_host_overrides_region(self):
|
|
274
|
+
stack = Stack(
|
|
275
|
+
API_KEY, DELIVERY_TOKEN, ENVIRONMENT,
|
|
276
|
+
host='custom.cdn.example.com',
|
|
277
|
+
region=ContentstackRegion.EU
|
|
278
|
+
)
|
|
279
|
+
self.assertEqual('custom.cdn.example.com', stack.host)
|
|
280
|
+
|
|
281
|
+
def test_stack_endpoint_built_from_resolved_host(self):
|
|
282
|
+
stack = Stack(API_KEY, DELIVERY_TOKEN, ENVIRONMENT, region=ContentstackRegion.EU)
|
|
283
|
+
self.assertEqual('https://eu-cdn.contentstack.com/v3', stack.endpoint)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == '__main__':
|
|
287
|
+
unittest.main()
|
|
@@ -1,29 +0,0 @@
|
|
|
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 .https_connection import HTTPSConnection
|
|
10
|
-
from contentstack.stack import Stack
|
|
11
|
-
from .utility import Utils
|
|
12
|
-
|
|
13
|
-
__all__ = (
|
|
14
|
-
"Entry",
|
|
15
|
-
"Asset",
|
|
16
|
-
"ContentType",
|
|
17
|
-
"HTTPSConnection",
|
|
18
|
-
"Stack",
|
|
19
|
-
"Utils"
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
__title__ = 'contentstack-delivery-python'
|
|
23
|
-
__author__ = 'contentstack'
|
|
24
|
-
__status__ = 'debug'
|
|
25
|
-
__version__ = 'v2.5.0'
|
|
26
|
-
__endpoint__ = 'cdn.contentstack.io'
|
|
27
|
-
__email__ = 'support@contentstack.com'
|
|
28
|
-
__developer_email__ = 'mobile@contentstack.com'
|
|
29
|
-
__license__ = "MIT"
|
|
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
|
|
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
|