dsff 1.2.0__tar.gz → 1.2.2__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 (42) hide show
  1. {dsff-1.2.0/src/dsff.egg-info → dsff-1.2.2}/PKG-INFO +31 -4
  2. {dsff-1.2.0 → dsff-1.2.2}/README.md +26 -1
  3. dsff-1.2.2/docs/coverage.svg +1 -0
  4. {dsff-1.2.0 → dsff-1.2.2}/pyproject.toml +2 -3
  5. dsff-1.2.2/src/dsff/VERSION.txt +1 -0
  6. dsff-1.2.2/src/dsff/formats/__common__.py +56 -0
  7. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/formats/__init__.py +15 -0
  8. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/formats/arff.py +76 -61
  9. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/formats/csv.py +12 -1
  10. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/formats/dataset.py +16 -3
  11. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/formats/db.py +31 -1
  12. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/formats/pa.py +10 -3
  13. {dsff-1.2.0 → dsff-1.2.2/src/dsff.egg-info}/PKG-INFO +31 -4
  14. dsff-1.2.2/src/dsff.egg-info/requires.txt +7 -0
  15. {dsff-1.2.0 → dsff-1.2.2}/tests/test_dsff.py +29 -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.egg-info/requires.txt +0 -4
  20. {dsff-1.2.0 → dsff-1.2.2}/.coveragerc +0 -0
  21. {dsff-1.2.0 → dsff-1.2.2}/.github/workflows/python-package.yml +0 -0
  22. {dsff-1.2.0 → dsff-1.2.2}/.gitignore +0 -0
  23. {dsff-1.2.0 → dsff-1.2.2}/.readthedocs.yml +0 -0
  24. {dsff-1.2.0 → dsff-1.2.2}/LICENSE +0 -0
  25. {dsff-1.2.0 → dsff-1.2.2}/docs/mkdocs.yml +0 -0
  26. {dsff-1.2.0 → dsff-1.2.2}/docs/pages/imgs/dsff-from.png +0 -0
  27. {dsff-1.2.0 → dsff-1.2.2}/docs/pages/imgs/dsff-to.png +0 -0
  28. {dsff-1.2.0 → dsff-1.2.2}/docs/pages/imgs/icon.png +0 -0
  29. {dsff-1.2.0 → dsff-1.2.2}/docs/pages/imgs/logo.png +0 -0
  30. {dsff-1.2.0 → dsff-1.2.2}/docs/pages/index.md +0 -0
  31. {dsff-1.2.0 → dsff-1.2.2}/docs/pages/usage.md +0 -0
  32. {dsff-1.2.0 → dsff-1.2.2}/docs/requirements.txt +0 -0
  33. {dsff-1.2.0 → dsff-1.2.2}/pytest.ini +0 -0
  34. {dsff-1.2.0 → dsff-1.2.2}/requirements.txt +0 -0
  35. {dsff-1.2.0 → dsff-1.2.2}/setup.cfg +0 -0
  36. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/__info__.py +0 -0
  37. {dsff-1.2.0 → dsff-1.2.2}/src/dsff/__init__.py +0 -0
  38. {dsff-1.2.0 → dsff-1.2.2}/src/dsff.egg-info/SOURCES.txt +0 -0
  39. {dsff-1.2.0 → dsff-1.2.2}/src/dsff.egg-info/dependency_links.txt +0 -0
  40. {dsff-1.2.0 → dsff-1.2.2}/src/dsff.egg-info/top_level.txt +0 -0
  41. {dsff-1.2.0 → dsff-1.2.2}/tests/__init__.py +0 -0
  42. {dsff-1.2.0 → dsff-1.2.2}/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.2
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.2
@@ -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_"):
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,66 @@
2
2
  from .__common__ import *
3
3
 
4
4
 
5
- __all__ = ["from_arff", "to_arff"]
5
+ __all__ = ["from_arff", "is_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):
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.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).group(1).strip("'\"")
60
19
  continue
61
- 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)
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
+ n_cols = len(d[0])
38
+ continue
39
+ if l.startswith("@ATTRIBUTE "):
40
+ if not attributes[0]:
41
+ attributes[0] = True
42
+ if len(d) == 0:
43
+ # start the attributes block
44
+ d.append([])
45
+ if attributes[1]:
46
+ raise BadInputData(f"{pf}found @ATTRIBUTE out of the attributes block)")
47
+ try:
48
+ header = re.match(r"@ATTRIBUTE\s+([^\s]+)\s+[A-Z]+$", l).group(1)
49
+ if header == "class":
50
+ header = target
51
+ d[0].append(header)
52
+ continue
53
+ except AttributeError:
54
+ raise BadInputData(f"{pf}failed on @ATTRIBUTE (bad type)")
55
+ if not data:
56
+ if l == "@DATA":
57
+ data = True
58
+ continue
59
+ else:
60
+ raise BadInputData(f"{pf}did not find @DATA where expected")
61
+ row = list(map(lambda x: x.strip("'\""), re.split(r",\s+", l)))
62
+ if len(row) != n_cols:
63
+ raise BadInputData(f"{pf}this row does not match the number of columns")
64
+ d.append(row)
67
65
  for i in range(n_cols):
68
66
  values = []
69
67
  for j, row in enumerate(d):
@@ -76,9 +74,26 @@ def from_arff(dsff, path=None, target=TARGET_NAME, missing=MISSING_TOKEN):
76
74
  if "".join(sorted(set(values))) in ["0", "1", "01"]:
77
75
  for j, row in enumerate(d):
78
76
  if j > 0:
79
- row[i] = {'0': "False", '1': "True"}[row[i]]
80
- dsff.write(data=d)
81
- dsff.write(features=features)
77
+ row[i] = {'0': "False", '1': "True"}[row[i]]
78
+ return d, features, metadata, title
79
+
80
+
81
+ def from_arff(dsff, path=None, target=TARGET_NAME, missing=MISSING_TOKEN):
82
+ """ Populate the DSFF file from an ARFF file. """
83
+ with open(path) as f:
84
+ d, ft, md, t = _parse(f, target, missing)
85
+ dsff['title'] = t
86
+ dsff.write(data=d, features=ft, metadata=md)
87
+
88
+
89
+ @text_or_path
90
+ def is_arff(text):
91
+ """ Check if the input text or path is a valid ARFF. """
92
+ try:
93
+ _parse(ensure_str(text))
94
+ return True
95
+ except (BadInputData, UnicodeDecodeError):
96
+ return False
82
97
 
83
98
 
84
99
  def to_arff(dsff, path=None, target=TARGET_NAME, exclude=DEFAULT_EXCL, missing=MISSING_TOKEN, text=False):
@@ -2,7 +2,7 @@
2
2
  from .__common__ import *
3
3
 
4
4
 
5
- __all__ = ["from_csv", "to_csv"]
5
+ __all__ = ["from_csv", "is_csv", "to_csv"]
6
6
 
7
7
 
8
8
  def from_csv(dsff, path=None, exclude=DEFAULT_EXCL):
@@ -18,6 +18,17 @@ def from_csv(dsff, path=None, exclude=DEFAULT_EXCL):
18
18
  dsff.write(features=features)
19
19
 
20
20
 
21
+ @text_or_path
22
+ def is_csv(text):
23
+ """ Check if the input text or path is a valid CSV. """
24
+ try:
25
+ dialect = csvmod.Sniffer().sniff(text := ensure_str(text))
26
+ csvmod.Sniffer().has_header(text)
27
+ return True
28
+ except (csvmod.Error, UnicodeDecodeError):
29
+ return False
30
+
31
+
21
32
  def to_csv(dsff, path=None, text=False):
22
33
  """ Create a CSV from the data worksheet, saved as a file or output as a string. """
23
34
  with (StringIO() if text else open(path, 'w+')) as f:
@@ -2,19 +2,32 @@
2
2
  from .__common__ import *
3
3
 
4
4
 
5
- __all__ = ["from_dataset", "to_dataset"]
5
+ __all__ = ["from_dataset", "is_dataset", "to_dataset"]
6
6
 
7
7
 
8
- def from_dataset(dsff, path=None):
9
- """ Populate the DSFF file from a Dataset structure. """
8
+ def _parse(path):
10
9
  if not isdir(path):
11
10
  raise BadInputData("Not a folder")
12
11
  else:
13
12
  if len(missing := [f for f in ["data.csv", "features.json", "metadata.json"] if not isfile(join(path, f))]) > 0:
14
13
  raise BadInputData(f"Not a valid dataset folder (missing: {', '.join(missing)})")
14
+
15
+
16
+ def from_dataset(dsff, path=None):
17
+ """ Populate the DSFF file from a Dataset structure. """
18
+ _parse(path)
15
19
  dsff.write(path)
16
20
 
17
21
 
22
+ def is_dataset(path):
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
+
18
31
  def to_dataset(dsff, path=None):
19
32
  """ Create a dataset folder according to the following structure ;
20
33
  name
@@ -2,7 +2,7 @@
2
2
  from .__common__ import *
3
3
 
4
4
 
5
- __all__ = ["from_db", "to_db"]
5
+ __all__ = ["from_db", "is_db", "to_db"]
6
6
 
7
7
 
8
8
  def from_db(dsff, path=None, exclude=DEFAULT_EXCL):
@@ -30,6 +30,36 @@ def from_db(dsff, path=None, exclude=DEFAULT_EXCL):
30
30
  conn.close()
31
31
 
32
32
 
33
+ @text_or_path
34
+ def is_db(data):
35
+ """ Check if the input data or path is a valid SQL database. """
36
+ from sqlite3 import connect, Error
37
+ from sys import version_info
38
+ if not data.startswith(b"SQLite format 3\x00"):
39
+ return False
40
+ if version_info.minor > 10:
41
+ try:
42
+ with connect(":memory:") as c:
43
+ c.deserialize(data)
44
+ c.execute("PRAGMA schema_version;")
45
+ return True
46
+ except Error:
47
+ return False
48
+ else: # pragma: no cover
49
+ from os import close, write
50
+ from tempfile import mkstemp
51
+ fd, path = mkstemp(suffix=".db")
52
+ try:
53
+ write(fd, data)
54
+ close(fd)
55
+ fd = None
56
+ with connect(f"file:{path}?mode=ro&immutable=1", uri=True) as c:
57
+ c.execute("PRAGMA schema_version;").fetchone()
58
+ return True
59
+ except (Error, OSError):
60
+ return False
61
+
62
+
33
63
  def to_db(dsff, path=None, text=False, primary_index=0):
34
64
  """ Create a SQLDB from the data worksheet, saved as a file or output as a string. """
35
65
  from json import dumps
@@ -10,7 +10,7 @@ def _nowrite(m):
10
10
 
11
11
 
12
12
  for module in ["feather", "orc", "parquet"]:
13
- __all__ += [f"from_{module}", f"to_{module}"]
13
+ __all__ += [f"from_{module}", f"is_{module}", f"to_{module}"]
14
14
  def gen_func(m):
15
15
  def from_(dsff, path=None, exclude=DEFAULT_EXCL):
16
16
  dataset = globals()[m].read_table(path)
@@ -18,12 +18,19 @@ for module in ["feather", "orc", "parquet"]:
18
18
  metadata=literal_eval(dataset.schema.metadata.pop(b'__metadata__', b"{}").decode()),
19
19
  features={k.decode(): v.decode() for k, v in dataset.schema.metadata.items()})
20
20
  from_.__name__ = f"from_{m}"
21
+ def is_(data):
22
+ try:
23
+ globals()[m].read_table(pyarrow.BufferReader(data))
24
+ return True
25
+ except Exception:
26
+ return False
27
+ is_.__name__ = f"is_{m}"
21
28
  def to_(dsff, path=None, text=False):
22
29
  with (BytesIO() if text else open(path, 'wb+')) as f:
23
30
  getattr(globals()[m], "write_table", getattr(globals()[m], f"write_{m}", _nowrite))(dsff._to_table(), f)
24
31
  if text:
25
32
  return f.getvalue()
26
33
  to_.__name__ = f"to_{m}"
27
- return from_, to_
28
- globals()[f'from_{module}'], globals()[f'to_{module}'] = gen_func(module)
34
+ return from_, text_or_path(is_), to_
35
+ globals()[f'from_{module}'], globals()[f'is_{module}'], globals()[f'to_{module}'] = gen_func(module)
29
36
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsff
3
- Version: 1.2.0
3
+ Version: 1.2.2
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,10 @@ 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))
91
92
  with DSFF() as f:
92
93
  f.from_arff(TEST_BASENAME)
93
94
  # test for multiple error scenarios
@@ -123,25 +124,17 @@ class TestDsff(TestCase):
123
124
  d.insert(10, d.pop(6))
124
125
 
125
126
  def test_conversion_csv(self):
127
+ self.assertFalse(is_csv(b"PK\x03\x04\x14\x00\x00\x00\x08\x00P\xb3T\\F\xc7MH"))
126
128
  # DSFF to CSV
127
129
  create_test_dsff()
128
130
  with DSFF(TEST) as f:
129
131
  self.assertIsNotNone(f.to_csv(text=True))
130
132
  self.assertIsNone(f.to_csv())
133
+ self.assertTrue(is_csv(f"{TEST}.csv"))
131
134
  # CSV to DSFF
132
135
  with DSFF() as f:
133
136
  f.from_csv(TEST_BASENAME)
134
137
 
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
138
  def test_conversion_dataset(self):
146
139
  # DSFF to FilelessDataset
147
140
  create_test_dsff()
@@ -150,6 +143,7 @@ class TestDsff(TestCase):
150
143
  # FilelessDataset to DSFF
151
144
  with DSFF() as f:
152
145
  f.from_dataset(TEST_BASENAME)
146
+ self.assertTrue(is_dataset(f"{TEST_BASENAME}"))
153
147
  # FilelessDataset to DSFF (bad input dataset)
154
148
  os.remove(os.path.join(TEST_BASENAME, "metadata.json"))
155
149
  with DSFF() as f:
@@ -161,6 +155,22 @@ class TestDsff(TestCase):
161
155
  with DSFF() as f:
162
156
  self.assertRaises(BadInputData, f.from_dataset, TEST_BASENAME)
163
157
  os.remove(TEST_BASENAME)
158
+ os.makedirs(TEST_BASENAME)
159
+ self.assertFalse(is_dataset(TEST_BASENAME))
160
+ rmdir(TEST_BASENAME)
161
+
162
+ def test_conversion_db(self):
163
+ self.assertFalse(is_db(b"PK\x03\x04\x14\x00\x00\x00\x08\x00P\xb3T\\F\xc7MH"))
164
+ self.assertFalse(is_db(b"SQLite format 3\x00\x10\x00\x01\x01\x00@"))
165
+ # DSFF to SQL database
166
+ create_test_dsff()
167
+ with DSFF(TEST) as f:
168
+ self.assertIsNotNone(f.to_db(text=True))
169
+ self.assertIsNone(f.to_db())
170
+ self.assertTrue(is_db(f"{TEST_BASENAME}.db"))
171
+ # SQL database to DSFF
172
+ with DSFF() as f:
173
+ f.from_db(TEST_BASENAME)
164
174
 
165
175
  def test_conversion_pyarrow_formats(self):
166
176
  if PYARROW:
@@ -172,13 +182,16 @@ class TestDsff(TestCase):
172
182
  f.from_dataset(path=TEST_BASENAME)
173
183
  self.assertIsNone(getattr(f, f"to_{fmt}")(TEST_BASENAME))
174
184
  self.assertIsNotNone(getattr(f, f"to_{fmt}")(text=True))
185
+ is_ = globals()[f'is_{fmt}']
186
+ self.assertTrue(is_(f"{TEST_BASENAME}.{fmt}"))
187
+ self.assertFalse(is_(b"PK\x03\x04\x14\x00\x00\x00\x08\x00P\xb3T\\F\xc7MH"))
175
188
  with DSFF(INMEMORY) as f:
176
189
  self.assertIsNone(getattr(f, f"from_{fmt}")(TEST_BASENAME))
177
190
  f.to_dataset(path=TEST_BASENAME)
178
191
  os.remove(f"{TEST_BASENAME}.{fmt}")
179
192
  rmdir(TEST_BASENAME)
180
193
  # test without the 'label' column
181
- with open(TEST_BASENAME + ".arff", 'w') as f:
194
+ with open(f"{TEST_BASENAME}.arff", 'w') as f:
182
195
  f.write(TEST_ARFF.replace("class", "test").replace("?", "-1"))
183
196
  with DSFF() as f:
184
197
  f.from_arff(TEST_BASENAME)
@@ -190,10 +203,13 @@ class TestDsff(TestCase):
190
203
  self.assertIsNone(f.to_dataset())
191
204
  f.name = TEST
192
205
  self.assertIsNotNone(f.headers)
206
+ self.assertTrue(DSFF.is_valid_format(TEST_BASENAME))
193
207
  with DSFF(INMEMORY) as f:
194
208
  self.assertRaises(EmptyDsffFile, f.to_arff)
195
209
  self.assertIsNone(f.from_dataset(TEST_BASENAME))
196
210
  self.assertIsNone(f.to_arff())
211
+ self.assertRaises(ValueError, DSFF.is_valid_format, 0)
212
+ self.assertTrue(DSFF.is_valid_format(f"undefined.arff"))
197
213
  os.remove(f"undefined.arff")
198
214
  rmdir(TEST_BASENAME)
199
215
  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,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