sbtabpy 1.1.1__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.
sbtab/SBtab.py ADDED
@@ -0,0 +1,1714 @@
1
+ """
2
+ sbtab is a table format for Systems Biology.
3
+
4
+ It can store different kinds of data. The SBtabTable and SBtabDocument
5
+ classes allow the storage, manipulation, and removal of data from the tables.
6
+
7
+ See specification for further information.
8
+ """
9
+
10
+ import datetime
11
+ import logging
12
+ import re
13
+ from collections.abc import Mapping
14
+ from pathlib import Path
15
+
16
+ from . import utils
17
+
18
+ # Sentinel for get_attribute(), so that default=None can be told from no default.
19
+ _MISSING = object()
20
+
21
+
22
+ def _attribute_pattern(attribute):
23
+ """
24
+ Return a regex matching attribute='value' in a declaration row.
25
+
26
+ The attribute name is escaped and must not be preceded by a word character,
27
+ so that e.g. 'Name' does not match inside "TableName='...'".
28
+ """
29
+ return r"(?<![\w])%s='([^']*)'" % re.escape(attribute)
30
+
31
+
32
+ def _bare_column_name(column):
33
+ """Return a column name without its (single) leading '!'."""
34
+ return column[1:] if column.startswith("!") else column
35
+
36
+
37
+ def read_csv(filepath, document_name, xlsx=False, definitions_file=None):
38
+ """
39
+ Read an sbtab file; it can be csv, but also tsv.
40
+
41
+ Parameters
42
+ ----------
43
+ filepath: str
44
+ Path to the file that shall be read.
45
+ document_name: str
46
+ A name for the document to be created.
47
+ xlsx: Bool
48
+ Boolean flag that indicates if the file is in xlsx format.
49
+
50
+ Returns: sbtab.SBtabDocument
51
+ sbtab document which is created from the given file.
52
+ """
53
+ sbtab_file = False
54
+
55
+ if xlsx:
56
+ try:
57
+ sbtab_xlsx = open(filepath, "rb")
58
+ sbtab_tsv = utils.xlsx_to_tsv(sbtab_xlsx, f="file")
59
+ sbtab_xlsx.close()
60
+ sbtab_doc = SBtabDocument(
61
+ document_name, sbtab_tsv, filepath, definitions_file
62
+ )
63
+ return sbtab_doc
64
+ except Exception as e:
65
+ raise SBtabError("The sbtab could not be generated: %s" % (str(e)))
66
+
67
+ try:
68
+ sbtab_file = open(filepath, "r")
69
+ sbtab_doc = SBtabDocument(
70
+ document_name, sbtab_file.read(), filepath, definitions_file
71
+ )
72
+ sbtab_file.close()
73
+ return sbtab_doc
74
+ except Exception as e:
75
+ if sbtab_file:
76
+ sbtab_file.close()
77
+ raise SBtabError("The sbtab could not be generated: %s" % (str(e)))
78
+
79
+
80
+ class SBtabError(Exception):
81
+ """Base class for errors in the sbtab class."""
82
+
83
+ def __init__(self, *args):
84
+ """Initialize SBtabError with message."""
85
+ super().__init__(*args)
86
+ self.message = args[0] if args else ""
87
+
88
+ def __str__(self):
89
+ """Return string representation of error."""
90
+ return self.message
91
+
92
+
93
+ class SBtabTable:
94
+ """SBtabTable (version 1.0.0 24/12/2018)."""
95
+
96
+ def __init__(self, table_string=None, filename=None):
97
+ """
98
+ Create sbtab table object from string.
99
+
100
+ Parameters
101
+ ----------
102
+ table : str
103
+ One sbtab string from read file.
104
+ filename : str
105
+ Filename with extension.
106
+ """
107
+ if filename:
108
+ self.set_filename(filename)
109
+ else:
110
+ self.filename = None
111
+
112
+ if table_string:
113
+ self.add_sbtab_string(table_string)
114
+
115
+ self.object_type = "table"
116
+
117
+ def _validate_extension(self, test=None):
118
+ """Check the extension of the file for invalid formats."""
119
+ valid_extensions = {".tsv", ".csv", ".xlsx"}
120
+ filename = test if test is not None else self.filename
121
+ suffix = Path(filename).suffix
122
+ if suffix not in valid_extensions:
123
+ raise SBtabError(
124
+ "The file extension of %s is not valid for an sbtab file." % filename
125
+ )
126
+ return True
127
+
128
+ def _singular(self):
129
+ """
130
+ Check that SBtabTable contains only one sbtab.
131
+
132
+ If more than one SBtabs are contained, an error is issued and
133
+ the usage of SBtabDocument is suggested.
134
+ """
135
+ header_row_count = 0
136
+ for row in self.table_string.split("\n"):
137
+ if row.startswith("!!SBtab") or row.startswith("!!ObjTables"):
138
+ header_row_count += 1
139
+
140
+ if header_row_count > 1:
141
+ raise SBtabError(
142
+ "There are more than one sbtab tables in this file. Please"
143
+ " use the SBtabDocument class instead of SBtabTable."
144
+ )
145
+
146
+ def _preprocess_table_string(self, table_string):
147
+ """
148
+ Preprocess the table string to fix common formatting problems.
149
+
150
+ There is so much stuff that can be made wrong with the input files.
151
+ This function tries to catch some of the common problems.
152
+ """
153
+ table_string = table_string.replace("\r", "")
154
+ table_string = table_string.replace("^M", "")
155
+ table_string_prep = ""
156
+
157
+ for row in table_string.split("\n"):
158
+ row = self._dequote(row)
159
+ row = row.replace("=''", "X@X")
160
+ while "''" in row:
161
+ row = row.replace("''", "'")
162
+ row = row.replace("X@X", "=''")
163
+ row.replace(
164
+ "%s,%s" % (self.delimiter, self.delimiter),
165
+ "%s%s" % (self.delimiter, self.delimiter),
166
+ )
167
+ table_string_prep += row + "\n"
168
+
169
+ return table_string_prep
170
+
171
+ def _cut_table_string(self, table_string, delimiter_test=None):
172
+ """
173
+ Cut the sbtab string into a list to harvest content.
174
+
175
+ The sbtab is initially given as one long string; cut down this
176
+ string into list to harvest content.
177
+ """
178
+ if delimiter_test:
179
+ delimiter = delimiter_test
180
+ else:
181
+ delimiter = self.delimiter
182
+
183
+ table_list = []
184
+ for row in table_string.split("\n"):
185
+ if row.replace(delimiter, "") != "" and row.replace(delimiter, "") != "[]":
186
+ if not row.startswith('"!') and not row.startswith("!"):
187
+ if "'" in row or "{" in row or "[" in row:
188
+ try:
189
+ cut_row = self._handle_row(row, delimiter)
190
+ table_list.append(cut_row)
191
+ except Exception:
192
+ print(
193
+ "Row %s could not be attached due to bad syntax." % row
194
+ )
195
+ else:
196
+ table_list.append(row.split(delimiter))
197
+ else:
198
+ table_list.append(row.split(delimiter))
199
+
200
+ return table_list
201
+
202
+ def _handle_row(self, row, delimiter):
203
+ """
204
+ Handle rows that may contain characters causing format troubles.
205
+
206
+ Some rows may contain characters that cause format troubles:
207
+ quotes, commas, JSON strings, and combinations of them. This
208
+ function carefully handles these issues and cuts the row into
209
+ its correct single pieces (which are the columns).
210
+ """
211
+ # provide an anchor for the end of the row to support our regex
212
+ row += delimiter
213
+
214
+ # first, unify the employed quotes to '
215
+ row = self._dequote(row)
216
+
217
+ # then, find all quoted columns
218
+ if "'" in row or "{" in row or "[" in row:
219
+ # find beginning and start of quoted columns
220
+ iterators = re.finditer(r"('.*?')%s" % delimiter, row)
221
+ indices = [0]
222
+
223
+ for i in iterators:
224
+ indices.append(i.start())
225
+ indices.append(i.end())
226
+
227
+ # remove duplicates
228
+ indices_set = list(sorted(set(indices)))
229
+
230
+ # cut row at the beginning and start indices
231
+ items_pre = [
232
+ row[i : j - 1]
233
+ for i, j in zip(indices_set, indices_set[1:] + [len(row) + 1])
234
+ ]
235
+
236
+ # further cut row at the delimiter and finish off items
237
+ items = []
238
+ jsons = []
239
+ jlist = []
240
+ running_json = False
241
+ running_jlist = False
242
+
243
+ for item in items_pre:
244
+ # in the case of a comma as separator, we need to be careful
245
+ # with the JSONs which naturally hold commas
246
+ if delimiter == ",":
247
+ # 1st case: we have a currently open JSON column
248
+ if running_json and not item.endswith("}'"):
249
+ jsons.append(item)
250
+ # 2nd case: we have a JSON column start
251
+ elif item.startswith("'{"):
252
+ jsons.append(item)
253
+ running_json = True
254
+ if item.endswith("}'"):
255
+ while "" in jsons:
256
+ jsons.remove("")
257
+ items.append(",".join(jsons))
258
+ jsons = []
259
+ running_json = False
260
+ # 3rd case: we have a JSON column end
261
+ elif running_json and item.endswith("}'"):
262
+ jsons.append(item)
263
+ while "" in jsons:
264
+ jsons.remove("")
265
+ items.append(",".join(jsons))
266
+ jsons = []
267
+ running_json = False
268
+
269
+ # 1bst case: we have a currently open Jlist column
270
+ elif running_jlist and not item.endswith("]'"):
271
+ if item.strip() != "":
272
+ jlist.append(item)
273
+ # 2bnd case: we have a Jlist column start
274
+ elif item.startswith("'["):
275
+ jlist.append(item)
276
+ running_jlist = True
277
+ if item.endswith("]'"):
278
+ while "" in jlist:
279
+ jlist.remove("")
280
+ items.append(",".join(jlist))
281
+ jlist = []
282
+ running_jlist = False
283
+ # 3brd case: we have a Jlist column end
284
+ elif running_jlist and item.endswith("]'"):
285
+ jlist.append(item)
286
+ while "" in jlist:
287
+ jlist.remove("")
288
+ items.append(",".join(jlist))
289
+ jlist = []
290
+ running_jlist = False
291
+
292
+ # 4th case: we have a quoted column
293
+ elif (
294
+ item.startswith("'")
295
+ and not item.startswith("'{")
296
+ and not item.endswith("}'")
297
+ and not item.startswith("'[")
298
+ and not item.endswith("]'")
299
+ ):
300
+ items.append(item)
301
+
302
+ # 5th case: we have a normal column
303
+ elif item == "":
304
+ items += [""]
305
+ else:
306
+ items = items + item.split(delimiter)
307
+ else:
308
+ # for all other delimiters we are comparably easy going:
309
+ if item.startswith("'"):
310
+ items.append(item)
311
+ else:
312
+ items = items + item.split(delimiter)
313
+
314
+ # remove the last element which was added in the beginning
315
+ items.pop()
316
+
317
+ return items
318
+
319
+ def _initialize_table(self):
320
+ """Load table information and class variables."""
321
+ # read a potential document row
322
+ self.doc_row = self._get_doc_row()
323
+
324
+ # Read the header row from table
325
+ self.header_row = self._get_header_row()
326
+
327
+ # Read the table information from header row
328
+ (
329
+ self.table_id,
330
+ self.table_type,
331
+ self.table_name,
332
+ self.table_document,
333
+ self.table_version,
334
+ self.standard_concentration,
335
+ ) = self._get_table_information()
336
+
337
+ # Read the columns of the table
338
+ self.columns, self.columns_dict = self._get_columns()
339
+
340
+ # Read data rows
341
+ self.value_rows = self._get_rows()
342
+
343
+ def _get_doc_row(self):
344
+ """
345
+ Check if the sbtab Table holds a !!!-line to declare a belonging sbtab document.
346
+
347
+ See if the sbtab Table holds a !!!-line to declare a belonging sbtab
348
+ document.
349
+ """
350
+ doc_row_dq = False
351
+
352
+ for row in self.table:
353
+ for entry in row:
354
+ if str(entry).startswith("!!!"):
355
+ doc_row = row
356
+ doc_row_dq = self._dequote(doc_row)
357
+
358
+ # determine if this is an sbtab or ObjTables Document
359
+ if "!!!ObjTables" in doc_row_dq:
360
+ self.document_format = "ObjTables"
361
+ elif "!!!SBtab" in doc_row_dq:
362
+ self.document_format = "sbtab"
363
+ else:
364
+ self.document_format = None
365
+
366
+ return doc_row_dq
367
+ elif str(entry).startswith('"!!!'):
368
+ rm2 = row.replace('""', "#").replace('"', "")
369
+ doc_row = rm2.replace("#", '"')
370
+ doc_row_dq = self._dequote(doc_row)
371
+
372
+ # determine if this is an sbtab or ObjTables Document
373
+ if "!!!ObjTables" in doc_row_dq:
374
+ self.document_format = "ObjTables"
375
+ elif "!!!SBtab" in doc_row_dq:
376
+ self.document_format = "sbtab"
377
+ else:
378
+ self.document_format = None
379
+
380
+ return doc_row_dq
381
+
382
+ def _get_header_row(self):
383
+ """Extract the declaration row from the sbtab file."""
384
+ header_row = None
385
+ # Find header row
386
+ for row in self.table:
387
+ for entry in row:
388
+ if str(entry).startswith("!!") and not str(entry).startswith("!!!"):
389
+ header_row = "".join(row).rstrip("\n")
390
+ break
391
+ elif str(entry).startswith("'!!") and not str(entry).startswith("'!!!"):
392
+ rm1 = entry.replace("''", "#")
393
+ rm2 = rm1.replace("'", "")
394
+ header_row = rm2.replace("#", "'")
395
+ break
396
+
397
+ # Save string or raise error
398
+ if not header_row:
399
+ raise SBtabError("""This is not a valid sbtab table, please use
400
+ validator to check format or have a look in the specification!""")
401
+
402
+ header_row_dq = self._dequote(header_row)
403
+
404
+ # determine if this is an sbtab or ObjTables Document
405
+ if "!!ObjTables" in header_row_dq:
406
+ self.table_format = "ObjTables"
407
+ elif "!!SBtab" in header_row_dq:
408
+ self.table_format = "sbtab"
409
+ else:
410
+ raise SBtabError("""This is not a valid sbtab table, please use
411
+ validator to check format or have a look in the specification!""")
412
+
413
+ return header_row_dq
414
+
415
+ def _dequote(self, row):
416
+ """
417
+ Bring consistency in the multifarious quotation mark problems.
418
+
419
+ Bring consistency in the multifarious quotation mark problems.
420
+ """
421
+ stupid_quotes = [
422
+ "\xe2\x80\x9d",
423
+ "\xe2\x80\x98",
424
+ "\xe2\x80\x99",
425
+ "\xe2\x80\x9b",
426
+ "\xe2\x80\x9c",
427
+ "\xe2\x80\x9f",
428
+ '"',
429
+ "\xe2\x80\xb2",
430
+ "\xe2\x80\xb3",
431
+ "\xe2\x80\xb4",
432
+ "\xe2\x80\xb5",
433
+ "\xe2\x80\xb6",
434
+ "\xe2\x80\xb7",
435
+ ]
436
+
437
+ for squote in stupid_quotes:
438
+ try:
439
+ row = row.replace(squote, "'")
440
+ except Exception:
441
+ pass
442
+
443
+ return row
444
+
445
+ def _generate_random_table_id(self):
446
+ """
447
+ Generate a random TableID for backwards compatibility.
448
+
449
+ The attribute TableID is enforced in sbtab. But to remain backwards compatible,
450
+ we generate a random TableID if none is given in the input file.
451
+ """
452
+ import random
453
+
454
+ i = random.randint(0, 1000)
455
+ table_id = "ID_%s" % str(i)
456
+
457
+ return table_id
458
+
459
+ def _get_table_information(self):
460
+ """Read declaration row and store the sbtab table attributes."""
461
+ # Save table id, otherwise raise error
462
+ try:
463
+ table_id = self._get_custom_table_information("TableID")
464
+ except Exception:
465
+ table_id = self._generate_random_table_id()
466
+ if "TableID=" not in self.header_row:
467
+ self.header_row = (
468
+ self.header_row.replace(self.delimiter, "").strip()
469
+ + " TableID='%s'" % table_id
470
+ )
471
+
472
+ # Save table type, otherwise raise error
473
+ try:
474
+ if self.table_format == "sbtab":
475
+ table_type = self._get_custom_table_information("TableType")
476
+ elif self.table_format == "ObjTables":
477
+ table_type = self._get_custom_table_information("class")
478
+ except Exception:
479
+ raise SBtabError("The TableType of the sbtab is not defined!")
480
+
481
+ # Save table name, otherwise create name
482
+ try:
483
+ table_name = self._get_custom_table_information("TableName")
484
+ except Exception:
485
+ table_name = table_type.capitalize() + "_unnamed"
486
+ self.header_row += " TableName='%s'" % table_name
487
+
488
+ # Save table document, otherwise return None
489
+ try:
490
+ table_document = self._get_custom_table_information("Document")
491
+ except Exception:
492
+ table_document = None
493
+
494
+ # save table version, otherwise return None
495
+ try:
496
+ table_version = self._get_custom_table_information("SBtabVersion")
497
+ except Exception:
498
+ table_version = None
499
+
500
+ # save table version, otherwise return None
501
+ try:
502
+ standard_concentration = self._get_custom_table_information(
503
+ "StandardConcentration"
504
+ )
505
+ except Exception:
506
+ standard_concentration = None
507
+
508
+ # save date
509
+ try:
510
+ self.date = self._get_custom_table_information("Date")
511
+ except Exception:
512
+ now = datetime.datetime.now()
513
+ self.date = "-".join([str(now.year), str(now.month), str(now.day)])
514
+ if "Date=" not in self.header_row:
515
+ self.header_row = (
516
+ self.header_row.replace(self.delimiter, "")
517
+ + " Date='%s'" % self.date
518
+ )
519
+
520
+ return (
521
+ table_id,
522
+ table_type,
523
+ table_name,
524
+ table_document,
525
+ table_version,
526
+ standard_concentration,
527
+ )
528
+
529
+ def _get_custom_table_information(self, attribute_name):
530
+ """
531
+ Retrieve the value of a table attribute in the declaration line.
532
+
533
+ Parameters
534
+ ----------
535
+ attribute_name : str
536
+ Name of the table attribute.
537
+ """
538
+ if re.search("%s='([^']*)'" % attribute_name, self.header_row) is not None:
539
+ return re.search("%s='([^']*)'" % attribute_name, self.header_row).group(1)
540
+ else:
541
+ raise SBtabError(
542
+ """The %s of the sbtab is
543
+ not defined!"""
544
+ % attribute_name
545
+ )
546
+
547
+ def _get_columns(self):
548
+ """Extract column headers, add mandatory first column name if necessary."""
549
+ # Save list of main columns
550
+ column_names = None
551
+ for row in self.table:
552
+ for _entry in row:
553
+ if str(row[0]).startswith("!") and not str(row[0]).startswith("!!"):
554
+ column_names = list(filter(lambda a: a != "", row))
555
+ break
556
+
557
+ if column_names is None:
558
+ raise SBtabError(
559
+ "No column header row (a row starting with a single '!') was found."
560
+ )
561
+
562
+ # Get column positions
563
+ columns = dict(map(reversed, enumerate(column_names)))
564
+
565
+ return column_names, columns
566
+
567
+ def _get_rows(self):
568
+ """Extract the rows of the sbtab."""
569
+ value_rows = []
570
+ # Add to comments, if row starts with '%'
571
+ self.comments = []
572
+
573
+ for row in self.table:
574
+ if str(row[0]).startswith("!"):
575
+ continue
576
+ elif str(row[0]).startswith("%"):
577
+ self.comments.append(list(row))
578
+ else:
579
+ if len(list(row)) >= len(self.columns):
580
+ value_rows.append(list(row)[: len(self.columns)])
581
+ else:
582
+ value_rows.append(
583
+ list(row) + [""] * (len(self.columns) - len(list(row)))
584
+ )
585
+
586
+ return value_rows
587
+
588
+ # Here, the sbtab API starts
589
+ def to_str(self):
590
+ """
591
+ Return the sbtab table as a string.
592
+
593
+ Returns: str
594
+ The sbtab table in form of a string.
595
+ """
596
+ table_string = [self.header_row]
597
+ table_string.append("\t".join(self.columns))
598
+ for row in self.value_rows:
599
+ row = "\t".join(row)
600
+ table_string.append(row)
601
+
602
+ return "\n".join(table_string)
603
+
604
+ def change_attribute(self, attribute, value):
605
+ """
606
+ Change the value of an sbtab attribute.
607
+
608
+ Parameters
609
+ ----------
610
+ attribute : str
611
+ Attribute from the sbtab object's declaration row.
612
+ value: str
613
+ Value that the attribute should take.
614
+ """
615
+ att_value_new = "%s='%s'" % (attribute, value)
616
+
617
+ match = re.search(_attribute_pattern(attribute), self.header_row)
618
+ if match is None:
619
+ self.header_row = self.header_row + " " + att_value_new
620
+ else:
621
+ self.header_row = (
622
+ self.header_row[: match.start()]
623
+ + att_value_new
624
+ + self.header_row[match.end() :]
625
+ )
626
+
627
+ def set_filename(self, filename):
628
+ """
629
+ Set the filename of the sbtab.
630
+
631
+ Parameters
632
+ ----------
633
+ filename: str or Path
634
+ New filename for the sbtab object.
635
+ """
636
+ self.filename = Path(filename)
637
+ # validate file extension
638
+ self._validate_extension()
639
+
640
+ def add_sbtab_string(self, sbtab_string, definitions_file=None):
641
+ """Set the content of the sbtab Table in form of a string."""
642
+ self.table_string = sbtab_string
643
+ # validate singular sbtab
644
+ self._singular()
645
+
646
+ # process string
647
+ self.delimiter = utils.check_delimiter(sbtab_string)
648
+ self.preprocess = self._preprocess_table_string(sbtab_string)
649
+ self.table = self._cut_table_string(self.preprocess)
650
+
651
+ # Initialise table
652
+ self._initialize_table()
653
+
654
+ def unset_attribute(self, attribute):
655
+ """
656
+ Remove an attribute from sbtab declaration row.
657
+
658
+ Parameters
659
+ ----------
660
+ attribute : str
661
+ Attribute that shall be removed from the sbtab object's declaration row.
662
+ """
663
+ obligatory_attributes = ["TableType", "TableID"]
664
+ if attribute in obligatory_attributes:
665
+ raise SBtabError(
666
+ "Attribute %s cannot be removed as it is obligatory." % attribute
667
+ )
668
+
669
+ match = re.search(_attribute_pattern(attribute), self.header_row)
670
+ if match is None:
671
+ raise SBtabError(
672
+ "Attribute %s is not in the header of the SBtabTable." % attribute
673
+ )
674
+ start = match.start()
675
+ if self.header_row[start - 1 : start] == " ":
676
+ start -= 1
677
+ self.header_row = self.header_row[:start] + self.header_row[match.end() :]
678
+
679
+ def get_attribute(self, attribute, default=_MISSING):
680
+ """
681
+ Return the value of an sbtab attribute.
682
+
683
+ Parameters
684
+ ----------
685
+ attribute : str
686
+ Attribute from the sbtab object's declaration row.
687
+ default : object
688
+ Returned if the attribute is absent. If no default is given, a
689
+ missing attribute raises SBtabError.
690
+
691
+ Returns: str
692
+ Value of the requested attribute.
693
+ """
694
+ match = re.search(_attribute_pattern(attribute), self.header_row)
695
+ if match is not None:
696
+ return match.group(1)
697
+ if default is not _MISSING:
698
+ return default
699
+ raise SBtabError(
700
+ "The attribute %s was not found in the header row." % attribute
701
+ )
702
+
703
+ @property
704
+ def column_names(self):
705
+ """
706
+ The column names without their leading '!', in table order.
707
+
708
+ Returns: list of str
709
+ A new list; modifying it does not change the table.
710
+ """
711
+ return [_bare_column_name(column) for column in self.columns]
712
+
713
+ def to_dict_rows(self, strip=True):
714
+ """
715
+ Return the rows as dictionaries keyed by column name (without the '!').
716
+
717
+ Every dictionary has every column as a key. The dictionaries are copies:
718
+ modifying them does not change the table; use change_value_by_name for
719
+ that.
720
+
721
+ Parameters
722
+ ----------
723
+ strip : bool
724
+ Strip leading and trailing whitespace from every value.
725
+
726
+ Returns: list of dict
727
+ One dictionary per row, e.g. {'ID': 'R1', 'ReactionFormula': 'A <=> B'}.
728
+ """
729
+ names = self.column_names
730
+ dict_rows = []
731
+ for row in self.value_rows:
732
+ values = [str(value) for value in row[: len(names)]]
733
+ values += [""] * (len(names) - len(values))
734
+ if strip:
735
+ values = [value.strip() for value in values]
736
+ dict_rows.append(dict(zip(names, values)))
737
+ return dict_rows
738
+
739
+ def change_value(self, row, column, new):
740
+ """
741
+ Change a single value in the sbtab table by position in the table.
742
+
743
+ Parameters
744
+ ----------
745
+ row : int
746
+ Number of rows in the table. First row is number 1.
747
+ column : int
748
+ Number of columns in the table. First column is number 1.
749
+ new : str
750
+ New entry.
751
+ """
752
+ try:
753
+ self.value_rows[row - 1]
754
+ except Exception:
755
+ raise SBtabError("The sbtab has only %s rows." % len(self.value_rows))
756
+ try:
757
+ self.columns[column - 1]
758
+ except Exception:
759
+ raise SBtabError("The sbtab has only %s columns." % len(self.columns))
760
+
761
+ try:
762
+ self.value_rows[row - 1][column - 1] = str(new)
763
+ except Exception:
764
+ raise SBtabError("Could not set the given value.")
765
+
766
+ return True
767
+
768
+ def change_value_by_name(self, name, column_name, new):
769
+ """
770
+ Change single value in the sbtab by column name and first row entry name.
771
+
772
+ Parameters
773
+ ----------
774
+ row : str
775
+ Name of the entry in the ID column.
776
+ column : str
777
+ Name of the column (with '!').
778
+ new : str
779
+ New entry.
780
+ """
781
+ try:
782
+ self.columns_dict[column_name]
783
+ except Exception:
784
+ raise SBtabError("The column %s is not in the sbtab." % column_name)
785
+
786
+ success = False
787
+ col = self.columns_dict[column_name]
788
+ for r in self.value_rows:
789
+ if r[0] == name:
790
+ r[col] = str(new)
791
+ success = True
792
+
793
+ if not success:
794
+ raise SBtabError("Row %s was not found in the sbtab." % name)
795
+
796
+ return True
797
+
798
+ def create_list(self):
799
+ """
800
+ Create a list object of the sbtab table object.
801
+
802
+ Returns: list
803
+ List containing of sbtab content: [declaration row, columns, value_rows]
804
+ """
805
+ # Create new list
806
+ sbtab_list = []
807
+
808
+ # Append the parts header row, main column row and
809
+ # value rows to the list
810
+ sbtab_list.append([self.header_row])
811
+ sbtab_list.append(self.columns)
812
+ sbtab_list.append(self.value_rows)
813
+
814
+ return sbtab_list
815
+
816
+ def add_row(self, row_list, position=None):
817
+ """
818
+ Add a row to the table, if position is None at the end of it.
819
+
820
+ Parameters
821
+ ----------
822
+ row_list : list
823
+ List of strings, containing the entries of the new row.
824
+ position : int
825
+ Position of new row in the table, 0 is on top.
826
+ """
827
+ if not isinstance(row_list, list):
828
+ raise SBtabError("%s is not a list" % row_list)
829
+
830
+ if len(row_list) != len(self.columns):
831
+ raise SBtabError("Given row %s has not the correct length." % row_list)
832
+
833
+ if position is not None and not isinstance(position, int):
834
+ raise SBtabError("Please provide an integer row position.")
835
+
836
+ for element in row_list:
837
+ if not isinstance(element, str):
838
+ raise SBtabError("Please only provide string elements in the list")
839
+
840
+ # If no position is set, add new row to the end
841
+ if position is None:
842
+ self.value_rows.append(row_list)
843
+ else:
844
+ self.value_rows.insert(position, row_list)
845
+
846
+ return True
847
+
848
+ def remove_row(self, position):
849
+ """
850
+ Remove one row from the table.
851
+
852
+ Parameters
853
+ ----------
854
+ position : int
855
+ Position of row to be removed. Starting with 1.
856
+ """
857
+ if not isinstance(position, int):
858
+ raise SBtabError("Please provide an integer row position.")
859
+
860
+ if position > len(self.value_rows):
861
+ raise SBtabError("The sbtab only has %s row/s." % len(self.value_rows))
862
+
863
+ del self.value_rows[position - 1]
864
+
865
+ return True
866
+
867
+ def add_column(self, column_list, position=None):
868
+ """
869
+ Add a column to the table, if position is None at the end of it.
870
+
871
+ Parameters
872
+ ----------
873
+ column_list : list
874
+ List of strings, containing the entries of the new column.
875
+ position : int
876
+ Position of new column in the table, 0 is right.
877
+ """
878
+ if not isinstance(column_list, list):
879
+ raise SBtabError("%s is not a list" % column_list)
880
+
881
+ if len(column_list) != (len(self.value_rows) + 1):
882
+ raise SBtabError("The given column list has not the correct length.")
883
+
884
+ if position is not None and not isinstance(position, int):
885
+ raise SBtabError("Please provide an integer column position.")
886
+
887
+ # If no position is set, add new column to the end
888
+ if not position:
889
+ for i, row in enumerate(self.value_rows):
890
+ row.append(str(column_list[i + 1]))
891
+ self.columns_dict[str(column_list[0])] = len(self.columns)
892
+ self.columns.append(str(column_list[0]))
893
+ else:
894
+ for i, row in enumerate(self.value_rows):
895
+ row.insert(position - 1, str(column_list[i + 1]))
896
+ self.columns_dict[str(column_list[0])] = position - 1
897
+ self.columns.insert(position - 1, str(column_list[0]))
898
+
899
+ return True
900
+
901
+ def remove_column(self, position):
902
+ """
903
+ Remove column from the table.
904
+
905
+ Parameters
906
+ ----------
907
+ position : int
908
+ Position of column to be removed. Sarting with 1.
909
+ """
910
+ if not isinstance(position, int):
911
+ raise SBtabError("Please provide an integer column position.")
912
+
913
+ if position > len(self.columns):
914
+ raise SBtabError(
915
+ "There are only %s columns in the sbtab" % str(len(self.columns))
916
+ )
917
+
918
+ # Remove entries on position
919
+ for row in self.value_rows:
920
+ del row[position - 1]
921
+
922
+ # Remove column from column list
923
+ column_to_remove = self.columns[position - 1]
924
+ del self.columns[position - 1]
925
+
926
+ # Remove column from columns dict
927
+ self.columns_dict.pop(column_to_remove)
928
+
929
+ return True
930
+
931
+ def write(self, filename):
932
+ """
933
+ Write sbtab to hard disk.
934
+
935
+ Parameters
936
+ ----------
937
+ filename: str or Path
938
+ Name for the output file.
939
+ """
940
+ path = Path(filename)
941
+ if path.suffix not in {".tsv", ".csv"}:
942
+ if self.delimiter == "\t":
943
+ path = path.with_suffix(".tsv")
944
+ elif self.delimiter in {",", ";"}:
945
+ path = path.with_suffix(".csv")
946
+ else:
947
+ raise SBtabError(
948
+ "The file extension is missing and the delimiter is no standard."
949
+ )
950
+
951
+ try:
952
+ table_string = self.to_str().replace("^M", "\n")
953
+ path.write_text(table_string)
954
+ return True
955
+ except Exception:
956
+ raise SBtabError("The file could not be written.")
957
+
958
+ def transpose_table(self):
959
+ """
960
+ Transpose sbtab table. Switch columns and rows.
961
+
962
+ Returns: sbtab.SBtabTable
963
+ sbtab object with transposed content.
964
+ """
965
+ # Initialize new table data
966
+ trans_columns = []
967
+ trans_columns_dict = {}
968
+ trans_value_rows = []
969
+
970
+ # Save old table data
971
+ columns = self.columns
972
+ value_rows = self.value_rows
973
+
974
+ # Append first entry to new column
975
+ trans_columns.append(columns.pop(0))
976
+
977
+ # Set new rows
978
+ for column in columns:
979
+ trans_value_rows.append([column])
980
+
981
+ # Set new values in tables
982
+ for row in value_rows:
983
+ trans_columns.append(row.pop(0))
984
+ for i, entry in enumerate(row):
985
+ trans_value_rows[i].append(entry)
986
+
987
+ # Write new columns dict
988
+ for i, column in enumerate(trans_columns):
989
+ trans_columns_dict[column] = i
990
+
991
+ # Overwrite old table data
992
+ self.columns = trans_columns
993
+ self.columns_dict = trans_columns_dict
994
+ self.value_rows = trans_value_rows
995
+
996
+ return True
997
+
998
+ def to_data_frame(self):
999
+ """
1000
+ Export sbtab table object as pandas dataframe.
1001
+
1002
+ Returns: pandas.DataFrame
1003
+ sbtab table object as pandas dataframe.
1004
+ """
1005
+ pd = utils.require_optional("pandas", "pandas")
1006
+ try:
1007
+ # value_rows, unlike the raw parsed grid, exists on every table,
1008
+ # including those built by from_data_frame and from_rows
1009
+ rows = [list(row) for row in self.value_rows]
1010
+ column_names = self.column_names
1011
+ n_cols = max(map(len, rows), default=len(column_names))
1012
+ while len(column_names) < n_cols:
1013
+ column_names += ["Col%d" % len(column_names)]
1014
+ df = pd.DataFrame(data=rows, columns=column_names)
1015
+ return df
1016
+ except Exception:
1017
+ raise SBtabError("Pandas dataframe could not be built.")
1018
+
1019
+ @staticmethod
1020
+ def from_data_frame(
1021
+ df,
1022
+ table_id,
1023
+ table_type,
1024
+ table_name=None,
1025
+ document_name=None,
1026
+ document=None,
1027
+ unit=None,
1028
+ sbtab_version="1.0",
1029
+ ) -> "SBtabTable":
1030
+ """
1031
+ Create sbtab table object from pandas dataframe.
1032
+
1033
+ Column names are expected to start with '!' (e.g. '!QuantityType');
1034
+ the '!' is added where it is missing. Values are converted to str.
1035
+
1036
+ Parameters
1037
+ ----------
1038
+ df : pandas.DataFrame
1039
+ Dataframe of the Python library pandas.
1040
+ table_id: str
1041
+ Mandatory table ID for the sbtab object.
1042
+ table_type: str
1043
+ Mandatory table type for the sbtab object.
1044
+ table_name: str
1045
+ Optional table name for the sbtab object.
1046
+ document_name: str
1047
+ Optional document name for the sbtab object.
1048
+ document: str
1049
+ Optional document for the sbtab object.
1050
+ unit: str
1051
+ Optional unit for an sbtab TableType Quantity.
1052
+ sbtab_version: str
1053
+ Optional sbtab Version.
1054
+
1055
+ Returns: sbtab.SBtabTable
1056
+ sbtab table object created from pandas dataframe.
1057
+ """
1058
+ return SBtabTable.from_rows(
1059
+ [str(column) for column in df.columns],
1060
+ df.values.tolist(),
1061
+ table_id=table_id,
1062
+ table_type=table_type,
1063
+ table_name=table_name,
1064
+ document_name=document_name,
1065
+ document=document,
1066
+ unit=unit,
1067
+ sbtab_version=sbtab_version,
1068
+ )
1069
+
1070
+ @classmethod
1071
+ def from_rows(
1072
+ cls,
1073
+ columns,
1074
+ rows,
1075
+ table_id,
1076
+ table_type,
1077
+ table_name=None,
1078
+ document_name=None,
1079
+ document=None,
1080
+ unit=None,
1081
+ sbtab_version="1.0",
1082
+ **attributes,
1083
+ ) -> "SBtabTable":
1084
+ """
1085
+ Create sbtab table object from column names and rows, without pandas.
1086
+
1087
+ The table is built directly, not parsed from text, so unlike a parsed
1088
+ table no Date attribute is added to its declaration row.
1089
+
1090
+ Parameters
1091
+ ----------
1092
+ columns : list of str
1093
+ Column names, with or without the leading '!' (added if missing).
1094
+ rows : list
1095
+ Each row is either a list of values, used by position (padded with
1096
+ '' or truncated to the number of columns), or a dict keyed by
1097
+ column name without the '!' (missing columns become '', unknown
1098
+ keys raise SBtabError). All values are converted to str.
1099
+ table_id: str
1100
+ Mandatory table ID for the sbtab object.
1101
+ table_type: str
1102
+ Mandatory table type for the sbtab object.
1103
+ table_name: str
1104
+ Optional table name for the sbtab object (defaults to table_id).
1105
+ document_name: str
1106
+ Optional document name for the sbtab object.
1107
+ document: str
1108
+ Optional document for the sbtab object.
1109
+ unit: str
1110
+ Optional unit for an sbtab TableType Quantity.
1111
+ sbtab_version: str
1112
+ Optional sbtab Version.
1113
+ **attributes: str
1114
+ Further declaration row attributes, in the order given,
1115
+ e.g. StandardConcentration='M'.
1116
+
1117
+ Returns: sbtab.SBtabTable
1118
+ sbtab table object created from the rows.
1119
+ """
1120
+ column_list = [
1121
+ column if column.startswith("!") else "!" + column for column in columns
1122
+ ]
1123
+ bare_names = [_bare_column_name(column) for column in column_list]
1124
+ n_cols = len(column_list)
1125
+
1126
+ value_rows = []
1127
+ for row in rows:
1128
+ if isinstance(row, Mapping):
1129
+ unknown = [key for key in row if key not in bare_names]
1130
+ if unknown:
1131
+ raise SBtabError(
1132
+ "Unknown column(s) %s; the columns are %s."
1133
+ % (", ".join(map(str, unknown)), ", ".join(bare_names))
1134
+ )
1135
+ values = [row.get(name, "") for name in bare_names]
1136
+ else:
1137
+ values = list(row)[:n_cols]
1138
+ values += [""] * (n_cols - len(values))
1139
+ value_rows.append([str(value) for value in values])
1140
+
1141
+ sbtab = cls()
1142
+
1143
+ sbtab.table_id = table_id
1144
+ sbtab.table_type = table_type
1145
+ sbtab.table_name = table_name or table_id
1146
+ sbtab.table_document = document
1147
+ sbtab.table_version = sbtab_version
1148
+
1149
+ header = [
1150
+ ("TableID", sbtab.table_id),
1151
+ ("TableType", sbtab.table_type),
1152
+ ("TableName", sbtab.table_name),
1153
+ ("SBtabVersion", sbtab.table_version),
1154
+ ]
1155
+ if document_name:
1156
+ header += [("DocumentName", document_name)]
1157
+ if document:
1158
+ header += [("Document", sbtab.table_document)]
1159
+ if unit:
1160
+ header += [("Unit", unit)]
1161
+ header += list(attributes.items())
1162
+ header_strings = ["!!SBtab"] + list(map(lambda x: "%s='%s'" % x, header))
1163
+
1164
+ sbtab.doc_row = None
1165
+ sbtab.header_row = " ".join(header_strings)
1166
+
1167
+ sbtab.columns = column_list
1168
+ sbtab.columns_dict = dict(map(reversed, enumerate(column_list)))
1169
+ sbtab.value_rows = value_rows
1170
+
1171
+ return sbtab
1172
+
1173
+
1174
+ class SBtabDocument:
1175
+ """The sbtab document class can consist of one or more sbtab Table objects."""
1176
+
1177
+ def __init__(
1178
+ self, name=None, sbtab_init=None, filename=None, definitions_file=None
1179
+ ):
1180
+ """
1181
+ Create SBtabDocument with an optional sbtab table object.
1182
+
1183
+ Parameters
1184
+ ----------
1185
+ name: str
1186
+ Name for the sbtab document.
1187
+ sbtab_init: str | sbtab.SBtabTable
1188
+ Initial sbtab table as either string or sbtab table object
1189
+ filename: str
1190
+ If sbtab_init is string, also provide a fiename.
1191
+ """
1192
+ if filename:
1193
+ self.filename = filename
1194
+ else:
1195
+ self.filename = None
1196
+ self.sbtabs = []
1197
+ self.id_to_sbtab = {}
1198
+ self.name_to_sbtab = {}
1199
+ self.type_to_sbtab = {}
1200
+ self.sbtab_filenames = []
1201
+ self.doc_row = False
1202
+ self.document_format = None
1203
+
1204
+ if name:
1205
+ self.name = name
1206
+ self._get_doc_row_attributes()
1207
+ else:
1208
+ self.name = None
1209
+
1210
+ # if there is an initial sbtab given, see if it is
1211
+ # a string or an sbtab
1212
+ if sbtab_init and isinstance(sbtab_init, str):
1213
+ self.add_sbtab_string(sbtab_init, filename, definitions_file)
1214
+ elif sbtab_init:
1215
+ self.add_sbtab(sbtab_init, definitions_file)
1216
+
1217
+ self.object_type = "doc"
1218
+
1219
+ def add_sbtab(self, sbtab, definitions_file=None):
1220
+ """
1221
+ Add an sbtab Table object to the sbtab Document.
1222
+
1223
+ Parameters
1224
+ ----------
1225
+ sbtab: sbtab.SBtabTable
1226
+ sbtab table object to be added to the document.
1227
+ """
1228
+ if not self.filename:
1229
+ self.filename = sbtab.filename
1230
+
1231
+ if sbtab.table_id in self.id_to_sbtab.keys():
1232
+ raise SBtabError(
1233
+ "A table with the ID %s is already in the document. Table IDs need"
1234
+ " to be unique within one document." % sbtab.table_id
1235
+ )
1236
+
1237
+ valid_type = self.check_type_validity(sbtab.table_type, definitions_file)
1238
+ if valid_type:
1239
+ self.name_to_sbtab[sbtab.table_name] = sbtab
1240
+ self.id_to_sbtab[sbtab.table_id] = sbtab
1241
+ self.sbtabs.append(sbtab)
1242
+ self.sbtab_filenames.append(sbtab.filename)
1243
+ if sbtab.table_type in self.type_to_sbtab:
1244
+ tabs = self.type_to_sbtab[sbtab.table_type]
1245
+ tabs.append(sbtab)
1246
+ self.type_to_sbtab[sbtab.table_type] = tabs
1247
+ else:
1248
+ self.type_to_sbtab[sbtab.table_type] = [sbtab]
1249
+
1250
+ self._get_doc_row_attributes()
1251
+ return True
1252
+
1253
+ def add_sbtab_string(self, sbtab_string, filename, definitions_file=None):
1254
+ """
1255
+ Add one or multiple sbtab files as a string.
1256
+
1257
+ Parameters
1258
+ ----------
1259
+ sbtab_string: str
1260
+ One string holding one or more sbtab tables.
1261
+ filename: str
1262
+ Name of the given sbtab.
1263
+ """
1264
+ # set filename if not given
1265
+ if not filename:
1266
+ sbtab_count = len(self.sbtabs)
1267
+ filename = "unnamed_sbtab_%s.tsv" % (str(sbtab_count))
1268
+
1269
+ if not self.filename:
1270
+ self.filename = filename
1271
+ if not self.name:
1272
+ self.name = filename
1273
+
1274
+ # see if there are more than one SBtabs in the string
1275
+ try:
1276
+ sbtab_amount = utils.count_tabs(sbtab_string)
1277
+ except Exception:
1278
+ raise SBtabError("The sbtab file could not be read properly.")
1279
+
1280
+ # here, we find a possible doc row
1281
+ for row in sbtab_string.split("\n"):
1282
+ if row.startswith("!!!"):
1283
+ self.doc_row = self._dequote(row)
1284
+ break
1285
+ elif row.startswith('"!!!'):
1286
+ rm1 = row.replace('""', "#")
1287
+ rm2 = rm1.replace('"', "")
1288
+ self.doc_row = self._dequote(rm2.replace("#", '"'))
1289
+ break
1290
+ else:
1291
+ self.doc_row = None
1292
+ self.document_format = None
1293
+
1294
+ # determine if this is an sbtab or ObjTables Document
1295
+ if self.doc_row:
1296
+ if "!!!ObjTables" in self.doc_row:
1297
+ self.document_format = "ObjTables"
1298
+ elif "!!!SBtab" in self.doc_row:
1299
+ self.document_format = "sbtab"
1300
+
1301
+ # if there are more than one SBtabs, cut them in single SBtabs
1302
+ try:
1303
+ if sbtab_amount > 1:
1304
+ sbtab_strings = utils.split_sbtabs(sbtab_string)
1305
+ for i, sbtab_s in enumerate(sbtab_strings):
1306
+ name_single = str(i) + "_" + str(self.filename)
1307
+ sbtab_single = SBtabTable(sbtab_s, name_single)
1308
+ logging.debug(
1309
+ "name = %s, type = %s"
1310
+ % (sbtab_single.table_name, sbtab_single.table_type)
1311
+ )
1312
+ self.add_sbtab(sbtab_single, definitions_file)
1313
+ else:
1314
+ sbtab = SBtabTable(sbtab_string, filename)
1315
+ self.add_sbtab(sbtab)
1316
+ except Exception as e:
1317
+ raise SBtabError(
1318
+ "The sbtab Table object could not be created properly: " + str(e)
1319
+ )
1320
+ return True
1321
+
1322
+ def _dequote(self, row):
1323
+ """Bring consistency in the multifarious quotation mark problems."""
1324
+ stupid_quotes = [
1325
+ '"',
1326
+ "\xe2\x80\x9d",
1327
+ "\xe2\x80\x98",
1328
+ "\xe2\x80\x99",
1329
+ "\xe2\x80\x9b",
1330
+ "\xe2\x80\x9c",
1331
+ "\xe2\x80\x9f",
1332
+ "\xe2\x80\xb2",
1333
+ "\xe2\x80\xb3",
1334
+ "\xe2\x80\xb4",
1335
+ "\xe2\x80\xb5",
1336
+ "\xe2\x80\xb6",
1337
+ "\xe2\x80\xb7",
1338
+ ]
1339
+
1340
+ for squote in stupid_quotes:
1341
+ try:
1342
+ row = row.replace(squote, "'")
1343
+ except Exception:
1344
+ pass
1345
+
1346
+ row = row.replace("\n", "")
1347
+
1348
+ return row
1349
+
1350
+ def check_type_validity(self, ttype, definitions_file=None):
1351
+ """
1352
+ Check if the given table type is supported by default.
1353
+
1354
+ Parameters
1355
+ ----------
1356
+ ttype: str
1357
+ Table type to be tested.
1358
+
1359
+ Returns: Bool
1360
+ Flag that indicates if the given table type is supported by default.
1361
+
1362
+ """
1363
+ try:
1364
+ supported_types = utils.extract_supported_table_types(definitions_file)
1365
+ except Exception:
1366
+ raise SBtabError(
1367
+ "The definition file could not be found to"
1368
+ " establish supported table types."
1369
+ )
1370
+
1371
+ if ttype in supported_types:
1372
+ return True
1373
+ else:
1374
+ raise SBtabError(
1375
+ "The table type %s is not supported. Make sure to provide a"
1376
+ " definition file that contains this table type." % ttype
1377
+ )
1378
+
1379
+ def _get_doc_row_attributes(self):
1380
+ """Read content of the !!!-document declaration row."""
1381
+ now = datetime.datetime.now()
1382
+ self.date = "-".join([str(now.year), str(now.month), str(now.day)])
1383
+
1384
+ if not self.doc_row:
1385
+ self.doc_row = "!!!SBtab DocumentName='%s' SBtabVersion='1.0' Date='%s'" % (
1386
+ self.name,
1387
+ self.date,
1388
+ )
1389
+ else:
1390
+ # save document name, otherwise raise error
1391
+ # (overrides name given at document initialisation)
1392
+ try:
1393
+ self.name = self.get_custom_doc_information("DocumentName")
1394
+ except Exception:
1395
+ pass
1396
+
1397
+ # save SBtabVersion
1398
+ try:
1399
+ self.version = self.get_custom_doc_information("SBtabVersion")
1400
+ except Exception:
1401
+ self.version = None
1402
+
1403
+ # save date
1404
+ try:
1405
+ self.date = self.get_custom_doc_information("Date")
1406
+ except Exception:
1407
+ if "Date=" not in self.doc_row:
1408
+ self.doc_row = self.doc_row + " Date='%s'" % self.date
1409
+
1410
+ # save document type
1411
+ try:
1412
+ self.doc_type = self.get_custom_doc_information("DocumentType")
1413
+ except Exception:
1414
+ self.doc_type = None
1415
+
1416
+ def change_attribute(self, attribute, value):
1417
+ """
1418
+ Change the value of an sbtab attribute.
1419
+
1420
+ Parameters
1421
+ ----------
1422
+ attribute: str
1423
+ Attribute from the sbtab document object's declaration row.
1424
+ value: str
1425
+ Value for the attribute.
1426
+ """
1427
+ try:
1428
+ att_value_new = "%s='%s'" % (attribute, value)
1429
+ except Exception:
1430
+ raise SBtabError("Please provide only strings as attribute and value.")
1431
+
1432
+ match = re.search(_attribute_pattern(attribute), self.doc_row)
1433
+ if match is None:
1434
+ self.doc_row = self.doc_row + " " + att_value_new
1435
+ else:
1436
+ self.doc_row = (
1437
+ self.doc_row[: match.start()]
1438
+ + att_value_new
1439
+ + self.doc_row[match.end() :]
1440
+ )
1441
+
1442
+ def unset_attribute(self, attribute):
1443
+ """
1444
+ Remove attribute from sbtab document object's declaration row.
1445
+
1446
+ Parameters
1447
+ ----------
1448
+ attribute: str
1449
+ Attribute that shall be removed.
1450
+ """
1451
+ obligatory_attributes = ["DocumentName"]
1452
+ if attribute in obligatory_attributes:
1453
+ raise SBtabError(
1454
+ "Attribute %s cannot be removed as it is obligatory." % attribute
1455
+ )
1456
+
1457
+ match = re.search(_attribute_pattern(attribute), self.doc_row)
1458
+ if match is None:
1459
+ raise SBtabError(
1460
+ "Attribute %s is not in the doc row of the SBtabDocument." % attribute
1461
+ )
1462
+ start = match.start()
1463
+ if self.doc_row[start - 1 : start] == " ":
1464
+ start -= 1
1465
+ self.doc_row = self.doc_row[:start] + self.doc_row[match.end() :]
1466
+
1467
+ def get_attribute(self, attribute, default=_MISSING):
1468
+ """
1469
+ Return the value of an sbtab attribute.
1470
+
1471
+ Parameters
1472
+ ----------
1473
+ attribute: str
1474
+ Attribute from the sbtab document object's declaration row.
1475
+ default : object
1476
+ Returned if the attribute is absent. If no default is given, a
1477
+ missing attribute raises SBtabError.
1478
+
1479
+ Returns: str
1480
+ Value of the requested attribute.
1481
+ """
1482
+ match = re.search(_attribute_pattern(attribute), self.doc_row or "")
1483
+ if match is not None:
1484
+ return match.group(1)
1485
+ if default is not _MISSING:
1486
+ return default
1487
+ raise SBtabError("The attribute %s was not found in the doc row." % attribute)
1488
+
1489
+ def set_version(self, version):
1490
+ """
1491
+ Set SBtabVersion of the document.
1492
+
1493
+ Parameters
1494
+ ----------
1495
+ version: str
1496
+ Version number of sbtab document.
1497
+ """
1498
+ try:
1499
+ self.version = version
1500
+ except Exception:
1501
+ raise SBtabError("Version could not be set to %s" % version)
1502
+
1503
+ def set_date(self, date):
1504
+ """
1505
+ Set date of the document.
1506
+
1507
+ Parameters
1508
+ ----------
1509
+ date: str
1510
+ Date of the sbtab document.
1511
+ """
1512
+ try:
1513
+ self.date = date
1514
+ except Exception:
1515
+ raise SBtabError("Date could not be set to %s" % date)
1516
+
1517
+ def set_doc_type(self, doc_type):
1518
+ """
1519
+ Set the type of the document.
1520
+
1521
+ Parameters
1522
+ ----------
1523
+ doc_type: str
1524
+ Type of the sbtab document.
1525
+ """
1526
+ try:
1527
+ self.doc_type = doc_type
1528
+ except Exception:
1529
+ raise SBtabError("Doc type could not be set to %s" % doc_type)
1530
+
1531
+ def remove_sbtab_by_name(self, name):
1532
+ """
1533
+ Remove sbtab Table from sbtab Document.
1534
+
1535
+ Parameters
1536
+ ----------
1537
+ name: str
1538
+ Name of the sbtab table to be removed.
1539
+ """
1540
+ for i, sbtab in enumerate(self.sbtabs):
1541
+ if sbtab.table_name == name:
1542
+ del self.sbtabs[i]
1543
+ del self.name_to_sbtab[sbtab.table_name]
1544
+ del self.id_to_sbtab[sbtab.table_id]
1545
+ del self.sbtab_filenames[i]
1546
+ tabs = self.type_to_sbtab[sbtab.table_type]
1547
+ for tab in tabs:
1548
+ if tab.table_name == sbtab.table_name:
1549
+ tabs.remove(tab)
1550
+ break
1551
+
1552
+ self.type_to_sbtab[sbtab.table_type] = tabs
1553
+ break
1554
+
1555
+ return True
1556
+
1557
+ def get_sbtab_by_name(self, name):
1558
+ """
1559
+ Return sbtab table object by given name.
1560
+
1561
+ Parameters
1562
+ ----------
1563
+ name: str
1564
+ Name of sbtab table to be returned.
1565
+
1566
+ Returns: sbtab.SBtabTable
1567
+ sbtab table object of requested name.
1568
+ """
1569
+ try:
1570
+ return self.name_to_sbtab[name]
1571
+ except Exception:
1572
+ return None
1573
+
1574
+ def get_sbtab_by_id(self, name):
1575
+ """
1576
+ Return sbtab table object by given ID.
1577
+
1578
+ Parameters
1579
+ ----------
1580
+ name: str
1581
+ TableID of a sbtab table object.
1582
+
1583
+ Returns: sbtab.SBtabTable
1584
+ sbtab table object of requested ID.
1585
+ """
1586
+ try:
1587
+ return self.id_to_sbtab[name]
1588
+ except Exception:
1589
+ return None
1590
+
1591
+ def get_sbtab_by_type(self, ttype):
1592
+ """
1593
+ Return list of sbtab objects by given table type.
1594
+
1595
+ Parameters
1596
+ ----------
1597
+ ttype: str
1598
+ Supported sbtab table type.
1599
+
1600
+ Returns: list
1601
+ List of sbtab.SBtabTable objects of the requested table type.
1602
+ """
1603
+ try:
1604
+ return self.type_to_sbtab[ttype]
1605
+ except Exception:
1606
+ return None
1607
+
1608
+ def set_filename(self, filename):
1609
+ """
1610
+ Set filename of sbtab document.
1611
+
1612
+ Parameters
1613
+ ----------
1614
+ filename: str or Path
1615
+ Filename for the sbtab document.
1616
+ """
1617
+ self.filename = Path(filename)
1618
+
1619
+ def set_name(self, name):
1620
+ """
1621
+ Set name of sbtab document.
1622
+
1623
+ Parameters
1624
+ ----------
1625
+ name: str
1626
+ Name for the sbtab document.
1627
+ """
1628
+ self.name = name
1629
+
1630
+ def write(self, filename=None):
1631
+ """
1632
+ Write SBtabDocument to hard disk.
1633
+
1634
+ Parameters
1635
+ ----------
1636
+ filename: str or Path
1637
+ Name for the output file.
1638
+ """
1639
+ path = Path(filename if filename is not None else self.filename)
1640
+ if path.suffix not in {".tsv", ".csv"}:
1641
+ delimiter = self.sbtabs[0].delimiter
1642
+ if delimiter == "\t":
1643
+ path = path.with_suffix(".tsv")
1644
+ elif delimiter in {",", ";"}:
1645
+ path = path.with_suffix(".csv")
1646
+ else:
1647
+ raise SBtabError(
1648
+ "The file extension is missing and the delimiter is not set."
1649
+ )
1650
+
1651
+ try:
1652
+ path.write_text(self.to_str())
1653
+ return True
1654
+ except Exception:
1655
+ raise SBtabError("The file could not be written.")
1656
+
1657
+ def to_str(self):
1658
+ """
1659
+ Return sbtab Document as one string.
1660
+
1661
+ Returns: str
1662
+ sbtab.SBtabDocument as string representation.
1663
+ """
1664
+ sbtab_document = self.doc_row + "\r\n"
1665
+ for sbtab in self.sbtabs:
1666
+ sbtab_document += sbtab.to_str() + "\n\n"
1667
+
1668
+ return sbtab_document
1669
+
1670
+ def get_custom_doc_information(self, attribute_name, test_row=None):
1671
+ """
1672
+ Retrieve the value of a doc attribute in the sbtab document declaration row.
1673
+
1674
+ Parameters
1675
+ ----------
1676
+ attribute_name: str
1677
+ Name of the requested attribute.
1678
+
1679
+ Returns: str
1680
+ Value of the requested attribute.
1681
+ """
1682
+ if test_row:
1683
+ doc_row = test_row
1684
+ else:
1685
+ doc_row = self.doc_row
1686
+
1687
+ if re.search("%s='([^']*)'" % attribute_name, doc_row) is not None:
1688
+ return re.search("%s='([^']*)'" % attribute_name, doc_row).group(1)
1689
+ else:
1690
+ raise SBtabError(
1691
+ """The %s of the Document is
1692
+ not defined!"""
1693
+ % attribute_name
1694
+ )
1695
+
1696
+ def set_doc_row(self, new_doc_row):
1697
+ """
1698
+ Set a new sbtab document declaration row.
1699
+
1700
+ Parameters
1701
+ ----------
1702
+ new_doc_row: str
1703
+ New sbtab document declaration row.
1704
+ """
1705
+ if not new_doc_row.startswith("!!!SBtab") and not new_doc_row.startswith(
1706
+ "!!!ObjTables"
1707
+ ):
1708
+ raise SBtabError('A doc row needs to be preceded with "!!!SBtab".')
1709
+
1710
+ if "DocumentName=" not in new_doc_row:
1711
+ raise SBtabError("A doc row needs to define the DocumentName attribute.")
1712
+
1713
+ self.doc_row = new_doc_row
1714
+ self._get_doc_row_attributes()