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/services.py CHANGED
@@ -5,7 +5,7 @@ With the abstract class Service (inheritating from FootprintBase)
5
5
  a default Mail Service is provided.
6
6
  """
7
7
 
8
- from configparser import NoOptionError, NoSectionError
8
+ import configparser
9
9
  import contextlib
10
10
  import hashlib
11
11
  import pprint
@@ -19,7 +19,10 @@ from bronx.stdtypes import date
19
19
  from bronx.stdtypes.dictionaries import UpperCaseDict
20
20
  from bronx.syntax.pretty import EncodedPrettyPrinter
21
21
  from vortex import sessions
22
- from vortex.util.config import GenericConfigParser, load_template, LegacyTemplatingAdapter
22
+ from vortex.util.config import (
23
+ load_template,
24
+ LegacyTemplatingAdapter,
25
+ )
23
26
 
24
27
  #: No automatic export
25
28
  __all__ = []
@@ -27,7 +30,7 @@ __all__ = []
27
30
  logger = loggers.getLogger(__name__)
28
31
 
29
32
  # See logging.handlers.SysLogHandler.priority_map
30
- criticals = ['debug', 'info', 'error', 'warning', 'critical']
33
+ criticals = ["debug", "info", "error", "warning", "critical"]
31
34
 
32
35
 
33
36
  class Service(footprints.FootprintBase):
@@ -36,31 +39,31 @@ class Service(footprints.FootprintBase):
36
39
  """
37
40
 
38
41
  _abstract = True
39
- _collector = ('service',)
42
+ _collector = ("service",)
40
43
  _footprint = dict(
41
- info = 'Abstract services class',
42
- attr = dict(
43
- kind = dict(),
44
- level = dict(
45
- optional = True,
46
- default = 'info',
47
- values = criticals,
48
- ),
49
- )
44
+ info="Abstract services class",
45
+ attr=dict(
46
+ kind=dict(),
47
+ level=dict(
48
+ optional=True,
49
+ default="info",
50
+ values=criticals,
51
+ ),
52
+ ),
50
53
  )
51
54
 
52
55
  def __init__(self, *args, **kw):
53
- logger.debug('Abstract service init %s', self.__class__)
56
+ logger.debug("Abstract service init %s", self.__class__)
54
57
  t = sessions.current()
55
- glove = kw.pop('glove', t.glove)
56
- sh = kw.pop('sh', t.system())
58
+ glove = kw.pop("glove", t.glove)
59
+ sh = kw.pop("sh", t.system())
57
60
  super().__init__(*args, **kw)
58
61
  self._glove = glove
59
62
  self._sh = sh
60
63
 
61
64
  @property
62
65
  def realkind(self):
63
- return 'service'
66
+ return "service"
64
67
 
65
68
  @property
66
69
  def sh(self):
@@ -89,7 +92,7 @@ class Service(footprints.FootprintBase):
89
92
  value = self.env.get(as_var, None)
90
93
  if not value:
91
94
  if as_conf is None:
92
- as_conf = 'services:' + key.lower()
95
+ as_conf = "services:" + key.lower()
93
96
  value = self.sh.default_target.get(as_conf, default)
94
97
  return value
95
98
 
@@ -104,69 +107,66 @@ class MailService(Service):
104
107
  """
105
108
 
106
109
  _footprint = dict(
107
- info = 'Mail services class',
108
- attr = dict(
109
- kind = dict(
110
- values = ['sendmail'],
111
- ),
112
- sender = dict(
113
- optional = True,
114
- default = '[glove::xmail]',
115
- ),
116
- to = dict(
117
- alias = ('receiver', 'recipients'),
118
- ),
119
- replyto = dict(
120
- optional = True,
121
- alias = ('reply', 'reply_to'),
122
- default = None,
123
- ),
124
- message = dict(
125
- optional = True,
126
- default = '',
127
- alias = ('contents', 'body'),
128
- type = str,
129
- ),
130
- filename = dict(
131
- optional = True,
132
- default = None,
133
- ),
134
- attachments = dict(
135
- type = footprints.FPList,
136
- optional = True,
137
- default = footprints.FPList(),
138
- alias = ('files', 'attach'),
139
- ),
140
- subject = dict(
141
- type = str,
142
- ),
143
- smtpserver = dict(
144
- optional = True,
145
- ),
146
- smtpport = dict(
147
- type = int,
148
- optional = True,
149
- ),
150
- smtpuser = dict(
151
- optional = True,
152
- ),
153
- smtppass = dict(
154
- optional = True,
155
- ),
156
- charset = dict(
157
- info = 'The encoding that should be used when sending the email',
158
- optional = True,
159
- default = 'utf-8',
160
- ),
161
- inputs_charset = dict(
162
- info = 'The encoding that should be used when reading input files',
163
- optional = True,
164
- ),
165
- commaspace = dict(
166
- optional = True,
167
- default = ', '
168
- )
169
- )
110
+ info="Mail services class",
111
+ attr=dict(
112
+ kind=dict(
113
+ values=["sendmail"],
114
+ ),
115
+ sender=dict(
116
+ optional=True,
117
+ default="[glove::xmail]",
118
+ ),
119
+ to=dict(
120
+ alias=("receiver", "recipients"),
121
+ ),
122
+ replyto=dict(
123
+ optional=True,
124
+ alias=("reply", "reply_to"),
125
+ default=None,
126
+ ),
127
+ message=dict(
128
+ optional=True,
129
+ default="",
130
+ alias=("contents", "body"),
131
+ type=str,
132
+ ),
133
+ filename=dict(
134
+ optional=True,
135
+ default=None,
136
+ ),
137
+ attachments=dict(
138
+ type=footprints.FPList,
139
+ optional=True,
140
+ default=footprints.FPList(),
141
+ alias=("files", "attach"),
142
+ ),
143
+ subject=dict(
144
+ type=str,
145
+ ),
146
+ smtpserver=dict(
147
+ optional=True,
148
+ ),
149
+ smtpport=dict(
150
+ type=int,
151
+ optional=True,
152
+ ),
153
+ smtpuser=dict(
154
+ optional=True,
155
+ ),
156
+ smtppass=dict(
157
+ optional=True,
158
+ ),
159
+ charset=dict(
160
+ info="The encoding that should be used when sending the email",
161
+ optional=True,
162
+ default="utf-8",
163
+ ),
164
+ inputs_charset=dict(
165
+ info="The encoding that should be used when reading input files",
166
+ optional=True,
167
+ ),
168
+ commaspace=dict(optional=True, default=", "),
169
+ ),
170
170
  )
171
171
 
172
172
  def attach(self, *args):
@@ -186,28 +186,41 @@ class MailService(Service):
186
186
  with open(self.filename, encoding=self.inputs_charset) as tmp:
187
187
  body += tmp.read()
188
188
  from email.message import EmailMessage
189
+
189
190
  msg = EmailMessage()
190
- msg.set_content(body, subtype="plain",
191
- charset=(self.charset if self.is_not_plain_ascii(body) else 'us-ascii'))
191
+ msg.set_content(
192
+ body,
193
+ subtype="plain",
194
+ charset=(
195
+ self.charset if self.is_not_plain_ascii(body) else "us-ascii"
196
+ ),
197
+ )
192
198
  return msg
193
199
 
194
200
  def as_multipart(self, msg):
195
201
  """Build a new multipart mail with default text contents and attachments."""
196
202
  from email.message import MIMEPart
203
+
197
204
  for xtra in self.attachments:
198
205
  if isinstance(xtra, MIMEPart):
199
206
  msg.add_attachment(xtra)
200
207
  elif self.sh.path.isfile(xtra):
201
208
  import mimetypes
209
+
202
210
  ctype, encoding = mimetypes.guess_type(xtra)
203
211
  if ctype is None or encoding is not None:
204
212
  # No guess could be made, or the file is encoded
205
213
  # (compressed), so use a generic bag-of-bits type.
206
- ctype = 'application/octet-stream'
207
- maintype, subtype = ctype.split('/', 1)
208
- with open(xtra, 'rb') as fp:
209
- msg.add_attachment(fp.read(), maintype, subtype,
210
- cte="base64", filename=xtra)
214
+ ctype = "application/octet-stream"
215
+ maintype, subtype = ctype.split("/", 1)
216
+ with open(xtra, "rb") as fp:
217
+ msg.add_attachment(
218
+ fp.read(),
219
+ maintype,
220
+ subtype,
221
+ cte="base64",
222
+ filename=xtra,
223
+ )
211
224
  return msg
212
225
 
213
226
  def _set_header(self, msg, header, value):
@@ -215,25 +228,30 @@ class MailService(Service):
215
228
 
216
229
  def set_headers(self, msg):
217
230
  """Put on the current message the header items associated to footprint attributes."""
218
- self._set_header(msg, 'From', self.sender)
219
- self._set_header(msg, 'To', self.commaspace.join(self.to.split()))
220
- self._set_header(msg, 'Subject', self.subject)
231
+ self._set_header(msg, "From", self.sender)
232
+ self._set_header(msg, "To", self.commaspace.join(self.to.split()))
233
+ self._set_header(msg, "Subject", self.subject)
221
234
  if self.replyto is not None:
222
- self._set_header(msg, 'Reply-To', self.commaspace.join(self.replyto.split()))
235
+ self._set_header(
236
+ msg, "Reply-To", self.commaspace.join(self.replyto.split())
237
+ )
223
238
 
224
239
  @contextlib.contextmanager
225
240
  def smtp_entrypoints(self):
226
241
  import smtplib
227
- my_smtpserver = self.actual_value('smtpserver',
228
- as_var='VORTEX_SMTPSERVER',
229
- default='localhost')
230
- my_smtpport = self.actual_value('smtpport',
231
- as_var='VORTEX_SMTPPORT',
232
- default=smtplib.SMTP_PORT)
242
+
243
+ my_smtpserver = self.actual_value(
244
+ "smtpserver", as_var="VORTEX_SMTPSERVER", default="localhost"
245
+ )
246
+ my_smtpport = self.actual_value(
247
+ "smtpport", as_var="VORTEX_SMTPPORT", default=smtplib.SMTP_PORT
248
+ )
233
249
  if not self.sh.default_target.isnetworknode:
234
- sshobj = self.sh.ssh('network', virtualnode=True, mandatory_hostcheck=False)
250
+ sshobj = self.sh.ssh(
251
+ "network", virtualnode=True, mandatory_hostcheck=False
252
+ )
235
253
  with sshobj.tunnel(my_smtpserver, my_smtpport) as tun:
236
- yield 'localhost', tun.entranceport
254
+ yield "localhost", tun.entranceport
237
255
  else:
238
256
  yield my_smtpserver, my_smtpport
239
257
 
@@ -246,9 +264,10 @@ class MailService(Service):
246
264
  msgcorpus = msg.as_string()
247
265
  with self.smtp_entrypoints() as (smtpserver, smtpport):
248
266
  import smtplib
267
+
249
268
  extras = dict()
250
269
  if smtpport:
251
- extras['port'] = smtpport
270
+ extras["port"] = smtpport
252
271
  smtp = smtplib.SMTP(smtpserver, **extras)
253
272
  if self.smtpuser and self.smtppass:
254
273
  smtp.login(self.smtpuser, self.smtppass)
@@ -265,20 +284,15 @@ class ReportService(Service):
265
284
 
266
285
  _abstract = True
267
286
  _footprint = dict(
268
- info = 'Report services class',
269
- attr = dict(
270
- kind = dict(
271
- values = ['sendreport']
272
- ),
273
- sender = dict(
274
- optional = True,
275
- default = '[glove::user]',
276
- ),
277
- subject = dict(
278
- optional = True,
279
- default = 'Test'
280
- ),
281
- )
287
+ info="Report services class",
288
+ attr=dict(
289
+ kind=dict(values=["sendreport"]),
290
+ sender=dict(
291
+ optional=True,
292
+ default="[glove::user]",
293
+ ),
294
+ subject=dict(optional=True, default="Test"),
295
+ ),
282
296
  )
283
297
 
284
298
  def __call__(self, *args):
@@ -291,14 +305,14 @@ class FileReportService(ReportService):
291
305
 
292
306
  _abstract = True
293
307
  _footprint = dict(
294
- info = 'File Report services class',
295
- attr = dict(
296
- kind = dict(
297
- values = ['sendreport', 'sendfilereport'],
298
- remap = dict(sendfilereport = 'sendreport'),
299
- ),
300
- filename = dict(),
301
- )
308
+ info="File Report services class",
309
+ attr=dict(
310
+ kind=dict(
311
+ values=["sendreport", "sendfilereport"],
312
+ remap=dict(sendfilereport="sendreport"),
313
+ ),
314
+ filename=dict(),
315
+ ),
302
316
  )
303
317
 
304
318
 
@@ -318,57 +332,74 @@ class SSHProxy(Service):
318
332
  """
319
333
 
320
334
  _footprint = dict(
321
- info = 'Remote command proxy',
322
- attr = dict(
323
- kind = dict(
324
- values = ['ssh', 'ssh_proxy'],
325
- remap = dict(autoremap = 'first'),
326
- ),
327
- hostname = dict(),
328
- genericnode = dict(
329
- optional = True,
330
- default = None,
331
- access = 'rwx',
332
- ),
333
- nodetype = dict(
334
- optional = True,
335
- values = ['login', 'transfer', 'transfert', 'network', 'agt', 'syslog'],
336
- default = 'network',
337
- remap = dict(transfer = 'transfert'),
338
- ),
339
- permut = dict(
340
- type = bool,
341
- optional = True,
342
- default = True,
343
- ),
344
- maxtries = dict(
345
- type = int,
346
- optional = True,
347
- default = 2,
348
- ),
349
- sshopts = dict(
350
- optional = True,
351
- type = footprints.FPList,
352
- default = None,
353
- ),
354
- )
335
+ info="Remote command proxy",
336
+ attr=dict(
337
+ kind=dict(
338
+ values=["ssh", "ssh_proxy"],
339
+ remap=dict(autoremap="first"),
340
+ ),
341
+ hostname=dict(),
342
+ genericnode=dict(
343
+ optional=True,
344
+ default=None,
345
+ access="rwx",
346
+ ),
347
+ nodetype=dict(
348
+ optional=True,
349
+ values=[
350
+ "login",
351
+ "transfer",
352
+ "transfert",
353
+ "network",
354
+ "agt",
355
+ "syslog",
356
+ ],
357
+ default="network",
358
+ remap=dict(transfer="transfert"),
359
+ ),
360
+ permut=dict(
361
+ type=bool,
362
+ optional=True,
363
+ default=True,
364
+ ),
365
+ maxtries=dict(
366
+ type=int,
367
+ optional=True,
368
+ default=2,
369
+ ),
370
+ sshopts=dict(
371
+ optional=True,
372
+ type=footprints.FPList,
373
+ default=None,
374
+ ),
375
+ ),
355
376
  )
356
377
 
357
378
  def __init__(self, *args, **kw):
358
- logger.debug('Remote command proxy init %s', self.__class__)
379
+ logger.debug("Remote command proxy init %s", self.__class__)
359
380
  super().__init__(*args, **kw)
360
381
  hostname, virtualnode = self._actual_hostname()
361
- extra_sshopts = None if self.sshopts is None else ' '.join(self.sshopts)
362
- self._sshobj = self.sh.ssh(hostname, sshopts=extra_sshopts,
363
- maxtries=self.maxtries, virtualnode=virtualnode,
364
- permut=self.permut, mandatory_hostcheck=False)
382
+ extra_sshopts = (
383
+ None if self.sshopts is None else " ".join(self.sshopts)
384
+ )
385
+ self._sshobj = self.sh.ssh(
386
+ hostname,
387
+ sshopts=extra_sshopts,
388
+ maxtries=self.maxtries,
389
+ virtualnode=virtualnode,
390
+ permut=self.permut,
391
+ mandatory_hostcheck=False,
392
+ )
365
393
 
366
394
  def _actual_hostname(self):
367
395
  """Build a list of candidate target hostnames."""
368
396
  myhostname = self.hostname.strip().lower()
369
397
  virtualnode = False
370
- if myhostname == 'node':
371
- if self.genericnode is not None and self.genericnode != 'no_generic':
398
+ if myhostname == "node":
399
+ if (
400
+ self.genericnode is not None
401
+ and self.genericnode != "no_generic"
402
+ ):
372
403
  myhostname = self.genericnode
373
404
  else:
374
405
  myhostname = self.nodetype
@@ -381,7 +412,7 @@ class SSHProxy(Service):
381
412
 
382
413
  def __call__(self, *args):
383
414
  """Remote execution."""
384
- return self._sshobj.execute(' '.join(args))
415
+ return self._sshobj.execute(" ".join(args))
385
416
 
386
417
 
387
418
  class JeevesService(Service):
@@ -390,38 +421,39 @@ class JeevesService(Service):
390
421
  """
391
422
 
392
423
  _footprint = dict(
393
- info = 'Jeeves services class',
394
- attr = dict(
395
- kind = dict(
396
- values = ['askjeeves']
397
- ),
398
- todo = dict(),
399
- jname = dict(
400
- optional = True,
401
- default = 'test',
402
- ),
403
- juser = dict(
404
- optional = True,
405
- default = '[glove::user]',
406
- ),
407
- jpath = dict(
408
- optional = True,
409
- default = None,
410
- access = 'rwx',
411
- ),
412
- jfile = dict(
413
- optional = True,
414
- default = 'vortex',
415
- ),
416
- )
424
+ info="Jeeves services class",
425
+ attr=dict(
426
+ kind=dict(values=["askjeeves"]),
427
+ todo=dict(),
428
+ jname=dict(
429
+ optional=True,
430
+ default="test",
431
+ ),
432
+ juser=dict(
433
+ optional=True,
434
+ default="[glove::user]",
435
+ ),
436
+ jpath=dict(
437
+ optional=True,
438
+ default=None,
439
+ access="rwx",
440
+ ),
441
+ jfile=dict(
442
+ optional=True,
443
+ default="vortex",
444
+ ),
445
+ ),
417
446
  )
418
447
 
419
448
  def __call__(self, *args):
420
449
  """Main action: ..."""
421
450
  if self.jpath is None:
422
- self.jpath = self.sh.path.join(self.env.HOME, 'jeeves', self.jname, 'depot')
451
+ self.jpath = self.sh.path.join(
452
+ self.env.HOME, "jeeves", self.jname, "depot"
453
+ )
423
454
  if self.sh.path.isdir(self.jpath):
424
455
  from jeeves import bertie
456
+
425
457
  data = dict()
426
458
  for arg in args:
427
459
  data.update(arg)
@@ -429,10 +461,11 @@ class JeevesService(Service):
429
461
  user=self.juser,
430
462
  jtag=self.sh.path.join(self.jpath, self.jfile),
431
463
  todo=self.todo,
432
- mail=data.pop('mail', self.glove.xmail),
433
- apps=data.pop('apps', (self.glove.vapp,)),
434
- conf=data.pop('conf', (self.glove.vconf,)),
435
- task=self.env.get('JOBNAME') or self.env.get('SMSNAME', 'interactif'),
464
+ mail=data.pop("mail", self.glove.xmail),
465
+ apps=data.pop("apps", (self.glove.vapp,)),
466
+ conf=data.pop("conf", (self.glove.vconf,)),
467
+ task=self.env.get("JOBNAME")
468
+ or self.env.get("SMSNAME", "interactif"),
436
469
  )
437
470
  fulltalk.update(
438
471
  data=data,
@@ -440,7 +473,7 @@ class JeevesService(Service):
440
473
  jr = bertie.ask(**fulltalk)
441
474
  return jr.todo, jr.last
442
475
  else:
443
- logger.error('No valid path to jeeves <%s>', self.jpath)
476
+ logger.error("No valid path to jeeves <%s>", self.jpath)
444
477
  return None
445
478
 
446
479
 
@@ -453,37 +486,41 @@ class HideService(Service):
453
486
  """
454
487
 
455
488
  _footprint = dict(
456
- info = 'Hide a given object on current filesystem',
457
- attr = dict(
458
- kind = dict(
459
- values = ['hidden', 'hide', 'hiddencache'],
460
- remap = dict(autoremap = 'first'),
489
+ info="Hide a given object on current filesystem",
490
+ attr=dict(
491
+ kind=dict(
492
+ values=["hidden", "hide", "hiddencache"],
493
+ remap=dict(autoremap="first"),
461
494
  ),
462
- rootdir = dict(
463
- optional = True,
464
- default = None,
495
+ rootdir=dict(
496
+ optional=True,
497
+ default=None,
465
498
  ),
466
- headdir = dict(
467
- optional = True,
468
- default = 'hidden',
499
+ headdir=dict(
500
+ optional=True,
501
+ default="hidden",
469
502
  ),
470
- asfmt = dict(
471
- optional = True,
472
- default = None,
503
+ asfmt=dict(
504
+ optional=True,
505
+ default=None,
473
506
  ),
474
- )
507
+ ),
475
508
  )
476
509
 
477
510
  def find_rootdir(self, filename):
478
511
  """Find a path for hiding files on the same filesystem."""
479
512
  username = self.sh.getlogname()
480
- work_dir = self.sh.path.join(self.sh.find_mount_point(filename), 'work')
513
+ work_dir = self.sh.path.join(
514
+ self.sh.find_mount_point(filename), "work"
515
+ )
481
516
  if not self.sh.path.exists(work_dir):
482
517
  logger.warning("path <%s> doesn't exist", work_dir)
483
518
  fullpath = self.sh.path.realpath(filename)
484
519
  if username not in fullpath:
485
- logger.error('No login <%s> in path <%s>', username, fullpath)
486
- raise ValueError('Login name not in actual path for hiding data')
520
+ logger.error("No login <%s> in path <%s>", username, fullpath)
521
+ raise ValueError(
522
+ "Login name not in actual path for hiding data"
523
+ )
487
524
  work_dir = fullpath.partition(username)[0]
488
525
  logger.warning("using work_dir = <%s>", work_dir)
489
526
  hidden_path = self.sh.path.join(work_dir, username, self.headdir)
@@ -494,21 +531,25 @@ class HideService(Service):
494
531
 
495
532
  rootdir = self.rootdir
496
533
  if rootdir is None:
497
- rootdir = self.sh.default_target.get('hidden_rootdir', None)
534
+ rootdir = self.sh.default_target.get("hidden_rootdir", None)
498
535
  if rootdir is not None:
499
536
  rootdir = self.sh.path.expanduser(rootdir)
500
537
 
501
538
  actual_rootdir = rootdir or self.find_rootdir(filename)
502
539
  destination = self.sh.path.join(
503
540
  actual_rootdir,
504
- '.'.join((
505
- 'HIDDEN',
506
- date.now().strftime('%Y%m%d%H%M%S.%f'),
507
- 'P{:06d}'.format(self.sh.getpid()),
508
- hashlib.md5(self.sh.path.abspath(filename).encode(encoding='utf-8')).hexdigest()
509
- ))
541
+ ".".join(
542
+ (
543
+ "HIDDEN",
544
+ date.now().strftime("%Y%m%d%H%M%S.%f"),
545
+ "P{:06d}".format(self.sh.getpid()),
546
+ hashlib.md5(
547
+ self.sh.path.abspath(filename).encode(encoding="utf-8")
548
+ ).hexdigest(),
549
+ )
550
+ ),
510
551
  )
511
- self.sh.cp(filename, destination, intent='in', fmt=self.asfmt)
552
+ self.sh.cp(filename, destination, intent="in", fmt=self.asfmt)
512
553
  return destination
513
554
 
514
555
 
@@ -519,20 +560,22 @@ class Directory:
519
560
  Directory (en) means Annuaire (fr).
520
561
  """
521
562
 
522
- def __init__(self, inifile, domain='meteo.fr', encoding=None):
563
+ def __init__(self, inifile, domain="meteo.fr", encoding=None):
523
564
  """Keep aliases in memory, as a dict of sets."""
524
- config = GenericConfigParser(inifile, encoding=encoding)
565
+ config = configparser.ConfigParser()
566
+ config.read(inifile, encoding=encoding)
525
567
  try:
526
- self.domain = config.get('general', 'default_domain')
527
- except NoOptionError:
568
+ self.domain = config.get("general", "default_domain")
569
+ except configparser.NoOptionError:
528
570
  self.domain = domain
529
571
  self.aliases = {
530
- k.lower(): set(v.lower().replace(',', ' ').split())
531
- for (k, v) in config.items('aliases')
572
+ k.lower(): set(v.lower().replace(",", " ").split())
573
+ for (k, v) in config.items("aliases")
532
574
  }
533
575
  count = self._flatten()
534
- logger.debug('opmail aliases flattened in %d iterations:\n%s',
535
- count, str(self))
576
+ logger.debug(
577
+ "opmail aliases flattened in %d iterations:\n%s", count, str(self)
578
+ )
536
579
 
537
580
  def get_addresses(self, definition, add_domain=True):
538
581
  """
@@ -540,20 +583,24 @@ class Directory:
540
583
  may reference aliases.
541
584
  """
542
585
  addresses = set()
543
- for item in definition.lower().replace(',', ' ').split():
586
+ for item in definition.lower().replace(",", " ").split():
544
587
  if item in self.aliases:
545
588
  addresses |= self.aliases[item]
546
589
  else:
547
590
  addresses |= {item}
548
591
  if add_domain:
549
- return ' '.join(self._add_domain(addresses))
550
- return ' '.join(addresses)
592
+ return " ".join(self._add_domain(addresses))
593
+ return " ".join(addresses)
551
594
 
552
595
  def __str__(self):
553
- return '\n'.join(sorted(
554
- ['{}: {}'.format(k, ' '.join(sorted(v)))
555
- for (k, v) in self.aliases.items()]
556
- ))
596
+ return "\n".join(
597
+ sorted(
598
+ [
599
+ "{}: {}".format(k, " ".join(sorted(v)))
600
+ for (k, v) in self.aliases.items()
601
+ ]
602
+ )
603
+ )
557
604
 
558
605
  def _flatten(self):
559
606
  """Resolve recursive definitions from the dict of sets."""
@@ -564,11 +611,18 @@ class Directory:
564
611
  count += 1
565
612
  for kref, vref in self.aliases.items():
566
613
  if kref in vref:
567
- logger.error('Cycle detected in the aliases directory.\n'
568
- 'offending key: %s.\n'
569
- 'directory being flattened:\n%s',
570
- str(kref), str(self))
571
- raise ValueError('Cycle for key <{}> in directory definition'.format(kref))
614
+ logger.error(
615
+ "Cycle detected in the aliases directory.\n"
616
+ "offending key: %s.\n"
617
+ "directory being flattened:\n%s",
618
+ str(kref),
619
+ str(self),
620
+ )
621
+ raise ValueError(
622
+ "Cycle for key <{}> in directory definition".format(
623
+ kref
624
+ )
625
+ )
572
626
  for k, v in self.aliases.items():
573
627
  if kref in v:
574
628
  v -= {kref}
@@ -579,11 +633,7 @@ class Directory:
579
633
 
580
634
  def _add_domain(self, aset):
581
635
  """Add domain where missing in a set of addresses."""
582
- return {
583
- v if '@' in v
584
- else v + '@' + self.domain
585
- for v in aset
586
- }
636
+ return {v if "@" in v else v + "@" + self.domain for v in aset}
587
637
 
588
638
 
589
639
  class PromptService(Service):
@@ -593,16 +643,16 @@ class PromptService(Service):
593
643
  """
594
644
 
595
645
  _footprint = dict(
596
- info = 'Simulate a call to a Service.',
597
- attr = dict(
598
- kind = dict(
599
- values = ('prompt',),
646
+ info="Simulate a call to a Service.",
647
+ attr=dict(
648
+ kind=dict(
649
+ values=("prompt",),
600
650
  ),
601
- comment = dict(
602
- optional = True,
603
- default = None,
651
+ comment=dict(
652
+ optional=True,
653
+ default=None,
604
654
  ),
605
- )
655
+ ),
606
656
  )
607
657
 
608
658
  def __call__(self, options):
@@ -610,8 +660,8 @@ class PromptService(Service):
610
660
 
611
661
  pf = EncodedPrettyPrinter().pformat
612
662
  logger_action = getattr(logger, self.level, logger.warning)
613
- msg = (self.comment or 'PromptService was called.') + '\noptions = {}'
614
- logger_action(msg.format(pf(options)).replace('\n', '\n<prompt>'))
663
+ msg = (self.comment or "PromptService was called.") + "\noptions = {}"
664
+ logger_action(msg.format(pf(options)).replace("\n", "\n<prompt>"))
615
665
  return True
616
666
 
617
667
 
@@ -622,51 +672,51 @@ class TemplatedMailService(MailService):
622
672
  """
623
673
 
624
674
  _footprint = dict(
625
- info = 'Templated mail services class',
626
- attr = dict(
627
- kind = dict(
628
- values = ['templatedmail'],
629
- ),
630
- id = dict(
631
- alias = ('template',),
632
- ),
633
- subject = dict(
634
- optional = True,
635
- default = None,
636
- access = 'rwx',
637
- ),
638
- to = dict(
639
- optional = True,
640
- default = None,
641
- access = 'rwx',
642
- ),
643
- message = dict(
644
- access = 'rwx',
645
- ),
646
- directory = dict(
647
- type = Directory,
648
- optional = True,
649
- default = None,
650
- ),
651
- catalog = dict(
652
- type = GenericConfigParser,
653
- ),
654
- dryrun = dict(
655
- info = "Do not actually send the email. Just render the template.",
656
- type = bool,
657
- optional = True,
658
- default = False,
659
- ),
660
- )
675
+ info="Templated mail services class",
676
+ attr=dict(
677
+ kind=dict(
678
+ values=["templatedmail"],
679
+ ),
680
+ id=dict(
681
+ alias=("template",),
682
+ ),
683
+ subject=dict(
684
+ optional=True,
685
+ default=None,
686
+ access="rwx",
687
+ ),
688
+ to=dict(
689
+ optional=True,
690
+ default=None,
691
+ access="rwx",
692
+ ),
693
+ message=dict(
694
+ access="rwx",
695
+ ),
696
+ directory=dict(
697
+ type=Directory,
698
+ optional=True,
699
+ default=None,
700
+ ),
701
+ catalog=dict(
702
+ type=configparser.ConfigParser,
703
+ ),
704
+ dryrun=dict(
705
+ info="Do not actually send the email. Just render the template.",
706
+ type=bool,
707
+ optional=True,
708
+ default=False,
709
+ ),
710
+ ),
661
711
  )
662
712
 
663
713
  _TEMPLATES_SUBDIR = None
664
714
 
665
715
  def __init__(self, *args, **kw):
666
- ticket = kw.pop('ticket', sessions.get())
716
+ ticket = kw.pop("ticket", sessions.get())
667
717
  super().__init__(*args, **kw)
668
718
  self._ticket = ticket
669
- logger.debug('TemplatedMail init for id <%s>', self.id)
719
+ logger.debug("TemplatedMail init for id <%s>", self.id)
670
720
 
671
721
  @property
672
722
  def ticket(self):
@@ -674,19 +724,21 @@ class TemplatedMailService(MailService):
674
724
 
675
725
  def header(self):
676
726
  """String prepended to the message body."""
677
- return ''
727
+ return ""
678
728
 
679
729
  def trailer(self):
680
730
  """String appended to the message body."""
681
- return ''
731
+ return ""
682
732
 
683
733
  def get_catalog_section(self):
684
734
  """Read section <id> (a dict-like) from the catalog."""
685
735
  try:
686
736
  section = dict(self.catalog.items(self.id))
687
- except NoSectionError:
688
- logger.error('Section <%s> is missing in catalog <%s>',
689
- self.id, self.catalog.file)
737
+ except configparser.NoSectionError:
738
+ logger.error(
739
+ "Section <%s> is missing in catalog",
740
+ self.id,
741
+ )
690
742
  section = None
691
743
  return section
692
744
 
@@ -707,28 +759,31 @@ class TemplatedMailService(MailService):
707
759
  """
708
760
  if not isinstance(tpl, (Template, LegacyTemplatingAdapter)):
709
761
  tpl = Template(tpl)
710
- result = ''
762
+ result = ""
711
763
  for level in range(depth):
712
764
  try:
713
765
  result = tpl.substitute(tpldict)
714
766
  except KeyError as exc:
715
- logger.error('Undefined key <%s> in template substitution level %d',
716
- str(exc), level + 1)
767
+ logger.error(
768
+ "Undefined key <%s> in template substitution level %d",
769
+ str(exc),
770
+ level + 1,
771
+ )
717
772
  result = tpl.safe_substitute(tpldict)
718
773
  except ValueError as exc:
719
- logger.error('Illegal syntax in template: %s', exc)
774
+ logger.error("Illegal syntax in template: %s", exc)
720
775
  result = tpl.safe_substitute(tpldict)
721
776
  tpl = Template(result)
722
777
  return result
723
778
 
724
779
  def _template_name_rewrite(self, tplguess):
725
- base = '@'
780
+ base = "@"
726
781
  if self._TEMPLATES_SUBDIR is not None:
727
- base = '@{!s}/'.format(self._TEMPLATES_SUBDIR)
782
+ base = "@{!s}/".format(self._TEMPLATES_SUBDIR)
728
783
  if not tplguess.startswith(base):
729
784
  tplguess = base + tplguess
730
- if not tplguess.endswith('.tpl'):
731
- tplguess += '.tpl'
785
+ if not tplguess.endswith(".tpl"):
786
+ tplguess += ".tpl"
732
787
  return tplguess
733
788
 
734
789
  def get_message(self, tpldict):
@@ -739,13 +794,15 @@ class TemplatedMailService(MailService):
739
794
  * header and trailer are added.
740
795
  """
741
796
  tpl = self.message
742
- if tpl == '':
743
- tplfile = self.section.get('template', self.id)
797
+ if tpl == "":
798
+ tplfile = self.section.get("template", self.id)
744
799
  tplfile = self._template_name_rewrite(tplfile)
745
800
  try:
746
- tpl = load_template(self.ticket, tplfile, encoding=self.inputs_charset)
801
+ tpl = load_template(
802
+ self.ticket, tplfile, encoding=self.inputs_charset
803
+ )
747
804
  except ValueError as exc:
748
- logger.error('%s', exc.message)
805
+ logger.error("%s", exc.message)
749
806
  return None
750
807
  message = self.substitute(tpl, tpldict)
751
808
  return self.header() + message + self.trailer()
@@ -758,9 +815,11 @@ class TemplatedMailService(MailService):
758
815
  """
759
816
  tpl = self.subject
760
817
  if tpl is None:
761
- tpl = self.section.get('subject', None)
818
+ tpl = self.section.get("subject", None)
762
819
  if tpl is None:
763
- logger.error('Missing <subject> definition for id <%s>.', self.id)
820
+ logger.error(
821
+ "Missing <subject> definition for id <%s>.", self.id
822
+ )
764
823
  return None
765
824
  subject = self.substitute(tpl, tpldict)
766
825
  return subject
@@ -776,9 +835,9 @@ class TemplatedMailService(MailService):
776
835
  """
777
836
  tpl = self.to
778
837
  if tpl is None:
779
- tpl = self.section.get('to', None)
838
+ tpl = self.section.get("to", None)
780
839
  if tpl is None:
781
- logger.error('Missing <to> definition for id <%s>.', self.id)
840
+ logger.error("Missing <to> definition for id <%s>.", self.id)
782
841
  return None
783
842
  to = self.substitute(tpl, tpldict)
784
843
  if self.directory:
@@ -834,38 +893,48 @@ class TemplatedMailService(MailService):
834
893
 
835
894
 
836
895
  class AbstractRdTemplatedMailService(TemplatedMailService):
837
-
838
896
  _abstract = True
839
897
 
840
898
  def header(self):
841
899
  """String prepended to the message body."""
842
900
  now = date.now()
843
- stamp1 = now.strftime('%A %d %B %Y')
844
- stamp2 = now.strftime('%X')
845
- return 'Email sent on {} at {} (from: {}).\n--\n\n'.format(stamp1, stamp2,
846
- self.sh.default_target.hostname)
901
+ stamp1 = now.strftime("%A %d %B %Y")
902
+ stamp2 = now.strftime("%X")
903
+ return "Email sent on {} at {} (from: {}).\n--\n\n".format(
904
+ stamp1, stamp2, self.sh.default_target.hostname
905
+ )
847
906
 
848
907
  def substitution_dictionary(self, add_ons=None):
849
908
  sdict = super().substitution_dictionary(add_ons=add_ons)
850
- sdict['jobid'] = self.sh.guess_job_identifier()
909
+ sdict["jobid"] = self.sh.guess_job_identifier()
851
910
  # Try to detect MTOOL data (this may be empty if MTOOL is not used):
852
911
  if self.env.MTOOL_STEP:
853
- mt_stack = ['\nMTOOL details:', ]
854
- mt_items = ['mtool_step_{:s}'.format(i)
855
- for i in ('abort', 'depot', 'spool', 'idnum')
856
- if 'mtool_step_{:s}'.format(i) in self.env]
857
- print_tablelike('{:s} = {!s}', mt_items, [self.env[i] for i in mt_items],
858
- output_callback=mt_stack.append)
859
- sdict['mtool_info'] = '\n'.join(mt_stack) + '\n'
912
+ mt_stack = [
913
+ "\nMTOOL details:",
914
+ ]
915
+ mt_items = [
916
+ "mtool_step_{:s}".format(i)
917
+ for i in ("abort", "depot", "spool", "idnum")
918
+ if "mtool_step_{:s}".format(i) in self.env
919
+ ]
920
+ print_tablelike(
921
+ "{:s} = {!s}",
922
+ mt_items,
923
+ [self.env[i] for i in mt_items],
924
+ output_callback=mt_stack.append,
925
+ )
926
+ sdict["mtool_info"] = "\n".join(mt_stack) + "\n"
860
927
  else:
861
- sdict['mtool_info'] = ''
928
+ sdict["mtool_info"] = ""
862
929
  # The list of footprints' defaults
863
930
  fpdefaults = footprints.setup.defaults
864
- sdict['fpdefaults'] = pprint.pformat(fpdefaults, indent=2)
931
+ sdict["fpdefaults"] = pprint.pformat(fpdefaults, indent=2)
865
932
  # A condensed indication on date/cutoff
866
- sdict['timeid'] = fpdefaults.get('date', None)
867
- if sdict['timeid']:
868
- sdict['timeid'] = sdict['timeid'].vortex(cutoff=fpdefaults.get('cutoff', 'X'))
933
+ sdict["timeid"] = fpdefaults.get("date", None)
934
+ if sdict["timeid"]:
935
+ sdict["timeid"] = sdict["timeid"].vortex(
936
+ cutoff=fpdefaults.get("cutoff", "X")
937
+ )
869
938
  # The generic host/cluster name
870
- sdict['host'] = self.sh.default_target.inetname
939
+ sdict["host"] = self.sh.default_target.inetname
871
940
  return sdict