biocypher 0.5.17__py3-none-any.whl → 0.5.20__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.

Potentially problematic release.


This version of biocypher might be problematic. Click here for more details.

biocypher/_misc.py CHANGED
@@ -13,7 +13,7 @@ Handy functions for use in various places.
13
13
  """
14
14
  from ._logger import logger
15
15
 
16
- logger.debug(f'Loading module {__name__}.')
16
+ logger.debug(f"Loading module {__name__}.")
17
17
 
18
18
  from typing import (
19
19
  Any,
@@ -31,7 +31,7 @@ from treelib import Tree
31
31
  import networkx as nx
32
32
  import stringcase
33
33
 
34
- __all__ = ['LIST_LIKE', 'SIMPLE_TYPES', 'ensure_iterable', 'to_list']
34
+ __all__ = ["LIST_LIKE", "SIMPLE_TYPES", "ensure_iterable", "to_list"]
35
35
 
36
36
  SIMPLE_TYPES = (
37
37
  bytes,
@@ -60,11 +60,9 @@ def to_list(value: Any) -> list:
60
60
  """
61
61
 
62
62
  if isinstance(value, LIST_LIKE):
63
-
64
63
  value = list(value)
65
64
 
66
65
  else:
67
-
68
66
  value = [value]
69
67
 
70
68
  return value
@@ -75,7 +73,7 @@ def ensure_iterable(value: Any) -> Iterable:
75
73
  Returns iterables, except strings, wraps simple types into tuple.
76
74
  """
77
75
 
78
- return value if isinstance(value, LIST_LIKE) else (value, )
76
+ return value if isinstance(value, LIST_LIKE) else (value,)
79
77
 
80
78
 
81
79
  def create_tree_visualisation(inheritance_tree: Union[dict, nx.Graph]) -> str:
@@ -84,7 +82,6 @@ def create_tree_visualisation(inheritance_tree: Union[dict, nx.Graph]) -> str:
84
82
  """
85
83
 
86
84
  if isinstance(inheritance_tree, nx.Graph):
87
-
88
85
  inheritance_tree = nx.to_dict_of_lists(inheritance_tree)
89
86
  # unlist values
90
87
  inheritance_tree = {k: v[0] for k, v in inheritance_tree.items() if v}
@@ -95,56 +92,48 @@ def create_tree_visualisation(inheritance_tree: Union[dict, nx.Graph]) -> str:
95
92
  root = list(parents - classes)
96
93
 
97
94
  if len(root) > 1:
98
-
99
- if 'entity' in root:
100
-
101
- root = 'entity' # default: good standard? TODO
95
+ if "entity" in root:
96
+ root = "entity" # default: good standard? TODO
102
97
 
103
98
  else:
104
-
105
99
  raise ValueError(
106
- 'Inheritance tree cannot have more than one root node. '
107
- f'Found {len(root)}: {root}.'
100
+ "Inheritance tree cannot have more than one root node. "
101
+ f"Found {len(root)}: {root}."
108
102
  )
109
103
 
110
104
  else:
111
-
112
105
  root = root[0]
113
106
 
114
107
  if not root:
115
108
  # find key whose value is None
116
- root = list(inheritance_tree.keys())[list(inheritance_tree.values()
117
- ).index(None)]
109
+ root = list(inheritance_tree.keys())[
110
+ list(inheritance_tree.values()).index(None)
111
+ ]
118
112
 
119
113
  tree = Tree()
120
114
 
121
115
  tree.create_node(root, root)
122
116
 
123
117
  while classes:
124
-
125
118
  for child in classes:
126
-
127
119
  parent = inheritance_tree[child]
128
120
 
129
121
  if parent in tree.nodes.keys() or parent == root:
130
-
131
122
  tree.create_node(child, child, parent=parent)
132
123
 
133
124
  for node in tree.nodes.keys():
134
-
135
125
  if node in classes:
136
-
137
126
  classes.remove(node)
138
127
 
139
128
  return tree
140
129
 
141
130
 
142
131
  # string conversion, adapted from Biolink Model Toolkit
143
- lowercase_pattern = re.compile(r'[a-zA-Z]*[a-z][a-zA-Z]*')
144
- underscore_pattern = re.compile(r'(?<!^)(?=[A-Z][a-z])')
132
+ lowercase_pattern = re.compile(r"[a-zA-Z]*[a-z][a-zA-Z]*")
133
+ underscore_pattern = re.compile(r"(?<!^)(?=[A-Z][a-z])")
145
134
 
146
135
 
147
- def from_pascal(s: str, sep: str = ' ') -> str:
136
+ def from_pascal(s: str, sep: str = " ") -> str:
148
137
  underscored = underscore_pattern.sub(sep, s)
149
138
  lowercased = lowercase_pattern.sub(
150
139
  lambda match: match.group(0).lower(),
@@ -163,7 +152,7 @@ def pascalcase_to_sentencecase(s: str) -> str:
163
152
  Returns:
164
153
  string in sentence case form
165
154
  """
166
- return from_pascal(s, sep=' ')
155
+ return from_pascal(s, sep=" ")
167
156
 
168
157
 
169
158
  def snakecase_to_sentencecase(s: str) -> str:
@@ -202,7 +191,7 @@ def sentencecase_to_pascalcase(s: str) -> str:
202
191
  Returns:
203
192
  string in PascalCase form
204
193
  """
205
- return re.sub(r'(?:^| )([a-zA-Z])', lambda match: match.group(1).upper(), s)
194
+ return re.sub(r"(?:^| )([a-zA-Z])", lambda match: match.group(1).upper(), s)
206
195
 
207
196
 
208
197
  def to_lower_sentence_case(s: str) -> str:
@@ -216,9 +205,9 @@ def to_lower_sentence_case(s: str) -> str:
216
205
  Returns:
217
206
  string in lower sentence case form
218
207
  """
219
- if '_' in s:
208
+ if "_" in s:
220
209
  return snakecase_to_sentencecase(s)
221
- elif ' ' in s:
210
+ elif " " in s:
222
211
  return s.lower()
223
212
  elif s[0].isupper():
224
213
  return pascalcase_to_sentencecase(s)
biocypher/_ontology.py CHANGED
@@ -17,7 +17,7 @@ import os
17
17
 
18
18
  from ._logger import logger
19
19
 
20
- logger.debug(f'Loading module {__name__}.')
20
+ logger.debug(f"Loading module {__name__}.")
21
21
 
22
22
  from typing import Optional
23
23
  from datetime import datetime
@@ -40,6 +40,7 @@ class OntologyAdapter:
40
40
  labels are formatted in lower sentence case. In some cases, this means that
41
41
  we replace underscores with spaces.
42
42
  """
43
+
43
44
  def __init__(
44
45
  self,
45
46
  ontology_file: str,
@@ -63,7 +64,7 @@ class OntologyAdapter:
63
64
  node in the head ontology that should be used to join to the
64
65
  root node of the tail ontology. Defaults to None.
65
66
 
66
- merge_nodes (bool): If True, head and tail join nodes will be
67
+ merge_nodes (bool): If True, head and tail join nodes will be
67
68
  merged, using the label of the head join node. If False, the
68
69
  tail join node will be attached as a child of the head join
69
70
  node.
@@ -76,7 +77,7 @@ class OntologyAdapter:
76
77
  be removed. Defaults to True.
77
78
  """
78
79
 
79
- logger.info(f'Instantiating OntologyAdapter class for {ontology_file}.')
80
+ logger.info(f"Instantiating OntologyAdapter class for {ontology_file}.")
80
81
 
81
82
  self._ontology_file = ontology_file
82
83
  self._root_label = root_label
@@ -93,7 +94,6 @@ class OntologyAdapter:
93
94
  )
94
95
 
95
96
  def _rdf_to_nx(self, g, root_label, switch_id_and_label=True):
96
-
97
97
  # Loop through all labels in the ontology
98
98
  for s, _, o in g.triples((None, rdflib.RDFS.label, None)):
99
99
  # If the label is the root label, set the root node to the subject of the label
@@ -102,7 +102,7 @@ class OntologyAdapter:
102
102
  break
103
103
  else:
104
104
  raise ValueError(
105
- f'Could not find root node with label {root_label}'
105
+ f"Could not find root node with label {root_label}"
106
106
  )
107
107
 
108
108
  # Create a directed graph to represent the ontology as a tree
@@ -110,7 +110,6 @@ class OntologyAdapter:
110
110
 
111
111
  # Define a recursive function to add subclasses to the graph
112
112
  def add_subclasses(node):
113
-
114
113
  # Only add nodes that have a label
115
114
  if (node, rdflib.RDFS.label, None) not in g:
116
115
  return
@@ -119,25 +118,23 @@ class OntologyAdapter:
119
118
 
120
119
  if nx_id not in G:
121
120
  G.add_node(nx_id)
122
- G.nodes[nx_id]['label'] = nx_label
121
+ G.nodes[nx_id]["label"] = nx_label
123
122
 
124
123
  # Recursively add all subclasses of the node to the graph
125
124
  for s, _, o in g.triples((None, rdflib.RDFS.subClassOf, node)):
126
-
127
125
  # Only add nodes that have a label
128
126
  if (s, rdflib.RDFS.label, None) not in g:
129
127
  continue
130
128
 
131
129
  s_id, s_label = _get_nx_id_and_label(s)
132
130
  G.add_node(s_id)
133
- G.nodes[s_id]['label'] = s_label
131
+ G.nodes[s_id]["label"] = s_label
134
132
 
135
133
  G.add_edge(s_id, nx_id)
136
134
  add_subclasses(s)
137
135
  add_parents(s)
138
136
 
139
137
  def add_parents(node):
140
-
141
138
  # Only add nodes that have a label
142
139
  if (node, rdflib.RDFS.label, None) not in g:
143
140
  return
@@ -146,7 +143,6 @@ class OntologyAdapter:
146
143
 
147
144
  # Recursively add all parents of the node to the graph
148
145
  for s, _, o in g.triples((node, rdflib.RDFS.subClassOf, None)):
149
-
150
146
  # Only add nodes that have a label
151
147
  if (o, rdflib.RDFS.label, None) not in g:
152
148
  continue
@@ -158,15 +154,16 @@ class OntologyAdapter:
158
154
  continue
159
155
 
160
156
  G.add_node(o_id)
161
- G.nodes[o_id]['label'] = o_label
157
+ G.nodes[o_id]["label"] = o_label
162
158
 
163
159
  G.add_edge(nx_id, o_id)
164
160
  add_parents(o)
165
161
 
166
162
  def _get_nx_id_and_label(node):
167
163
  node_id_str = self._remove_prefix(str(node))
168
- node_label_str = str(g.value(node,
169
- rdflib.RDFS.label)).replace('_', ' ')
164
+ node_label_str = str(g.value(node, rdflib.RDFS.label)).replace(
165
+ "_", " "
166
+ )
170
167
  node_label_str = _misc.to_lower_sentence_case(node_label_str)
171
168
 
172
169
  nx_id = node_label_str if switch_id_and_label else node_id_str
@@ -185,7 +182,7 @@ class OntologyAdapter:
185
182
  everything before the last separator.
186
183
  """
187
184
  if self._remove_prefixes:
188
- return uri.rsplit('#', 1)[-1].rsplit('/', 1)[-1]
185
+ return uri.rsplit("#", 1)[-1].rsplit("/", 1)[-1]
189
186
  else:
190
187
  return uri
191
188
 
@@ -202,17 +199,17 @@ class OntologyAdapter:
202
199
  """
203
200
  Get the format of the ontology file.
204
201
  """
205
- if ontology_file.endswith('.owl'):
206
- return 'application/rdf+xml'
207
- elif ontology_file.endswith('.obo'):
208
- raise NotImplementedError('OBO format not yet supported')
209
- elif ontology_file.endswith('.rdf'):
210
- return 'application/rdf+xml'
211
- elif ontology_file.endswith('.ttl'):
212
- return 'ttl'
202
+ if ontology_file.endswith(".owl"):
203
+ return "application/rdf+xml"
204
+ elif ontology_file.endswith(".obo"):
205
+ raise NotImplementedError("OBO format not yet supported")
206
+ elif ontology_file.endswith(".rdf"):
207
+ return "application/rdf+xml"
208
+ elif ontology_file.endswith(".ttl"):
209
+ return "ttl"
213
210
  else:
214
211
  raise ValueError(
215
- f'Could not determine format of ontology file {ontology_file}'
212
+ f"Could not determine format of ontology file {ontology_file}"
216
213
  )
217
214
 
218
215
  def get_nx_graph(self):
@@ -254,10 +251,11 @@ class Ontology:
254
251
  while an arbitrary number of other resources can become "tail" ontologies at
255
252
  arbitrary fusion points inside the "head" ontology.
256
253
  """
254
+
257
255
  def __init__(
258
256
  self,
259
257
  head_ontology: dict,
260
- ontology_mapping: 'OntologyMapping',
258
+ ontology_mapping: "OntologyMapping",
261
259
  tail_ontologies: Optional[dict] = None,
262
260
  ):
263
261
  """
@@ -271,7 +269,7 @@ class Ontology:
271
269
  """
272
270
 
273
271
  self._head_ontology_meta = head_ontology
274
- self.extended_schema = ontology_mapping.extended_schema
272
+ self.mapping = ontology_mapping
275
273
  self._tail_ontology_meta = tail_ontologies
276
274
 
277
275
  self._tail_ontologies = None
@@ -311,21 +309,21 @@ class Ontology:
311
309
  instance variable (head) or a dictionary (tail).
312
310
  """
313
311
 
314
- logger.info('Loading ontologies...')
312
+ logger.info("Loading ontologies...")
315
313
 
316
314
  self._head_ontology = OntologyAdapter(
317
- self._head_ontology_meta['url'],
318
- self._head_ontology_meta['root_node'],
315
+ self._head_ontology_meta["url"],
316
+ self._head_ontology_meta["root_node"],
319
317
  )
320
318
 
321
319
  if self._tail_ontology_meta:
322
320
  self._tail_ontologies = {}
323
321
  for key, value in self._tail_ontology_meta.items():
324
322
  self._tail_ontologies[key] = OntologyAdapter(
325
- ontology_file = value['url'],
326
- root_label = value['tail_join_node'],
327
- head_join_node = value['head_join_node'],
328
- merge_nodes = value.get('merge_nodes', True),
323
+ ontology_file=value["url"],
324
+ root_label=value["tail_join_node"],
325
+ head_join_node=value["head_join_node"],
326
+ merge_nodes=value.get("merge_nodes", True),
329
327
  )
330
328
 
331
329
  def _assert_join_node(self, adapter: OntologyAdapter) -> None:
@@ -342,10 +340,9 @@ class Ontology:
342
340
  head_join_node = adapter.get_head_join_node()
343
341
 
344
342
  if head_join_node not in self._head_ontology.get_nx_graph().nodes:
345
-
346
343
  raise ValueError(
347
- f'Head join node {head_join_node} not found in '
348
- f'head ontology.'
344
+ f"Head join node {head_join_node} not found in "
345
+ f"head ontology."
349
346
  )
350
347
 
351
348
  def _join_ontologies(self, adapter: OntologyAdapter) -> None:
@@ -383,11 +380,9 @@ class Ontology:
383
380
  # as parent of tail join node
384
381
  tail_ontology_subtree.add_node(
385
382
  head_join_node,
386
- **self._head_ontology.get_nx_graph().nodes[head_join_node]
387
- )
388
- tail_ontology_subtree.add_edge(
389
- tail_join_node, head_join_node
383
+ **self._head_ontology.get_nx_graph().nodes[head_join_node],
390
384
  )
385
+ tail_ontology_subtree.add_edge(tail_join_node, head_join_node)
391
386
 
392
387
  # else rename tail join node to match head join node if necessary
393
388
  elif not tail_join_node == head_join_node:
@@ -408,47 +403,44 @@ class Ontology:
408
403
  if not self._nx_graph:
409
404
  self._nx_graph = self._head_ontology.get_nx_graph().copy()
410
405
 
411
- for key, value in self.extended_schema.items():
412
-
413
- if not value.get('is_a'):
414
-
415
- if self._nx_graph.has_node(value.get('synonym_for')):
416
-
406
+ for key, value in self.mapping.extended_schema.items():
407
+ if not value.get("is_a"):
408
+ if self._nx_graph.has_node(value.get("synonym_for")):
417
409
  continue
418
-
410
+
419
411
  if not self._nx_graph.has_node(key):
420
-
421
412
  raise ValueError(
422
- f'Node {key} not found in ontology, but also has no '
423
- 'inheritance definition. Please check your schema for '
424
- 'spelling errors or a missing `is_a` definition.'
413
+ f"Node {key} not found in ontology, but also has no "
414
+ "inheritance definition. Please check your schema for "
415
+ "spelling errors or a missing `is_a` definition."
425
416
  )
426
-
417
+
427
418
  continue
428
419
 
429
- parents = _misc.to_list(value.get('is_a'))
420
+ parents = _misc.to_list(value.get("is_a"))
430
421
  child = key
431
422
 
432
423
  while parents:
433
424
  parent = parents.pop(0)
434
425
 
435
426
  if parent not in self._nx_graph.nodes:
436
-
437
427
  self._nx_graph.add_node(parent)
438
428
  self._nx_graph.nodes[parent][
439
- 'label'] = _misc.sentencecase_to_pascalcase(parent)
429
+ "label"
430
+ ] = _misc.sentencecase_to_pascalcase(parent)
440
431
 
441
432
  # mark parent as user extension
442
- self._nx_graph.nodes[parent]['user_extension'] = True
433
+ self._nx_graph.nodes[parent]["user_extension"] = True
443
434
  self._extended_nodes.add(parent)
444
435
 
445
436
  if child not in self._nx_graph.nodes:
446
437
  self._nx_graph.add_node(child)
447
438
  self._nx_graph.nodes[child][
448
- 'label'] = _misc.sentencecase_to_pascalcase(child)
439
+ "label"
440
+ ] = _misc.sentencecase_to_pascalcase(child)
449
441
 
450
442
  # mark child as user extension
451
- self._nx_graph.nodes[child]['user_extension'] = True
443
+ self._nx_graph.nodes[child]["user_extension"] = True
452
444
  self._extended_nodes.add(child)
453
445
 
454
446
  self._nx_graph.add_edge(child, parent)
@@ -463,29 +455,28 @@ class Ontology:
463
455
  if not self._nx_graph:
464
456
  self._nx_graph = self._head_ontology.get_nx_graph().copy()
465
457
 
466
- if 'entity' not in self._nx_graph.nodes:
458
+ if "entity" not in self._nx_graph.nodes:
467
459
  return
468
460
 
469
461
  # biolink classes that are disjoint from entity
470
462
  disjoint_classes = [
471
- 'frequency qualifier mixin',
472
- 'chemical entity to entity association mixin',
473
- 'ontology class',
474
- 'relationship quantifier',
475
- 'physical essence or occurrent',
476
- 'gene or gene product',
477
- 'subject of investigation',
463
+ "frequency qualifier mixin",
464
+ "chemical entity to entity association mixin",
465
+ "ontology class",
466
+ "relationship quantifier",
467
+ "physical essence or occurrent",
468
+ "gene or gene product",
469
+ "subject of investigation",
478
470
  ]
479
471
 
480
472
  for node in disjoint_classes:
481
-
482
473
  if not self._nx_graph.nodes.get(node):
483
-
484
474
  self._nx_graph.add_node(node)
485
475
  self._nx_graph.nodes[node][
486
- 'label'] = _misc.sentencecase_to_pascalcase(node)
476
+ "label"
477
+ ] = _misc.sentencecase_to_pascalcase(node)
487
478
 
488
- self._nx_graph.add_edge(node, 'entity')
479
+ self._nx_graph.add_edge(node, "entity")
489
480
 
490
481
  def _add_properties(self) -> None:
491
482
  """
@@ -494,22 +485,19 @@ class Ontology:
494
485
  setting the synonym as the primary node label.
495
486
  """
496
487
 
497
- for key, value in self.extended_schema.items():
498
-
488
+ for key, value in self.mapping.extended_schema.items():
499
489
  if key in self._nx_graph.nodes:
500
-
501
490
  self._nx_graph.nodes[key].update(value)
502
491
 
503
- if value.get('synonym_for'):
504
-
492
+ if value.get("synonym_for"):
505
493
  # change node label to synonym
506
- if value['synonym_for'] not in self._nx_graph.nodes:
494
+ if value["synonym_for"] not in self._nx_graph.nodes:
507
495
  raise ValueError(
508
496
  f'Node {value["synonym_for"]} not found in ontology.'
509
497
  )
510
498
 
511
499
  self._nx_graph = nx.relabel_nodes(
512
- self._nx_graph, {value['synonym_for']: key}
500
+ self._nx_graph, {value["synonym_for"]: key}
513
501
  )
514
502
 
515
503
  def get_ancestors(self, node_label: str) -> list:
@@ -541,42 +529,41 @@ class Ontology:
541
529
  """
542
530
 
543
531
  if not self._nx_graph:
544
- raise ValueError('Ontology not loaded.')
532
+ raise ValueError("Ontology not loaded.")
545
533
 
546
534
  if not self._tail_ontologies:
547
- msg = f'Showing ontology structure based on {self._head_ontology._ontology_file}'
535
+ msg = f"Showing ontology structure based on {self._head_ontology._ontology_file}"
548
536
 
549
537
  else:
550
- msg = f'Showing ontology structure based on {len(self._tail_ontology_meta)+1} ontologies: '
538
+ msg = f"Showing ontology structure based on {len(self._tail_ontology_meta)+1} ontologies: "
551
539
 
552
540
  print(msg)
553
541
 
554
542
  if not full:
555
-
556
543
  # set of leaves and their intermediate parents up to the root
557
- filter_nodes = set(self.extended_schema.keys())
544
+ filter_nodes = set(self.mapping.extended_schema.keys())
558
545
 
559
- for node in self.extended_schema.keys():
546
+ for node in self.mapping.extended_schema.keys():
560
547
  filter_nodes.update(self.get_ancestors(node).nodes)
561
548
 
562
549
  # filter graph
563
550
  G = self._nx_graph.subgraph(filter_nodes)
564
551
 
565
552
  else:
566
-
567
553
  G = self._nx_graph
568
554
 
569
555
  if not to_disk:
570
-
571
556
  # create tree
572
557
  tree = _misc.create_tree_visualisation(G)
573
558
 
574
559
  # add synonym information
575
- for node in self.extended_schema:
576
- if self.extended_schema[node].get('synonym_for'):
560
+ for node in self.mapping.extended_schema:
561
+ if not isinstance(self.mapping.extended_schema[node], dict):
562
+ continue
563
+ if self.mapping.extended_schema[node].get("synonym_for"):
577
564
  tree.nodes[node].tag = (
578
- f'{node} = '
579
- f"{self.extended_schema[node].get('synonym_for')}"
565
+ f"{node} = "
566
+ f"{self.mapping.extended_schema[node].get('synonym_for')}"
580
567
  )
581
568
 
582
569
  tree.show()
@@ -584,26 +571,24 @@ class Ontology:
584
571
  return tree
585
572
 
586
573
  else:
587
-
588
574
  # convert lists/dicts to strings for vis only
589
575
  for node in G.nodes:
590
-
591
576
  # rename node and use former id as label
592
- label = G.nodes[node].get('label')
577
+ label = G.nodes[node].get("label")
593
578
 
594
579
  if not label:
595
580
  label = node
596
581
 
597
582
  G = nx.relabel_nodes(G, {node: label})
598
- G.nodes[label]['label'] = node
583
+ G.nodes[label]["label"] = node
599
584
 
600
585
  for attrib in G.nodes[label]:
601
586
  if type(G.nodes[label][attrib]) in [list, dict]:
602
587
  G.nodes[label][attrib] = str(G.nodes[label][attrib])
603
588
 
604
- path = os.path.join(to_disk, 'ontology_structure.graphml')
589
+ path = os.path.join(to_disk, "ontology_structure.graphml")
605
590
 
606
- logger.info(f'Writing ontology structure to {path}.')
591
+ logger.info(f"Writing ontology structure to {path}.")
607
592
 
608
593
  nx.write_graphml(G, path)
609
594
 
@@ -616,10 +601,10 @@ class Ontology:
616
601
  """
617
602
 
618
603
  d = {
619
- 'node_id': self._get_current_id(),
620
- 'node_label': 'BioCypher',
621
- 'properties': {
622
- 'schema': 'self.extended_schema',
604
+ "node_id": self._get_current_id(),
605
+ "node_label": "BioCypher",
606
+ "properties": {
607
+ "schema": "self.ontology_mapping.extended_schema",
623
608
  },
624
609
  }
625
610
 
@@ -635,5 +620,4 @@ class Ontology:
635
620
  """
636
621
 
637
622
  now = datetime.now()
638
- return now.strftime('v%Y%m%d-%H%M%S')
639
-
623
+ return now.strftime("v%Y%m%d-%H%M%S")