ceci 2.3.1__tar.gz → 2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. {ceci-2.3.1 → ceci-2.4}/PKG-INFO +1 -1
  2. {ceci-2.3.1 → ceci-2.4}/ceci/__init__.py +1 -0
  3. {ceci-2.3.1 → ceci-2.4}/ceci/_version.py +3 -3
  4. ceci-2.4/ceci/file_types.py +496 -0
  5. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/graph.py +41 -0
  6. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/pipeline.py +6 -1
  7. {ceci-2.3.1 → ceci-2.4}/ceci.egg-info/PKG-INFO +1 -1
  8. {ceci-2.3.1 → ceci-2.4}/ceci.egg-info/SOURCES.txt +1 -0
  9. {ceci-2.3.1 → ceci-2.4}/.coveragerc +0 -0
  10. {ceci-2.3.1 → ceci-2.4}/.github/workflows/ci.yml +0 -0
  11. {ceci-2.3.1 → ceci-2.4}/.github/workflows/publish.yml +0 -0
  12. {ceci-2.3.1 → ceci-2.4}/.gitignore +0 -0
  13. {ceci-2.3.1 → ceci-2.4}/.pylintrc +0 -0
  14. {ceci-2.3.1 → ceci-2.4}/.readthedocs.yml +0 -0
  15. {ceci-2.3.1 → ceci-2.4}/LICENSE +0 -0
  16. {ceci-2.3.1 → ceci-2.4}/README.md +0 -0
  17. {ceci-2.3.1 → ceci-2.4}/ceci/__main__.py +0 -0
  18. {ceci-2.3.1 → ceci-2.4}/ceci/config.py +0 -0
  19. {ceci-2.3.1 → ceci-2.4}/ceci/errors.py +0 -0
  20. {ceci-2.3.1 → ceci-2.4}/ceci/main.py +0 -0
  21. {ceci-2.3.1 → ceci-2.4}/ceci/minirunner.py +0 -0
  22. {ceci-2.3.1 → ceci-2.4}/ceci/monitor.py +0 -0
  23. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/__init__.py +0 -0
  24. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/dry_run.py +0 -0
  25. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/file_manager.py +0 -0
  26. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/flow_chart.py +0 -0
  27. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/mini.py +0 -0
  28. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/parsl.py +0 -0
  29. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/sec.py +0 -0
  30. {ceci-2.3.1 → ceci-2.4}/ceci/pipeline/templates.py +0 -0
  31. {ceci-2.3.1 → ceci-2.4}/ceci/sites/__init__.py +0 -0
  32. {ceci-2.3.1 → ceci-2.4}/ceci/sites/ccin2p3.py +0 -0
  33. {ceci-2.3.1 → ceci-2.4}/ceci/sites/local.py +0 -0
  34. {ceci-2.3.1 → ceci-2.4}/ceci/sites/nersc.py +0 -0
  35. {ceci-2.3.1 → ceci-2.4}/ceci/sites/site.py +0 -0
  36. {ceci-2.3.1 → ceci-2.4}/ceci/stage.py +0 -0
  37. {ceci-2.3.1 → ceci-2.4}/ceci/tools/__init__.py +0 -0
  38. {ceci-2.3.1 → ceci-2.4}/ceci/tools/ancestors.py +0 -0
  39. {ceci-2.3.1 → ceci-2.4}/ceci/update_pipelines_for_ceci_2.py +0 -0
  40. {ceci-2.3.1 → ceci-2.4}/ceci/utils.py +0 -0
  41. {ceci-2.3.1 → ceci-2.4}/ceci.egg-info/dependency_links.txt +0 -0
  42. {ceci-2.3.1 → ceci-2.4}/ceci.egg-info/entry_points.txt +0 -0
  43. {ceci-2.3.1 → ceci-2.4}/ceci.egg-info/requires.txt +0 -0
  44. {ceci-2.3.1 → ceci-2.4}/ceci.egg-info/top_level.txt +0 -0
  45. {ceci-2.3.1 → ceci-2.4}/ceci.png +0 -0
  46. {ceci-2.3.1 → ceci-2.4}/ceci_example/__init__.py +0 -0
  47. {ceci-2.3.1 → ceci-2.4}/ceci_example/__main__.py +0 -0
  48. {ceci-2.3.1 → ceci-2.4}/ceci_example/example_stages.py +0 -0
  49. {ceci-2.3.1 → ceci-2.4}/ceci_example/types.py +0 -0
  50. {ceci-2.3.1 → ceci-2.4}/ceci_example_namespace/example_stages.py +0 -0
  51. {ceci-2.3.1 → ceci-2.4}/do_cover.sh +0 -0
  52. {ceci-2.3.1 → ceci-2.4}/docs/Makefile +0 -0
  53. {ceci-2.3.1 → ceci-2.4}/docs/conf.py +0 -0
  54. {ceci-2.3.1 → ceci-2.4}/docs/config.rst +0 -0
  55. {ceci-2.3.1 → ceci-2.4}/docs/config2.rst +0 -0
  56. {ceci-2.3.1 → ceci-2.4}/docs/index.rst +0 -0
  57. {ceci-2.3.1 → ceci-2.4}/docs/installation.rst +0 -0
  58. {ceci-2.3.1 → ceci-2.4}/docs/launchers.rst +0 -0
  59. {ceci-2.3.1 → ceci-2.4}/docs/logo.png +0 -0
  60. {ceci-2.3.1 → ceci-2.4}/docs/overview.rst +0 -0
  61. {ceci-2.3.1 → ceci-2.4}/docs/requirements.txt +0 -0
  62. {ceci-2.3.1 → ceci-2.4}/docs/sites.rst +0 -0
  63. {ceci-2.3.1 → ceci-2.4}/docs/stages.rst +0 -0
  64. {ceci-2.3.1 → ceci-2.4}/docs/tutorial.rst +0 -0
  65. {ceci-2.3.1 → ceci-2.4}/nb/ceci_interactive_example.ipynb +0 -0
  66. {ceci-2.3.1 → ceci-2.4}/pyproject.toml +0 -0
  67. {ceci-2.3.1 → ceci-2.4}/setup.cfg +0 -0
  68. {ceci-2.3.1 → ceci-2.4}/setup.py +0 -0
  69. {ceci-2.3.1 → ceci-2.4}/tests/__init__.py +0 -0
  70. {ceci-2.3.1 → ceci-2.4}/tests/config.yml +0 -0
  71. {ceci-2.3.1 → ceci-2.4}/tests/config_nm.yml +0 -0
  72. {ceci-2.3.1 → ceci-2.4}/tests/create_registry_entries.py +0 -0
  73. {ceci-2.3.1 → ceci-2.4}/tests/inputs/dm.txt +0 -0
  74. {ceci-2.3.1 → ceci-2.4}/tests/inputs/fiducial_cosmology.txt +0 -0
  75. {ceci-2.3.1 → ceci-2.4}/tests/north.yml +0 -0
  76. {ceci-2.3.1 → ceci-2.4}/tests/template.yml +0 -0
  77. {ceci-2.3.1 → ceci-2.4}/tests/test.hdf5 +0 -0
  78. {ceci-2.3.1 → ceci-2.4}/tests/test.yml +0 -0
  79. {ceci-2.3.1 → ceci-2.4}/tests/test_cmd.py +0 -0
  80. {ceci-2.3.1 → ceci-2.4}/tests/test_dask.py +0 -0
  81. {ceci-2.3.1 → ceci-2.4}/tests/test_dataregistry.yml +0 -0
  82. {ceci-2.3.1 → ceci-2.4}/tests/test_executables.py +0 -0
  83. {ceci-2.3.1 → ceci-2.4}/tests/test_flow_chart.py +0 -0
  84. {ceci-2.3.1 → ceci-2.4}/tests/test_graph.py +0 -0
  85. {ceci-2.3.1 → ceci-2.4}/tests/test_helpers.py +0 -0
  86. {ceci-2.3.1 → ceci-2.4}/tests/test_interactive.py +0 -0
  87. {ceci-2.3.1 → ceci-2.4}/tests/test_main.py +0 -0
  88. {ceci-2.3.1 → ceci-2.4}/tests/test_minirunner.py +0 -0
  89. {ceci-2.3.1 → ceci-2.4}/tests/test_monitor.py +0 -0
  90. {ceci-2.3.1 → ceci-2.4}/tests/test_namespace.yml +0 -0
  91. {ceci-2.3.1 → ceci-2.4}/tests/test_pipeline.py +0 -0
  92. {ceci-2.3.1 → ceci-2.4}/tests/test_python_paths.py +0 -0
  93. {ceci-2.3.1 → ceci-2.4}/tests/test_site.py +0 -0
  94. {ceci-2.3.1 → ceci-2.4}/tests/test_snapshot.py +0 -0
  95. {ceci-2.3.1 → ceci-2.4}/tests/test_stage.py +0 -0
  96. {ceci-2.3.1 → ceci-2.4}/tests/test_update_script.py +0 -0
  97. {ceci-2.3.1 → ceci-2.4}/tests/test_utils.py +0 -0
  98. {ceci-2.3.1 → ceci-2.4}/tests/update_test_files/config.yml +0 -0
  99. {ceci-2.3.1 → ceci-2.4}/tests/update_test_files/pipeline1.yml +0 -0
  100. {ceci-2.3.1 → ceci-2.4}/tests/update_test_files/pipeline2.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ceci
3
- Version: 2.3.1
3
+ Version: 2.4
4
4
  Summary: Lightweight pipeline engine for LSST DESC
5
5
  Author: Joe Zuntz
6
6
  License: BSD 3-Clause License
@@ -9,6 +9,7 @@ from .pipeline import (
9
9
  FlowChartPipeline,
10
10
  )
11
11
  from .main import run_pipeline
12
+ from . import file_types
12
13
  import importlib.metadata
13
14
 
14
15
  try:
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.3.1'
32
- __version_tuple__ = version_tuple = (2, 3, 1)
31
+ __version__ = version = '2.4'
32
+ __version_tuple__ = version_tuple = (2, 4)
33
33
 
34
- __commit_id__ = commit_id = 'g453d12b8e'
34
+ __commit_id__ = commit_id = 'g727f0fadf'
@@ -0,0 +1,496 @@
1
+ import uuid
2
+ import datetime
3
+ import socket
4
+ import getpass
5
+ import warnings
6
+ import pathlib
7
+ import yaml
8
+ import shutil
9
+ import pickle
10
+ from io import UnsupportedOperation
11
+
12
+
13
+ class FileValidationError(Exception):
14
+ pass
15
+
16
+
17
+ class DataFile:
18
+ """
19
+ A class representing a DataFile to be made by pipeline stages
20
+ and passed on to subsequent ones.
21
+
22
+ DataFile itself should not be instantiated - instead subclasses
23
+ should be defined for different file types.
24
+
25
+ These subclasses are used in the definition of pipeline stages
26
+ to indicate what kind of file is expected. The "suffix" attribute,
27
+ which must be defined on subclasses, indicates the file suffix.
28
+
29
+ The open method, which can optionally be overridden, is used by the
30
+ machinery of the PipelineStage class to open an input our output
31
+ named by a tag.
32
+
33
+ """
34
+
35
+ supports_parallel_write = False
36
+ suffix = None
37
+
38
+ def __init__(self, path, mode, extra_provenance=None, validate=True, **kwargs):
39
+ self.path = path
40
+ self.mode = mode
41
+
42
+ if mode not in ["r", "w"]:
43
+ raise ValueError(f"File 'mode' argument must be 'r' or 'w' not '{mode}'")
44
+
45
+ self.file = self.open(path, mode, **kwargs)
46
+
47
+ if validate and mode == "r":
48
+ self.validate()
49
+
50
+ if mode == "w":
51
+ self.provenance = self.generate_provenance(extra_provenance)
52
+ self.write_provenance()
53
+ else:
54
+ self.provenance = self.read_provenance()
55
+
56
+ @staticmethod
57
+ def generate_provenance(extra_provenance=None):
58
+ """
59
+ Generate provenance information - a dictionary
60
+ of useful information about the origina
61
+ """
62
+ UUID = uuid.uuid4().hex
63
+ creation = datetime.datetime.now().isoformat()
64
+ domain = socket.getfqdn()
65
+ username = getpass.getuser()
66
+
67
+ # Add other provenance and related
68
+ provenance = {
69
+ "uuid": UUID,
70
+ "creation": creation,
71
+ "domain": domain,
72
+ "username": username,
73
+ }
74
+
75
+ if extra_provenance:
76
+ provenance.update(extra_provenance)
77
+ return provenance
78
+
79
+ def write_provenance(self):
80
+ """
81
+ Concrete subclasses (for which it is possible) should override
82
+ this method to save the dictionary self.provenance to the file.
83
+ """
84
+ pass
85
+
86
+ def add_provenance(self, key, value):
87
+ """
88
+ Concrete subclasses (for which it is possible) should override
89
+ this method to save the a new string key/value pair to file
90
+ """
91
+ pass
92
+
93
+ def read_provenance(self):
94
+ """
95
+ Concrete subclasses for which it is possible should override
96
+ this method and read the provenance information from the file.
97
+
98
+ Other classes will return this dictionary of UNKNOWNs
99
+ """
100
+ provenance = {
101
+ "uuid": "UNKNOWN",
102
+ "creation": "UNKNOWN",
103
+ "domain": "UNKNOWN",
104
+ "username": "UNKNOWN",
105
+ }
106
+
107
+ return provenance
108
+
109
+ def validate(self):
110
+ """
111
+ Concrete subclasses should override this method
112
+ to check that all expected columns are present.
113
+ """
114
+ pass
115
+
116
+ @classmethod
117
+ def open(cls, path, mode):
118
+ """
119
+ Open a data file. The base implementation of this function just
120
+ opens and returns a standard python file object.
121
+
122
+ Subclasses can override to either open files using different openers
123
+ (like fitsio.FITS), or, for more specific data types, return an
124
+ instance of the class itself to use as an intermediary for the file.
125
+
126
+ """
127
+ return open(path, mode)
128
+
129
+ def close(self):
130
+ self.file.close()
131
+
132
+ @classmethod
133
+ def make_name(cls, tag):
134
+ if cls.suffix:
135
+ return f"{tag}.{cls.suffix}"
136
+ else:
137
+ return tag
138
+
139
+ def __enter__(self):
140
+ return self
141
+
142
+ def __exit__(self, exc_type, exc_value, traceback):
143
+ self.close()
144
+
145
+
146
+ class HDFFile(DataFile):
147
+ supports_parallel_write = True
148
+ """
149
+ A data file in the HDF5 format.
150
+ Using these files requires the h5py package, which in turn
151
+ requires an HDF5 library installation.
152
+
153
+ """
154
+ suffix = "hdf5"
155
+ required_datasets = []
156
+
157
+ @classmethod
158
+ def open(cls, path, mode, **kwargs):
159
+ # Suppress a warning that h5py always displays
160
+ # on import
161
+ with warnings.catch_warnings():
162
+ warnings.simplefilter("ignore")
163
+ import h5py
164
+ # Return an open h5py File
165
+ return h5py.File(path, mode, **kwargs)
166
+
167
+ def write_provenance(self):
168
+ """
169
+ Write provenance information to a new group,
170
+ called 'provenance'
171
+ """
172
+ if self.mode == "r":
173
+ raise UnsupportedOperation(f"Cannot write provenance to an HDF5 file opened in read-only mode ({self.mode}")
174
+
175
+ # This method *must* be called by all the processes in a parallel
176
+ # run.
177
+ self._provenance_group = self.file.create_group("provenance")
178
+
179
+ # Call the sub-method to do each item
180
+ for key, value in self.provenance.items():
181
+ self._provenance_group.attrs[key] = value
182
+
183
+ def read_provenance(self):
184
+ try:
185
+ group = self.file["provenance"]
186
+ attrs = group.attrs
187
+ except KeyError:
188
+ group = None
189
+ attrs = {}
190
+
191
+ provenance = {
192
+ "uuid": attrs.get("uuid", "UNKNOWN"),
193
+ "creation": attrs.get("creation", "UNKNOWN"),
194
+ "domain": attrs.get("domain", "UNKNOWN"),
195
+ "username": attrs.get("username", "UNKNOWN"),
196
+ }
197
+
198
+ self._provenance_group = group
199
+
200
+ return provenance
201
+
202
+ def validate(self):
203
+ missing = [name for name in self.required_datasets if name not in self.file]
204
+ if missing:
205
+ text = "\n".join(missing)
206
+ raise FileValidationError(f"These data sets are missing from HDF file {self.path}:\n{text}")
207
+
208
+ def close(self):
209
+ self.file.close()
210
+
211
+
212
+ class FitsFile(DataFile):
213
+ """
214
+ A data file in the FITS format.
215
+ Using these files requires the fitsio package.
216
+ """
217
+
218
+ suffix = "fits"
219
+ required_columns = []
220
+
221
+ @classmethod
222
+ def open(cls, path, mode, **kwargs):
223
+ import fitsio
224
+
225
+ # Fitsio doesn't have pure 'w' modes, just 'rw'.
226
+ # Maybe we should check if the file already exists here?
227
+ if mode == "w":
228
+ mode = "rw"
229
+ return fitsio.FITS(path, mode=mode, **kwargs)
230
+
231
+ def missing_columns(self, columns, hdu=1):
232
+ """
233
+ Check that all supplied columns exist
234
+ and are in the chosen HDU
235
+ """
236
+ ext = self.file[hdu]
237
+ found_cols = ext.get_colnames()
238
+ missing_columns = [col for col in columns if col not in found_cols]
239
+ return missing_columns
240
+
241
+ def write_provenance(self):
242
+ """
243
+ Write provenance information to a new group,
244
+ called 'provenance'
245
+ """
246
+ # Call the sub-method to do each item
247
+ if self.mode == "r":
248
+ raise UnsupportedOperation(f"Cannot write provenance to a FITS file opened in read-only mode ({self.mode}")
249
+
250
+ for key, value in self.provenance.items():
251
+ if isinstance(value, str) and "\n" in value:
252
+ values = value.split("\n")
253
+ for i, v in enumerate(values):
254
+ self.file[0].write_key(key + f"_{i}", v)
255
+ else:
256
+ self.file[0].write_key(key, value)
257
+
258
+ def read_provenance(self):
259
+ header = self.file[0].read_header()
260
+ provenance = {
261
+ "uuid": header.get("uuid", "UNKNOWN"),
262
+ "creation": header.get("creation", "UNKNOWN"),
263
+ "domain": header.get("domain", "UNKNOWN"),
264
+ "username": header.get("username", "UNKNOWN"),
265
+ }
266
+ return provenance
267
+
268
+ def validate(self):
269
+ """Check that the catalog has all the required columns and complain otherwise"""
270
+ # Find any columns that do not exist in the file
271
+ missing = self.missing_columns(self.required_columns)
272
+
273
+ # If there are any, raise an exception that lists them explicitly
274
+ if missing:
275
+ text = "\n".join(missing)
276
+ raise FileValidationError(f"These columns are missing from FITS file {self.path}:\n{text}")
277
+
278
+ def close(self):
279
+ self.file.close()
280
+
281
+
282
+ class TextFile(DataFile):
283
+ """
284
+ A data file in plain text format.
285
+ """
286
+
287
+ suffix = "txt"
288
+
289
+
290
+ class YamlFile(DataFile):
291
+ """
292
+ A data file in yaml format.
293
+ The top-level object in TXPipe YAML
294
+ files should always be a dictionary.
295
+ """
296
+
297
+ suffix = "yml"
298
+
299
+ def __init__(self, path, mode, extra_provenance=None, validate=True, load_mode="full"):
300
+ self.path = path
301
+ self.mode = mode
302
+ self.file = self.open(path, mode)
303
+
304
+ if mode == "r":
305
+ if load_mode == "safe":
306
+ self.content = yaml.safe_load(self.file)
307
+ elif load_mode == "full":
308
+ self.content = yaml.full_load(self.file)
309
+ elif load_mode == "unsafe":
310
+ self.content = yaml.unsafe_load(self.file)
311
+ else:
312
+ raise ValueError(f"Unknown value {yaml_load} of load_mode. Should be 'safe', 'full', or 'unsafe'")
313
+ # get provenance
314
+ self.provenance = self.read_provenance()
315
+
316
+ else:
317
+ self.provenance = self.generate_provenance(extra_provenance)
318
+ self.write_provenance()
319
+
320
+ def read(self, key):
321
+ return self.content[key]
322
+
323
+ def write(self, d):
324
+ if not isinstance(d, dict):
325
+ raise ValueError("Only dicts should be passed to YamlFile.write")
326
+ yaml.dump(d, self.file)
327
+
328
+ def write_provenance(self):
329
+ d = {"provenance": self.provenance}
330
+ self.write(d)
331
+
332
+ def read_provenance(self):
333
+ prov = self.content.pop("provenance", {})
334
+ req_provenance = {
335
+ "uuid": prov.get("uuid", "UNKNOWN"),
336
+ "creation": prov.get("creation", "UNKNOWN"),
337
+ "domain": prov.get("domain", "UNKNOWN"),
338
+ "username": prov.get("username", "UNKNOWN"),
339
+ }
340
+ prov.update(req_provenance)
341
+ return prov
342
+
343
+
344
+ class Directory(DataFile):
345
+ suffix = ""
346
+
347
+ @classmethod
348
+ def open(self, path, mode):
349
+ p = pathlib.Path(path)
350
+
351
+ if mode == "w":
352
+ if p.exists():
353
+ shutil.rmtree(p)
354
+ p.mkdir(parents=True)
355
+ else:
356
+ if not p.is_dir():
357
+ raise ValueError(f"Directory input {path} does not exist")
358
+ return p
359
+
360
+ def close(self):
361
+ pass
362
+
363
+ def write_provenance(self):
364
+ """
365
+ Write provenance information to a new group,
366
+ called 'provenance'
367
+ """
368
+ # This method *must* be called by all the processes in a parallel
369
+ # run.
370
+ if self.mode == "r":
371
+ raise UnsupportedOperation(f"Cannot write provenance to a directory opened in read-only mode ({self.mode})")
372
+
373
+ self._provenance_file = open(self.file / "provenance.yml", "w")
374
+
375
+ # Call the sub-method to do each item
376
+ yaml.dump(self.provenance, self._provenance_file)
377
+ self._provenance_file.flush()
378
+
379
+ def read_provenance(self):
380
+ try:
381
+ f = open(self.file / "provenance.yml")
382
+ attrs = yaml.load(f)
383
+
384
+ except KeyError:
385
+ f = None
386
+ attrs = {}
387
+
388
+ self._provenance_file = f
389
+
390
+ provenance = {
391
+ "uuid": attrs.get("uuid", "UNKNOWN"),
392
+ "creation": attrs.get("creation", "UNKNOWN"),
393
+ "domain": attrs.get("domain", "UNKNOWN"),
394
+ "username": attrs.get("username", "UNKNOWN"),
395
+ }
396
+
397
+ return provenance
398
+
399
+
400
+ class FileCollection(Directory):
401
+ """
402
+ Represents a grouped bundle of files, for cases where you don't
403
+ know the exact list in advance.
404
+ """
405
+
406
+ suffix = ""
407
+
408
+ def write_listing(self, filenames):
409
+ """
410
+ Write a listing file in the directory recording
411
+ (presumably) the filenames put in it.
412
+ """
413
+ fn = self.path_for_file("txpipe_listing.txt")
414
+ with open(fn, "w") as f:
415
+ yaml.dump(filenames, f)
416
+
417
+ def read_listing(self):
418
+ """
419
+ Read a listing file from the directory.
420
+ """
421
+ fn = self.path_for_file("txpipe_listing.txt")
422
+ with open(fn, "w") as f:
423
+ filenames = yaml.safe_load(f)
424
+ return filenames
425
+
426
+ def path_for_file(self, filename):
427
+ """
428
+ Get the path for a file inside the collection.
429
+ Does not check if the file exists or anything like
430
+ that.
431
+ """
432
+ return str(self.file / filename)
433
+
434
+
435
+ class PNGFile(DataFile):
436
+ suffix = "png"
437
+
438
+ @classmethod
439
+ def open(self, path, mode, **kwargs):
440
+ import matplotlib
441
+ import matplotlib.pyplot as plt
442
+
443
+ if mode != "w":
444
+ raise ValueError("Reading existing PNG files is not supported")
445
+ return plt.figure(**kwargs)
446
+
447
+ def close(self):
448
+ import matplotlib.pyplot as plt
449
+
450
+ self.file.savefig(self.path, metadata=self.provenance)
451
+ plt.close(self.file)
452
+
453
+ def write_provenance(self):
454
+ # provenance is written on closing the file
455
+ pass
456
+
457
+ def read_provenance(self):
458
+ raise ValueError("Reading existing PNG files is not supported")
459
+
460
+
461
+ class PickleFile(DataFile):
462
+ suffix = "pkl"
463
+
464
+ @classmethod
465
+ def open(self, path, mode, **kwargs):
466
+ return open(path, mode + "b")
467
+
468
+ def write_provenance(self):
469
+ self.write(self.provenance)
470
+
471
+ def read_provenance(self):
472
+ return self.read()
473
+
474
+ def write(self, obj):
475
+ if self.mode != "w":
476
+ raise UnsupportedOperation(f"Cannot write to pickle file opened in read-only ({self.mode})")
477
+ pickle.dump(obj, self.file)
478
+
479
+ def read(self):
480
+ if self.mode != "r":
481
+ raise UnsupportedOperation(f"Cannot read from pickle file opened in write-only ({self.mode})")
482
+ return pickle.load(self.file)
483
+
484
+
485
+ class ParquetFile(DataFile):
486
+ suffix = "pq"
487
+
488
+ def open(self, path, mode):
489
+ import pyarrow.parquet
490
+
491
+ if mode != "r":
492
+ raise NotImplementedError("Not implemented writing to Parquet")
493
+ return pyarrow.parquet.ParquetFile(path)
494
+
495
+ def close(self):
496
+ pass
@@ -197,3 +197,44 @@ def trim_pipeline_graph(graph, from_=None, to_=None):
197
197
  converted_inputs[node_name] = file_name
198
198
 
199
199
  return trimmed_graph, converted_inputs
200
+
201
+ def combine_output_nodes(graph):
202
+ """
203
+ Combine output nodes that share the same parent and have no
204
+ children into a single node.
205
+
206
+ This is useful for plotting flow charts.
207
+
208
+ Parameters
209
+ ----------
210
+ graph : networkx.DiGraph
211
+ The pipeline graph to modify
212
+ """
213
+ groups = collections.defaultdict(list)
214
+ # find groups of nodes that have no outgoing edges and all
215
+ # share the same incoming edge.
216
+ for node in graph.nodes_iter():
217
+ node_data = graph.get_node(node).attr
218
+ if node_data["type"] != "output" or len(graph.successors(node)) != 0:
219
+ continue
220
+ predecessors = list(graph.predecessors(node))
221
+
222
+ # This shouldn't happen in the default case,
223
+ # but might if things change later, so let's deal with it
224
+ # just in case.
225
+ if len(predecessors) != 1:
226
+ continue
227
+
228
+ parent = predecessors[0]
229
+ groups[parent].append(node)
230
+
231
+ for parent, group in groups.items():
232
+ if len(group) < 2:
233
+ continue
234
+ # make a new node combining all these nodes
235
+ new_node_name = "\n".join(group)
236
+ graph.add_node(new_node_name, type="output")
237
+ # connect the parent to the new node
238
+ graph.add_edge(parent, new_node_name)
239
+ # remove the old nodes
240
+ graph.remove_nodes_from(group)
@@ -9,7 +9,7 @@ from abc import abstractmethod
9
9
  from ..stage import PipelineStage
10
10
  from ..sites import load, set_default_site, get_default_site
11
11
  from ..utils import extra_paths
12
- from .graph import build_graph, get_static_ordering, trim_pipeline_graph
12
+ from .graph import build_graph, get_static_ordering, trim_pipeline_graph, combine_output_nodes
13
13
  from .file_manager import FileManager
14
14
  from .sec import StageExecutionConfig
15
15
  from .templates import read_and_apply_template
@@ -898,6 +898,11 @@ class Pipeline:
898
898
  # generate a graphviz object from the networkx graph
899
899
  graph = networkx.nx_agraph.to_agraph(self.graph)
900
900
 
901
+ # Some stages have long lists of output nodes like plots that
902
+ # are final outputs not used in the rest of the pipeline. We
903
+ # can combine these to make the graph more readable.
904
+ combine_output_nodes(graph)
905
+
901
906
  # set the colours and styles for the boxes
902
907
  for node in graph.nodes_iter():
903
908
  node_type = node.attr['type']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ceci
3
- Version: 2.3.1
3
+ Version: 2.4
4
4
  Summary: Lightweight pipeline engine for LSST DESC
5
5
  Author: Joe Zuntz
6
6
  License: BSD 3-Clause License
@@ -15,6 +15,7 @@ ceci/__main__.py
15
15
  ceci/_version.py
16
16
  ceci/config.py
17
17
  ceci/errors.py
18
+ ceci/file_types.py
18
19
  ceci/main.py
19
20
  ceci/minirunner.py
20
21
  ceci/monitor.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes