| 
456
 | 
     1 """
 | 
| 
 | 
     2 General utilities for COBRAxy.
 | 
| 
 | 
     3 
 | 
| 
 | 
     4 This module provides:
 | 
| 
 | 
     5 - File and path helpers (FileFormat, FilePath)
 | 
| 
 | 
     6 - Error and result handling utilities (CustomErr, Result)
 | 
| 
 | 
     7 - Basic I/O helpers (CSV/TSV, pickle, SVG)
 | 
| 
 | 
     8 - Lightweight CLI argument parsers (Bool, Float)
 | 
| 
 | 
     9 - Model loader utilities for COBRA models, including compressed formats
 | 
| 
 | 
    10 """
 | 
| 
392
 | 
    11 import math
 | 
| 
 | 
    12 import re
 | 
| 
 | 
    13 import sys
 | 
| 
 | 
    14 import csv
 | 
| 
 | 
    15 import pickle
 | 
| 
 | 
    16 import lxml.etree as ET
 | 
| 
 | 
    17 
 | 
| 
 | 
    18 from enum import Enum
 | 
| 
 | 
    19 from itertools import count
 | 
| 
456
 | 
    20 from typing import Any, Callable, Dict, Generic, List, Literal, Optional, TypeVar, Union, Tuple
 | 
| 
392
 | 
    21 
 | 
| 
 | 
    22 import pandas as pd
 | 
| 
 | 
    23 import cobra
 | 
| 
 | 
    24 
 | 
| 
 | 
    25 import zipfile
 | 
| 
 | 
    26 import gzip
 | 
| 
 | 
    27 import bz2
 | 
| 
 | 
    28 from io import StringIO
 | 
| 
 | 
    29 
 | 
| 
394
 | 
    30 
 | 
| 
456
 | 
    31 from typing import Any, Callable, Dict, Generic, List, Literal, Optional, TypeVar, Union, Tuple
 | 
| 
392
 | 
    32 class ValueErr(Exception):
 | 
| 
 | 
    33     def __init__(self, param_name, expected, actual):
 | 
| 
 | 
    34         super().__init__(f"Invalid value for {param_name}: expected {expected}, got {actual}")
 | 
| 
 | 
    35 
 | 
| 
 | 
    36 class PathErr(Exception):
 | 
| 
 | 
    37     def __init__(self, path, message):
 | 
| 
 | 
    38         super().__init__(f"Path error for '{path}': {message}")
 | 
| 
 | 
    39 
 | 
| 
 | 
    40 class FileFormat(Enum):
 | 
| 
 | 
    41     """
 | 
| 
 | 
    42     Encodes possible file extensions to conditionally save data in a different format.
 | 
| 
 | 
    43     """
 | 
| 
456
 | 
    44     DAT    = ("dat",)
 | 
| 
 | 
    45     CSV    = ("csv",)
 | 
| 
 | 
    46     TSV    = ("tsv",)
 | 
| 
 | 
    47     SVG    = ("svg",)
 | 
| 
 | 
    48     PNG    = ("png",)
 | 
| 
 | 
    49     PDF    = ("pdf",)
 | 
| 
392
 | 
    50 
 | 
| 
456
 | 
    51     # Compressed variants for common model formats
 | 
| 
 | 
    52     XML    = ("xml", "xml.gz", "xml.zip", "xml.bz2")
 | 
| 
 | 
    53     JSON   = ("json", "json.gz", "json.zip", "json.bz2")
 | 
| 
 | 
    54     MAT    = ("mat", "mat.gz", "mat.zip", "mat.bz2")
 | 
| 
 | 
    55     YML    = ("yml", "yml.gz", "yml.zip", "yml.bz2")
 | 
| 
 | 
    56 
 | 
| 
 | 
    57     TXT    = ("txt",)
 | 
| 
 | 
    58     PICKLE = ("pickle", "pk", "p")
 | 
| 
392
 | 
    59 
 | 
| 
 | 
    60     def __init__(self, *extensions):
 | 
| 
 | 
    61         self.extensions = extensions
 | 
| 
 | 
    62         # Store original extension when set via fromExt
 | 
| 
 | 
    63         self._original_extension = None
 | 
| 
 | 
    64 
 | 
| 
 | 
    65     @classmethod
 | 
| 
 | 
    66     def fromExt(cls, ext: str) -> "FileFormat":
 | 
| 
 | 
    67         """
 | 
| 
 | 
    68         Converts a file extension string to a FileFormat instance.
 | 
| 
 | 
    69         Args:
 | 
| 
 | 
    70             ext : The file extension as a string.
 | 
| 
 | 
    71         Returns:
 | 
| 
 | 
    72             FileFormat: The FileFormat instance corresponding to the file extension.
 | 
| 
 | 
    73         """
 | 
| 
 | 
    74         variantName = ext.upper()
 | 
| 
 | 
    75         if variantName in FileFormat.__members__: 
 | 
| 
 | 
    76             instance = FileFormat[variantName]
 | 
| 
 | 
    77             instance._original_extension = ext
 | 
| 
 | 
    78             return instance
 | 
| 
 | 
    79         
 | 
| 
 | 
    80         variantName = ext.lower()
 | 
| 
 | 
    81         for member in cls:
 | 
| 
 | 
    82             if variantName in member.value: 
 | 
| 
 | 
    83                 # Create a copy-like behavior by storing the original extension
 | 
| 
 | 
    84                 member._original_extension = ext
 | 
| 
 | 
    85                 return member
 | 
| 
 | 
    86         
 | 
| 
 | 
    87         raise ValueErr("ext", "a valid FileFormat file extension", ext)
 | 
| 
 | 
    88 
 | 
| 
 | 
    89     def __str__(self) -> str:
 | 
| 
 | 
    90         """
 | 
| 
 | 
    91         (Private) converts to str representation. Good practice for usage with argparse.
 | 
| 
 | 
    92         Returns:
 | 
| 
 | 
    93             str : the string representation of the file extension.
 | 
| 
 | 
    94         """
 | 
| 
 | 
    95         if hasattr(self, '_original_extension') and self._original_extension:
 | 
| 
 | 
    96             return self._original_extension
 | 
| 
 | 
    97         
 | 
| 
 | 
    98         if self == FileFormat.XML:
 | 
| 
 | 
    99             return "xml"
 | 
| 
 | 
   100         elif self == FileFormat.JSON:
 | 
| 
 | 
   101             return "json"
 | 
| 
 | 
   102         elif self == FileFormat.MAT:
 | 
| 
 | 
   103             return "mat"
 | 
| 
 | 
   104         elif self == FileFormat.YML:
 | 
| 
 | 
   105             return "yml"
 | 
| 
 | 
   106         
 | 
| 
 | 
   107         return self.value[-1]
 | 
| 
 | 
   108 
 | 
| 
 | 
   109 class FilePath():
 | 
| 
 | 
   110     """
 | 
| 
456
 | 
   111     Represents a file path with format-aware helpers.
 | 
| 
392
 | 
   112     """
 | 
| 
 | 
   113     def __init__(self, filePath: str, ext: FileFormat, *, prefix="") -> None:
 | 
| 
 | 
   114         """
 | 
| 
456
 | 
   115         Initialize FilePath.
 | 
| 
392
 | 
   116         Args:
 | 
| 
456
 | 
   117             path: File name stem.
 | 
| 
 | 
   118             ext: File extension (FileFormat).
 | 
| 
 | 
   119             prefix: Optional directory path (trailing '/' auto-added).
 | 
| 
392
 | 
   120         """
 | 
| 
 | 
   121         self.ext = ext
 | 
| 
 | 
   122         self.filePath = filePath
 | 
| 
 | 
   123 
 | 
| 
 | 
   124         if prefix and prefix[-1] != '/': 
 | 
| 
 | 
   125             prefix += '/'
 | 
| 
 | 
   126         self.prefix = prefix
 | 
| 
 | 
   127     
 | 
| 
 | 
   128     @classmethod
 | 
| 
 | 
   129     def fromStrPath(cls, path: str) -> "FilePath":
 | 
| 
 | 
   130         """
 | 
| 
456
 | 
   131     Parse a string path into a FilePath, supporting double extensions for models (e.g., .json.gz).
 | 
| 
392
 | 
   132         Args:
 | 
| 
 | 
   133             path : the string containing the path
 | 
| 
 | 
   134         Raises:
 | 
| 
 | 
   135             PathErr : if the provided string doesn't represent a valid path.
 | 
| 
 | 
   136         Returns:
 | 
| 
 | 
   137             FilePath : the constructed instance.
 | 
| 
 | 
   138         """
 | 
| 
 | 
   139         result = re.search(r"^(?P<prefix>.*\/)?(?P<name>.*)\.(?P<ext>[^.]*)$", path)
 | 
| 
 | 
   140         if not result or not result["name"] or not result["ext"]:
 | 
| 
 | 
   141             raise PathErr(path, "cannot recognize folder structure or extension in path")
 | 
| 
 | 
   142 
 | 
| 
 | 
   143         prefix = result["prefix"] if result["prefix"] else ""
 | 
| 
 | 
   144         name, ext = result["name"], result["ext"]
 | 
| 
 | 
   145 
 | 
| 
 | 
   146         parts = path.split(".")
 | 
| 
 | 
   147         if len(parts) >= 3:  
 | 
| 
 | 
   148             penultimate = parts[-2]
 | 
| 
 | 
   149             last = parts[-1]
 | 
| 
 | 
   150             double_ext = f"{penultimate}.{last}"
 | 
| 
 | 
   151             
 | 
| 
 | 
   152             try:
 | 
| 
 | 
   153                 ext_format = FileFormat.fromExt(double_ext)
 | 
| 
 | 
   154                 name = ".".join(parts[:-2])
 | 
| 
 | 
   155                 if '/' in name:
 | 
| 
 | 
   156                     prefix = name[:name.rfind('/') + 1]
 | 
| 
 | 
   157                     name = name[name.rfind('/') + 1:]
 | 
| 
 | 
   158                 return cls(name, ext_format, prefix=prefix)
 | 
| 
 | 
   159             except ValueErr:
 | 
| 
 | 
   160                 pass
 | 
| 
 | 
   161 
 | 
| 
 | 
   162         try:
 | 
| 
 | 
   163             ext_format = FileFormat.fromExt(ext)
 | 
| 
 | 
   164             return cls(name, ext_format, prefix=prefix)
 | 
| 
 | 
   165         except ValueErr:
 | 
| 
 | 
   166             raise PathErr(path, f"unsupported file extension: {ext}")
 | 
| 
 | 
   167 
 | 
| 
 | 
   168     def show(self) -> str:
 | 
| 
 | 
   169         """
 | 
| 
 | 
   170         Shows the path as a string.
 | 
| 
 | 
   171         Returns:
 | 
| 
 | 
   172             str : the path shown as a string.
 | 
| 
 | 
   173         """
 | 
| 
 | 
   174         return f"{self.prefix}{self.filePath}.{self.ext}"
 | 
| 
 | 
   175     
 | 
| 
 | 
   176     def __str__(self) -> str: 
 | 
| 
 | 
   177         return self.show()
 | 
| 
 | 
   178 
 | 
| 
 | 
   179 # ERRORS
 | 
| 
 | 
   180 def terminate(msg :str) -> None:
 | 
| 
 | 
   181     """
 | 
| 
 | 
   182     Terminate the execution of the script with an error message.
 | 
| 
 | 
   183     
 | 
| 
 | 
   184     Args:
 | 
| 
 | 
   185         msg (str): The error message to be displayed.
 | 
| 
 | 
   186     
 | 
| 
 | 
   187     Returns:
 | 
| 
 | 
   188         None
 | 
| 
 | 
   189     """
 | 
| 
 | 
   190     sys.exit(f"Execution aborted: {msg}\n")
 | 
| 
 | 
   191 
 | 
| 
 | 
   192 def logWarning(msg :str, loggerPath :str) -> None:
 | 
| 
 | 
   193     """
 | 
| 
 | 
   194     Log a warning message to an output log file and print it to the console. The final period and a
 | 
| 
 | 
   195     newline is added by the function.
 | 
| 
 | 
   196 
 | 
| 
 | 
   197     Args:
 | 
| 
456
 | 
   198         msg (str): The warning message to be logged and printed.
 | 
| 
392
 | 
   199         loggerPath : The file path of the output log file. Given as a string, parsed to a FilePath and
 | 
| 
 | 
   200         immediately read back (beware relative expensive operation, log with caution).
 | 
| 
 | 
   201 
 | 
| 
 | 
   202     Returns:
 | 
| 
 | 
   203         None
 | 
| 
 | 
   204     """
 | 
| 
456
 | 
   205     # Note: validates path via FilePath; keep logging minimal to avoid overhead.
 | 
| 
392
 | 
   206     with open(FilePath.fromStrPath(loggerPath).show(), 'a') as log: log.write(f"{msg}.\n")
 | 
| 
 | 
   207 
 | 
| 
 | 
   208 class CustomErr(Exception):
 | 
| 
 | 
   209     """
 | 
| 
 | 
   210     Custom error class to handle exceptions in a structured way, with a unique identifier and a message.
 | 
| 
 | 
   211     """
 | 
| 
 | 
   212     __idGenerator = count()
 | 
| 
 | 
   213     errName = "Custom Error"
 | 
| 
 | 
   214     def __init__(self, msg :str, details = "", explicitErrCode = -1) -> None:
 | 
| 
 | 
   215         """
 | 
| 
 | 
   216         (Private) Initializes an instance of CustomErr.
 | 
| 
 | 
   217 
 | 
| 
 | 
   218         Args:
 | 
| 
 | 
   219             msg (str): Error message to be displayed.
 | 
| 
 | 
   220             details (str): Informs the user more about the error encountered. Defaults to "".
 | 
| 
 | 
   221             explicitErrCode (int): Explicit error code to be used. Defaults to -1.
 | 
| 
 | 
   222         
 | 
| 
 | 
   223         Returns:
 | 
| 
 | 
   224             None : practically, a CustomErr instance.
 | 
| 
 | 
   225         """
 | 
| 
 | 
   226         self.msg     = msg
 | 
| 
 | 
   227         self.details = details
 | 
| 
 | 
   228 
 | 
| 
 | 
   229         self.id = max(explicitErrCode, next(CustomErr.__idGenerator))
 | 
| 
 | 
   230 
 | 
| 
 | 
   231     def throw(self, loggerPath = "") -> None:
 | 
| 
 | 
   232         """
 | 
| 
456
 | 
   233         Raises the current CustomErr instance, optionally logging it first.
 | 
| 
 | 
   234 
 | 
| 
 | 
   235         Args:
 | 
| 
 | 
   236             loggerPath (str): Optional path to a log file to append this error before raising.
 | 
| 
392
 | 
   237 
 | 
| 
 | 
   238         Raises:
 | 
| 
 | 
   239             self: The current CustomErr instance.
 | 
| 
456
 | 
   240 
 | 
| 
392
 | 
   241         Returns:
 | 
| 
 | 
   242             None
 | 
| 
 | 
   243         """
 | 
| 
456
 | 
   244         if loggerPath:
 | 
| 
 | 
   245             logWarning(str(self), loggerPath)
 | 
| 
392
 | 
   246         raise self
 | 
| 
 | 
   247 
 | 
| 
 | 
   248     def abort(self) -> None:
 | 
| 
 | 
   249         """
 | 
| 
 | 
   250         Aborts the execution of the script.
 | 
| 
 | 
   251         
 | 
| 
 | 
   252         Returns:
 | 
| 
 | 
   253             None
 | 
| 
 | 
   254         """
 | 
| 
 | 
   255         terminate(str(self))
 | 
| 
 | 
   256 
 | 
| 
 | 
   257     def __str__(self) -> str:
 | 
| 
 | 
   258         """
 | 
| 
 | 
   259         (Private) Returns a string representing the current CustomErr instance.
 | 
| 
 | 
   260 
 | 
| 
 | 
   261         Returns:
 | 
| 
 | 
   262             str: A string representing the current CustomErr instance.
 | 
| 
 | 
   263         """
 | 
| 
 | 
   264         return f"{CustomErr.errName} #{self.id}: {self.msg}, {self.details}."
 | 
| 
 | 
   265 
 | 
| 
 | 
   266 class ArgsErr(CustomErr):
 | 
| 
 | 
   267     """
 | 
| 
 | 
   268     CustomErr subclass for UI arguments errors.
 | 
| 
 | 
   269     """
 | 
| 
 | 
   270     errName = "Args Error"
 | 
| 
 | 
   271     def __init__(self, argName :str, expected :Any, actual :Any, msg = "no further details provided") -> None:
 | 
| 
 | 
   272         super().__init__(f"argument \"{argName}\" expected {expected} but got {actual}", msg)
 | 
| 
 | 
   273 
 | 
| 
 | 
   274 class DataErr(CustomErr):
 | 
| 
 | 
   275     """
 | 
| 
 | 
   276     CustomErr subclass for data formatting errors.
 | 
| 
 | 
   277     """
 | 
| 
 | 
   278     errName = "Data Format Error"
 | 
| 
 | 
   279     def __init__(self, fileName :str, msg = "no further details provided") -> None:
 | 
| 
 | 
   280         super().__init__(f"file \"{fileName}\" contains malformed data", msg)
 | 
| 
 | 
   281 
 | 
| 
 | 
   282 class PathErr(CustomErr):
 | 
| 
 | 
   283     """
 | 
| 
 | 
   284     CustomErr subclass for filepath formatting errors.
 | 
| 
 | 
   285     """
 | 
| 
 | 
   286     errName = "Path Error"
 | 
| 
 | 
   287     def __init__(self, path :FilePath, msg = "no further details provided") -> None:
 | 
| 
 | 
   288         super().__init__(f"path \"{path}\" is invalid", msg)
 | 
| 
 | 
   289 
 | 
| 
 | 
   290 class ValueErr(CustomErr):
 | 
| 
 | 
   291     """
 | 
| 
 | 
   292     CustomErr subclass for any value error.
 | 
| 
 | 
   293     """
 | 
| 
 | 
   294     errName = "Value Error"
 | 
| 
 | 
   295     def __init__(self, valueName: str, expected :Any, actual :Any, msg = "no further details provided") -> None:
 | 
| 
 | 
   296         super().__init__("value " + f"\"{valueName}\" " * bool(valueName) + f"was supposed to be {expected}, but got {actual} instead", msg)
 | 
| 
 | 
   297 
 | 
| 
 | 
   298 # RESULT
 | 
| 
 | 
   299 T = TypeVar('T')
 | 
| 
 | 
   300 E = TypeVar('E', bound = CustomErr) # should bind to Result.ResultErr but python happened!
 | 
| 
 | 
   301 class Result(Generic[T, E]):
 | 
| 
 | 
   302     class ResultErr(CustomErr):
 | 
| 
 | 
   303         """
 | 
| 
 | 
   304         CustomErr subclass for all Result errors.
 | 
| 
 | 
   305         """
 | 
| 
 | 
   306         errName = "Result Error"
 | 
| 
 | 
   307         def __init__(self, msg = "no further details provided") -> None:
 | 
| 
 | 
   308             super().__init__(msg)
 | 
| 
 | 
   309     """
 | 
| 
 | 
   310     Class to handle the result of an operation, with a value and a boolean flag to indicate
 | 
| 
 | 
   311     whether the operation was successful or not.
 | 
| 
 | 
   312     """
 | 
| 
 | 
   313     def __init__(self, value :Union[T, E], isOk :bool) -> None:
 | 
| 
 | 
   314         """
 | 
| 
456
 | 
   315         Initialize an instance of Result.
 | 
| 
392
 | 
   316 
 | 
| 
 | 
   317         Args:
 | 
| 
 | 
   318             value (Union[T, E]): The value to be stored in the Result instance.
 | 
| 
 | 
   319             isOk (bool): A boolean flag to indicate whether the operation was successful or not.
 | 
| 
 | 
   320         
 | 
| 
 | 
   321             Returns:
 | 
| 
 | 
   322                 None : practically, a Result instance.
 | 
| 
 | 
   323         """
 | 
| 
 | 
   324         self.isOk  = isOk
 | 
| 
 | 
   325         self.isErr = not isOk
 | 
| 
 | 
   326         self.value = value
 | 
| 
 | 
   327 
 | 
| 
 | 
   328     @classmethod
 | 
| 
 | 
   329     def Ok(cls,  value :T) -> "Result":
 | 
| 
 | 
   330         """
 | 
| 
456
 | 
   331         Construct a successful Result.
 | 
| 
392
 | 
   332 
 | 
| 
 | 
   333         Args:
 | 
| 
 | 
   334             value (T): The value to be stored in the Result instance, set as successful.
 | 
| 
 | 
   335 
 | 
| 
 | 
   336         Returns:
 | 
| 
 | 
   337             Result: A new Result instance with a successful operation.
 | 
| 
 | 
   338         """
 | 
| 
 | 
   339         return Result(value, isOk = True)
 | 
| 
 | 
   340     
 | 
| 
 | 
   341     @classmethod
 | 
| 
 | 
   342     def Err(cls, value :E) -> "Result": 
 | 
| 
 | 
   343         """
 | 
| 
456
 | 
   344         Construct a failed Result.
 | 
| 
392
 | 
   345 
 | 
| 
 | 
   346         Args:
 | 
| 
 | 
   347             value (E): The value to be stored in the Result instance, set as failed.
 | 
| 
 | 
   348 
 | 
| 
 | 
   349         Returns:
 | 
| 
 | 
   350             Result: A new Result instance with a failed operation.
 | 
| 
 | 
   351         """
 | 
| 
 | 
   352         return Result(value, isOk = False)
 | 
| 
 | 
   353 
 | 
| 
 | 
   354     def unwrap(self) -> T:
 | 
| 
 | 
   355         """
 | 
| 
 | 
   356         Unwraps the value of the Result instance, if the operation was successful.
 | 
| 
 | 
   357 
 | 
| 
 | 
   358         Raises:
 | 
| 
 | 
   359             ResultErr: If the operation was not successful.
 | 
| 
 | 
   360 
 | 
| 
 | 
   361         Returns:
 | 
| 
 | 
   362             T: The value of the Result instance, if the operation was successful.
 | 
| 
 | 
   363         """
 | 
| 
 | 
   364         if self.isOk: return self.value
 | 
| 
 | 
   365         raise Result.ResultErr(f"Unwrapped Result.Err : {self.value}")
 | 
| 
 | 
   366 
 | 
| 
 | 
   367     def unwrapOr(self, default :T) -> T:
 | 
| 
 | 
   368         """
 | 
| 
 | 
   369         Unwraps the value of the Result instance, if the operation was successful, otherwise
 | 
| 
 | 
   370         it returns a default value.
 | 
| 
 | 
   371 
 | 
| 
 | 
   372         Args:
 | 
| 
 | 
   373             default (T): The default value to be returned if the operation was not successful.
 | 
| 
 | 
   374 
 | 
| 
 | 
   375         Returns:
 | 
| 
 | 
   376             T: The value of the Result instance, if the operation was successful,
 | 
| 
 | 
   377             otherwise the default value.
 | 
| 
 | 
   378         """
 | 
| 
 | 
   379         return self.value if self.isOk else default
 | 
| 
 | 
   380     
 | 
| 
 | 
   381     def expect(self, err :"Result.ResultErr") -> T:
 | 
| 
 | 
   382         """
 | 
| 
 | 
   383         Expects that the value of the Result instance is successful, otherwise it raises an error.
 | 
| 
 | 
   384 
 | 
| 
 | 
   385         Args:
 | 
| 
 | 
   386             err (Exception): The error to be raised if the operation was not successful.
 | 
| 
 | 
   387 
 | 
| 
 | 
   388         Raises:
 | 
| 
 | 
   389             err: The error raised if the operation was not successful.
 | 
| 
 | 
   390 
 | 
| 
 | 
   391         Returns:
 | 
| 
 | 
   392             T: The value of the Result instance, if the operation was successful.
 | 
| 
 | 
   393         """
 | 
| 
 | 
   394         if self.isOk: return self.value
 | 
| 
 | 
   395         raise err
 | 
| 
 | 
   396 
 | 
| 
 | 
   397     U = TypeVar("U")
 | 
| 
 | 
   398     def map(self, mapper: Callable[[T], U]) -> "Result[U, E]":
 | 
| 
 | 
   399         """
 | 
| 
 | 
   400         Maps the value of the current Result to whatever is returned by the mapper function.
 | 
| 
 | 
   401         If the Result contained an unsuccessful operation to begin with it remains unchanged
 | 
| 
 | 
   402         (a reference to the current instance is returned).
 | 
| 
 | 
   403         If the mapper function panics the returned result instance will be of the error kind.
 | 
| 
 | 
   404 
 | 
| 
 | 
   405         Args:
 | 
| 
 | 
   406             mapper (Callable[[T], U]): The mapper operation to be applied to the Result value.
 | 
| 
 | 
   407 
 | 
| 
 | 
   408         Returns:
 | 
| 
 | 
   409             Result[U, E]: The result of the mapper operation applied to the Result value.
 | 
| 
 | 
   410         """
 | 
| 
 | 
   411         if self.isErr: return self
 | 
| 
 | 
   412         try: return Result.Ok(mapper(self.value))
 | 
| 
 | 
   413         except Exception as e: return Result.Err(e)
 | 
| 
 | 
   414     
 | 
| 
 | 
   415     D = TypeVar("D", bound = "Result.ResultErr")
 | 
| 
 | 
   416     def mapErr(self, mapper :Callable[[E], D]) -> "Result[T, D]":
 | 
| 
 | 
   417         """
 | 
| 
 | 
   418         Maps the error of the current Result to whatever is returned by the mapper function.
 | 
| 
 | 
   419         If the Result contained a successful operation it remains unchanged
 | 
| 
 | 
   420         (a reference to the current instance is returned).
 | 
| 
 | 
   421         If the mapper function panics this method does as well.
 | 
| 
 | 
   422 
 | 
| 
 | 
   423         Args:
 | 
| 
 | 
   424             mapper (Callable[[E], D]): The mapper operation to be applied to the Result error.
 | 
| 
 | 
   425 
 | 
| 
 | 
   426         Returns:
 | 
| 
 | 
   427             Result[U, E]: The result of the mapper operation applied to the Result error.
 | 
| 
 | 
   428         """
 | 
| 
 | 
   429         if self.isOk: return self
 | 
| 
 | 
   430         return Result.Err(mapper(self.value))
 | 
| 
 | 
   431 
 | 
| 
 | 
   432     def __str__(self):
 | 
| 
 | 
   433         return f"Result::{'Ok' if self.isOk else 'Err'}({self.value})"
 | 
| 
 | 
   434 
 | 
| 
 | 
   435 # FILES
 | 
| 
 | 
   436 def readPickle(path :FilePath) -> Any:
 | 
| 
 | 
   437     """
 | 
| 
 | 
   438     Reads the contents of a .pickle file, which needs to exist at the given path.
 | 
| 
 | 
   439 
 | 
| 
 | 
   440     Args:
 | 
| 
 | 
   441         path : the path to the .pickle file.
 | 
| 
 | 
   442     
 | 
| 
 | 
   443     Returns:
 | 
| 
 | 
   444         Any : the data inside a pickle file, could be anything.
 | 
| 
 | 
   445     """
 | 
| 
 | 
   446     with open(path.show(), "rb") as fd: return pickle.load(fd)
 | 
| 
 | 
   447 
 | 
| 
 | 
   448 def writePickle(path :FilePath, data :Any) -> None:
 | 
| 
 | 
   449     """
 | 
| 
 | 
   450     Saves any data in a .pickle file, created at the given path.
 | 
| 
 | 
   451 
 | 
| 
 | 
   452     Args:
 | 
| 
 | 
   453         path : the path to the .pickle file.
 | 
| 
 | 
   454         data : the data to be written to the file.
 | 
| 
 | 
   455     
 | 
| 
 | 
   456     Returns:
 | 
| 
 | 
   457         None
 | 
| 
 | 
   458     """
 | 
| 
 | 
   459     with open(path.show(), "wb") as fd: pickle.dump(data, fd)
 | 
| 
 | 
   460 
 | 
| 
 | 
   461 def readCsv(path :FilePath, delimiter = ',', *, skipHeader = True) -> List[List[str]]:
 | 
| 
 | 
   462     """
 | 
| 
 | 
   463     Reads the contents of a .csv file, which needs to exist at the given path.
 | 
| 
 | 
   464 
 | 
| 
 | 
   465     Args:
 | 
| 
 | 
   466         path : the path to the .csv file.
 | 
| 
 | 
   467         delimiter : allows other subformats such as .tsv to be opened by the same method (\\t delimiter).
 | 
| 
 | 
   468         skipHeader : whether the first row of the file is a header and should be skipped.
 | 
| 
 | 
   469     
 | 
| 
 | 
   470     Returns:
 | 
| 
 | 
   471         List[List[str]] : list of rows from the file, each parsed as a list of strings originally separated by commas.
 | 
| 
 | 
   472     """
 | 
| 
 | 
   473     with open(path.show(), "r", newline = "") as fd: return list(csv.reader(fd, delimiter = delimiter))[skipHeader:]
 | 
| 
 | 
   474 
 | 
| 
427
 | 
   475 def findIdxByName(header: List[str], name: str, colName="name") -> Optional[int]:
 | 
| 
 | 
   476     """
 | 
| 
 | 
   477     Find the indices of the 'ReactionID' column and a user-specified column name
 | 
| 
 | 
   478     within the header row of a tabular file.
 | 
| 
 | 
   479 
 | 
| 
 | 
   480     Args:
 | 
| 
 | 
   481         header (List[str]): The header row, as a list of column names.
 | 
| 
 | 
   482         name (str): The name of the column to look for (e.g. 'GPR').
 | 
| 
 | 
   483         colName (str, optional): Label used in error messages for clarity. Defaults to "name".
 | 
| 
 | 
   484 
 | 
| 
 | 
   485     Returns:
 | 
| 
 | 
   486         Tuple[int, int]: A tuple containing:
 | 
| 
 | 
   487             - The index of the 'ReactionID' column.
 | 
| 
 | 
   488             - The index of the requested column `name`.
 | 
| 
 | 
   489 
 | 
| 
 | 
   490     Raises:
 | 
| 
 | 
   491         ValueError: If 'ReactionID' or the requested column `name` is not found in the header.
 | 
| 
 | 
   492 
 | 
| 
 | 
   493     Notes:
 | 
| 
 | 
   494         Both 'ReactionID' and the requested column are mandatory for downstream processing.
 | 
| 
 | 
   495     """
 | 
| 
 | 
   496 
 | 
| 
 | 
   497     col_index = {col_name: idx for idx, col_name in enumerate(header)}
 | 
| 
 | 
   498 
 | 
| 
 | 
   499     if name not in col_index or "ReactionID" not in col_index:
 | 
| 
 | 
   500         raise ValueError(f"Tabular file must contain 'ReactionID' and {name} columns.")
 | 
| 
 | 
   501 
 | 
| 
 | 
   502     id_idx = col_index["ReactionID"]
 | 
| 
 | 
   503     idx_gpr = col_index[name]
 | 
| 
 | 
   504 
 | 
| 
 | 
   505     return id_idx, idx_gpr
 | 
| 
 | 
   506 
 | 
| 
 | 
   507 
 | 
| 
392
 | 
   508 def readSvg(path :FilePath, customErr :Optional[Exception] = None) -> ET.ElementTree:
 | 
| 
 | 
   509     """
 | 
| 
 | 
   510     Reads the contents of a .svg file, which needs to exist at the given path.
 | 
| 
 | 
   511 
 | 
| 
 | 
   512     Args:
 | 
| 
 | 
   513         path : the path to the .svg file.
 | 
| 
 | 
   514     
 | 
| 
 | 
   515     Raises:
 | 
| 
 | 
   516         DataErr : if the map is malformed.
 | 
| 
 | 
   517     
 | 
| 
 | 
   518     Returns:
 | 
| 
 | 
   519         Any : the data inside a svg file, could be anything.
 | 
| 
 | 
   520     """
 | 
| 
 | 
   521     try: return ET.parse(path.show())
 | 
| 
 | 
   522     except (ET.XMLSyntaxError, ET.XMLSchemaParseError) as err:
 | 
| 
 | 
   523         raise customErr if customErr else err
 | 
| 
 | 
   524 
 | 
| 
 | 
   525 def writeSvg(path :FilePath, data:ET.ElementTree) -> None:
 | 
| 
 | 
   526     """
 | 
| 
 | 
   527     Saves svg data opened with lxml.etree in a .svg file, created at the given path.
 | 
| 
 | 
   528 
 | 
| 
 | 
   529     Args:
 | 
| 
 | 
   530         path : the path to the .svg file.
 | 
| 
 | 
   531         data : the data to be written to the file.
 | 
| 
 | 
   532     
 | 
| 
 | 
   533     Returns:
 | 
| 
 | 
   534         None
 | 
| 
 | 
   535     """
 | 
| 
 | 
   536     with open(path.show(), "wb") as fd: fd.write(ET.tostring(data))
 | 
| 
 | 
   537 
 | 
| 
 | 
   538 # UI ARGUMENTS
 | 
| 
 | 
   539 class Bool:
 | 
| 
456
 | 
   540     """Simple boolean CLI argument parser accepting 'true' or 'false' (case-insensitive)."""
 | 
| 
392
 | 
   541     def __init__(self, argName :str) -> None:
 | 
| 
 | 
   542         self.argName = argName
 | 
| 
 | 
   543 
 | 
| 
 | 
   544     def __call__(self, s :str) -> bool: return self.check(s)
 | 
| 
 | 
   545 
 | 
| 
 | 
   546     def check(self, s :str) -> bool:
 | 
| 
 | 
   547         s = s.lower()
 | 
| 
 | 
   548         if s == "true" : return True
 | 
| 
 | 
   549         if s == "false": return False
 | 
| 
 | 
   550         raise ArgsErr(self.argName, "boolean string (true or false, not case sensitive)", f"\"{s}\"")
 | 
| 
 | 
   551 
 | 
| 
 | 
   552 class Float:
 | 
| 
456
 | 
   553     """Float CLI argument parser supporting NaN and None keywords (case-insensitive)."""
 | 
| 
392
 | 
   554     def __init__(self, argName = "Dataset values, not an argument") -> None:
 | 
| 
 | 
   555         self.argName = argName
 | 
| 
 | 
   556     
 | 
| 
 | 
   557     def __call__(self, s :str) -> float: return self.check(s)
 | 
| 
 | 
   558 
 | 
| 
 | 
   559     def check(self, s :str) -> float:
 | 
| 
 | 
   560         try: return float(s)
 | 
| 
 | 
   561         except ValueError:
 | 
| 
 | 
   562             s = s.lower()
 | 
| 
 | 
   563             if s == "nan" or s == "none": return math.nan
 | 
| 
 | 
   564             raise ArgsErr(self.argName, "numeric string or \"None\" or \"NaN\" (not case sensitive)", f"\"{s}\"")
 | 
| 
 | 
   565 
 | 
| 
 | 
   566 # MODELS
 | 
| 
 | 
   567 OldRule = List[Union[str, "OldRule"]]
 | 
| 
 | 
   568 class Model(Enum):
 | 
| 
 | 
   569     """
 | 
| 
 | 
   570     Represents a metabolic model, either custom or locally supported. Custom models don't point
 | 
| 
 | 
   571     to valid file paths.
 | 
| 
 | 
   572     """
 | 
| 
 | 
   573 
 | 
| 
 | 
   574     Recon   = "Recon"
 | 
| 
 | 
   575     ENGRO2  = "ENGRO2"
 | 
| 
 | 
   576     ENGRO2_no_legend = "ENGRO2_no_legend"
 | 
| 
 | 
   577     HMRcore = "HMRcore"
 | 
| 
 | 
   578     HMRcore_no_legend = "HMRcore_no_legend"
 | 
| 
456
 | 
   579     Custom  = "Custom" 
 | 
| 
392
 | 
   580 
 | 
| 
 | 
   581     def __raiseMissingPathErr(self, path :Optional[FilePath]) -> None:
 | 
| 
 | 
   582         if not path: raise PathErr("<<MISSING>>", "it's necessary to provide a custom path when retrieving files from a custom model")
 | 
| 
 | 
   583 
 | 
| 
 | 
   584     def getRules(self, toolDir :str, customPath :Optional[FilePath] = None) -> Dict[str, Dict[str, OldRule]]:
 | 
| 
 | 
   585         """
 | 
| 
 | 
   586         Open "rules" file for this model.
 | 
| 
 | 
   587 
 | 
| 
 | 
   588         Returns:
 | 
| 
 | 
   589             Dict[str, Dict[str, OldRule]] : the rules for this model.
 | 
| 
 | 
   590         """
 | 
| 
 | 
   591         path = customPath if self is Model.Custom else FilePath(f"{self.name}_rules", FileFormat.PICKLE, prefix = f"{toolDir}/local/pickle files/")
 | 
| 
 | 
   592         self.__raiseMissingPathErr(path)
 | 
| 
 | 
   593         return readPickle(path)
 | 
| 
 | 
   594     
 | 
| 
 | 
   595     def getTranslator(self, toolDir :str, customPath :Optional[FilePath] = None) -> Dict[str, Dict[str, str]]:
 | 
| 
 | 
   596         """
 | 
| 
 | 
   597         Open "gene translator (old: gene_in_rule)" file for this model.
 | 
| 
 | 
   598 
 | 
| 
 | 
   599         Returns:
 | 
| 
 | 
   600             Dict[str, Dict[str, str]] : the translator dict for this model.
 | 
| 
 | 
   601         """
 | 
| 
 | 
   602         path = customPath if self is Model.Custom else FilePath(f"{self.name}_genes", FileFormat.PICKLE, prefix = f"{toolDir}/local/pickle files/")
 | 
| 
 | 
   603         self.__raiseMissingPathErr(path)
 | 
| 
 | 
   604         return readPickle(path)
 | 
| 
 | 
   605     
 | 
| 
 | 
   606     def getMap(self, toolDir = ".", customPath :Optional[FilePath] = None) -> ET.ElementTree:
 | 
| 
456
 | 
   607         """Open the SVG metabolic map for this model."""
 | 
| 
392
 | 
   608         path = customPath if self is Model.Custom else FilePath(f"{self.name}_map", FileFormat.SVG, prefix = f"{toolDir}/local/svg metabolic maps/")
 | 
| 
 | 
   609         self.__raiseMissingPathErr(path)
 | 
| 
 | 
   610         return readSvg(path, customErr = DataErr(path, f"custom map in wrong format"))
 | 
| 
 | 
   611     
 | 
| 
 | 
   612     def getCOBRAmodel(self, toolDir = ".", customPath :Optional[FilePath] = None, customExtension :Optional[FilePath]=None)->cobra.Model:
 | 
| 
456
 | 
   613         """Load the COBRA model for this enum variant (supports Custom with explicit path/extension)."""
 | 
| 
392
 | 
   614         if(self is Model.Custom):
 | 
| 
 | 
   615             return self.load_custom_model(customPath, customExtension)
 | 
| 
 | 
   616         else:
 | 
| 
 | 
   617             return cobra.io.read_sbml_model(FilePath(f"{self.name}", FileFormat.XML, prefix = f"{toolDir}/local/models/").show())
 | 
| 
 | 
   618         
 | 
| 
 | 
   619     def load_custom_model(self, file_path :FilePath, ext :Optional[FileFormat] = None) -> cobra.Model:
 | 
| 
456
 | 
   620         """Load a COBRA model from a custom path, supporting XML, JSON, MAT, and YML (compressed or not)."""
 | 
| 
392
 | 
   621         ext = ext if ext else file_path.ext
 | 
| 
 | 
   622         try:
 | 
| 
 | 
   623             if str(ext) in FileFormat.XML.value:
 | 
| 
 | 
   624                 return cobra.io.read_sbml_model(file_path.show())
 | 
| 
 | 
   625             
 | 
| 
 | 
   626             if str(ext) in FileFormat.JSON.value:
 | 
| 
 | 
   627                 # Compressed files are not automatically handled by cobra
 | 
| 
 | 
   628                 if(ext == "json"):
 | 
| 
 | 
   629                     return cobra.io.load_json_model(file_path.show())
 | 
| 
 | 
   630                 else: 
 | 
| 
 | 
   631                     return self.extract_model(file_path, ext, "json")
 | 
| 
 | 
   632 
 | 
| 
 | 
   633             if str(ext) in FileFormat.MAT.value:
 | 
| 
 | 
   634                 # Compressed files are not automatically handled by cobra
 | 
| 
 | 
   635                 if(ext == "mat"):
 | 
| 
 | 
   636                     return cobra.io.load_matlab_model(file_path.show())
 | 
| 
 | 
   637                 else: 
 | 
| 
 | 
   638                     return self.extract_model(file_path, ext, "mat")
 | 
| 
 | 
   639 
 | 
| 
 | 
   640             if str(ext) in FileFormat.YML.value:
 | 
| 
 | 
   641                 # Compressed files are not automatically handled by cobra
 | 
| 
 | 
   642                 if(ext == "yml"):
 | 
| 
 | 
   643                     return cobra.io.load_yaml_model(file_path.show())
 | 
| 
 | 
   644                 else: 
 | 
| 
 | 
   645                     return self.extract_model(file_path, ext, "yml")
 | 
| 
 | 
   646 
 | 
| 
 | 
   647         except Exception as e: raise DataErr(file_path, e.__str__())
 | 
| 
 | 
   648         raise DataErr(file_path,
 | 
| 
 | 
   649             f"Fomat \"{file_path.ext}\" is not recognized, only JSON, XML, MAT and YAML (.yml) files are supported.")
 | 
| 
 | 
   650     
 | 
| 
 | 
   651 
 | 
| 
 | 
   652     def extract_model(self, file_path:FilePath, ext :FileFormat, model_encoding:Literal["json", "mat", "yml"]) -> cobra.Model:
 | 
| 
 | 
   653         """
 | 
| 
 | 
   654         Extract JSON, MAT and YAML COBRA model from a compressed file (zip, gz, bz2).
 | 
| 
 | 
   655         
 | 
| 
 | 
   656         Args:
 | 
| 
 | 
   657             file_path: File path of the model
 | 
| 
 | 
   658             ext: File extensions of class FileFormat (should be .zip, .gz or .bz2)
 | 
| 
 | 
   659             
 | 
| 
 | 
   660         Returns:
 | 
| 
 | 
   661             cobra.Model: COBRApy model 
 | 
| 
 | 
   662             
 | 
| 
 | 
   663         Raises:
 | 
| 
 | 
   664             Exception: Extraction errors
 | 
| 
 | 
   665         """
 | 
| 
 | 
   666         ext_str = str(ext)
 | 
| 
 | 
   667 
 | 
| 
 | 
   668         try:
 | 
| 
 | 
   669             if '.zip' in ext_str:
 | 
| 
 | 
   670                 with zipfile.ZipFile(file_path.show(), 'r') as zip_ref:
 | 
| 
 | 
   671                     with zip_ref.open(zip_ref.namelist()[0]) as json_file:
 | 
| 
 | 
   672                         content = json_file.read().decode('utf-8')
 | 
| 
 | 
   673                         if model_encoding == "json":
 | 
| 
 | 
   674                             return cobra.io.load_json_model(StringIO(content))
 | 
| 
 | 
   675                         elif model_encoding == "mat":
 | 
| 
 | 
   676                             return cobra.io.load_matlab_model(StringIO(content))
 | 
| 
 | 
   677                         elif model_encoding == "yml":
 | 
| 
 | 
   678                             return cobra.io.load_yaml_model(StringIO(content))
 | 
| 
 | 
   679                         else:
 | 
| 
 | 
   680                             raise ValueError(f"Unsupported model encoding: {model_encoding}. Supported: json, mat, yml")
 | 
| 
 | 
   681             elif '.gz' in ext_str:
 | 
| 
 | 
   682                 with gzip.open(file_path.show(), 'rt', encoding='utf-8') as gz_ref:
 | 
| 
 | 
   683                     if model_encoding == "json":
 | 
| 
 | 
   684                         return cobra.io.load_json_model(gz_ref)
 | 
| 
 | 
   685                     elif model_encoding == "mat":
 | 
| 
 | 
   686                         return cobra.io.load_matlab_model(gz_ref)
 | 
| 
 | 
   687                     elif model_encoding == "yml":
 | 
| 
 | 
   688                         return cobra.io.load_yaml_model(gz_ref)
 | 
| 
 | 
   689                     else:
 | 
| 
 | 
   690                         raise ValueError(f"Unsupported model encoding: {model_encoding}. Supported: json, mat, yml")
 | 
| 
 | 
   691             elif '.bz2' in ext_str:
 | 
| 
 | 
   692                 with bz2.open(file_path.show(), 'rt', encoding='utf-8') as bz2_ref:
 | 
| 
 | 
   693                     if model_encoding == "json":
 | 
| 
 | 
   694                         return cobra.io.load_json_model(bz2_ref)
 | 
| 
 | 
   695                     elif model_encoding == "mat":
 | 
| 
 | 
   696                         return cobra.io.load_matlab_model(bz2_ref)
 | 
| 
 | 
   697                     elif model_encoding == "yml":
 | 
| 
 | 
   698                         return cobra.io.load_yaml_model(bz2_ref)
 | 
| 
 | 
   699                     else:
 | 
| 
 | 
   700                         raise ValueError(f"Unsupported model encoding: {model_encoding}. Supported: json, mat, yml")
 | 
| 
 | 
   701             else:
 | 
| 
 | 
   702                 raise ValueError(f"Compression format not supported: {ext_str}. Supported: .zip, .gz and .bz2")
 | 
| 
 | 
   703             
 | 
| 
 | 
   704         except Exception as e:
 | 
| 
 | 
   705             raise Exception(f"Error during model extraction: {str(e)}")
 | 
| 
 | 
   706         
 | 
| 
 | 
   707 
 | 
| 
 | 
   708 
 | 
| 
394
 | 
   709     def __str__(self) -> str: return self.value
 | 
| 
 | 
   710 
 | 
| 
 | 
   711 
 |