muninn 6.2__tar.gz → 6.3__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.
- {muninn-6.2 → muninn-6.3}/CHANGES +20 -0
- {muninn-6.2 → muninn-6.3}/PKG-INFO +1 -1
- {muninn-6.2 → muninn-6.3}/muninn/__init__.py +14 -1
- {muninn-6.2 → muninn-6.3}/muninn/archive.py +50 -30
- muninn-6.3/muninn/database/base.py +6 -0
- {muninn-6.2 → muninn-6.3}/muninn/database/postgresql.py +8 -4
- {muninn-6.2 → muninn-6.3}/muninn/database/sql.py +1 -0
- {muninn-6.2 → muninn-6.3}/muninn/database/sqlite.py +4 -1
- {muninn-6.2 → muninn-6.3}/muninn/remote.py +36 -23
- {muninn-6.2 → muninn-6.3}/muninn/storage/fs.py +1 -1
- {muninn-6.2 → muninn-6.3}/muninn/storage/s3.py +22 -15
- {muninn-6.2 → muninn-6.3}/muninn/storage/swift.py +16 -13
- muninn-6.3/muninn/tools/info.py +53 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/retrieve.py +2 -1
- {muninn-6.2 → muninn-6.3}/muninn/util.py +6 -2
- {muninn-6.2 → muninn-6.3}/muninn.egg-info/PKG-INFO +1 -1
- {muninn-6.2 → muninn-6.3}/muninn.egg-info/SOURCES.txt +1 -0
- {muninn-6.2 → muninn-6.3}/setup.py +1 -1
- {muninn-6.2 → muninn-6.3}/test/product_type.py.template +4 -1
- {muninn-6.2 → muninn-6.3}/test/test.py +118 -90
- muninn-6.2/muninn/tools/info.py +0 -45
- {muninn-6.2 → muninn-6.3}/DEVELOPER.md +0 -0
- {muninn-6.2 → muninn-6.3}/LICENSE +0 -0
- {muninn-6.2 → muninn-6.3}/MANIFEST.in +0 -0
- {muninn-6.2 → muninn-6.3}/README.md +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/_compat.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/config.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/core.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/database/__init__.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/database/blobgeometry.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/database/ewkb.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/enum.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/exceptions.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/extension.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/function.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/geometry.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/language.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/schema.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/storage/__init__.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/storage/base.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/struct.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/__init__.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/attach.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/destroy.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/export.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/hash.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/ingest.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/list_tags.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/prepare.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/pull.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/remove.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/search.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/strip.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/summary.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/tag.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/untag.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/update.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/tools/utils.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn/visitor.py +0 -0
- {muninn-6.2 → muninn-6.3}/muninn.egg-info/dependency_links.txt +0 -0
- {muninn-6.2 → muninn-6.3}/muninn.egg-info/entry_points.txt +0 -0
- {muninn-6.2 → muninn-6.3}/muninn.egg-info/top_level.txt +0 -0
- {muninn-6.2 → muninn-6.3}/setup.cfg +0 -0
- {muninn-6.2 → muninn-6.3}/test/coverage.sh +0 -0
- {muninn-6.2 → muninn-6.3}/test/data/README +0 -0
- {muninn-6.2 → muninn-6.3}/test/data/multi/1.txt +0 -0
- {muninn-6.2 → muninn-6.3}/test/data/multi/2.txt +0 -0
- {muninn-6.2 → muninn-6.3}/test/data/pi.txt +0 -0
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
6.3 2023-12-13
|
|
2
|
+
~~~~~~~~~~~~~~
|
|
3
|
+
|
|
4
|
+
* Added support for extension-specific configuration sections in archive
|
|
5
|
+
configuration files.
|
|
6
|
+
|
|
7
|
+
* Introduced `timeout` and `retries` options for the
|
|
8
|
+
`extension:muninn.remote` section in an archive configuration.
|
|
9
|
+
The number of retries now defaults back to 0.
|
|
10
|
+
|
|
11
|
+
* Added option to muninn-info to show all archives if no archive parameter
|
|
12
|
+
is provided.
|
|
13
|
+
|
|
14
|
+
* Added muninn.list_archives() function.
|
|
15
|
+
|
|
16
|
+
* Improved support for using muninn on Windows (e.g. '\' in paths are now
|
|
17
|
+
translated to '/' when stored as muninn metadata)
|
|
18
|
+
|
|
19
|
+
* Fixed issue where S3 bucket prefix object was created with one '/' too many.
|
|
20
|
+
|
|
1
21
|
6.2 2023-09-20
|
|
2
22
|
~~~~~~~~~~~~~~
|
|
3
23
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import absolute_import, division, print_function
|
|
6
6
|
|
|
7
|
-
__version__ = "6.
|
|
7
|
+
__version__ = "6.3"
|
|
8
8
|
__copyright__ = "Copyright (C) 2014-2023 S[&]T, The Netherlands."
|
|
9
9
|
|
|
10
10
|
__all__ = ["Error", "InternalError", "Struct", "Archive", "open", "config_path"]
|
|
@@ -43,6 +43,19 @@ def open(id=None, **kwargs):
|
|
|
43
43
|
|
|
44
44
|
return Archive.create(configuration, id)
|
|
45
45
|
|
|
46
|
+
def list_archives():
|
|
47
|
+
"""Search locations found in the 'MUNINN_CONFIG_PATH' environment variable
|
|
48
|
+
and return names of all found archives."""
|
|
49
|
+
|
|
50
|
+
result = []
|
|
51
|
+
for path in filter(None, config_path().split(":")):
|
|
52
|
+
if _os.path.isfile(path):
|
|
53
|
+
result.append(_os.path.basename(path)[:-4])
|
|
54
|
+
else:
|
|
55
|
+
result.extend([f[:-4] for f in _os.listdir(path) if f.endswith('.cfg')])
|
|
56
|
+
|
|
57
|
+
return result
|
|
58
|
+
|
|
46
59
|
|
|
47
60
|
def _read_archive_config_file(path):
|
|
48
61
|
try:
|
|
@@ -100,6 +100,34 @@ def _inspect_nargs(func):
|
|
|
100
100
|
return len(getargspec(func).args)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def _register_extension(archive, extension, name, plugin_type, list_name=None, get_name=None):
|
|
104
|
+
if list_name is None:
|
|
105
|
+
list_name = plugin_type + 's'
|
|
106
|
+
if get_name is None:
|
|
107
|
+
get_name = plugin_type
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
list_function = getattr(extension, list_name)
|
|
111
|
+
get_function = getattr(extension, get_name)
|
|
112
|
+
except AttributeError:
|
|
113
|
+
raise Error("extension %r does not implement the namespace extension API" % name)
|
|
114
|
+
|
|
115
|
+
config_section = archive._configuration.get('extension:' + extension.__name__)
|
|
116
|
+
|
|
117
|
+
list_args = []
|
|
118
|
+
if _inspect_nargs(list_function) == 1:
|
|
119
|
+
list_args.append(config_section)
|
|
120
|
+
|
|
121
|
+
for plugin_name in list_function(*list_args):
|
|
122
|
+
|
|
123
|
+
get_args = [plugin_name]
|
|
124
|
+
if _inspect_nargs(get_function) == 2:
|
|
125
|
+
get_args.append(config_section)
|
|
126
|
+
|
|
127
|
+
plugin = get_function(*get_args)
|
|
128
|
+
getattr(archive, 'register_' + plugin_type)(plugin_name, plugin)
|
|
129
|
+
|
|
130
|
+
|
|
103
131
|
_CORE_PROP_NAMES = [
|
|
104
132
|
'uuid',
|
|
105
133
|
'active',
|
|
@@ -151,58 +179,43 @@ class Archive(object):
|
|
|
151
179
|
product_type_extensions = options.pop("product_type_extensions", [])
|
|
152
180
|
remote_backend_extensions = options.pop("remote_backend_extensions", [])
|
|
153
181
|
hook_extensions = options.pop("hook_extensions", [])
|
|
154
|
-
archive = Archive(database=database, storage=storage, id=id,
|
|
182
|
+
archive = Archive(database=database, storage=storage, id=id,
|
|
183
|
+
configuration=configuration, **options)
|
|
155
184
|
|
|
156
185
|
# Register core namespace.
|
|
157
186
|
archive.register_namespace("core", Core)
|
|
158
187
|
|
|
159
|
-
# Register custom
|
|
188
|
+
# Register custom extensions.
|
|
160
189
|
for name in namespace_extensions:
|
|
161
190
|
extension = _load_extension(name)
|
|
162
|
-
|
|
163
|
-
for namespace in extension.namespaces():
|
|
164
|
-
archive.register_namespace(namespace, extension.namespace(namespace))
|
|
165
|
-
except AttributeError:
|
|
166
|
-
raise Error("extension %r does not implement the namespace extension API" % name)
|
|
191
|
+
_register_extension(archive, extension, name, 'namespace')
|
|
167
192
|
|
|
168
|
-
# Register product types.
|
|
169
193
|
for name in product_type_extensions:
|
|
170
194
|
extension = _load_extension(name)
|
|
171
|
-
|
|
172
|
-
for product_type in extension.product_types():
|
|
173
|
-
archive.register_product_type(product_type, extension.product_type_plugin(product_type))
|
|
174
|
-
except AttributeError:
|
|
175
|
-
raise Error("extension %r does not implement the product type extension API" % name)
|
|
195
|
+
_register_extension(archive, extension, name, 'product_type', get_name='product_type_plugin')
|
|
176
196
|
|
|
177
|
-
# Register custom remote backends.
|
|
178
197
|
for name in remote_backend_extensions:
|
|
179
198
|
extension = _load_extension(name)
|
|
180
|
-
|
|
181
|
-
for remote_backend in extension.remote_backends():
|
|
182
|
-
archive.register_remote_backend(remote_backend, extension.remote_backend(remote_backend))
|
|
183
|
-
except AttributeError:
|
|
184
|
-
raise Error("extension %r does not implement the remote backend extension API" % name)
|
|
199
|
+
_register_extension(archive, extension, name, 'remote_backend')
|
|
185
200
|
|
|
186
|
-
# Register hook extensions.
|
|
187
201
|
for name in hook_extensions:
|
|
188
202
|
extension = _load_extension(name)
|
|
189
|
-
|
|
190
|
-
for hook_extension in extension.hook_extensions():
|
|
191
|
-
archive.register_hook_extension(hook_extension, extension.hook_extension(hook_extension))
|
|
192
|
-
except AttributeError:
|
|
193
|
-
raise Error("extension %r does not implement the hook extension API" % name)
|
|
203
|
+
_register_extension(archive, extension, name, 'hook_extension')
|
|
194
204
|
|
|
195
205
|
return archive
|
|
196
206
|
|
|
197
207
|
def __init__(self, database, storage, cascade_grace_period=0,
|
|
198
|
-
max_cascade_cycles=25, auth_file=None, id=None, tempdir=None
|
|
208
|
+
max_cascade_cycles=25, auth_file=None, id=None, tempdir=None,
|
|
209
|
+
configuration=None):
|
|
210
|
+
self._configuration = configuration
|
|
211
|
+
|
|
199
212
|
self._cascade_grace_period = datetime.timedelta(minutes=cascade_grace_period)
|
|
200
213
|
self._max_cascade_cycles = max_cascade_cycles
|
|
201
214
|
self._auth_file = auth_file
|
|
202
215
|
|
|
203
216
|
self._namespace_schemas = {}
|
|
204
217
|
self._product_type_plugins = {}
|
|
205
|
-
self._remote_backend_plugins =
|
|
218
|
+
self._remote_backend_plugins = {}
|
|
206
219
|
self._hook_extensions = collections.OrderedDict()
|
|
207
220
|
|
|
208
221
|
self._export_formats = set()
|
|
@@ -214,6 +227,8 @@ class Archive(object):
|
|
|
214
227
|
self._tempdir = tempdir
|
|
215
228
|
self.id = id # Archive id (usually name of configuration file)
|
|
216
229
|
|
|
230
|
+
_register_extension(self, remote, 'muninn.remote', 'remote_backend')
|
|
231
|
+
|
|
217
232
|
def __enter__(self):
|
|
218
233
|
return self
|
|
219
234
|
|
|
@@ -311,6 +326,10 @@ class Archive(object):
|
|
|
311
326
|
if remote_backend in self._remote_backend_plugins:
|
|
312
327
|
raise Error("redefinition of remote backend: \"%s\"" % remote_backend)
|
|
313
328
|
|
|
329
|
+
for method in ['identify', 'pull']:
|
|
330
|
+
if not hasattr(plugin, method):
|
|
331
|
+
raise Error("missing '%s' method in plugin for remote backend \"%s\"" % (method, remote_backend))
|
|
332
|
+
|
|
314
333
|
self._remote_backend_plugins[remote_backend] = plugin
|
|
315
334
|
|
|
316
335
|
def remote_backend(self, remote_backend):
|
|
@@ -394,7 +413,7 @@ class Archive(object):
|
|
|
394
413
|
if len(set(basenames)) < len(basenames):
|
|
395
414
|
raise Error("basename of each part should be unique for multi-part products")
|
|
396
415
|
|
|
397
|
-
return paths
|
|
416
|
+
return [path.replace('\\', '/') for path in paths]
|
|
398
417
|
|
|
399
418
|
def _extract_hash_type(self, hash_value):
|
|
400
419
|
prefix, middle, _ = hash_value.partition(':')
|
|
@@ -1097,7 +1116,7 @@ class Archive(object):
|
|
|
1097
1116
|
ingest_path = os.path.dirname(ingest_path)
|
|
1098
1117
|
current_path = self.root()
|
|
1099
1118
|
if existing.core.archive_path:
|
|
1100
|
-
current_path =
|
|
1119
|
+
current_path = util.fwd_join(current_path, existing.core.archive_path)
|
|
1101
1120
|
if existing.core.archive_path != properties.core.archive_path:
|
|
1102
1121
|
raise Error('cannot force ingest because of archive_path mismatch')
|
|
1103
1122
|
if ingest_path == current_path:
|
|
@@ -1132,6 +1151,7 @@ class Archive(object):
|
|
|
1132
1151
|
properties.core.remote_url = 'file://' + os.path.realpath(paths[0])
|
|
1133
1152
|
else:
|
|
1134
1153
|
properties.core.remote_url = 'file://' + os.path.realpath(os.path.dirname(paths[0])) # TODO test
|
|
1154
|
+
properties.core.remote_url = properties.core.remote_url.replace('\\', '/')
|
|
1135
1155
|
self.update_properties(Struct({'core': {'remote_url': properties.core.remote_url}}),
|
|
1136
1156
|
properties.core.uuid)
|
|
1137
1157
|
|
|
@@ -1533,7 +1553,7 @@ class Archive(object):
|
|
|
1533
1553
|
def root(self):
|
|
1534
1554
|
"""Return the archive root path."""
|
|
1535
1555
|
if self._storage is not None:
|
|
1536
|
-
return self._storage._root
|
|
1556
|
+
return self._storage._root.replace('\\', '/')
|
|
1537
1557
|
|
|
1538
1558
|
def search(self, where="", order_by=[], limit=None, parameters={}, namespaces=[], property_names=[]):
|
|
1539
1559
|
"""Search the product catalogue for products matching the specified search expression.
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import absolute_import, division, print_function
|
|
6
6
|
|
|
7
|
-
from muninn._compat import dictkeys, dictvalues, is_python2_unicode
|
|
8
|
-
|
|
9
7
|
import re
|
|
10
8
|
import functools
|
|
11
9
|
import inspect
|
|
@@ -24,6 +22,8 @@ try:
|
|
|
24
22
|
except ImportError:
|
|
25
23
|
pass
|
|
26
24
|
|
|
25
|
+
from .base import DatabaseBackend
|
|
26
|
+
|
|
27
27
|
import muninn.config as config
|
|
28
28
|
import muninn.database.sql as sql
|
|
29
29
|
import muninn.database.ewkb as ewkb
|
|
@@ -34,6 +34,8 @@ from muninn.function import Prototype
|
|
|
34
34
|
from muninn.schema import *
|
|
35
35
|
from muninn.struct import Struct
|
|
36
36
|
|
|
37
|
+
from muninn._compat import dictkeys, dictvalues, is_python2_unicode
|
|
38
|
+
|
|
37
39
|
PG_UNIQUE_VIOLATION = '23505'
|
|
38
40
|
|
|
39
41
|
|
|
@@ -74,6 +76,8 @@ def geometry_send_hex(geometry):
|
|
|
74
76
|
|
|
75
77
|
def _connect_pg8000(connection_string):
|
|
76
78
|
kwargs = dict(arg.split('=') for arg in connection_string.split(' '))
|
|
79
|
+
if 'dbname' in kwargs:
|
|
80
|
+
kwargs['database'] = kwargs.pop('dbname')
|
|
77
81
|
_connection = pg8000.connect(**kwargs)
|
|
78
82
|
|
|
79
83
|
geography_oid = _get_db_type_id(_connection, "geography")
|
|
@@ -282,7 +286,7 @@ class PostgresqlConnection(object):
|
|
|
282
286
|
return self._connection.encoding
|
|
283
287
|
|
|
284
288
|
|
|
285
|
-
class PostgresqlBackend(
|
|
289
|
+
class PostgresqlBackend(DatabaseBackend):
|
|
286
290
|
def __init__(self, connection_string="", table_prefix="", library="psycopg2"):
|
|
287
291
|
self._connection = PostgresqlConnection(connection_string, library)
|
|
288
292
|
self._library = library
|
|
@@ -330,7 +334,7 @@ class PostgresqlBackend(object):
|
|
|
330
334
|
result.append("ALTER TABLE %s ADD COLUMN uuid UUID PRIMARY KEY;" % self._table_name(namespace))
|
|
331
335
|
result.append("ALTER TABLE %s ADD CONSTRAINT %s_uuid_fkey FOREIGN KEY (uuid) REFERENCES %s (uuid) ON "
|
|
332
336
|
"DELETE CASCADE;" % (self._table_name(namespace), self._table_name(namespace),
|
|
333
|
-
|
|
337
|
+
self._core_table_name))
|
|
334
338
|
|
|
335
339
|
schema = self._namespace_schema(namespace)
|
|
336
340
|
for name in schema:
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import absolute_import, division, print_function
|
|
6
6
|
|
|
7
|
-
from muninn._compat import dictkeys, dictvalues
|
|
8
7
|
import os
|
|
9
8
|
import re
|
|
10
9
|
import datetime
|
|
@@ -12,6 +11,10 @@ import functools
|
|
|
12
11
|
import json
|
|
13
12
|
import uuid
|
|
14
13
|
|
|
14
|
+
from .base import DatabaseBackend
|
|
15
|
+
|
|
16
|
+
from muninn._compat import dictkeys, dictvalues
|
|
17
|
+
|
|
15
18
|
# Select a version of dbapi2 that's available.
|
|
16
19
|
# Since we need the spatialite module, we need a version of sqlite3 that supports extensions.
|
|
17
20
|
# The defaulft version of sqlite3 that comes with Python does not support this, so we prefer pysqlite2
|
|
@@ -34,7 +34,7 @@ def get_credentials(archive, url):
|
|
|
34
34
|
return None
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def download_http_oath2(url, target_dir, credentials, timeout
|
|
37
|
+
def download_http_oath2(url, target_dir, credentials, timeout, retries):
|
|
38
38
|
import requests
|
|
39
39
|
from requests_oauthlib import OAuth2Session
|
|
40
40
|
from oauthlib.oauth2 import LegacyApplicationClient
|
|
@@ -46,7 +46,6 @@ def download_http_oath2(url, target_dir, credentials, timeout=60):
|
|
|
46
46
|
password=credentials['password'], client_id=credentials['client_id'],
|
|
47
47
|
client_secret=credentials['client_secret'])
|
|
48
48
|
try:
|
|
49
|
-
retry = 1
|
|
50
49
|
while True:
|
|
51
50
|
try:
|
|
52
51
|
r = session.get(url, timeout=timeout, stream=True)
|
|
@@ -60,9 +59,9 @@ def download_http_oath2(url, target_dir, credentials, timeout=60):
|
|
|
60
59
|
for block in r.iter_content(1048576): # use 1MB blocks
|
|
61
60
|
output.write(block)
|
|
62
61
|
except requests.exceptions.ReadTimeout:
|
|
63
|
-
if
|
|
62
|
+
if retries <= 0:
|
|
64
63
|
raise
|
|
65
|
-
|
|
64
|
+
retries -= 1
|
|
66
65
|
else:
|
|
67
66
|
break
|
|
68
67
|
except Exception as e:
|
|
@@ -70,13 +69,12 @@ def download_http_oath2(url, target_dir, credentials, timeout=60):
|
|
|
70
69
|
return local_file
|
|
71
70
|
|
|
72
71
|
|
|
73
|
-
def download_http(url, target_dir, credentials
|
|
72
|
+
def download_http(url, target_dir, credentials, timeout, retries):
|
|
74
73
|
import requests
|
|
75
74
|
auth = None
|
|
76
75
|
if credentials is not None:
|
|
77
76
|
auth = (credentials['username'], credentials['password'])
|
|
78
77
|
try:
|
|
79
|
-
retry = 1
|
|
80
78
|
while True:
|
|
81
79
|
try:
|
|
82
80
|
r = requests.get(url, timeout=timeout, stream=True, auth=auth)
|
|
@@ -90,9 +88,9 @@ def download_http(url, target_dir, credentials=None, timeout=60):
|
|
|
90
88
|
for block in r.iter_content(1048576): # use 1MB blocks
|
|
91
89
|
output.write(block)
|
|
92
90
|
except requests.exceptions.ReadTimeout:
|
|
93
|
-
if
|
|
91
|
+
if retries <= 0:
|
|
94
92
|
raise
|
|
95
|
-
|
|
93
|
+
retries -= 1
|
|
96
94
|
else:
|
|
97
95
|
break
|
|
98
96
|
except Exception as e:
|
|
@@ -100,7 +98,7 @@ def download_http(url, target_dir, credentials=None, timeout=60):
|
|
|
100
98
|
return local_file
|
|
101
99
|
|
|
102
100
|
|
|
103
|
-
def download_ftp(url, target_dir, credentials
|
|
101
|
+
def download_ftp(url, target_dir, credentials, timeout):
|
|
104
102
|
url = urlparse(url)
|
|
105
103
|
if url.username:
|
|
106
104
|
username = url.username
|
|
@@ -176,7 +174,7 @@ def download_s3(url, target_dir, credentials=None):
|
|
|
176
174
|
return paths
|
|
177
175
|
|
|
178
176
|
|
|
179
|
-
def download_sftp(url, target_dir, credentials
|
|
177
|
+
def download_sftp(url, target_dir, credentials, timeout):
|
|
180
178
|
import paramiko
|
|
181
179
|
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
|
182
180
|
url = urlparse(url)
|
|
@@ -203,10 +201,15 @@ def download_sftp(url, target_dir, credentials=None, timeout=60):
|
|
|
203
201
|
|
|
204
202
|
|
|
205
203
|
class RemoteBackend(object):
|
|
206
|
-
def __init__(self, prefix):
|
|
204
|
+
def __init__(self, prefix, config=None):
|
|
207
205
|
self.prefix = prefix
|
|
206
|
+
self.timeout = 60
|
|
207
|
+
self.retries = 0
|
|
208
|
+
if config is not None:
|
|
209
|
+
self.timeout = int(config.get('timeout', self.timeout))
|
|
210
|
+
self.retries = int(config.get('retries', self.retries))
|
|
208
211
|
|
|
209
|
-
def
|
|
212
|
+
def identify(self, url):
|
|
210
213
|
result = False
|
|
211
214
|
if self.prefix:
|
|
212
215
|
result = url.startswith(self.prefix)
|
|
@@ -260,16 +263,17 @@ class HTTPBackend(RemoteBackend):
|
|
|
260
263
|
def pull(self, archive, product, target_dir):
|
|
261
264
|
credentials = get_credentials(archive, product.core.remote_url)
|
|
262
265
|
if credentials and 'auth_type' in credentials and credentials['auth_type'] == "oauth2":
|
|
263
|
-
file_path = download_http_oath2(product.core.remote_url, target_dir, credentials
|
|
266
|
+
file_path = download_http_oath2(product.core.remote_url, target_dir, credentials, self.timeout,
|
|
267
|
+
self.retries)
|
|
264
268
|
else:
|
|
265
|
-
file_path = download_http(product.core.remote_url, target_dir, credentials
|
|
269
|
+
file_path = download_http(product.core.remote_url, target_dir, credentials, self.timeout, self.retries)
|
|
266
270
|
return self.auto_extract(file_path, product)
|
|
267
271
|
|
|
268
272
|
|
|
269
273
|
class FTPBackend(RemoteBackend):
|
|
270
274
|
def pull(self, archive, product, target_dir):
|
|
271
275
|
credentials = get_credentials(archive, product.core.remote_url)
|
|
272
|
-
file_path = download_ftp(product.core.remote_url, target_dir, credentials
|
|
276
|
+
file_path = download_ftp(product.core.remote_url, target_dir, credentials, self.timeout)
|
|
273
277
|
return self.auto_extract(file_path, product)
|
|
274
278
|
|
|
275
279
|
|
|
@@ -285,17 +289,17 @@ class S3Backend(RemoteBackend):
|
|
|
285
289
|
class SFTPBackend(RemoteBackend):
|
|
286
290
|
def pull(self, archive, product, target_dir):
|
|
287
291
|
credentials = get_credentials(archive, product.core.remote_url)
|
|
288
|
-
file_path = download_sftp(product.core.remote_url, target_dir, credentials
|
|
292
|
+
file_path = download_sftp(product.core.remote_url, target_dir, credentials, self.timeout)
|
|
289
293
|
return self.auto_extract(file_path, product)
|
|
290
294
|
|
|
291
295
|
|
|
292
296
|
REMOTE_BACKENDS = {
|
|
293
|
-
'http':
|
|
294
|
-
'https': HTTPBackend
|
|
295
|
-
'file':
|
|
296
|
-
'ftp':
|
|
297
|
-
's3':
|
|
298
|
-
'sftp':
|
|
297
|
+
'http': (HTTPBackend, 'http://'),
|
|
298
|
+
'https': (HTTPBackend, 'https://'),
|
|
299
|
+
'file': (FileBackend, 'file://'),
|
|
300
|
+
'ftp': (FTPBackend, 'ftp://'),
|
|
301
|
+
's3': (S3Backend, 's3://'),
|
|
302
|
+
'sftp': (SFTPBackend, 'sftp://'),
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
|
|
@@ -308,7 +312,7 @@ def retrieve_function(archive, product, verify_hash_download):
|
|
|
308
312
|
url = product.core.remote_url
|
|
309
313
|
for prot in archive.remote_backends():
|
|
310
314
|
_backend = archive.remote_backend(prot)
|
|
311
|
-
if _backend.
|
|
315
|
+
if _backend.identify(url):
|
|
312
316
|
backend = _backend
|
|
313
317
|
if backend is None:
|
|
314
318
|
raise Error("The protocol of '%s' is not supported" % url)
|
|
@@ -331,3 +335,12 @@ def retrieve_function(archive, product, verify_hash_download):
|
|
|
331
335
|
return paths
|
|
332
336
|
|
|
333
337
|
return retrieve_files
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def remote_backends():
|
|
341
|
+
return list(REMOTE_BACKENDS)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def remote_backend(name, configuration):
|
|
345
|
+
backend, prefix = REMOTE_BACKENDS[name]
|
|
346
|
+
return backend(prefix, configuration)
|
|
@@ -152,7 +152,7 @@ class FilesystemStorageBackend(StorageBackend):
|
|
|
152
152
|
if use_enclosing_directory:
|
|
153
153
|
os.rename(tmp_path, abs_product_path)
|
|
154
154
|
else:
|
|
155
|
-
assert
|
|
155
|
+
assert len(paths) == 1 and os.path.basename(paths[0]) == physical_name
|
|
156
156
|
tmp_product_path = os.path.join(tmp_path, physical_name)
|
|
157
157
|
os.rename(tmp_product_path, abs_product_path)
|
|
158
158
|
anything_stored = True
|
|
@@ -86,7 +86,7 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
86
86
|
endpoint_url += ':%d' % port
|
|
87
87
|
elif port is not None:
|
|
88
88
|
endpoint_url += ':%d' % port
|
|
89
|
-
self.global_prefix =
|
|
89
|
+
self.global_prefix = util.fwd_join(endpoint_url, bucket, prefix)
|
|
90
90
|
|
|
91
91
|
self._root = bucket
|
|
92
92
|
|
|
@@ -150,6 +150,7 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
150
150
|
raise Error("S3 storage backend does not support ingesting already archived products")
|
|
151
151
|
|
|
152
152
|
def _upload_file(self, key, path):
|
|
153
|
+
key = key.replace('\\', '/')
|
|
153
154
|
obj = self._resource.Object(self.bucket, key)
|
|
154
155
|
if os.path.getsize(path) == 0: # upload_file can hang on empty files
|
|
155
156
|
self._resource.Object(self.bucket, key).put()
|
|
@@ -158,7 +159,10 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
158
159
|
|
|
159
160
|
def _create_dir(self, key):
|
|
160
161
|
# using put, as upload_file/upload_fileobj do not like the trailing slash
|
|
161
|
-
|
|
162
|
+
key = key.replace('\\', '/')
|
|
163
|
+
if not key.endswith('/'):
|
|
164
|
+
key = key + '/'
|
|
165
|
+
self._resource.Object(self.bucket, key).put()
|
|
162
166
|
|
|
163
167
|
def put(self, paths, properties, use_enclosing_directory, use_symlinks=None,
|
|
164
168
|
retrieve_files=None, run_for_product=None):
|
|
@@ -173,7 +177,7 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
173
177
|
physical_name = properties.core.physical_name
|
|
174
178
|
|
|
175
179
|
if not use_enclosing_directory and retrieve_files is None:
|
|
176
|
-
assert
|
|
180
|
+
assert len(paths) == 1 and os.path.basename(paths[0]) == physical_name
|
|
177
181
|
|
|
178
182
|
tmp_root = self.get_tmp_root(properties)
|
|
179
183
|
with util.TemporaryDirectory(dir=tmp_root, prefix=".put-",
|
|
@@ -183,11 +187,11 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
183
187
|
|
|
184
188
|
# Upload file(s)
|
|
185
189
|
for path in paths:
|
|
186
|
-
key = self._prefix +
|
|
190
|
+
key = self._prefix + util.fwd_join(archive_path, physical_name)
|
|
187
191
|
|
|
188
192
|
# Add enclosing dir
|
|
189
193
|
if use_enclosing_directory:
|
|
190
|
-
key =
|
|
194
|
+
key = util.fwd_join(key, os.path.basename(path))
|
|
191
195
|
|
|
192
196
|
if os.path.isdir(path):
|
|
193
197
|
self._create_dir(key)
|
|
@@ -203,7 +207,7 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
203
207
|
|
|
204
208
|
for filename in files:
|
|
205
209
|
filekey = os.path.normpath(os.path.join(key, rel_root, filename))
|
|
206
|
-
filepath =
|
|
210
|
+
filepath = util.fwd_join(root, filename)
|
|
207
211
|
self._upload_file(filekey, filepath)
|
|
208
212
|
anything_stored = True
|
|
209
213
|
|
|
@@ -218,13 +222,13 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
218
222
|
raise StorageError(e, anything_stored)
|
|
219
223
|
|
|
220
224
|
def get(self, product, target_path, use_enclosing_directory, use_symlinks=None):
|
|
221
|
-
paths =
|
|
225
|
+
paths = set()
|
|
222
226
|
|
|
223
227
|
if use_symlinks:
|
|
224
228
|
raise Error("S3 storage backend does not support symlinks")
|
|
225
229
|
|
|
226
230
|
archive_path = product.core.archive_path
|
|
227
|
-
product_path =
|
|
231
|
+
product_path = util.fwd_join(archive_path, product.core.physical_name)
|
|
228
232
|
prefix = self._prefix + product_path
|
|
229
233
|
|
|
230
234
|
objs = list(self._resource.Bucket(self.bucket).objects.filter(Prefix=prefix))
|
|
@@ -232,12 +236,11 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
232
236
|
raise Error("no data for product '%s' (%s)" % (product.core.product_name, product.core.uuid))
|
|
233
237
|
|
|
234
238
|
for obj in objs:
|
|
235
|
-
rel_path = os.path.relpath(obj.key, self._prefix + archive_path)
|
|
239
|
+
rel_path = os.path.relpath(obj.key, self._prefix + archive_path).replace('\\', '/')
|
|
236
240
|
if use_enclosing_directory:
|
|
237
241
|
rel_path = '/'.join(rel_path.split('/')[1:])
|
|
242
|
+
paths.add(os.path.normpath(os.path.join(target_path, rel_path.split('/')[0])))
|
|
238
243
|
target = os.path.normpath(os.path.join(target_path, rel_path))
|
|
239
|
-
if os.path.dirname(rel_path) == '':
|
|
240
|
-
paths.append(target)
|
|
241
244
|
|
|
242
245
|
if obj.key.endswith('/'):
|
|
243
246
|
util.make_path(target)
|
|
@@ -248,16 +251,18 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
248
251
|
self._resource.Object(self.bucket, obj.key).download_file(target, ExtraArgs=self._download_args,
|
|
249
252
|
Config=self._transfer_config)
|
|
250
253
|
|
|
251
|
-
return paths
|
|
254
|
+
return list(paths)
|
|
252
255
|
|
|
253
256
|
def delete(self, product_path, properties):
|
|
254
257
|
prefix = self._prefix + product_path
|
|
258
|
+
prefix = prefix.replace('\\', '/')
|
|
255
259
|
for obj in self._resource.Bucket(self.bucket).objects.filter(Prefix=prefix):
|
|
256
260
|
obj.delete()
|
|
257
261
|
|
|
258
262
|
def size(self, product_path):
|
|
259
263
|
total = 0
|
|
260
264
|
prefix = self._prefix + product_path
|
|
265
|
+
prefix = prefix.replace('\\', '/')
|
|
261
266
|
for obj in self._resource.Bucket(self.bucket).objects.filter(Prefix=prefix):
|
|
262
267
|
total += obj.size
|
|
263
268
|
return total
|
|
@@ -267,8 +272,8 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
267
272
|
if product.core.archive_path == archive_path:
|
|
268
273
|
return paths
|
|
269
274
|
|
|
270
|
-
product_path =
|
|
271
|
-
new_product_path =
|
|
275
|
+
product_path = util.fwd_join(self._prefix, product.core.archive_path, product.core.physical_name)
|
|
276
|
+
new_product_path = util.fwd_join(self._prefix, archive_path, product.core.physical_name)
|
|
272
277
|
|
|
273
278
|
objs = list(self._resource.Bucket(self.bucket).objects.filter(Prefix=product_path))
|
|
274
279
|
if not objs:
|
|
@@ -276,11 +281,13 @@ class S3StorageBackend(StorageBackend): # TODO '/' in keys to indicate director
|
|
|
276
281
|
|
|
277
282
|
for obj in objs:
|
|
278
283
|
new_key = os.path.normpath(os.path.join(new_product_path, os.path.relpath(obj.key, product_path)))
|
|
284
|
+
new_key = new_key.replace('\\', '/')
|
|
279
285
|
if obj.key.endswith('/'):
|
|
280
286
|
self._create_dir(new_key)
|
|
281
287
|
else:
|
|
282
288
|
self._resource.Object(self.bucket, new_key).copy(CopySource={'Bucket': self.bucket, 'Key': obj.key},
|
|
283
|
-
|
|
289
|
+
ExtraArgs=self._copy_args,
|
|
290
|
+
Config=self._transfer_config)
|
|
284
291
|
self._resource.Object(self.bucket, obj.key).delete()
|
|
285
292
|
|
|
286
293
|
return paths
|