Repository: h2020charisma/ramanchada2 (language: python)

Issue:
add support for File objects in spectrum.from_local_file  
add support for File objects in spectrum.from_local_file , in addition to current file names

useful when used in backend implementations


Discussion / hints:
If the filename is not known to the `from_local_file` function, the file type cannot be deduced by the file extension, so the file object should always be given together with the file type. Do you see any related issues?
currently I pass the file name as well (as below).  file extension only is an option as well 
one use case is sending a file through multipart web form,  we obtain file object and file names (without paths) 

files: list[UploadFile] = File(...),
FastAPI class is https://fastapi.tiangolo.com/reference/uploadfile/ , all frameworks always have File object and a name

```
def read_spectrum_native(file,f_name,prefix="rcapi_"):
    native_filename=None
    try:
        filename, file_extension = os.path.splitext(f_name)
        # because rc2works with file paths only, no url nor file objects
        with tempfile.NamedTemporaryFile(delete=False,prefix=prefix,suffix=file_extension) as tmp:
            shutil.copyfileobj(file,tmp)
            native_filename = tmp.name
        spe =  from_local_file(native_filename)
        return spe
    except Exception as err:
        raise err
    finally:
        if native_filename!=None:
            os.remove(native_filename)
```            

another case is obtaining the file as a stream , e.g. load directly from URL  without writing locally


Fix patch:
diff --git a/src/ramanchada2/spectrum/__init__.py b/src/ramanchada2/spectrum/__init__.py
index f7aef336..659a9243 100644
--- a/src/ramanchada2/spectrum/__init__.py
+++ b/src/ramanchada2/spectrum/__init__.py
@@ -1,18 +1,18 @@
-#!/usr/bin/env python3
-
-from .spectrum import Spectrum  # noqa
 from .arithmetics import *  # noqa
 from .baseline import *  # noqa
 from .calc import *  # noqa
 from .calibration import *  # noqa
-from .filters import *  # noqa
-from .peaks import *  # noqa
 from .creators.from_cache_or_calc import from_cache_or_calc
 from .creators.from_chada import from_chada
+from .creators.from_delta_lines import from_delta_lines
 from .creators.from_local_file import from_local_file
 from .creators.from_simulation import from_simulation
-from .creators.from_theoretical_lines import from_theoretical_lines
-from .creators.from_spectral_component_collection import from_spectral_component_collection
-from .creators.from_delta_lines import from_delta_lines
+from .creators.from_spectral_component_collection import \
+    from_spectral_component_collection
+from .creators.from_stream import from_stream
 from .creators.from_test_spe import from_test_spe
+from .creators.from_theoretical_lines import from_theoretical_lines
 from .creators.hdr_from_multi_exposure import hdr_from_multi_exposure
+from .filters import *  # noqa
+from .peaks import *  # noqa
+from .spectrum import Spectrum  # noqa
diff --git a/src/ramanchada2/spectrum/creators/from_stream.py b/src/ramanchada2/spectrum/creators/from_stream.py
new file mode 100644
index 00000000..f0fcd352
--- /dev/null
+++ b/src/ramanchada2/spectrum/creators/from_stream.py
@@ -0,0 +1,83 @@
+import io
+import os
+import shutil
+import tempfile
+from typing import Literal, Optional, Union
+
+import spc_io
+from pydantic import validate_call
+
+from ramanchada2.io.experimental import rc1_parser, read_csv, read_txt
+from ramanchada2.misc.spectrum_deco import add_spectrum_constructor
+from ramanchada2.misc.types import SpeMetadataModel
+
+from ..spectrum import Spectrum
+
+
+@add_spectrum_constructor()
+@validate_call(config=dict(arbitrary_types_allowed=True))
+def from_stream(in_stream: Union[io.TextIOBase, io.BytesIO, io.BufferedReader],
+                filetype: Union[None, Literal['spc', 'sp', 'spa', '0', '1', '2',
+                                              'wdf', 'ngs', 'jdx', 'dx',
+                                              'txt', 'txtr', 'csv', 'prn', 'rruf']],
+                filename: Optional[str] = None,
+                backend: Union[None, Literal['native', 'rc1_parser']] = None,
+                ):
+    def load_native():
+        if filetype in {'txt', 'txtr', 'prn', 'rruf'}:
+            if isinstance(in_stream, io.TextIOBase):
+                fp = in_stream
+            else:
+                fp = io.TextIOWrapper(in_stream)
+            x, y, meta = read_txt(fp)
+        elif filetype in {'csv'}:
+            if isinstance(in_stream, io.TextIOBase):
+                fp = in_stream
+            else:
+                fp = io.TextIOWrapper(in_stream)
+            x, y, meta = read_csv(fp)
+        elif filetype in {'spc'}:
+            if isinstance(in_stream, io.TextIOBase):
+                raise ValueError('For spc filetype does not support io.TextIOBase')
+            fp = in_stream
+            spc = spc_io.SPC.from_bytes_io(fp)
+            if len(spc) != 1:
+                raise ValueError(f'Single subfile SPCs are supported. {len(spc)} subfiles found')
+            x = spc[0].xarray
+            y = spc[0].yarray
+            meta = spc.log_book.text
+        else:
+            raise ValueError(f'filetype {filetype} not supported')
+        meta["Original file"] = os.path.basename(filename) if filename else 'N/A loaded from stream'
+        spe = Spectrum(x=x, y=y, metadata=meta)  # type: ignore
+        return spe
+
+    def load_rc1():
+        with tempfile.TemporaryDirectory(suffix='ramanchada2') as dn:
+            fn = os.path.basename(filename or in_stream.name or f'noname.{filetype}')
+            path = os.path.join(dn, fn)
+            print(path)
+            if isinstance(in_stream, io.TextIOBase):
+                with open(path, 'w') as fp:
+                    shutil.copyfileobj(in_stream, fp)
+                    print(f'shutil.copyfileobj({in_stream}, {fp}')
+            else:
+                with open(path, 'wb') as fp:
+                    shutil.copyfileobj(in_stream, fp)
+                    print(f'shutil.copyfileobj({in_stream}, {fp}')
+            x, y, meta = rc1_parser.parse(path, filetype)
+        spe = Spectrum(x=x, y=y, metadata=SpeMetadataModel.model_validate(meta))
+        return spe
+
+    if backend == 'native':
+        spe = load_native()
+    elif backend == 'rc1_parser':
+        spe = load_rc1()
+    elif backend is None:
+        try:
+            spe = load_native()
+        except Exception:
+            spe = load_rc1()
+
+    spe._sort_x()
+    return spe


Test patch:
diff --git a/tests/spectrum/creators/test_from_stream.py b/tests/spectrum/creators/test_from_stream.py
new file mode 100644
index 00000000..108e933b
--- /dev/null
+++ b/tests/spectrum/creators/test_from_stream.py
@@ -0,0 +1,29 @@
+import os
+
+import pytest
+
+import ramanchada2 as rc2
+
+
+def test_from_stream(experimental_filename):
+    ft = os.path.splitext(experimental_filename)[1][1:]
+    if ft in {'l6s', 'wxd'}:
+        print('skip', ft)
+        return
+    print(ft)
+    with open(experimental_filename, 'rb') as fp:
+        spe = rc2.spectrum.from_stream(fp, filetype=ft, backend='rc1_parser')
+        assert len(spe.x) > 10
+
+    if ft not in {'wdf'}:
+        with open(experimental_filename, 'rb') as fp:
+            spe = rc2.spectrum.from_stream(fp, filetype=ft, backend='native')
+            assert len(spe.x) > 10
+
+        if ft in {'spc'}:
+            with pytest.raises(UnicodeDecodeError):
+                with open(experimental_filename, 'r') as fp:
+                    rc2.spectrum.from_stream(fp, filetype=ft)
+        else:
+            with open(experimental_filename, 'r') as fp:
+                rc2.spectrum.from_stream(fp, filetype=ft)