genelastic 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. genelastic/__init__.py +0 -13
  2. genelastic/api/__init__.py +0 -0
  3. genelastic/api/extends/__init__.py +0 -0
  4. genelastic/api/extends/example.py +6 -0
  5. genelastic/api/routes.py +221 -0
  6. genelastic/api/server.py +80 -0
  7. genelastic/api/settings.py +14 -0
  8. genelastic/common/__init__.py +39 -0
  9. genelastic/common/cli.py +63 -0
  10. genelastic/common/elastic.py +214 -0
  11. genelastic/common/exceptions.py +4 -0
  12. genelastic/common/types.py +25 -0
  13. genelastic/import_data/__init__.py +27 -0
  14. genelastic/{analyses.py → import_data/analyses.py} +19 -20
  15. genelastic/{analysis.py → import_data/analysis.py} +71 -66
  16. genelastic/{bi_process.py → import_data/bi_process.py} +8 -6
  17. genelastic/{bi_processes.py → import_data/bi_processes.py} +10 -9
  18. genelastic/import_data/cli_gen_data.py +116 -0
  19. genelastic/import_data/cli_import.py +379 -0
  20. genelastic/import_data/cli_info.py +256 -0
  21. genelastic/import_data/cli_integrity.py +384 -0
  22. genelastic/import_data/cli_validate.py +54 -0
  23. genelastic/import_data/constants.py +24 -0
  24. genelastic/{data_file.py → import_data/data_file.py} +26 -21
  25. genelastic/import_data/filename_pattern.py +57 -0
  26. genelastic/{import_bundle.py → import_data/import_bundle.py} +58 -48
  27. genelastic/import_data/import_bundle_factory.py +298 -0
  28. genelastic/{logger.py → import_data/logger.py} +22 -18
  29. genelastic/import_data/random_bundle.py +402 -0
  30. genelastic/{tags.py → import_data/tags.py} +48 -27
  31. genelastic/{wet_process.py → import_data/wet_process.py} +8 -4
  32. genelastic/{wet_processes.py → import_data/wet_processes.py} +15 -9
  33. genelastic/ui/__init__.py +0 -0
  34. genelastic/ui/server.py +87 -0
  35. genelastic/ui/settings.py +11 -0
  36. genelastic-0.7.0.dist-info/METADATA +105 -0
  37. genelastic-0.7.0.dist-info/RECORD +40 -0
  38. {genelastic-0.6.0.dist-info → genelastic-0.7.0.dist-info}/WHEEL +1 -1
  39. genelastic-0.7.0.dist-info/entry_points.txt +6 -0
  40. genelastic/common.py +0 -151
  41. genelastic/constants.py +0 -45
  42. genelastic/filename_pattern.py +0 -62
  43. genelastic/gen_data.py +0 -193
  44. genelastic/import_bundle_factory.py +0 -288
  45. genelastic/import_data.py +0 -294
  46. genelastic/info.py +0 -248
  47. genelastic/integrity.py +0 -324
  48. genelastic/validate_data.py +0 -41
  49. genelastic-0.6.0.dist-info/METADATA +0 -36
  50. genelastic-0.6.0.dist-info/RECORD +0 -25
  51. genelastic-0.6.0.dist-info/entry_points.txt +0 -6
  52. {genelastic-0.6.0.dist-info → genelastic-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,402 @@
1
+ import copy
2
+ import random
3
+ import shutil
4
+ import sys
5
+ import tempfile
6
+ import typing
7
+ from abc import ABC, abstractmethod
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+ from biophony import BioSeqGen, CovGen, Elements, FastaWriter, MutSim
12
+
13
+ from genelastic.common import (
14
+ RandomAnalysisData,
15
+ RandomBiProcessData,
16
+ RandomWetProcessData,
17
+ )
18
+
19
+
20
+ class RandomBundleItem(ABC):
21
+ """Abstract class representing a randomly generated bundle item."""
22
+
23
+ def _random_alphanum_str(
24
+ self, chars: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", n: int = 4
25
+ ) -> str:
26
+ """Generate a random alphanumerical string."""
27
+ return "".join(random.sample(list(chars), n))
28
+
29
+ @abstractmethod
30
+ def to_dict(self) -> typing.Any: # noqa: ANN401
31
+ """Return the randomly generated item data as a dict."""
32
+
33
+
34
+ class RandomWetProcess(RandomBundleItem):
35
+ """Generate a random wet lab process.
36
+
37
+ :param seed: Set a seed for data reproducibility.
38
+ """
39
+
40
+ KITS: typing.ClassVar = [
41
+ {
42
+ "generic_kit": "truseq-illumina",
43
+ "library_kit": "truseq-illumina",
44
+ "sequencing_kit": "truseq-illumina",
45
+ },
46
+ {
47
+ "generic_kit": "smrtbellprepkit3.0",
48
+ "library_kit": "smrtbellprepkit3.0",
49
+ "sequencing_kit": "revio_polymerase_sequencing",
50
+ },
51
+ {
52
+ "generic_kit": "sqk-lsk114",
53
+ "library_kit": "sqk-lsk114",
54
+ "sequencing_kit": "sqk-lsk114",
55
+ },
56
+ ]
57
+
58
+ def __init__(self, seed: int | None = None) -> None:
59
+ random.seed(seed)
60
+
61
+ self._proc_id = self._random_alphanum_str(n=8)
62
+ self._manufacturer = random.choice(["illumina", "ont", "pacbio"])
63
+ self._sequencer = random.choice(
64
+ ["novaseqxplus", "promethion", "novaseq6000", "revio"]
65
+ )
66
+
67
+ kit: dict[str, str] = random.choice(self.KITS)
68
+ self._generic_kit = kit["generic_kit"]
69
+ self._library_kit = kit["library_kit"]
70
+ self._sequencing_kit = kit["sequencing_kit"]
71
+
72
+ self._fragmentation = random.choice(range(100, 401, 50))
73
+ self._reads_size = random.choice(range(100, 401, 50))
74
+ self._flowcell_type = f"{random.choice(range(10, 101, 10))}b"
75
+ self._sequencing_type = "wgs" + random.choice(["", "-iclr", "-lowpass"])
76
+ self._error_rate_expected = round(random.uniform(0.01, 0.1), 2)
77
+
78
+ def to_dict(self) -> RandomWetProcessData:
79
+ """Return the generated wet lab process as a dictionary."""
80
+ return {
81
+ "proc_id": self._proc_id,
82
+ "manufacturer": self._manufacturer,
83
+ "sequencer": self._sequencer,
84
+ "generic_kit": self._generic_kit,
85
+ "library_kit": self._library_kit,
86
+ "sequencing_kit": self._sequencing_kit,
87
+ "fragmentation": self._fragmentation,
88
+ "reads_size": self._reads_size,
89
+ "input_type": "gdna",
90
+ "amplification": "pcr-free",
91
+ "flowcell_type": self._flowcell_type,
92
+ "sequencing_type": self._sequencing_type,
93
+ "error_rate_expected": self._error_rate_expected,
94
+ }
95
+
96
+
97
+ class RandomBiProcess(RandomBundleItem):
98
+ """Generate a random bioinformatics process.
99
+
100
+ :param seed: Set a seed for data reproducibility.
101
+ """
102
+
103
+ STEPS: typing.ClassVar = [
104
+ {"name": "basecalling", "cmd": ["bclconvert", "dorado", "smrtlink"]},
105
+ {"name": "mapping", "cmd": ["bwa", "dragmap", "minimap", "pbmm"]},
106
+ {"name": "postmapping", "cmd": ["bqsr", "dragen"]},
107
+ {
108
+ "name": "smallvarcalling",
109
+ "cmd": [
110
+ "gatk_haplotypecaller",
111
+ "octopus",
112
+ "glimpse",
113
+ "dragen",
114
+ "deepvariant",
115
+ "clair",
116
+ ],
117
+ "output": "smallvar",
118
+ },
119
+ {
120
+ "name": "svcalling",
121
+ "cmd": ["manta", "dragen", "sniffles", "cutesv", "pbsv"],
122
+ "output": "sv",
123
+ },
124
+ {"name": "secondary_qc", "cmd": ["genomx", "dragen", "lrqc"]},
125
+ {"name": "trimming", "cmd": ["dragen", "seqfiltering"]},
126
+ {"name": "phasing", "cmd": ["whatshap"]},
127
+ ]
128
+
129
+ def __init__(self, seed: int | None = None) -> None:
130
+ random.seed(seed)
131
+
132
+ self._proc_id = self._random_alphanum_str(n=8)
133
+
134
+ version_str_len = random.choice(range(1, 5))
135
+ self._pipeline_version = self._generate_version(version_str_len)
136
+ self._name = random.choice(
137
+ ["varscope", "glimpse", "dragen", "vacana", "pbvaria"]
138
+ )
139
+
140
+ self._steps: list[dict[str, str]] = []
141
+ self._generate_steps()
142
+
143
+ @staticmethod
144
+ def _generate_version(count: int) -> str:
145
+ """Generate a random version string.
146
+
147
+ :param count: Count of numbers present in the number string.
148
+ :raises ValueError: If count is less than 1.
149
+ :return: A random version string with the specified count of numbers.
150
+ """
151
+ if count < 1:
152
+ msg = "Count of numbers present in the version string must be > 0."
153
+ raise ValueError(msg)
154
+
155
+ lower_bound = 0
156
+ # Do not use 0 for versions string with only one number.
157
+ if count == 1:
158
+ lower_bound = 1
159
+
160
+ version_parts = [
161
+ str(random.randint(lower_bound, 9)) for _ in range(count)
162
+ ]
163
+ return ".".join(version_parts)
164
+
165
+ def _generate_steps(self) -> None:
166
+ steps_count = random.randint(1, 5)
167
+ random_steps = copy.deepcopy(random.sample(self.STEPS, steps_count))
168
+ for random_step in random_steps:
169
+ random_step["version"] = self._generate_version(
170
+ random.choice(range(1, 5))
171
+ )
172
+ random_step["cmd"] = random.choice(random_step["cmd"])
173
+ self._steps.append(random_step)
174
+
175
+ def to_dict(self) -> RandomBiProcessData:
176
+ """Return the generated bi informatics process as a dictionary."""
177
+ return {
178
+ "proc_id": self._proc_id,
179
+ "name": self._name,
180
+ "pipeline_version": self._pipeline_version,
181
+ "steps": self._steps,
182
+ "sequencing_type": "wgs",
183
+ }
184
+
185
+
186
+ class RandomAnalysis(RandomBundleItem):
187
+ """Generate a random analysis."""
188
+
189
+ def __init__( # noqa: PLR0913
190
+ self,
191
+ folder: Path,
192
+ seq_len: int,
193
+ nb_chrom: int,
194
+ wet_proc_id: str,
195
+ bi_proc_id: str,
196
+ *,
197
+ do_gen_coverage: bool,
198
+ ) -> None:
199
+ self._folder = folder
200
+ self._seq_len = seq_len
201
+ self._nb_chrom = nb_chrom
202
+ self._sample_name = "HG0003"
203
+ self._source = "CNRGH"
204
+ self._wet_process_id = wet_proc_id
205
+ self._bi_process_id = bi_proc_id
206
+ self._barcode = self._random_alphanum_str(n=6)
207
+ self._reference_genome = "hg38"
208
+ self._prefix = (
209
+ f"{self._sample_name}_{self._source}_{self._wet_process_id}_{self._bi_process_id}_"
210
+ f"{self._barcode}_{self._reference_genome}"
211
+ )
212
+
213
+ self._gen_vcf_file()
214
+ if do_gen_coverage:
215
+ self.gen_cov_file()
216
+
217
+ def _gen_vcf_file(self) -> None:
218
+ """Generate a dummy VCF file."""
219
+ temp_dir = Path(tempfile.mkdtemp())
220
+
221
+ try:
222
+ fasta_out_file = temp_dir / "seq.fasta"
223
+ vcf_out_file = self._folder / f"{self._prefix}.vcf"
224
+
225
+ # 1 - Generate a FASTA file and save it to a temporary directory.
226
+ gen = BioSeqGen(
227
+ elements=Elements(), seqlen=self._seq_len, count=self._nb_chrom
228
+ )
229
+ with fasta_out_file.open("w", encoding="utf-8") as f:
230
+ FastaWriter(f, header=False).write_seqs(gen)
231
+
232
+ # 2 - Generate a VCF from the previously created FASTA file.
233
+ MutSim(
234
+ fasta_file=str(fasta_out_file),
235
+ vcf_file=str(vcf_out_file),
236
+ snp_rate=0.02,
237
+ ins_rate=0.01,
238
+ del_rate=0.01,
239
+ ).run()
240
+
241
+ finally:
242
+ shutil.rmtree(temp_dir)
243
+
244
+ def gen_cov_file(self) -> None:
245
+ """Generate a dummy coverage file."""
246
+ chrom_end = self._seq_len - 1
247
+
248
+ output_path = self._folder / f"{self._prefix}.cov.tsv"
249
+ with output_path.open("w", encoding="utf-8") as f:
250
+ for chrom in range(1, self._nb_chrom + 1):
251
+ coverage = CovGen(
252
+ chrom=str(chrom),
253
+ min_pos=0,
254
+ max_pos=chrom_end,
255
+ min_depth=5,
256
+ max_depth=15,
257
+ depth_offset=0,
258
+ depth_change_rate=0.1,
259
+ )
260
+
261
+ for item in coverage:
262
+ f.write(item.to_bed_line() + "\n")
263
+
264
+ def to_dict(self) -> RandomAnalysisData:
265
+ """Return the generated analysis as a dictionary."""
266
+ return {
267
+ "file_prefix": "%S_%F_%W_%B_%A_%R",
268
+ "sample_name": self._sample_name,
269
+ "source": self._source,
270
+ "barcode": self._barcode,
271
+ "wet_process": self._wet_process_id,
272
+ "bi_process": self._bi_process_id,
273
+ "reference_genome": self._reference_genome,
274
+ "flowcell": self._random_alphanum_str(n=8),
275
+ "lanes": [random.randint(1, 10)],
276
+ "seq_indices": [
277
+ "DUAL219",
278
+ "DUAL222",
279
+ "DUAL225",
280
+ "DUAL228",
281
+ "DUAL289",
282
+ ],
283
+ "data_path": str(self._folder),
284
+ }
285
+
286
+
287
+ class RandomBundle(RandomBundleItem):
288
+ """Generate a random analyses bundle."""
289
+
290
+ def __init__( # noqa: PLR0913
291
+ self,
292
+ folder: Path,
293
+ analyses_count: int,
294
+ processes_count: int,
295
+ nb_chrom: int,
296
+ seq_len: int,
297
+ *,
298
+ do_gen_coverage: bool,
299
+ ) -> None:
300
+ self._folder = folder
301
+ self._analyses_count = analyses_count
302
+ self._processes_count = processes_count
303
+ self._nb_chrom = nb_chrom
304
+ self._seq_len = seq_len
305
+ self._do_gen_coverage = do_gen_coverage
306
+ self._analyses: list[RandomAnalysisData] = []
307
+
308
+ self._wet_processes = [
309
+ RandomWetProcess().to_dict() for _ in range(self._processes_count)
310
+ ]
311
+ self._assigned_wet_processes = self._assign_processes(
312
+ self._wet_processes, self._analyses_count
313
+ )
314
+
315
+ self._bi_processes = [
316
+ RandomBiProcess().to_dict() for _ in range(self._processes_count)
317
+ ]
318
+ self._assigned_bi_processes = self._assign_processes(
319
+ self._bi_processes, self._analyses_count
320
+ )
321
+
322
+ self._analyses.extend(
323
+ [
324
+ RandomAnalysis(
325
+ self._folder,
326
+ self._seq_len,
327
+ self._nb_chrom,
328
+ str(self._assigned_wet_processes[i]["proc_id"]),
329
+ str(self._assigned_bi_processes[i]["proc_id"]),
330
+ do_gen_coverage=self._do_gen_coverage,
331
+ ).to_dict()
332
+ for i in range(self._analyses_count)
333
+ ]
334
+ )
335
+
336
+ @staticmethod
337
+ def _assign_processes(
338
+ random_processes: list[dict[str, typing.Any]],
339
+ analyses_count: int,
340
+ ) -> list[dict[str, typing.Any]]:
341
+ """Assigns a specified number of processes to analyses.
342
+
343
+ This function ensures that the returned list contains exactly `analyses_count` processes:
344
+ - If there are more processes than required, it selects a random subset.
345
+ - If there are fewer processes than required, it extends the list by randomly selecting additional
346
+ processes until the desired size is reached.
347
+ - If the number of processes matches the number of analyses, the same list is returned.
348
+
349
+ :param random_processes: A list of available processes.
350
+ :param analyses_count: The number of processes required.
351
+ :raises ValueError: If the input list `random_processes` is empty.
352
+ :returns: A list of processes with a length of `analyses_count`.
353
+ """
354
+ if not random_processes:
355
+ msg = "Random processes list is empty."
356
+ raise ValueError(msg)
357
+
358
+ if len(random_processes) > analyses_count:
359
+ # Case 1: More processes than analyses.
360
+ # Select a random subset of processes with the required size.
361
+ return random.sample(random_processes, analyses_count)
362
+
363
+ # Case 2: Equal or fewer processes than analyses.
364
+ # If the number of processes equals the number of analyses, return the same list.
365
+ # Otherwise, extend the list by randomly selecting additional processes until the desired size is reached.
366
+ random_process_copy = random_processes.copy()
367
+
368
+ while len(random_process_copy) < analyses_count:
369
+ random_process_copy.append(random.choice(random_processes))
370
+
371
+ return random_process_copy
372
+
373
+ # Write import bundle YAML
374
+ def to_yaml(self, output_file: Path | None) -> None:
375
+ """Export the generated bundle in YAML format to a file or stdout."""
376
+ # Standard output
377
+ if not output_file:
378
+ sys.stdout.write("---\n")
379
+ yaml.dump(self.to_dict(), sys.stdout)
380
+
381
+ # File
382
+ else:
383
+ with output_file.open("w", encoding="utf-8") as f:
384
+ f.write("---\n")
385
+ yaml.dump(self.to_dict(), f)
386
+
387
+ def to_dict(
388
+ self,
389
+ ) -> dict[
390
+ str,
391
+ int
392
+ | list[RandomAnalysisData]
393
+ | list[RandomBiProcessData]
394
+ | list[RandomWetProcessData],
395
+ ]:
396
+ """Return the generated bundle as a dictionary."""
397
+ return {
398
+ "version": 3,
399
+ "analyses": self._analyses,
400
+ "bi_processes": self._bi_processes,
401
+ "wet_processes": self._wet_processes,
402
+ }
@@ -1,23 +1,24 @@
1
- # pylint: disable=missing-module-docstring
2
1
  import logging
3
2
  import re
4
3
  import typing
5
4
 
6
- from .common import BundleDict
7
- from .constants import DEFAULT_TAG2FIELD, DEFAULT_TAG_SUFFIX, DEFAULT_TAG_PREFIX
5
+ from genelastic.common import BundleDict
8
6
 
9
- logger = logging.getLogger('genelastic')
7
+ from .constants import DEFAULT_TAG2FIELD, DEFAULT_TAG_PREFIX, DEFAULT_TAG_SUFFIX
10
8
 
11
- TagsDefinition: typing.TypeAlias = typing.Dict[str, typing.Dict[str, str | typing.Dict[str, str]]]
9
+ logger = logging.getLogger("genelastic")
10
+
11
+ TagsDefinition: typing.TypeAlias = dict[str, dict[str, str | dict[str, str]]]
12
12
 
13
13
 
14
14
  class Tags:
15
- """
16
- This class handles the definition of default and custom tags.
15
+ """This class handles the definition of default and custom tags.
17
16
  Tags are used to extract custom metadata from files belonging to an analysis.
18
17
  """
19
- def __init__(self, documents: typing.Sequence[BundleDict] | None):
20
- self._tags: typing.Dict[str, typing.Dict[str, str]] = DEFAULT_TAG2FIELD
18
+
19
+ def __init__(self, documents: typing.Sequence[BundleDict] | None) -> None:
20
+ """Create a Tag instance."""
21
+ self._tags: dict[str, dict[str, str]] = DEFAULT_TAG2FIELD
21
22
  self._tag_prefix: str = DEFAULT_TAG_PREFIX
22
23
  self._tag_suffix: str = DEFAULT_TAG_SUFFIX
23
24
 
@@ -29,11 +30,15 @@ class Tags:
29
30
 
30
31
  if redefined_tags:
31
32
  self._build_tags(redefined_tags)
32
- logger.info("The following tags will be used to extract metadata from filenames : %s",
33
- self._tags)
33
+ logger.info(
34
+ "The following tags will be used to extract metadata from filenames : %s",
35
+ self._tags,
36
+ )
34
37
  else:
35
- logger.info("Using the default tags to extract metadata from filenames : %s",
36
- self._tags)
38
+ logger.info(
39
+ "Using the default tags to extract metadata from filenames : %s",
40
+ self._tags,
41
+ )
37
42
 
38
43
  def _build_tags(self, redefined_tags: TagsDefinition) -> None:
39
44
  # Erase the tags defined by defaults.
@@ -52,22 +57,31 @@ class Tags:
52
57
 
53
58
  for tag_name, tag_attrs in redefined_tags["match"].items():
54
59
  if isinstance(tag_attrs, dict): # extra type check for mypy
55
- self._tags[f"{self._tag_prefix}{tag_name}{self._tag_suffix}"] = tag_attrs
60
+ self._tags[
61
+ f"{self._tag_prefix}{tag_name}{self._tag_suffix}"
62
+ ] = tag_attrs
56
63
 
57
64
  @staticmethod
58
- def _search_redefined_tags(documents: typing.Sequence[BundleDict]) -> TagsDefinition | None:
59
-
60
- documents_with_redefined_tags: typing.List[BundleDict] = \
61
- [d for d in documents if 'tags' in d]
62
- bundle_paths = [d['bundle_file'] for d in documents_with_redefined_tags]
65
+ def _search_redefined_tags(
66
+ documents: typing.Sequence[BundleDict],
67
+ ) -> TagsDefinition | None:
68
+ documents_with_redefined_tags: list[BundleDict] = [
69
+ d for d in documents if "tags" in d
70
+ ]
71
+ bundle_paths = [d["bundle_file"] for d in documents_with_redefined_tags]
63
72
 
64
73
  # If there are more than one 'tags' redefinition across the documents, raise an error.
65
74
  if len(documents_with_redefined_tags) > 1:
66
- raise RuntimeError(f"Only one 'tags' key should be defined across all documents, "
67
- f"but multiple were found : {', '.join(bundle_paths)}")
75
+ msg = (
76
+ f"Only one 'tags' key should be defined across all documents, "
77
+ f"but multiple were found : {', '.join(bundle_paths)}"
78
+ )
79
+ raise RuntimeError(msg)
68
80
 
69
81
  if len(documents_with_redefined_tags) == 1:
70
- redefined_tags: TagsDefinition = documents_with_redefined_tags[0]['tags']
82
+ redefined_tags: TagsDefinition = documents_with_redefined_tags[0][
83
+ "tags"
84
+ ]
71
85
  return redefined_tags
72
86
 
73
87
  return None
@@ -83,9 +97,8 @@ class Tags:
83
97
  return self._tag_suffix
84
98
 
85
99
  @property
86
- def items(self) -> typing.ItemsView[str, typing.Dict[str, str]]:
87
- """
88
- Returns the tag items : the key is the tag name,
100
+ def items(self) -> typing.ItemsView[str, dict[str, str]]:
101
+ """Returns the tag items : the key is the tag name,
89
102
  and the value is the tag attributes (a dict containing the 'field' and 'regex' keys).
90
103
  """
91
104
  return self._tags.items()
@@ -93,10 +106,18 @@ class Tags:
93
106
  @property
94
107
  def search_regex(self) -> str:
95
108
  """Returns a regex to search for a tag inside a string."""
96
- return r"(" + re.escape(self._tag_prefix) + r"\w+" + re.escape(self._tag_suffix) + r")"
109
+ return (
110
+ r"("
111
+ + re.escape(self._tag_prefix)
112
+ + r"\w+"
113
+ + re.escape(self._tag_suffix)
114
+ + r")"
115
+ )
97
116
 
98
117
  def __len__(self) -> int:
118
+ """Return the number of registered tags."""
99
119
  return len(self._tags)
100
120
 
101
- def __getitem__(self, key: str) -> typing.Dict[str, str]:
121
+ def __getitem__(self, key: str) -> dict[str, str]:
122
+ """Return a tag by its key."""
102
123
  return self._tags[key]
@@ -1,4 +1,3 @@
1
- # pylint: disable=missing-module-docstring
2
1
  import copy
3
2
 
4
3
  from genelastic.common import WetProcessesData
@@ -6,9 +5,14 @@ from genelastic.common import WetProcessesData
6
5
 
7
6
  class WetProcess:
8
7
  """Class WetProcess that represents a wet process."""
9
- def __init__(self, proc_id: str,
10
- bundle_file: str | None = None,
11
- **data: str | int | float) -> None:
8
+
9
+ def __init__(
10
+ self,
11
+ proc_id: str,
12
+ bundle_file: str | None = None,
13
+ **data: str | float,
14
+ ) -> None:
15
+ """Create a WetProcess instance."""
12
16
  self._proc_id = proc_id
13
17
  self._bundle_file = bundle_file
14
18
  self._data: WetProcessesData = data
@@ -1,23 +1,26 @@
1
- # pylint: disable=missing-module-docstring
2
1
  import logging
3
2
  import typing
4
3
 
5
- from .common import BundleDict
4
+ from genelastic.common import BundleDict
5
+
6
6
  from .wet_process import WetProcess
7
7
 
8
- logger = logging.getLogger('genelastic')
8
+ logger = logging.getLogger("genelastic")
9
9
 
10
10
 
11
11
  class WetProcesses:
12
12
  """Class WetProcesses is a container of WetProces objects."""
13
13
 
14
14
  def __init__(self) -> None:
15
- self._dict: typing.Dict[str, WetProcess] = {}
15
+ """Create an empty container."""
16
+ self._dict: dict[str, WetProcess] = {}
16
17
 
17
18
  def __len__(self) -> int:
19
+ """Return the number of WetProcess objects inside the container."""
18
20
  return len(self._dict)
19
21
 
20
22
  def __getitem__(self, key: str) -> WetProcess:
23
+ """Return a WetProcess present in the container by its key."""
21
24
  return self._dict[key]
22
25
 
23
26
  def add(self, process: WetProcess) -> None:
@@ -25,20 +28,23 @@ class WetProcesses:
25
28
  If a WetProces object with the same ID already exists in the container, the program exits.
26
29
  """
27
30
  if process.id in self._dict:
28
- raise ValueError(f"A wet process with the id '{process.id}' is already present.")
31
+ msg = (
32
+ f"A wet process with the id '{process.id}' is already present."
33
+ )
34
+ raise ValueError(msg)
29
35
 
30
36
  # Add one WetProcess object.
31
37
  self._dict[process.id] = process
32
38
 
33
- def get_process_ids(self) -> typing.Set[str]:
39
+ def get_process_ids(self) -> set[str]:
34
40
  """Get a list of the wet processes IDs."""
35
41
  return set(self._dict.keys())
36
42
 
37
43
  @classmethod
38
- def from_array_of_dicts(cls, arr: typing.Sequence[BundleDict]
39
- ) -> typing.Self:
44
+ def from_array_of_dicts(
45
+ cls, arr: typing.Sequence[BundleDict]
46
+ ) -> typing.Self:
40
47
  """Build a WetProcesses instance."""
41
-
42
48
  wet_processes = cls()
43
49
 
44
50
  for d in arr:
File without changes