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.
Files changed (68) hide show
  1. {muninn-6.2 → muninn-6.3}/CHANGES +20 -0
  2. {muninn-6.2 → muninn-6.3}/PKG-INFO +1 -1
  3. {muninn-6.2 → muninn-6.3}/muninn/__init__.py +14 -1
  4. {muninn-6.2 → muninn-6.3}/muninn/archive.py +50 -30
  5. muninn-6.3/muninn/database/base.py +6 -0
  6. {muninn-6.2 → muninn-6.3}/muninn/database/postgresql.py +8 -4
  7. {muninn-6.2 → muninn-6.3}/muninn/database/sql.py +1 -0
  8. {muninn-6.2 → muninn-6.3}/muninn/database/sqlite.py +4 -1
  9. {muninn-6.2 → muninn-6.3}/muninn/remote.py +36 -23
  10. {muninn-6.2 → muninn-6.3}/muninn/storage/fs.py +1 -1
  11. {muninn-6.2 → muninn-6.3}/muninn/storage/s3.py +22 -15
  12. {muninn-6.2 → muninn-6.3}/muninn/storage/swift.py +16 -13
  13. muninn-6.3/muninn/tools/info.py +53 -0
  14. {muninn-6.2 → muninn-6.3}/muninn/tools/retrieve.py +2 -1
  15. {muninn-6.2 → muninn-6.3}/muninn/util.py +6 -2
  16. {muninn-6.2 → muninn-6.3}/muninn.egg-info/PKG-INFO +1 -1
  17. {muninn-6.2 → muninn-6.3}/muninn.egg-info/SOURCES.txt +1 -0
  18. {muninn-6.2 → muninn-6.3}/setup.py +1 -1
  19. {muninn-6.2 → muninn-6.3}/test/product_type.py.template +4 -1
  20. {muninn-6.2 → muninn-6.3}/test/test.py +118 -90
  21. muninn-6.2/muninn/tools/info.py +0 -45
  22. {muninn-6.2 → muninn-6.3}/DEVELOPER.md +0 -0
  23. {muninn-6.2 → muninn-6.3}/LICENSE +0 -0
  24. {muninn-6.2 → muninn-6.3}/MANIFEST.in +0 -0
  25. {muninn-6.2 → muninn-6.3}/README.md +0 -0
  26. {muninn-6.2 → muninn-6.3}/muninn/_compat.py +0 -0
  27. {muninn-6.2 → muninn-6.3}/muninn/config.py +0 -0
  28. {muninn-6.2 → muninn-6.3}/muninn/core.py +0 -0
  29. {muninn-6.2 → muninn-6.3}/muninn/database/__init__.py +0 -0
  30. {muninn-6.2 → muninn-6.3}/muninn/database/blobgeometry.py +0 -0
  31. {muninn-6.2 → muninn-6.3}/muninn/database/ewkb.py +0 -0
  32. {muninn-6.2 → muninn-6.3}/muninn/enum.py +0 -0
  33. {muninn-6.2 → muninn-6.3}/muninn/exceptions.py +0 -0
  34. {muninn-6.2 → muninn-6.3}/muninn/extension.py +0 -0
  35. {muninn-6.2 → muninn-6.3}/muninn/function.py +0 -0
  36. {muninn-6.2 → muninn-6.3}/muninn/geometry.py +0 -0
  37. {muninn-6.2 → muninn-6.3}/muninn/language.py +0 -0
  38. {muninn-6.2 → muninn-6.3}/muninn/schema.py +0 -0
  39. {muninn-6.2 → muninn-6.3}/muninn/storage/__init__.py +0 -0
  40. {muninn-6.2 → muninn-6.3}/muninn/storage/base.py +0 -0
  41. {muninn-6.2 → muninn-6.3}/muninn/struct.py +0 -0
  42. {muninn-6.2 → muninn-6.3}/muninn/tools/__init__.py +0 -0
  43. {muninn-6.2 → muninn-6.3}/muninn/tools/attach.py +0 -0
  44. {muninn-6.2 → muninn-6.3}/muninn/tools/destroy.py +0 -0
  45. {muninn-6.2 → muninn-6.3}/muninn/tools/export.py +0 -0
  46. {muninn-6.2 → muninn-6.3}/muninn/tools/hash.py +0 -0
  47. {muninn-6.2 → muninn-6.3}/muninn/tools/ingest.py +0 -0
  48. {muninn-6.2 → muninn-6.3}/muninn/tools/list_tags.py +0 -0
  49. {muninn-6.2 → muninn-6.3}/muninn/tools/prepare.py +0 -0
  50. {muninn-6.2 → muninn-6.3}/muninn/tools/pull.py +0 -0
  51. {muninn-6.2 → muninn-6.3}/muninn/tools/remove.py +0 -0
  52. {muninn-6.2 → muninn-6.3}/muninn/tools/search.py +0 -0
  53. {muninn-6.2 → muninn-6.3}/muninn/tools/strip.py +0 -0
  54. {muninn-6.2 → muninn-6.3}/muninn/tools/summary.py +0 -0
  55. {muninn-6.2 → muninn-6.3}/muninn/tools/tag.py +0 -0
  56. {muninn-6.2 → muninn-6.3}/muninn/tools/untag.py +0 -0
  57. {muninn-6.2 → muninn-6.3}/muninn/tools/update.py +0 -0
  58. {muninn-6.2 → muninn-6.3}/muninn/tools/utils.py +0 -0
  59. {muninn-6.2 → muninn-6.3}/muninn/visitor.py +0 -0
  60. {muninn-6.2 → muninn-6.3}/muninn.egg-info/dependency_links.txt +0 -0
  61. {muninn-6.2 → muninn-6.3}/muninn.egg-info/entry_points.txt +0 -0
  62. {muninn-6.2 → muninn-6.3}/muninn.egg-info/top_level.txt +0 -0
  63. {muninn-6.2 → muninn-6.3}/setup.cfg +0 -0
  64. {muninn-6.2 → muninn-6.3}/test/coverage.sh +0 -0
  65. {muninn-6.2 → muninn-6.3}/test/data/README +0 -0
  66. {muninn-6.2 → muninn-6.3}/test/data/multi/1.txt +0 -0
  67. {muninn-6.2 → muninn-6.3}/test/data/multi/2.txt +0 -0
  68. {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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.2
2
2
  Name: muninn
3
- Version: 6.2
3
+ Version: 6.3
4
4
  Summary: Data product catalogue and archive system
5
5
  Home-page: https://github.com/stcorp/muninn
6
6
  Author: S[&]T
@@ -4,7 +4,7 @@
4
4
 
5
5
  from __future__ import absolute_import, division, print_function
6
6
 
7
- __version__ = "6.2"
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, **options)
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 namespaces.
188
+ # Register custom extensions.
160
189
  for name in namespace_extensions:
161
190
  extension = _load_extension(name)
162
- try:
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
- try:
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
- try:
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
- try:
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 = copy.copy(remote.REMOTE_BACKENDS)
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 = os.path.join(current_path, existing.core.archive_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.
@@ -0,0 +1,6 @@
1
+ #
2
+ # Copyright (C) 2014-2023 S[&]T, The Netherlands.
3
+ #
4
+
5
+ class DatabaseBackend(object):
6
+ pass
@@ -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(object):
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
- self._core_table_name))
337
+ self._core_table_name))
334
338
 
335
339
  schema = self._namespace_schema(namespace)
336
340
  for name in schema:
@@ -455,6 +455,7 @@ class SQLBuilder(object):
455
455
 
456
456
  join_set = set()
457
457
  result_fields = []
458
+
458
459
  for field in group_by + ['count'] + aggregates:
459
460
  ident = Identifier(field, self._namespace_schemas)
460
461
  result_fields.append(ident.resolve)
@@ -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=60):
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 retry <= 0:
62
+ if retries <= 0:
64
63
  raise
65
- retry -= 1
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=None, timeout=60):
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 retry <= 0:
91
+ if retries <= 0:
94
92
  raise
95
- retry -= 1
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=None, timeout=60):
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=None, timeout=60):
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 indentify(self, url):
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=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=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=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': HTTPBackend(prefix='http://'),
294
- 'https': HTTPBackend(prefix='https://'),
295
- 'file': FileBackend(prefix='file://'),
296
- 'ftp': FTPBackend(prefix='ftp://'),
297
- 's3': S3Backend(prefix='s3://'),
298
- 'sftp': SFTPBackend(prefix='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.indentify(url):
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(len(paths) == 1 and os.path.basename(paths[0]) == physical_name)
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 = os.path.join(endpoint_url, bucket, 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
- self._resource.Object(self.bucket, key+'/').put()
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(len(paths) == 1 and os.path.basename(paths[0]) == physical_name)
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 + os.path.join(archive_path, physical_name)
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 = os.path.join(key, os.path.basename(path))
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 = os.path.join(root, filename)
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 = os.path.join(archive_path, product.core.physical_name)
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 = os.path.join(self._prefix, product.core.archive_path, product.core.physical_name)
271
- new_product_path = os.path.join(self._prefix, archive_path, product.core.physical_name)
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
- ExtraArgs=self._copy_args, Config=self._transfer_config)
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