vortex-nwp 2.0.0b1__py3-none-any.whl → 2.0.0b2__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 (139) hide show
  1. vortex/__init__.py +59 -45
  2. vortex/algo/__init__.py +3 -2
  3. vortex/algo/components.py +940 -614
  4. vortex/algo/mpitools.py +802 -497
  5. vortex/algo/serversynctools.py +34 -33
  6. vortex/config.py +19 -22
  7. vortex/data/__init__.py +9 -3
  8. vortex/data/abstractstores.py +593 -655
  9. vortex/data/containers.py +217 -162
  10. vortex/data/contents.py +65 -39
  11. vortex/data/executables.py +93 -102
  12. vortex/data/flow.py +40 -34
  13. vortex/data/geometries.py +228 -132
  14. vortex/data/handlers.py +428 -225
  15. vortex/data/outflow.py +15 -15
  16. vortex/data/providers.py +185 -163
  17. vortex/data/resources.py +48 -42
  18. vortex/data/stores.py +544 -413
  19. vortex/gloves.py +114 -87
  20. vortex/layout/__init__.py +1 -8
  21. vortex/layout/contexts.py +150 -84
  22. vortex/layout/dataflow.py +353 -202
  23. vortex/layout/monitor.py +264 -128
  24. vortex/nwp/__init__.py +5 -2
  25. vortex/nwp/algo/__init__.py +14 -5
  26. vortex/nwp/algo/assim.py +205 -151
  27. vortex/nwp/algo/clim.py +683 -517
  28. vortex/nwp/algo/coupling.py +447 -225
  29. vortex/nwp/algo/eda.py +437 -229
  30. vortex/nwp/algo/eps.py +403 -231
  31. vortex/nwp/algo/forecasts.py +420 -271
  32. vortex/nwp/algo/fpserver.py +683 -307
  33. vortex/nwp/algo/ifsnaming.py +205 -145
  34. vortex/nwp/algo/ifsroot.py +210 -122
  35. vortex/nwp/algo/monitoring.py +132 -76
  36. vortex/nwp/algo/mpitools.py +321 -191
  37. vortex/nwp/algo/odbtools.py +617 -353
  38. vortex/nwp/algo/oopsroot.py +449 -273
  39. vortex/nwp/algo/oopstests.py +90 -56
  40. vortex/nwp/algo/request.py +287 -206
  41. vortex/nwp/algo/stdpost.py +878 -522
  42. vortex/nwp/data/__init__.py +22 -4
  43. vortex/nwp/data/assim.py +125 -137
  44. vortex/nwp/data/boundaries.py +121 -68
  45. vortex/nwp/data/climfiles.py +193 -211
  46. vortex/nwp/data/configfiles.py +73 -69
  47. vortex/nwp/data/consts.py +426 -401
  48. vortex/nwp/data/ctpini.py +59 -43
  49. vortex/nwp/data/diagnostics.py +94 -66
  50. vortex/nwp/data/eda.py +50 -51
  51. vortex/nwp/data/eps.py +195 -146
  52. vortex/nwp/data/executables.py +440 -434
  53. vortex/nwp/data/fields.py +63 -48
  54. vortex/nwp/data/gridfiles.py +183 -111
  55. vortex/nwp/data/logs.py +250 -217
  56. vortex/nwp/data/modelstates.py +180 -151
  57. vortex/nwp/data/monitoring.py +72 -99
  58. vortex/nwp/data/namelists.py +254 -202
  59. vortex/nwp/data/obs.py +400 -308
  60. vortex/nwp/data/oopsexec.py +22 -20
  61. vortex/nwp/data/providers.py +90 -65
  62. vortex/nwp/data/query.py +71 -82
  63. vortex/nwp/data/stores.py +49 -36
  64. vortex/nwp/data/surfex.py +136 -137
  65. vortex/nwp/syntax/__init__.py +1 -1
  66. vortex/nwp/syntax/stdattrs.py +173 -111
  67. vortex/nwp/tools/__init__.py +2 -2
  68. vortex/nwp/tools/addons.py +22 -17
  69. vortex/nwp/tools/agt.py +24 -12
  70. vortex/nwp/tools/bdap.py +16 -5
  71. vortex/nwp/tools/bdcp.py +4 -1
  72. vortex/nwp/tools/bdm.py +3 -0
  73. vortex/nwp/tools/bdmp.py +14 -9
  74. vortex/nwp/tools/conftools.py +728 -378
  75. vortex/nwp/tools/drhook.py +12 -8
  76. vortex/nwp/tools/grib.py +65 -39
  77. vortex/nwp/tools/gribdiff.py +22 -17
  78. vortex/nwp/tools/ifstools.py +82 -42
  79. vortex/nwp/tools/igastuff.py +167 -143
  80. vortex/nwp/tools/mars.py +14 -2
  81. vortex/nwp/tools/odb.py +234 -125
  82. vortex/nwp/tools/partitioning.py +61 -37
  83. vortex/nwp/tools/satrad.py +27 -12
  84. vortex/nwp/util/async.py +83 -55
  85. vortex/nwp/util/beacon.py +10 -10
  86. vortex/nwp/util/diffpygram.py +174 -86
  87. vortex/nwp/util/ens.py +144 -63
  88. vortex/nwp/util/hooks.py +30 -19
  89. vortex/nwp/util/taskdeco.py +28 -24
  90. vortex/nwp/util/usepygram.py +278 -172
  91. vortex/nwp/util/usetnt.py +31 -17
  92. vortex/sessions.py +72 -39
  93. vortex/syntax/__init__.py +1 -1
  94. vortex/syntax/stdattrs.py +410 -171
  95. vortex/syntax/stddeco.py +31 -22
  96. vortex/toolbox.py +327 -192
  97. vortex/tools/__init__.py +11 -2
  98. vortex/tools/actions.py +125 -59
  99. vortex/tools/addons.py +111 -92
  100. vortex/tools/arm.py +42 -22
  101. vortex/tools/compression.py +72 -69
  102. vortex/tools/date.py +11 -4
  103. vortex/tools/delayedactions.py +242 -132
  104. vortex/tools/env.py +75 -47
  105. vortex/tools/folder.py +342 -171
  106. vortex/tools/grib.py +311 -149
  107. vortex/tools/lfi.py +423 -216
  108. vortex/tools/listings.py +109 -40
  109. vortex/tools/names.py +218 -156
  110. vortex/tools/net.py +632 -298
  111. vortex/tools/parallelism.py +93 -61
  112. vortex/tools/prestaging.py +55 -31
  113. vortex/tools/schedulers.py +172 -105
  114. vortex/tools/services.py +402 -333
  115. vortex/tools/storage.py +293 -358
  116. vortex/tools/surfex.py +24 -24
  117. vortex/tools/systems.py +1211 -631
  118. vortex/tools/targets.py +156 -100
  119. vortex/util/__init__.py +1 -1
  120. vortex/util/config.py +377 -327
  121. vortex/util/empty.py +2 -2
  122. vortex/util/helpers.py +56 -24
  123. vortex/util/introspection.py +18 -12
  124. vortex/util/iosponge.py +8 -4
  125. vortex/util/roles.py +4 -6
  126. vortex/util/storefunctions.py +39 -13
  127. vortex/util/structs.py +3 -3
  128. vortex/util/worker.py +29 -17
  129. vortex_nwp-2.0.0b2.dist-info/METADATA +66 -0
  130. vortex_nwp-2.0.0b2.dist-info/RECORD +142 -0
  131. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.dist-info}/WHEEL +1 -1
  132. vortex/layout/appconf.py +0 -109
  133. vortex/layout/jobs.py +0 -1276
  134. vortex/layout/nodes.py +0 -1424
  135. vortex/layout/subjobs.py +0 -464
  136. vortex_nwp-2.0.0b1.dist-info/METADATA +0 -50
  137. vortex_nwp-2.0.0b1.dist-info/RECORD +0 -146
  138. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.dist-info}/LICENSE +0 -0
  139. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.0.0b2.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,11 @@ 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
+ GenericConfigParser,
24
+ load_template,
25
+ LegacyTemplatingAdapter,
26
+ )
23
27
 
24
28
  #: No automatic export
25
29
  __all__ = []
@@ -27,7 +31,7 @@ __all__ = []
27
31
  logger = loggers.getLogger(__name__)
28
32
 
29
33
  # See logging.handlers.SysLogHandler.priority_map
30
- criticals = ['debug', 'info', 'error', 'warning', 'critical']
34
+ criticals = ["debug", "info", "error", "warning", "critical"]
31
35
 
32
36
 
33
37
  class Service(footprints.FootprintBase):
@@ -36,31 +40,31 @@ class Service(footprints.FootprintBase):
36
40
  """
37
41
 
38
42
  _abstract = True
39
- _collector = ('service',)
43
+ _collector = ("service",)
40
44
  _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
- )
45
+ info="Abstract services class",
46
+ attr=dict(
47
+ kind=dict(),
48
+ level=dict(
49
+ optional=True,
50
+ default="info",
51
+ values=criticals,
52
+ ),
53
+ ),
50
54
  )
51
55
 
52
56
  def __init__(self, *args, **kw):
53
- logger.debug('Abstract service init %s', self.__class__)
57
+ logger.debug("Abstract service init %s", self.__class__)
54
58
  t = sessions.current()
55
- glove = kw.pop('glove', t.glove)
56
- sh = kw.pop('sh', t.system())
59
+ glove = kw.pop("glove", t.glove)
60
+ sh = kw.pop("sh", t.system())
57
61
  super().__init__(*args, **kw)
58
62
  self._glove = glove
59
63
  self._sh = sh
60
64
 
61
65
  @property
62
66
  def realkind(self):
63
- return 'service'
67
+ return "service"
64
68
 
65
69
  @property
66
70
  def sh(self):
@@ -89,7 +93,7 @@ class Service(footprints.FootprintBase):
89
93
  value = self.env.get(as_var, None)
90
94
  if not value:
91
95
  if as_conf is None:
92
- as_conf = 'services:' + key.lower()
96
+ as_conf = "services:" + key.lower()
93
97
  value = self.sh.default_target.get(as_conf, default)
94
98
  return value
95
99
 
@@ -104,69 +108,66 @@ class MailService(Service):
104
108
  """
105
109
 
106
110
  _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
- )
111
+ info="Mail services class",
112
+ attr=dict(
113
+ kind=dict(
114
+ values=["sendmail"],
115
+ ),
116
+ sender=dict(
117
+ optional=True,
118
+ default="[glove::xmail]",
119
+ ),
120
+ to=dict(
121
+ alias=("receiver", "recipients"),
122
+ ),
123
+ replyto=dict(
124
+ optional=True,
125
+ alias=("reply", "reply_to"),
126
+ default=None,
127
+ ),
128
+ message=dict(
129
+ optional=True,
130
+ default="",
131
+ alias=("contents", "body"),
132
+ type=str,
133
+ ),
134
+ filename=dict(
135
+ optional=True,
136
+ default=None,
137
+ ),
138
+ attachments=dict(
139
+ type=footprints.FPList,
140
+ optional=True,
141
+ default=footprints.FPList(),
142
+ alias=("files", "attach"),
143
+ ),
144
+ subject=dict(
145
+ type=str,
146
+ ),
147
+ smtpserver=dict(
148
+ optional=True,
149
+ ),
150
+ smtpport=dict(
151
+ type=int,
152
+ optional=True,
153
+ ),
154
+ smtpuser=dict(
155
+ optional=True,
156
+ ),
157
+ smtppass=dict(
158
+ optional=True,
159
+ ),
160
+ charset=dict(
161
+ info="The encoding that should be used when sending the email",
162
+ optional=True,
163
+ default="utf-8",
164
+ ),
165
+ inputs_charset=dict(
166
+ info="The encoding that should be used when reading input files",
167
+ optional=True,
168
+ ),
169
+ commaspace=dict(optional=True, default=", "),
170
+ ),
170
171
  )
171
172
 
172
173
  def attach(self, *args):
@@ -186,28 +187,41 @@ class MailService(Service):
186
187
  with open(self.filename, encoding=self.inputs_charset) as tmp:
187
188
  body += tmp.read()
188
189
  from email.message import EmailMessage
190
+
189
191
  msg = EmailMessage()
190
- msg.set_content(body, subtype="plain",
191
- charset=(self.charset if self.is_not_plain_ascii(body) else 'us-ascii'))
192
+ msg.set_content(
193
+ body,
194
+ subtype="plain",
195
+ charset=(
196
+ self.charset if self.is_not_plain_ascii(body) else "us-ascii"
197
+ ),
198
+ )
192
199
  return msg
193
200
 
194
201
  def as_multipart(self, msg):
195
202
  """Build a new multipart mail with default text contents and attachments."""
196
203
  from email.message import MIMEPart
204
+
197
205
  for xtra in self.attachments:
198
206
  if isinstance(xtra, MIMEPart):
199
207
  msg.add_attachment(xtra)
200
208
  elif self.sh.path.isfile(xtra):
201
209
  import mimetypes
210
+
202
211
  ctype, encoding = mimetypes.guess_type(xtra)
203
212
  if ctype is None or encoding is not None:
204
213
  # No guess could be made, or the file is encoded
205
214
  # (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)
215
+ ctype = "application/octet-stream"
216
+ maintype, subtype = ctype.split("/", 1)
217
+ with open(xtra, "rb") as fp:
218
+ msg.add_attachment(
219
+ fp.read(),
220
+ maintype,
221
+ subtype,
222
+ cte="base64",
223
+ filename=xtra,
224
+ )
211
225
  return msg
212
226
 
213
227
  def _set_header(self, msg, header, value):
@@ -215,25 +229,30 @@ class MailService(Service):
215
229
 
216
230
  def set_headers(self, msg):
217
231
  """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)
232
+ self._set_header(msg, "From", self.sender)
233
+ self._set_header(msg, "To", self.commaspace.join(self.to.split()))
234
+ self._set_header(msg, "Subject", self.subject)
221
235
  if self.replyto is not None:
222
- self._set_header(msg, 'Reply-To', self.commaspace.join(self.replyto.split()))
236
+ self._set_header(
237
+ msg, "Reply-To", self.commaspace.join(self.replyto.split())
238
+ )
223
239
 
224
240
  @contextlib.contextmanager
225
241
  def smtp_entrypoints(self):
226
242
  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)
243
+
244
+ my_smtpserver = self.actual_value(
245
+ "smtpserver", as_var="VORTEX_SMTPSERVER", default="localhost"
246
+ )
247
+ my_smtpport = self.actual_value(
248
+ "smtpport", as_var="VORTEX_SMTPPORT", default=smtplib.SMTP_PORT
249
+ )
233
250
  if not self.sh.default_target.isnetworknode:
234
- sshobj = self.sh.ssh('network', virtualnode=True, mandatory_hostcheck=False)
251
+ sshobj = self.sh.ssh(
252
+ "network", virtualnode=True, mandatory_hostcheck=False
253
+ )
235
254
  with sshobj.tunnel(my_smtpserver, my_smtpport) as tun:
236
- yield 'localhost', tun.entranceport
255
+ yield "localhost", tun.entranceport
237
256
  else:
238
257
  yield my_smtpserver, my_smtpport
239
258
 
@@ -246,9 +265,10 @@ class MailService(Service):
246
265
  msgcorpus = msg.as_string()
247
266
  with self.smtp_entrypoints() as (smtpserver, smtpport):
248
267
  import smtplib
268
+
249
269
  extras = dict()
250
270
  if smtpport:
251
- extras['port'] = smtpport
271
+ extras["port"] = smtpport
252
272
  smtp = smtplib.SMTP(smtpserver, **extras)
253
273
  if self.smtpuser and self.smtppass:
254
274
  smtp.login(self.smtpuser, self.smtppass)
@@ -265,20 +285,15 @@ class ReportService(Service):
265
285
 
266
286
  _abstract = True
267
287
  _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
- )
288
+ info="Report services class",
289
+ attr=dict(
290
+ kind=dict(values=["sendreport"]),
291
+ sender=dict(
292
+ optional=True,
293
+ default="[glove::user]",
294
+ ),
295
+ subject=dict(optional=True, default="Test"),
296
+ ),
282
297
  )
283
298
 
284
299
  def __call__(self, *args):
@@ -291,14 +306,14 @@ class FileReportService(ReportService):
291
306
 
292
307
  _abstract = True
293
308
  _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
- )
309
+ info="File Report services class",
310
+ attr=dict(
311
+ kind=dict(
312
+ values=["sendreport", "sendfilereport"],
313
+ remap=dict(sendfilereport="sendreport"),
314
+ ),
315
+ filename=dict(),
316
+ ),
302
317
  )
303
318
 
304
319
 
@@ -318,57 +333,74 @@ class SSHProxy(Service):
318
333
  """
319
334
 
320
335
  _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
- )
336
+ info="Remote command proxy",
337
+ attr=dict(
338
+ kind=dict(
339
+ values=["ssh", "ssh_proxy"],
340
+ remap=dict(autoremap="first"),
341
+ ),
342
+ hostname=dict(),
343
+ genericnode=dict(
344
+ optional=True,
345
+ default=None,
346
+ access="rwx",
347
+ ),
348
+ nodetype=dict(
349
+ optional=True,
350
+ values=[
351
+ "login",
352
+ "transfer",
353
+ "transfert",
354
+ "network",
355
+ "agt",
356
+ "syslog",
357
+ ],
358
+ default="network",
359
+ remap=dict(transfer="transfert"),
360
+ ),
361
+ permut=dict(
362
+ type=bool,
363
+ optional=True,
364
+ default=True,
365
+ ),
366
+ maxtries=dict(
367
+ type=int,
368
+ optional=True,
369
+ default=2,
370
+ ),
371
+ sshopts=dict(
372
+ optional=True,
373
+ type=footprints.FPList,
374
+ default=None,
375
+ ),
376
+ ),
355
377
  )
356
378
 
357
379
  def __init__(self, *args, **kw):
358
- logger.debug('Remote command proxy init %s', self.__class__)
380
+ logger.debug("Remote command proxy init %s", self.__class__)
359
381
  super().__init__(*args, **kw)
360
382
  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)
383
+ extra_sshopts = (
384
+ None if self.sshopts is None else " ".join(self.sshopts)
385
+ )
386
+ self._sshobj = self.sh.ssh(
387
+ hostname,
388
+ sshopts=extra_sshopts,
389
+ maxtries=self.maxtries,
390
+ virtualnode=virtualnode,
391
+ permut=self.permut,
392
+ mandatory_hostcheck=False,
393
+ )
365
394
 
366
395
  def _actual_hostname(self):
367
396
  """Build a list of candidate target hostnames."""
368
397
  myhostname = self.hostname.strip().lower()
369
398
  virtualnode = False
370
- if myhostname == 'node':
371
- if self.genericnode is not None and self.genericnode != 'no_generic':
399
+ if myhostname == "node":
400
+ if (
401
+ self.genericnode is not None
402
+ and self.genericnode != "no_generic"
403
+ ):
372
404
  myhostname = self.genericnode
373
405
  else:
374
406
  myhostname = self.nodetype
@@ -381,7 +413,7 @@ class SSHProxy(Service):
381
413
 
382
414
  def __call__(self, *args):
383
415
  """Remote execution."""
384
- return self._sshobj.execute(' '.join(args))
416
+ return self._sshobj.execute(" ".join(args))
385
417
 
386
418
 
387
419
  class JeevesService(Service):
@@ -390,38 +422,39 @@ class JeevesService(Service):
390
422
  """
391
423
 
392
424
  _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
- )
425
+ info="Jeeves services class",
426
+ attr=dict(
427
+ kind=dict(values=["askjeeves"]),
428
+ todo=dict(),
429
+ jname=dict(
430
+ optional=True,
431
+ default="test",
432
+ ),
433
+ juser=dict(
434
+ optional=True,
435
+ default="[glove::user]",
436
+ ),
437
+ jpath=dict(
438
+ optional=True,
439
+ default=None,
440
+ access="rwx",
441
+ ),
442
+ jfile=dict(
443
+ optional=True,
444
+ default="vortex",
445
+ ),
446
+ ),
417
447
  )
418
448
 
419
449
  def __call__(self, *args):
420
450
  """Main action: ..."""
421
451
  if self.jpath is None:
422
- self.jpath = self.sh.path.join(self.env.HOME, 'jeeves', self.jname, 'depot')
452
+ self.jpath = self.sh.path.join(
453
+ self.env.HOME, "jeeves", self.jname, "depot"
454
+ )
423
455
  if self.sh.path.isdir(self.jpath):
424
456
  from jeeves import bertie
457
+
425
458
  data = dict()
426
459
  for arg in args:
427
460
  data.update(arg)
@@ -429,10 +462,11 @@ class JeevesService(Service):
429
462
  user=self.juser,
430
463
  jtag=self.sh.path.join(self.jpath, self.jfile),
431
464
  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'),
465
+ mail=data.pop("mail", self.glove.xmail),
466
+ apps=data.pop("apps", (self.glove.vapp,)),
467
+ conf=data.pop("conf", (self.glove.vconf,)),
468
+ task=self.env.get("JOBNAME")
469
+ or self.env.get("SMSNAME", "interactif"),
436
470
  )
437
471
  fulltalk.update(
438
472
  data=data,
@@ -440,7 +474,7 @@ class JeevesService(Service):
440
474
  jr = bertie.ask(**fulltalk)
441
475
  return jr.todo, jr.last
442
476
  else:
443
- logger.error('No valid path to jeeves <%s>', self.jpath)
477
+ logger.error("No valid path to jeeves <%s>", self.jpath)
444
478
  return None
445
479
 
446
480
 
@@ -453,37 +487,41 @@ class HideService(Service):
453
487
  """
454
488
 
455
489
  _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'),
490
+ info="Hide a given object on current filesystem",
491
+ attr=dict(
492
+ kind=dict(
493
+ values=["hidden", "hide", "hiddencache"],
494
+ remap=dict(autoremap="first"),
461
495
  ),
462
- rootdir = dict(
463
- optional = True,
464
- default = None,
496
+ rootdir=dict(
497
+ optional=True,
498
+ default=None,
465
499
  ),
466
- headdir = dict(
467
- optional = True,
468
- default = 'hidden',
500
+ headdir=dict(
501
+ optional=True,
502
+ default="hidden",
469
503
  ),
470
- asfmt = dict(
471
- optional = True,
472
- default = None,
504
+ asfmt=dict(
505
+ optional=True,
506
+ default=None,
473
507
  ),
474
- )
508
+ ),
475
509
  )
476
510
 
477
511
  def find_rootdir(self, filename):
478
512
  """Find a path for hiding files on the same filesystem."""
479
513
  username = self.sh.getlogname()
480
- work_dir = self.sh.path.join(self.sh.find_mount_point(filename), 'work')
514
+ work_dir = self.sh.path.join(
515
+ self.sh.find_mount_point(filename), "work"
516
+ )
481
517
  if not self.sh.path.exists(work_dir):
482
518
  logger.warning("path <%s> doesn't exist", work_dir)
483
519
  fullpath = self.sh.path.realpath(filename)
484
520
  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')
521
+ logger.error("No login <%s> in path <%s>", username, fullpath)
522
+ raise ValueError(
523
+ "Login name not in actual path for hiding data"
524
+ )
487
525
  work_dir = fullpath.partition(username)[0]
488
526
  logger.warning("using work_dir = <%s>", work_dir)
489
527
  hidden_path = self.sh.path.join(work_dir, username, self.headdir)
@@ -494,21 +532,25 @@ class HideService(Service):
494
532
 
495
533
  rootdir = self.rootdir
496
534
  if rootdir is None:
497
- rootdir = self.sh.default_target.get('hidden_rootdir', None)
535
+ rootdir = self.sh.default_target.get("hidden_rootdir", None)
498
536
  if rootdir is not None:
499
537
  rootdir = self.sh.path.expanduser(rootdir)
500
538
 
501
539
  actual_rootdir = rootdir or self.find_rootdir(filename)
502
540
  destination = self.sh.path.join(
503
541
  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
- ))
542
+ ".".join(
543
+ (
544
+ "HIDDEN",
545
+ date.now().strftime("%Y%m%d%H%M%S.%f"),
546
+ "P{:06d}".format(self.sh.getpid()),
547
+ hashlib.md5(
548
+ self.sh.path.abspath(filename).encode(encoding="utf-8")
549
+ ).hexdigest(),
550
+ )
551
+ ),
510
552
  )
511
- self.sh.cp(filename, destination, intent='in', fmt=self.asfmt)
553
+ self.sh.cp(filename, destination, intent="in", fmt=self.asfmt)
512
554
  return destination
513
555
 
514
556
 
@@ -519,20 +561,21 @@ class Directory:
519
561
  Directory (en) means Annuaire (fr).
520
562
  """
521
563
 
522
- def __init__(self, inifile, domain='meteo.fr', encoding=None):
564
+ def __init__(self, inifile, domain="meteo.fr", encoding=None):
523
565
  """Keep aliases in memory, as a dict of sets."""
524
566
  config = GenericConfigParser(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