vortex-nwp 2.0.0b1__py3-none-any.whl → 2.1.0__py3-none-any.whl

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 (141) hide show
  1. vortex/__init__.py +75 -47
  2. vortex/algo/__init__.py +3 -2
  3. vortex/algo/components.py +944 -618
  4. vortex/algo/mpitools.py +802 -497
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/serversynctools.py +34 -33
  7. vortex/config.py +19 -22
  8. vortex/data/__init__.py +9 -3
  9. vortex/data/abstractstores.py +593 -655
  10. vortex/data/containers.py +217 -162
  11. vortex/data/contents.py +65 -39
  12. vortex/data/executables.py +93 -102
  13. vortex/data/flow.py +40 -34
  14. vortex/data/geometries.py +228 -132
  15. vortex/data/handlers.py +436 -227
  16. vortex/data/outflow.py +15 -15
  17. vortex/data/providers.py +185 -163
  18. vortex/data/resources.py +48 -42
  19. vortex/data/stores.py +540 -417
  20. vortex/data/sync_templates/__init__.py +0 -0
  21. vortex/gloves.py +114 -87
  22. vortex/layout/__init__.py +1 -8
  23. vortex/layout/contexts.py +150 -84
  24. vortex/layout/dataflow.py +353 -202
  25. vortex/layout/monitor.py +264 -128
  26. vortex/nwp/__init__.py +5 -2
  27. vortex/nwp/algo/__init__.py +14 -5
  28. vortex/nwp/algo/assim.py +205 -151
  29. vortex/nwp/algo/clim.py +683 -517
  30. vortex/nwp/algo/coupling.py +447 -225
  31. vortex/nwp/algo/eda.py +437 -229
  32. vortex/nwp/algo/eps.py +403 -231
  33. vortex/nwp/algo/forecasts.py +416 -275
  34. vortex/nwp/algo/fpserver.py +683 -307
  35. vortex/nwp/algo/ifsnaming.py +205 -145
  36. vortex/nwp/algo/ifsroot.py +215 -122
  37. vortex/nwp/algo/monitoring.py +137 -76
  38. vortex/nwp/algo/mpitools.py +330 -190
  39. vortex/nwp/algo/odbtools.py +637 -353
  40. vortex/nwp/algo/oopsroot.py +454 -273
  41. vortex/nwp/algo/oopstests.py +90 -56
  42. vortex/nwp/algo/request.py +287 -206
  43. vortex/nwp/algo/stdpost.py +878 -522
  44. vortex/nwp/data/__init__.py +22 -4
  45. vortex/nwp/data/assim.py +125 -137
  46. vortex/nwp/data/boundaries.py +121 -68
  47. vortex/nwp/data/climfiles.py +193 -211
  48. vortex/nwp/data/configfiles.py +73 -69
  49. vortex/nwp/data/consts.py +426 -401
  50. vortex/nwp/data/ctpini.py +59 -43
  51. vortex/nwp/data/diagnostics.py +94 -66
  52. vortex/nwp/data/eda.py +50 -51
  53. vortex/nwp/data/eps.py +195 -146
  54. vortex/nwp/data/executables.py +440 -434
  55. vortex/nwp/data/fields.py +63 -48
  56. vortex/nwp/data/gridfiles.py +183 -111
  57. vortex/nwp/data/logs.py +250 -217
  58. vortex/nwp/data/modelstates.py +180 -151
  59. vortex/nwp/data/monitoring.py +72 -99
  60. vortex/nwp/data/namelists.py +254 -202
  61. vortex/nwp/data/obs.py +400 -308
  62. vortex/nwp/data/oopsexec.py +22 -20
  63. vortex/nwp/data/providers.py +90 -65
  64. vortex/nwp/data/query.py +71 -82
  65. vortex/nwp/data/stores.py +49 -36
  66. vortex/nwp/data/surfex.py +136 -137
  67. vortex/nwp/syntax/__init__.py +1 -1
  68. vortex/nwp/syntax/stdattrs.py +173 -111
  69. vortex/nwp/tools/__init__.py +2 -2
  70. vortex/nwp/tools/addons.py +22 -17
  71. vortex/nwp/tools/agt.py +24 -12
  72. vortex/nwp/tools/bdap.py +16 -5
  73. vortex/nwp/tools/bdcp.py +4 -1
  74. vortex/nwp/tools/bdm.py +3 -0
  75. vortex/nwp/tools/bdmp.py +14 -9
  76. vortex/nwp/tools/conftools.py +728 -378
  77. vortex/nwp/tools/drhook.py +12 -8
  78. vortex/nwp/tools/grib.py +65 -39
  79. vortex/nwp/tools/gribdiff.py +22 -17
  80. vortex/nwp/tools/ifstools.py +82 -42
  81. vortex/nwp/tools/igastuff.py +167 -143
  82. vortex/nwp/tools/mars.py +14 -2
  83. vortex/nwp/tools/odb.py +234 -125
  84. vortex/nwp/tools/partitioning.py +61 -37
  85. vortex/nwp/tools/satrad.py +27 -12
  86. vortex/nwp/util/async.py +83 -55
  87. vortex/nwp/util/beacon.py +10 -10
  88. vortex/nwp/util/diffpygram.py +174 -86
  89. vortex/nwp/util/ens.py +144 -63
  90. vortex/nwp/util/hooks.py +30 -19
  91. vortex/nwp/util/taskdeco.py +28 -24
  92. vortex/nwp/util/usepygram.py +278 -172
  93. vortex/nwp/util/usetnt.py +31 -17
  94. vortex/sessions.py +72 -39
  95. vortex/syntax/__init__.py +1 -1
  96. vortex/syntax/stdattrs.py +410 -171
  97. vortex/syntax/stddeco.py +31 -22
  98. vortex/toolbox.py +327 -192
  99. vortex/tools/__init__.py +11 -2
  100. vortex/tools/actions.py +110 -121
  101. vortex/tools/addons.py +111 -92
  102. vortex/tools/arm.py +42 -22
  103. vortex/tools/compression.py +72 -69
  104. vortex/tools/date.py +11 -4
  105. vortex/tools/delayedactions.py +242 -132
  106. vortex/tools/env.py +75 -47
  107. vortex/tools/folder.py +342 -171
  108. vortex/tools/grib.py +341 -162
  109. vortex/tools/lfi.py +423 -216
  110. vortex/tools/listings.py +109 -40
  111. vortex/tools/names.py +218 -156
  112. vortex/tools/net.py +655 -299
  113. vortex/tools/parallelism.py +93 -61
  114. vortex/tools/prestaging.py +55 -31
  115. vortex/tools/schedulers.py +172 -105
  116. vortex/tools/services.py +403 -334
  117. vortex/tools/storage.py +293 -358
  118. vortex/tools/surfex.py +24 -24
  119. vortex/tools/systems.py +1234 -643
  120. vortex/tools/targets.py +156 -100
  121. vortex/util/__init__.py +1 -1
  122. vortex/util/config.py +378 -327
  123. vortex/util/empty.py +2 -2
  124. vortex/util/helpers.py +56 -24
  125. vortex/util/introspection.py +18 -12
  126. vortex/util/iosponge.py +8 -4
  127. vortex/util/roles.py +4 -6
  128. vortex/util/storefunctions.py +39 -13
  129. vortex/util/structs.py +3 -3
  130. vortex/util/worker.py +29 -17
  131. vortex_nwp-2.1.0.dist-info/METADATA +67 -0
  132. vortex_nwp-2.1.0.dist-info/RECORD +144 -0
  133. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/WHEEL +1 -1
  134. vortex/layout/appconf.py +0 -109
  135. vortex/layout/jobs.py +0 -1276
  136. vortex/layout/nodes.py +0 -1424
  137. vortex/layout/subjobs.py +0 -464
  138. vortex_nwp-2.0.0b1.dist-info/METADATA +0 -50
  139. vortex_nwp-2.0.0b1.dist-info/RECORD +0 -146
  140. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info/licenses}/LICENSE +0 -0
  141. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/top_level.txt +0 -0
vortex/tools/net.py CHANGED
@@ -26,6 +26,8 @@ from bronx.fancies import loggers
26
26
  from bronx.net.netrc import netrc
27
27
  from bronx.syntax.decorators import nicedeco, secure_getattr
28
28
 
29
+ from vortex.config import get_from_config_w_default, ConfigurationError
30
+
29
31
  #: No automatic export
30
32
  __all__ = []
31
33
 
@@ -44,28 +46,28 @@ def uriparse(uristring):
44
46
  * username
45
47
  * password
46
48
  """
47
- (realscheme, other) = uristring.split(':', 1)
48
- rp = urlparse.urlparse('http:' + other)
49
+ (realscheme, other) = uristring.split(":", 1)
50
+ rp = urlparse.urlparse("http:" + other)
49
51
  uridict = rp._asdict()
50
- netloc = uridict['netloc'].split('@', 1)
51
- hostport = netloc.pop().split(':')
52
- uridict['netloc'] = hostport.pop(0)
52
+ netloc = uridict["netloc"].split("@", 1)
53
+ hostport = netloc.pop().split(":")
54
+ uridict["netloc"] = hostport.pop(0)
53
55
  if hostport:
54
- uridict['port'] = hostport.pop()
56
+ uridict["port"] = hostport.pop()
55
57
  else:
56
- uridict['port'] = None
58
+ uridict["port"] = None
57
59
  if netloc:
58
- userpass = netloc.pop().split(':')
59
- uridict['username'] = userpass.pop(0)
60
+ userpass = netloc.pop().split(":")
61
+ uridict["username"] = userpass.pop(0)
60
62
  if userpass:
61
- uridict['password'] = userpass.pop()
63
+ uridict["password"] = userpass.pop()
62
64
  else:
63
- uridict['password'] = None
65
+ uridict["password"] = None
64
66
  else:
65
- uridict['username'] = None
66
- uridict['password'] = None
67
- uridict['scheme'] = realscheme
68
- uridict['query'] = urlparse.parse_qs(uridict['query'])
67
+ uridict["username"] = None
68
+ uridict["password"] = None
69
+ uridict["scheme"] = realscheme
70
+ uridict["query"] = urlparse.parse_qs(uridict["query"])
69
71
  return uridict
70
72
 
71
73
 
@@ -74,20 +76,24 @@ def uriunparse(uridesc):
74
76
  return urlparse.urlunparse(uridesc)
75
77
 
76
78
 
77
- def http_post_data(url, data, ok_statuses=(), proxies=None, headers=None, verify=None):
79
+ def http_post_data(
80
+ url, data, ok_statuses=(), proxies=None, headers=None, verify=None
81
+ ):
78
82
  """Make a http/https POST request, encoding **data**."""
79
83
  if isinstance(proxies, (list, tuple)):
80
- proxies = {scheme: proxies for scheme in ('http', 'https')}
84
+ proxies = {scheme: proxies for scheme in ("http", "https")}
81
85
  # Try to use the requests package
82
86
  try:
83
87
  import requests
88
+
84
89
  use_requests = True
85
90
  except ImportError:
86
91
  use_requests = False
87
92
  # The modern way
88
93
  if use_requests:
89
- resp = requests.post(url=url, data=data, headers=headers,
90
- proxies=proxies, verify=verify)
94
+ resp = requests.post(
95
+ url=url, data=data, headers=headers, proxies=proxies, verify=verify
96
+ )
91
97
  if ok_statuses:
92
98
  is_ok = resp.status_code in ok_statuses
93
99
  else:
@@ -95,16 +101,20 @@ def http_post_data(url, data, ok_statuses=(), proxies=None, headers=None, verify
95
101
  return is_ok, resp.status_code, resp.headers, resp.text
96
102
  else:
97
103
  if not isinstance(data, bytes):
98
- data = urlparse.urlencode(data).encode('utf-8')
99
- if uriparse(url)['scheme'] == 'https':
100
- raise RuntimeError('HTTPS is not properly supported by urllib.request ({}).'
101
- .format(url))
104
+ data = urlparse.urlencode(data).encode("utf-8")
105
+ if uriparse(url)["scheme"] == "https":
106
+ raise RuntimeError(
107
+ "HTTPS is not properly supported by urllib.request ({}).".format(
108
+ url
109
+ )
110
+ )
102
111
  handlers = []
103
112
  if isinstance(proxies, dict):
104
113
  handlers.append(urlrequest.ProxyHandler(proxies))
105
- opener = urlrequest.build_opener(* handlers)
106
- req = urlrequest.Request(url=url, data=data,
107
- headers={} if headers is None else headers)
114
+ opener = urlrequest.build_opener(*handlers)
115
+ req = urlrequest.Request(
116
+ url=url, data=data, headers={} if headers is None else headers
117
+ )
108
118
  try:
109
119
  req_f = opener.open(req)
110
120
  except Exception as e:
@@ -116,7 +126,7 @@ def http_post_data(url, data, ok_statuses=(), proxies=None, headers=None, verify
116
126
  try:
117
127
  req_rc = req_f.getcode()
118
128
  req_info = req_f.info()
119
- req_data = req_f.read().decode('utf-8')
129
+ req_data = req_f.read().decode("utf-8")
120
130
  if ok_statuses:
121
131
  return req_rc in ok_statuses, req_rc, req_info, req_data
122
132
  else:
@@ -146,21 +156,21 @@ def netrc_lookup(logname, hostname, nrcfile=None):
146
156
  auth = nrc.authenticators(hostname, login=logname)
147
157
  if not auth:
148
158
  # self.host may be a FQDN, try to guess only the hostname
149
- auth = nrc.authenticators(hostname.split('.')[0], login=logname)
159
+ auth = nrc.authenticators(hostname.split(".")[0], login=logname)
150
160
  # for backward compatibility: This might be removed one day
151
161
  if not auth:
152
162
  auth = nrc.authenticators(hostname)
153
163
  if not auth:
154
164
  # self.host may be a FQDN, try to guess only the hostname
155
- auth = nrc.authenticators(hostname.split('.')[0])
165
+ auth = nrc.authenticators(hostname.split(".")[0])
156
166
  # End of backward compatibility section
157
167
  if auth:
158
168
  actual_logname = auth[0]
159
169
  actual_pwd = auth[2]
160
170
  else:
161
- logger.warning('netrc lookup failed (%s)', str(auth))
171
+ logger.warning("netrc lookup failed (%s)", str(auth))
162
172
  else:
163
- logger.warning('unable to fetch .netrc file')
173
+ logger.warning("unable to fetch .netrc file")
164
174
  return actual_logname, actual_pwd
165
175
 
166
176
 
@@ -170,7 +180,7 @@ class ExtendedFtplib:
170
180
  It wraps the standard ftplib object to add or overwrite methods.
171
181
  """
172
182
 
173
- def __init__(self, system, ftpobj, hostname='', port=DEFAULT_FTP_PORT):
183
+ def __init__(self, system, ftpobj, hostname="", port=DEFAULT_FTP_PORT):
174
184
  """
175
185
  :param ~vortex.tools.systems.OSExtended system: The system object to work with
176
186
  :param ftplib.FTP ftpobj: The FTP object to work with / to extend
@@ -178,7 +188,7 @@ class ExtendedFtplib:
178
188
  self._system = system
179
189
  self._ftplib = ftpobj
180
190
  self._closed = True
181
- self._logname = 'not_logged_in'
191
+ self._logname = "not_logged_in"
182
192
  self._created = datetime.now()
183
193
  self._opened = None
184
194
  self._deleted = None
@@ -200,8 +210,8 @@ class ExtendedFtplib:
200
210
  Nicely formatted print, built as the concatenation
201
211
  of the class full name and `logname` and `length` attributes.
202
212
  """
203
- return '{:s} | host={:s} logname={:s} since={!s}>'.format(
204
- repr(self).rstrip('>'),
213
+ return "{:s} | host={:s} logname={:s} since={!s}>".format(
214
+ repr(self).rstrip(">"),
205
215
  self.host,
206
216
  self.logname,
207
217
  self.length,
@@ -212,10 +222,11 @@ class ExtendedFtplib:
212
222
  """Gateway to undefined method or attributes if present in ``_ftplib``."""
213
223
  actualattr = getattr(self._ftplib, key)
214
224
  if callable(actualattr):
225
+
215
226
  def osproxy(*args, **kw):
216
227
  cmd = [key]
217
228
  cmd.extend(args)
218
- cmd.extend(['{:s}={!s}'.format(x, kw[x]) for x in kw.keys()])
229
+ cmd.extend(["{:s}={!s}".format(x, kw[x]) for x in kw.keys()])
219
230
  self.stderr(*cmd)
220
231
  return actualattr(*args, **kw)
221
232
 
@@ -234,7 +245,7 @@ class ExtendedFtplib:
234
245
 
235
246
  def stderr(self, cmd, *args):
236
247
  """Proxy to local system's standard error."""
237
- self.system.stderr('ftp:' + cmd, *args)
248
+ self.system.stderr("ftp:" + cmd, *args)
238
249
 
239
250
  @property
240
251
  def closed(self):
@@ -254,12 +265,14 @@ class ExtendedFtplib:
254
265
  topnow = datetime.now() if self._deleted is None else self._deleted
255
266
  timelength = (topnow - self._opened).total_seconds()
256
267
  except TypeError:
257
- logger.warning('Could not evaluate connexion length %s', repr(self))
268
+ logger.warning(
269
+ "Could not evaluate connexion length %s", repr(self)
270
+ )
258
271
  return timelength
259
272
 
260
273
  def close(self):
261
274
  """Proxy to ftplib :meth:`ftplib.FTP.close`."""
262
- self.stderr('close')
275
+ self.stderr("close")
263
276
  rc = True
264
277
  if not self.closed:
265
278
  rc = self._ftplib.close() or True
@@ -269,7 +282,7 @@ class ExtendedFtplib:
269
282
 
270
283
  def login(self, *args):
271
284
  """Proxy to ftplib :meth:`ftplib.FTP.login`."""
272
- self.stderr('login', args[0])
285
+ self.stderr("login", args[0])
273
286
  self._logname = args[0]
274
287
  # kept for debugging, but this exposes the user's password!
275
288
  # logger.debug('FTP login <args:%s>', str(args))
@@ -279,48 +292,48 @@ class ExtendedFtplib:
279
292
  self._deleted = None
280
293
  self._opened = datetime.now()
281
294
  else:
282
- logger.warning('FTP could not login <args:%s>', str(args))
295
+ logger.warning("FTP could not login <args:%s>", str(args))
283
296
  return rc
284
297
 
285
298
  def list(self, *args):
286
299
  """Returns standard directory listing from ftp protocol."""
287
- self.stderr('list', *args)
300
+ self.stderr("list", *args)
288
301
  contents = []
289
- self.retrlines('LIST', callback=contents.append)
302
+ self.retrlines("LIST", callback=contents.append)
290
303
  return contents
291
304
 
292
305
  def dir(self, *args):
293
306
  """Proxy to ftplib :meth:`ftplib.FTP.dir`."""
294
- self.stderr('dir', *args)
307
+ self.stderr("dir", *args)
295
308
  return self._ftplib.dir(*args)
296
309
 
297
310
  def ls(self, *args):
298
311
  """Returns directory listing."""
299
- self.stderr('ls', *args)
312
+ self.stderr("ls", *args)
300
313
  return self.dir(*args)
301
314
 
302
315
  ll = ls
303
316
 
304
317
  def get(self, source, destination):
305
318
  """Retrieve a remote `destination` file to a local `source` file object."""
306
- self.stderr('get', source, destination)
319
+ self.stderr("get", source, destination)
307
320
  if isinstance(destination, str):
308
321
  self.system.filecocoon(destination)
309
- target = open(destination, 'wb')
322
+ target = open(destination, "wb")
310
323
  xdestination = True
311
324
  else:
312
325
  target = destination
313
326
  xdestination = False
314
- logger.info('FTP <get:{:s}>'.format(source))
327
+ logger.info("FTP <get:{:s}>".format(source))
315
328
  rc = False
316
329
  try:
317
- self.retrbinary('RETR ' + source, target.write)
330
+ self.retrbinary("RETR " + source, target.write)
318
331
  if xdestination:
319
332
  target.seek(0, io.SEEK_END)
320
333
  if self.size(source) == target.tell():
321
334
  rc = True
322
335
  else:
323
- logger.error('FTP incomplete get %s', repr(source))
336
+ logger.error("FTP incomplete get %s", repr(source))
324
337
  else:
325
338
  rc = True
326
339
  finally:
@@ -341,9 +354,9 @@ class ExtendedFtplib:
341
354
  When `exact` is True, the size is checked against the size of the
342
355
  destination, and a mismatch is considered a failure.
343
356
  """
344
- self.stderr('put', source, destination)
357
+ self.stderr("put", source, destination)
345
358
  if isinstance(source, str):
346
- inputsrc = open(source, 'rb')
359
+ inputsrc = open(source, "rb")
347
360
  xsource = True
348
361
  else:
349
362
  inputsrc = source
@@ -354,43 +367,60 @@ class ExtendedFtplib:
354
367
  exact = True
355
368
  inputsrc.seek(0)
356
369
  except AttributeError:
357
- logger.warning('Could not rewind <source:%s>', str(source))
370
+ logger.warning("Could not rewind <source:%s>", str(source))
358
371
  except OSError:
359
- logger.debug('Seek trouble <source:%s>', str(source))
372
+ logger.debug("Seek trouble <source:%s>", str(source))
360
373
 
361
374
  self.rmkdir(destination)
362
375
  try:
363
376
  self.delete(destination)
364
- logger.info('Replacing <file:%s>', str(destination))
377
+ logger.info("Replacing <file:%s>", str(destination))
365
378
  except ftplib.error_perm:
366
- logger.info('Creating <file:%s>', str(destination))
367
- except (ValueError, TypeError, OSError,
368
- ftplib.error_proto, ftplib.error_reply, ftplib.error_temp) as e:
369
- logger.error('Serious delete trouble <file:%s> <error:%s>',
370
- str(destination), str(e))
371
-
372
- logger.info('FTP <put:%s>', str(destination))
379
+ logger.info("Creating <file:%s>", str(destination))
380
+ except (
381
+ ValueError,
382
+ TypeError,
383
+ OSError,
384
+ ftplib.error_proto,
385
+ ftplib.error_reply,
386
+ ftplib.error_temp,
387
+ ) as e:
388
+ logger.error(
389
+ "Serious delete trouble <file:%s> <error:%s>",
390
+ str(destination),
391
+ str(e),
392
+ )
393
+
394
+ logger.info("FTP <put:%s>", str(destination))
373
395
  rc = False
374
396
 
375
397
  if size is not None:
376
398
  try:
377
- self.voidcmd('ALLO {:d}'.format(size))
399
+ self.voidcmd("ALLO {:d}".format(size))
378
400
  except ftplib.error_perm:
379
401
  pass
380
402
 
381
403
  try:
382
- self.storbinary('STOR ' + destination, inputsrc)
404
+ self.storbinary("STOR " + destination, inputsrc)
383
405
  if exact:
384
406
  if self.size(destination) == size:
385
407
  rc = True
386
408
  else:
387
- logger.error('FTP incomplete put %s (%d / %d bytes)', repr(source),
388
- self.size(destination), size)
409
+ logger.error(
410
+ "FTP incomplete put %s (%d / %d bytes)",
411
+ repr(source),
412
+ self.size(destination),
413
+ size,
414
+ )
389
415
  else:
390
416
  rc = True
391
417
  if self.size(destination) != size:
392
- logger.info('FTP put %s: estimated %s bytes, real %s bytes',
393
- repr(source), str(size), self.size(destination))
418
+ logger.info(
419
+ "FTP put %s: estimated %s bytes, real %s bytes",
420
+ repr(source),
421
+ str(size),
422
+ self.size(destination),
423
+ )
394
424
  finally:
395
425
  if xsource:
396
426
  inputsrc.close()
@@ -398,29 +428,29 @@ class ExtendedFtplib:
398
428
 
399
429
  def rmkdir(self, destination):
400
430
  """Recursive directory creation (mimics `mkdir -p`)."""
401
- self.stderr('rmkdir', destination)
431
+ self.stderr("rmkdir", destination)
402
432
  origin = self.pwd()
403
- if destination.startswith('/'):
404
- path_pre = '/'
405
- elif destination.startswith('~'):
406
- path_pre = ''
433
+ if destination.startswith("/"):
434
+ path_pre = "/"
435
+ elif destination.startswith("~"):
436
+ path_pre = ""
407
437
  else:
408
- path_pre = origin + '/'
438
+ path_pre = origin + "/"
409
439
 
410
- for subdir in self.system.path.dirname(destination).split('/'):
440
+ for subdir in self.system.path.dirname(destination).split("/"):
411
441
  current = path_pre + subdir
412
442
  try:
413
443
  self.cwd(current)
414
- path_pre = current + '/'
444
+ path_pre = current + "/"
415
445
  except ftplib.error_perm:
416
- self.stderr('mkdir', current)
446
+ self.stderr("mkdir", current)
417
447
  try:
418
448
  self.mkd(current)
419
449
  except ftplib.error_perm as errmkd:
420
- if 'File exists' not in str(errmkd):
450
+ if "File exists" not in str(errmkd):
421
451
  raise
422
452
  self.cwd(current)
423
- path_pre = current + '/'
453
+ path_pre = current + "/"
424
454
  self.cwd(origin)
425
455
 
426
456
  def cd(self, destination):
@@ -433,16 +463,16 @@ class ExtendedFtplib:
433
463
 
434
464
  def mtime(self, filename):
435
465
  """Retrieve the modification time of a file."""
436
- resp = self.sendcmd('MDTM ' + filename)
437
- if resp[:3] == '213':
466
+ resp = self.sendcmd("MDTM " + filename)
467
+ if resp[:3] == "213":
438
468
  s = resp[3:].strip().split()[-1]
439
469
  return int(s)
440
470
 
441
471
  def size(self, filename):
442
472
  """Retrieve the size of a file."""
443
473
  # The SIZE command is defined in RFC-3659
444
- resp = self.sendcmd('SIZE ' + filename)
445
- if resp[:3] == '213':
474
+ resp = self.sendcmd("SIZE " + filename)
475
+ if resp[:3] == "213":
446
476
  s = resp[3:].strip().split()[-1]
447
477
  return int(s)
448
478
 
@@ -465,11 +495,23 @@ class StdFtp:
465
495
  :class:`ExtendedFtplib` and :class:`ftplib.FTP` class).
466
496
  """
467
497
 
468
- _PROXY_TYPES = ('no-auth-logname-based', )
469
-
470
- _NO_AUTOLOGIN = ('set_debuglevel', 'connect', 'login', 'stderr',)
471
-
472
- def __init__(self, system, hostname, port=DEFAULT_FTP_PORT, nrcfile=None, ignoreproxy=False):
498
+ _PROXY_TYPES = ("no-auth-logname-based",)
499
+
500
+ _NO_AUTOLOGIN = (
501
+ "set_debuglevel",
502
+ "connect",
503
+ "login",
504
+ "stderr",
505
+ )
506
+
507
+ def __init__(
508
+ self,
509
+ system,
510
+ hostname,
511
+ port=DEFAULT_FTP_PORT,
512
+ nrcfile=None,
513
+ ignoreproxy=False,
514
+ ):
473
515
  """
474
516
  :param ~vortex.tools.systems.OSExtended system: The system object to work with
475
517
  :param str hostname: The remote host's network name
@@ -477,12 +519,18 @@ class StdFtp:
477
519
  :param str nrcfile: The path to the .netrc file (if `None` the ~/.netrc default is used)
478
520
  :param bool ignoreproxy: Forcibly ignore any proxy related environment variables
479
521
  """
480
- logger.debug('FTP init <host:%s>', hostname)
522
+ logger.debug("FTP init <host:%s>", hostname)
481
523
  self._system = system
482
524
  if ignoreproxy:
483
- self._proxy_host, self._proxy_port, self._proxy_type = (None, None, None)
525
+ self._proxy_host, self._proxy_port, self._proxy_type = (
526
+ None,
527
+ None,
528
+ None,
529
+ )
484
530
  else:
485
- self._proxy_host, self._proxy_port, self._proxy_type = self._proxy_init()
531
+ self._proxy_host, self._proxy_port, self._proxy_type = (
532
+ self._proxy_init()
533
+ )
486
534
  self._hostname = hostname
487
535
  self._port = port
488
536
  self._nrcfile = nrcfile
@@ -495,18 +543,22 @@ class StdFtp:
495
543
  """Return the proxy type, address and port."""
496
544
  p_netloc = (None, None)
497
545
  p_url = self.system.env.get(
498
- 'VORTEX_FTP_PROXY', self.system.env.get('FTP_PROXY', None)
546
+ "VORTEX_FTP_PROXY", self.system.env.get("FTP_PROXY", None)
499
547
  )
500
548
  if p_url:
501
- p_netloc = p_url.split(':', 1)
549
+ p_netloc = p_url.split(":", 1)
502
550
  if len(p_netloc) == 1:
503
551
  p_netloc.append(DEFAULT_FTP_PORT)
504
552
  else:
505
553
  p_netloc[1] = int(p_netloc[1])
506
- p_type = self.system.env.get('VORTEX_FTP_PROXY_TYPE', self._PROXY_TYPES[0])
554
+ p_type = self.system.env.get(
555
+ "VORTEX_FTP_PROXY_TYPE", self._PROXY_TYPES[0]
556
+ )
507
557
  if p_type not in self._PROXY_TYPES:
508
- raise ValueError('Incorrect value for the VORTEX_FTP_PROXY_TYPE ' +
509
- 'environment variable (got: {:s})'.format(p_type))
558
+ raise ValueError(
559
+ "Incorrect value for the VORTEX_FTP_PROXY_TYPE "
560
+ + "environment variable (got: {:s})".format(p_type)
561
+ )
510
562
  return p_netloc[0], p_netloc[1], p_type
511
563
 
512
564
  def _extended_ftp_host_and_port(self):
@@ -523,9 +575,9 @@ class StdFtp:
523
575
  It is created on-demand.
524
576
  """
525
577
  if self._internal_ftp is None:
526
- self._internal_ftp = ExtendedFtplib(self._system,
527
- ftplib.FTP(),
528
- * self._extended_ftp_host_and_port())
578
+ self._internal_ftp = ExtendedFtplib(
579
+ self._system, ftplib.FTP(), *self._extended_ftp_host_and_port()
580
+ )
529
581
  return self._internal_ftp
530
582
 
531
583
  _loginlike_extended_ftp = _extended_ftp
@@ -559,7 +611,7 @@ class StdFtp:
559
611
  @property
560
612
  def proxy(self):
561
613
  if self._proxy_host:
562
- return '{0._proxy_host}:{0._proxy_port}'.format(self)
614
+ return "{0._proxy_host}:{0._proxy_port}".format(self)
563
615
  else:
564
616
  return None
565
617
 
@@ -570,15 +622,20 @@ class StdFtp:
570
622
 
571
623
  def netpath(self, remote):
572
624
  """The complete qualified net path of the remote resource."""
573
- return '{:s}@{:s}:{:s}'.format(self.logname if self.logname is not None else 'unknown',
574
- self.host, remote)
625
+ return "{:s}@{:s}:{:s}".format(
626
+ self.logname if self.logname is not None else "unknown",
627
+ self.host,
628
+ remote,
629
+ )
575
630
 
576
631
  def delayedlogin(self):
577
632
  """Login to the FTP server (if it was not already done)."""
578
633
  if self._loginlike_extended_ftp.closed:
579
634
  if self._logname is None or self.cached_pwd is None:
580
- logger.warning('FTP logname/password must be set first. Use the fastlogin method.')
581
- raise RuntimeError('logname/password were not provided')
635
+ logger.warning(
636
+ "FTP logname/password must be set first. Use the fastlogin method."
637
+ )
638
+ raise RuntimeError("logname/password were not provided")
582
639
  return self.login(self._logname, self.cached_pwd)
583
640
  else:
584
641
  return True
@@ -588,11 +645,15 @@ class StdFtp:
588
645
  if logname and password:
589
646
  bare_logname = logname
590
647
  else:
591
- bare_logname, password = netrc_lookup(logname, self.host, nrcfile=self._nrcfile)
648
+ bare_logname, password = netrc_lookup(
649
+ logname, self.host, nrcfile=self._nrcfile
650
+ )
592
651
  logname = bare_logname
593
652
  if logname and self._proxy_host:
594
653
  if self._proxy_type == self._PROXY_TYPES[0]:
595
- logname = '{0:s}@{1.host:s}:{1.port:d}'.format(bare_logname, self)
654
+ logname = "{0:s}@{1.host:s}:{1.port:d}".format(
655
+ bare_logname, self
656
+ )
596
657
  if logname:
597
658
  return logname, password, bare_logname
598
659
  else:
@@ -614,7 +675,9 @@ class StdFtp:
614
675
  necessary).
615
676
  """
616
677
  rc = False
617
- p_logname, p_password, p_barelogname = self._process_logname_password(logname, password)
678
+ p_logname, p_password, p_barelogname = self._process_logname_password(
679
+ logname, password
680
+ )
618
681
  if p_logname and p_password:
619
682
  self._logname = p_logname
620
683
  self._cached_pwd = p_password
@@ -627,7 +690,7 @@ class StdFtp:
627
690
 
628
691
  def _extended_ftp_lookup_check(self, key):
629
692
  """Are we allowed to look for *key* in the `self._extended_ftp` object ?"""
630
- return not key.startswith('_')
693
+ return not key.startswith("_")
631
694
 
632
695
  def _extended_ftp_lookup(self, key):
633
696
  """Look if the `self._extended_ftp` object can provide a given method.
@@ -637,6 +700,7 @@ class StdFtp:
637
700
  """
638
701
  actualattr = getattr(self._extended_ftp, key)
639
702
  if callable(actualattr):
703
+
640
704
  def osproxy(*args, **kw):
641
705
  # For most of the native commands, we want autologin to be performed
642
706
  if key not in self._NO_AUTOLOGIN:
@@ -679,9 +743,20 @@ class AutoRetriesFtp(StdFtp):
679
743
  the retry-on-failure capability.
680
744
  """
681
745
 
682
- def __init__(self, system, hostname, port=DEFAULT_FTP_PORT, nrcfile=None, ignoreproxy=False,
683
- retrycount_default=6, retrycount_connect=8, retrycount_login=3,
684
- retrydelay_default=15, retrydelay_connect=15, retrydelay_login=10):
746
+ def __init__(
747
+ self,
748
+ system,
749
+ hostname,
750
+ port=DEFAULT_FTP_PORT,
751
+ nrcfile=None,
752
+ ignoreproxy=False,
753
+ retrycount_default=6,
754
+ retrycount_connect=8,
755
+ retrycount_login=3,
756
+ retrydelay_default=15,
757
+ retrydelay_connect=15,
758
+ retrydelay_login=10,
759
+ ):
685
760
  """
686
761
  :param ~vortex.tools.systems.OSExtended system: The system object to work with.
687
762
  :param str hostname: The remote host's network name.
@@ -695,7 +770,7 @@ class AutoRetriesFtp(StdFtp):
695
770
  :param int retrycount_login: The maximum number of retries when login in to the FTP server.
696
771
  :param int retrydelay_login: The delay (in seconds) between two retries when login in to the FTP server.
697
772
  """
698
- logger.debug('AutoRetries FTP init <host:%s>', hostname)
773
+ logger.debug("AutoRetries FTP init <host:%s>", hostname)
699
774
  # Retry stuff
700
775
  self.retrycount_default = retrycount_default
701
776
  self.retrycount_connect = retrycount_connect
@@ -706,11 +781,17 @@ class AutoRetriesFtp(StdFtp):
706
781
  # Reset everything
707
782
  self._initialise()
708
783
  # Finalise
709
- super().__init__(system, hostname, port=port, nrcfile=nrcfile, ignoreproxy=ignoreproxy)
784
+ super().__init__(
785
+ system,
786
+ hostname,
787
+ port=port,
788
+ nrcfile=nrcfile,
789
+ ignoreproxy=ignoreproxy,
790
+ )
710
791
 
711
792
  def _initialise(self):
712
793
  self._internal_retries_max = None
713
- self._cwd = ''
794
+ self._cwd = ""
714
795
  self._autodestroy()
715
796
 
716
797
  def _autodestroy(self):
@@ -720,25 +801,39 @@ class AutoRetriesFtp(StdFtp):
720
801
  def _get_extended_ftp(self, retrycount, retrydelay, exceptions_extras):
721
802
  """Delay the call to 'connect' as much as possible."""
722
803
  if self._internal_ftp is None:
723
- eftplib = self._retry_wrapped_callable(ExtendedFtplib,
724
- retrycount=retrycount,
725
- retrydelay=retrydelay,
726
- exceptions_extras=exceptions_extras)
727
- self._internal_ftp = eftplib(self._system, ftplib.FTP(),
728
- * self._extended_ftp_host_and_port())
804
+ eftplib = self._retry_wrapped_callable(
805
+ ExtendedFtplib,
806
+ retrycount=retrycount,
807
+ retrydelay=retrydelay,
808
+ exceptions_extras=exceptions_extras,
809
+ )
810
+ self._internal_ftp = eftplib(
811
+ self._system, ftplib.FTP(), *self._extended_ftp_host_and_port()
812
+ )
729
813
  return self._internal_ftp
730
814
 
731
815
  @property
732
816
  def _extended_ftp(self):
733
817
  """Delay the call to 'connect' as much as possible."""
734
- return self._get_extended_ftp(self.retrycount_connect, self.retrydelay_connect,
735
- [socket.timeout, ])
818
+ return self._get_extended_ftp(
819
+ self.retrycount_connect,
820
+ self.retrydelay_connect,
821
+ [
822
+ socket.timeout,
823
+ ],
824
+ )
736
825
 
737
826
  @property
738
827
  def _loginlike_extended_ftp(self):
739
828
  """Delay the call to 'connect' as much as possible."""
740
- return self._get_extended_ftp(self.retrycount_login, self.retrydelay_login,
741
- [ftplib.error_perm, socket.error, ])
829
+ return self._get_extended_ftp(
830
+ self.retrycount_login,
831
+ self.retrydelay_login,
832
+ [
833
+ ftplib.error_perm,
834
+ socket.error,
835
+ ],
836
+ )
742
837
 
743
838
  def _actual_login(self, *args):
744
839
  """Actually log in + save logname/password + correct the cwd if needed."""
@@ -750,22 +845,23 @@ class AutoRetriesFtp(StdFtp):
750
845
  self._cached_pwd = args[1]
751
846
  if rc and self._cwd:
752
847
  cocoondir = self._cwd
753
- self._cwd = ''
848
+ self._cwd = ""
754
849
  rc = rc and self.cwd(cocoondir)
755
850
  return rc
756
851
 
757
852
  def login(self, *args):
758
853
  """Proxy to ftplib :meth:`ftplib.FTP.login`."""
759
- wftplogin = self._retry_wrapped_callable(self._actual_login,
760
- retrycount=self.retrycount_login,
761
- retrydelay=self.retrydelay_login,
762
- exceptions_extras=[ftplib.error_perm,
763
- socket.error,
764
- EOFError])
854
+ wftplogin = self._retry_wrapped_callable(
855
+ self._actual_login,
856
+ retrycount=self.retrycount_login,
857
+ retrydelay=self.retrydelay_login,
858
+ exceptions_extras=[ftplib.error_perm, socket.error, EOFError],
859
+ )
765
860
  return wftplogin(*args)
766
861
 
767
- def _retry_wrapped_callable(self, func, retrycount=None, retrydelay=None,
768
- exceptions_extras=None):
862
+ def _retry_wrapped_callable(
863
+ self, func, retrycount=None, retrydelay=None, exceptions_extras=None
864
+ ):
769
865
  """
770
866
  Wraps the *func* function in order to implement a retry on failure
771
867
  mechanism.
@@ -782,7 +878,11 @@ class AutoRetriesFtp(StdFtp):
782
878
  """
783
879
  actual_rcount = retrycount or self.retrycount_default
784
880
  actual_rdelay = retrydelay or self.retrydelay_default
785
- actual_exc = [ftplib.error_temp, ftplib.error_proto, ftplib.error_reply, ]
881
+ actual_exc = [
882
+ ftplib.error_temp,
883
+ ftplib.error_proto,
884
+ ftplib.error_reply,
885
+ ]
786
886
  if exceptions_extras:
787
887
  actual_exc.extend(exceptions_extras)
788
888
  actual_exc = tuple(actual_exc)
@@ -791,22 +891,29 @@ class AutoRetriesFtp(StdFtp):
791
891
  globalcounter_driver = self._internal_retries_max is None
792
892
  if globalcounter_driver:
793
893
  self._internal_retries_max = actual_rcount
794
- retriesleft = max(min(self._internal_retries_max, actual_rcount), 1)
894
+ retriesleft = max(
895
+ min(self._internal_retries_max, actual_rcount), 1
896
+ )
795
897
  try:
796
898
  while retriesleft:
797
899
  try:
798
900
  return func(*args, **kw)
799
901
  except actual_exc as e:
800
- logger.warning('An error occurred (in "%s"): %s',
801
- func.__name__, e)
902
+ logger.warning(
903
+ 'An error occurred (in "%s"): %s', func.__name__, e
904
+ )
802
905
  retriesleft -= 1
803
906
  self._internal_retries_max -= 1
804
907
  if not retriesleft:
805
- logger.warning('The maximum number of retries (%d) was reached.',
806
- actual_rcount)
908
+ logger.warning(
909
+ "The maximum number of retries (%d) was reached.",
910
+ actual_rcount,
911
+ )
807
912
  raise
808
- logger.warning('Sleeping %d sec. before the next attempt.',
809
- actual_rdelay)
913
+ logger.warning(
914
+ "Sleeping %d sec. before the next attempt.",
915
+ actual_rdelay,
916
+ )
810
917
  self._autodestroy()
811
918
  self.system.sleep(actual_rdelay)
812
919
  finally:
@@ -825,15 +932,19 @@ class AutoRetriesFtp(StdFtp):
825
932
  attr = self._extended_ftp_lookup(key)
826
933
  if callable(attr):
827
934
  if key not in self._NO_AUTOLOGIN:
828
- attr = self._retry_wrapped_callable(attr,
829
- exceptions_extras=[socket.error, ])
935
+ attr = self._retry_wrapped_callable(
936
+ attr,
937
+ exceptions_extras=[
938
+ socket.error,
939
+ ],
940
+ )
830
941
  setattr(self, key, attr)
831
942
  return attr
832
943
  raise AttributeError(key)
833
944
 
834
945
  def cwd(self, pathname):
835
946
  """Change the current directory to the *pathname* directory."""
836
- todo = self._retry_wrapped_callable(self._extended_ftp_lookup('cwd'))
947
+ todo = self._retry_wrapped_callable(self._extended_ftp_lookup("cwd"))
837
948
  rc = todo(pathname)
838
949
  if rc:
839
950
  if self.system.path.isabs(pathname):
@@ -850,7 +961,9 @@ class AutoRetriesFtp(StdFtp):
850
961
  def quit(self):
851
962
  """Quit the current ftp session politely."""
852
963
  try:
853
- rc = self._retry_wrapped_callable(self._extended_ftp_lookup('quit'))()
964
+ rc = self._retry_wrapped_callable(
965
+ self._extended_ftp_lookup("quit")
966
+ )()
854
967
  finally:
855
968
  self._initialise()
856
969
  return rc
@@ -885,7 +998,7 @@ class ResetableAutoRetriesFtp(AutoRetriesFtp):
885
998
  def reset(self):
886
999
  """Reset the current working directory to its initial value."""
887
1000
  if self._initialpath is not None and self._cwd:
888
- self._cwd = ''
1001
+ self._cwd = ""
889
1002
  return self.cwd(self._initialpath)
890
1003
 
891
1004
 
@@ -904,7 +1017,9 @@ class PooledResetableAutoRetriesFtp(ResetableAutoRetriesFtp):
904
1017
  """
905
1018
  self._pool = pool
906
1019
  super().__init__(*kargs, **kwargs)
907
- logger.debug('Pooled FTP init <host:%s> <pool:%s>', self.host, repr(pool))
1020
+ logger.debug(
1021
+ "Pooled FTP init <host:%s> <pool:%s>", self.host, repr(pool)
1022
+ )
908
1023
 
909
1024
  def forceclose(self):
910
1025
  """Really quit the ftp session."""
@@ -965,40 +1080,60 @@ class FtpConnectionPool:
965
1080
 
966
1081
  def __str__(self):
967
1082
  """Print a summary of the connection pool activity."""
968
- out = 'Current connection pool size: {:d}\n'.format(self.poolsize)
969
- out += ' # of created objects: {:d}\n'.format(self._created)
970
- out += ' # of re-used objects: {:d}\n'.format(self._reused)
971
- out += ' # of given back objects: {:d}\n'.format(self._givenback)
1083
+ out = "Current connection pool size: {:d}\n".format(self.poolsize)
1084
+ out += " # of created objects: {:d}\n".format(self._created)
1085
+ out += " # of re-used objects: {:d}\n".format(self._reused)
1086
+ out += " # of given back objects: {:d}\n".format(self._givenback)
972
1087
  if self.poolsize:
973
- out += '\nDetailed list of current spare clients:\n'
1088
+ out += "\nDetailed list of current spare clients:\n"
974
1089
  for ident, hpool in self._reusable.items():
975
1090
  for client in hpool:
976
- out += ' - {id[1]:s}@{id[0]:s}: {cl!r}\n'.format(id=ident, cl=client)
1091
+ out += " - {id[1]:s}@{id[0]:s}: {cl!r}\n".format(
1092
+ id=ident, cl=client
1093
+ )
977
1094
  return out
978
1095
 
979
- def deal(self, hostname, logname, port=DEFAULT_FTP_PORT, delayed=True, ignoreproxy=False):
1096
+ def deal(
1097
+ self,
1098
+ hostname,
1099
+ logname,
1100
+ port=DEFAULT_FTP_PORT,
1101
+ delayed=True,
1102
+ ignoreproxy=False,
1103
+ ):
980
1104
  """Retrieve an FTP client for the *hostname*/*logname* pair."""
981
1105
  p_logname, _ = netrc_lookup(logname, hostname, nrcfile=self._nrcfile)
982
1106
  if self._reusable[(hostname, port, p_logname)]:
983
1107
  ftpc = self._reusable[(hostname, port, p_logname)].pop()
984
1108
  ftpc.reset()
985
- logger.debug('Re-using a client: %s', repr(ftpc))
1109
+ logger.debug("Re-using a client: %s", repr(ftpc))
986
1110
  if not delayed:
987
1111
  # If requested, ensure that we are logged in
988
1112
  ftpc.delayedlogin()
989
1113
  self._reused += 1
990
1114
  return ftpc
991
1115
  else:
992
- ftpc = self._FTPCLIENT_CLASS(self, self._system, hostname,
993
- port=port, nrcfile=self._nrcfile,
994
- ignoreproxy=self._ignoreproxy)
1116
+ ftpc = self._FTPCLIENT_CLASS(
1117
+ self,
1118
+ self._system,
1119
+ hostname,
1120
+ port=port,
1121
+ nrcfile=self._nrcfile,
1122
+ ignoreproxy=self._ignoreproxy,
1123
+ )
995
1124
  rc = ftpc.fastlogin(p_logname, delayed=delayed)
996
1125
  if rc:
997
- logger.debug('Creating a new client: %s', repr(ftpc))
1126
+ logger.debug("Creating a new client: %s", repr(ftpc))
998
1127
  self._created += 1
999
1128
  return ftpc
1000
1129
  else:
1001
- logger.warning('Could not login on %s:%d as %s [%s]', hostname, port, p_logname, str(rc))
1130
+ logger.warning(
1131
+ "Could not login on %s:%d as %s [%s]",
1132
+ hostname,
1133
+ port,
1134
+ p_logname,
1135
+ str(rc),
1136
+ )
1002
1137
  return None
1003
1138
 
1004
1139
  def relinquishing(self, client):
@@ -1010,19 +1145,32 @@ class FtpConnectionPool:
1010
1145
  its `close` method is called.
1011
1146
  """
1012
1147
  assert isinstance(client, self._FTPCLIENT_CLASS)
1013
- self._reusable[(client.host, client.port, client.logname)].append(client)
1148
+ self._reusable[(client.host, client.port, client.logname)].append(
1149
+ client
1150
+ )
1014
1151
  self._givenback += 1
1015
- logger.debug("Spare client for %s@%s:%d has been stored (poolsize=%d).",
1016
- client.logname, client.host, client.port, self.poolsize)
1152
+ logger.debug(
1153
+ "Spare client for %s@%s:%d has been stored (poolsize=%d).",
1154
+ client.logname,
1155
+ client.host,
1156
+ client.port,
1157
+ self.poolsize,
1158
+ )
1017
1159
  if self.poolsize >= self._REUSABLE_THRESHOLD:
1018
- logger.warning('The FTP pool is too big ! (%d >= %d). Here are the details:\n%s',
1019
- self.poolsize, self._REUSABLE_THRESHOLD, str(self))
1160
+ logger.warning(
1161
+ "The FTP pool is too big ! (%d >= %d). Here are the details:\n%s",
1162
+ self.poolsize,
1163
+ self._REUSABLE_THRESHOLD,
1164
+ str(self),
1165
+ )
1020
1166
 
1021
1167
  def clear(self):
1022
1168
  """Destroy all the spare FTP clients."""
1023
1169
  for hpool in self._reusable.values():
1024
1170
  for client in hpool:
1025
- logger.debug("Destroying client for %s@%s", client.logname, client.host)
1171
+ logger.debug(
1172
+ "Destroying client for %s@%s", client.logname, client.host
1173
+ )
1026
1174
  client.forceclose()
1027
1175
  hpool.clear()
1028
1176
 
@@ -1046,15 +1194,37 @@ class Ssh:
1046
1194
  self._logname = logname
1047
1195
  self._remote = hostname
1048
1196
 
1049
- target = sh.default_target
1050
- self._sshcmd = target.get(key='services:sshcmd', default='ssh')
1051
- self._scpcmd = target.get(key='services:scpcmd', default='scp')
1197
+ def _get_ssh_config(key, default):
1198
+ config = get_from_config_w_default(
1199
+ section="ssh", key=key, default=default
1200
+ )
1201
+ try:
1202
+ val = config.pop("default")
1203
+ except AttributeError:
1204
+ assert isinstance(config, str)
1205
+ return config
1206
+ except KeyError:
1207
+ msg = (
1208
+ "A default value must be specified for configuration option"
1209
+ f" {key}. See vortex-nwp.readthedocs.io/en/latest/user-guide/configuration.html#ssh"
1210
+ )
1211
+ raise ConfigurationError(msg)
1212
+
1213
+ for k, v in config.items():
1214
+ if re.match(k, socket.gethostname()):
1215
+ val = v
1216
+ return val
1217
+
1218
+ self._sshcmd = _get_ssh_config(key="sshcmd", default="ssh")
1219
+ self._scpcmd = _get_ssh_config(key="scpcmd", default="scp")
1052
1220
  self._sshopts = (
1053
- target.get(key='services:sshopts', default='-x').split() +
1054
- (sshopts or '').split())
1221
+ _get_ssh_config(key="sshopts", default="").split()
1222
+ + (sshopts or "").split()
1223
+ )
1055
1224
  self._scpopts = (
1056
- target.get(key='services:scpopts', default='-Bp').split() +
1057
- (scpopts or '').split())
1225
+ _get_ssh_config(key="scpopts", default="").split()
1226
+ + (scpopts or "").split()
1227
+ )
1058
1228
 
1059
1229
  @property
1060
1230
  def sh(self):
@@ -1062,13 +1232,15 @@ class Ssh:
1062
1232
 
1063
1233
  @property
1064
1234
  def remote(self):
1065
- return ('' if self._logname is None else self._logname + '@') + self._remote
1235
+ return (
1236
+ "" if self._logname is None else self._logname + "@"
1237
+ ) + self._remote
1066
1238
 
1067
1239
  def check_ok(self):
1068
1240
  """Is the connexion ok ?"""
1069
- return self.execute('true') is not False
1241
+ return self.execute("true") is not False
1070
1242
 
1071
- def execute(self, remote_command, sshopts=''):
1243
+ def execute(self, remote_command, sshopts=""):
1072
1244
  """Execute the command remotely.
1073
1245
 
1074
1246
  Return the output of the command (list of lines), or False on error.
@@ -1083,12 +1255,24 @@ class Ssh:
1083
1255
  myremote = self.remote
1084
1256
  if myremote is None:
1085
1257
  return False
1086
- cmd = ([self._sshcmd, ] +
1087
- self._sshopts + sshopts.split() +
1088
- [myremote, ] + [remote_command, ])
1258
+ cmd = (
1259
+ [
1260
+ self._sshcmd,
1261
+ ]
1262
+ + self._sshopts
1263
+ + sshopts.split()
1264
+ + [
1265
+ myremote,
1266
+ ]
1267
+ + [
1268
+ remote_command,
1269
+ ]
1270
+ )
1089
1271
  return self.sh.spawn(cmd, output=True, fatal=False)
1090
1272
 
1091
- def background_execute(self, remote_command, sshopts='', stdout=None, stderr=None):
1273
+ def background_execute(
1274
+ self, remote_command, sshopts="", stdout=None, stderr=None
1275
+ ):
1092
1276
  """Execute the command remotely and return the object representing the ssh process.
1093
1277
 
1094
1278
  Return a Popen object representing the ssh process. The user is reponsible
@@ -1097,9 +1281,19 @@ class Ssh:
1097
1281
  myremote = self.remote
1098
1282
  if myremote is None:
1099
1283
  return False
1100
- cmd = ([self._sshcmd, ] +
1101
- self._sshopts + sshopts.split() +
1102
- [myremote, ] + [remote_command, ])
1284
+ cmd = (
1285
+ [
1286
+ self._sshcmd,
1287
+ ]
1288
+ + self._sshopts
1289
+ + sshopts.split()
1290
+ + [
1291
+ myremote,
1292
+ ]
1293
+ + [
1294
+ remote_command,
1295
+ ]
1296
+ )
1103
1297
  return self.sh.popen(cmd, stdout=stdout, stderr=stderr)
1104
1298
 
1105
1299
  def cocoon(self, destination):
@@ -1108,14 +1302,18 @@ class Ssh:
1108
1302
  Return ``False`` on failure.
1109
1303
  """
1110
1304
  remote_dir = self.sh.path.dirname(destination)
1111
- if remote_dir == '':
1305
+ if remote_dir == "":
1112
1306
  return True
1113
1307
  logger.debug('Cocooning remote directory "%s"', remote_dir)
1114
1308
  cmd = 'mkdir -p "{}"'.format(remote_dir)
1115
1309
  rc = self.execute(cmd)
1116
1310
  if not rc:
1117
- logger.error('Cannot cocoon on %s (user: %s) for %s',
1118
- str(self._remote), str(self._logname), destination)
1311
+ logger.error(
1312
+ "Cannot cocoon on %s (user: %s) for %s",
1313
+ str(self._remote),
1314
+ str(self._logname),
1315
+ destination,
1316
+ )
1119
1317
  return rc
1120
1318
 
1121
1319
  def remove(self, target):
@@ -1128,33 +1326,43 @@ class Ssh:
1128
1326
  cmd = 'rm -fr "{}"'.format(target)
1129
1327
  rc = self.execute(cmd)
1130
1328
  if not rc:
1131
- logger.error('Cannot remove from %s (user: %s) item "%s"',
1132
- str(self._remote), str(self._logname), target)
1329
+ logger.error(
1330
+ 'Cannot remove from %s (user: %s) item "%s"',
1331
+ str(self._remote),
1332
+ str(self._logname),
1333
+ target,
1334
+ )
1133
1335
  return rc
1134
1336
 
1135
1337
  def _scp_putget_commons(self, source, destination):
1136
1338
  """Common checks on source and destination."""
1137
1339
  if not isinstance(source, str):
1138
- msg = 'Source is not a plain file path: {!r}'.format(source)
1340
+ msg = "Source is not a plain file path: {!r}".format(source)
1139
1341
  raise TypeError(msg)
1140
1342
  if not isinstance(destination, str):
1141
- msg = 'Destination is not a plain file path: {!r}'.format(destination)
1343
+ msg = "Destination is not a plain file path: {!r}".format(
1344
+ destination
1345
+ )
1142
1346
  raise TypeError(msg)
1143
1347
 
1144
1348
  # avoid special cases
1145
- if destination == '' or destination == '.':
1146
- destination = './'
1349
+ if destination == "" or destination == ".":
1350
+ destination = "./"
1147
1351
  else:
1148
- if destination.endswith('..'):
1149
- destination += '/'
1150
- if '../' in destination:
1151
- raise ValueError('"../" is not allowed in the destination path')
1152
- if destination.endswith('/'):
1153
- destination = self.sh.path.join(destination, self.sh.path.basename(source))
1352
+ if destination.endswith(".."):
1353
+ destination += "/"
1354
+ if "../" in destination:
1355
+ raise ValueError(
1356
+ '"../" is not allowed in the destination path'
1357
+ )
1358
+ if destination.endswith("/"):
1359
+ destination = self.sh.path.join(
1360
+ destination, self.sh.path.basename(source)
1361
+ )
1154
1362
 
1155
1363
  return source, destination
1156
1364
 
1157
- def scpput(self, source, destination, scpopts=''):
1365
+ def scpput(self, source, destination, scpopts=""):
1158
1366
  r"""Send ``source`` to ``destination``.
1159
1367
 
1160
1368
  - ``source`` is a single file or a directory, not a pattern (no '\*.grib').
@@ -1170,7 +1378,7 @@ class Ssh:
1170
1378
  source, destination = self._scp_putget_commons(source, destination)
1171
1379
 
1172
1380
  if not self.sh.path.exists(source):
1173
- logger.error('No such file or directory: %s', source)
1381
+ logger.error("No such file or directory: %s", source)
1174
1382
  return False
1175
1383
 
1176
1384
  source = self.sh.path.realpath(source)
@@ -1186,25 +1394,29 @@ class Ssh:
1186
1394
  return False
1187
1395
 
1188
1396
  if self.sh.path.isdir(source):
1189
- scpopts += ' -r'
1397
+ scpopts += " -r"
1190
1398
 
1191
- if not self.remove(destination + '.tmp'):
1399
+ if not self.remove(destination + ".tmp"):
1192
1400
  return False
1193
1401
 
1194
1402
  # transfer to a temporary place.
1195
1403
  # when ``destination`` contains spaces, 1 round of quoting
1196
1404
  # is necessary, to avoid an 'scp: ambiguous target' error.
1197
- cmd = ([self._scpcmd, ] +
1198
- self._scpopts + scpopts.split() +
1199
- [source,
1200
- myremote + ':' + shlex.quote(destination + '.tmp')])
1405
+ cmd = (
1406
+ [
1407
+ self._scpcmd,
1408
+ ]
1409
+ + self._scpopts
1410
+ + scpopts.split()
1411
+ + [source, myremote + ":" + shlex.quote(destination + ".tmp")]
1412
+ )
1201
1413
  rc = self.sh.spawn(cmd, output=False, fatal=False)
1202
1414
  if rc:
1203
1415
  # success, rename the tmp
1204
1416
  rc = self.execute('mv "{0}.tmp" "{0}"'.format(destination))
1205
1417
  return rc
1206
1418
 
1207
- def scpget(self, source, destination, scpopts='', isadir=False):
1419
+ def scpget(self, source, destination, scpopts="", isadir=False):
1208
1420
  r"""Send ``source`` to ``destination``.
1209
1421
 
1210
1422
  - ``source`` is the remote name, not a pattern (no '\*.grib').
@@ -1229,19 +1441,23 @@ class Ssh:
1229
1441
  if isadir:
1230
1442
  if not self.sh.remove(destination):
1231
1443
  return False
1232
- scpopts += ' -r'
1444
+ scpopts += " -r"
1233
1445
 
1234
1446
  # transfer to a temporary place.
1235
1447
  # when ``source`` contains spaces, 1 round of quoting
1236
1448
  # is necessary, to avoid an 'scp: ambiguous target' error.
1237
- cmd = ([self._scpcmd, ] +
1238
- self._scpopts + scpopts.split() +
1239
- [myremote + ':' + shlex.quote(source),
1240
- destination + '.tmp'])
1449
+ cmd = (
1450
+ [
1451
+ self._scpcmd,
1452
+ ]
1453
+ + self._scpopts
1454
+ + scpopts.split()
1455
+ + [myremote + ":" + shlex.quote(source), destination + ".tmp"]
1456
+ )
1241
1457
  rc = self.sh.spawn(cmd, output=False, fatal=False)
1242
1458
  if rc:
1243
1459
  # success, rename the tmp
1244
- rc = self.sh.move(destination + '.tmp', destination)
1460
+ rc = self.sh.move(destination + ".tmp", destination)
1245
1461
  return rc
1246
1462
 
1247
1463
  def get_permissions(self, source):
@@ -1252,7 +1468,7 @@ class Ssh:
1252
1468
  mode = self.sh.stat(source).st_mode
1253
1469
  return stat.S_IMODE(mode)
1254
1470
 
1255
- def scpput_stream(self, stream, destination, permissions=None, sshopts=''):
1471
+ def scpput_stream(self, stream, destination, permissions=None, sshopts=""):
1256
1472
  """Send the ``stream`` to the ``destination``.
1257
1473
 
1258
1474
  - ``stream`` is a ``file`` (typically returned by open(),
@@ -1262,11 +1478,15 @@ class Ssh:
1262
1478
  Return True for ok, False on error.
1263
1479
  """
1264
1480
  if not isinstance(stream, io.IOBase):
1265
- msg = "stream is a {}, should be a <type 'file'>".format(type(stream))
1481
+ msg = "stream is a {}, should be a <type 'file'>".format(
1482
+ type(stream)
1483
+ )
1266
1484
  raise TypeError(msg)
1267
1485
 
1268
1486
  if not isinstance(destination, str):
1269
- msg = 'Destination is not a plain file path: {!r}'.format(destination)
1487
+ msg = "Destination is not a plain file path: {!r}".format(
1488
+ destination
1489
+ )
1270
1490
  raise TypeError(msg)
1271
1491
 
1272
1492
  myremote = self.remote
@@ -1277,16 +1497,25 @@ class Ssh:
1277
1497
  return False
1278
1498
 
1279
1499
  # transfer to a tmp, rename and set permissions in one go
1280
- remote_cmd = 'cat > {0}.tmp && mv {0}.tmp {0}'.format(shlex.quote(destination))
1500
+ remote_cmd = "cat > {0}.tmp && mv {0}.tmp {0}".format(
1501
+ shlex.quote(destination)
1502
+ )
1281
1503
  if permissions:
1282
- remote_cmd += ' && chmod -v {:o} {}'.format(permissions, shlex.quote(destination))
1283
-
1284
- cmd = ([self._sshcmd, ] +
1285
- self._sshopts + sshopts.split() +
1286
- [myremote, remote_cmd])
1504
+ remote_cmd += " && chmod -v {:o} {}".format(
1505
+ permissions, shlex.quote(destination)
1506
+ )
1507
+
1508
+ cmd = (
1509
+ [
1510
+ self._sshcmd,
1511
+ ]
1512
+ + self._sshopts
1513
+ + sshopts.split()
1514
+ + [myremote, remote_cmd]
1515
+ )
1287
1516
  return self.sh.spawn(cmd, stdin=stream, output=False, fatal=False)
1288
1517
 
1289
- def scpget_stream(self, source, stream, sshopts=''):
1518
+ def scpget_stream(self, source, stream, sshopts=""):
1290
1519
  """Send the ``source`` to the ``stream``.
1291
1520
 
1292
1521
  - ``source`` is the remote file name.
@@ -1296,11 +1525,13 @@ class Ssh:
1296
1525
  Return True for ok, False on error.
1297
1526
  """
1298
1527
  if not isinstance(stream, io.IOBase):
1299
- msg = "stream is a {}, should be a <type 'file'>".format(type(stream))
1528
+ msg = "stream is a {}, should be a <type 'file'>".format(
1529
+ type(stream)
1530
+ )
1300
1531
  raise TypeError(msg)
1301
1532
 
1302
1533
  if not isinstance(source, str):
1303
- msg = 'Source is not a plain file path: {!r}'.format(source)
1534
+ msg = "Source is not a plain file path: {!r}".format(source)
1304
1535
  raise TypeError(msg)
1305
1536
 
1306
1537
  myremote = self.remote
@@ -1308,14 +1539,25 @@ class Ssh:
1308
1539
  return False
1309
1540
 
1310
1541
  # transfer to a tmp, rename and set permissions in one go
1311
- remote_cmd = 'cat {}'.format(shlex.quote(source))
1312
- cmd = ([self._sshcmd, ] +
1313
- self._sshopts + sshopts.split() +
1314
- [myremote, remote_cmd])
1542
+ remote_cmd = "cat {}".format(shlex.quote(source))
1543
+ cmd = (
1544
+ [
1545
+ self._sshcmd,
1546
+ ]
1547
+ + self._sshopts
1548
+ + sshopts.split()
1549
+ + [myremote, remote_cmd]
1550
+ )
1315
1551
  return self.sh.spawn(cmd, output=stream, fatal=False)
1316
1552
 
1317
- def tunnel(self, finaldestination, finalport=0, entranceport=None,
1318
- maxwait=3., checkdelay=0.25):
1553
+ def tunnel(
1554
+ self,
1555
+ finaldestination,
1556
+ finalport=0,
1557
+ entranceport=None,
1558
+ maxwait=3.0,
1559
+ checkdelay=0.25,
1560
+ ):
1319
1561
  """Create an SSH tunnel and check that it actually starts.
1320
1562
 
1321
1563
  :param str finaldestination: The destination hostname (i.e the machine
@@ -1343,45 +1585,84 @@ class Ssh:
1343
1585
  entranceport = self.sh.available_localport()
1344
1586
  else:
1345
1587
  if self.sh.check_localport(entranceport):
1346
- logger.error('The SSH tunnel creation failed ' +
1347
- '(entrance: %d, dest: %s:%d, via %s).',
1348
- entranceport, finaldestination, finalport, myremote)
1349
- logger.error('The entrance port is already in use.')
1588
+ logger.error(
1589
+ "The SSH tunnel creation failed "
1590
+ + "(entrance: %d, dest: %s:%d, via %s).",
1591
+ entranceport,
1592
+ finaldestination,
1593
+ finalport,
1594
+ myremote,
1595
+ )
1596
+ logger.error("The entrance port is already in use.")
1350
1597
  return False
1351
- if finaldestination == 'socks':
1352
- p = self.sh.popen([self._sshcmd, ] + self._sshopts +
1353
- ['-N', '-D', '{:d}'.format(entranceport), myremote],
1354
- stdin=False, output=False)
1598
+ if finaldestination == "socks":
1599
+ p = self.sh.popen(
1600
+ [
1601
+ self._sshcmd,
1602
+ ]
1603
+ + self._sshopts
1604
+ + ["-N", "-D", "{:d}".format(entranceport), myremote],
1605
+ stdin=False,
1606
+ output=False,
1607
+ )
1355
1608
  else:
1356
1609
  if finalport <= 0:
1357
- raise ValueError('Erroneous finalport value: {!s}'.format(finalport))
1358
- p = self.sh.popen([self._sshcmd, ] + self._sshopts +
1359
- ['-N', '-L',
1360
- '{:d}:{:s}:{:d}'.format(entranceport,
1361
- finaldestination, finalport),
1362
- myremote],
1363
- stdin=False, output=False)
1364
- tunnel = ActiveSshTunnel(self.sh, p, entranceport, finaldestination, finalport)
1365
- elapsed = 0.
1366
- while (not self.sh.check_localport(entranceport)) and elapsed < maxwait:
1610
+ raise ValueError(
1611
+ "Erroneous finalport value: {!s}".format(finalport)
1612
+ )
1613
+ p = self.sh.popen(
1614
+ [
1615
+ self._sshcmd,
1616
+ ]
1617
+ + self._sshopts
1618
+ + [
1619
+ "-N",
1620
+ "-L",
1621
+ "{:d}:{:s}:{:d}".format(
1622
+ entranceport, finaldestination, finalport
1623
+ ),
1624
+ myremote,
1625
+ ],
1626
+ stdin=False,
1627
+ output=False,
1628
+ )
1629
+ tunnel = ActiveSshTunnel(
1630
+ self.sh, p, entranceport, finaldestination, finalport
1631
+ )
1632
+ elapsed = 0.0
1633
+ while (
1634
+ not self.sh.check_localport(entranceport)
1635
+ ) and elapsed < maxwait:
1367
1636
  self.sh.sleep(checkdelay)
1368
1637
  elapsed += checkdelay
1369
1638
  if not self.sh.check_localport(entranceport):
1370
- logger.error('The SSH tunnel creation failed ' +
1371
- '(entrance: %d, dest: %s:%d, via %s).',
1372
- entranceport, finaldestination, finalport, myremote)
1639
+ logger.error(
1640
+ "The SSH tunnel creation failed "
1641
+ + "(entrance: %d, dest: %s:%d, via %s).",
1642
+ entranceport,
1643
+ finaldestination,
1644
+ finalport,
1645
+ myremote,
1646
+ )
1373
1647
  tunnel.close()
1374
1648
  tunnel = False
1375
- logger.info('SSH tunnel opened, enjoy the ride ! ' +
1376
- '(entrance: %d, dest: %s:%d, via %s).',
1377
- entranceport, finaldestination, finalport, myremote)
1649
+ logger.info(
1650
+ "SSH tunnel opened, enjoy the ride ! "
1651
+ + "(entrance: %d, dest: %s:%d, via %s).",
1652
+ entranceport,
1653
+ finaldestination,
1654
+ finalport,
1655
+ myremote,
1656
+ )
1378
1657
  return tunnel
1379
1658
 
1380
1659
 
1381
1660
  class ActiveSshTunnel:
1382
1661
  """Hold an opened SSH tunnel."""
1383
1662
 
1384
- def __init__(self, sh, activeprocess, entranceport, finaldestination, finalport):
1663
+ def __init__(
1664
+ self, sh, activeprocess, entranceport, finaldestination, finalport
1665
+ ):
1385
1666
  """
1386
1667
  :param Popen activeprocess: The active tunnel process.
1387
1668
  :param int entranceport: Tunnel's entrance port.
@@ -1407,12 +1688,18 @@ class ActiveSshTunnel:
1407
1688
  t0 = time.time()
1408
1689
  while self.opened and time.time() - t0 < 5:
1409
1690
  self._sh.sleep(0.1)
1410
- logger.debug("Tunnel termination took: %f seconds", time.time() - t0)
1691
+ logger.debug(
1692
+ "Tunnel termination took: %f seconds", time.time() - t0
1693
+ )
1411
1694
  if self.opened:
1412
1695
  logger.debug("Tunnel termination failed: issuing SIGKILL")
1413
1696
  self.activeprocess.kill()
1414
- logger.info('SSH tunnel closed (entrance: %d, dest: %s:%d).',
1415
- self.entranceport, self.finaldestination, self.finalport)
1697
+ logger.info(
1698
+ "SSH tunnel closed (entrance: %d, dest: %s:%d).",
1699
+ self.entranceport,
1700
+ self.finaldestination,
1701
+ self.finalport,
1702
+ )
1416
1703
 
1417
1704
  @property
1418
1705
  def opened(self):
@@ -1444,9 +1731,14 @@ def _check_fatal(func):
1444
1731
  try:
1445
1732
  rc = func(self, *args[1:], **kwargs)
1446
1733
  if not rc:
1447
- logger.error("The maximum number of retries (%s) was reached...", self._maxtries)
1734
+ logger.error(
1735
+ "The maximum number of retries (%s) was reached...",
1736
+ self._maxtries,
1737
+ )
1448
1738
  if self._fatal:
1449
- raise RuntimeError("Could not execute the SSH command.")
1739
+ raise RuntimeError(
1740
+ "Could not execute the SSH command."
1741
+ )
1450
1742
  finally:
1451
1743
  self._fatal_in_progress = False
1452
1744
  return rc
@@ -1474,7 +1766,11 @@ def _tryagain(func):
1474
1766
  rc = func(self, *args[1:], **kwargs)
1475
1767
  while not rc and trycount < self._maxtries:
1476
1768
  trycount += 1
1477
- logger.info("Trying again (retries=%d/%d)...", trycount, self._maxtries)
1769
+ logger.info(
1770
+ "Trying again (retries=%d/%d)...",
1771
+ trycount,
1772
+ self._maxtries,
1773
+ )
1478
1774
  self.sh.sleep(self._triesdelay)
1479
1775
  rc = func(self, *args[1:], **kwargs)
1480
1776
  finally:
@@ -1503,13 +1799,17 @@ class _AssistedSshMeta(type):
1503
1799
  """
1504
1800
  bare_methods = list(d.keys())
1505
1801
  # Add the tryagain decorator...
1506
- for tagain in [x for x in d['_auto_retries'] if x not in bare_methods]:
1802
+ for tagain in [x for x in d["_auto_retries"] if x not in bare_methods]:
1507
1803
  inherited = [base for base in b if hasattr(base, tagain)]
1508
1804
  d[tagain] = _tryagain(getattr(inherited[0], tagain))
1509
1805
  # Add the check_fatal decorator...
1510
- for cfatal in [x for x in d['_auto_checkfatal'] if x not in bare_methods]:
1806
+ for cfatal in [
1807
+ x for x in d["_auto_checkfatal"] if x not in bare_methods
1808
+ ]:
1511
1809
  inherited = [base for base in b if hasattr(base, cfatal)]
1512
- d[cfatal] = _check_fatal(d.get(cfatal, getattr(inherited[0], cfatal)))
1810
+ d[cfatal] = _check_fatal(
1811
+ d.get(cfatal, getattr(inherited[0], cfatal))
1812
+ )
1513
1813
  return super().__new__(cls, n, b, d)
1514
1814
 
1515
1815
 
@@ -1577,16 +1877,42 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
1577
1877
 
1578
1878
  """
1579
1879
 
1580
- _auto_checkfatal = ['check_ok', 'execute', 'cocoon', 'remove',
1581
- 'scpput', 'scpget', 'scpput_stream', 'scpget_stream',
1582
- 'tunnel']
1880
+ _auto_checkfatal = [
1881
+ "check_ok",
1882
+ "execute",
1883
+ "cocoon",
1884
+ "remove",
1885
+ "scpput",
1886
+ "scpget",
1887
+ "scpput_stream",
1888
+ "scpget_stream",
1889
+ "tunnel",
1890
+ ]
1583
1891
  # No retries on scpput_stream since it's not guaranteed that the stream is seekable.
1584
- _auto_retries = ['check_ok', 'execute', 'cocoon', 'remove',
1585
- 'scpput', 'scpget', 'tunnel']
1586
-
1587
- def __init__(self, sh, hostname, logname=None, sshopts=None, scpopts=None,
1588
- maxtries=1, triesdelay=1, virtualnode=False, permut=True,
1589
- fatal=False, mandatory_hostcheck=True):
1892
+ _auto_retries = [
1893
+ "check_ok",
1894
+ "execute",
1895
+ "cocoon",
1896
+ "remove",
1897
+ "scpput",
1898
+ "scpget",
1899
+ "tunnel",
1900
+ ]
1901
+
1902
+ def __init__(
1903
+ self,
1904
+ sh,
1905
+ hostname,
1906
+ logname=None,
1907
+ sshopts=None,
1908
+ scpopts=None,
1909
+ maxtries=1,
1910
+ triesdelay=1,
1911
+ virtualnode=False,
1912
+ permut=True,
1913
+ fatal=False,
1914
+ mandatory_hostcheck=True,
1915
+ ):
1590
1916
  """
1591
1917
  :param System sh: The :class:`System` object that is to be used.
1592
1918
  :param hostname: The target hostname(s).
@@ -1616,7 +1942,9 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
1616
1942
  self._fatal = fatal
1617
1943
  self._mandatory_hostcheck = mandatory_hostcheck
1618
1944
  if self._virtualnode and isinstance(self._remote, (list, tuple)):
1619
- raise ValueError('When virtual nodes are used, the hostname must be a string')
1945
+ raise ValueError(
1946
+ "When virtual nodes are used, the hostname must be a string"
1947
+ )
1620
1948
 
1621
1949
  self._retry_in_progress = False
1622
1950
  self._fatal_in_progress = False
@@ -1640,7 +1968,7 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
1640
1968
  else:
1641
1969
  targets = [self._remote]
1642
1970
  if self._logname is not None:
1643
- targets = [self._logname + '@' + x for x in targets]
1971
+ targets = [self._logname + "@" + x for x in targets]
1644
1972
  if self._permut:
1645
1973
  random.shuffle(targets)
1646
1974
  return targets
@@ -1667,7 +1995,16 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
1667
1995
  if self._mandatory_hostcheck:
1668
1996
  if self._chosen_target is None:
1669
1997
  for guess in self.targets:
1670
- cmd = [self._sshcmd, ] + self._sshopts + [guess, 'true', ]
1998
+ cmd = (
1999
+ [
2000
+ self._sshcmd,
2001
+ ]
2002
+ + self._sshopts
2003
+ + [
2004
+ guess,
2005
+ "true",
2006
+ ]
2007
+ )
1671
2008
  try:
1672
2009
  self.sh.spawn(cmd, output=False, silent=True)
1673
2010
  except Exception:
@@ -1680,9 +2017,16 @@ class AssistedSsh(Ssh, metaclass=_AssistedSshMeta):
1680
2017
  return next(self._targets_iter)
1681
2018
 
1682
2019
 
1683
- _ConnectionStatusAttrs = ('Family', 'LocalAddr', 'LocalPort', 'DestAddr', 'DestPort', 'Status')
1684
- TcpConnectionStatus = namedtuple('TcpConnectionStatus', _ConnectionStatusAttrs)
1685
- UdpConnectionStatus = namedtuple('UdpConnectionStatus', _ConnectionStatusAttrs)
2020
+ _ConnectionStatusAttrs = (
2021
+ "Family",
2022
+ "LocalAddr",
2023
+ "LocalPort",
2024
+ "DestAddr",
2025
+ "DestPort",
2026
+ "Status",
2027
+ )
2028
+ TcpConnectionStatus = namedtuple("TcpConnectionStatus", _ConnectionStatusAttrs)
2029
+ UdpConnectionStatus = namedtuple("UdpConnectionStatus", _ConnectionStatusAttrs)
1686
2030
 
1687
2031
 
1688
2032
  class AbstractNetstats(metaclass=abc.ABCMeta):
@@ -1730,11 +2074,9 @@ class AbstractNetstats(metaclass=abc.ABCMeta):
1730
2074
  class LinuxNetstats(AbstractNetstats):
1731
2075
  """A Netstats implementation for Linux (based on the /proc/net data)."""
1732
2076
 
1733
- _LINUX_LPORT = '/proc/sys/net/ipv4/ip_local_port_range'
1734
- _LINUX_PORTS_V4 = {'tcp': '/proc/net/tcp',
1735
- 'udp': '/proc/net/udp'}
1736
- _LINUX_PORTS_V6 = {'tcp': '/proc/net/tcp6',
1737
- 'udp': '/proc/net/udp6'}
2077
+ _LINUX_LPORT = "/proc/sys/net/ipv4/ip_local_port_range"
2078
+ _LINUX_PORTS_V4 = {"tcp": "/proc/net/tcp", "udp": "/proc/net/udp"}
2079
+ _LINUX_PORTS_V6 = {"tcp": "/proc/net/tcp6", "udp": "/proc/net/udp6"}
1738
2080
  _LINUX_AF_INET4 = socket.AF_INET
1739
2081
  _LINUX_AF_INET6 = socket.AF_INET6
1740
2082
 
@@ -1747,7 +2089,9 @@ class LinuxNetstats(AbstractNetstats):
1747
2089
  with open(self._LINUX_LPORT) as tmprange:
1748
2090
  tmpports = [int(x) for x in tmprange.readline().split()]
1749
2091
  unports = set(range(5001, 65536))
1750
- self.__unprivileged_ports = sorted(unports - set(range(tmpports[0], tmpports[1] + 1)))
2092
+ self.__unprivileged_ports = sorted(
2093
+ unports - set(range(tmpports[0], tmpports[1] + 1))
2094
+ )
1751
2095
  return self.__unprivileged_ports
1752
2096
 
1753
2097
  @classmethod
@@ -1755,8 +2099,7 @@ class LinuxNetstats(AbstractNetstats):
1755
2099
  if family == cls._LINUX_AF_INET4:
1756
2100
  packed = struct.pack(b"<I", int(hexip, 16))
1757
2101
  elif family == cls._LINUX_AF_INET6:
1758
- packed = struct.unpack(b">IIII",
1759
- binascii.a2b_hex(hexip))
2102
+ packed = struct.unpack(b">IIII", binascii.a2b_hex(hexip))
1760
2103
  packed = struct.pack(b"@IIII", *packed)
1761
2104
  else:
1762
2105
  raise ValueError("Unknown address family.")
@@ -1766,25 +2109,38 @@ class LinuxNetstats(AbstractNetstats):
1766
2109
  tmpports = dict()
1767
2110
  with open(self._LINUX_PORTS_V4[proto]) as netstats:
1768
2111
  netstats.readline() # Skip the header line
1769
- tmpports[self._LINUX_AF_INET4] = [re.split(r':\b|\s+', x.strip())[1:6]
1770
- for x in netstats.readlines()]
2112
+ tmpports[self._LINUX_AF_INET4] = [
2113
+ re.split(r":\b|\s+", x.strip())[1:6]
2114
+ for x in netstats.readlines()
2115
+ ]
1771
2116
  try:
1772
2117
  with open(self._LINUX_PORTS_V6[proto]) as netstats:
1773
2118
  netstats.readline() # Skip the header line
1774
- tmpports[self._LINUX_AF_INET6] = [re.split(r':\b|\s+', x.strip())[1:6]
1775
- for x in netstats.readlines()]
2119
+ tmpports[self._LINUX_AF_INET6] = [
2120
+ re.split(r":\b|\s+", x.strip())[1:6]
2121
+ for x in netstats.readlines()
2122
+ ]
1776
2123
  except OSError:
1777
2124
  # Apparently, no IPv6 support on this machine
1778
2125
  tmpports[self._LINUX_AF_INET6] = []
1779
- tmpports = [[rclass(family,
1780
- self._ip_from_hex(l[0], family), int(l[1], 16),
1781
- self._ip_from_hex(l[2], family), int(l[3], 16),
1782
- int(l[4], 16)) for l in tmpports[family]]
1783
- for family in (self._LINUX_AF_INET4, self._LINUX_AF_INET6)]
2126
+ tmpports = [
2127
+ [
2128
+ rclass(
2129
+ family,
2130
+ self._ip_from_hex(l[0], family),
2131
+ int(l[1], 16),
2132
+ self._ip_from_hex(l[2], family),
2133
+ int(l[3], 16),
2134
+ int(l[4], 16),
2135
+ )
2136
+ for l in tmpports[family]
2137
+ ]
2138
+ for family in (self._LINUX_AF_INET4, self._LINUX_AF_INET6)
2139
+ ]
1784
2140
  return functools.reduce(operator.add, tmpports)
1785
2141
 
1786
2142
  def tcp_netstats(self):
1787
- return self._generic_netstats('tcp', TcpConnectionStatus)
2143
+ return self._generic_netstats("tcp", TcpConnectionStatus)
1788
2144
 
1789
2145
  def udp_netstats(self):
1790
- return self._generic_netstats('udp', UdpConnectionStatus)
2146
+ return self._generic_netstats("udp", UdpConnectionStatus)