Repository: PickwickSoft/pystreamapi (language: python)

Issue:
CSV data loader
**Is your feature request related to a problem? Please describe.**
It is always complicated to load data from CSV files. I need much code for reading CSV.

**Describe the solution you'd like**
I'd like to use a loader directly in the `Stream.of(...)` method.


**Additional context**
Currently:
```python
data = []
with open('data.csv', 'r') as f:
    # Skip header
    next(f)
    for line in f:
        attr1, attr2 = line.split(',')
        data.append(Obj(attr1, attr2))

Stream.of(data)\
    ...
``` 

Solution:
```python
Stream.of(csv("data.csv")\
    ...
``` 

The signature of the CSV loader could look like that:
```python
def csv(file_path, delimiter=',')
``` 


Discussion / hints:

Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index f8fab56..a36eabb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "streams.py"
-version = "0.3.3"
+version = "1.0.0"
 authors = ["Stefan Garlonta <stefan@pickwicksoft.org>"]
 description = "A stream library for Python inspired by Java Stream API"
 keywords = ["streams", "parallel", "data"]
diff --git a/pystreamapi/__init__.py b/pystreamapi/__init__.py
index 3f434d9..8dbaf49 100644
--- a/pystreamapi/__init__.py
+++ b/pystreamapi/__init__.py
@@ -1,5 +1,5 @@
 from pystreamapi.__stream import Stream
 from pystreamapi._streams.error.__levels import ErrorLevel
 
-__version__ = "0.3.3"
+__version__ = "1.0.0"
 __all__ = ["Stream", "ErrorLevel"]
diff --git a/pystreamapi/loaders/__csv_loader.py b/pystreamapi/loaders/__csv_loader.py
new file mode 100644
index 0000000..9c9ef97
--- /dev/null
+++ b/pystreamapi/loaders/__csv_loader.py
@@ -0,0 +1,48 @@
+import contextlib
+import os
+from collections import namedtuple
+from csv import reader
+
+
+def csv(file_path: str, delimiter=',', encoding="utf-8") -> list:
+    """
+    Loads a CSV file and converts it into a list of namedtuples.
+
+    Returns:
+        list: A list of namedtuples, where each namedtuple represents a row in the CSV.
+        :param encoding: The encoding of the CSV file.
+        :param file_path: The path to the CSV file.
+        :param delimiter: The delimiter used in the CSV file.
+    """
+    file_path = __validate_path(file_path)
+    # skipcq: PTC-W6004
+    with open(file_path, 'r', newline='', encoding=encoding) as csvfile:
+        csvreader = reader(csvfile, delimiter=delimiter)
+
+        # Create a namedtuple type, casting the header values to int or float if possible
+        Row = namedtuple('Row', list(next(csvreader, [])))
+
+        # Process the data, casting values to int or float if possible
+        data = [Row(*[__try_cast(value) for value in row]) for row in csvreader]
+
+    return data
+
+
+def __validate_path(file_path: str):
+    """Validate a path string to prevent path traversal attacks"""
+    if not os.path.isabs(file_path):
+        raise ValueError("The file_path must be an absolute path.")
+
+    if not os.path.exists(file_path):
+        raise FileNotFoundError("The specified file does not exist.")
+
+    return file_path
+
+
+def __try_cast(value):
+    """Try to cast value to primary data types from python (int, float, bool)"""
+    for cast in (int, float):
+        with contextlib.suppress(ValueError):
+            return cast(value)
+    # Try to cast to bool
+    return value.lower() == 'true' if value.lower() in ('true', 'false') else value
diff --git a/pystreamapi/loaders/__init__.py b/pystreamapi/loaders/__init__.py
new file mode 100644
index 0000000..51f7ca8
--- /dev/null
+++ b/pystreamapi/loaders/__init__.py
@@ -0,0 +1,5 @@
+from pystreamapi.loaders.__csv_loader import csv
+
+__all__ = [
+    'csv'
+]


Test patch:
diff --git a/tests/assets/data.csv b/tests/assets/data.csv
new file mode 100644
index 0000000..5fd27af
--- /dev/null
+++ b/tests/assets/data.csv
@@ -0,0 +1,3 @@
+attr1,attr2
+1,2.0
+a,b
\ No newline at end of file
diff --git a/tests/assets/data2.csv b/tests/assets/data2.csv
new file mode 100644
index 0000000..6956ce9
--- /dev/null
+++ b/tests/assets/data2.csv
@@ -0,0 +1,2 @@
+attr1;attr2
+1;2
\ No newline at end of file
diff --git a/tests/assets/empty.csv b/tests/assets/empty.csv
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_loaders.py b/tests/test_loaders.py
new file mode 100644
index 0000000..161551a
--- /dev/null
+++ b/tests/test_loaders.py
@@ -0,0 +1,37 @@
+import os
+from unittest import TestCase
+from pystreamapi.loaders import csv
+
+class TestLoaders(TestCase):
+
+    def setUp(self) -> None:
+        cwd = os.path.dirname(os.path.realpath(__file__))
+        self.path = os.path.join(cwd, 'assets')
+
+    def test_csv_loader(self):
+        data = csv(f'{self.path}/data.csv')
+        self.assertEqual(len(data), 2)
+        self.assertEqual(data[0].attr1, 1)
+        self.assertIsInstance(data[0].attr1, int)
+        self.assertEqual(data[0].attr2, 2.0)
+        self.assertIsInstance(data[0].attr2, float)
+        self.assertEqual(data[1].attr1, 'a')
+        self.assertIsInstance(data[1].attr1, str)
+
+    def test_csv_loader_with_custom_delimiter(self):
+        data = csv(f'{self.path}/data2.csv', delimiter=';')
+        self.assertEqual(len(data), 1)
+        self.assertEqual(data[0].attr1, 1)
+        self.assertIsInstance(data[0].attr1, int)
+
+    def test_csv_loader_with_empty_file(self):
+        data = csv(f'{self.path}/empty.csv')
+        self.assertEqual(len(data), 0)
+
+    def test_csv_loader_with_invalid_path(self):
+        with self.assertRaises(FileNotFoundError):
+            csv(f'{self.path}/invalid.csv')
+
+    def test_csv_loader_with_non_absolute_path(self):
+        with self.assertRaises(ValueError):
+            csv('invalid.csv')