ceci 2.3__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.
- {ceci-2.3 → ceci-2.4}/.coveragerc +1 -1
- ceci-2.4/.readthedocs.yml +14 -0
- {ceci-2.3 → ceci-2.4}/PKG-INFO +1 -1
- {ceci-2.3 → ceci-2.4}/ceci/__init__.py +1 -0
- {ceci-2.3 → ceci-2.4}/ceci/_version.py +3 -3
- ceci-2.4/ceci/file_types.py +496 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/graph.py +42 -1
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/mini.py +2 -2
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/pipeline.py +6 -1
- {ceci-2.3 → ceci-2.4}/ceci/sites/nersc.py +1 -1
- {ceci-2.3 → ceci-2.4}/ceci.egg-info/PKG-INFO +1 -1
- {ceci-2.3 → ceci-2.4}/ceci.egg-info/SOURCES.txt +3 -0
- {ceci-2.3 → ceci-2.4}/docs/conf.py +5 -5
- {ceci-2.3 → ceci-2.4}/docs/config.rst +24 -2
- {ceci-2.3 → ceci-2.4}/docs/launchers.rst +2 -2
- ceci-2.4/docs/requirements.txt +1 -0
- {ceci-2.3 → ceci-2.4}/docs/sites.rst +10 -14
- {ceci-2.3 → ceci-2.4}/tests/template.yml +0 -1
- {ceci-2.3 → ceci-2.4}/tests/test.yml +0 -1
- {ceci-2.3 → ceci-2.4}/tests/test_dataregistry.yml +0 -1
- {ceci-2.3 → ceci-2.4}/tests/test_namespace.yml +0 -1
- {ceci-2.3 → ceci-2.4}/.github/workflows/ci.yml +0 -0
- {ceci-2.3 → ceci-2.4}/.github/workflows/publish.yml +0 -0
- {ceci-2.3 → ceci-2.4}/.gitignore +0 -0
- {ceci-2.3 → ceci-2.4}/.pylintrc +0 -0
- {ceci-2.3 → ceci-2.4}/LICENSE +0 -0
- {ceci-2.3 → ceci-2.4}/README.md +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/__main__.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/config.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/errors.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/main.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/minirunner.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/monitor.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/__init__.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/dry_run.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/file_manager.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/flow_chart.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/parsl.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/sec.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/pipeline/templates.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/sites/__init__.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/sites/ccin2p3.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/sites/local.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/sites/site.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/stage.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/tools/__init__.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/tools/ancestors.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/update_pipelines_for_ceci_2.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci/utils.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci.egg-info/dependency_links.txt +0 -0
- {ceci-2.3 → ceci-2.4}/ceci.egg-info/entry_points.txt +0 -0
- {ceci-2.3 → ceci-2.4}/ceci.egg-info/requires.txt +0 -0
- {ceci-2.3 → ceci-2.4}/ceci.egg-info/top_level.txt +0 -0
- {ceci-2.3 → ceci-2.4}/ceci.png +0 -0
- {ceci-2.3 → ceci-2.4}/ceci_example/__init__.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci_example/__main__.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci_example/example_stages.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci_example/types.py +0 -0
- {ceci-2.3 → ceci-2.4}/ceci_example_namespace/example_stages.py +0 -0
- {ceci-2.3 → ceci-2.4}/do_cover.sh +0 -0
- {ceci-2.3 → ceci-2.4}/docs/Makefile +0 -0
- {ceci-2.3 → ceci-2.4}/docs/config2.rst +0 -0
- {ceci-2.3 → ceci-2.4}/docs/index.rst +0 -0
- {ceci-2.3 → ceci-2.4}/docs/installation.rst +0 -0
- {ceci-2.3 → ceci-2.4}/docs/logo.png +0 -0
- {ceci-2.3 → ceci-2.4}/docs/overview.rst +0 -0
- {ceci-2.3 → ceci-2.4}/docs/stages.rst +0 -0
- {ceci-2.3 → ceci-2.4}/docs/tutorial.rst +0 -0
- {ceci-2.3 → ceci-2.4}/nb/ceci_interactive_example.ipynb +0 -0
- {ceci-2.3 → ceci-2.4}/pyproject.toml +0 -0
- {ceci-2.3 → ceci-2.4}/setup.cfg +0 -0
- {ceci-2.3 → ceci-2.4}/setup.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/__init__.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/config.yml +0 -0
- {ceci-2.3 → ceci-2.4}/tests/config_nm.yml +0 -0
- {ceci-2.3 → ceci-2.4}/tests/create_registry_entries.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/inputs/dm.txt +0 -0
- {ceci-2.3 → ceci-2.4}/tests/inputs/fiducial_cosmology.txt +0 -0
- {ceci-2.3 → ceci-2.4}/tests/north.yml +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test.hdf5 +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_cmd.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_dask.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_executables.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_flow_chart.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_graph.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_helpers.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_interactive.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_main.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_minirunner.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_monitor.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_pipeline.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_python_paths.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_site.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_snapshot.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_stage.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_update_script.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/test_utils.py +0 -0
- {ceci-2.3 → ceci-2.4}/tests/update_test_files/config.yml +0 -0
- {ceci-2.3 → ceci-2.4}/tests/update_test_files/pipeline1.yml +0 -0
- {ceci-2.3 → ceci-2.4}/tests/update_test_files/pipeline2.yml +0 -0
{ceci-2.3 → ceci-2.4}/PKG-INFO
RENAMED
|
@@ -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.
|
|
32
|
-
__version_tuple__ = version_tuple = (2,
|
|
31
|
+
__version__ = version = '2.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 4)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
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
|
|
@@ -71,7 +71,7 @@ def build_graph(stage_names, stage_classes, stage_aliases, overall_inputs):
|
|
|
71
71
|
for tag, stages in missing.items():
|
|
72
72
|
stages = ", ".join(stages)
|
|
73
73
|
msg.append(f"- {tag} - needed by stages {stages}")
|
|
74
|
-
msg = "\n.join(msg)
|
|
74
|
+
msg = "\n".join(msg)
|
|
75
75
|
raise ValueError(f"The following files are needed by the pipeline but not generated by any stage:\n{msg}")
|
|
76
76
|
|
|
77
77
|
# Now check for cycles in the graph
|
|
@@ -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)
|
|
@@ -6,8 +6,8 @@ class MiniPipeline(Pipeline):
|
|
|
6
6
|
"""A pipeline subclass that uses Minirunner, a sub-module
|
|
7
7
|
of ceci, to run.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
jobs on
|
|
9
|
+
Minirunner is a small tool I wrote suitable for interactive
|
|
10
|
+
jobs on NERSC. It launches jobs locally (not through a batch system),
|
|
11
11
|
which parsl can also do, but it has a simple and clearer (to me at least)
|
|
12
12
|
understanding of available nodes and cores.
|
|
13
13
|
|
|
@@ -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']
|
|
@@ -149,7 +149,7 @@ class NerscBatchSite(NerscSite):
|
|
|
149
149
|
walltime = self.config.get("walltime", "00:30:00")
|
|
150
150
|
setup_script = self.config.get(
|
|
151
151
|
"setup",
|
|
152
|
-
"/
|
|
152
|
+
"/dev/null",
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
provider = SlurmProvider(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.coveragerc
|
|
2
2
|
.gitignore
|
|
3
3
|
.pylintrc
|
|
4
|
+
.readthedocs.yml
|
|
4
5
|
LICENSE
|
|
5
6
|
README.md
|
|
6
7
|
ceci.png
|
|
@@ -14,6 +15,7 @@ ceci/__main__.py
|
|
|
14
15
|
ceci/_version.py
|
|
15
16
|
ceci/config.py
|
|
16
17
|
ceci/errors.py
|
|
18
|
+
ceci/file_types.py
|
|
17
19
|
ceci/main.py
|
|
18
20
|
ceci/minirunner.py
|
|
19
21
|
ceci/monitor.py
|
|
@@ -57,6 +59,7 @@ docs/installation.rst
|
|
|
57
59
|
docs/launchers.rst
|
|
58
60
|
docs/logo.png
|
|
59
61
|
docs/overview.rst
|
|
62
|
+
docs/requirements.txt
|
|
60
63
|
docs/sites.rst
|
|
61
64
|
docs/stages.rst
|
|
62
65
|
docs/tutorial.rst
|
|
@@ -20,13 +20,13 @@ sys.path.insert(0, os.path.abspath('..'))
|
|
|
20
20
|
# -- Project information -----------------------------------------------------
|
|
21
21
|
|
|
22
22
|
project = 'ceci'
|
|
23
|
-
copyright = '2018,
|
|
24
|
-
author = 'Joe Zuntz'
|
|
23
|
+
copyright = '2018, LSSTDESC'
|
|
24
|
+
author = 'Joe Zuntz, LSSTDESC'
|
|
25
25
|
|
|
26
26
|
# The short X.Y version
|
|
27
27
|
version = ''
|
|
28
28
|
# The full version, including alpha/beta/rc tags
|
|
29
|
-
release = '
|
|
29
|
+
release = '2.3'
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
# -- General configuration ---------------------------------------------------
|
|
@@ -41,7 +41,7 @@ release = '0.0.1'
|
|
|
41
41
|
extensions = [
|
|
42
42
|
'sphinx.ext.autodoc',
|
|
43
43
|
'sphinx.ext.doctest',
|
|
44
|
-
'sphinx.ext.intersphinx',
|
|
44
|
+
# 'sphinx.ext.intersphinx',
|
|
45
45
|
'sphinx.ext.todo',
|
|
46
46
|
'sphinx.ext.coverage',
|
|
47
47
|
'sphinx.ext.mathjax',
|
|
@@ -169,7 +169,7 @@ texinfo_documents = [
|
|
|
169
169
|
# -- Options for intersphinx extension ---------------------------------------
|
|
170
170
|
|
|
171
171
|
# Example configuration for intersphinx: refer to the Python standard library.
|
|
172
|
-
intersphinx_mapping = {'https://docs.python.org/': None}
|
|
172
|
+
# intersphinx_mapping = {'https://docs.python.org/': None}
|
|
173
173
|
|
|
174
174
|
# -- Options for todo extension ----------------------------------------------
|
|
175
175
|
|
|
@@ -16,7 +16,7 @@ Here is an example, from ``test/test.yml``. The different pieces are described
|
|
|
16
16
|
name: mini
|
|
17
17
|
interval: 0.5
|
|
18
18
|
# and three sites:
|
|
19
|
-
# local,
|
|
19
|
+
# local, nersc-batch, and nersc-interactive
|
|
20
20
|
site:
|
|
21
21
|
name: local
|
|
22
22
|
max_threads: 2
|
|
@@ -114,7 +114,7 @@ Site
|
|
|
114
114
|
|
|
115
115
|
The ``site`` parameter should be a dictionary that configures the machine on which you are running the pipeline.
|
|
116
116
|
|
|
117
|
-
The ``name`` item in the dictionary sets which site is used. These options are currently allowed: ``local``, ``
|
|
117
|
+
The ``name`` item in the dictionary sets which site is used. These options are currently allowed: ``local``, ``nersc-batch``, and ``nersc-interactive``.
|
|
118
118
|
|
|
119
119
|
See the :ref:`sites` page for information on these sites, and the other options they take.
|
|
120
120
|
|
|
@@ -161,3 +161,25 @@ Any executable specified by ``pre_script`` will be run before the pipeline. If
|
|
|
161
161
|
Any executable specified by ``post_script`` will be run after the pipeline, but only if the pipeline completes successfully. If the post_script returns a non-zero status then it will be returned as the ceci exit code, but no exception will be raised.
|
|
162
162
|
|
|
163
163
|
Both scripts are called with the same arguments as the original executable was called with.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
Templates
|
|
167
|
+
---------
|
|
168
|
+
|
|
169
|
+
You can use `Jinja2 <https://jinja.palletsprojects.com>`_ to allow you to use *templates* in your pipeline YML files.
|
|
170
|
+
|
|
171
|
+
This allows you to set variables on the ceci command line which are then used to
|
|
172
|
+
modify the pipeline text. This lets you use a single parameter file for a set of
|
|
173
|
+
runs. In TXPipe we use this to have a single pipeline file that can process one of several
|
|
174
|
+
different fields depending on the parameter choice.
|
|
175
|
+
|
|
176
|
+
See the `Jinja2 <https://jinja.palletsprojects.com>`_ documentation for information on the
|
|
177
|
+
template syntax. Then to set input variables, use the `-t` flag on the ceci command line, in the form:
|
|
178
|
+
|
|
179
|
+
.. code-block:: bash
|
|
180
|
+
|
|
181
|
+
ceci -t variable1=value1 variable2=value2 pipeline.yml
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
There's not much point using template in interactive code - you should probably
|
|
185
|
+
just set up your pipeline files programatically.
|
|
@@ -14,9 +14,9 @@ Minirunner
|
|
|
14
14
|
|
|
15
15
|
The ``mini`` launcher is a minimal in-built launcher with only basic features, but it's useful for small to medium sized jobs.
|
|
16
16
|
|
|
17
|
-
Minirunner understands the concept of Nodes versus Cores on supercomputers, and on
|
|
17
|
+
Minirunner understands the concept of Nodes versus Cores on supercomputers, and on NERSC the numbers are determined from SLURM environment variables. If running on the login node, one node with four cores is assigned.
|
|
18
18
|
|
|
19
|
-
Minirunner does not launch jobs - if you want to use it in
|
|
19
|
+
Minirunner does not launch jobs - if you want to use it in NERSC batch mode you should call it from within the job submission script.
|
|
20
20
|
|
|
21
21
|
Minirunner options
|
|
22
22
|
^^^^^^^^^^^^^^^^^^
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sphinx_rtd_theme
|
|
@@ -5,7 +5,7 @@ Sites
|
|
|
5
5
|
|
|
6
6
|
A site is a machine where a pipeline is to be run. Ceci currently only supports running a pipeline at a single site, not splitting it up between them.
|
|
7
7
|
|
|
8
|
-
Three sites are currently supported: ``local``, ``
|
|
8
|
+
Three sites are currently supported: ``local``, ``nersc-batch``, and ``nersc-interactive``.
|
|
9
9
|
|
|
10
10
|
See also the :ref:`launchers` page for how to configure the manager that runs the pipeline.
|
|
11
11
|
|
|
@@ -45,35 +45,31 @@ The local site is a general one and represents running in a straightforward loca
|
|
|
45
45
|
``max_threads`` is optional and controls the maximum number of stages run at the same time. Its default depends on the launcher used.
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
NERSC Interactive
|
|
49
|
+
------------------
|
|
50
50
|
|
|
51
|
-
The ``
|
|
51
|
+
The ``nersc-interactive`` site is used to run jobs interactively on NERSC compute nodes. You should first use the ``salloc`` command to get an interactive allocation, and then within that run ``ceci``.
|
|
52
52
|
|
|
53
|
-
There are no additional options for the ``
|
|
53
|
+
There are no additional options for the ``nersc-interactive`` site: the number of parallel stages is given by the number of nodes that you ask for in ``salloc``.
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
NERSC Batch
|
|
58
|
+
------------
|
|
59
59
|
|
|
60
|
-
The site ``
|
|
60
|
+
The site ``nersc-batch`` runs on the Perlmutter supercomputer at NERSC, and submits jobs to the SLURM batch system. In this mode, you should call ceci from the login node and stay logged in while the jobs run.
|
|
61
61
|
|
|
62
|
-
These options can be used for the ``
|
|
62
|
+
These options can be used for the ``nersc-batch`` site:
|
|
63
63
|
|
|
64
64
|
.. code-block:: yaml
|
|
65
65
|
|
|
66
66
|
launcher:
|
|
67
|
-
name:
|
|
68
|
-
cpu_type: haswell # optional
|
|
67
|
+
name: nersc-interactive
|
|
69
68
|
queue: debug # optional
|
|
70
69
|
max_jobs: 2 # optional
|
|
71
70
|
account: m1727 # optional
|
|
72
71
|
walltime: 00:30:00 # optional
|
|
73
|
-
setup: /global/projecta/projectdirs/lsst/groups/WL/users/zuntz/setup-cori
|
|
74
|
-
# ^^ optional
|
|
75
72
|
|
|
76
|
-
``cpu_type`` is optional and controls which partition of cori is used for jobs, and should be `haswell` or `KNL`.
|
|
77
73
|
|
|
78
74
|
``queue`` is optional and controls which SLURM queue jobs are launcher on. It can be ``debug``, ``regular``, or ``premium``. See `the nersc documentation <http://docs.nersc.gov/>`_
|
|
79
75
|
for a description of each.
|
|
File without changes
|
|
File without changes
|
{ceci-2.3 → ceci-2.4}/.gitignore
RENAMED
|
File without changes
|
{ceci-2.3 → ceci-2.4}/.pylintrc
RENAMED
|
File without changes
|
{ceci-2.3 → ceci-2.4}/LICENSE
RENAMED
|
File without changes
|
{ceci-2.3 → ceci-2.4}/README.md
RENAMED
|
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
|
{ceci-2.3 → ceci-2.4}/ceci.png
RENAMED
|
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
|
{ceci-2.3 → ceci-2.4}/setup.cfg
RENAMED
|
File without changes
|
{ceci-2.3 → ceci-2.4}/setup.py
RENAMED
|
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
|