dsff 1.2.0__tar.gz → 1.2.3__tar.gz

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.
Files changed (45) hide show
  1. {dsff-1.2.0/src/dsff.egg-info → dsff-1.2.3}/PKG-INFO +31 -4
  2. {dsff-1.2.0 → dsff-1.2.3}/README.md +26 -1
  3. dsff-1.2.3/docs/coverage.svg +1 -0
  4. {dsff-1.2.0 → dsff-1.2.3}/pyproject.toml +2 -3
  5. dsff-1.2.3/src/dsff/VERSION.txt +1 -0
  6. dsff-1.2.3/src/dsff/formats/__common__.py +56 -0
  7. {dsff-1.2.0 → dsff-1.2.3}/src/dsff/formats/__init__.py +15 -0
  8. {dsff-1.2.0 → dsff-1.2.3}/src/dsff/formats/arff.py +84 -61
  9. dsff-1.2.3/src/dsff/formats/csv.py +41 -0
  10. dsff-1.2.3/src/dsff/formats/dataset.py +67 -0
  11. {dsff-1.2.0 → dsff-1.2.3}/src/dsff/formats/db.py +44 -7
  12. dsff-1.2.3/src/dsff/formats/pa.py +46 -0
  13. {dsff-1.2.0 → dsff-1.2.3/src/dsff.egg-info}/PKG-INFO +31 -4
  14. dsff-1.2.3/src/dsff.egg-info/requires.txt +7 -0
  15. {dsff-1.2.0 → dsff-1.2.3}/tests/test_dsff.py +35 -13
  16. dsff-1.2.0/docs/coverage.svg +0 -1
  17. dsff-1.2.0/src/dsff/VERSION.txt +0 -1
  18. dsff-1.2.0/src/dsff/formats/__common__.py +0 -33
  19. dsff-1.2.0/src/dsff/formats/csv.py +0 -29
  20. dsff-1.2.0/src/dsff/formats/dataset.py +0 -40
  21. dsff-1.2.0/src/dsff/formats/pa.py +0 -29
  22. dsff-1.2.0/src/dsff.egg-info/requires.txt +0 -4
  23. {dsff-1.2.0 → dsff-1.2.3}/.coveragerc +0 -0
  24. {dsff-1.2.0 → dsff-1.2.3}/.github/workflows/python-package.yml +0 -0
  25. {dsff-1.2.0 → dsff-1.2.3}/.gitignore +0 -0
  26. {dsff-1.2.0 → dsff-1.2.3}/.readthedocs.yml +0 -0
  27. {dsff-1.2.0 → dsff-1.2.3}/LICENSE +0 -0
  28. {dsff-1.2.0 → dsff-1.2.3}/docs/mkdocs.yml +0 -0
  29. {dsff-1.2.0 → dsff-1.2.3}/docs/pages/imgs/dsff-from.png +0 -0
  30. {dsff-1.2.0 → dsff-1.2.3}/docs/pages/imgs/dsff-to.png +0 -0
  31. {dsff-1.2.0 → dsff-1.2.3}/docs/pages/imgs/icon.png +0 -0
  32. {dsff-1.2.0 → dsff-1.2.3}/docs/pages/imgs/logo.png +0 -0
  33. {dsff-1.2.0 → dsff-1.2.3}/docs/pages/index.md +0 -0
  34. {dsff-1.2.0 → dsff-1.2.3}/docs/pages/usage.md +0 -0
  35. {dsff-1.2.0 → dsff-1.2.3}/docs/requirements.txt +0 -0
  36. {dsff-1.2.0 → dsff-1.2.3}/pytest.ini +0 -0
  37. {dsff-1.2.0 → dsff-1.2.3}/requirements.txt +0 -0
  38. {dsff-1.2.0 → dsff-1.2.3}/setup.cfg +0 -0
  39. {dsff-1.2.0 → dsff-1.2.3}/src/dsff/__info__.py +0 -0
  40. {dsff-1.2.0 → dsff-1.2.3}/src/dsff/__init__.py +0 -0
  41. {dsff-1.2.0 → dsff-1.2.3}/src/dsff.egg-info/SOURCES.txt +0 -0
  42. {dsff-1.2.0 → dsff-1.2.3}/src/dsff.egg-info/dependency_links.txt +0 -0
  43. {dsff-1.2.0 → dsff-1.2.3}/src/dsff.egg-info/top_level.txt +0 -0
  44. {dsff-1.2.0 → dsff-1.2.3}/tests/__init__.py +0 -0
  45. {dsff-1.2.0 → dsff-1.2.3}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsff
3
- Version: 1.2.0
3
+ Version: 1.2.3
4
4
  Summary: DataSet File Format (DSFF)
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -696,8 +696,10 @@ Requires-Python: <4,>=3.10
696
696
  Description-Content-Type: text/markdown
697
697
  License-File: LICENSE
698
698
  Requires-Dist: openpyxl
699
- Provides-Extra: extra
700
- Requires-Dist: pyarrow; extra == "extra"
699
+ Provides-Extra: all
700
+ Requires-Dist: dsff[arrow]; extra == "all"
701
+ Provides-Extra: arrow
702
+ Requires-Dist: pyarrow; extra == "arrow"
701
703
  Dynamic: license-file
702
704
 
703
705
  <p align="center" id="top"><img src="https://github.com/packing-box/python-dsff/raw/main/docs/pages/imgs/logo.png"></p>
@@ -738,9 +740,34 @@ pip install --user dsff
738
740
  ```python
739
741
  >>> import dsff
740
742
  >>> with dsff.DSFF("/path/to/my-dataset.dsff") as f:
741
- f.to_dataset() # creates ./[dsff-title] with data.csv, features.json and metadata.json
743
+ f.to_dataset() # creates ./my-dataset with data.csv, features.json and metadata.json
742
744
  ```
743
745
 
746
+ **
747
+
748
+ ## Extensions
749
+
750
+ **Install all available extensions**
751
+
752
+ ```sh
753
+ pip install --user dsff[all]
754
+ ```
755
+
756
+ **Dealing with [Apache Arrow](https://arrow.apache.org/) formats**
757
+
758
+ ```sh
759
+ pip install --user dsff[arrow]
760
+ ```
761
+
762
+ ```python
763
+ >>> import dsff
764
+ >>> with dsff.DSFF("/path/to/my-dataset.dsff") as f:
765
+ f.to_feather() # creates ./my-dataset.feather
766
+ f.to_orc() # creates ./my-dataset.orc
767
+ f.to_parquet() # creates ./my-dataset.parquet
768
+ ```
769
+
770
+
744
771
  ## Related Projects
745
772
 
746
773
  You may also like these:
@@ -36,9 +36,34 @@ pip install --user dsff
36
36
  ```python
37
37
  >>> import dsff
38
38
  >>> with dsff.DSFF("/path/to/my-dataset.dsff") as f:
39
- f.to_dataset() # creates ./[dsff-title] with data.csv, features.json and metadata.json
39
+ f.to_dataset() # creates ./my-dataset with data.csv, features.json and metadata.json
40
40
  ```
41
41
 
42
+ **
43
+
44
+ ## Extensions
45
+
46
+ **Install all available extensions**
47
+
48
+ ```sh
49
+ pip install --user dsff[all]
50
+ ```
51
+
52
+ **Dealing with [Apache Arrow](https://arrow.apache.org/) formats**
53
+
54
+ ```sh
55
+ pip install --user dsff[arrow]
56
+ ```
57
+
58
+ ```python
59
+ >>> import dsff
60
+ >>> with dsff.DSFF("/path/to/my-dataset.dsff") as f:
61
+ f.to_feather() # creates ./my-dataset.feather
62
+ f.to_orc() # creates ./my-dataset.orc
63
+ f.to_parquet() # creates ./my-dataset.parquet
64
+ ```
65
+
66
+
42
67
  ## Related Projects
43
68
 
44
69
  You may also like these:
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 93.24%"><title>coverage: 93.24%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#4c1"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">93.24%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">93.24%</text></g></svg>
@@ -34,9 +34,8 @@ dependencies = [
34
34
  dynamic = ["version"]
35
35
 
36
36
  [project.optional-dependencies]
37
- extra = [
38
- "pyarrow",
39
- ]
37
+ all = ["dsff[arrow]"]
38
+ arrow = ["pyarrow"]
40
39
 
41
40
  [project.readme]
42
41
  file = "README.md"
@@ -0,0 +1 @@
1
+ 1.2.3
@@ -0,0 +1,56 @@
1
+ # -*- coding: UTF-8 -*-
2
+ import csv as csvmod
3
+ import json
4
+ import re
5
+ from ast import literal_eval
6
+ from io import BytesIO, StringIO
7
+ from os import makedirs, remove
8
+ from os.path import basename, exists, expanduser, isfile, isdir, join, splitext
9
+ try: # pragma: no cover
10
+ import pyarrow
11
+ import pyarrow.feather as feather
12
+ import pyarrow.orc as orc
13
+ import pyarrow.parquet as parquet
14
+ import pandas
15
+ PYARROW = True
16
+ except ImportError: # pragma: no cover
17
+ PYARROW = False
18
+
19
+
20
+ __all__ = ["basename", "csvmod", "ensure_str", "exists", "expanduser", "isfile", "isdir", "join", "json",
21
+ "literal_eval", "makedirs", "re", "remove", "splitext", "text_or_path", "BytesIO", "StringIO",
22
+ "CSV_DELIMITER", "DEFAULT_EXCL", "INMEMORY", "META_EXCL", "MISSING_TOKEN", "PYARROW", "TARGET_NAME"]
23
+ if PYARROW:
24
+ __all__ += ["feather", "orc", "pandas", "parquet", "pyarrow"]
25
+
26
+
27
+ CSV_DELIMITER = ";"
28
+ DEFAULT_EXCL = ("hash", "realpath", "format", "size", "ctime", "mtime") # origin: executables used in the Packing Box
29
+ INMEMORY = "<memory>"
30
+ META_EXCL = ["created", "modified", "revision"]
31
+ MISSING_TOKEN = "?"
32
+ TARGET_NAME = "label"
33
+
34
+
35
+ def ensure_str(string):
36
+ try:
37
+ return string.decode()
38
+ except AttributeError:
39
+ return str(string)
40
+
41
+
42
+ def text_or_path(f):
43
+ def _wrapper(txt_path, *args, **kwargs):
44
+ if not isinstance(txt_path, bytes):
45
+ if isinstance(txt_path, str):
46
+ if not re.search(r"[<>:\'\"/\\|?*\x00-\x1f]", txt_path, re.I) and exists(txt_path):
47
+ if not isdir(txt_path):
48
+ with open(txt_path, "rb") as fh:
49
+ txt_path = fh.read()
50
+ else:
51
+ txt_path = txt_path.encode()
52
+ else:
53
+ raise ValueError(f"Bad input text or path ({type(txt_path)})")
54
+ return f(txt_path, *args, **kwargs)
55
+ return _wrapper
56
+
@@ -22,6 +22,9 @@ if PYARROW: # pragma: no cover
22
22
 
23
23
 
24
24
  __all__ = ["DSFF"]
25
+ for k in list(globals().keys()):
26
+ if k.startswith("is_") or k.startswith("load_"):
27
+ __all__.append(k)
25
28
 
26
29
  _FORMAT_TEXT_ALIAS = {'arff': "ARFF", 'csv': "CSV", 'db': "SQL", 'orc': "ORC"}
27
30
  _NO_EXT_FORMAT = ["dataset"]
@@ -343,4 +346,16 @@ class DSFF:
343
346
  if p != INMEMORY and not p.endswith(".dsff"):
344
347
  p += ".dsff"
345
348
  return p
349
+
350
+ @staticmethod
351
+ @text_or_path
352
+ def format(text):
353
+ for k, func in globals().items():
354
+ if k.startswith("is_") and func(text):
355
+ return k.split("_", 1)[1]
356
+
357
+ @staticmethod
358
+ @text_or_path
359
+ def is_valid_format(text):
360
+ return DSFF.format(text) is not None
346
361
 
@@ -2,68 +2,67 @@
2
2
  from .__common__ import *
3
3
 
4
4
 
5
- __all__ = ["from_arff", "to_arff"]
5
+ __all__ = ["from_arff", "is_arff", "load_arff", "to_arff"]
6
6
 
7
7
 
8
- def from_arff(dsff, path=None, target=TARGET_NAME, missing=MISSING_TOKEN):
9
- """ Populate the DSFF file from an ARFF file. """
10
- d, features = [], {}
11
- with open(path) as f:
12
- relation, attributes, data = False, [False, False], False
13
- for n, l in enumerate(f, 1):
14
- l, pf = l.strip(), f"Line {n}: "
15
- # the file shall start with "@RELATION"
16
- if not relation:
17
- if l.startswith("@RELATION "):
18
- relation = True
19
- try:
20
- dsff['title'] = re.match(r"@RELATION\s+('[^']*'|\"[^\"]*\")$", l).group(1).strip("'\"")
21
- continue
22
- except Exception as e:
23
- raise BadInputData(f"{pf}failed on @RELATION ({e})")
24
- else:
25
- raise BadInputData(f"{pf}did not find @RELATION")
26
- # get metadata and feature descriptions from comments
27
- if l.startswith("%"):
28
- if re.match(r"^\%\s+metadata\s*\:\s*\{.*\}$", l):
29
- dsff.write(metadata=literal_eval(l.split(":", 1)[1]))
30
- elif (m := re.match(r"^\%\s+(.*?)\s*\:\s*(.*?)$", l)):
31
- name, descr = m.groups()
32
- features[name] = descr
33
- continue
34
- # then ignore blank lines
35
- if l == "":
36
- if attributes[0] and not attributes[1]:
37
- # close the atributes block
38
- attributes[1] = True
39
- n_cols = len(d[0])
40
- continue
41
- if l.startswith("@ATTRIBUTE "):
42
- if not attributes[0]:
43
- attributes[0] = True
44
- if len(d) == 0:
45
- # start the attributes block
46
- d.append([])
47
- if attributes[1]:
48
- raise BadInputData(f"{pf}found @ATTRIBUTE out of the attributes block)")
8
+ def _parse(text_or_fh, target=TARGET_NAME, missing=MISSING_TOKEN, **kw):
9
+ d, features, metadata, title = [], {}, {}, ""
10
+ relation, attributes, data = False, [False, False], False
11
+ for n, l in enumerate(t.splitlines() if isinstance(t := text_or_fh, str) else t):
12
+ l, pf = l.strip(), f"Line {n}: "
13
+ # the file shall start with "@RELATION"
14
+ if not relation:
15
+ if l.upper().startswith("@RELATION "):
16
+ relation = True
49
17
  try:
50
- header = re.match(r"@ATTRIBUTE\s+([^\s]+)\s+[A-Z]+$", l).group(1)
51
- if header == "class":
52
- header = target
53
- d[0].append(header)
54
- continue
55
- except AttributeError:
56
- raise BadInputData(f"{pf}failed on @ATTRIBUTE (bad type)")
57
- if not data:
58
- if l == "@DATA":
59
- data = True
18
+ title = re.match(r"@RELATION\s+('[^']*'|\"[^\"]*\")$", l, re.I).group(1).strip("'\"")
60
19
  continue
20
+ except Exception as e:
21
+ raise BadInputData(f"{pf}failed on @RELATION ({e})")
22
+ else:
23
+ raise BadInputData(f"{pf}did not find @RELATION")
24
+ # get metadata and feature descriptions from comments
25
+ if l.startswith("%"):
26
+ if re.match(r"^\%\s+metadata\s*\:\s*\{.*\}$", l):
27
+ metadata = literal_eval(l.split(":", 1)[1])
28
+ elif (m := re.match(r"^\%\s+(.*?)\s*\:\s*(.*?)$", l)):
29
+ name, descr = m.groups()
30
+ features[name] = descr
31
+ continue
32
+ # then ignore blank lines
33
+ if l == "":
34
+ if attributes[0] and not attributes[1]:
35
+ # close the atributes block
36
+ attributes[1] = True
37
+ continue
38
+ if l.upper().startswith("@ATTRIBUTE "):
39
+ if not attributes[0]:
40
+ attributes[0] = True
41
+ if len(d) == 0:
42
+ # start the attributes block
43
+ d.append([])
44
+ if attributes[1]:
45
+ raise BadInputData(f"{pf}found @ATTRIBUTE out of the attributes block)")
46
+ try:
47
+ header = re.match(r"@ATTRIBUTE\s+([^\s]+)\s+(?:[a-zA-Z]+|\{.*?\})$", l, re.I).group(1).strip("'\"")
48
+ if header == "class":
49
+ header = target
61
50
  else:
62
- raise BadInputData(f"{pf}did not find @DATA where expected")
63
- row = list(map(lambda x: x.strip("'\""), re.split(r",\s+", l)))
64
- if len(row) != n_cols:
65
- raise BadInputData(f"{pf}this row does not match the number of columns")
66
- d.append(row)
51
+ features.setdefault(header, "")
52
+ d[0].append(header)
53
+ continue
54
+ except AttributeError:
55
+ raise BadInputData(f"{pf}failed on @ATTRIBUTE (bad type)")
56
+ if not data:
57
+ if l.upper() == "@DATA":
58
+ data = True
59
+ n_cols = len(d[0])
60
+ continue
61
+ else:
62
+ raise BadInputData(f"{pf}did not find @DATA where expected")
63
+ if len(row := list(map(lambda x: x.strip("'\""), re.split(r",\s*", l)))) != n_cols:
64
+ raise BadInputData(f"{pf}this row does not match the number of columns")
65
+ d.append(row)
67
66
  for i in range(n_cols):
68
67
  values = []
69
68
  for j, row in enumerate(d):
@@ -76,12 +75,36 @@ def from_arff(dsff, path=None, target=TARGET_NAME, missing=MISSING_TOKEN):
76
75
  if "".join(sorted(set(values))) in ["0", "1", "01"]:
77
76
  for j, row in enumerate(d):
78
77
  if j > 0:
79
- row[i] = {'0': "False", '1': "True"}[row[i]]
80
- dsff.write(data=d)
81
- dsff.write(features=features)
78
+ row[i] = {'0': "False", '1': "True"}[row[i]]
79
+ return d, features, metadata, title
80
+
81
+
82
+ def from_arff(dsff, path=None, target=TARGET_NAME, missing=MISSING_TOKEN, **kw):
83
+ """ Populate the DSFF file from an ARFF file. """
84
+ with open(path) as f:
85
+ d, ft, md, t = _parse(f, target, missing)
86
+ dsff['title'] = t
87
+ dsff.write(data=d, features=ft, metadata=md)
88
+
89
+
90
+ @text_or_path
91
+ def is_arff(text, target=TARGET_NAME, missing=MISSING_TOKEN, **kw):
92
+ """ Check if the input text or path is a valid ARFF. """
93
+ try:
94
+ _parse(ensure_str(text))
95
+ return True
96
+ except (BadInputData, UnicodeDecodeError):
97
+ return False
98
+
99
+
100
+ def load_arff(path, target=TARGET_NAME, missing=MISSING_TOKEN, **kw):
101
+ """ Load an ARFF file as a dictionary with data, features and metadata. """
102
+ with open(path) as f:
103
+ d, ft, md, _ = _parse(f, target, missing)
104
+ return {'data': d, 'features': ft, 'metadata': md}
82
105
 
83
106
 
84
- def to_arff(dsff, path=None, target=TARGET_NAME, exclude=DEFAULT_EXCL, missing=MISSING_TOKEN, text=False):
107
+ def to_arff(dsff, path=None, target=TARGET_NAME, exclude=DEFAULT_EXCL, missing=MISSING_TOKEN, text=False, **kw):
85
108
  """ Output the dataset in ARFF format, suitable for use with the Weka framework, saved as a file or output as a
86
109
  string. """
87
110
  name = splitext(basename(path))[0]
@@ -0,0 +1,41 @@
1
+ # -*- coding: UTF-8 -*-
2
+ from .__common__ import *
3
+
4
+
5
+ __all__ = ["from_csv", "is_csv", "load_csv", "to_csv"]
6
+
7
+
8
+ def from_csv(dsff, path=None, exclude=DEFAULT_EXCL, **kw):
9
+ """ Populate the DSFF file from a CSV file. """
10
+ dsff.write(**load_csv(path))
11
+
12
+
13
+ @text_or_path
14
+ def is_csv(text, **kw):
15
+ """ Check if the input text or path is a valid CSV. """
16
+ try:
17
+ dialect = csvmod.Sniffer().sniff(text := ensure_str(text))
18
+ csvmod.Sniffer().has_header(text)
19
+ return True
20
+ except (csvmod.Error, UnicodeDecodeError):
21
+ return False
22
+
23
+
24
+ def load_csv(path, exclude=DEFAULT_EXCL, **kw):
25
+ """ Load a CSV file as a dictionary with data, features and metadata. """
26
+ data = {'metadata': {}}
27
+ with open(expanduser(path)) as f:
28
+ data['data'] = [r for r in csvmod.reader(f, delimiter=CSV_DELIMITER)]
29
+ data['features'] = {h: "" for h in data['data'][0] if h not in exclude}
30
+ return data
31
+
32
+
33
+ def to_csv(dsff, path=None, text=False, **kw):
34
+ """ Create a CSV from the data worksheet, saved as a file or output as a string. """
35
+ with (StringIO() if text else open(path, 'w+')) as f:
36
+ writer = csvmod.writer(f, delimiter=";")
37
+ for row in dsff.data:
38
+ writer.writerow(row)
39
+ if text:
40
+ return f.getvalue()
41
+
@@ -0,0 +1,67 @@
1
+ # -*- coding: UTF-8 -*-
2
+ from .__common__ import *
3
+ from .csv import load_csv
4
+
5
+
6
+ __all__ = ["from_dataset", "is_dataset", "load_dataset", "to_dataset"]
7
+
8
+
9
+ def _parse(path):
10
+ if not isdir(expanduser(path)):
11
+ raise BadInputData("Not a folder")
12
+ if len(missing := [f for f in ["data.csv", "features.json", "metadata.json"] if not isfile(join(path, f))]) > 0:
13
+ raise BadInputData(f"Not a valid dataset folder (missing: {', '.join(missing)})")
14
+
15
+
16
+ def from_dataset(dsff, path=None, **kw):
17
+ """ Populate the DSFF file from a Dataset structure. """
18
+ _parse(path)
19
+ dsff.write(path)
20
+
21
+
22
+ def is_dataset(path, **kw):
23
+ """ Check if the input path is a valid Dataset. """
24
+ try:
25
+ _parse(path)
26
+ return True
27
+ except BadInputData:
28
+ return False
29
+
30
+
31
+ def load_dataset(path, **kw):
32
+ """ Load a dataset folder as a dictionary with data, features and metadata. """
33
+ if not isdir(d := expanduser(str(path))):
34
+ raise BadInputData("Not a folder")
35
+ dp, fp, mp = join(d, "data.csv"), join(d, "features.json"), join(d, "metadata.json")
36
+ data = {}
37
+ data['data'] = load_csv(dp)['data']
38
+ with open(fp) as f:
39
+ data['features'] = json.load(f)
40
+ with open(mp) as f:
41
+ data['metadata'] = json.load(f)
42
+ return data
43
+
44
+
45
+ def to_dataset(dsff, path=None, **kw):
46
+ """ Create a dataset folder according to the following structure ;
47
+ name
48
+ +-- data.csv
49
+ +-- features.json
50
+ +-- metadata.json
51
+ """
52
+ makedirs(path, exist_ok=True)
53
+ # handle data first
54
+ dsff.logger.debug("> making data.csv...")
55
+ with open(join(path, "data.csv"), 'w+') as f:
56
+ writer = csvmod.writer(f, delimiter=";")
57
+ for cells in dsff['data'].rows:
58
+ writer.writerow([dsff._DSFF__eval(c.value) for c in cells])
59
+ # then handle features dictionary
60
+ dsff.logger.debug("> making features.json...")
61
+ with open(join(path, "features.json"), 'w+') as f:
62
+ json.dump(dsff.features, f, indent=2)
63
+ # finally handle metadata dictionary
64
+ dsff.logger.debug("> making metadata.json...")
65
+ with open(join(path, "metadata.json"), 'w+') as f:
66
+ json.dump(dsff.metadata, f, indent=2)
67
+
@@ -2,15 +2,51 @@
2
2
  from .__common__ import *
3
3
 
4
4
 
5
- __all__ = ["from_db", "to_db"]
5
+ __all__ = ["from_db", "is_db", "load_db", "to_db"]
6
6
 
7
7
 
8
- def from_db(dsff, path=None, exclude=DEFAULT_EXCL):
8
+ def from_db(dsff, path=None, **kw):
9
9
  """ Populate the DSFF file from a SQLDB file. """
10
+ dsff.write(**load_db(path))
11
+
12
+
13
+ @text_or_path
14
+ def is_db(data, **kw):
15
+ """ Check if the input data or path is a valid SQL database. """
16
+ from sqlite3 import connect, Error
17
+ from sys import version_info
18
+ if not data.startswith(b"SQLite format 3\x00"):
19
+ return False
20
+ if version_info.minor > 10:
21
+ try:
22
+ with connect(":memory:") as c:
23
+ c.deserialize(data)
24
+ c.execute("PRAGMA schema_version;")
25
+ return True
26
+ except Error:
27
+ return False
28
+ else: # pragma: no cover
29
+ from os import close, write
30
+ from tempfile import mkstemp
31
+ fd, path = mkstemp(suffix=".db")
32
+ try:
33
+ write(fd, data)
34
+ close(fd)
35
+ fd = None
36
+ with connect(f"file:{path}?mode=ro&immutable=1", uri=True) as c:
37
+ c.execute("PRAGMA schema_version;").fetchone()
38
+ return True
39
+ except (Error, OSError):
40
+ return False
41
+
42
+
43
+ def load_db(path, **kw):
44
+ """ Load a SQLDB file as a dictionary with data, features and metadata. """
10
45
  from json import loads
46
+ from os.path import basename, splitext
11
47
  from sqlite3 import connect
12
48
  conn = connect(path)
13
- cursor = conn.cursor()
49
+ cursor, data = conn.cursor(), {}
14
50
  # list tables
15
51
  cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
16
52
  tables = [table[0] for table in cursor.fetchall()]
@@ -20,17 +56,18 @@ def from_db(dsff, path=None, exclude=DEFAULT_EXCL):
20
56
  cursor.execute("PRAGMA table_info('data')")
21
57
  headers = [[col[1] for col in cursor.fetchall()]]
22
58
  cursor.execute("SELECT * FROM data;")
23
- dsff.write(headers + [r for r in cursor.fetchall()])
59
+ data['data'] = headers + [r for r in cursor.fetchall()]
24
60
  # import feature definitions
25
61
  cursor.execute("SELECT name,description FROM features;")
26
- dsff.write(features={r[0]: r[1] for r in cursor.fetchall()})
62
+ data['features'] = {r[0]: r[1] for r in cursor.fetchall()}
27
63
  # import metadata
28
64
  cursor.execute("SELECT key,value FROM metadata;")
29
- dsff.write(metadata={r[0]: loads(r[1]) if isinstance(r[1], str) else r[1] for r in cursor.fetchall()})
65
+ data['metadata'] = {r[0]: loads(r[1]) if isinstance(r[1], str) else r[1] for r in cursor.fetchall()}
30
66
  conn.close()
67
+ return data
31
68
 
32
69
 
33
- def to_db(dsff, path=None, text=False, primary_index=0):
70
+ def to_db(dsff, path=None, text=False, primary_index=0, **kw):
34
71
  """ Create a SQLDB from the data worksheet, saved as a file or output as a string. """
35
72
  from json import dumps
36
73
  from sqlite3 import connect
@@ -0,0 +1,46 @@
1
+ # -*- coding: UTF-8 -*-
2
+ from .__common__ import *
3
+
4
+
5
+ __all__ = []
6
+
7
+
8
+ def _nowrite(m):
9
+ raise NotImplementedError(f"none of {m}.write_table and {m}.write_{m} is implemented")
10
+
11
+
12
+ def _parse(ds):
13
+ return {
14
+ 'data': [ds.schema.names] + [list(r.values()) for r in ds.to_pylist()],
15
+ 'features': {k.decode(): v.decode() for k, v in ds.schema.metadata.items()},
16
+ 'metadata': literal_eval(ds.schema.metadata.pop(b'__metadata__', b"{}").decode()),
17
+ }
18
+
19
+
20
+ for module in ["feather", "orc", "parquet"]:
21
+ __all__ += [f"from_{module}", f"is_{module}", f"load_{module}", f"to_{module}"]
22
+ def gen_func(m):
23
+ def from_(dsff, path=None, **kw):
24
+ from os.path import basename, splitext
25
+ dsff.write(**_parse(globals()[m].read_table(path)))
26
+ from_.__name__ = f"from_{m}"
27
+ def is_(data, **kw):
28
+ try:
29
+ globals()[m].read_table(pyarrow.BufferReader(data))
30
+ return True
31
+ except Exception:
32
+ return False
33
+ is_.__name__ = f"is_{m}"
34
+ def load_(path, **kw):
35
+ return _parse(globals()[m].read_table(path))
36
+ load_.__name__ = f"load_{m}"
37
+ def to_(dsff, path=None, text=False, **kw):
38
+ with (BytesIO() if text else open(path, 'wb+')) as f:
39
+ getattr(globals()[m], "write_table", getattr(globals()[m], f"write_{m}", _nowrite))(dsff._to_table(), f)
40
+ if text:
41
+ return f.getvalue()
42
+ to_.__name__ = f"to_{m}"
43
+ return from_, text_or_path(is_), load_, to_
44
+ globals()[f'from_{module}'], globals()[f'is_{module}'], globals()[f'load_{module}'], globals()[f'to_{module}'] = \
45
+ gen_func(module)
46
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsff
3
- Version: 1.2.0
3
+ Version: 1.2.3
4
4
  Summary: DataSet File Format (DSFF)
5
5
  Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -696,8 +696,10 @@ Requires-Python: <4,>=3.10
696
696
  Description-Content-Type: text/markdown
697
697
  License-File: LICENSE
698
698
  Requires-Dist: openpyxl
699
- Provides-Extra: extra
700
- Requires-Dist: pyarrow; extra == "extra"
699
+ Provides-Extra: all
700
+ Requires-Dist: dsff[arrow]; extra == "all"
701
+ Provides-Extra: arrow
702
+ Requires-Dist: pyarrow; extra == "arrow"
701
703
  Dynamic: license-file
702
704
 
703
705
  <p align="center" id="top"><img src="https://github.com/packing-box/python-dsff/raw/main/docs/pages/imgs/logo.png"></p>
@@ -738,9 +740,34 @@ pip install --user dsff
738
740
  ```python
739
741
  >>> import dsff
740
742
  >>> with dsff.DSFF("/path/to/my-dataset.dsff") as f:
741
- f.to_dataset() # creates ./[dsff-title] with data.csv, features.json and metadata.json
743
+ f.to_dataset() # creates ./my-dataset with data.csv, features.json and metadata.json
742
744
  ```
743
745
 
746
+ **
747
+
748
+ ## Extensions
749
+
750
+ **Install all available extensions**
751
+
752
+ ```sh
753
+ pip install --user dsff[all]
754
+ ```
755
+
756
+ **Dealing with [Apache Arrow](https://arrow.apache.org/) formats**
757
+
758
+ ```sh
759
+ pip install --user dsff[arrow]
760
+ ```
761
+
762
+ ```python
763
+ >>> import dsff
764
+ >>> with dsff.DSFF("/path/to/my-dataset.dsff") as f:
765
+ f.to_feather() # creates ./my-dataset.feather
766
+ f.to_orc() # creates ./my-dataset.orc
767
+ f.to_parquet() # creates ./my-dataset.parquet
768
+ ```
769
+
770
+
744
771
  ## Related Projects
745
772
 
746
773
  You may also like these:
@@ -0,0 +1,7 @@
1
+ openpyxl
2
+
3
+ [all]
4
+ dsff[arrow]
5
+
6
+ [arrow]
7
+ pyarrow
@@ -76,6 +76,7 @@ class TestDsff(TestCase):
76
76
  TEST_METADATA[k] = v
77
77
 
78
78
  def test_conversion_arff(self):
79
+ self.assertTrue(is_arff(TEST_ARFF))
79
80
  # DSFF to ARFF
80
81
  create_test_dsff()
81
82
  with DSFF(TEST) as f:
@@ -84,10 +85,11 @@ class TestDsff(TestCase):
84
85
  with DSFF(mode='w+') as f:
85
86
  self.assertRaises(ValueError, f.to_arff, ())
86
87
  # ARFF to DSFF
87
- arff = TEST_BASENAME + ".arff"
88
88
  # use a test ARFF with a comment line
89
- with open(arff, 'w') as f:
89
+ with open(arff := f"{TEST_BASENAME}.arff", 'w') as f:
90
90
  f.write(TEST_ARFF)
91
+ self.assertTrue(is_arff(arff))
92
+ self.assertIsInstance(load_arff(arff), dict)
91
93
  with DSFF() as f:
92
94
  f.from_arff(TEST_BASENAME)
93
95
  # test for multiple error scenarios
@@ -123,25 +125,18 @@ class TestDsff(TestCase):
123
125
  d.insert(10, d.pop(6))
124
126
 
125
127
  def test_conversion_csv(self):
128
+ self.assertFalse(is_csv(b"PK\x03\x04\x14\x00\x00\x00\x08\x00P\xb3T\\F\xc7MH"))
126
129
  # DSFF to CSV
127
130
  create_test_dsff()
128
131
  with DSFF(TEST) as f:
129
132
  self.assertIsNotNone(f.to_csv(text=True))
130
133
  self.assertIsNone(f.to_csv())
134
+ self.assertTrue(is_csv(csv := f"{TEST_BASENAME}.csv"))
135
+ self.assertIsInstance(load_csv(csv), dict)
131
136
  # CSV to DSFF
132
137
  with DSFF() as f:
133
138
  f.from_csv(TEST_BASENAME)
134
139
 
135
- def test_conversion_db(self):
136
- # DSFF to SQL database
137
- create_test_dsff()
138
- with DSFF(TEST) as f:
139
- self.assertIsNotNone(f.to_db(text=True))
140
- self.assertIsNone(f.to_db())
141
- # SQL database to DSFF
142
- with DSFF() as f:
143
- f.from_db(TEST_BASENAME)
144
-
145
140
  def test_conversion_dataset(self):
146
141
  # DSFF to FilelessDataset
147
142
  create_test_dsff()
@@ -150,6 +145,9 @@ class TestDsff(TestCase):
150
145
  # FilelessDataset to DSFF
151
146
  with DSFF() as f:
152
147
  f.from_dataset(TEST_BASENAME)
148
+ self.assertTrue(is_dataset(TEST_BASENAME))
149
+ self.assertRaises(BadInputData, load_dataset, 0)
150
+ self.assertIsInstance(load_dataset(TEST_BASENAME), dict)
153
151
  # FilelessDataset to DSFF (bad input dataset)
154
152
  os.remove(os.path.join(TEST_BASENAME, "metadata.json"))
155
153
  with DSFF() as f:
@@ -161,6 +159,23 @@ class TestDsff(TestCase):
161
159
  with DSFF() as f:
162
160
  self.assertRaises(BadInputData, f.from_dataset, TEST_BASENAME)
163
161
  os.remove(TEST_BASENAME)
162
+ os.makedirs(TEST_BASENAME)
163
+ self.assertFalse(is_dataset(TEST_BASENAME))
164
+ rmdir(TEST_BASENAME)
165
+
166
+ def test_conversion_db(self):
167
+ self.assertFalse(is_db(b"PK\x03\x04\x14\x00\x00\x00\x08\x00P\xb3T\\F\xc7MH"))
168
+ self.assertFalse(is_db(b"SQLite format 3\x00\x10\x00\x01\x01\x00@"))
169
+ # DSFF to SQL database
170
+ create_test_dsff()
171
+ with DSFF(TEST) as f:
172
+ self.assertIsNotNone(f.to_db(text=True))
173
+ self.assertIsNone(f.to_db())
174
+ self.assertTrue(is_db(db := f"{TEST_BASENAME}.db"))
175
+ self.assertIsInstance(load_db(db), dict)
176
+ # SQL database to DSFF
177
+ with DSFF() as f:
178
+ f.from_db(TEST_BASENAME)
164
179
 
165
180
  def test_conversion_pyarrow_formats(self):
166
181
  if PYARROW:
@@ -172,13 +187,17 @@ class TestDsff(TestCase):
172
187
  f.from_dataset(path=TEST_BASENAME)
173
188
  self.assertIsNone(getattr(f, f"to_{fmt}")(TEST_BASENAME))
174
189
  self.assertIsNotNone(getattr(f, f"to_{fmt}")(text=True))
190
+ is_ = globals()[f'is_{fmt}']
191
+ self.assertTrue(is_(fn := f"{TEST_BASENAME}.{fmt}"))
192
+ self.assertFalse(is_(b"PK\x03\x04\x14\x00\x00\x00\x08\x00P\xb3T\\F\xc7MH"))
193
+ self.assertIsInstance(globals()[f'load_{fmt}'](fn), dict)
175
194
  with DSFF(INMEMORY) as f:
176
195
  self.assertIsNone(getattr(f, f"from_{fmt}")(TEST_BASENAME))
177
196
  f.to_dataset(path=TEST_BASENAME)
178
197
  os.remove(f"{TEST_BASENAME}.{fmt}")
179
198
  rmdir(TEST_BASENAME)
180
199
  # test without the 'label' column
181
- with open(TEST_BASENAME + ".arff", 'w') as f:
200
+ with open(f"{TEST_BASENAME}.arff", 'w') as f:
182
201
  f.write(TEST_ARFF.replace("class", "test").replace("?", "-1"))
183
202
  with DSFF() as f:
184
203
  f.from_arff(TEST_BASENAME)
@@ -190,10 +209,13 @@ class TestDsff(TestCase):
190
209
  self.assertIsNone(f.to_dataset())
191
210
  f.name = TEST
192
211
  self.assertIsNotNone(f.headers)
212
+ self.assertTrue(DSFF.is_valid_format(TEST_BASENAME))
193
213
  with DSFF(INMEMORY) as f:
194
214
  self.assertRaises(EmptyDsffFile, f.to_arff)
195
215
  self.assertIsNone(f.from_dataset(TEST_BASENAME))
196
216
  self.assertIsNone(f.to_arff())
217
+ self.assertRaises(ValueError, DSFF.is_valid_format, 0)
218
+ self.assertTrue(DSFF.is_valid_format(f"undefined.arff"))
197
219
  os.remove(f"undefined.arff")
198
220
  rmdir(TEST_BASENAME)
199
221
  with DSFF(INMEMORY) as f:
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 89.26%"><title>coverage: 89.26%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#97ca00"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">89.26%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">89.26%</text></g></svg>
@@ -1 +0,0 @@
1
- 1.2.0
@@ -1,33 +0,0 @@
1
- # -*- coding: UTF-8 -*-
2
- import csv as csvmod
3
- import json
4
- import re
5
- from ast import literal_eval
6
- from io import BytesIO, StringIO
7
- from os import makedirs, remove
8
- from os.path import basename, expanduser, isfile, isdir, join, splitext
9
- try: # pragma: no cover
10
- import pyarrow
11
- import pyarrow.feather as feather
12
- import pyarrow.orc as orc
13
- import pyarrow.parquet as parquet
14
- import pandas
15
- PYARROW = True
16
- except ImportError: # pragma: no cover
17
- PYARROW = False
18
-
19
-
20
- __all__ = ["basename", "csvmod", "expanduser", "isfile", "isdir", "join", "json", "literal_eval", "makedirs",
21
- "remove", "splitext", "re", "BytesIO", "StringIO",
22
- "CSV_DELIMITER", "DEFAULT_EXCL", "INMEMORY", "META_EXCL", "MISSING_TOKEN", "PYARROW", "TARGET_NAME"]
23
- if PYARROW:
24
- __all__ += ["feather", "orc", "pandas", "pyarrow", "parquet"]
25
-
26
-
27
- CSV_DELIMITER = ";"
28
- DEFAULT_EXCL = ("hash", "realpath", "format", "size", "ctime", "mtime") # origin: executables used in the Packing Box
29
- INMEMORY = "<memory>"
30
- META_EXCL = ["created", "modified", "revision"]
31
- MISSING_TOKEN = "?"
32
- TARGET_NAME = "label"
33
-
@@ -1,29 +0,0 @@
1
- # -*- coding: UTF-8 -*-
2
- from .__common__ import *
3
-
4
-
5
- __all__ = ["from_csv", "to_csv"]
6
-
7
-
8
- def from_csv(dsff, path=None, exclude=DEFAULT_EXCL):
9
- """ Populate the DSFF file from a CSV file. """
10
- dsff.write(path)
11
- features = {}
12
- for headers in dsff['data'].rows:
13
- for header in headers:
14
- if header.value in exclude:
15
- continue
16
- features[header.value] = ""
17
- break
18
- dsff.write(features=features)
19
-
20
-
21
- def to_csv(dsff, path=None, text=False):
22
- """ Create a CSV from the data worksheet, saved as a file or output as a string. """
23
- with (StringIO() if text else open(path, 'w+')) as f:
24
- writer = csvmod.writer(f, delimiter=";")
25
- for row in dsff.data:
26
- writer.writerow(row)
27
- if text:
28
- return f.getvalue()
29
-
@@ -1,40 +0,0 @@
1
- # -*- coding: UTF-8 -*-
2
- from .__common__ import *
3
-
4
-
5
- __all__ = ["from_dataset", "to_dataset"]
6
-
7
-
8
- def from_dataset(dsff, path=None):
9
- """ Populate the DSFF file from a Dataset structure. """
10
- if not isdir(path):
11
- raise BadInputData("Not a folder")
12
- else:
13
- if len(missing := [f for f in ["data.csv", "features.json", "metadata.json"] if not isfile(join(path, f))]) > 0:
14
- raise BadInputData(f"Not a valid dataset folder (missing: {', '.join(missing)})")
15
- dsff.write(path)
16
-
17
-
18
- def to_dataset(dsff, path=None):
19
- """ Create a dataset folder according to the following structure ;
20
- name
21
- +-- data.csv
22
- +-- features.json
23
- +-- metadata.json
24
- """
25
- makedirs(path, exist_ok=True)
26
- # handle data first
27
- dsff.logger.debug("> making data.csv...")
28
- with open(join(path, "data.csv"), 'w+') as f:
29
- writer = csvmod.writer(f, delimiter=";")
30
- for cells in dsff['data'].rows:
31
- writer.writerow([dsff._DSFF__eval(c.value) for c in cells])
32
- # then handle features dictionary
33
- dsff.logger.debug("> making features.json...")
34
- with open(join(path, "features.json"), 'w+') as f:
35
- json.dump(dsff.features, f, indent=2)
36
- # finally handle metadata dictionary
37
- dsff.logger.debug("> making metadata.json...")
38
- with open(join(path, "metadata.json"), 'w+') as f:
39
- json.dump(dsff.metadata, f, indent=2)
40
-
@@ -1,29 +0,0 @@
1
- # -*- coding: UTF-8 -*-
2
- from .__common__ import *
3
-
4
-
5
- __all__ = []
6
-
7
-
8
- def _nowrite(m):
9
- raise NotImplementedError(f"none of {m}.write_table and {m}.write_{m} is implemented")
10
-
11
-
12
- for module in ["feather", "orc", "parquet"]:
13
- __all__ += [f"from_{module}", f"to_{module}"]
14
- def gen_func(m):
15
- def from_(dsff, path=None, exclude=DEFAULT_EXCL):
16
- dataset = globals()[m].read_table(path)
17
- dsff.write(data=[dataset.schema.names] + [list(r.values()) for r in dataset.to_pylist()],
18
- metadata=literal_eval(dataset.schema.metadata.pop(b'__metadata__', b"{}").decode()),
19
- features={k.decode(): v.decode() for k, v in dataset.schema.metadata.items()})
20
- from_.__name__ = f"from_{m}"
21
- def to_(dsff, path=None, text=False):
22
- with (BytesIO() if text else open(path, 'wb+')) as f:
23
- getattr(globals()[m], "write_table", getattr(globals()[m], f"write_{m}", _nowrite))(dsff._to_table(), f)
24
- if text:
25
- return f.getvalue()
26
- to_.__name__ = f"to_{m}"
27
- return from_, to_
28
- globals()[f'from_{module}'], globals()[f'to_{module}'] = gen_func(module)
29
-
@@ -1,4 +0,0 @@
1
- openpyxl
2
-
3
- [extra]
4
- pyarrow
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes